@tuskydp/cli 0.3.0 → 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 +19 -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.map +1 -1
- package/dist/src/commands/download.js +2 -59
- package/dist/src/commands/download.js.map +1 -1
- package/dist/src/commands/export.d.ts +5 -26
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +6 -46
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/files.js +2 -2
- package/dist/src/commands/files.js.map +1 -1
- 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/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.js +1 -1
- package/dist/src/commands/trash.js.map +1 -1
- package/dist/src/commands/upload.d.ts.map +1 -1
- package/dist/src/commands/upload.js +3 -49
- 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/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 +2 -4
- 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 +0 -58
- 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 +18 -56
- 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/tui/files-panel.d.ts +0 -1
- package/dist/src/tui/files-panel.d.ts.map +1 -1
- package/dist/src/tui/files-panel.js +1 -2
- 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 +7 -42
- 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 +2 -6
- package/dist/src/tui/overview.js.map +1 -1
- package/dist/src/tui/trash-screen.js +1 -1
- package/dist/src/tui/trash-screen.js.map +1 -1
- 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 +2 -63
- package/src/commands/export.ts +7 -67
- package/src/commands/files.ts +2 -2
- package/src/commands/mcp.ts +16 -10
- package/src/commands/sui.ts +69 -0
- package/src/commands/trash.ts +1 -1
- package/src/commands/upload.ts +3 -59
- package/src/commands/vault.ts +2 -23
- package/src/config.ts +3 -4
- package/src/index.ts +2 -4
- package/src/lib/resolve.ts +3 -4
- package/src/mcp/context.ts +1 -11
- package/src/mcp/server.ts +0 -69
- package/src/mcp/tools/account.ts +1 -3
- package/src/mcp/tools/files.ts +19 -70
- package/src/mcp/tools/vaults.ts +3 -3
- package/src/tui/files-panel.ts +1 -3
- package/src/tui/index.ts +7 -51
- package/src/tui/overview.ts +2 -5
- package/src/tui/trash-screen.ts +1 -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 -256
- 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 -32
- package/dist/src/crypto.d.ts.map +0 -1
- package/dist/src/crypto.js +0 -121
- 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 -309
- package/src/commands/encryption.ts +0 -305
- package/src/crypto.ts +0 -165
- package/src/lib/keyring.ts +0 -50
package/src/mcp/server.ts
CHANGED
|
@@ -9,12 +9,6 @@
|
|
|
9
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
11
|
import { TuskyClient, TuskyError } from '@tuskydp/sdk';
|
|
12
|
-
import {
|
|
13
|
-
deriveMasterKey,
|
|
14
|
-
verifyPassphrase,
|
|
15
|
-
unwrapMasterKey,
|
|
16
|
-
} from '../crypto.js';
|
|
17
|
-
import { loadMasterKey } from '../lib/keyring.js';
|
|
18
12
|
import { getSuiKeypair } from '../seal.js';
|
|
19
13
|
import type { McpContext } from './context.js';
|
|
20
14
|
|
|
@@ -27,58 +21,6 @@ import { registerTrashTools } from './tools/trash.js';
|
|
|
27
21
|
import { registerSharedVaultTools } from './tools/sharedVaults.js';
|
|
28
22
|
import { CLI_VERSION } from '../version.js';
|
|
29
23
|
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
// Encryption bootstrap
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Attempt to unlock the master key using (in order):
|
|
36
|
-
* 1. TUSKYDP_PASSWORD env var → derive wrapping key → unwrap master key
|
|
37
|
-
* 2. Existing session file (~/.tusky/session.enc)
|
|
38
|
-
*
|
|
39
|
-
* Returns the master key Buffer, or null if neither method works.
|
|
40
|
-
*/
|
|
41
|
-
async function unlockMasterKey(sdk: TuskyClient): Promise<Buffer | null> {
|
|
42
|
-
const password = process.env.TUSKYDP_PASSWORD;
|
|
43
|
-
|
|
44
|
-
if (password) {
|
|
45
|
-
try {
|
|
46
|
-
const params = await sdk.account.getEncryptionParams();
|
|
47
|
-
if (!params.setupComplete) {
|
|
48
|
-
console.error('[tusky-mcp] Encryption not set up on this account. Skipping encryption unlock.');
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const salt = Buffer.from(params.salt!, 'base64');
|
|
53
|
-
const verifier = Buffer.from(params.verifier!, 'base64');
|
|
54
|
-
const wrappingKey = deriveMasterKey(password, salt);
|
|
55
|
-
|
|
56
|
-
if (!verifyPassphrase(wrappingKey, verifier)) {
|
|
57
|
-
console.error('[tusky-mcp] TUSKYDP_PASSWORD is incorrect. Encryption will be unavailable.');
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (params.encryptedMasterKey) {
|
|
62
|
-
return unwrapMasterKey(Buffer.from(params.encryptedMasterKey, 'base64'), wrappingKey);
|
|
63
|
-
}
|
|
64
|
-
// Legacy accounts where wrapping key IS the master key
|
|
65
|
-
return wrappingKey;
|
|
66
|
-
} catch (err: any) {
|
|
67
|
-
console.error(`[tusky-mcp] Failed to unlock encryption: ${err.message}`);
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Fallback: try existing session file
|
|
73
|
-
const sessionKey = loadMasterKey();
|
|
74
|
-
if (sessionKey) {
|
|
75
|
-
console.error('[tusky-mcp] Using encryption key from existing session.');
|
|
76
|
-
return sessionKey;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
24
|
// ---------------------------------------------------------------------------
|
|
83
25
|
// Server lifecycle
|
|
84
26
|
// ---------------------------------------------------------------------------
|
|
@@ -107,15 +49,6 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
|
|
|
107
49
|
process.exit(1);
|
|
108
50
|
}
|
|
109
51
|
|
|
110
|
-
// Unlock encryption (best effort)
|
|
111
|
-
const masterKey = await unlockMasterKey(sdk);
|
|
112
|
-
if (masterKey) {
|
|
113
|
-
console.error('[tusky-mcp] Encryption unlocked — private vault operations are available.');
|
|
114
|
-
} else {
|
|
115
|
-
console.error('[tusky-mcp] Encryption not unlocked — private vault encrypt/decrypt unavailable.');
|
|
116
|
-
console.error('[tusky-mcp] Set TUSKYDP_PASSWORD env var or run `tusky encryption unlock` first.');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
52
|
// Load Sui keypair for SEAL shared vault operations (best effort)
|
|
120
53
|
const suiKeypair = getSuiKeypair();
|
|
121
54
|
if (suiKeypair) {
|
|
@@ -127,8 +60,6 @@ export async function startMcpServer(options: McpServerOptions): Promise<void> {
|
|
|
127
60
|
// Build context
|
|
128
61
|
const ctx: McpContext = {
|
|
129
62
|
sdk,
|
|
130
|
-
getMasterKey: () => masterKey,
|
|
131
|
-
isEncryptionReady: () => masterKey !== null,
|
|
132
63
|
getSuiKeypair: () => suiKeypair,
|
|
133
64
|
isSealReady: () => suiKeypair !== null,
|
|
134
65
|
};
|
package/src/mcp/tools/account.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { wrapToolError } from './helpers.js';
|
|
|
9
9
|
export function registerAccountTools(server: McpServer, ctx: McpContext) {
|
|
10
10
|
server.tool(
|
|
11
11
|
'tusky_account_info',
|
|
12
|
-
'Get account information including email, plan, storage usage
|
|
12
|
+
'Get account information including email, plan, and storage usage',
|
|
13
13
|
{},
|
|
14
14
|
async () => {
|
|
15
15
|
try {
|
|
@@ -24,8 +24,6 @@ export function registerAccountTools(server: McpServer, ctx: McpContext) {
|
|
|
24
24
|
storageLimit: account.storageLimitFormatted,
|
|
25
25
|
storageUsedBytes: account.storageUsedBytes,
|
|
26
26
|
storageLimitBytes: account.storageLimitBytes,
|
|
27
|
-
encryptionSetup: account.encryptionSetupComplete,
|
|
28
|
-
encryptionUnlocked: ctx.isEncryptionReady(),
|
|
29
27
|
createdAt: account.createdAt,
|
|
30
28
|
};
|
|
31
29
|
|
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,7 +12,6 @@ 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 { encryptBuffer, decryptBuffer, encryptMetadata, decryptMetadata } from '../../crypto.js';
|
|
17
15
|
import { isSealConfigured, sealEncrypt, sealDecrypt, sealEncryptMetadata, sealDecryptMetadata } from '../../seal.js';
|
|
18
16
|
import { wrapToolError } from './helpers.js';
|
|
19
17
|
|
|
@@ -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
|
|
|
@@ -119,22 +99,16 @@ async function fetchAndDecryptFile(fileId: string, ctx: McpContext) {
|
|
|
119
99
|
|
|
120
100
|
/**
|
|
121
101
|
* Resolve the real filename from a FileStatusResponse.
|
|
122
|
-
* Decrypts
|
|
102
|
+
* Decrypts encryptedMetadata (shared vaults) when present.
|
|
123
103
|
* Falls back to the stored placeholder name on failure.
|
|
124
104
|
*/
|
|
125
105
|
async function resolveFilename(
|
|
126
|
-
fileInfo: { name: string; nameEncrypted?: boolean;
|
|
106
|
+
fileInfo: { name: string; nameEncrypted?: boolean; encryptedMetadata?: string | null; fileId?: string },
|
|
127
107
|
ctx: McpContext,
|
|
128
108
|
): Promise<string> {
|
|
129
109
|
if (!fileInfo.nameEncrypted) return fileInfo.name;
|
|
130
110
|
try {
|
|
131
|
-
if (fileInfo.
|
|
132
|
-
const masterKey = ctx.getMasterKey();
|
|
133
|
-
if (masterKey) {
|
|
134
|
-
const meta = decryptMetadata(fileInfo.encryptedName, masterKey);
|
|
135
|
-
return meta.n;
|
|
136
|
-
}
|
|
137
|
-
} else if (fileInfo.encryptedMetadata && fileInfo.fileId) {
|
|
111
|
+
if (fileInfo.encryptedMetadata && fileInfo.fileId) {
|
|
138
112
|
const keypair = ctx.getSuiKeypair();
|
|
139
113
|
if (keypair) {
|
|
140
114
|
const fileObj = await ctx.sdk.files.get(fileInfo.fileId);
|
|
@@ -150,7 +124,7 @@ async function resolveFilename(
|
|
|
150
124
|
}
|
|
151
125
|
|
|
152
126
|
/**
|
|
153
|
-
* Encrypt (if
|
|
127
|
+
* Encrypt (if shared vault) and upload a buffer to Tusky.
|
|
154
128
|
* Shared by tusky_file_upload (from disk) and tusky_file_create (from content).
|
|
155
129
|
*/
|
|
156
130
|
async function encryptAndUpload(
|
|
@@ -162,40 +136,16 @@ async function encryptAndUpload(
|
|
|
162
136
|
ctx: McpContext,
|
|
163
137
|
) {
|
|
164
138
|
const vault = await ctx.sdk.vaults.get(vaultId);
|
|
165
|
-
const isPrivate = vault.visibility === 'private';
|
|
166
139
|
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
167
140
|
|
|
168
141
|
let uploadBody: Buffer;
|
|
169
142
|
let encryptionMeta: {
|
|
170
|
-
wrappedKey?: string;
|
|
171
|
-
encryptionIv?: string;
|
|
172
|
-
plaintextSizeBytes?: number;
|
|
173
|
-
plaintextChecksumSha256?: string;
|
|
174
143
|
sealIdentity?: string;
|
|
175
144
|
sealEncryptedObject?: string;
|
|
176
|
-
encryptedName?: string;
|
|
177
145
|
encryptedMetadata?: string;
|
|
178
146
|
} = {};
|
|
179
147
|
|
|
180
|
-
if (
|
|
181
|
-
const masterKey = ctx.getMasterKey();
|
|
182
|
-
if (!masterKey) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
'Encryption passphrase required for private vault uploads. ' +
|
|
185
|
-
'Set the TUSKYDP_PASSWORD environment variable in your MCP server config.',
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
|
|
190
|
-
uploadBody = ciphertext;
|
|
191
|
-
encryptionMeta = {
|
|
192
|
-
wrappedKey,
|
|
193
|
-
encryptionIv: iv,
|
|
194
|
-
plaintextSizeBytes: fileBuffer.length,
|
|
195
|
-
plaintextChecksumSha256: plaintextChecksum,
|
|
196
|
-
encryptedName: encryptMetadata(fileName, mimeType, masterKey),
|
|
197
|
-
};
|
|
198
|
-
} else if (isShared) {
|
|
148
|
+
if (isShared) {
|
|
199
149
|
const keypair = ctx.getSuiKeypair();
|
|
200
150
|
if (!keypair) {
|
|
201
151
|
throw new Error(
|
|
@@ -210,7 +160,6 @@ async function encryptAndUpload(
|
|
|
210
160
|
encryptionMeta = {
|
|
211
161
|
sealIdentity: sealResult.sealIdentity,
|
|
212
162
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
213
|
-
plaintextSizeBytes: fileBuffer.length,
|
|
214
163
|
encryptedMetadata: await sealEncryptMetadata(fileName, mimeType, vault, fileNonce),
|
|
215
164
|
};
|
|
216
165
|
} else {
|
|
@@ -241,7 +190,7 @@ async function encryptAndUpload(
|
|
|
241
190
|
// Confirm upload
|
|
242
191
|
const file = await ctx.sdk.files.confirmUpload(fileId);
|
|
243
192
|
|
|
244
|
-
return { file,
|
|
193
|
+
return { file, isShared, uploadedSizeBytes: uploadBody.length };
|
|
245
194
|
}
|
|
246
195
|
|
|
247
196
|
// ---------------------------------------------------------------------------
|
|
@@ -252,7 +201,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
252
201
|
// ── Upload file from disk ──────────────────────────────────────────────
|
|
253
202
|
server.tool(
|
|
254
203
|
'tusky_file_upload',
|
|
255
|
-
'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.',
|
|
256
205
|
{
|
|
257
206
|
filePath: z.string().describe('Absolute or relative path to the local file to upload'),
|
|
258
207
|
vaultId: z.string().describe('Target vault ID'),
|
|
@@ -277,7 +226,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
277
226
|
const fileName = basename(resolvedPath);
|
|
278
227
|
const mimeType = lookup(resolvedPath) || 'application/octet-stream';
|
|
279
228
|
|
|
280
|
-
const { file,
|
|
229
|
+
const { file, isShared, uploadedSizeBytes } = await encryptAndUpload(
|
|
281
230
|
fileBuffer, fileName, mimeType, vaultId, folderId, ctx,
|
|
282
231
|
);
|
|
283
232
|
|
|
@@ -285,8 +234,8 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
285
234
|
content: [{ type: 'text' as const, text: JSON.stringify({
|
|
286
235
|
...file,
|
|
287
236
|
localPath: resolvedPath,
|
|
288
|
-
encrypted:
|
|
289
|
-
encryptionType:
|
|
237
|
+
encrypted: isShared,
|
|
238
|
+
encryptionType: isShared ? 'seal' : 'none',
|
|
290
239
|
uploadedSizeBytes,
|
|
291
240
|
}, null, 2) }],
|
|
292
241
|
};
|
|
@@ -299,7 +248,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
299
248
|
// ── Create file from content ─────────────────────────────────────────
|
|
300
249
|
server.tool(
|
|
301
250
|
'tusky_file_create',
|
|
302
|
-
'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.',
|
|
303
252
|
{
|
|
304
253
|
name: z.string().describe('File name including extension (e.g. "report.md", "data.json", "image.png")'),
|
|
305
254
|
content: z.string().describe('File content: plain text for text files, or base64-encoded string for binary files'),
|
|
@@ -318,15 +267,15 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
318
267
|
|
|
319
268
|
const mimeType = lookup(name) || 'application/octet-stream';
|
|
320
269
|
|
|
321
|
-
const { file,
|
|
270
|
+
const { file, isShared, uploadedSizeBytes } = await encryptAndUpload(
|
|
322
271
|
fileBuffer, name, mimeType, vaultId, folderId, ctx,
|
|
323
272
|
);
|
|
324
273
|
|
|
325
274
|
return {
|
|
326
275
|
content: [{ type: 'text' as const, text: JSON.stringify({
|
|
327
276
|
...file,
|
|
328
|
-
encrypted:
|
|
329
|
-
encryptionType:
|
|
277
|
+
encrypted: isShared,
|
|
278
|
+
encryptionType: isShared ? 'seal' : 'none',
|
|
330
279
|
uploadedSizeBytes,
|
|
331
280
|
}, null, 2) }],
|
|
332
281
|
};
|
|
@@ -339,7 +288,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
339
288
|
// ── Download file (to disk) ────────────────────────────────────────────
|
|
340
289
|
server.tool(
|
|
341
290
|
'tusky_file_download',
|
|
342
|
-
'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.',
|
|
343
292
|
{
|
|
344
293
|
fileId: z.string().describe('File ID to download'),
|
|
345
294
|
outputPath: z.string().describe('Local path to save the file (must be writable)'),
|
|
@@ -375,7 +324,7 @@ export function registerFileTools(server: McpServer, ctx: McpContext) {
|
|
|
375
324
|
// ── Read file content (inline) ───────────────────────────────────────
|
|
376
325
|
server.tool(
|
|
377
326
|
'tusky_file_read',
|
|
378
|
-
'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.',
|
|
379
328
|
{
|
|
380
329
|
fileId: z.string().describe('File ID to read'),
|
|
381
330
|
},
|
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/tui/files-panel.ts
CHANGED
|
@@ -12,7 +12,6 @@ export interface FileItem {
|
|
|
12
12
|
id: string;
|
|
13
13
|
name: string;
|
|
14
14
|
sizeBytes: number;
|
|
15
|
-
plaintextSizeBytes: number | null;
|
|
16
15
|
mimeType: string;
|
|
17
16
|
status: string;
|
|
18
17
|
createdAt: string;
|
|
@@ -118,7 +117,6 @@ export class FilesPanel {
|
|
|
118
117
|
id: f.id,
|
|
119
118
|
name: f.name,
|
|
120
119
|
sizeBytes: f.sizeBytes || 0,
|
|
121
|
-
plaintextSizeBytes: f.plaintextSizeBytes || null,
|
|
122
120
|
mimeType: f.mimeType || '',
|
|
123
121
|
status: f.status || 'unknown',
|
|
124
122
|
createdAt: f.createdAt || '',
|
|
@@ -183,7 +181,7 @@ export class FilesPanel {
|
|
|
183
181
|
}
|
|
184
182
|
// file
|
|
185
183
|
const f = item.file;
|
|
186
|
-
const size = formatBytes(f.
|
|
184
|
+
const size = formatBytes(f.sizeBytes);
|
|
187
185
|
const status = f.status;
|
|
188
186
|
const statusTagged = `${statusColor(f.status)}${f.status}${statusColorClose(f.status)}`;
|
|
189
187
|
const date = f.createdAt ? formatDate(f.createdAt) : '';
|
package/src/tui/index.ts
CHANGED
|
@@ -16,8 +16,6 @@ function collectFiles(dir: string): string[] {
|
|
|
16
16
|
import { lookup } from 'mime-types';
|
|
17
17
|
import { createSDKClient } from '../sdk.js';
|
|
18
18
|
import { cliConfig, getApiUrl } from '../config.js';
|
|
19
|
-
import { encryptBuffer, decryptBuffer } from '../crypto.js';
|
|
20
|
-
import { loadMasterKey } from '../lib/keyring.js';
|
|
21
19
|
import { isSealConfigured, sealEncrypt, sealDecrypt, getSuiKeypair } from '../seal.js';
|
|
22
20
|
import { showAuthScreen } from './auth-screen.js';
|
|
23
21
|
import { VaultsPanel, VaultItem } from './vaults-panel.js';
|
|
@@ -135,12 +133,12 @@ export async function launchTui() {
|
|
|
135
133
|
setFocus(currentFocus);
|
|
136
134
|
return;
|
|
137
135
|
}
|
|
138
|
-
const visIdx = await selectDialog(screen, 'Visibility', ['
|
|
136
|
+
const visIdx = await selectDialog(screen, 'Visibility', ['public', 'shared']);
|
|
139
137
|
if (visIdx < 0) {
|
|
140
138
|
setFocus(currentFocus);
|
|
141
139
|
return;
|
|
142
140
|
}
|
|
143
|
-
const visMap = ['
|
|
141
|
+
const visMap = ['public', 'shared'] as const;
|
|
144
142
|
const visibility = visMap[visIdx];
|
|
145
143
|
|
|
146
144
|
try {
|
|
@@ -225,18 +223,7 @@ export async function launchTui() {
|
|
|
225
223
|
|
|
226
224
|
try {
|
|
227
225
|
const vault = await sdk.vaults.get(vaultId);
|
|
228
|
-
const isPrivate = vault.visibility === 'private';
|
|
229
226
|
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
230
|
-
let masterKey: Buffer | null = null;
|
|
231
|
-
|
|
232
|
-
if (isPrivate) {
|
|
233
|
-
masterKey = loadMasterKey();
|
|
234
|
-
if (!masterKey) {
|
|
235
|
-
showError(screen, 'Encryption session not unlocked. Run: tusky encryption unlock');
|
|
236
|
-
setFocus(currentFocus);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
227
|
|
|
241
228
|
if (isShared) {
|
|
242
229
|
const keypair = getSuiKeypair();
|
|
@@ -262,24 +249,11 @@ export async function launchTui() {
|
|
|
262
249
|
|
|
263
250
|
let uploadBody: Buffer;
|
|
264
251
|
let encryptionMeta: {
|
|
265
|
-
wrappedKey?: string;
|
|
266
|
-
encryptionIv?: string;
|
|
267
|
-
plaintextSizeBytes?: number;
|
|
268
|
-
plaintextChecksumSha256?: string;
|
|
269
252
|
sealIdentity?: string;
|
|
270
253
|
sealEncryptedObject?: string;
|
|
271
254
|
} = {};
|
|
272
255
|
|
|
273
|
-
if (
|
|
274
|
-
const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
|
|
275
|
-
uploadBody = ciphertext;
|
|
276
|
-
encryptionMeta = {
|
|
277
|
-
wrappedKey,
|
|
278
|
-
encryptionIv: iv,
|
|
279
|
-
plaintextSizeBytes: stat.size,
|
|
280
|
-
plaintextChecksumSha256: plaintextChecksum,
|
|
281
|
-
};
|
|
282
|
-
} else if (isShared) {
|
|
256
|
+
if (isShared) {
|
|
283
257
|
statusBar.setHints(`SEAL encrypting ${successCount + 1}/${total}: ${fileName}...`);
|
|
284
258
|
screen.render();
|
|
285
259
|
const fileNonce = randomUUID();
|
|
@@ -288,7 +262,6 @@ export async function launchTui() {
|
|
|
288
262
|
encryptionMeta = {
|
|
289
263
|
sealIdentity: sealResult.sealIdentity,
|
|
290
264
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
291
|
-
plaintextSizeBytes: stat.size,
|
|
292
265
|
};
|
|
293
266
|
} else {
|
|
294
267
|
uploadBody = fileBuffer;
|
|
@@ -315,7 +288,7 @@ export async function launchTui() {
|
|
|
315
288
|
successCount++;
|
|
316
289
|
}
|
|
317
290
|
|
|
318
|
-
const label =
|
|
291
|
+
const label = isShared ? 'shared/SEAL' : 'public';
|
|
319
292
|
showMessage(screen, `Uploaded ${successCount} file(s) [${label}]`);
|
|
320
293
|
await filesPanel.loadForVault(vaultId, filesPanel.getCurrentVaultName());
|
|
321
294
|
await vaultsPanel.load();
|
|
@@ -378,7 +351,7 @@ export async function launchTui() {
|
|
|
378
351
|
const arrayBuf = await response.arrayBuffer();
|
|
379
352
|
let fileBuffer: Buffer = Buffer.from(new Uint8Array(arrayBuf));
|
|
380
353
|
|
|
381
|
-
// Decrypt if needed
|
|
354
|
+
// Decrypt if needed — SEAL-encrypted (shared vault)
|
|
382
355
|
if (encryption?.encrypted) {
|
|
383
356
|
if ('type' in encryption && encryption.type === 'seal') {
|
|
384
357
|
statusBar.setHints(`SEAL decrypting "${file.name}"...`);
|
|
@@ -393,23 +366,6 @@ export async function launchTui() {
|
|
|
393
366
|
const vault = await sdk.vaults.get(fileInfo.vaultId);
|
|
394
367
|
const decrypted = await sealDecrypt(new Uint8Array(fileBuffer), vault, keypair);
|
|
395
368
|
fileBuffer = Buffer.from(decrypted);
|
|
396
|
-
} else {
|
|
397
|
-
statusBar.setHints(`Decrypting "${file.name}"...`);
|
|
398
|
-
screen.render();
|
|
399
|
-
const masterKey = loadMasterKey();
|
|
400
|
-
if (!masterKey) {
|
|
401
|
-
showError(screen, 'Encryption not unlocked. Run: tusky encryption unlock');
|
|
402
|
-
setFocus(currentFocus);
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
const enc = encryption as { type: 'passphrase'; encrypted: true; wrappedKey: string; iv: string; plaintextChecksumSha256: string | null };
|
|
406
|
-
fileBuffer = decryptBuffer(
|
|
407
|
-
fileBuffer,
|
|
408
|
-
enc.wrappedKey,
|
|
409
|
-
enc.iv,
|
|
410
|
-
masterKey,
|
|
411
|
-
enc.plaintextChecksumSha256 ?? undefined,
|
|
412
|
-
);
|
|
413
369
|
}
|
|
414
370
|
}
|
|
415
371
|
|
|
@@ -542,7 +498,7 @@ export async function launchTui() {
|
|
|
542
498
|
['Name', file.name],
|
|
543
499
|
['ID', file.id],
|
|
544
500
|
['MIME', file.mimeType],
|
|
545
|
-
['Size', formatBytes(file.
|
|
501
|
+
['Size', formatBytes(file.sizeBytes)],
|
|
546
502
|
['Status', `${open}${file.status}${close}`],
|
|
547
503
|
['Encrypted', file.encrypted ? 'Yes' : 'No'],
|
|
548
504
|
['Uploaded', file.createdAt ? formatDate(file.createdAt) : 'N/A'],
|
|
@@ -647,7 +603,7 @@ export async function launchTui() {
|
|
|
647
603
|
' Enter Open folder / view details',
|
|
648
604
|
'',
|
|
649
605
|
'{bold}Actions{/bold}',
|
|
650
|
-
' n Create new vault (
|
|
606
|
+
' n Create new vault (public/shared)',
|
|
651
607
|
' f Create folder in current vault',
|
|
652
608
|
' u Upload file to current vault/folder',
|
|
653
609
|
' d Download selected file',
|
package/src/tui/overview.ts
CHANGED
|
@@ -77,10 +77,7 @@ export async function showOverview(
|
|
|
77
77
|
const vaultRows = vaults.map((v) => {
|
|
78
78
|
let vis: string;
|
|
79
79
|
let visTag: string;
|
|
80
|
-
if (v.visibility === '
|
|
81
|
-
vis = 'private';
|
|
82
|
-
visTag = '{yellow-fg}private{/yellow-fg}';
|
|
83
|
-
} else if (v.visibility === 'shared') {
|
|
80
|
+
if (v.visibility === 'shared') {
|
|
84
81
|
vis = 'shared';
|
|
85
82
|
visTag = '{magenta-fg}shared{/magenta-fg}';
|
|
86
83
|
} else {
|
|
@@ -119,7 +116,7 @@ export async function showOverview(
|
|
|
119
116
|
// Calculate column widths for files table
|
|
120
117
|
const fileRows = files.map((f) => ({
|
|
121
118
|
name: f.name || '',
|
|
122
|
-
size: formatBytes(f.
|
|
119
|
+
size: formatBytes(f.sizeBytes ?? 0),
|
|
123
120
|
status: f.status || '',
|
|
124
121
|
statusTag: `${statusColor(f.status)}${f.status}${statusColorClose(f.status)}`,
|
|
125
122
|
date: f.createdAt ? formatDate(f.createdAt) : 'N/A',
|
package/src/tui/trash-screen.ts
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `tusky decrypt` — Decrypt a file downloaded from Walrus.
|
|
3
|
-
*
|
|
4
|
-
* Works in two modes:
|
|
5
|
-
* 1. With --export <manifest.json> — reads wrappedKey/iv from the export
|
|
6
|
-
* manifest, derives master key from passphrase + salt. Fully offline.
|
|
7
|
-
* 2. Without --export — fetches encryption params from the Tusky API and
|
|
8
|
-
* looks up the file metadata by ID. Requires API access.
|
|
9
|
-
*
|
|
10
|
-
* In both modes, the passphrase is resolved from:
|
|
11
|
-
* --passphrase flag > TUSKYDP_PASSWORD env var > interactive prompt
|
|
12
|
-
*/
|
|
13
|
-
import type { Command } from 'commander';
|
|
14
|
-
export declare function registerDecryptCommand(program: Command): void;
|
|
15
|
-
//# sourceMappingURL=decrypt.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../../src/commands/decrypt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsFzC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QAiNtD"}
|