@jclvsh/dropspace 2.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.
Files changed (40) hide show
  1. package/README.md +273 -0
  2. package/dist/cli-utils.d.ts +4 -0
  3. package/dist/cli-utils.js +31 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +31 -0
  6. package/dist/client.d.ts +11 -0
  7. package/dist/client.js +110 -0
  8. package/dist/commands/connections.d.ts +3 -0
  9. package/dist/commands/connections.js +23 -0
  10. package/dist/commands/dropspace.d.ts +3 -0
  11. package/dist/commands/dropspace.js +15 -0
  12. package/dist/commands/keys.d.ts +3 -0
  13. package/dist/commands/keys.js +70 -0
  14. package/dist/commands/launches.d.ts +3 -0
  15. package/dist/commands/launches.js +263 -0
  16. package/dist/commands/personas.d.ts +3 -0
  17. package/dist/commands/personas.js +116 -0
  18. package/dist/commands/usage.d.ts +3 -0
  19. package/dist/commands/usage.js +15 -0
  20. package/dist/commands/webhooks.d.ts +3 -0
  21. package/dist/commands/webhooks.js +108 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +25 -0
  24. package/dist/tools/connections.d.ts +3 -0
  25. package/dist/tools/connections.js +30 -0
  26. package/dist/tools/dropspace.d.ts +3 -0
  27. package/dist/tools/dropspace.js +21 -0
  28. package/dist/tools/keys.d.ts +3 -0
  29. package/dist/tools/keys.js +107 -0
  30. package/dist/tools/launches.d.ts +3 -0
  31. package/dist/tools/launches.js +492 -0
  32. package/dist/tools/personas.d.ts +3 -0
  33. package/dist/tools/personas.js +152 -0
  34. package/dist/tools/usage.d.ts +3 -0
  35. package/dist/tools/usage.js +21 -0
  36. package/dist/tools/webhooks.d.ts +3 -0
  37. package/dist/tools/webhooks.js +183 -0
  38. package/dist/types.d.ts +257 -0
  39. package/dist/types.js +1 -0
  40. package/package.json +33 -0
@@ -0,0 +1,152 @@
1
+ import { z } from "zod";
2
+ export function registerPersonaTools(server, client) {
3
+ server.tool("list_personas", "list your personas with pagination", {
4
+ page: z.number().int().positive().optional().describe("page number (default: 1)"),
5
+ page_size: z.number().int().min(1).max(100).optional().describe("items per page (default: 50, max: 100)"),
6
+ }, async ({ page, page_size }) => {
7
+ try {
8
+ const params = {};
9
+ if (page !== undefined)
10
+ params.page = String(page);
11
+ if (page_size !== undefined)
12
+ params.page_size = String(page_size);
13
+ const result = await client.get("/personas", params);
14
+ return {
15
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
16
+ };
17
+ }
18
+ catch (error) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text",
23
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
24
+ },
25
+ ],
26
+ isError: true,
27
+ };
28
+ }
29
+ });
30
+ server.tool("create_persona", "create a new persona for AI content generation tone/style", {
31
+ name: z.string().min(1).max(100).describe("persona name (1-100 characters)"),
32
+ }, async ({ name }) => {
33
+ try {
34
+ const result = await client.post("/personas", {
35
+ name,
36
+ });
37
+ return {
38
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
39
+ };
40
+ }
41
+ catch (error) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
47
+ },
48
+ ],
49
+ isError: true,
50
+ };
51
+ }
52
+ });
53
+ server.tool("get_persona", "get a persona by ID with analysis details and writing samples", {
54
+ id: z.string().uuid().describe("persona ID"),
55
+ }, async ({ id }) => {
56
+ try {
57
+ const result = await client.get(`/personas/${id}`);
58
+ return {
59
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
60
+ };
61
+ }
62
+ catch (error) {
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
68
+ },
69
+ ],
70
+ isError: true,
71
+ };
72
+ }
73
+ });
74
+ server.tool("update_persona", "update a persona's name or writing samples", {
75
+ id: z.string().uuid().describe("persona ID"),
76
+ name: z.string().min(1).max(100).optional().describe("new persona name"),
77
+ custom_samples: z.array(z.unknown()).max(50).optional().describe("custom writing samples (max 50)"),
78
+ twitter_samples: z.array(z.unknown()).max(50).optional().describe("twitter writing samples (max 50)"),
79
+ reddit_samples: z.array(z.unknown()).max(50).optional().describe("reddit writing samples (max 50)"),
80
+ facebook_samples: z.array(z.unknown()).max(50).optional().describe("facebook writing samples (max 50)"),
81
+ instagram_samples: z.array(z.unknown()).max(50).optional().describe("instagram writing samples (max 50)"),
82
+ tiktok_samples: z.array(z.unknown()).max(50).optional().describe("tiktok writing samples (max 50)"),
83
+ linkedin_samples: z.array(z.unknown()).max(50).optional().describe("linkedin writing samples (max 50)"),
84
+ }, async ({ id, ...body }) => {
85
+ try {
86
+ const result = await client.patch(`/personas/${id}`, body);
87
+ return {
88
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
89
+ };
90
+ }
91
+ catch (error) {
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text",
96
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
97
+ },
98
+ ],
99
+ isError: true,
100
+ };
101
+ }
102
+ });
103
+ server.tool("delete_persona", "delete a persona", {
104
+ id: z.string().uuid().describe("persona ID"),
105
+ }, async ({ id }) => {
106
+ try {
107
+ await client.delete(`/personas/${id}`);
108
+ return {
109
+ content: [{ type: "text", text: "persona deleted successfully" }],
110
+ };
111
+ }
112
+ catch (error) {
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
118
+ },
119
+ ],
120
+ isError: true,
121
+ };
122
+ }
123
+ });
124
+ server.tool("analyze_persona", "trigger AI analysis of a persona's writing style (queued async)", {
125
+ id: z.string().uuid().describe("persona ID"),
126
+ platforms: z.array(z.string()).optional().describe("specific platforms to analyze samples from"),
127
+ include_custom_samples: z.boolean().optional().describe("include custom writing samples in analysis"),
128
+ }, async ({ id, platforms, include_custom_samples }) => {
129
+ try {
130
+ const body = {};
131
+ if (platforms)
132
+ body.platforms = platforms;
133
+ if (include_custom_samples !== undefined)
134
+ body.include_custom_samples = include_custom_samples;
135
+ const result = await client.post(`/personas/${id}/analyze`, body);
136
+ return {
137
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
138
+ };
139
+ }
140
+ catch (error) {
141
+ return {
142
+ content: [
143
+ {
144
+ type: "text",
145
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
146
+ },
147
+ ],
148
+ isError: true,
149
+ };
150
+ }
151
+ });
152
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { DropspaceClient } from "../client.js";
3
+ export declare function registerUsageTools(server: McpServer, client: DropspaceClient): void;
@@ -0,0 +1,21 @@
1
+ export function registerUsageTools(server, client) {
2
+ server.tool("get_usage", "get your current plan, usage limits, and billing period (requires read scope)", {}, async () => {
3
+ try {
4
+ const result = await client.get("/usage");
5
+ return {
6
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
7
+ };
8
+ }
9
+ catch (error) {
10
+ return {
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
15
+ },
16
+ ],
17
+ isError: true,
18
+ };
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { DropspaceClient } from "../client.js";
3
+ export declare function registerWebhookTools(server: McpServer, client: DropspaceClient): void;
@@ -0,0 +1,183 @@
1
+ import { z } from "zod";
2
+ const WEBHOOK_EVENTS = [
3
+ "launch.completed",
4
+ "launch.failed",
5
+ "launch.partial",
6
+ "media.ready",
7
+ "persona.analyzed",
8
+ "post.deleted",
9
+ ];
10
+ export function registerWebhookTools(server, client) {
11
+ server.tool("list_webhooks", "list your webhooks (requires admin scope)", {}, async () => {
12
+ try {
13
+ const result = await client.get("/webhooks");
14
+ return {
15
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
16
+ };
17
+ }
18
+ catch (error) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text",
23
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
24
+ },
25
+ ],
26
+ isError: true,
27
+ };
28
+ }
29
+ });
30
+ server.tool("create_webhook", "create a webhook endpoint — the secret is returned once, store it securely (requires admin scope)", {
31
+ url: z
32
+ .string()
33
+ .url()
34
+ .refine((u) => u.startsWith("https://"), {
35
+ message: "webhook url must use https",
36
+ })
37
+ .describe("HTTPS webhook URL"),
38
+ events: z
39
+ .array(z.enum(WEBHOOK_EVENTS))
40
+ .min(1)
41
+ .describe("events to subscribe to: launch.completed, launch.failed, launch.partial, media.ready, persona.analyzed, post.deleted"),
42
+ }, async (args) => {
43
+ try {
44
+ const result = await client.post("/webhooks", args);
45
+ return {
46
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
47
+ };
48
+ }
49
+ catch (error) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
55
+ },
56
+ ],
57
+ isError: true,
58
+ };
59
+ }
60
+ });
61
+ server.tool("get_webhook", "get a webhook by ID (requires admin scope)", {
62
+ id: z.string().uuid().describe("webhook ID"),
63
+ }, async ({ id }) => {
64
+ try {
65
+ const result = await client.get(`/webhooks/${id}`);
66
+ return {
67
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
68
+ };
69
+ }
70
+ catch (error) {
71
+ return {
72
+ content: [
73
+ {
74
+ type: "text",
75
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
76
+ },
77
+ ],
78
+ isError: true,
79
+ };
80
+ }
81
+ });
82
+ server.tool("update_webhook", "update a webhook's URL, events, or active status (requires admin scope)", {
83
+ id: z.string().uuid().describe("webhook ID"),
84
+ url: z
85
+ .string()
86
+ .url()
87
+ .refine((u) => u.startsWith("https://"), {
88
+ message: "webhook url must use https",
89
+ })
90
+ .optional()
91
+ .describe("new HTTPS webhook URL"),
92
+ events: z.array(z.enum(WEBHOOK_EVENTS)).min(1).optional().describe("new events to subscribe to"),
93
+ active: z.boolean().optional().describe("enable or disable the webhook"),
94
+ }, async ({ id, ...body }) => {
95
+ try {
96
+ const result = await client.patch(`/webhooks/${id}`, body);
97
+ return {
98
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
99
+ };
100
+ }
101
+ catch (error) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
107
+ },
108
+ ],
109
+ isError: true,
110
+ };
111
+ }
112
+ });
113
+ server.tool("delete_webhook", "delete a webhook (requires admin scope)", {
114
+ id: z.string().uuid().describe("webhook ID"),
115
+ }, async ({ id }) => {
116
+ try {
117
+ await client.delete(`/webhooks/${id}`);
118
+ return {
119
+ content: [{ type: "text", text: "webhook deleted successfully" }],
120
+ };
121
+ }
122
+ catch (error) {
123
+ return {
124
+ content: [
125
+ {
126
+ type: "text",
127
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
128
+ },
129
+ ],
130
+ isError: true,
131
+ };
132
+ }
133
+ });
134
+ server.tool("rotate_webhook_secret", "rotate a webhook's signing secret — the new secret is returned once, store it securely (requires admin scope)", {
135
+ id: z.string().uuid().describe("webhook ID"),
136
+ }, async ({ id }) => {
137
+ try {
138
+ const result = await client.post(`/webhooks/${id}/rotate-secret`);
139
+ return {
140
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
141
+ };
142
+ }
143
+ catch (error) {
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
149
+ },
150
+ ],
151
+ isError: true,
152
+ };
153
+ }
154
+ });
155
+ server.tool("list_webhook_deliveries", "list delivery attempts for a webhook with pagination (requires admin scope)", {
156
+ id: z.string().uuid().describe("webhook ID"),
157
+ page: z.number().int().positive().optional().describe("page number (default: 1)"),
158
+ page_size: z.number().int().min(1).max(100).optional().describe("items per page (default: 50, max: 100)"),
159
+ }, async ({ id, page, page_size }) => {
160
+ try {
161
+ const params = {};
162
+ if (page !== undefined)
163
+ params.page = String(page);
164
+ if (page_size !== undefined)
165
+ params.page_size = String(page_size);
166
+ const result = await client.get(`/webhooks/${id}/deliveries`, params);
167
+ return {
168
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
169
+ };
170
+ }
171
+ catch (error) {
172
+ return {
173
+ content: [
174
+ {
175
+ type: "text",
176
+ text: `error: ${error instanceof Error ? error.message : String(error)}`,
177
+ },
178
+ ],
179
+ isError: true,
180
+ };
181
+ }
182
+ });
183
+ }
@@ -0,0 +1,257 @@
1
+ /** Paginated API response wrapper */
2
+ export interface PaginatedResponse<T> {
3
+ data: T[];
4
+ pagination: {
5
+ page: number;
6
+ page_size: number;
7
+ total: number;
8
+ total_pages: number;
9
+ };
10
+ }
11
+ /** Single-item API response wrapper */
12
+ export interface DataResponse<T> {
13
+ data: T;
14
+ }
15
+ /** API error response */
16
+ export interface ApiErrorResponse {
17
+ error: {
18
+ code: string;
19
+ message: string;
20
+ field?: string;
21
+ };
22
+ }
23
+ /** Launch resource */
24
+ export interface Launch {
25
+ id: string;
26
+ name: string;
27
+ status: string;
28
+ platforms: string[];
29
+ scheduled_date: string | null;
30
+ product_url: string | null;
31
+ product_description: string | null;
32
+ persona_id: string | null;
33
+ media_mode: string | null;
34
+ dropspace_platforms: string[] | null;
35
+ user_platform_accounts: Record<string, string> | null;
36
+ platform_contents: Record<string, unknown> | null;
37
+ media_assets: unknown[] | null;
38
+ posting_status?: Record<string, PostingStatus>;
39
+ error_message: string | null;
40
+ started_at: string | null;
41
+ completed_at: string | null;
42
+ created_at: string;
43
+ updated_at: string;
44
+ }
45
+ /** Per-platform posting status */
46
+ export interface PostingStatus {
47
+ status: string;
48
+ account_type?: string;
49
+ post_url?: string;
50
+ error_message?: string;
51
+ posted_at?: string;
52
+ }
53
+ /** Launch status detail */
54
+ export interface LaunchStatusDetail {
55
+ launch_id: string;
56
+ launch_status: string;
57
+ posting_logs: PostingLog[];
58
+ }
59
+ /** Posting log entry */
60
+ export interface PostingLog {
61
+ id: string;
62
+ platform: string;
63
+ status: string;
64
+ post_url: string | null;
65
+ post_id: string | null;
66
+ error_message: string | null;
67
+ error_code: string | null;
68
+ attempt_count: number;
69
+ posted_at: string | null;
70
+ created_at: string;
71
+ }
72
+ /** Launch analytics */
73
+ export interface LaunchAnalytics {
74
+ launch_id: string;
75
+ launch_name: string;
76
+ launch_status: string;
77
+ summary: {
78
+ total: number;
79
+ successful: number;
80
+ failed: number;
81
+ pending: number;
82
+ };
83
+ fetched_at: string | null;
84
+ next_refresh_at: string | null;
85
+ platforms: Array<{
86
+ platform: string;
87
+ status: string;
88
+ post_url: string | null;
89
+ post_id: string | null;
90
+ posted_at: string | null;
91
+ cache_status?: string;
92
+ is_deleted?: boolean;
93
+ deleted_detected_at?: string | null;
94
+ deletion_reason?: string | null;
95
+ metrics: Record<string, unknown> | null;
96
+ }>;
97
+ }
98
+ /** Batch launch analytics item (no fetched_at/next_refresh_at — those are single-launch only) */
99
+ export interface BatchLaunchAnalyticsItem {
100
+ launch_id: string;
101
+ launch_name: string;
102
+ launch_status: string;
103
+ summary: {
104
+ total: number;
105
+ successful: number;
106
+ failed: number;
107
+ pending: number;
108
+ };
109
+ platforms: Array<{
110
+ platform: string;
111
+ status: string;
112
+ post_url: string | null;
113
+ post_id: string | null;
114
+ posted_at: string | null;
115
+ cache_status?: string;
116
+ is_deleted?: boolean;
117
+ deleted_detected_at?: string | null;
118
+ deletion_reason?: string | null;
119
+ metrics: Record<string, unknown> | null;
120
+ }>;
121
+ }
122
+ /** Batch launch analytics response */
123
+ export interface BatchLaunchAnalyticsResponse {
124
+ data: BatchLaunchAnalyticsItem[];
125
+ errors: Array<{
126
+ id: string;
127
+ error: {
128
+ code: string;
129
+ message: string;
130
+ };
131
+ }>;
132
+ }
133
+ /** Persona resource */
134
+ export interface Persona {
135
+ id: string;
136
+ name: string;
137
+ persona_analysis: unknown | null;
138
+ persona_analysis_structured?: unknown | null;
139
+ build_status: string;
140
+ build_progress: number;
141
+ build_started_at: string | null;
142
+ build_error: string | null;
143
+ last_analyzed_at: string | null;
144
+ custom_samples?: unknown[];
145
+ twitter_samples?: unknown[];
146
+ reddit_samples?: unknown[];
147
+ facebook_samples?: unknown[];
148
+ instagram_samples?: unknown[];
149
+ tiktok_samples?: unknown[];
150
+ linkedin_samples?: unknown[];
151
+ created_at: string;
152
+ updated_at: string;
153
+ }
154
+ /** Connection (OAuth token) */
155
+ export interface Connection {
156
+ id: string;
157
+ platform: string;
158
+ entity_id: string | null;
159
+ account_info: unknown | null;
160
+ account_type: string | null;
161
+ is_active: boolean;
162
+ expires_at: string | null;
163
+ created_at: string;
164
+ updated_at: string;
165
+ }
166
+ /** API key resource */
167
+ export interface ApiKey {
168
+ id: string;
169
+ name: string;
170
+ key_prefix: string;
171
+ scopes: string[];
172
+ last_used_at: string | null;
173
+ revoked_at: string | null;
174
+ created_at: string;
175
+ }
176
+ /** Response when creating an API key (includes raw key, shown once) */
177
+ export interface CreateApiKeyResponse {
178
+ key: string;
179
+ api_key: ApiKey;
180
+ }
181
+ /** Webhook resource */
182
+ export interface Webhook {
183
+ id: string;
184
+ url: string;
185
+ events: string[];
186
+ secret?: string;
187
+ active: boolean;
188
+ created_at: string;
189
+ updated_at: string;
190
+ }
191
+ /** Response when rotating a webhook secret */
192
+ export interface RotateSecretResponse {
193
+ id: string;
194
+ secret: string;
195
+ }
196
+ /** Webhook delivery log entry */
197
+ export interface WebhookDelivery {
198
+ id: string;
199
+ webhook_id: string;
200
+ event_type: string;
201
+ status: string;
202
+ attempts: number;
203
+ response_status: number | null;
204
+ response_body: string | null;
205
+ delivered_at: string | null;
206
+ created_at: string;
207
+ }
208
+ /** Dropspace platform status entry */
209
+ export interface DropspacePlatformStatus {
210
+ platform: string;
211
+ connected: boolean;
212
+ account_name?: string;
213
+ }
214
+ /** Dropspace account status */
215
+ export interface DropspaceStatus {
216
+ platforms: DropspacePlatformStatus[];
217
+ connected_platforms: string[];
218
+ timestamp: string;
219
+ }
220
+ /** Usage limit entry (with used/remaining) */
221
+ export interface UsageLimitEntry {
222
+ limit: number | "unlimited";
223
+ used: number;
224
+ remaining: number | "unlimited";
225
+ }
226
+ /** Usage limit entry (limit only, no tracked usage) */
227
+ export interface UsageLimitOnly {
228
+ limit: number | "unlimited";
229
+ }
230
+ /** Usage and plan data */
231
+ export interface UsageData {
232
+ plan: string;
233
+ billing_period: {
234
+ start: string;
235
+ end: string;
236
+ };
237
+ limits: {
238
+ launches_per_month: UsageLimitEntry;
239
+ ai_images_per_month: UsageLimitEntry;
240
+ ai_videos_per_month: UsageLimitEntry;
241
+ personas: UsageLimitEntry;
242
+ analyses_per_persona: UsageLimitOnly;
243
+ regenerations_per_launch: UsageLimitOnly;
244
+ };
245
+ features: {
246
+ can_connect_own_accounts: boolean;
247
+ can_post_to_official_accounts: boolean;
248
+ allowed_platforms: string[] | null;
249
+ };
250
+ }
251
+ /** Retry content generation response */
252
+ export interface RetryContentResponse {
253
+ retried: string[];
254
+ succeeded: string[];
255
+ still_failing: string[];
256
+ rate_limited: string[];
257
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@jclvsh/dropspace",
3
+ "version": "2.0.0",
4
+ "description": "MCP server and CLI for the Dropspace API",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "dropspace-mcp": "dist/index.js",
10
+ "dropspace": "dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "start": "node dist/index.js"
19
+ },
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.11.0",
22
+ "commander": "^14.0.3",
23
+ "zod": "^3.24.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^22.0.0",
27
+ "typescript": "^5.7.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "license": "MIT"
33
+ }