@safekb/mcp 0.13.0 → 0.14.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 +4 -0
- package/dist/safekb-mcp.js +108 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -377,6 +377,10 @@ custom fields and the key name of every custom field (sealed ones too) stay read
|
|
|
377
377
|
sealed credentials start with the tenant default (Settings → Sealed credentials; `use_only` until
|
|
378
378
|
set). Only you make anything more visible, in the SafeKB app; `set_sealed_credential_llm_visibility`
|
|
379
379
|
only tightens. The client enforces this: a client older than 0.11.0 reveals every sealed credential, and 0.11.0 ignores field overrides.
|
|
380
|
+
- Entering a secret without the agent seeing it: `insert_sealed_credential` and
|
|
381
|
+
`update_sealed_credential` take `typed_at_sidecar`, e.g. `["custom:number", "custom:cvc", "password"]`.
|
|
382
|
+
The call waits while you type each field at the sidecar (`safekb-mcp --sidecar`), hidden and twice;
|
|
383
|
+
the agent passes only the visible fields. Or enter it in the SafeKB web app.
|
|
380
384
|
- Using a secret without showing it: SafeKB only provides the value. `secret_exec` runs your own
|
|
381
385
|
program with it in env, stdin or a temp file (`files`, mode 0600, deleted afterwards), never argv;
|
|
382
386
|
every integration (HTTP API, browser form, `adb shell input text`, ssh, wallet, `cryptsetup`, …) is
|
package/dist/safekb-mcp.js
CHANGED
|
@@ -2581,7 +2581,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
2581
2581
|
// package.json
|
|
2582
2582
|
var package_default = {
|
|
2583
2583
|
name: "@safekb/mcp",
|
|
2584
|
-
version: "0.
|
|
2584
|
+
version: "0.14.0",
|
|
2585
2585
|
description: "SafeKB local MCP client: proxies the hosted SafeKB tools and adds zero-knowledge sealed documents.",
|
|
2586
2586
|
license: "SEE LICENSE IN LICENSE",
|
|
2587
2587
|
author: "Mustafa Öztürk",
|
|
@@ -9895,6 +9895,47 @@ class CredentialMigration {
|
|
|
9895
9895
|
}
|
|
9896
9896
|
}
|
|
9897
9897
|
|
|
9898
|
+
// src/sealed/typedFields.ts
|
|
9899
|
+
var NAMED = ["password", "totp", "sealed_notes"];
|
|
9900
|
+
var CUSTOM_PREFIX3 = customFieldName("");
|
|
9901
|
+
var NOTHING_TYPED = { named: {}, custom: [] };
|
|
9902
|
+
function checked(fields2, passed) {
|
|
9903
|
+
if (new Set(fields2).size !== fields2.length) {
|
|
9904
|
+
throw new ClientError("typed_at_sidecar names a field twice");
|
|
9905
|
+
}
|
|
9906
|
+
for (const field of fields2) {
|
|
9907
|
+
if (NAMED.includes(field)) {
|
|
9908
|
+
if (passed[field] !== undefined) {
|
|
9909
|
+
throw new ClientError(`${field} is both passed and typed_at_sidecar: pass it one way only`);
|
|
9910
|
+
}
|
|
9911
|
+
} else if (field.startsWith(CUSTOM_PREFIX3) && field.length > CUSTOM_PREFIX3.length) {
|
|
9912
|
+
const key = field.slice(CUSTOM_PREFIX3.length);
|
|
9913
|
+
if (passed.custom?.some((item) => item.key === key)) {
|
|
9914
|
+
throw new ClientError(`custom field '${key}' is both in custom and typed_at_sidecar: pass it one way only`);
|
|
9915
|
+
}
|
|
9916
|
+
} else {
|
|
9917
|
+
throw new ClientError(`typed_at_sidecar: '${field}' is not a sealed field. Use password, totp, sealed_notes or custom:{key}.`);
|
|
9918
|
+
}
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
async function typeAtSidecar(entry, credential2, fields2, passed) {
|
|
9922
|
+
checked(fields2, passed);
|
|
9923
|
+
const typed = { named: {}, custom: [] };
|
|
9924
|
+
for (const field of fields2) {
|
|
9925
|
+
const value = await entry.secret(credential2, field);
|
|
9926
|
+
if (field.startsWith(CUSTOM_PREFIX3)) {
|
|
9927
|
+
typed.custom.push({ key: field.slice(CUSTOM_PREFIX3.length), value });
|
|
9928
|
+
} else {
|
|
9929
|
+
typed.named[field] = value;
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
return typed;
|
|
9933
|
+
}
|
|
9934
|
+
function withTypedCustom(sealed, typed) {
|
|
9935
|
+
const keys = new Set(typed.custom.map(({ key }) => key));
|
|
9936
|
+
return [...sealed.filter(({ key }) => !keys.has(key)), ...typed.custom];
|
|
9937
|
+
}
|
|
9938
|
+
|
|
9898
9939
|
// src/sealed/credentialService.ts
|
|
9899
9940
|
var NOT_FOUND2 = 404;
|
|
9900
9941
|
var VISIBLE_NAMES = [
|
|
@@ -10006,6 +10047,7 @@ class CredentialService {
|
|
|
10006
10047
|
const id = newObjectId();
|
|
10007
10048
|
const custom = splitCustom(args.custom ?? []);
|
|
10008
10049
|
const { dek, encDek } = await this.access.newDek(args.root_id, id);
|
|
10050
|
+
const typed = await this.typed(args.title, args);
|
|
10009
10051
|
const save = await this.access.seal(id, dek, {
|
|
10010
10052
|
title: args.title,
|
|
10011
10053
|
username: textOrNull(args.username, null),
|
|
@@ -10014,10 +10056,10 @@ class CredentialService {
|
|
|
10014
10056
|
notes: textOrNull(args.notes, null),
|
|
10015
10057
|
custom: custom.visible
|
|
10016
10058
|
}, {
|
|
10017
|
-
password: textOrNull(args.password, null),
|
|
10018
|
-
totp: this.checkedTotp(textOrNull(args.totp, null)),
|
|
10019
|
-
sealed_notes: textOrNull(args.sealed_notes, null),
|
|
10020
|
-
custom: custom.sealed
|
|
10059
|
+
password: typed.named.password ?? textOrNull(args.password, null),
|
|
10060
|
+
totp: this.checkedTotp(typed.named.totp ?? textOrNull(args.totp, null)),
|
|
10061
|
+
sealed_notes: typed.named.sealed_notes ?? textOrNull(args.sealed_notes, null),
|
|
10062
|
+
custom: withTypedCustom(custom.sealed, typed)
|
|
10021
10063
|
});
|
|
10022
10064
|
const sigs = await this.access.sign({
|
|
10023
10065
|
op: "credential_create",
|
|
@@ -10051,6 +10093,7 @@ class CredentialService {
|
|
|
10051
10093
|
throw new ClientError(`${attestation.warning} ${describeChanges(attestation.changed_visible_fields, current)} ` + "Show this to the user. Only after they have reviewed it, repeat with accept_visible_changes: true (and pass the right values for any field to correct).");
|
|
10052
10094
|
}
|
|
10053
10095
|
const custom = args.custom === undefined ? { visible: current.custom, sealed: values.custom } : splitCustom(args.custom);
|
|
10096
|
+
const typed = await this.typed(args.title ?? current.title, args);
|
|
10054
10097
|
const save = await this.access.seal(credential2.id, dek, {
|
|
10055
10098
|
title: args.title ?? current.title,
|
|
10056
10099
|
username: textOrNull(args.username, current.username),
|
|
@@ -10059,10 +10102,10 @@ class CredentialService {
|
|
|
10059
10102
|
notes: textOrNull(args.notes, current.notes),
|
|
10060
10103
|
custom: custom.visible
|
|
10061
10104
|
}, {
|
|
10062
|
-
password: textOrNull(args.password, values.password),
|
|
10063
|
-
totp: this.checkedTotp(textOrNull(args.totp, values.totp)),
|
|
10064
|
-
sealed_notes: textOrNull(args.sealed_notes, values.sealed_notes),
|
|
10065
|
-
custom: custom.sealed
|
|
10105
|
+
password: typed.named.password ?? textOrNull(args.password, values.password),
|
|
10106
|
+
totp: this.checkedTotp(typed.named.totp ?? textOrNull(args.totp, values.totp)),
|
|
10107
|
+
sealed_notes: typed.named.sealed_notes ?? textOrNull(args.sealed_notes, values.sealed_notes),
|
|
10108
|
+
custom: withTypedCustom(custom.sealed, typed)
|
|
10066
10109
|
});
|
|
10067
10110
|
const sigs = await this.access.sign({
|
|
10068
10111
|
op: "credential_update",
|
|
@@ -10120,6 +10163,10 @@ class CredentialService {
|
|
|
10120
10163
|
});
|
|
10121
10164
|
return { id: credentialId, deleted: true };
|
|
10122
10165
|
}
|
|
10166
|
+
typed(title, args) {
|
|
10167
|
+
const atSidecar = args.typed_at_sidecar;
|
|
10168
|
+
return atSidecar === undefined || atSidecar.fields.length === 0 ? Promise.resolve(NOTHING_TYPED) : typeAtSidecar(atSidecar.entry, title, atSidecar.fields, args);
|
|
10169
|
+
}
|
|
10123
10170
|
checkedTotp(value) {
|
|
10124
10171
|
if (value !== null)
|
|
10125
10172
|
parseTotp(value);
|
|
@@ -31313,7 +31360,7 @@ function decodeDirective(html) {
|
|
|
31313
31360
|
|
|
31314
31361
|
// ../shared/js/canvas-markdown/src/html.ts
|
|
31315
31362
|
var ENTITY = /&(#x[0-9a-fA-F]+|#[0-9]+|amp|lt|gt|quot|apos);/g;
|
|
31316
|
-
var
|
|
31363
|
+
var NAMED2 = {
|
|
31317
31364
|
amp: "&",
|
|
31318
31365
|
lt: "<",
|
|
31319
31366
|
gt: ">",
|
|
@@ -31329,7 +31376,7 @@ function unescapeHtmlText(text2) {
|
|
|
31329
31376
|
return String.fromCodePoint(Number.parseInt(body.slice(2), 16));
|
|
31330
31377
|
if (body.startsWith("#"))
|
|
31331
31378
|
return String.fromCodePoint(Number(body.slice(1)));
|
|
31332
|
-
return
|
|
31379
|
+
return NAMED2[body];
|
|
31333
31380
|
});
|
|
31334
31381
|
}
|
|
31335
31382
|
|
|
@@ -31660,12 +31707,12 @@ class BlockParser {
|
|
|
31660
31707
|
}
|
|
31661
31708
|
item(taskable) {
|
|
31662
31709
|
this.pos++;
|
|
31663
|
-
const
|
|
31710
|
+
const checked2 = taskable ? this.takeTaskMarker() : null;
|
|
31664
31711
|
const children = this.blocks((token) => token.type === "list_item_close");
|
|
31665
31712
|
this.pos++;
|
|
31666
31713
|
return {
|
|
31667
|
-
task:
|
|
31668
|
-
checked:
|
|
31714
|
+
task: checked2 !== null,
|
|
31715
|
+
checked: checked2 === true,
|
|
31669
31716
|
children: children.length > 0 ? children : [node("paragraph")]
|
|
31670
31717
|
};
|
|
31671
31718
|
}
|
|
@@ -34474,7 +34521,7 @@ import { connect } from "node:net";
|
|
|
34474
34521
|
|
|
34475
34522
|
// src/sidecar/protocol.ts
|
|
34476
34523
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
34477
|
-
var SIDECAR_PROTOCOL_VERSION =
|
|
34524
|
+
var SIDECAR_PROTOCOL_VERSION = 3;
|
|
34478
34525
|
var SIDECAR_SOCKET_NAME = "sidecar.sock";
|
|
34479
34526
|
var SIDECAR_SUPPORTED = process.platform !== "win32";
|
|
34480
34527
|
function defaultSidecarSocket(configPath) {
|
|
@@ -34619,6 +34666,10 @@ class SidecarQuestions {
|
|
|
34619
34666
|
const { passphrase } = await this.asking("The passphrase", () => this.client.request({ op: "passphrase", of }));
|
|
34620
34667
|
return passphrase;
|
|
34621
34668
|
}
|
|
34669
|
+
async secret(credential2, field2) {
|
|
34670
|
+
const { value } = await this.asking(`The value of ${field2}`, () => this.client.request({ op: "secret", credential: credential2, field: field2 }));
|
|
34671
|
+
return value;
|
|
34672
|
+
}
|
|
34622
34673
|
async ask(notice, question) {
|
|
34623
34674
|
const { typed: typed2 } = await this.asking("The confirmation", () => this.client.request({ op: "confirm", notice, question }));
|
|
34624
34675
|
return typed2;
|
|
@@ -34648,6 +34699,9 @@ class NoSidecarQuestions {
|
|
|
34648
34699
|
ask() {
|
|
34649
34700
|
return this.refuse();
|
|
34650
34701
|
}
|
|
34702
|
+
secret() {
|
|
34703
|
+
return Promise.reject(new ClientError("Typing a secret at the sidecar is not available on this platform. Ask the user to enter it in the SafeKB web app (Credentials)."));
|
|
34704
|
+
}
|
|
34651
34705
|
refuse() {
|
|
34652
34706
|
return Promise.reject(new ClientError(`This needs the user to type at the sidecar, which is not available on this platform. Ask them to run it at a terminal: ${this.commandLine}`));
|
|
34653
34707
|
}
|
|
@@ -34721,6 +34775,15 @@ The MCP client asks for the passphrase of ${printable2(of)}. It is used for this
|
|
|
34721
34775
|
return typed2.length === 0 ? null : typed2;
|
|
34722
34776
|
});
|
|
34723
34777
|
}
|
|
34778
|
+
secret({ credential: credential2, field: field2 }, signal) {
|
|
34779
|
+
return this.inTurn(async () => {
|
|
34780
|
+
signal.throwIfAborted();
|
|
34781
|
+
this.reader.write(`
|
|
34782
|
+
The MCP client asks for ${printable2(field2)} of credential "${printable2(credential2)}". It is sealed on this machine; the model never sees it.
|
|
34783
|
+
`);
|
|
34784
|
+
return this.readTwice(`${printable2(field2)} (not shown; empty to decline): `, signal);
|
|
34785
|
+
});
|
|
34786
|
+
}
|
|
34724
34787
|
typed(notice, question, signal) {
|
|
34725
34788
|
return this.inTurn(async () => {
|
|
34726
34789
|
signal.throwIfAborted();
|
|
@@ -34852,9 +34915,12 @@ ${printableLines(notice)}`);
|
|
|
34852
34915
|
` : `Forgotten.
|
|
34853
34916
|
`);
|
|
34854
34917
|
}
|
|
34855
|
-
|
|
34918
|
+
readNewPassphrase(signal) {
|
|
34919
|
+
return this.readTwice("New passphrase (not shown; empty to decline): ", signal);
|
|
34920
|
+
}
|
|
34921
|
+
async readTwice(prompt, signal) {
|
|
34856
34922
|
for (;; ) {
|
|
34857
|
-
const first = await this.reader.read(
|
|
34923
|
+
const first = await this.reader.read(prompt, { hidden: true, signal });
|
|
34858
34924
|
if (first.length === 0)
|
|
34859
34925
|
return null;
|
|
34860
34926
|
const second = await this.reader.read("Repeat it: ", {
|
|
@@ -35118,6 +35184,8 @@ class SidecarServer {
|
|
|
35118
35184
|
return {};
|
|
35119
35185
|
case "passphrase":
|
|
35120
35186
|
return { passphrase: await this.passphrase(request, clientGone) };
|
|
35187
|
+
case "secret":
|
|
35188
|
+
return { value: await this.secret(request, clientGone) };
|
|
35121
35189
|
case "confirm":
|
|
35122
35190
|
return {
|
|
35123
35191
|
typed: await this.prompter.typed(requireText(request.notice, "notice"), requireText(request.question, "question"), clientGone)
|
|
@@ -35144,6 +35212,16 @@ class SidecarServer {
|
|
|
35144
35212
|
}
|
|
35145
35213
|
return typed2;
|
|
35146
35214
|
}
|
|
35215
|
+
async secret(request, clientGone) {
|
|
35216
|
+
const credential2 = requireText(request.credential, "credential");
|
|
35217
|
+
const field2 = requireText(request.field, "field");
|
|
35218
|
+
const typed2 = await this.prompter.secret({ credential: credential2, field: field2 }, clientGone);
|
|
35219
|
+
clientGone.throwIfAborted();
|
|
35220
|
+
if (typed2 === null) {
|
|
35221
|
+
throw new ClientError(`The sidecar's user entered no ${field2} for credential "${credential2}"`);
|
|
35222
|
+
}
|
|
35223
|
+
return typed2;
|
|
35224
|
+
}
|
|
35147
35225
|
async create(rootId, label2, clientGone) {
|
|
35148
35226
|
const chosen = await this.prompter.newSecret(rootId, label2, clientGone);
|
|
35149
35227
|
clientGone.throwIfAborted();
|
|
@@ -36097,13 +36175,15 @@ var sealedFields = {
|
|
|
36097
36175
|
password: exports_external.string().describe(`Sealed. ${CLEARABLE}`).optional(),
|
|
36098
36176
|
totp: exports_external.string().describe(`Sealed TOTP seed (base32) or otpauth://totp URI. ${CLEARABLE}`).optional(),
|
|
36099
36177
|
sealed_notes: exports_external.string().describe(`Sealed notes. ${CLEARABLE}`).optional(),
|
|
36178
|
+
typed_at_sidecar: exports_external.array(exports_external.string().min(1)).describe(`Sealed fields the user types at the sidecar instead of you passing them: password, totp, sealed_notes or custom:{key} (a new or replaced sealed custom field). The call waits while they type each one, hidden and twice. ${SIDECAR_TYPING} Use this for any secret the user has not already given you, e.g. a card number, CVC or PIN; set the visible fields (title, holder name, last 4 digits) yourself.`).optional(),
|
|
36100
36179
|
custom: exports_external.array(exports_external.strictObject({
|
|
36101
36180
|
key: exports_external.string().min(1).describe("Field name. Always stored in clear, also for a sealed field: never put a secret in it."),
|
|
36102
36181
|
value: exports_external.string(),
|
|
36103
36182
|
sealed: exports_external.boolean().describe("true: the value is encrypted on this machine (the key name is not). false: visible to the server.")
|
|
36104
36183
|
})).describe("The complete list of custom fields; it replaces the stored list.").optional()
|
|
36105
36184
|
};
|
|
36106
|
-
function credentialTools(credentials, migration, names) {
|
|
36185
|
+
function credentialTools(credentials, migration, names, entry) {
|
|
36186
|
+
const atSidecar = (fields2) => fields2 === undefined ? undefined : { fields: fields2, entry };
|
|
36107
36187
|
return [
|
|
36108
36188
|
defineTool("reveal_sealed_credential", `Decrypt one sealed field of a sealed credential on this machine. The server only sees that the ciphertext was fetched. Find credentials and their visible fields with the standard credential tools (search_credentials, get_credential). Refused unless its llm_visibility is visible. ${EXPOSURE_NOTE}`, {
|
|
36109
36189
|
credential_id: credentialId,
|
|
@@ -36123,7 +36203,8 @@ function credentialTools(credentials, migration, names) {
|
|
|
36123
36203
|
...sealedFields
|
|
36124
36204
|
}, async (args) => credentials.insert({
|
|
36125
36205
|
...args,
|
|
36126
|
-
root_id: await names.resolve(args.root_id)
|
|
36206
|
+
root_id: await names.resolve(args.root_id),
|
|
36207
|
+
typed_at_sidecar: atSidecar(args.typed_at_sidecar)
|
|
36127
36208
|
})),
|
|
36128
36209
|
defineTool("update_sealed_credential", `Change a sealed credential with its seal root's signature. Pass only the fields to change; the save also attests the visible fields as they are now. If visible fields were changed without the key since the last sealed save, the call fails and names them until accept_visible_changes is true. ${SECRET_ARGUMENT_NOTE}`, {
|
|
36129
36210
|
credential_id: credentialId,
|
|
@@ -36132,7 +36213,10 @@ function credentialTools(credentials, migration, names) {
|
|
|
36132
36213
|
title: exports_external.string().min(1).describe("Visible to the server.").optional(),
|
|
36133
36214
|
...visibleFields,
|
|
36134
36215
|
...sealedFields
|
|
36135
|
-
}, (args) => credentials.update(
|
|
36216
|
+
}, (args) => credentials.update({
|
|
36217
|
+
...args,
|
|
36218
|
+
typed_at_sidecar: atSidecar(args.typed_at_sidecar)
|
|
36219
|
+
})),
|
|
36136
36220
|
defineTool("move_sealed_credential", "Move a sealed credential to another seal root of its space. Both roots must be openable here: its key is re-wrapped under the new root and both roots sign. Afterwards only the new root's secret opens it. Whoever held the old root's secret keeps what they already read.", {
|
|
36137
36221
|
credential_id: credentialId,
|
|
36138
36222
|
expected_version: expectedVersion,
|
|
@@ -36335,11 +36419,12 @@ The keys of this machine, and backups, are handled by tools that never take or r
|
|
|
36335
36419
|
|
|
36336
36420
|
A credential with \`sealed: true\` belongs to a seal root. Its title, username, url, tags, notes and visible custom fields are readable by the server: find it with the standard credential tools. Its password, TOTP seed, sealed_notes and sealed custom fields open only here: reveal_sealed_credential, get_sealed_totp, insert_sealed_credential, update_sealed_credential.
|
|
36337
36421
|
|
|
36422
|
+
- A secret the user has not given you yet (a card number, a PIN, a new password): never ask for it in the conversation. Call insert_sealed_credential or update_sealed_credential with typed_at_sidecar naming the fields; the user types them at the sidecar (safekb-mcp --sidecar) while the call waits. Or they enter it in the SafeKB web app.
|
|
36338
36423
|
- Reveal a secret only when the task needs it, and prefer get_sealed_totp over revealing a TOTP seed.
|
|
36339
36424
|
- llm_visibility is set per credential, and llm_field_visibility overrides it for single sealed fields. reveal_sealed_credential and get_sealed_totp work only on a field that is visible. Only the user makes anything visible, in the SafeKB app.
|
|
36340
36425
|
- To use a secret without seeing it (visible or use_only fields): secret_exec runs your own program with it in env, stdin or a temp file; every integration (HTTP, browser, adb, ssh, ...) is that program. Name it by handle, {{secret://<credential_id>/<field>}}; a totp handle becomes the current code. Prefer this over revealing, even for a visible field. A human_only field is never used by a tool: ask the user to enter it.
|
|
36341
36426
|
- visible_tampered means the visible fields were changed without the key since the last sealed save. Do not trust the url or username of such a credential until the user has reviewed the change.`;
|
|
36342
|
-
function createSealedTools(ctx) {
|
|
36427
|
+
function createSealedTools(ctx, entry) {
|
|
36343
36428
|
const services = createSealedServices(ctx);
|
|
36344
36429
|
const { names } = ctx;
|
|
36345
36430
|
return [
|
|
@@ -36348,7 +36433,7 @@ function createSealedTools(ctx) {
|
|
|
36348
36433
|
...treeTools(services.tree, names),
|
|
36349
36434
|
...attachmentTools(services.attachments),
|
|
36350
36435
|
...depositTools(services.deposits, names),
|
|
36351
|
-
...credentialTools(services.credentials, services.migration, names),
|
|
36436
|
+
...credentialTools(services.credentials, services.migration, names, entry),
|
|
36352
36437
|
...sinkTools(new ExecSink(services.secrets))
|
|
36353
36438
|
];
|
|
36354
36439
|
}
|
|
@@ -36694,7 +36779,7 @@ async function openTools(options, config3, session, systemKeyring, chosen) {
|
|
|
36694
36779
|
};
|
|
36695
36780
|
const conversions = createConversions(sealed, new KbApi(http), sharedCanvasMarkdown);
|
|
36696
36781
|
const localTools = [
|
|
36697
|
-
...createSealedTools(sealed),
|
|
36782
|
+
...createSealedTools(sealed, userQuestionsOf(options)("")),
|
|
36698
36783
|
...unsealTools(conversions.unseal),
|
|
36699
36784
|
...options.sealedOnly ? [] : sealTools(conversions.seal),
|
|
36700
36785
|
...await adminTools({ options, config: config3, systemKeyring, chosen })
|
package/package.json
CHANGED