@kammcs/buildit-mcp 0.2.0 → 0.8.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/CHANGELOG.md +149 -0
- package/README.md +273 -32
- package/SECURITY.md +1 -1
- package/dist/api/client.js +5 -1
- package/dist/api/generated/operations.js +840 -11
- package/dist/api/generated/schemas.js +936 -3
- package/dist/api/generated/strict.js +1088 -3
- package/dist/capabilities.js +236 -0
- package/dist/channel/api.js +159 -0
- package/dist/channel/approvals.js +259 -0
- package/dist/channel/handoffs.js +190 -0
- package/dist/channel/item-events.js +164 -0
- package/dist/channel/lock.js +181 -0
- package/dist/channel/loop.js +1034 -0
- package/dist/channel/mask.js +365 -0
- package/dist/channel/threads.js +60 -0
- package/dist/channel/tool-ring.js +124 -0
- package/dist/channel/typing.js +150 -0
- package/dist/channel/waiters.js +229 -0
- package/dist/channel/workers.js +66 -0
- package/dist/cli/claude-config.js +100 -0
- package/dist/cli/commands.js +438 -0
- package/dist/config.js +356 -6
- package/dist/errors.js +49 -1
- package/dist/index.js +122 -6
- package/dist/log.js +3 -2
- package/dist/secrets/keychain.js +273 -0
- package/dist/server.js +61 -5
- package/dist/tools/approve.js +791 -0
- package/dist/tools/ask.js +898 -0
- package/dist/tools/chat.js +98 -1
- package/dist/tools/handoff.js +409 -0
- package/dist/tools/items-read.js +6 -3
- package/dist/tools/items-write.js +77 -16
- package/dist/tools/plans.js +221 -14
- package/dist/tools/reply.js +138 -0
- package/dist/tools/shared.js +24 -1
- package/dist/tools/threads.js +149 -0
- package/dist/tools/whoami.js +53 -3
- package/dist/toolsets/catalog.js +3 -1
- package/dist/toolsets/registry.js +2 -0
- package/dist/transports/stdio.js +288 -12
- package/package.json +5 -1
package/dist/transports/stdio.js
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import { isJSONRPCRequest, } from '@modelcontextprotocol/server';
|
|
1
|
+
import { isJSONRPCNotification, isJSONRPCRequest, PROTOCOL_VERSION_META_KEY, ProtocolErrorCode, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/server';
|
|
2
2
|
import { serveStdio, StdioServerTransport, } from '@modelcontextprotocol/server/stdio';
|
|
3
3
|
import { ApiClient } from '../api/client.js';
|
|
4
|
+
import { CHANNEL_NOTIFICATION, ChannelLoop, ChannelState, } from '../channel/loop.js';
|
|
4
5
|
import { invalidatesIdentity } from '../identity.js';
|
|
5
6
|
import { createSwitchableMcpServer, DEGRADED_LIST_TTL_MS, listedByPolicy, resolveListedTools, identityFromApi, } from '../server.js';
|
|
7
|
+
import { PREEMPT_WAIT_MS } from '../channel/lock.js';
|
|
8
|
+
import { TypingIndicator } from '../channel/typing.js';
|
|
9
|
+
import { ToolApprovals } from '../channel/approvals.js';
|
|
10
|
+
import { APPROVAL_HOOK_TOOLS, createApprovalHookTools, READ_ONLY_SAFE_HOOKS, createApproveTool, } from '../tools/approve.js';
|
|
11
|
+
import { createAskTool } from '../tools/ask.js';
|
|
12
|
+
import { createReplyTool, createTurnEndedTool } from '../tools/reply.js';
|
|
13
|
+
import { createListThreadsTool } from '../tools/threads.js';
|
|
14
|
+
import { createHandoffTools } from '../tools/handoff.js';
|
|
15
|
+
import { suggestedCapabilities } from '../capabilities.js';
|
|
6
16
|
import { CATALOG } from '../toolsets/catalog.js';
|
|
7
17
|
import { compareVersions, SERVER_VERSION } from '../version.js';
|
|
18
|
+
/**
|
|
19
|
+
* The plugin's AskUserQuestion and PermissionRequest hooks' timeout, in
|
|
20
|
+
* seconds (plugin/hooks/hooks.json). A card's wait ends a minute before it,
|
|
21
|
+
* so the answer arrives in time.
|
|
22
|
+
*/
|
|
23
|
+
export const HOOK_TIMEOUT_S = 3600;
|
|
24
|
+
/** How long a hook's card waits: the configured minutes, a minute under the hook's timeout at most. */
|
|
25
|
+
export function hookWaitSeconds(minutes) {
|
|
26
|
+
return Math.min(minutes * 60, HOOK_TIMEOUT_S - 60);
|
|
27
|
+
}
|
|
8
28
|
/** A known identity is read again at most this often. */
|
|
9
29
|
export const IDENTITY_REFRESH_MS = 60_000;
|
|
10
30
|
/** Methods whose answer depends on the token's scopes: they wait for a due refresh. */
|
|
@@ -111,13 +131,17 @@ export class StdioIdentity {
|
|
|
111
131
|
class GatedTransport {
|
|
112
132
|
inner;
|
|
113
133
|
before;
|
|
134
|
+
closed;
|
|
114
135
|
onclose;
|
|
115
136
|
onerror;
|
|
116
137
|
onmessage;
|
|
117
138
|
queue = Promise.resolve();
|
|
118
|
-
constructor(inner, before
|
|
139
|
+
constructor(inner, before,
|
|
140
|
+
/** Told when the connection closes (stdin ended, or close()). */
|
|
141
|
+
closed = () => undefined) {
|
|
119
142
|
this.inner = inner;
|
|
120
143
|
this.before = before;
|
|
144
|
+
this.closed = closed;
|
|
121
145
|
}
|
|
122
146
|
start() {
|
|
123
147
|
this.inner.onmessage = (message, extra) => {
|
|
@@ -129,6 +153,71 @@ class GatedTransport {
|
|
|
129
153
|
});
|
|
130
154
|
};
|
|
131
155
|
this.inner.onerror = (error) => this.onerror?.(error);
|
|
156
|
+
this.inner.onclose = () => {
|
|
157
|
+
this.closed();
|
|
158
|
+
this.onclose?.();
|
|
159
|
+
};
|
|
160
|
+
return this.inner.start();
|
|
161
|
+
}
|
|
162
|
+
send(message, options) {
|
|
163
|
+
return this.inner.send(message, options);
|
|
164
|
+
}
|
|
165
|
+
close() {
|
|
166
|
+
return this.inner.close();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Whether a message claims a protocol revision in its _meta (the 2026-07-28 way of opening). */
|
|
170
|
+
function claimsRevision(message) {
|
|
171
|
+
if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message))
|
|
172
|
+
return false;
|
|
173
|
+
const params = message.params;
|
|
174
|
+
return params?._meta?.[PROTOCOL_VERSION_META_KEY] !== undefined;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Keeps a connection on the 2025 handshake (channel mode): until
|
|
178
|
+
* `initialize`, a request that claims a revision is answered with the
|
|
179
|
+
* unsupported-version error naming only the 2025-era revisions, which makes
|
|
180
|
+
* a client fall back to `initialize`; such a notification is dropped.
|
|
181
|
+
*/
|
|
182
|
+
export class LegacyOnlyTransport {
|
|
183
|
+
inner;
|
|
184
|
+
onclose;
|
|
185
|
+
onerror;
|
|
186
|
+
onmessage;
|
|
187
|
+
initialized = false;
|
|
188
|
+
constructor(inner) {
|
|
189
|
+
this.inner = inner;
|
|
190
|
+
}
|
|
191
|
+
start() {
|
|
192
|
+
this.inner.onmessage = (message, extra) => {
|
|
193
|
+
if (!this.initialized && claimsRevision(message)) {
|
|
194
|
+
if (isJSONRPCRequest(message)) {
|
|
195
|
+
const meta = message.params._meta ?? {};
|
|
196
|
+
const requested = meta[PROTOCOL_VERSION_META_KEY];
|
|
197
|
+
void this.inner
|
|
198
|
+
.send({
|
|
199
|
+
jsonrpc: '2.0',
|
|
200
|
+
id: message.id,
|
|
201
|
+
error: {
|
|
202
|
+
code: ProtocolErrorCode.UnsupportedProtocolVersion,
|
|
203
|
+
message: 'Unsupported protocol version: this channel server speaks the 2025 handshake.',
|
|
204
|
+
data: {
|
|
205
|
+
supported: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
206
|
+
requested: typeof requested === 'string' ? requested : 'unknown',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
.catch((error) => {
|
|
211
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (isJSONRPCRequest(message) && message.method === 'initialize')
|
|
217
|
+
this.initialized = true;
|
|
218
|
+
this.onmessage?.(message, extra);
|
|
219
|
+
};
|
|
220
|
+
this.inner.onerror = (error) => this.onerror?.(error);
|
|
132
221
|
this.inner.onclose = () => this.onclose?.();
|
|
133
222
|
return this.inner.start();
|
|
134
223
|
}
|
|
@@ -139,6 +228,68 @@ class GatedTransport {
|
|
|
139
228
|
return this.inner.close();
|
|
140
229
|
}
|
|
141
230
|
}
|
|
231
|
+
/** The tools that post in a thread and stop its "is writing" themselves. */
|
|
232
|
+
const POSTING_TOOLS = new Set(['reply', 'ask', 'turn_ended', 'list_threads']);
|
|
233
|
+
/**
|
|
234
|
+
* A tool call is activity for "is writing": for the thread it names (its
|
|
235
|
+
* thread_id or card_thread, or an item's key that the session works on),
|
|
236
|
+
* else for the only working thread. A call that works on an active item's
|
|
237
|
+
* thread (a worker reading or changing its item) shows "is writing" there
|
|
238
|
+
* again; the posting tools stop it themselves.
|
|
239
|
+
*/
|
|
240
|
+
function noteActivity(state, tool, args) {
|
|
241
|
+
const typing = state.typing;
|
|
242
|
+
if (typing === undefined)
|
|
243
|
+
return;
|
|
244
|
+
// The plugin's approval hooks: tool_seen counts the activity itself.
|
|
245
|
+
if (APPROVAL_HOOK_TOOLS.has(tool))
|
|
246
|
+
return;
|
|
247
|
+
const a = (args !== null && typeof args === 'object' ? args : {});
|
|
248
|
+
const str = (v) => typeof v === 'string' && v !== '' ? v : undefined;
|
|
249
|
+
const ref = str(a.thread_id) ?? str(a.card_thread) ?? str(a.item);
|
|
250
|
+
const key = ref?.startsWith('#') ? ref.slice(1) : ref;
|
|
251
|
+
const thread = key === undefined
|
|
252
|
+
? undefined
|
|
253
|
+
: state.activeItems.has(key)
|
|
254
|
+
? key
|
|
255
|
+
: state.items.get(key.toUpperCase());
|
|
256
|
+
if (thread === undefined || !state.activeItems.has(thread)) {
|
|
257
|
+
typing.activity(key !== undefined && thread === undefined ? key : undefined);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (POSTING_TOOLS.has(tool))
|
|
261
|
+
typing.activity(thread);
|
|
262
|
+
else
|
|
263
|
+
typing.work(thread);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The tools only channel mode has (reply, ask, list_threads, and the
|
|
267
|
+
* plugin's hooks: turn_ended, approve and the hooks that end approvals),
|
|
268
|
+
* under the operator's read-only and exclude rules.
|
|
269
|
+
*/
|
|
270
|
+
function channelTools(policy, state, ask, approve) {
|
|
271
|
+
return [
|
|
272
|
+
createReplyTool(state),
|
|
273
|
+
createAskTool(state, ask),
|
|
274
|
+
createListThreadsTool(state),
|
|
275
|
+
createTurnEndedTool(state),
|
|
276
|
+
createApproveTool(state, approve),
|
|
277
|
+
...createApprovalHookTools(state),
|
|
278
|
+
...createHandoffTools(state),
|
|
279
|
+
].filter((tool) => (!policy.readOnly || tool.annotations.readOnlyHint || READ_ONLY_SAFE_HOOKS.has(tool.name)) &&
|
|
280
|
+
!policy.excludeTools.includes(tool.name));
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* The channel tools this server's API serves: a tool that needs operations
|
|
284
|
+
* the API's /v1/meta leaves out is not listed (when the API doesn't say,
|
|
285
|
+
* every tool is, and an older one says so when called).
|
|
286
|
+
*/
|
|
287
|
+
function servedTools(api, tools) {
|
|
288
|
+
const served = api.served?.operations;
|
|
289
|
+
if (served === undefined)
|
|
290
|
+
return [...tools];
|
|
291
|
+
return tools.filter((t) => (t.requiresOperations ?? []).every((op) => served.has(op)));
|
|
292
|
+
}
|
|
142
293
|
export function startStdio(config, logger, options = {}) {
|
|
143
294
|
if (config.token === undefined)
|
|
144
295
|
throw new Error('stdio mode needs a token');
|
|
@@ -159,14 +310,49 @@ export function startStdio(config, logger, options = {}) {
|
|
|
159
310
|
invalidations.listener();
|
|
160
311
|
},
|
|
161
312
|
});
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
313
|
+
const channel = config.channel !== undefined;
|
|
314
|
+
const state = new ChannelState();
|
|
315
|
+
state.isChannel = options.channelOwner ?? true;
|
|
316
|
+
state.folder = config.channel?.folder;
|
|
317
|
+
if (channel && state.isChannel) {
|
|
318
|
+
state.typing = new TypingIndicator({
|
|
319
|
+
api,
|
|
320
|
+
logger: logger.child({ component: 'typing' }),
|
|
321
|
+
...options.typing,
|
|
322
|
+
});
|
|
323
|
+
state.approvals = new ToolApprovals({
|
|
324
|
+
logger: logger.child({ component: 'approvals' }),
|
|
325
|
+
journal: options.approvalJournal,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
const extraTools = channel
|
|
329
|
+
? channelTools(policy, state, {
|
|
330
|
+
questionsInApp: config.ask.questionsInApp,
|
|
331
|
+
hookWaitS: hookWaitSeconds(config.ask.timeoutMinutes),
|
|
332
|
+
...options.ask,
|
|
333
|
+
}, {
|
|
334
|
+
approvalsInApp: config.approvals.inApp,
|
|
335
|
+
hookWaitS: hookWaitSeconds(config.approvals.timeoutMinutes),
|
|
336
|
+
...options.approve,
|
|
337
|
+
})
|
|
338
|
+
: [];
|
|
339
|
+
// Once a channel's session has ended, the listing stays as it last was
|
|
340
|
+
// (without reply), so Claude keeps its tools, which then say why.
|
|
341
|
+
let lastKnown;
|
|
342
|
+
const isEnded = () => state.ended !== undefined;
|
|
343
|
+
const frozen = () => {
|
|
344
|
+
const base = lastKnown ?? {
|
|
345
|
+
...listedByPolicy(catalog, policy),
|
|
346
|
+
identity: 'unverified',
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
...base,
|
|
350
|
+
tools: base.tools.filter((t) => !extraTools.includes(t) || t.runsAfterSessionEnd === true),
|
|
351
|
+
};
|
|
165
352
|
};
|
|
166
|
-
// Read the
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
void api
|
|
353
|
+
// Read what the API serves at startup: it warns when the API needs a newer server, and
|
|
354
|
+
// channel tools newer than the server (the handoffs) are left out of the listing.
|
|
355
|
+
const metaRead = api
|
|
170
356
|
.getMeta()
|
|
171
357
|
.then((meta) => {
|
|
172
358
|
if (meta.min_mcp_version && compareVersions(SERVER_VERSION, meta.min_mcp_version) < 0) {
|
|
@@ -177,27 +363,117 @@ export function startStdio(config, logger, options = {}) {
|
|
|
177
363
|
}
|
|
178
364
|
})
|
|
179
365
|
.catch(() => undefined);
|
|
366
|
+
const identity = new StdioIdentity(async () => {
|
|
367
|
+
if (isEnded())
|
|
368
|
+
return frozen();
|
|
369
|
+
const listed = await resolveListedTools(identityFromApi(api), catalog, policy, logger);
|
|
370
|
+
if (extraTools.length > 0)
|
|
371
|
+
await metaRead;
|
|
372
|
+
const served = servedTools(api, extraTools);
|
|
373
|
+
const shown = served.length > 0 ? { ...listed, tools: [...listed.tools, ...served] } : listed;
|
|
374
|
+
if (shown.identity === 'known')
|
|
375
|
+
lastKnown = shown;
|
|
376
|
+
// The session may have ended while the identity was read.
|
|
377
|
+
return isEnded() ? frozen() : shown;
|
|
378
|
+
}, logger, options.now);
|
|
379
|
+
invalidations.listener = () => {
|
|
380
|
+
identity.invalidate();
|
|
381
|
+
};
|
|
382
|
+
// Read the identity at startup.
|
|
383
|
+
void identity.refresh();
|
|
180
384
|
// Everything the configuration allows is registered; the identity decides what is shown.
|
|
181
385
|
const allowed = listedByPolicy(catalog, policy);
|
|
182
386
|
const wire = options.stdin && options.stdout
|
|
183
387
|
? new StdioServerTransport(options.stdin, options.stdout)
|
|
184
388
|
: new StdioServerTransport();
|
|
185
|
-
|
|
389
|
+
let loop;
|
|
390
|
+
const stopLoop = () => loop?.stop() ?? Promise.resolve();
|
|
391
|
+
const handle = serveStdio(async () => {
|
|
186
392
|
const listed = await identity.refresh();
|
|
187
393
|
const switchable = createSwitchableMcpServer({
|
|
188
|
-
|
|
394
|
+
// What /v1/meta said is known by now (refresh waited for it): the instructions leave out
|
|
395
|
+
// the channel tools this API doesn't serve.
|
|
396
|
+
tools: [...allowed.tools, ...servedTools(api, extraTools)],
|
|
189
397
|
resources: allowed.resources,
|
|
190
398
|
prompts: allowed.prompts,
|
|
191
399
|
api,
|
|
192
400
|
logger,
|
|
401
|
+
channel,
|
|
402
|
+
...(channel
|
|
403
|
+
? {
|
|
404
|
+
channelState: state,
|
|
405
|
+
...(config.channel?.fromPlugin === true
|
|
406
|
+
? { lead: { maxParallelItems: config.ask.maxParallelItems } }
|
|
407
|
+
: {}),
|
|
408
|
+
onToolCall: (tool, args) => {
|
|
409
|
+
noteActivity(state, tool, args);
|
|
410
|
+
},
|
|
411
|
+
sessionEnded: () => state.ended,
|
|
412
|
+
onSessionOver: (code) => {
|
|
413
|
+
void loop?.end(code);
|
|
414
|
+
},
|
|
415
|
+
}
|
|
416
|
+
: {}),
|
|
193
417
|
...(listed.identity === 'known' ? {} : { toolsListTtlMs: DEGRADED_LIST_TTL_MS }),
|
|
194
418
|
});
|
|
195
419
|
identity.attach(switchable);
|
|
420
|
+
if (channel) {
|
|
421
|
+
const mcp = switchable.server.server;
|
|
422
|
+
// Notifications sent before the client has initialized would be lost.
|
|
423
|
+
mcp.oninitialized = () => {
|
|
424
|
+
if (loop !== undefined)
|
|
425
|
+
return;
|
|
426
|
+
if (!state.isChannel) {
|
|
427
|
+
logger.warn('another buildit server in this folder receives the messages from buildIt.Social (it holds the channel lock); this one serves its tools without the channel. Remove the extra server (claude mcp remove <name>; "buildit-mcp status" names them)');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
loop = new ChannelLoop({
|
|
431
|
+
api,
|
|
432
|
+
logger: logger.child({ component: 'channel' }),
|
|
433
|
+
state,
|
|
434
|
+
notify: (params) => mcp.notification({ method: CHANNEL_NOTIFICATION, params }),
|
|
435
|
+
// What this computer can do, as tag names only (BUILDIT_CAPABILITIES replaces it).
|
|
436
|
+
capabilities: () => suggestedCapabilities(config.channel?.capabilities),
|
|
437
|
+
...(options.channelLock === undefined
|
|
438
|
+
? {}
|
|
439
|
+
: {
|
|
440
|
+
owns: () => options.channelLock?.owns() ?? true,
|
|
441
|
+
...(options.channelLock.preempted === undefined
|
|
442
|
+
? {}
|
|
443
|
+
: { firstPollDelayMs: options.preemptWaitMs ?? PREEMPT_WAIT_MS }),
|
|
444
|
+
}),
|
|
445
|
+
onEnded: async (reason) => {
|
|
446
|
+
if (state.ended !== undefined) {
|
|
447
|
+
// The server stays up: Claude keeps its tools (which now say the session
|
|
448
|
+
// ended), without reply.
|
|
449
|
+
switchable.show(frozen());
|
|
450
|
+
try {
|
|
451
|
+
switchable.server.sendToolListChanged();
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// A client that is gone can't be told.
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
await options.onSessionEnded?.(reason);
|
|
458
|
+
},
|
|
459
|
+
...options.channelLoop,
|
|
460
|
+
});
|
|
461
|
+
void loop.start();
|
|
462
|
+
};
|
|
463
|
+
}
|
|
196
464
|
return switchable.server;
|
|
197
465
|
}, {
|
|
198
466
|
onerror: (error) => {
|
|
199
467
|
logger.error('stdio transport error', { error });
|
|
200
468
|
},
|
|
201
|
-
transport: new GatedTransport(wire, (message) => identity.beforeMessage(message))
|
|
469
|
+
transport: new GatedTransport(channel ? new LegacyOnlyTransport(wire) : wire, (message) => identity.beforeMessage(message), () => {
|
|
470
|
+
void stopLoop();
|
|
471
|
+
}),
|
|
202
472
|
});
|
|
473
|
+
return {
|
|
474
|
+
async close() {
|
|
475
|
+
await stopLoop();
|
|
476
|
+
await handle.close();
|
|
477
|
+
},
|
|
478
|
+
};
|
|
203
479
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kammcs/buildit-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "MCP server for buildIt.Social: lets AI agents work with an org's projects, pages and channels through the buildIt.Social agent API, with a personal access token.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kamm Creative Solutions",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"inspect:docker": "npm run build && docker build -t buildit-mcp:inspect . && tsx scripts/inspect.ts --image buildit-mcp:inspect",
|
|
47
47
|
"image:publish": "tsx scripts/publish-image.ts",
|
|
48
48
|
"bundle": "npm run build && tsx scripts/bundle.ts",
|
|
49
|
+
"dev-marketplace": "tsx scripts/dev-marketplace.ts",
|
|
49
50
|
"conformance": "npm run build && tsx scripts/conformance.ts",
|
|
50
51
|
"generate": "tsx scripts/generate.ts",
|
|
51
52
|
"privacy": "tsx scripts/privacy.ts",
|
|
@@ -73,5 +74,8 @@
|
|
|
73
74
|
"typescript": "6.0.3",
|
|
74
75
|
"typescript-eslint": "8.71.1",
|
|
75
76
|
"vitest": "5.0.3"
|
|
77
|
+
},
|
|
78
|
+
"optionalDependencies": {
|
|
79
|
+
"@napi-rs/keyring": "2.1.1"
|
|
76
80
|
}
|
|
77
81
|
}
|