@kakunin/mcp 0.1.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/README.md +72 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +310 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# @kakunin/mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server for the [Kakunin](https://kakunin.ai) AI agent compliance API. Lets AI agents self-verify scope, check their own risk score, and log behavioral events — all from within Claude, Cursor, or any MCP-compatible runtime.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @kakunin/mcp
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Tools
|
|
10
|
+
|
|
11
|
+
### `verify_agent_scope`
|
|
12
|
+
Check whether this agent is authorised to perform an action before executing it. Verifies the active X.509 certificate, permitted_actions scope, financial limits, and revocation status.
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"action": "initiate EUR/USD trade on euronext for 50000 USD",
|
|
17
|
+
"venue": "euronext",
|
|
18
|
+
"amount_usd": 50000
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Returns `{ allowed: true|false, reason, certificate_status, permitted_actions }`.
|
|
23
|
+
|
|
24
|
+
### `check_risk_score`
|
|
25
|
+
Retrieve the agent's rolling 30-day risk score, band (`low`/`medium`/`high`), drift trend, and actionable guidance. No input required.
|
|
26
|
+
|
|
27
|
+
### `audit_log_append`
|
|
28
|
+
Append a behavioral event to the agent's immutable audit log. Returns risk score + transaction ID. Events scoring ≥ 0.85 auto-trigger a certificate revocation check.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"action_type": "transaction_initiated",
|
|
33
|
+
"details": { "amount_usd": 50000, "venue": "NYSE" }
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
**Prerequisites:** Node ≥ 18, Kakunin API key + Agent ID from [kakunin.ai/dashboard](https://kakunin.ai/dashboard).
|
|
40
|
+
|
|
41
|
+
### Claude Desktop
|
|
42
|
+
|
|
43
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"kakunin": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["-y", "@kakunin/mcp"],
|
|
51
|
+
"env": {
|
|
52
|
+
"KAKUNIN_API_KEY": "kkn_live_...",
|
|
53
|
+
"KAKUNIN_AGENT_ID": "agt_..."
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Environment variables
|
|
61
|
+
|
|
62
|
+
| Variable | Required | Description |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `KAKUNIN_API_KEY` | ✅ | API key (`kkn_live_...` or `kkn_test_...`) |
|
|
65
|
+
| `KAKUNIN_AGENT_ID` | ✅ | Agent ID the server acts on behalf of |
|
|
66
|
+
| `KAKUNIN_BASE_URL` | optional | Override API base (default: `https://kakunin.ai`) |
|
|
67
|
+
|
|
68
|
+
## Sandbox mode
|
|
69
|
+
|
|
70
|
+
Use a `kkn_test_...` key for development — hits the sandbox CA, no cost, 100 free certs/day.
|
|
71
|
+
|
|
72
|
+
Full docs at [docs.kakunin.ai](https://docs.kakunin.ai).
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
|
|
5
|
+
// src/client.ts
|
|
6
|
+
var DEFAULT_BASE_URL = "https://api.kakunin.ai/v1";
|
|
7
|
+
var KakuninMcpClient = class {
|
|
8
|
+
apiKey;
|
|
9
|
+
agentId;
|
|
10
|
+
baseUrl;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.apiKey = config.apiKey;
|
|
14
|
+
this.agentId = config.agentId;
|
|
15
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
16
|
+
this.timeoutMs = config.timeoutMs ?? 8e3;
|
|
17
|
+
}
|
|
18
|
+
async request(path, options = {}) {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch(`${this.baseUrl}${path}`, {
|
|
24
|
+
...options,
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
28
|
+
"User-Agent": `@kakunin/mcp/0.1.0`,
|
|
29
|
+
...options.headers
|
|
30
|
+
},
|
|
31
|
+
signal: controller.signal
|
|
32
|
+
});
|
|
33
|
+
} finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
38
|
+
throw new Error(`Kakunin API error ${res.status}: ${body.error ?? res.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
const json = await res.json();
|
|
41
|
+
return json.data;
|
|
42
|
+
}
|
|
43
|
+
/** Fetch the agent's current active certificate, including permitted_actions. */
|
|
44
|
+
async getActiveCertificate() {
|
|
45
|
+
const agent = await this.request(
|
|
46
|
+
`/agents/${this.agentId}`
|
|
47
|
+
);
|
|
48
|
+
const active = agent.certificates.find((c) => c.status === "active");
|
|
49
|
+
return active ?? null;
|
|
50
|
+
}
|
|
51
|
+
/** Fetch rolling 30-day risk profile. */
|
|
52
|
+
async getRiskProfile() {
|
|
53
|
+
return this.request(`/agents/${this.agentId}/risk`);
|
|
54
|
+
}
|
|
55
|
+
/** Ingest a behavioral event. Returns risk score synchronously. */
|
|
56
|
+
async ingestEvent(actionType, metadata, sessionId) {
|
|
57
|
+
return this.request("/events", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
agent_id: this.agentId,
|
|
61
|
+
action_type: actionType,
|
|
62
|
+
details: metadata,
|
|
63
|
+
...sessionId !== void 0 ? { session_id: sessionId } : {},
|
|
64
|
+
occurred_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// src/tools/verify-scope.ts
|
|
71
|
+
import { z } from "zod";
|
|
72
|
+
var verifyScopeInputSchema = z.object({
|
|
73
|
+
action: z.string().min(1).describe(
|
|
74
|
+
'The action or endpoint the agent wants to perform. Can be a scope string (e.g. "write:invoices"), an API path (e.g. "/api/v1/execute_trade"), or a plain description (e.g. "initiate EUR/USD trade on euronext for 50000 USD").'
|
|
75
|
+
),
|
|
76
|
+
venue: z.string().optional().describe('Optional trading venue or system being accessed (e.g. "euronext", "xetra").'),
|
|
77
|
+
amount_usd: z.number().optional().describe("Optional transaction amount in USD for financial scope enforcement.")
|
|
78
|
+
});
|
|
79
|
+
async function verifyScopeHandler(client2, input) {
|
|
80
|
+
const cert = await client2.getActiveCertificate();
|
|
81
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
82
|
+
if (!cert) {
|
|
83
|
+
return {
|
|
84
|
+
allowed: false,
|
|
85
|
+
reason: "No active certificate found. Agent must be certified before performing actions.",
|
|
86
|
+
certificate_status: "none",
|
|
87
|
+
permitted_actions: [],
|
|
88
|
+
checked_at: checkedAt
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (cert.status !== "active") {
|
|
92
|
+
return {
|
|
93
|
+
allowed: false,
|
|
94
|
+
reason: `Certificate is ${cert.status}. All actions are blocked until a new certificate is issued.`,
|
|
95
|
+
certificate_status: cert.status,
|
|
96
|
+
permitted_actions: cert.permitted_actions,
|
|
97
|
+
checked_at: checkedAt
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (input.amount_usd !== void 0 && cert.financial_scope?.max_single_trade_usd !== void 0) {
|
|
101
|
+
if (input.amount_usd > cert.financial_scope.max_single_trade_usd) {
|
|
102
|
+
return {
|
|
103
|
+
allowed: false,
|
|
104
|
+
reason: `Transaction amount $${input.amount_usd.toLocaleString()} USD exceeds the per-trade limit of $${cert.financial_scope.max_single_trade_usd.toLocaleString()} USD encoded in this certificate.`,
|
|
105
|
+
certificate_status: "active",
|
|
106
|
+
permitted_actions: cert.permitted_actions,
|
|
107
|
+
checked_at: checkedAt
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (input.venue !== void 0 && cert.financial_scope?.permitted_venues !== void 0) {
|
|
112
|
+
const venues = cert.financial_scope.permitted_venues;
|
|
113
|
+
if (!venues.includes(input.venue)) {
|
|
114
|
+
return {
|
|
115
|
+
allowed: false,
|
|
116
|
+
reason: `Venue "${input.venue}" is not in the permitted venues list: ${venues.join(", ")}.`,
|
|
117
|
+
certificate_status: "active",
|
|
118
|
+
permitted_actions: cert.permitted_actions,
|
|
119
|
+
checked_at: checkedAt
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const action = input.action;
|
|
124
|
+
const allowed = isActionPermitted(action, cert.permitted_actions);
|
|
125
|
+
return {
|
|
126
|
+
allowed,
|
|
127
|
+
reason: allowed ? `Action "${action}" is within the scope of this certificate.` : `Action "${action}" is not in the permitted actions for this certificate. Permitted: ${cert.permitted_actions.join(", ")}.`,
|
|
128
|
+
certificate_status: "active",
|
|
129
|
+
permitted_actions: cert.permitted_actions,
|
|
130
|
+
checked_at: checkedAt
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function isActionPermitted(action, permitted) {
|
|
134
|
+
const normalized = action.toLowerCase().trim();
|
|
135
|
+
for (const perm of permitted) {
|
|
136
|
+
const p = perm.toLowerCase().trim();
|
|
137
|
+
if (p === normalized) return true;
|
|
138
|
+
if (p.endsWith(":*")) {
|
|
139
|
+
const prefix = p.slice(0, -1);
|
|
140
|
+
if (normalized.startsWith(prefix)) return true;
|
|
141
|
+
}
|
|
142
|
+
if (normalized.startsWith("/") || normalized.startsWith("http")) {
|
|
143
|
+
const resource = p.includes(":") ? p.split(":")[1] : p;
|
|
144
|
+
if (resource && normalized.includes(resource)) return true;
|
|
145
|
+
}
|
|
146
|
+
if (!normalized.startsWith("/") && !normalized.includes(":")) {
|
|
147
|
+
if (p.includes(normalized) || normalized.includes(p.replace(/.*:/, ""))) return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/tools/check-risk.ts
|
|
154
|
+
async function checkRiskHandler(client2) {
|
|
155
|
+
const profile = await client2.getRiskProfile();
|
|
156
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
157
|
+
const trend = profile.drift.drift_trend ?? "insufficient_data";
|
|
158
|
+
const recommendation = buildRecommendation(profile.dominant_band, trend, profile.avg_score);
|
|
159
|
+
return {
|
|
160
|
+
score: profile.avg_score,
|
|
161
|
+
band: profile.dominant_band,
|
|
162
|
+
trend,
|
|
163
|
+
recommendation,
|
|
164
|
+
recent_high_risk_events: profile.recent_high_risk_events,
|
|
165
|
+
checked_at: checkedAt
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function buildRecommendation(band, trend, score) {
|
|
169
|
+
if (band === "high") {
|
|
170
|
+
return `Risk score ${score.toFixed(2)} is HIGH. Certificate revocation check has been queued. You should pause non-essential operations and notify your human operator immediately.`;
|
|
171
|
+
}
|
|
172
|
+
if (band === "medium" && trend === "increasing") {
|
|
173
|
+
return `Risk score ${score.toFixed(2)} is MEDIUM and increasing. Consider reducing transaction frequency or scope until trend stabilises.`;
|
|
174
|
+
}
|
|
175
|
+
if (band === "medium") {
|
|
176
|
+
return `Risk score ${score.toFixed(2)} is MEDIUM. Continue with caution \u2014 avoid high-value or irreversible operations without human confirmation.`;
|
|
177
|
+
}
|
|
178
|
+
if (trend === "increasing") {
|
|
179
|
+
return `Risk score ${score.toFixed(2)} is LOW but trend is increasing. Monitor closely \u2014 consider logging additional context with audit_log_append.`;
|
|
180
|
+
}
|
|
181
|
+
return `Risk score ${score.toFixed(2)} is LOW. Normal operations permitted.`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/tools/audit-log-append.ts
|
|
185
|
+
import { z as z2 } from "zod";
|
|
186
|
+
var ACTION_TYPES = [
|
|
187
|
+
"api_call",
|
|
188
|
+
"authentication_attempt",
|
|
189
|
+
"authentication_failure",
|
|
190
|
+
"data_access",
|
|
191
|
+
"data_mutation",
|
|
192
|
+
"transaction_initiated",
|
|
193
|
+
"transaction_anomaly",
|
|
194
|
+
"unauthorized_access_attempt",
|
|
195
|
+
"message_signed",
|
|
196
|
+
"message_verification_failed"
|
|
197
|
+
];
|
|
198
|
+
var auditLogAppendInputSchema = z2.object({
|
|
199
|
+
event_type: z2.enum(ACTION_TYPES).describe(
|
|
200
|
+
'Type of behavioral event being logged. Use "transaction_initiated" for financial operations, "data_mutation" for write/delete ops, "api_call" for standard API calls.'
|
|
201
|
+
),
|
|
202
|
+
metadata: z2.record(z2.unknown()).describe(
|
|
203
|
+
"Arbitrary key-value pairs describing the event. For transactions: include amount, currency, venue. For data ops: include resource_type, resource_id, operation. Avoid PII \u2014 this is stored in the immutable audit log."
|
|
204
|
+
),
|
|
205
|
+
session_id: z2.string().optional().describe("Optional session ID for grouping related events in the audit trail.")
|
|
206
|
+
});
|
|
207
|
+
async function auditLogAppendHandler(client2, input) {
|
|
208
|
+
const result = await client2.ingestEvent(
|
|
209
|
+
input.event_type,
|
|
210
|
+
input.metadata,
|
|
211
|
+
input.session_id
|
|
212
|
+
);
|
|
213
|
+
return {
|
|
214
|
+
inserted: true,
|
|
215
|
+
tx_id: result.event_id,
|
|
216
|
+
risk_score: result.risk_score,
|
|
217
|
+
risk_band: result.risk_band,
|
|
218
|
+
revocation_check_queued: result.revocation_check_queued,
|
|
219
|
+
logged_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/index.ts
|
|
224
|
+
var apiKey = process.env["KAKUNIN_API_KEY"];
|
|
225
|
+
var agentId = process.env["KAKUNIN_AGENT_ID"];
|
|
226
|
+
var baseUrl = process.env["KAKUNIN_BASE_URL"];
|
|
227
|
+
if (!apiKey) {
|
|
228
|
+
console.error("[kakunin-mcp] KAKUNIN_API_KEY is required");
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
if (!agentId) {
|
|
232
|
+
console.error("[kakunin-mcp] KAKUNIN_AGENT_ID is required");
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
var client = new KakuninMcpClient({
|
|
236
|
+
apiKey,
|
|
237
|
+
agentId,
|
|
238
|
+
...baseUrl !== void 0 ? { baseUrl } : {}
|
|
239
|
+
});
|
|
240
|
+
var server = new McpServer({
|
|
241
|
+
name: "kakunin",
|
|
242
|
+
version: "0.1.0"
|
|
243
|
+
});
|
|
244
|
+
server.tool(
|
|
245
|
+
"verify_agent_scope",
|
|
246
|
+
"Check whether this agent is authorised to perform a specific action or call a specific endpoint. Verifies the active X.509 certificate, permitted_actions scope, financial limits, and revocation status. Call this BEFORE executing any action that might exceed scope \u2014 not after.",
|
|
247
|
+
verifyScopeInputSchema.shape,
|
|
248
|
+
async (input) => {
|
|
249
|
+
const result = await verifyScopeHandler(client, input);
|
|
250
|
+
return {
|
|
251
|
+
content: [
|
|
252
|
+
{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: JSON.stringify(result, null, 2)
|
|
255
|
+
}
|
|
256
|
+
]
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
);
|
|
260
|
+
server.tool(
|
|
261
|
+
"check_risk_score",
|
|
262
|
+
"Retrieve this agent's rolling 30-day risk score, risk band (low/medium/high), and drift trend. Returns actionable guidance \u2014 use this to decide whether to self-throttle, escalate to a human, or proceed normally.",
|
|
263
|
+
// No input parameters
|
|
264
|
+
{},
|
|
265
|
+
async () => {
|
|
266
|
+
const result = await checkRiskHandler(client);
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: JSON.stringify(result, null, 2)
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
server.tool(
|
|
278
|
+
"audit_log_append",
|
|
279
|
+
"Append a behavioral event to this agent's immutable audit log. Use this to voluntarily record actions \u2014 transactions, data operations, API calls. Returns the risk score for the event and a transaction ID for traceability. High-risk events (score >= 0.85) automatically trigger a certificate revocation check.",
|
|
280
|
+
auditLogAppendInputSchema.shape,
|
|
281
|
+
async (input) => {
|
|
282
|
+
const parsed = auditLogAppendInputSchema.safeParse(input);
|
|
283
|
+
if (!parsed.success) {
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: JSON.stringify({ error: parsed.error.message }, null, 2)
|
|
289
|
+
}
|
|
290
|
+
],
|
|
291
|
+
isError: true
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const result = await auditLogAppendHandler(client, parsed.data);
|
|
295
|
+
return {
|
|
296
|
+
content: [
|
|
297
|
+
{
|
|
298
|
+
type: "text",
|
|
299
|
+
text: JSON.stringify(result, null, 2)
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
);
|
|
305
|
+
var transport = new StdioServerTransport();
|
|
306
|
+
await server.connect(transport);
|
|
307
|
+
process.on("SIGINT", async () => {
|
|
308
|
+
await server.close();
|
|
309
|
+
process.exit(0);
|
|
310
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kakunin/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kakunin MCP server — lets AI agents query their own scope, risk score, and log behavioral events",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"kakunin-mcp": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"dev": "node --env-file=.env dist/index.js",
|
|
26
|
+
"prepublishOnly": "npm run build && npm run typecheck"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"ai-agents",
|
|
32
|
+
"kyc",
|
|
33
|
+
"kakunin",
|
|
34
|
+
"compliance"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://kakunin.ai",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/nqzai/kakunin.git",
|
|
41
|
+
"directory": "mcp/kakunin-mcp"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
45
|
+
"zod": "^3.23.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^25.9.1",
|
|
49
|
+
"tsup": "^8.0.0",
|
|
50
|
+
"typescript": "^5.4.0",
|
|
51
|
+
"vitest": "^1.6.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"typescript": ">=5.0.0"
|
|
55
|
+
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"typescript": {
|
|
58
|
+
"optional": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=18.0.0"
|
|
63
|
+
}
|
|
64
|
+
}
|