@tuskydp/cli 0.2.1 → 0.4.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 +39 -0
- package/dist/src/commands/account.d.ts.map +1 -1
- package/dist/src/commands/account.js +0 -1
- package/dist/src/commands/account.js.map +1 -1
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +8 -5
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/download.d.ts +1 -0
- package/dist/src/commands/download.d.ts.map +1 -1
- package/dist/src/commands/download.js +35 -22
- package/dist/src/commands/download.js.map +1 -1
- package/dist/src/commands/export.d.ts +9 -24
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +31 -59
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/files.d.ts.map +1 -1
- package/dist/src/commands/files.js +91 -12
- package/dist/src/commands/files.js.map +1 -1
- package/dist/src/commands/folder.d.ts +3 -0
- package/dist/src/commands/folder.d.ts.map +1 -0
- package/dist/src/commands/folder.js +151 -0
- package/dist/src/commands/folder.js.map +1 -0
- package/dist/src/commands/mcp.d.ts.map +1 -1
- package/dist/src/commands/mcp.js +15 -9
- package/dist/src/commands/mcp.js.map +1 -1
- package/dist/src/commands/rehydrate.d.ts +1 -0
- package/dist/src/commands/rehydrate.d.ts.map +1 -1
- package/dist/src/commands/rehydrate.js +15 -7
- package/dist/src/commands/rehydrate.js.map +1 -1
- package/dist/src/commands/sui.d.ts +3 -0
- package/dist/src/commands/sui.d.ts.map +1 -0
- package/dist/src/commands/sui.js +64 -0
- package/dist/src/commands/sui.js.map +1 -0
- package/dist/src/commands/trash.d.ts +3 -0
- package/dist/src/commands/trash.d.ts.map +1 -0
- package/dist/src/commands/trash.js +109 -0
- package/dist/src/commands/trash.js.map +1 -0
- package/dist/src/commands/upload.d.ts +4 -0
- package/dist/src/commands/upload.d.ts.map +1 -1
- package/dist/src/commands/upload.js +82 -27
- package/dist/src/commands/upload.js.map +1 -1
- package/dist/src/commands/vault.d.ts.map +1 -1
- package/dist/src/commands/vault.js +2 -24
- package/dist/src/commands/vault.js.map +1 -1
- package/dist/src/commands/wallet.d.ts +3 -0
- package/dist/src/commands/wallet.d.ts.map +1 -0
- package/dist/src/commands/wallet.js +126 -0
- package/dist/src/commands/wallet.js.map +1 -0
- package/dist/src/commands/webhook.d.ts +3 -0
- package/dist/src/commands/webhook.d.ts.map +1 -0
- package/dist/src/commands/webhook.js +172 -0
- package/dist/src/commands/webhook.js.map +1 -0
- package/dist/src/config.d.ts +2 -2
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +2 -3
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.js +19 -9
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/resolve.d.ts.map +1 -1
- package/dist/src/lib/resolve.js +4 -5
- package/dist/src/lib/resolve.js.map +1 -1
- package/dist/src/mcp/context.d.ts +1 -9
- package/dist/src/mcp/context.d.ts.map +1 -1
- package/dist/src/mcp/context.js +1 -2
- package/dist/src/mcp/context.js.map +1 -1
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +2 -59
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools/account.d.ts.map +1 -1
- package/dist/src/mcp/tools/account.js +1 -3
- package/dist/src/mcp/tools/account.js.map +1 -1
- package/dist/src/mcp/tools/files.d.ts +2 -3
- package/dist/src/mcp/tools/files.d.ts.map +1 -1
- package/dist/src/mcp/tools/files.js +46 -49
- package/dist/src/mcp/tools/files.js.map +1 -1
- package/dist/src/mcp/tools/vaults.js +2 -2
- package/dist/src/mcp/tools/vaults.js.map +1 -1
- package/dist/src/seal.d.ts +16 -0
- package/dist/src/seal.d.ts.map +1 -1
- package/dist/src/seal.js +23 -0
- package/dist/src/seal.js.map +1 -1
- package/dist/src/tui/files-panel.d.ts +31 -2
- package/dist/src/tui/files-panel.d.ts.map +1 -1
- package/dist/src/tui/files-panel.js +119 -13
- package/dist/src/tui/files-panel.js.map +1 -1
- package/dist/src/tui/index.d.ts.map +1 -1
- package/dist/src/tui/index.js +252 -48
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/overview.d.ts.map +1 -1
- package/dist/src/tui/overview.js +21 -9
- package/dist/src/tui/overview.js.map +1 -1
- package/dist/src/tui/trash-screen.d.ts +4 -0
- package/dist/src/tui/trash-screen.d.ts.map +1 -0
- package/dist/src/tui/trash-screen.js +190 -0
- package/dist/src/tui/trash-screen.js.map +1 -0
- package/dist/src/tui/vaults-panel.d.ts +8 -0
- package/dist/src/tui/vaults-panel.d.ts.map +1 -1
- package/dist/src/tui/vaults-panel.js +45 -6
- package/dist/src/tui/vaults-panel.js.map +1 -1
- package/dist/src/version.d.ts +2 -0
- package/dist/src/version.d.ts.map +1 -0
- package/dist/src/version.js +21 -0
- package/dist/src/version.js.map +1 -0
- package/package.json +3 -3
- package/src/__tests__/seal.test.ts +7 -54
- package/src/commands/account.ts +0 -1
- package/src/commands/auth.ts +7 -5
- package/src/commands/download.ts +38 -28
- package/src/commands/export.ts +37 -81
- package/src/commands/files.ts +95 -11
- package/src/commands/folder.ts +169 -0
- package/src/commands/mcp.ts +16 -10
- package/src/commands/rehydrate.ts +15 -8
- package/src/commands/sui.ts +69 -0
- package/src/commands/trash.ts +121 -0
- package/src/commands/upload.ts +98 -31
- package/src/commands/vault.ts +2 -23
- package/src/commands/wallet.ts +183 -0
- package/src/commands/webhook.ts +193 -0
- package/src/config.ts +3 -4
- package/src/index.ts +19 -10
- package/src/lib/resolve.ts +3 -4
- package/src/mcp/context.ts +1 -11
- package/src/mcp/server.ts +2 -70
- package/src/mcp/tools/account.ts +1 -3
- package/src/mcp/tools/files.ts +50 -63
- package/src/mcp/tools/vaults.ts +3 -3
- package/src/seal.ts +34 -1
- package/src/tui/files-panel.ts +140 -14
- package/src/tui/index.ts +264 -52
- package/src/tui/overview.ts +20 -9
- package/src/tui/trash-screen.ts +203 -0
- package/src/tui/vaults-panel.ts +55 -6
- package/src/version.ts +21 -0
- package/vitest.config.ts +1 -0
- package/dist/src/client.d.ts +0 -120
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js +0 -152
- package/dist/src/client.js.map +0 -1
- package/dist/src/commands/decrypt.d.ts +0 -15
- package/dist/src/commands/decrypt.d.ts.map +0 -1
- package/dist/src/commands/decrypt.js +0 -224
- package/dist/src/commands/decrypt.js.map +0 -1
- package/dist/src/commands/encryption.d.ts +0 -3
- package/dist/src/commands/encryption.d.ts.map +0 -1
- package/dist/src/commands/encryption.js +0 -254
- package/dist/src/commands/encryption.js.map +0 -1
- package/dist/src/crypto.d.ts +0 -16
- package/dist/src/crypto.d.ts.map +0 -1
- package/dist/src/crypto.js +0 -95
- package/dist/src/crypto.js.map +0 -1
- package/dist/src/lib/keyring.d.ts +0 -4
- package/dist/src/lib/keyring.d.ts.map +0 -1
- package/dist/src/lib/keyring.js +0 -49
- package/dist/src/lib/keyring.js.map +0 -1
- package/src/__tests__/crypto.test.ts +0 -315
- package/src/commands/decrypt.ts +0 -276
- package/src/commands/encryption.ts +0 -305
- package/src/crypto.ts +0 -130
- package/src/lib/keyring.ts +0 -50
package/src/mcp/tools/files.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP tools — File operations
|
|
3
3
|
*
|
|
4
|
-
* Handles upload with
|
|
5
|
-
*
|
|
6
|
-
* rehydration polling for cold files.
|
|
4
|
+
* Handles upload with SEAL encryption for shared vaults, download
|
|
5
|
+
* with decryption and rehydration polling for cold files.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { z } from 'zod';
|
|
@@ -13,8 +12,7 @@ import { basename, resolve, join } from 'path';
|
|
|
13
12
|
import { lookup } from 'mime-types';
|
|
14
13
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
14
|
import type { McpContext } from '../context.js';
|
|
16
|
-
import {
|
|
17
|
-
import { isSealConfigured, sealEncrypt, sealDecrypt } from '../../seal.js';
|
|
15
|
+
import { isSealConfigured, sealEncrypt, sealDecrypt, sealEncryptMetadata, sealDecryptMetadata } from '../../seal.js';
|
|
18
16
|
import { wrapToolError } from './helpers.js';
|
|
19
17
|
|
|
20
18
|
// ---------------------------------------------------------------------------
|
|
@@ -79,10 +77,9 @@ async function fetchAndDecryptFile(fileId: string, ctx: McpContext) {
|
|
|
79
77
|
const arrayBuf = await response.arrayBuffer();
|
|
80
78
|
let fileBuffer: Buffer = Buffer.from(arrayBuf as ArrayBuffer);
|
|
81
79
|
|
|
82
|
-
// Decrypt if needed —
|
|
80
|
+
// Decrypt if needed — SEAL-encrypted (shared vault)
|
|
83
81
|
if (encryption?.encrypted) {
|
|
84
82
|
if ('type' in encryption && encryption.type === 'seal') {
|
|
85
|
-
// SEAL-encrypted (shared vault)
|
|
86
83
|
const keypair = ctx.getSuiKeypair();
|
|
87
84
|
if (!keypair) {
|
|
88
85
|
throw new Error(
|
|
@@ -94,23 +91,6 @@ async function fetchAndDecryptFile(fileId: string, ctx: McpContext) {
|
|
|
94
91
|
const vault = await ctx.sdk.vaults.get(fileInfo.vaultId);
|
|
95
92
|
const decrypted = await sealDecrypt(new Uint8Array(fileBuffer), vault, keypair);
|
|
96
93
|
fileBuffer = Buffer.from(decrypted);
|
|
97
|
-
} else {
|
|
98
|
-
// Passphrase-encrypted (private vault)
|
|
99
|
-
const masterKey = ctx.getMasterKey();
|
|
100
|
-
if (!masterKey) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
'Encryption passphrase required to decrypt this file. ' +
|
|
103
|
-
'Set the TUSKYDP_PASSWORD environment variable in your MCP server config.',
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
const enc = encryption as { type: 'passphrase'; encrypted: true; wrappedKey: string; iv: string; plaintextChecksumSha256: string | null };
|
|
107
|
-
fileBuffer = decryptBuffer(
|
|
108
|
-
fileBuffer,
|
|
109
|
-
enc.wrappedKey,
|
|
110
|
-
enc.iv,
|
|
111
|
-
masterKey,
|
|
112
|
-
enc.plaintextChecksumSha256 ?? undefined,
|
|
113
|
-
);
|
|
114
94
|
}
|
|
115
95
|
}
|
|
116
96
|
|
|
@@ -118,7 +98,33 @@ async function fetchAndDecryptFile(fileId: string, ctx: McpContext) {
|
|
|
118
98
|
}
|
|
119
99
|
|
|
120
100
|
/**
|
|
121
|
-
*
|
|
101
|
+
* Resolve the real filename from a FileStatusResponse.
|
|
102
|
+
* Decrypts encryptedMetadata (shared vaults) when present.
|
|
103
|
+
* Falls back to the stored placeholder name on failure.
|
|
104
|
+
*/
|
|
105
|
+
async function resolveFilename(
|
|
106
|
+
fileInfo: { name: string; nameEncrypted?: boolean; encryptedMetadata?: string | null; fileId?: string },
|
|
107
|
+
ctx: McpContext,
|
|
108
|
+
): Promise<string> {
|
|
109
|
+
if (!fileInfo.nameEncrypted) return fileInfo.name;
|
|
110
|
+
try {
|
|
111
|
+
if (fileInfo.encryptedMetadata && fileInfo.fileId) {
|
|
112
|
+
const keypair = ctx.getSuiKeypair();
|
|
113
|
+
if (keypair) {
|
|
114
|
+
const fileObj = await ctx.sdk.files.get(fileInfo.fileId);
|
|
115
|
+
const vault = await ctx.sdk.vaults.get(fileObj.vaultId);
|
|
116
|
+
const meta = await sealDecryptMetadata(fileInfo.encryptedMetadata, vault, keypair);
|
|
117
|
+
return meta.n;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
// Fall through to placeholder
|
|
122
|
+
}
|
|
123
|
+
return fileInfo.name;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Encrypt (if shared vault) and upload a buffer to Tusky.
|
|
122
128
|
* Shared by tusky_file_upload (from disk) and tusky_file_create (from content).
|
|
123
129
|
*/
|
|
124
130
|
async function encryptAndUpload(
|
|
@@ -130,37 +136,16 @@ async function encryptAndUpload(
|
|
|
130
136
|
ctx: McpContext,
|
|
131
137
|
) {
|
|
132
138
|
const vault = await ctx.sdk.vaults.get(vaultId);
|
|
133
|
-
const isPrivate = vault.visibility === 'private';
|
|
134
139
|
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
135
140
|
|
|
136
141
|
let uploadBody: Buffer;
|
|
137
142
|
let encryptionMeta: {
|
|
138
|
-
wrappedKey?: string;
|
|
139
|
-
encryptionIv?: string;
|
|
140
|
-
plaintextSizeBytes?: number;
|
|
141
|
-
plaintextChecksumSha256?: string;
|
|
142
143
|
sealIdentity?: string;
|
|
143
144
|
sealEncryptedObject?: string;
|
|
145
|
+
encryptedMetadata?: string;
|
|
144
146
|
} = {};
|
|
145
147
|
|
|
146
|
-
if (
|
|
147
|
-
const masterKey = ctx.getMasterKey();
|
|
148
|
-
if (!masterKey) {
|
|
149
|
-
throw new Error(
|
|
150
|
-
'Encryption passphrase required for private vault uploads. ' +
|
|
151
|
-
'Set the TUSKYDP_PASSWORD environment variable in your MCP server config.',
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
|
|
156
|
-
uploadBody = ciphertext;
|
|
157
|
-
encryptionMeta = {
|
|
158
|
-
wrappedKey,
|
|
159
|
-
encryptionIv: iv,
|
|
160
|
-
plaintextSizeBytes: fileBuffer.length,
|
|
161
|
-
plaintextChecksumSha256: plaintextChecksum,
|
|
162
|
-
};
|
|
163
|
-
} else if (isShared) {
|
|
148
|
+
if (isShared) {
|
|
164
149
|
const keypair = ctx.getSuiKeypair();
|
|
165
150
|
if (!keypair) {
|
|
166
151
|
throw new Error(
|
|
@@ -175,7 +160,7 @@ async function encryptAndUpload(
|
|
|
175
160
|
encryptionMeta = {
|
|
176
161
|
sealIdentity: sealResult.sealIdentity,
|
|
177
162
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
178
|
-
|
|
163
|
+
encryptedMetadata: await sealEncryptMetadata(fileName, mimeType, vault, fileNonce),
|
|
179
164
|
};
|
|
180
165
|
} else {
|
|
181
166
|
uploadBody = fileBuffer;
|
|
@@ -205,7 +190,7 @@ async function encryptAndUpload(
|
|
|
205
190
|
// Confirm upload
|
|
206
191
|
const file = await ctx.sdk.files.confirmUpload(fileId);
|
|
207
192
|
|
|
208
|
-
return { file,
|
|
193
|
+
return { file, isShared, uploadedSizeBytes: uploadBody.length };
|
|
209
194
|
}
|
|
210
195
|
|
|
211
196
|
// ---------------------------------------------------------------------------
|
|
@@ -216,7 +201,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
216
201
|
// ── Upload file from disk ──────────────────────────────────────────────
|
|
217
202
|
server.tool(
|
|
218
203
|
'tusky_file_upload',
|
|
219
|
-
'Upload a file from a local filesystem path to a vault. Handles encryption transparently for
|
|
204
|
+
'Upload a file from a local filesystem path to a vault. Handles SEAL encryption transparently for shared vaults. Use tusky_file_create instead if you want to upload content directly without a file on disk.',
|
|
220
205
|
{
|
|
221
206
|
filePath: z.string().describe('Absolute or relative path to the local file to upload'),
|
|
222
207
|
vaultId: z.string().describe('Target vault ID'),
|
|
@@ -241,7 +226,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
241
226
|
const fileName = basename(resolvedPath);
|
|
242
227
|
const mimeType = lookup(resolvedPath) || 'application/octet-stream';
|
|
243
228
|
|
|
244
|
-
const { file,
|
|
229
|
+
const { file, isShared, uploadedSizeBytes } = await encryptAndUpload(
|
|
245
230
|
fileBuffer, fileName, mimeType, vaultId, folderId, ctx,
|
|
246
231
|
);
|
|
247
232
|
|
|
@@ -249,8 +234,8 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
249
234
|
content: [{ type: 'text' as const, text: JSON.stringify({
|
|
250
235
|
...file,
|
|
251
236
|
localPath: resolvedPath,
|
|
252
|
-
encrypted:
|
|
253
|
-
encryptionType:
|
|
237
|
+
encrypted: isShared,
|
|
238
|
+
encryptionType: isShared ? 'seal' : 'none',
|
|
254
239
|
uploadedSizeBytes,
|
|
255
240
|
}, null, 2) }],
|
|
256
241
|
};
|
|
@@ -263,7 +248,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
263
248
|
// ── Create file from content ─────────────────────────────────────────
|
|
264
249
|
server.tool(
|
|
265
250
|
'tusky_file_create',
|
|
266
|
-
'Create a file in a vault from raw content (text or base64-encoded binary). Use this when you want to upload content directly without needing a file on disk. Handles encryption transparently for
|
|
251
|
+
'Create a file in a vault from raw content (text or base64-encoded binary). Use this when you want to upload content directly without needing a file on disk. Handles SEAL encryption transparently for shared vaults.',
|
|
267
252
|
{
|
|
268
253
|
name: z.string().describe('File name including extension (e.g. "report.md", "data.json", "image.png")'),
|
|
269
254
|
content: z.string().describe('File content: plain text for text files, or base64-encoded string for binary files'),
|
|
@@ -282,15 +267,15 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
282
267
|
|
|
283
268
|
const mimeType = lookup(name) || 'application/octet-stream';
|
|
284
269
|
|
|
285
|
-
const { file,
|
|
270
|
+
const { file, isShared, uploadedSizeBytes } = await encryptAndUpload(
|
|
286
271
|
fileBuffer, name, mimeType, vaultId, folderId, ctx,
|
|
287
272
|
);
|
|
288
273
|
|
|
289
274
|
return {
|
|
290
275
|
content: [{ type: 'text' as const, text: JSON.stringify({
|
|
291
276
|
...file,
|
|
292
|
-
encrypted:
|
|
293
|
-
encryptionType:
|
|
277
|
+
encrypted: isShared,
|
|
278
|
+
encryptionType: isShared ? 'seal' : 'none',
|
|
294
279
|
uploadedSizeBytes,
|
|
295
280
|
}, null, 2) }],
|
|
296
281
|
};
|
|
@@ -303,7 +288,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
303
288
|
// ── Download file (to disk) ────────────────────────────────────────────
|
|
304
289
|
server.tool(
|
|
305
290
|
'tusky_file_download',
|
|
306
|
-
'Download a file by ID and save it to a local path on disk. Handles decryption for
|
|
291
|
+
'Download a file by ID and save it to a local path on disk. Handles SEAL decryption for shared vaults and rehydration for cold files. Use tusky_file_read instead if you just want the file content returned directly.',
|
|
307
292
|
{
|
|
308
293
|
fileId: z.string().describe('File ID to download'),
|
|
309
294
|
outputPath: z.string().describe('Local path to save the file (must be writable)'),
|
|
@@ -313,13 +298,14 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
313
298
|
const { fileBuffer, encryption } = await fetchAndDecryptFile(fileId, ctx);
|
|
314
299
|
|
|
315
300
|
const fileInfo = await ctx.sdk.files.getStatus(fileId);
|
|
301
|
+
const realName = await resolveFilename({ ...fileInfo, fileId }, ctx);
|
|
316
302
|
const finalPath = resolve(outputPath);
|
|
317
303
|
|
|
318
304
|
writeFileSync(finalPath, fileBuffer);
|
|
319
305
|
|
|
320
306
|
const result = {
|
|
321
307
|
fileId,
|
|
322
|
-
name:
|
|
308
|
+
name: realName,
|
|
323
309
|
savedTo: finalPath,
|
|
324
310
|
sizeBytes: fileBuffer.length,
|
|
325
311
|
mimeType: fileInfo.mimeType,
|
|
@@ -338,7 +324,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
338
324
|
// ── Read file content (inline) ───────────────────────────────────────
|
|
339
325
|
server.tool(
|
|
340
326
|
'tusky_file_read',
|
|
341
|
-
'Read a file\'s content and return it directly. For text files the content is returned as text. For binary files it is returned as base64. Handles decryption for
|
|
327
|
+
'Read a file\'s content and return it directly. For text files the content is returned as text. For binary files it is returned as base64. Handles SEAL decryption for shared vaults and rehydration for cold files transparently. Prefer this over tusky_file_download when you need to inspect file content.',
|
|
342
328
|
{
|
|
343
329
|
fileId: z.string().describe('File ID to read'),
|
|
344
330
|
},
|
|
@@ -346,6 +332,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
346
332
|
try {
|
|
347
333
|
const { fileBuffer, encryption } = await fetchAndDecryptFile(fileId, ctx);
|
|
348
334
|
const fileInfo = await ctx.sdk.files.getStatus(fileId);
|
|
335
|
+
const realName = await resolveFilename({ ...fileInfo, fileId }, ctx);
|
|
349
336
|
|
|
350
337
|
const isText = isTextMimeType(fileInfo.mimeType);
|
|
351
338
|
|
|
@@ -354,7 +341,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
354
341
|
content: [
|
|
355
342
|
{
|
|
356
343
|
type: 'text' as const,
|
|
357
|
-
text: `--- ${
|
|
344
|
+
text: `--- ${realName} (${fileInfo.mimeType}, ${fileBuffer.length} bytes${encryption?.encrypted ? ', decrypted' : ''}) ---\n\n${fileBuffer.toString('utf-8')}`,
|
|
358
345
|
},
|
|
359
346
|
],
|
|
360
347
|
};
|
|
@@ -367,7 +354,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
367
354
|
type: 'text' as const,
|
|
368
355
|
text: JSON.stringify({
|
|
369
356
|
fileId,
|
|
370
|
-
name:
|
|
357
|
+
name: realName,
|
|
371
358
|
mimeType: fileInfo.mimeType,
|
|
372
359
|
sizeBytes: fileBuffer.length,
|
|
373
360
|
wasEncrypted: encryption?.encrypted ?? false,
|
package/src/mcp/tools/vaults.ts
CHANGED
|
@@ -15,8 +15,8 @@ export function registerVaultTools(server: McpServer, ctx: McpContext) {
|
|
|
15
15
|
{
|
|
16
16
|
name: z.string().describe('Vault name'),
|
|
17
17
|
description: z.string().optional().describe('Vault description'),
|
|
18
|
-
visibility: z.enum(['public', '
|
|
19
|
-
'Vault visibility. "
|
|
18
|
+
visibility: z.enum(['public', 'shared']).optional().describe(
|
|
19
|
+
'Vault visibility. "shared" uses SEAL protocol for multi-party access. Defaults to "public".',
|
|
20
20
|
),
|
|
21
21
|
},
|
|
22
22
|
async ({ name, description, visibility }) => {
|
|
@@ -24,7 +24,7 @@ export function registerVaultTools(server: McpServer, ctx: McpContext) {
|
|
|
24
24
|
const vault = await ctx.sdk.vaults.create({
|
|
25
25
|
name,
|
|
26
26
|
description,
|
|
27
|
-
visibility: visibility ?? '
|
|
27
|
+
visibility: visibility ?? 'public',
|
|
28
28
|
});
|
|
29
29
|
return {
|
|
30
30
|
content: [{ type: 'text' as const, text: JSON.stringify(vault, null, 2) }],
|
package/src/seal.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
|
18
18
|
import { SuiJsonRpcClient } from '@mysten/sui/jsonRpc';
|
|
19
19
|
import { Transaction } from '@mysten/sui/transactions';
|
|
20
20
|
import { fromHex } from '@mysten/sui/utils';
|
|
21
|
-
import { SEAL_PACKAGE_ID, SEAL_DEFAULT_KEY_SERVER_CONFIGS } from '@tuskydp/shared/constants.js';
|
|
21
|
+
import { SEAL_PACKAGE_ID, SEAL_DEFAULT_KEY_SERVER_CONFIGS, METADATA_NONCE_PREFIX } from '@tuskydp/shared/constants.js';
|
|
22
22
|
import type { VaultResponse } from '@tuskydp/sdk';
|
|
23
23
|
import { getSuiPrivateKey } from './config.js';
|
|
24
24
|
|
|
@@ -193,6 +193,39 @@ export async function sealEncrypt(
|
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Encrypt filename + MIME type using SEAL for a shared vault.
|
|
198
|
+
* Uses a distinct nonce (METADATA_NONCE_PREFIX + fileNonce) so the metadata
|
|
199
|
+
* identity differs from the file content identity.
|
|
200
|
+
*
|
|
201
|
+
* Returns base64-encoded full SEAL EncryptedObject (stored in encrypted_metadata column).
|
|
202
|
+
*/
|
|
203
|
+
export async function sealEncryptMetadata(
|
|
204
|
+
name: string,
|
|
205
|
+
mimeType: string,
|
|
206
|
+
vault: VaultResponse,
|
|
207
|
+
fileNonce: string,
|
|
208
|
+
): Promise<string> {
|
|
209
|
+
const plaintext = new TextEncoder().encode(JSON.stringify({ n: name, m: mimeType }));
|
|
210
|
+
const metaNonce = METADATA_NONCE_PREFIX + fileNonce;
|
|
211
|
+
const result = await sealEncrypt(plaintext, vault, metaNonce);
|
|
212
|
+
return Buffer.from(result.encryptedData).toString('base64');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Decrypt an encryptedMetadata blob produced by sealEncryptMetadata.
|
|
217
|
+
* Returns the original { n: name, m: mimeType }.
|
|
218
|
+
*/
|
|
219
|
+
export async function sealDecryptMetadata(
|
|
220
|
+
encryptedBase64: string,
|
|
221
|
+
vault: VaultResponse,
|
|
222
|
+
keypair: import('@mysten/sui/keypairs/ed25519').Ed25519Keypair,
|
|
223
|
+
): Promise<{ n: string; m: string }> {
|
|
224
|
+
const encryptedData = new Uint8Array(Buffer.from(encryptedBase64, 'base64'));
|
|
225
|
+
const plaintext = await sealDecrypt(encryptedData, vault, keypair);
|
|
226
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
227
|
+
}
|
|
228
|
+
|
|
196
229
|
// ---------------------------------------------------------------------------
|
|
197
230
|
// Decrypt
|
|
198
231
|
// ---------------------------------------------------------------------------
|
package/src/tui/files-panel.ts
CHANGED
|
@@ -2,17 +2,25 @@ import blessed from 'blessed';
|
|
|
2
2
|
import type { TuskyClient } from '@tuskydp/sdk';
|
|
3
3
|
import { formatBytes, formatDate, formatRow, statusColor, statusColorClose, getCurrentTheme } from './helpers.js';
|
|
4
4
|
|
|
5
|
+
export interface FolderItem {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
parentId: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export interface FileItem {
|
|
6
12
|
id: string;
|
|
7
13
|
name: string;
|
|
8
14
|
sizeBytes: number;
|
|
9
|
-
plaintextSizeBytes: number | null;
|
|
10
15
|
mimeType: string;
|
|
11
16
|
status: string;
|
|
12
17
|
createdAt: string;
|
|
13
18
|
walrusBlobId: string | null;
|
|
14
19
|
walrusBlobObjectId: string | null;
|
|
15
20
|
encrypted: boolean;
|
|
21
|
+
folderId: string | null;
|
|
22
|
+
autoRenew: boolean;
|
|
23
|
+
ppuEpochEnd: string | null;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
export class FilesPanel {
|
|
@@ -20,9 +28,15 @@ export class FilesPanel {
|
|
|
20
28
|
private screen: blessed.Widgets.Screen;
|
|
21
29
|
private sdk: TuskyClient;
|
|
22
30
|
private files: FileItem[] = [];
|
|
31
|
+
private folders: FolderItem[] = [];
|
|
32
|
+
/** Combined display items: folders first, then files */
|
|
33
|
+
private displayItems: Array<{ type: 'folder'; folder: FolderItem } | { type: 'file'; file: FileItem } | { type: 'parent' }> = [];
|
|
23
34
|
private loading = false;
|
|
24
35
|
private currentVaultId: string | null = null;
|
|
25
36
|
private currentVaultName = '';
|
|
37
|
+
private currentFolderId: string | null = null;
|
|
38
|
+
private currentFolderName: string | null = null;
|
|
39
|
+
private folderStack: Array<{ id: string | null; name: string | null }> = [];
|
|
26
40
|
|
|
27
41
|
constructor(
|
|
28
42
|
screen: blessed.Widgets.Screen,
|
|
@@ -63,32 +77,77 @@ export class FilesPanel {
|
|
|
63
77
|
});
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
async loadForVault(vaultId: string, vaultName: string) {
|
|
80
|
+
async loadForVault(vaultId: string, vaultName: string, folderId?: string | null) {
|
|
67
81
|
if (this.loading) return;
|
|
68
82
|
this.loading = true;
|
|
69
83
|
this.currentVaultId = vaultId;
|
|
70
84
|
this.currentVaultName = vaultName;
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
|
|
86
|
+
if (folderId !== undefined) {
|
|
87
|
+
this.currentFolderId = folderId;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const folderPath = this.currentFolderName
|
|
91
|
+
? `${vaultName} / ${this.currentFolderName}`
|
|
92
|
+
: vaultName;
|
|
93
|
+
this.list.setLabel(` Files — ${folderPath} `);
|
|
94
|
+
this.list.setItems(['Loading...'] as any);
|
|
73
95
|
this.screen.render();
|
|
74
96
|
|
|
75
97
|
try {
|
|
76
|
-
|
|
77
|
-
|
|
98
|
+
// Load folders and files for current context
|
|
99
|
+
const [foldersResult, filesResult] = await Promise.all([
|
|
100
|
+
this.sdk.folders.list({ vaultId, parentId: this.currentFolderId || undefined }).catch(() => []),
|
|
101
|
+
this.sdk.files.list({
|
|
102
|
+
vaultId,
|
|
103
|
+
folderId: this.currentFolderId || undefined,
|
|
104
|
+
limit: 100,
|
|
105
|
+
sortBy: 'createdAt',
|
|
106
|
+
order: 'desc',
|
|
107
|
+
}),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
this.folders = (foldersResult as any[]).map((f) => ({
|
|
111
|
+
id: f.id,
|
|
112
|
+
name: f.name,
|
|
113
|
+
parentId: f.parentId || null,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
this.files = filesResult.files.map((f) => ({
|
|
78
117
|
id: f.id,
|
|
79
118
|
name: f.name,
|
|
80
119
|
sizeBytes: f.sizeBytes || 0,
|
|
81
|
-
plaintextSizeBytes: f.plaintextSizeBytes || null,
|
|
82
120
|
mimeType: f.mimeType || '',
|
|
83
121
|
status: f.status || 'unknown',
|
|
84
122
|
createdAt: f.createdAt || '',
|
|
85
123
|
walrusBlobId: f.walrusBlobId || null,
|
|
86
124
|
walrusBlobObjectId: f.walrusBlobObjectId || null,
|
|
87
125
|
encrypted: f.encrypted || false,
|
|
126
|
+
folderId: f.folderId || null,
|
|
127
|
+
autoRenew: f.autoRenew || false,
|
|
128
|
+
ppuEpochEnd: f.ppuEpochEnd || null,
|
|
88
129
|
}));
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
// Build combined display list
|
|
132
|
+
this.displayItems = [];
|
|
133
|
+
|
|
134
|
+
// Show ".." to go up if we're in a subfolder
|
|
135
|
+
if (this.currentFolderId) {
|
|
136
|
+
this.displayItems.push({ type: 'parent' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Folders first
|
|
140
|
+
for (const folder of this.folders) {
|
|
141
|
+
this.displayItems.push({ type: 'folder', folder });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Then files
|
|
145
|
+
for (const file of this.files) {
|
|
146
|
+
this.displayItems.push({ type: 'file', file });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.displayItems.length === 0) {
|
|
150
|
+
this.list.setItems(['(empty — press u to upload, f to create folder)'] as any);
|
|
92
151
|
} else {
|
|
93
152
|
const totalW = (this.list.width as number) - 4;
|
|
94
153
|
const sizeW = 9;
|
|
@@ -97,13 +156,37 @@ export class FilesPanel {
|
|
|
97
156
|
const encW = 3;
|
|
98
157
|
const nameW = Math.max(10, totalW - sizeW - statusW - dateW - encW - 4);
|
|
99
158
|
|
|
100
|
-
const items = this.
|
|
101
|
-
|
|
159
|
+
const items = this.displayItems.map((item) => {
|
|
160
|
+
if (item.type === 'parent') {
|
|
161
|
+
return formatRow([
|
|
162
|
+
{ text: '../', tagged: '{yellow-fg}../{/yellow-fg}', width: nameW },
|
|
163
|
+
{ text: '', width: sizeW, align: 'right' },
|
|
164
|
+
{ text: '', width: statusW },
|
|
165
|
+
{ text: '', width: encW },
|
|
166
|
+
{ text: '', width: dateW, align: 'right' },
|
|
167
|
+
], totalW);
|
|
168
|
+
}
|
|
169
|
+
if (item.type === 'folder') {
|
|
170
|
+
const name = item.folder.name;
|
|
171
|
+
const display = name.length > nameW - 2
|
|
172
|
+
? name.slice(0, nameW - 3) + '...'
|
|
173
|
+
: name + '/';
|
|
174
|
+
return formatRow([
|
|
175
|
+
{ text: display, tagged: `{cyan-fg}${display}{/cyan-fg}`, width: nameW },
|
|
176
|
+
{ text: '', width: sizeW, align: 'right' },
|
|
177
|
+
{ text: 'folder', tagged: '{cyan-fg}folder{/cyan-fg}', width: statusW },
|
|
178
|
+
{ text: '', width: encW },
|
|
179
|
+
{ text: '', width: dateW, align: 'right' },
|
|
180
|
+
], totalW);
|
|
181
|
+
}
|
|
182
|
+
// file
|
|
183
|
+
const f = item.file;
|
|
184
|
+
const size = formatBytes(f.sizeBytes);
|
|
102
185
|
const status = f.status;
|
|
103
186
|
const statusTagged = `${statusColor(f.status)}${f.status}${statusColorClose(f.status)}`;
|
|
104
187
|
const date = f.createdAt ? formatDate(f.createdAt) : '';
|
|
105
188
|
const enc = f.encrypted ? '🔒' : '';
|
|
106
|
-
const name = f.name.length > nameW ? f.name.slice(0, nameW - 1) + '
|
|
189
|
+
const name = f.name.length > nameW ? f.name.slice(0, nameW - 1) + '...' : f.name;
|
|
107
190
|
return formatRow([
|
|
108
191
|
{ text: name, width: nameW },
|
|
109
192
|
{ text: size, width: sizeW, align: 'right' },
|
|
@@ -122,18 +205,57 @@ export class FilesPanel {
|
|
|
122
205
|
this.screen.render();
|
|
123
206
|
}
|
|
124
207
|
|
|
208
|
+
/** Navigate into a folder or back to parent */
|
|
209
|
+
async navigateToFolder(folderId: string | null, folderName: string | null) {
|
|
210
|
+
if (!this.currentVaultId) return;
|
|
211
|
+
|
|
212
|
+
// Push current location onto stack before navigating
|
|
213
|
+
this.folderStack.push({ id: this.currentFolderId, name: this.currentFolderName });
|
|
214
|
+
this.currentFolderId = folderId;
|
|
215
|
+
this.currentFolderName = folderName;
|
|
216
|
+
await this.loadForVault(this.currentVaultId, this.currentVaultName);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Navigate back to parent folder */
|
|
220
|
+
async navigateUp() {
|
|
221
|
+
if (!this.currentVaultId || !this.currentFolderId) return;
|
|
222
|
+
|
|
223
|
+
const prev = this.folderStack.pop();
|
|
224
|
+
this.currentFolderId = prev?.id || null;
|
|
225
|
+
this.currentFolderName = prev?.name || null;
|
|
226
|
+
await this.loadForVault(this.currentVaultId, this.currentVaultName);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Reset folder navigation when switching vaults */
|
|
230
|
+
resetFolderNav() {
|
|
231
|
+
this.currentFolderId = null;
|
|
232
|
+
this.currentFolderName = null;
|
|
233
|
+
this.folderStack = [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getSelectedItem(): { type: 'parent' } | { type: 'folder'; folder: FolderItem } | { type: 'file'; file: FileItem } | null {
|
|
237
|
+
const idx = (this.list as any).selected as number;
|
|
238
|
+
return this.displayItems[idx] || null;
|
|
239
|
+
}
|
|
240
|
+
|
|
125
241
|
clear() {
|
|
126
242
|
this.files = [];
|
|
243
|
+
this.folders = [];
|
|
244
|
+
this.displayItems = [];
|
|
127
245
|
this.currentVaultId = null;
|
|
128
246
|
this.currentVaultName = '';
|
|
247
|
+
this.currentFolderId = null;
|
|
248
|
+
this.currentFolderName = null;
|
|
249
|
+
this.folderStack = [];
|
|
129
250
|
this.list.setLabel(' Files ');
|
|
130
251
|
this.list.setItems(['Select a vault'] as any);
|
|
131
252
|
this.screen.render();
|
|
132
253
|
}
|
|
133
254
|
|
|
134
255
|
getSelected(): FileItem | null {
|
|
135
|
-
const
|
|
136
|
-
|
|
256
|
+
const item = this.getSelectedItem();
|
|
257
|
+
if (item && item.type === 'file') return item.file;
|
|
258
|
+
return null;
|
|
137
259
|
}
|
|
138
260
|
|
|
139
261
|
getFiles(): FileItem[] {
|
|
@@ -148,6 +270,10 @@ export class FilesPanel {
|
|
|
148
270
|
return this.currentVaultName;
|
|
149
271
|
}
|
|
150
272
|
|
|
273
|
+
getCurrentFolderId(): string | null {
|
|
274
|
+
return this.currentFolderId;
|
|
275
|
+
}
|
|
276
|
+
|
|
151
277
|
focus() {
|
|
152
278
|
this.list.focus();
|
|
153
279
|
this.list.style.border = { fg: getCurrentTheme().borderFocus } as any;
|