@tday-com/mcp 0.0.0-stage → 0.2.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 +36 -0
- package/README.md +105 -2
- package/dist/api-client.d.ts +23 -0
- package/dist/api-client.js +187 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +78 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +66 -0
- package/dist/demo-import.d.ts +6 -0
- package/dist/demo-import.js +101 -0
- package/dist/download-safety.d.ts +7 -0
- package/dist/download-safety.js +76 -0
- package/dist/installer-agents.d.ts +17 -0
- package/dist/installer-agents.js +283 -0
- package/dist/installer-callback.d.ts +13 -0
- package/dist/installer-callback.js +91 -0
- package/dist/installer-slash-command.d.ts +12 -0
- package/dist/installer-slash-command.js +262 -0
- package/dist/installer.d.ts +2 -0
- package/dist/installer.js +127 -0
- package/dist/observability.d.ts +1 -0
- package/dist/observability.js +77 -0
- package/dist/package-info.d.ts +11 -0
- package/dist/package-info.js +17 -0
- package/dist/result-format.d.ts +4 -0
- package/dist/result-format.js +154 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +37 -0
- package/dist/tool-schemas.d.ts +460 -0
- package/dist/tool-schemas.js +189 -0
- package/dist/tools-account.d.ts +6 -0
- package/dist/tools-account.js +34 -0
- package/dist/tools-demo.d.ts +11 -0
- package/dist/tools-demo.js +57 -0
- package/dist/tools-knowledge.d.ts +4 -0
- package/dist/tools-knowledge.js +111 -0
- package/dist/tools-launch.d.ts +4 -0
- package/dist/tools-launch.js +365 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +296 -0
- package/package.json +58 -4
package/dist/tools.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { api, firstImageUrl, normalizeWebsite, query, } from "./api-client.js";
|
|
5
|
+
import { MAX_IMAGE_BYTES, prepareOutputFilePath, validateImageUrl, } from "./download-safety.js";
|
|
6
|
+
import { argsObject, emptyArgsSchema, listArgsSchema, outputSchemas, readOnly, safeIdSchema, safePath, stripPresentationArgs, writeLocal, writeRemote, } from "./tool-schemas.js";
|
|
7
|
+
import { demoImportAvailable, demoTools } from "./tools-demo.js";
|
|
8
|
+
import { knowledgeTools } from "./tools-knowledge.js";
|
|
9
|
+
import { accountTools, imageFormatSchema } from "./tools-account.js";
|
|
10
|
+
import { launchTools } from "./tools-launch.js";
|
|
11
|
+
const PLATFORM_BADGE_NOTE = 'This image carries a "Made with tday.com" badge because the organisation is on the Free plan. The owner can remove it by upgrading (tday_whoami has the link).';
|
|
12
|
+
export { responseFormatFromArgs, safePath } from "./tool-schemas.js";
|
|
13
|
+
export { ALLOWED_IMAGE_HOSTS, MAX_IMAGE_BYTES, prepareOutputFilePath, sandboxFilePath, validateImageUrl, } from "./download-safety.js";
|
|
14
|
+
export { formatToolResultText, stringifyResult, toStructuredContent, } from "./result-format.js";
|
|
15
|
+
export { knowledgeTools, KNOWLEDGE_ENTRY_TYPES } from "./tools-knowledge.js";
|
|
16
|
+
export const tools = [
|
|
17
|
+
{
|
|
18
|
+
name: "tday_whoami",
|
|
19
|
+
title: "Who am I?",
|
|
20
|
+
description: "Get the linked tday account: every organisation the person belongs to, their role in it (owner, admin or member), its plan, its credit balance and when it refreshes, what the plan's output includes (sizes, the Free badge, voiceover), what upgrading would change, its brands, and links to its usage and billing pages. Each organisation has a `summary` sentence to show the person as it is. A design or launch video spends the credits of its BRAND's organisation, so check the organisation that owns the brand before quoting a balance. Only an owner can change a plan; for an admin or member, say who the owner is instead of telling them to upgrade. Use first to verify MCP is connected to the expected account.",
|
|
21
|
+
inputSchema: emptyArgsSchema,
|
|
22
|
+
outputSchema: outputSchemas.whoami,
|
|
23
|
+
annotations: readOnly,
|
|
24
|
+
handler: (_args, config) => api(config, "/api/v1/account"),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "tday_list_brands",
|
|
28
|
+
title: "List brands",
|
|
29
|
+
description: "List brands accessible to the authenticated user as lightweight summaries. Supports API-backed limit/offset pagination.",
|
|
30
|
+
inputSchema: listArgsSchema,
|
|
31
|
+
outputSchema: outputSchemas.listBrands,
|
|
32
|
+
annotations: readOnly,
|
|
33
|
+
handler: (args, config) => api(config, `/api/brands/summary${query({ limit: Number(args.limit), offset: Number(args.offset) })}`),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "tday_get_brand",
|
|
37
|
+
title: "Get brand",
|
|
38
|
+
description: "Fetch a single brand with its colors, logos, fonts, images, and knowledge base. Use after tday_list_brands when detailed brand context is needed.",
|
|
39
|
+
inputSchema: argsObject({
|
|
40
|
+
brandId: safeIdSchema.describe("Brand ID from tday"),
|
|
41
|
+
}),
|
|
42
|
+
outputSchema: outputSchemas.getBrand,
|
|
43
|
+
annotations: readOnly,
|
|
44
|
+
handler: (args, config) => api(config, safePath("/api/brands/:brandId", args)),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "tday_list_designs",
|
|
48
|
+
title: "List designs",
|
|
49
|
+
description: "List the authenticated user's designs as lightweight summaries. Supports API-backed limit/offset pagination.",
|
|
50
|
+
inputSchema: listArgsSchema,
|
|
51
|
+
outputSchema: outputSchemas.listDesigns,
|
|
52
|
+
annotations: readOnly,
|
|
53
|
+
handler: (args, config) => api(config, `/api/designs/summary${query({ limit: Number(args.limit), offset: Number(args.offset) })}`),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "tday_get_design",
|
|
57
|
+
title: "Get design",
|
|
58
|
+
description: "Fetch a single design by id, including its generations. Use after tday_list_designs when generation IDs or full design details are needed.",
|
|
59
|
+
inputSchema: argsObject({
|
|
60
|
+
designId: safeIdSchema.describe("Design ID from tday"),
|
|
61
|
+
}),
|
|
62
|
+
outputSchema: outputSchemas.getDesign,
|
|
63
|
+
annotations: readOnly,
|
|
64
|
+
handler: (args, config) => api(config, safePath("/api/designs/:designId", args)),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "tday_generate_design",
|
|
68
|
+
title: "Generate design",
|
|
69
|
+
description: "Generate a new tday design from a text prompt against a brand, then poll with tday_poll_generation until it is completed or failed. An image costs 10 credits from the brand's organisation (tday_get_credit_prices has every price; tday_whoami has the balance) and a failed generation gives them back. The response says which organisation paid and what it has left; a refusal for credits means nothing was started, so do not retry. Omit format for square (1080x1080), or landscape (1200x630) where the plan does not include square.",
|
|
70
|
+
inputSchema: argsObject({
|
|
71
|
+
prompt: z
|
|
72
|
+
.string()
|
|
73
|
+
.trim()
|
|
74
|
+
.min(1)
|
|
75
|
+
.max(5000)
|
|
76
|
+
.describe("Design prompt. Be specific about audience, format, copy, and visual direction."),
|
|
77
|
+
brandId: safeIdSchema.describe("Brand ID from tday"),
|
|
78
|
+
name: z.string().trim().min(1).max(200).optional().describe("Short design name"),
|
|
79
|
+
format: imageFormatSchema,
|
|
80
|
+
}),
|
|
81
|
+
outputSchema: outputSchemas.generate,
|
|
82
|
+
annotations: writeRemote,
|
|
83
|
+
handler: (args, config) => api(config, "/api/v1/generate", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: JSON.stringify(stripPresentationArgs(args)),
|
|
86
|
+
}),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "tday_poll_generation",
|
|
90
|
+
title: "Poll generation",
|
|
91
|
+
description: "Poll a design generation. Returns status (pending/ideating/generating/visualising/reviewing/refining/completed/failed), editorUrl, imageUrl when completed, and platformBadge when the image carries the Free plan's badge. Call repeatedly until status is completed or failed.",
|
|
92
|
+
inputSchema: argsObject({
|
|
93
|
+
designId: safeIdSchema.describe("Design ID from tday"),
|
|
94
|
+
generationId: safeIdSchema.describe("Generation ID from tday"),
|
|
95
|
+
}),
|
|
96
|
+
outputSchema: outputSchemas.poll,
|
|
97
|
+
annotations: { ...readOnly, idempotentHint: true },
|
|
98
|
+
handler: async (args, config) => {
|
|
99
|
+
const path = safePath("/api/designs/:designId/generate/:generationId/status?poll=1", args);
|
|
100
|
+
const data = await api(config, path);
|
|
101
|
+
const generation = data.complete?.generation;
|
|
102
|
+
const status = data.status ?? generation?.status ?? "unknown";
|
|
103
|
+
const imageUrl = firstImageUrl(generation);
|
|
104
|
+
return {
|
|
105
|
+
designId: args.designId,
|
|
106
|
+
generationId: args.generationId,
|
|
107
|
+
status,
|
|
108
|
+
editorUrl: `${config.webBaseUrl}/designs/${args.designId}`,
|
|
109
|
+
...(imageUrl ? { imageUrl } : {}),
|
|
110
|
+
...(generation?.completedAt ? { completedAt: generation.completedAt } : {}),
|
|
111
|
+
...(data.platformBadge ? { platformBadge: true, badgeNote: PLATFORM_BADGE_NOTE } : {}),
|
|
112
|
+
...(status === "failed"
|
|
113
|
+
? { failureMessage: data.complete?.message ?? "Generation did not complete." }
|
|
114
|
+
: {}),
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "tday_create_brand",
|
|
120
|
+
title: "Create brand",
|
|
121
|
+
description: "Create a new brand from a website URL. Extracts colors, fonts, and logos automatically from the site. You can also pass a name and description.",
|
|
122
|
+
inputSchema: argsObject({
|
|
123
|
+
website: z
|
|
124
|
+
.string()
|
|
125
|
+
.trim()
|
|
126
|
+
.min(1)
|
|
127
|
+
.max(2048)
|
|
128
|
+
.describe("Website URL to extract brand assets from, for example stripe.com or https://stripe.com"),
|
|
129
|
+
name: z.string().trim().min(1).max(200).optional().describe("Brand name, auto-detected from website if omitted"),
|
|
130
|
+
description: z.string().trim().min(1).max(1000).optional().describe("Short brand description"),
|
|
131
|
+
industry: z.string().trim().min(1).max(120).optional().describe("Industry category"),
|
|
132
|
+
}),
|
|
133
|
+
outputSchema: outputSchemas.createBrand,
|
|
134
|
+
annotations: writeRemote,
|
|
135
|
+
handler: (args, config) => {
|
|
136
|
+
const body = { website: normalizeWebsite(String(args.website)) };
|
|
137
|
+
if (args.name)
|
|
138
|
+
body.name = args.name;
|
|
139
|
+
if (args.description)
|
|
140
|
+
body.description = args.description;
|
|
141
|
+
if (args.industry)
|
|
142
|
+
body.industry = args.industry;
|
|
143
|
+
return api(config, "/api/brands", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: JSON.stringify(body),
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "tday_download_image",
|
|
151
|
+
title: "Download image",
|
|
152
|
+
description: "Download a completed generated design image to a local file. Pass designId + generationId to save the exported file (what a download from tday gives you), or an imageUrl to save that exact image. Writes only inside the current working directory and caps downloads at 50MB.",
|
|
153
|
+
inputSchema: argsObject({
|
|
154
|
+
imageUrl: z
|
|
155
|
+
.string()
|
|
156
|
+
.trim()
|
|
157
|
+
.url()
|
|
158
|
+
.optional()
|
|
159
|
+
.describe("Completed generation image URL. Optional if designId and generationId are provided."),
|
|
160
|
+
designId: safeIdSchema.optional().describe("Design ID from tday. Optional if imageUrl is provided."),
|
|
161
|
+
generationId: safeIdSchema.optional().describe("Generation ID from tday. Optional if imageUrl is provided."),
|
|
162
|
+
filePath: z
|
|
163
|
+
.string()
|
|
164
|
+
.trim()
|
|
165
|
+
.min(1)
|
|
166
|
+
.max(500)
|
|
167
|
+
.describe("Local path to save the image, relative to cwd, for example ./tday-designs/poster.png"),
|
|
168
|
+
overwrite: z
|
|
169
|
+
.boolean()
|
|
170
|
+
.default(false)
|
|
171
|
+
.describe("Whether to overwrite an existing regular file. Defaults to false for safety."),
|
|
172
|
+
}),
|
|
173
|
+
outputSchema: outputSchemas.download,
|
|
174
|
+
annotations: writeLocal,
|
|
175
|
+
handler: async (args, config) => {
|
|
176
|
+
const hasUrl = Boolean(args.imageUrl);
|
|
177
|
+
const hasIds = Boolean(args.designId && args.generationId);
|
|
178
|
+
if (!hasUrl && !hasIds) {
|
|
179
|
+
throw new Error("Provide either imageUrl, or both designId and generationId.");
|
|
180
|
+
}
|
|
181
|
+
if ((args.designId && !args.generationId) || (!args.designId && args.generationId)) {
|
|
182
|
+
throw new Error("designId and generationId must be provided together.");
|
|
183
|
+
}
|
|
184
|
+
// The ids win: the export route answers the file a download gets, which for a brand whose
|
|
185
|
+
// downloads carry the Free-tier badge is a badged copy of the stored (clean) preview.
|
|
186
|
+
const imageUrl = hasIds
|
|
187
|
+
? await imageUrlForGeneration(args, config)
|
|
188
|
+
: String(args.imageUrl);
|
|
189
|
+
const url = validateImageUrl(imageUrl);
|
|
190
|
+
const filePath = await prepareOutputFilePath(String(args.filePath), {
|
|
191
|
+
overwrite: Boolean(args.overwrite),
|
|
192
|
+
});
|
|
193
|
+
const res = await fetch(url.href);
|
|
194
|
+
if (res.url)
|
|
195
|
+
validateImageUrl(res.url);
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
throw new Error(`Failed to download image: ${res.status} ${res.statusText}`);
|
|
198
|
+
}
|
|
199
|
+
const contentLength = Number(res.headers.get("content-length") ?? 0);
|
|
200
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_IMAGE_BYTES) {
|
|
201
|
+
throw new Error(`Image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit according to content-length. Download aborted.`);
|
|
202
|
+
}
|
|
203
|
+
const contentType = res.headers.get("content-type");
|
|
204
|
+
if (contentType &&
|
|
205
|
+
!contentType.toLowerCase().startsWith("image/") &&
|
|
206
|
+
!contentType.toLowerCase().startsWith("application/octet-stream")) {
|
|
207
|
+
throw new Error(`Download did not return an image (content-type: ${contentType})`);
|
|
208
|
+
}
|
|
209
|
+
// Stream with size cap to prevent OOM from oversized responses.
|
|
210
|
+
const chunks = [];
|
|
211
|
+
let totalBytes = 0;
|
|
212
|
+
const reader = res.body?.getReader();
|
|
213
|
+
if (!reader) {
|
|
214
|
+
throw new Error("No response body from image URL");
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
for (;;) {
|
|
218
|
+
const { done, value } = await reader.read();
|
|
219
|
+
if (done)
|
|
220
|
+
break;
|
|
221
|
+
totalBytes += value.byteLength;
|
|
222
|
+
if (totalBytes > MAX_IMAGE_BYTES) {
|
|
223
|
+
throw new Error(`Image exceeds ${MAX_IMAGE_BYTES / 1024 / 1024}MB limit. Download aborted.`);
|
|
224
|
+
}
|
|
225
|
+
chunks.push(value);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
reader.releaseLock();
|
|
230
|
+
}
|
|
231
|
+
const buffer = Buffer.concat(chunks);
|
|
232
|
+
const flags = constants.O_WRONLY |
|
|
233
|
+
constants.O_CREAT |
|
|
234
|
+
(args.overwrite ? constants.O_TRUNC : constants.O_EXCL) |
|
|
235
|
+
(constants.O_NOFOLLOW ?? 0);
|
|
236
|
+
const handle = await open(filePath, flags, 0o600);
|
|
237
|
+
try {
|
|
238
|
+
await handle.writeFile(buffer);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
await handle.close();
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
saved: true,
|
|
245
|
+
path: filePath,
|
|
246
|
+
size: buffer.byteLength,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
...accountTools,
|
|
251
|
+
...launchTools,
|
|
252
|
+
...knowledgeTools,
|
|
253
|
+
...demoTools,
|
|
254
|
+
];
|
|
255
|
+
/**
|
|
256
|
+
* The tools this account is shown: every tool, minus the demo-import tools unless the
|
|
257
|
+
* account may import (tools-demo.ts). `tools` stays the full set so `callTool` can still
|
|
258
|
+
* name them; their routes refuse an account without access.
|
|
259
|
+
*/
|
|
260
|
+
export async function listedTools(config) {
|
|
261
|
+
if (await demoImportAvailable(config))
|
|
262
|
+
return tools;
|
|
263
|
+
const hidden = new Set(demoTools.map((tool) => tool.name));
|
|
264
|
+
return tools.filter((tool) => !hidden.has(tool.name));
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The generation's DOWNLOAD at 1x, from the export route. The image a generation reports is
|
|
268
|
+
* the clean preview; the export route hands out the file that may leave tday.
|
|
269
|
+
*/
|
|
270
|
+
async function imageUrlForGeneration(args, config) {
|
|
271
|
+
const data = await api(config, safePath("/api/designs/:designId/generations/:generationId/export", args), { method: "POST", body: JSON.stringify({ scale: 1 }) });
|
|
272
|
+
const imageUrl = typeof data?.image?.url === "string" ? data.image.url : null;
|
|
273
|
+
if (!imageUrl) {
|
|
274
|
+
throw new Error("No image URL found for this generation. Poll tday_poll_generation until status is completed, then try again.");
|
|
275
|
+
}
|
|
276
|
+
return imageUrl;
|
|
277
|
+
}
|
|
278
|
+
function formatZodError(name, error) {
|
|
279
|
+
const details = error.issues
|
|
280
|
+
.map((issue) => {
|
|
281
|
+
const path = issue.path.length ? issue.path.join(".") : "arguments";
|
|
282
|
+
return `${path}: ${issue.message}`;
|
|
283
|
+
})
|
|
284
|
+
.join("; ");
|
|
285
|
+
return `Invalid arguments for ${name}: ${details}`;
|
|
286
|
+
}
|
|
287
|
+
export async function callTool(name, args, config) {
|
|
288
|
+
const tool = tools.find((t) => t.name === name);
|
|
289
|
+
if (!tool) {
|
|
290
|
+
throw new Error(`Unknown tool: ${name}. Available tools: ${tools.map((t) => t.name).join(", ")}`);
|
|
291
|
+
}
|
|
292
|
+
const parsed = tool.inputSchema.safeParse(args);
|
|
293
|
+
if (!parsed.success)
|
|
294
|
+
throw new Error(formatZodError(tool.name, parsed.error));
|
|
295
|
+
return tool.handler(parsed.data, config);
|
|
296
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tday-com/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server and CLI installer for tday.com - connect Claude Code, Codex, Cursor, and other AI agents to your tday.com account.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"tday",
|
|
9
|
+
"design-generation",
|
|
10
|
+
"ai",
|
|
11
|
+
"claude",
|
|
12
|
+
"codex",
|
|
13
|
+
"cursor"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://tday.com",
|
|
16
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
17
|
+
"author": "tday.com",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"bin": {
|
|
20
|
+
"mcp": "dist/cli.js",
|
|
21
|
+
"tday": "dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
33
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
34
|
+
"test": "bun test",
|
|
35
|
+
"prepublishOnly": "bun run build",
|
|
36
|
+
"start": "node dist/cli.js serve"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
|
+
"@opentelemetry/api": "^1.9.1",
|
|
41
|
+
"@opentelemetry/api-logs": "^0.222.0",
|
|
42
|
+
"@opentelemetry/auto-instrumentations-node": "^0.80.0",
|
|
43
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
44
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
|
|
45
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
46
|
+
"@opentelemetry/resources": "^2.11.0",
|
|
47
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
48
|
+
"@opentelemetry/sdk-metrics": "^2.11.0",
|
|
49
|
+
"@opentelemetry/sdk-node": "^0.222.0",
|
|
50
|
+
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
51
|
+
"zod": "^3.25.76"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^26.1.2",
|
|
55
|
+
"typescript": "^5.6.3"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|