@nextclaw/server 0.5.21 → 0.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +16 -1
- package/dist/index.js +186 -2
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -20,12 +20,24 @@ type ProviderConfigView = {
|
|
|
20
20
|
apiBase?: string | null;
|
|
21
21
|
extraHeaders?: Record<string, string> | null;
|
|
22
22
|
wireApi?: "auto" | "chat" | "responses" | null;
|
|
23
|
+
models?: string[];
|
|
23
24
|
};
|
|
24
25
|
type ProviderConfigUpdate = {
|
|
25
26
|
apiKey?: string | null;
|
|
26
27
|
apiBase?: string | null;
|
|
27
28
|
extraHeaders?: Record<string, string> | null;
|
|
28
29
|
wireApi?: "auto" | "chat" | "responses" | null;
|
|
30
|
+
models?: string[] | null;
|
|
31
|
+
};
|
|
32
|
+
type ProviderConnectionTestRequest = ProviderConfigUpdate & {
|
|
33
|
+
model?: string | null;
|
|
34
|
+
};
|
|
35
|
+
type ProviderConnectionTestResult = {
|
|
36
|
+
success: boolean;
|
|
37
|
+
provider: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
latencyMs: number;
|
|
40
|
+
message: string;
|
|
29
41
|
};
|
|
30
42
|
type AgentProfileView = {
|
|
31
43
|
id: string;
|
|
@@ -276,11 +288,13 @@ type ConfigView = {
|
|
|
276
288
|
type ProviderSpecView = {
|
|
277
289
|
name: string;
|
|
278
290
|
displayName?: string;
|
|
291
|
+
modelPrefix?: string;
|
|
279
292
|
keywords: string[];
|
|
280
293
|
envKey: string;
|
|
281
294
|
isGateway?: boolean;
|
|
282
295
|
isLocal?: boolean;
|
|
283
296
|
defaultApiBase?: string;
|
|
297
|
+
defaultModels?: string[];
|
|
284
298
|
supportsWireApi?: boolean;
|
|
285
299
|
wireApiOptions?: Array<"auto" | "chat" | "responses">;
|
|
286
300
|
defaultWireApi?: "auto" | "chat" | "responses";
|
|
@@ -578,6 +592,7 @@ declare function updateModel(configPath: string, patch: {
|
|
|
578
592
|
maxTokens?: number;
|
|
579
593
|
}): ConfigView;
|
|
580
594
|
declare function updateProvider(configPath: string, providerName: string, patch: ProviderConfigUpdate): ProviderConfigView | null;
|
|
595
|
+
declare function testProviderConnection(configPath: string, providerName: string, patch: ProviderConnectionTestRequest): Promise<ProviderConnectionTestResult | null>;
|
|
581
596
|
declare function updateChannel(configPath: string, channelName: string, patch: Record<string, unknown>): Record<string, unknown> | null;
|
|
582
597
|
declare function listSessions(configPath: string, query?: {
|
|
583
598
|
q?: string;
|
|
@@ -590,4 +605,4 @@ declare function deleteSession(configPath: string, key: string): boolean;
|
|
|
590
605
|
declare function updateRuntime(configPath: string, patch: RuntimeConfigUpdate): Pick<ConfigView, "agents" | "bindings" | "session">;
|
|
591
606
|
declare function updateSecrets(configPath: string, patch: SecretsConfigUpdate): SecretsView;
|
|
592
607
|
|
|
593
|
-
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderSpecView, type RuntimeConfigUpdate, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, updateChannel, updateModel, updateProvider, updateRuntime, updateSecrets };
|
|
608
|
+
export { type AgentBindingView, type AgentProfileView, type ApiError, type ApiResponse, type BindingPeerView, type ChannelSpecView, type ChatTurnRequest, type ChatTurnResult, type ChatTurnStreamEvent, type ChatTurnView, type ConfigActionExecuteRequest, type ConfigActionExecuteResult, type ConfigActionManifest, type ConfigActionType, type ConfigMetaView, type ConfigSchemaResponse, type ConfigUiHint, type ConfigUiHints, type ConfigView, type CronActionResult, type CronEnableRequest, type CronJobStateView, type CronJobView, type CronListView, type CronPayloadView, type CronRunRequest, type CronScheduleView, type MarketplaceApiConfig, type MarketplaceInstallKind, type MarketplaceInstallSkillParams, type MarketplaceInstallSpec, type MarketplaceInstalledRecord, type MarketplaceInstalledView, type MarketplaceInstaller, type MarketplaceItemSummary, type MarketplaceItemType, type MarketplaceItemView, type MarketplaceListView, type MarketplacePluginInstallRequest, type MarketplacePluginInstallResult, type MarketplacePluginManageAction, type MarketplacePluginManageRequest, type MarketplacePluginManageResult, type MarketplaceRecommendationView, type MarketplaceSkillInstallRequest, type MarketplaceSkillInstallResult, type MarketplaceSkillManageAction, type MarketplaceSkillManageRequest, type MarketplaceSkillManageResult, type MarketplaceSort, type ProviderConfigUpdate, type ProviderConfigView, type ProviderConnectionTestRequest, type ProviderConnectionTestResult, type ProviderSpecView, type RuntimeConfigUpdate, type SecretProviderEnvView, type SecretProviderExecView, type SecretProviderFileView, type SecretProviderView, type SecretRefView, type SecretSourceView, type SecretsConfigUpdate, type SecretsView, type SessionConfigView, type SessionEntryView, type SessionEventView, type SessionHistoryView, type SessionMessageView, type SessionPatchUpdate, type SessionsListView, type UiChatRuntime, type UiServerEvent, type UiServerHandle, type UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createUiRouter, deleteSession, executeConfigAction, getSessionHistory, listSessions, loadConfigOrDefault, patchSession, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSecrets };
|
package/dist/index.js
CHANGED
|
@@ -19,9 +19,11 @@ import {
|
|
|
19
19
|
saveConfig,
|
|
20
20
|
ConfigSchema,
|
|
21
21
|
probeFeishu,
|
|
22
|
+
LiteLLMProvider,
|
|
22
23
|
PROVIDERS,
|
|
23
24
|
buildConfigSchema,
|
|
24
25
|
findProviderByName,
|
|
26
|
+
getProviderName,
|
|
25
27
|
getPackageVersion,
|
|
26
28
|
hasSecretRef,
|
|
27
29
|
isSensitiveConfigPath,
|
|
@@ -30,6 +32,33 @@ import {
|
|
|
30
32
|
} from "@nextclaw/core";
|
|
31
33
|
var MASK_MIN_LENGTH = 8;
|
|
32
34
|
var EXTRA_SENSITIVE_PATH_PATTERNS = [/authorization/i, /cookie/i, /session/i, /bearer/i];
|
|
35
|
+
var PROVIDER_TEST_MODEL_FALLBACKS = {
|
|
36
|
+
openai: "gpt-5-mini",
|
|
37
|
+
deepseek: "deepseek-v3.2",
|
|
38
|
+
gemini: "gemini-3-flash-preview",
|
|
39
|
+
zhipu: "glm-5",
|
|
40
|
+
dashscope: "qwen3.5-flash",
|
|
41
|
+
moonshot: "kimi-k2.5",
|
|
42
|
+
minimax: "MiniMax-M2.5",
|
|
43
|
+
groq: "llama-3.1-8b-instant",
|
|
44
|
+
openrouter: "openai/gpt-5.3-codex",
|
|
45
|
+
aihubmix: "gpt-5.3-codex",
|
|
46
|
+
anthropic: "claude-opus-4-6"
|
|
47
|
+
};
|
|
48
|
+
var PREFERRED_PROVIDER_ORDER = [
|
|
49
|
+
"openai",
|
|
50
|
+
"anthropic",
|
|
51
|
+
"gemini",
|
|
52
|
+
"openrouter",
|
|
53
|
+
"dashscope",
|
|
54
|
+
"deepseek",
|
|
55
|
+
"minimax",
|
|
56
|
+
"moonshot",
|
|
57
|
+
"zhipu"
|
|
58
|
+
];
|
|
59
|
+
var PREFERRED_PROVIDER_ORDER_INDEX = new Map(
|
|
60
|
+
PREFERRED_PROVIDER_ORDER.map((name, index) => [name, index])
|
|
61
|
+
);
|
|
33
62
|
function matchesExtraSensitivePath(path) {
|
|
34
63
|
if (path === "session" || path.startsWith("session.")) {
|
|
35
64
|
return false;
|
|
@@ -231,6 +260,23 @@ function maskApiKey(value) {
|
|
|
231
260
|
apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
|
|
232
261
|
};
|
|
233
262
|
}
|
|
263
|
+
function normalizeModelList(input) {
|
|
264
|
+
if (!input || input.length === 0) {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
268
|
+
for (const item of input) {
|
|
269
|
+
if (typeof item !== "string") {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const trimmed = item.trim();
|
|
273
|
+
if (!trimmed) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
deduped.add(trimmed);
|
|
277
|
+
}
|
|
278
|
+
return [...deduped];
|
|
279
|
+
}
|
|
234
280
|
function toProviderView(config, provider, providerName, uiHints, spec) {
|
|
235
281
|
const apiKeyPath = `providers.${providerName}.apiKey`;
|
|
236
282
|
const apiKeyRefSet = hasSecretRef(config, apiKeyPath);
|
|
@@ -244,7 +290,8 @@ function toProviderView(config, provider, providerName, uiHints, spec) {
|
|
|
244
290
|
apiKeySet: masked.apiKeySet || apiKeyRefSet,
|
|
245
291
|
apiKeyMasked: masked.apiKeyMasked ?? (apiKeyRefSet ? "****" : void 0),
|
|
246
292
|
apiBase: provider.apiBase ?? null,
|
|
247
|
-
extraHeaders: extraHeaders && Object.keys(extraHeaders).length > 0 ? extraHeaders : null
|
|
293
|
+
extraHeaders: extraHeaders && Object.keys(extraHeaders).length > 0 ? extraHeaders : null,
|
|
294
|
+
models: normalizeModelList(provider.models ?? [])
|
|
248
295
|
};
|
|
249
296
|
if (spec?.supportsWireApi) {
|
|
250
297
|
view.wireApi = provider.wireApi ?? spec.defaultWireApi ?? "auto";
|
|
@@ -288,15 +335,30 @@ function buildConfigMeta(config) {
|
|
|
288
335
|
const providers = PROVIDERS.map((spec) => ({
|
|
289
336
|
name: spec.name,
|
|
290
337
|
displayName: spec.displayName,
|
|
338
|
+
modelPrefix: spec.modelPrefix,
|
|
291
339
|
keywords: spec.keywords,
|
|
292
340
|
envKey: spec.envKey,
|
|
293
341
|
isGateway: spec.isGateway,
|
|
294
342
|
isLocal: spec.isLocal,
|
|
295
343
|
defaultApiBase: spec.defaultApiBase,
|
|
344
|
+
defaultModels: normalizeModelList(spec.defaultModels ?? []),
|
|
296
345
|
supportsWireApi: spec.supportsWireApi,
|
|
297
346
|
wireApiOptions: spec.wireApiOptions,
|
|
298
347
|
defaultWireApi: spec.defaultWireApi
|
|
299
|
-
}))
|
|
348
|
+
})).sort((left, right) => {
|
|
349
|
+
const leftRank = PREFERRED_PROVIDER_ORDER_INDEX.get(left.name);
|
|
350
|
+
const rightRank = PREFERRED_PROVIDER_ORDER_INDEX.get(right.name);
|
|
351
|
+
if (leftRank !== void 0 && rightRank !== void 0) {
|
|
352
|
+
return leftRank - rightRank;
|
|
353
|
+
}
|
|
354
|
+
if (leftRank !== void 0) {
|
|
355
|
+
return -1;
|
|
356
|
+
}
|
|
357
|
+
if (rightRank !== void 0) {
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
return left.name.localeCompare(right.name);
|
|
361
|
+
});
|
|
300
362
|
const channels = Object.keys(config.channels).map((name) => ({
|
|
301
363
|
name,
|
|
302
364
|
displayName: name,
|
|
@@ -394,12 +456,117 @@ function updateProvider(configPath, providerName, patch) {
|
|
|
394
456
|
if (Object.prototype.hasOwnProperty.call(patch, "wireApi") && spec?.supportsWireApi) {
|
|
395
457
|
provider.wireApi = patch.wireApi ?? spec.defaultWireApi ?? "auto";
|
|
396
458
|
}
|
|
459
|
+
if (Object.prototype.hasOwnProperty.call(patch, "models")) {
|
|
460
|
+
provider.models = normalizeModelList(patch.models ?? []);
|
|
461
|
+
}
|
|
397
462
|
const next = ConfigSchema.parse(config);
|
|
398
463
|
saveConfig(next, configPath);
|
|
399
464
|
const uiHints = buildUiHints(next);
|
|
400
465
|
const updated = next.providers[providerName];
|
|
401
466
|
return toProviderView(next, updated, providerName, uiHints, spec ?? void 0);
|
|
402
467
|
}
|
|
468
|
+
function normalizeOptionalString(value) {
|
|
469
|
+
if (typeof value !== "string") {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const trimmed = value.trim();
|
|
473
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
474
|
+
}
|
|
475
|
+
function normalizeHeaders(input) {
|
|
476
|
+
if (!input) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const entries = Object.entries(input).map(([key, value]) => [key.trim(), String(value ?? "").trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
|
|
480
|
+
if (entries.length === 0) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return Object.fromEntries(entries);
|
|
484
|
+
}
|
|
485
|
+
function resolveTestModel(config, providerName, requestedModel) {
|
|
486
|
+
if (requestedModel) {
|
|
487
|
+
return requestedModel;
|
|
488
|
+
}
|
|
489
|
+
const defaultModel = normalizeOptionalString(config.agents.defaults.model);
|
|
490
|
+
if (defaultModel) {
|
|
491
|
+
const routedProvider = getProviderName(config, defaultModel);
|
|
492
|
+
if (!routedProvider || routedProvider === providerName) {
|
|
493
|
+
return defaultModel;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return PROVIDER_TEST_MODEL_FALLBACKS[providerName] ?? defaultModel ?? null;
|
|
497
|
+
}
|
|
498
|
+
function stringifyError(error) {
|
|
499
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
500
|
+
return raw.replace(/\s+/g, " ").trim();
|
|
501
|
+
}
|
|
502
|
+
async function testProviderConnection(configPath, providerName, patch) {
|
|
503
|
+
const config = loadConfigOrDefault(configPath);
|
|
504
|
+
const provider = config.providers[providerName];
|
|
505
|
+
if (!provider) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const spec = findProviderByName(providerName);
|
|
509
|
+
const hasApiKeyPatch = Object.prototype.hasOwnProperty.call(patch, "apiKey");
|
|
510
|
+
const providedApiKey = normalizeOptionalString(patch.apiKey);
|
|
511
|
+
const currentApiKey = normalizeOptionalString(provider.apiKey);
|
|
512
|
+
const apiKey = hasApiKeyPatch ? providedApiKey : currentApiKey;
|
|
513
|
+
const hasApiBasePatch = Object.prototype.hasOwnProperty.call(patch, "apiBase");
|
|
514
|
+
const patchedApiBase = normalizeOptionalString(patch.apiBase);
|
|
515
|
+
const currentApiBase = normalizeOptionalString(provider.apiBase);
|
|
516
|
+
const apiBase = hasApiBasePatch ? patchedApiBase ?? spec?.defaultApiBase ?? null : currentApiBase ?? spec?.defaultApiBase ?? null;
|
|
517
|
+
const hasHeadersPatch = Object.prototype.hasOwnProperty.call(patch, "extraHeaders");
|
|
518
|
+
const extraHeaders = hasHeadersPatch ? normalizeHeaders(patch.extraHeaders ?? null) : normalizeHeaders(provider.extraHeaders ?? null);
|
|
519
|
+
const wireApi = spec?.supportsWireApi ? patch.wireApi ?? provider.wireApi ?? spec.defaultWireApi ?? "auto" : null;
|
|
520
|
+
if (!apiKey && !spec?.isLocal) {
|
|
521
|
+
return {
|
|
522
|
+
success: false,
|
|
523
|
+
provider: providerName,
|
|
524
|
+
latencyMs: 0,
|
|
525
|
+
message: "API key is required before testing the connection."
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const requestedModel = normalizeOptionalString(patch.model);
|
|
529
|
+
const model = resolveTestModel(config, providerName, requestedModel);
|
|
530
|
+
if (!model) {
|
|
531
|
+
return {
|
|
532
|
+
success: false,
|
|
533
|
+
provider: providerName,
|
|
534
|
+
latencyMs: 0,
|
|
535
|
+
message: "No test model found. Set a default model first, then try again."
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
const probe = new LiteLLMProvider({
|
|
539
|
+
apiKey,
|
|
540
|
+
apiBase,
|
|
541
|
+
defaultModel: model,
|
|
542
|
+
extraHeaders,
|
|
543
|
+
providerName,
|
|
544
|
+
wireApi
|
|
545
|
+
});
|
|
546
|
+
const startedAtMs = Date.now();
|
|
547
|
+
try {
|
|
548
|
+
await probe.chat({
|
|
549
|
+
model,
|
|
550
|
+
messages: [{ role: "user", content: "ping" }],
|
|
551
|
+
maxTokens: 8
|
|
552
|
+
});
|
|
553
|
+
return {
|
|
554
|
+
success: true,
|
|
555
|
+
provider: providerName,
|
|
556
|
+
model,
|
|
557
|
+
latencyMs: Date.now() - startedAtMs,
|
|
558
|
+
message: "Connection test passed."
|
|
559
|
+
};
|
|
560
|
+
} catch (error) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
provider: providerName,
|
|
564
|
+
model,
|
|
565
|
+
latencyMs: Date.now() - startedAtMs,
|
|
566
|
+
message: stringifyError(error) || "Connection test failed."
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
}
|
|
403
570
|
function updateChannel(configPath, channelName, patch) {
|
|
404
571
|
const config = loadConfigOrDefault(configPath);
|
|
405
572
|
const channel = config.channels[channelName];
|
|
@@ -1594,6 +1761,22 @@ function createUiRouter(options) {
|
|
|
1594
1761
|
options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
|
|
1595
1762
|
return c.json(ok(result));
|
|
1596
1763
|
});
|
|
1764
|
+
app.post("/api/config/providers/:provider/test", async (c) => {
|
|
1765
|
+
const provider = c.req.param("provider");
|
|
1766
|
+
const body = await readJson(c.req.raw);
|
|
1767
|
+
if (!body.ok) {
|
|
1768
|
+
return c.json(err("INVALID_BODY", "invalid json body"), 400);
|
|
1769
|
+
}
|
|
1770
|
+
const result = await testProviderConnection(
|
|
1771
|
+
options.configPath,
|
|
1772
|
+
provider,
|
|
1773
|
+
body.data
|
|
1774
|
+
);
|
|
1775
|
+
if (!result) {
|
|
1776
|
+
return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
|
|
1777
|
+
}
|
|
1778
|
+
return c.json(ok(result));
|
|
1779
|
+
});
|
|
1597
1780
|
app.put("/api/config/channels/:channel", async (c) => {
|
|
1598
1781
|
const channel = c.req.param("channel");
|
|
1599
1782
|
const body = await readJson(c.req.raw);
|
|
@@ -2014,6 +2197,7 @@ export {
|
|
|
2014
2197
|
loadConfigOrDefault,
|
|
2015
2198
|
patchSession,
|
|
2016
2199
|
startUiServer,
|
|
2200
|
+
testProviderConnection,
|
|
2017
2201
|
updateChannel,
|
|
2018
2202
|
updateModel,
|
|
2019
2203
|
updateProvider,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.23",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Nextclaw UI/API server.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@hono/node-server": "^1.13.3",
|
|
18
|
-
"@nextclaw/openclaw-compat": "^0.1.
|
|
18
|
+
"@nextclaw/openclaw-compat": "^0.1.30",
|
|
19
19
|
"hono": "^4.6.2",
|
|
20
20
|
"ws": "^8.18.0",
|
|
21
|
-
"@nextclaw/core": "^0.6.
|
|
21
|
+
"@nextclaw/core": "^0.6.39"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^20.17.6",
|