@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/commands/export.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tusky export` — Export file metadata for disaster recovery.
|
|
3
3
|
*
|
|
4
|
-
* Dumps all file records (including Walrus blob IDs
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Dumps all file records (including Walrus blob IDs and epoch info)
|
|
5
|
+
* to a JSON file. This allows users to recover their data directly
|
|
6
|
+
* from Walrus if the Tusky service is unavailable.
|
|
7
7
|
*
|
|
8
8
|
* The export contains everything needed to:
|
|
9
9
|
* 1. Fetch files from Walrus aggregators using blobId
|
|
10
|
-
* 2.
|
|
11
|
-
* 3. Renew Walrus epochs using blobObjectId + a Sui wallet
|
|
10
|
+
* 2. Renew Walrus epochs using blobObjectId + a Sui wallet
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
import type { Command } from 'commander';
|
|
@@ -35,20 +34,10 @@ export interface ExportedFile {
|
|
|
35
34
|
walrusBlobObjectId: string | null;
|
|
36
35
|
/** Epoch when Walrus storage expires (renew before this) */
|
|
37
36
|
walrusEpochEnd: string | null;
|
|
38
|
-
/** Whether the file is client-side encrypted */
|
|
37
|
+
/** Whether the file is client-side encrypted (SEAL for shared vaults) */
|
|
39
38
|
encrypted: boolean;
|
|
40
|
-
/** AES-256-GCM file key, wrapped with the master key (base64) */
|
|
41
|
-
wrappedKey: string | null;
|
|
42
|
-
/** AES-256-GCM initialization vector (base64) */
|
|
43
|
-
encryptionIv: string | null;
|
|
44
|
-
/** SHA-256 of the plaintext — for integrity verification after decryption */
|
|
45
|
-
plaintextChecksumSha256: string | null;
|
|
46
|
-
/** Original plaintext size in bytes (before encryption) */
|
|
47
|
-
plaintextSizeBytes: number | null;
|
|
48
39
|
/** Whether the filename is encrypted */
|
|
49
40
|
nameEncrypted: boolean;
|
|
50
|
-
/** AES-256-GCM encrypted filename+mimeType blob (private vaults) */
|
|
51
|
-
encryptedName: string | null;
|
|
52
41
|
/** SEAL-encrypted filename+mimeType blob (shared vaults) */
|
|
53
42
|
encryptedMetadata: string | null;
|
|
54
43
|
createdAt: string;
|
|
@@ -62,16 +51,6 @@ export interface ExportManifest {
|
|
|
62
51
|
email: string;
|
|
63
52
|
plan: string;
|
|
64
53
|
};
|
|
65
|
-
/** Account encryption params — needed to derive master key from passphrase */
|
|
66
|
-
encryption: {
|
|
67
|
-
setupComplete: boolean;
|
|
68
|
-
/** PBKDF2 salt (base64) */
|
|
69
|
-
salt: string | null;
|
|
70
|
-
/** HMAC verifier for passphrase validation (base64) */
|
|
71
|
-
verifier: string | null;
|
|
72
|
-
/** Master key wrapped with passphrase-derived key (base64) */
|
|
73
|
-
encryptedMasterKey: string | null;
|
|
74
|
-
};
|
|
75
54
|
walrusNetwork: string;
|
|
76
55
|
walrusAggregatorUrl: string;
|
|
77
56
|
totalFiles: number;
|
|
@@ -85,7 +64,7 @@ export interface ExportManifest {
|
|
|
85
64
|
export function registerExportCommand(program: Command) {
|
|
86
65
|
program
|
|
87
66
|
.command('export')
|
|
88
|
-
.description('Export file metadata for disaster recovery (Walrus blob IDs,
|
|
67
|
+
.description('Export file metadata for disaster recovery (Walrus blob IDs, epoch info)')
|
|
89
68
|
.option('-o, --output <path>', 'Output file path', 'tusky-export.json')
|
|
90
69
|
.option('--stdout', 'Write JSON manifest to stdout instead of a file')
|
|
91
70
|
.option('--vault <vaultId>', 'Export only files from a specific vault')
|
|
@@ -98,9 +77,8 @@ export function registerExportCommand(program: Command) {
|
|
|
98
77
|
spinner.start();
|
|
99
78
|
|
|
100
79
|
try {
|
|
101
|
-
// Fetch account
|
|
80
|
+
// Fetch account
|
|
102
81
|
const account = await sdk.account.get();
|
|
103
|
-
const encryptionParams = await sdk.account.getEncryptionParams();
|
|
104
82
|
|
|
105
83
|
// Fetch all vaults
|
|
106
84
|
spinner.text = 'Fetching vaults...';
|
|
@@ -146,12 +124,7 @@ export function registerExportCommand(program: Command) {
|
|
|
146
124
|
walrusBlobObjectId: file.walrusBlobObjectId,
|
|
147
125
|
walrusEpochEnd: file.walrusEpochEnd,
|
|
148
126
|
encrypted: file.encrypted,
|
|
149
|
-
wrappedKey: file.wrappedKey,
|
|
150
|
-
encryptionIv: file.encryptionIv,
|
|
151
|
-
plaintextChecksumSha256: file.plaintextChecksumSha256,
|
|
152
|
-
plaintextSizeBytes: file.plaintextSizeBytes,
|
|
153
127
|
nameEncrypted: file.nameEncrypted ?? false,
|
|
154
|
-
encryptedName: file.encryptedName ?? null,
|
|
155
128
|
encryptedMetadata: file.encryptedMetadata ?? null,
|
|
156
129
|
createdAt: file.createdAt,
|
|
157
130
|
});
|
|
@@ -178,12 +151,6 @@ export function registerExportCommand(program: Command) {
|
|
|
178
151
|
email: account.email,
|
|
179
152
|
plan: account.planName,
|
|
180
153
|
},
|
|
181
|
-
encryption: {
|
|
182
|
-
setupComplete: encryptionParams.setupComplete,
|
|
183
|
-
salt: encryptionParams.salt ?? null,
|
|
184
|
-
verifier: encryptionParams.verifier ?? null,
|
|
185
|
-
encryptedMasterKey: encryptionParams.encryptedMasterKey ?? null,
|
|
186
|
-
},
|
|
187
154
|
walrusNetwork: isMainnet ? 'mainnet' : 'testnet',
|
|
188
155
|
walrusAggregatorUrl,
|
|
189
156
|
totalFiles: exportedFiles.length,
|
|
@@ -201,30 +168,6 @@ export function registerExportCommand(program: Command) {
|
|
|
201
168
|
' OR: walrus read <walrusBlobId> -o <filename>',
|
|
202
169
|
' OR: tusky rehydrate <walrusBlobId> -o <filename>',
|
|
203
170
|
'',
|
|
204
|
-
'--- DECRYPT PRIVATE VAULT FILES ---',
|
|
205
|
-
'Files with encrypted=true are AES-256-GCM encrypted.',
|
|
206
|
-
'',
|
|
207
|
-
'Option A — Using the Tusky CLI decrypt command (easiest):',
|
|
208
|
-
' tusky decrypt <encrypted-file> --export <this-file.json> -o <output>',
|
|
209
|
-
' This reads the wrappedKey/iv from this manifest and prompts for your passphrase.',
|
|
210
|
-
' You can also pass --passphrase <passphrase> or set TUSKYDP_PASSWORD env var.',
|
|
211
|
-
' If the Tusky API is still reachable, you can skip the export and use:',
|
|
212
|
-
' tusky decrypt <encrypted-file> --file-id <fileId> -o <output>',
|
|
213
|
-
'',
|
|
214
|
-
'Option B — Using the standalone script (zero dependencies, fully offline):',
|
|
215
|
-
' node tusky-decrypt.mjs <encrypted-file> <output> <passphrase> <salt> <wrappedKey> <iv> [encryptedMasterKey]',
|
|
216
|
-
' The salt and encryptedMasterKey come from your account encryption params.',
|
|
217
|
-
'',
|
|
218
|
-
'Option C — Manual decryption (any language with AES-256-GCM + PBKDF2):',
|
|
219
|
-
' 1. Derive wrapping key: PBKDF2(passphrase, salt, 600000 iterations, SHA-256, 32 bytes)',
|
|
220
|
-
' 2. If your account has an encryptedMasterKey, unwrap it: AES-256-GCM-decrypt(encryptedMasterKey, wrappingKey)',
|
|
221
|
-
' Otherwise the wrapping key IS the master key (legacy accounts)',
|
|
222
|
-
' 3. Unwrap the file key: AES-256-GCM-decrypt(wrappedKey, masterKey)',
|
|
223
|
-
' wrappedKey format: [12-byte IV | ciphertext | 16-byte auth tag]',
|
|
224
|
-
' 4. Decrypt the file: AES-256-GCM-decrypt(fileData, fileKey, encryptionIv)',
|
|
225
|
-
' File data format: [ciphertext | 16-byte auth tag]',
|
|
226
|
-
' 5. Verify integrity: SHA-256(plaintext) should match plaintextChecksumSha256',
|
|
227
|
-
'',
|
|
228
171
|
'--- RENEW WALRUS EPOCHS ---',
|
|
229
172
|
'Files with a walrusBlobObjectId can have their storage extended:',
|
|
230
173
|
' walrus extend <walrusBlobObjectId> --epochs 5',
|
|
@@ -248,16 +191,13 @@ export function registerExportCommand(program: Command) {
|
|
|
248
191
|
if (!options.stdout) {
|
|
249
192
|
// Summary stats (only for disk output — stdout is clean JSON)
|
|
250
193
|
const withBlob = exportedFiles.filter((f) => f.walrusBlobId).length;
|
|
251
|
-
const encrypted = exportedFiles.filter((f) => f.encrypted).length;
|
|
252
194
|
const hotOnly = exportedFiles.filter((f) => !f.walrusBlobId && f.status === 'hot').length;
|
|
253
195
|
|
|
254
196
|
console.log(` On Walrus: ${withBlob} files (recoverable from Walrus network)`);
|
|
255
|
-
console.log(` Encrypted: ${encrypted} files (require passphrase to decrypt)`);
|
|
256
197
|
if (hotOnly > 0) {
|
|
257
198
|
console.log(chalk.yellow(` Hot only: ${hotOnly} files (NOT yet on Walrus — only in Tusky hot cache)`));
|
|
258
199
|
}
|
|
259
200
|
console.log('');
|
|
260
|
-
console.log(chalk.yellow(' Store this file securely — it contains encryption keys.'));
|
|
261
201
|
console.log(chalk.dim(' See the "recovery.instructions" field inside for full recovery steps.'));
|
|
262
202
|
}
|
|
263
203
|
} catch (err: any) {
|
package/src/commands/files.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function registerFileCommands(program: Command) {
|
|
|
62
62
|
|
|
63
63
|
const row = [
|
|
64
64
|
f.name,
|
|
65
|
-
formatBytes(f.
|
|
65
|
+
formatBytes(f.sizeBytes),
|
|
66
66
|
statusColor(f.status),
|
|
67
67
|
];
|
|
68
68
|
if (showFolder) {
|
|
@@ -113,7 +113,7 @@ export function registerFileCommands(program: Command) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
console.log(`Name: ${file.name}`);
|
|
116
|
-
console.log(`Size: ${formatBytes(file.
|
|
116
|
+
console.log(`Size: ${formatBytes(file.sizeBytes)}`);
|
|
117
117
|
console.log(`MIME: ${file.mimeType}`);
|
|
118
118
|
console.log(`Status: ${file.status}`);
|
|
119
119
|
console.log(`Encrypted: ${file.encrypted ? 'Yes' : 'No'}`);
|
package/src/commands/mcp.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
|
9
9
|
import { join, dirname, resolve } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import { getApiUrl, getApiKey, cliConfig } from '../config.js';
|
|
12
|
+
import { getApiUrl, getApiKey, cliConfig, getSuiPrivateKey } from '../config.js';
|
|
13
13
|
import { startMcpServer } from '../mcp/server.js';
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -93,15 +93,16 @@ function getTuskyBinaryCommand(): { command: string; args: string[] } {
|
|
|
93
93
|
return { command: 'npx', args: ['-y', '@tuskydp/cli', 'mcp', 'serve'] };
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function buildMcpServerEntry(apiKey?: string, apiUrl?: string) {
|
|
96
|
+
function buildMcpServerEntry(apiKey?: string, apiUrl?: string, suiPrivKey?: string) {
|
|
97
97
|
const { command, args } = getTuskyBinaryCommand();
|
|
98
98
|
|
|
99
99
|
const env: Record<string, string> = {};
|
|
100
100
|
if (apiKey) env.TUSKYDP_API_KEY = apiKey;
|
|
101
101
|
if (apiUrl) env.TUSKYDP_API_URL = apiUrl;
|
|
102
102
|
|
|
103
|
-
//
|
|
104
|
-
|
|
103
|
+
// Include Sui private key if provided or stored in config
|
|
104
|
+
const resolvedSuiKey = suiPrivKey || getSuiPrivateKey() || '';
|
|
105
|
+
env.TUSKYDP_SUI_PRIVATE_KEY = resolvedSuiKey;
|
|
105
106
|
|
|
106
107
|
const entry: Record<string, unknown> = {
|
|
107
108
|
command,
|
|
@@ -158,7 +159,8 @@ export function registerMcpCommands(program: Command) {
|
|
|
158
159
|
.option('--target <target>', 'Target client: claude-code, claude-desktop, cursor', 'claude-code')
|
|
159
160
|
.option('--api-key <key>', 'API key to embed (defaults to stored key)')
|
|
160
161
|
.option('--api-url <url>', 'API URL override')
|
|
161
|
-
.
|
|
162
|
+
.option('--sui-priv-key <key>', 'Sui Ed25519 private key for shared vault access (defaults to stored key)')
|
|
163
|
+
.action(async (options: { target: string; apiKey?: string; apiUrl?: string; suiPrivKey?: string }) => {
|
|
162
164
|
const targets = getTargets();
|
|
163
165
|
const target = targets.find((t) => t.name === options.target);
|
|
164
166
|
|
|
@@ -177,8 +179,8 @@ export function registerMcpCommands(program: Command) {
|
|
|
177
179
|
// Only include non-default values
|
|
178
180
|
const resolvedApiUrl = apiUrl && apiUrl !== 'https://api.tusky.ai' ? apiUrl : undefined;
|
|
179
181
|
|
|
180
|
-
// Build the server entry
|
|
181
|
-
const serverEntry = buildMcpServerEntry(apiKey, resolvedApiUrl);
|
|
182
|
+
// Build the server entry (SUI key: flag > config)
|
|
183
|
+
const serverEntry = buildMcpServerEntry(apiKey, resolvedApiUrl, options.suiPrivKey);
|
|
182
184
|
|
|
183
185
|
// Read existing config
|
|
184
186
|
const config = readJsonFile(target.configPath);
|
|
@@ -205,9 +207,13 @@ export function registerMcpCommands(program: Command) {
|
|
|
205
207
|
console.log('');
|
|
206
208
|
}
|
|
207
209
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
const suiKey = options.suiPrivKey || getSuiPrivateKey();
|
|
211
|
+
if (!suiKey) {
|
|
212
|
+
console.log(chalk.yellow(' Note: No Sui key found. Set it up for shared vault access:'));
|
|
213
|
+
console.log(chalk.dim(' Run: tusky sui setup <suiprivkey1...>'));
|
|
214
|
+
console.log(chalk.dim(' Then re-run: tusky mcp install-config'));
|
|
215
|
+
console.log('');
|
|
216
|
+
}
|
|
211
217
|
|
|
212
218
|
if (options.target === 'claude-desktop') {
|
|
213
219
|
console.log(chalk.dim(' Restart Claude Desktop to pick up the changes.'));
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { cliConfig } from '../config.js';
|
|
4
|
+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
|
|
5
|
+
|
|
6
|
+
export function registerSuiCommands(program: Command) {
|
|
7
|
+
const sui = program
|
|
8
|
+
.command('sui')
|
|
9
|
+
.description('Manage Sui wallet key for shared vault access');
|
|
10
|
+
|
|
11
|
+
// ── tusky sui setup ──────────────────────────────────────────────────
|
|
12
|
+
sui
|
|
13
|
+
.command('setup <private-key>')
|
|
14
|
+
.description('Save a Sui Ed25519 private key to config for shared vault operations')
|
|
15
|
+
.action((privateKey: string) => {
|
|
16
|
+
let address: string;
|
|
17
|
+
try {
|
|
18
|
+
const keypair = Ed25519Keypair.fromSecretKey(privateKey);
|
|
19
|
+
address = keypair.toSuiAddress();
|
|
20
|
+
} catch {
|
|
21
|
+
console.error(chalk.red('Invalid Sui private key. Expected suiprivkey1... format.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
cliConfig.set('suiPrivateKey', privateKey);
|
|
26
|
+
console.log(chalk.green('Sui private key saved to config.'));
|
|
27
|
+
console.log(chalk.dim(` Sui address: ${address}`));
|
|
28
|
+
console.log('');
|
|
29
|
+
console.log(chalk.yellow(' Security note: The key is stored in your local config file.'));
|
|
30
|
+
console.log(chalk.dim(` Location: ${cliConfig.path}`));
|
|
31
|
+
console.log(chalk.dim(' Run `tusky sui clear` to remove it.'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── tusky sui show ───────────────────────────────────────────────────
|
|
35
|
+
sui
|
|
36
|
+
.command('show')
|
|
37
|
+
.description('Show the configured Sui address (does not reveal the private key)')
|
|
38
|
+
.action(() => {
|
|
39
|
+
const envKey = process.env.TUSKYDP_SUI_PRIVATE_KEY;
|
|
40
|
+
const configKey = cliConfig.get('suiPrivateKey');
|
|
41
|
+
const key = envKey || configKey;
|
|
42
|
+
const source = envKey ? 'env var (TUSKYDP_SUI_PRIVATE_KEY)' : 'config';
|
|
43
|
+
|
|
44
|
+
if (!key) {
|
|
45
|
+
console.log(chalk.dim('No Sui private key configured.'));
|
|
46
|
+
console.log(chalk.dim('Run: tusky sui setup <private-key>'));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const keypair = Ed25519Keypair.fromSecretKey(key);
|
|
52
|
+
const address = keypair.toSuiAddress();
|
|
53
|
+
console.log(`Sui address: ${address}`);
|
|
54
|
+
console.log(chalk.dim(`Source: ${source}`));
|
|
55
|
+
} catch {
|
|
56
|
+
console.error(chalk.red('Stored Sui key is invalid. Run: tusky sui setup <private-key>'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ── tusky sui clear ──────────────────────────────────────────────────
|
|
62
|
+
sui
|
|
63
|
+
.command('clear')
|
|
64
|
+
.description('Remove the stored Sui private key from config')
|
|
65
|
+
.action(() => {
|
|
66
|
+
cliConfig.delete('suiPrivateKey');
|
|
67
|
+
console.log(chalk.green('Sui private key removed from config.'));
|
|
68
|
+
});
|
|
69
|
+
}
|
package/src/commands/trash.ts
CHANGED
|
@@ -51,7 +51,7 @@ export function registerTrashCommands(program: Command) {
|
|
|
51
51
|
for (const f of trashed.files) {
|
|
52
52
|
table.push([
|
|
53
53
|
f.name,
|
|
54
|
-
formatBytes(f.
|
|
54
|
+
formatBytes(f.sizeBytes),
|
|
55
55
|
f.status,
|
|
56
56
|
f.deletedAt ? formatDate(f.deletedAt) : 'N/A',
|
|
57
57
|
chalk.dim(shortId(f.id)),
|
package/src/commands/upload.ts
CHANGED
|
@@ -9,8 +9,6 @@ import { createSDKClient } from '../sdk.js';
|
|
|
9
9
|
import { resolveVault } from '../lib/resolve.js';
|
|
10
10
|
import { formatBytes } from '../lib/output.js';
|
|
11
11
|
import { createSpinner } from '../lib/progress.js';
|
|
12
|
-
import { encryptBuffer, encryptMetadata } from '../crypto.js';
|
|
13
|
-
import { loadMasterKey } from '../lib/keyring.js';
|
|
14
12
|
import { isSealConfigured, sealEncrypt, sealEncryptMetadata, getSuiKeypair } from '../seal.js';
|
|
15
13
|
|
|
16
14
|
async function readStdin(): Promise<Buffer> {
|
|
@@ -79,18 +77,8 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
79
77
|
|
|
80
78
|
const vaultId = await resolveVault(sdk, options.vault);
|
|
81
79
|
const vault = await sdk.vaults.get(vaultId);
|
|
82
|
-
const isPrivate = vault.visibility === 'private';
|
|
83
80
|
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
84
81
|
|
|
85
|
-
let masterKey: Buffer | null = null;
|
|
86
|
-
if (isPrivate) {
|
|
87
|
-
masterKey = loadMasterKey();
|
|
88
|
-
if (!masterKey) {
|
|
89
|
-
console.error(chalk.red('Encryption session not unlocked. Run: tusky encryption unlock'));
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
82
|
const spinner = createSpinner(`Uploading ${fileName}`);
|
|
95
83
|
spinner.start();
|
|
96
84
|
|
|
@@ -98,28 +86,12 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
98
86
|
const mimeType = lookup(fileName) || 'application/octet-stream';
|
|
99
87
|
let uploadBody: Buffer;
|
|
100
88
|
let encryptionMeta: {
|
|
101
|
-
wrappedKey?: string;
|
|
102
|
-
encryptionIv?: string;
|
|
103
|
-
plaintextSizeBytes?: number;
|
|
104
|
-
plaintextChecksumSha256?: string;
|
|
105
89
|
sealIdentity?: string;
|
|
106
90
|
sealEncryptedObject?: string;
|
|
107
|
-
encryptedName?: string;
|
|
108
91
|
encryptedMetadata?: string;
|
|
109
92
|
} = {};
|
|
110
93
|
|
|
111
|
-
if (
|
|
112
|
-
spinner.text = `Encrypting ${fileName}...`;
|
|
113
|
-
const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
|
|
114
|
-
uploadBody = ciphertext;
|
|
115
|
-
encryptionMeta = {
|
|
116
|
-
wrappedKey,
|
|
117
|
-
encryptionIv: iv,
|
|
118
|
-
plaintextSizeBytes: fileBuffer.length,
|
|
119
|
-
plaintextChecksumSha256: plaintextChecksum,
|
|
120
|
-
encryptedName: encryptMetadata(fileName, mimeType, masterKey),
|
|
121
|
-
};
|
|
122
|
-
} else if (isShared) {
|
|
94
|
+
if (isShared) {
|
|
123
95
|
spinner.text = `SEAL encrypting ${fileName}...`;
|
|
124
96
|
const fileNonce = randomUUID();
|
|
125
97
|
const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
|
|
@@ -127,7 +99,6 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
127
99
|
encryptionMeta = {
|
|
128
100
|
sealIdentity: sealResult.sealIdentity,
|
|
129
101
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
130
|
-
plaintextSizeBytes: fileBuffer.length,
|
|
131
102
|
encryptedMetadata: await sealEncryptMetadata(fileName, mimeType, vault, fileNonce),
|
|
132
103
|
};
|
|
133
104
|
} else {
|
|
@@ -169,18 +140,8 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
169
140
|
// ── File path mode (original behavior) ───────────────────────────
|
|
170
141
|
const vaultId = await resolveVault(sdk, options.vault);
|
|
171
142
|
const vault = await sdk.vaults.get(vaultId);
|
|
172
|
-
const isPrivate = vault.visibility === 'private';
|
|
173
143
|
const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
|
|
174
144
|
|
|
175
|
-
let masterKey: Buffer | null = null;
|
|
176
|
-
if (isPrivate) {
|
|
177
|
-
masterKey = loadMasterKey();
|
|
178
|
-
if (!masterKey) {
|
|
179
|
-
console.error(chalk.red('Encryption session not unlocked. Run: tusky encryption unlock'));
|
|
180
|
-
process.exit(1);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
145
|
if (isShared) {
|
|
185
146
|
const keypair = getSuiKeypair();
|
|
186
147
|
if (!keypair) {
|
|
@@ -209,28 +170,12 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
209
170
|
|
|
210
171
|
let uploadBody: Buffer;
|
|
211
172
|
let encryptionMeta: {
|
|
212
|
-
wrappedKey?: string;
|
|
213
|
-
encryptionIv?: string;
|
|
214
|
-
plaintextSizeBytes?: number;
|
|
215
|
-
plaintextChecksumSha256?: string;
|
|
216
173
|
sealIdentity?: string;
|
|
217
174
|
sealEncryptedObject?: string;
|
|
218
|
-
encryptedName?: string;
|
|
219
175
|
encryptedMetadata?: string;
|
|
220
176
|
} = {};
|
|
221
177
|
|
|
222
|
-
if (
|
|
223
|
-
spinner.text = `Encrypting ${basename(filePath)}...`;
|
|
224
|
-
const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
|
|
225
|
-
uploadBody = ciphertext;
|
|
226
|
-
encryptionMeta = {
|
|
227
|
-
wrappedKey,
|
|
228
|
-
encryptionIv: iv,
|
|
229
|
-
plaintextSizeBytes: stat.size,
|
|
230
|
-
plaintextChecksumSha256: plaintextChecksum,
|
|
231
|
-
encryptedName: encryptMetadata(basename(filePath), mimeType, masterKey),
|
|
232
|
-
};
|
|
233
|
-
} else if (isShared) {
|
|
178
|
+
if (isShared) {
|
|
234
179
|
spinner.text = `SEAL encrypting ${basename(filePath)}...`;
|
|
235
180
|
const fileNonce = randomUUID();
|
|
236
181
|
const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
|
|
@@ -238,7 +183,6 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
238
183
|
encryptionMeta = {
|
|
239
184
|
sealIdentity: sealResult.sealIdentity,
|
|
240
185
|
sealEncryptedObject: sealResult.sealEncryptedObject,
|
|
241
|
-
plaintextSizeBytes: stat.size,
|
|
242
186
|
encryptedMetadata: await sealEncryptMetadata(basename(filePath), mimeType, vault, fileNonce),
|
|
243
187
|
};
|
|
244
188
|
} else {
|
|
@@ -283,7 +227,7 @@ export async function uploadCommand(paths: string[], options: {
|
|
|
283
227
|
}
|
|
284
228
|
|
|
285
229
|
if (filePaths.length > 1) {
|
|
286
|
-
const label =
|
|
230
|
+
const label = isShared ? 'shared' : 'public';
|
|
287
231
|
console.log(`\nUploaded ${successCount} file(s) (${formatBytes(totalSize)}) to vault "${vault.name}" [${label}]`);
|
|
288
232
|
}
|
|
289
233
|
}
|
package/src/commands/vault.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { resolveVault } from '../lib/resolve.js';
|
|
|
8
8
|
|
|
9
9
|
/** Map vault visibility to a display label. */
|
|
10
10
|
function visibilityLabel(v: string): string {
|
|
11
|
-
if (v === 'private') return 'private';
|
|
12
11
|
if (v === 'shared') return 'shared';
|
|
13
12
|
return 'public';
|
|
14
13
|
}
|
|
@@ -18,34 +17,14 @@ export function registerVaultCommands(program: Command) {
|
|
|
18
17
|
|
|
19
18
|
// ── create ──────────────────────────────────────────────────────────
|
|
20
19
|
vault.command('create <name>')
|
|
21
|
-
.description('Create a new vault (
|
|
22
|
-
.option('--public', 'Create as public vault (unencrypted, shareable via URL)')
|
|
20
|
+
.description('Create a new vault (public by default)')
|
|
23
21
|
.option('--shared', 'Create as shared vault (SEAL-encrypted, requires linked Sui address)')
|
|
24
22
|
.option('--description <text>', 'Vault description')
|
|
25
23
|
.action(async (name: string, options) => {
|
|
26
24
|
const sdk = getSDKClientFromParent(vault);
|
|
27
|
-
let visibility: 'public' | '
|
|
28
|
-
if (options.public) visibility = 'public';
|
|
25
|
+
let visibility: 'public' | 'shared' = 'public';
|
|
29
26
|
if (options.shared) visibility = 'shared';
|
|
30
27
|
|
|
31
|
-
if (options.public && options.shared) {
|
|
32
|
-
console.error(chalk.red('Cannot use both --public and --shared.'));
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (visibility === 'private') {
|
|
37
|
-
try {
|
|
38
|
-
const { setupComplete } = await sdk.account.getEncryptionParams();
|
|
39
|
-
if (!setupComplete) {
|
|
40
|
-
console.log(chalk.red('Encryption must be set up before creating private vaults.'));
|
|
41
|
-
console.log(chalk.dim(' Run: tusky encryption setup'));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
// If endpoint fails, proceed anyway
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
28
|
if (visibility === 'shared') {
|
|
50
29
|
try {
|
|
51
30
|
const acct = await sdk.account.get();
|
package/src/config.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface TuskyDPConfig {
|
|
|
5
5
|
apiKey?: string;
|
|
6
6
|
defaultVault?: string;
|
|
7
7
|
outputFormat: 'table' | 'json' | 'plain';
|
|
8
|
-
|
|
8
|
+
suiPrivateKey?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const cliConfig = new Conf<TuskyDPConfig>({
|
|
@@ -13,7 +13,6 @@ export const cliConfig = new Conf<TuskyDPConfig>({
|
|
|
13
13
|
defaults: {
|
|
14
14
|
apiUrl: 'https://api.tusky.ai',
|
|
15
15
|
outputFormat: 'table',
|
|
16
|
-
encryptionEnabled: true,
|
|
17
16
|
},
|
|
18
17
|
});
|
|
19
18
|
|
|
@@ -39,8 +38,8 @@ export function getOutputFormat(override?: string): 'table' | 'json' | 'plain' {
|
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
40
|
* Get the Sui private key for SEAL operations on shared vaults.
|
|
42
|
-
*
|
|
41
|
+
* Priority: TUSKYDP_SUI_PRIVATE_KEY env var > stored config key.
|
|
43
42
|
*/
|
|
44
43
|
export function getSuiPrivateKey(): string | null {
|
|
45
|
-
return process.env.TUSKYDP_SUI_PRIVATE_KEY || null;
|
|
44
|
+
return process.env.TUSKYDP_SUI_PRIVATE_KEY || cliConfig.get('suiPrivateKey') || null;
|
|
46
45
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { registerAuthCommands } from './commands/auth.js';
|
|
4
|
-
import { registerEncryptionCommands } from './commands/encryption.js';
|
|
5
4
|
import { registerVaultCommands } from './commands/vault.js';
|
|
6
5
|
import { registerFileCommands } from './commands/files.js';
|
|
7
6
|
import { registerAccountCommands } from './commands/account.js';
|
|
@@ -14,8 +13,8 @@ import { downloadCommand } from './commands/download.js';
|
|
|
14
13
|
import { rehydrateCommand } from './commands/rehydrate.js';
|
|
15
14
|
import { registerTuiCommand } from './commands/tui.js';
|
|
16
15
|
import { registerMcpCommands } from './commands/mcp.js';
|
|
16
|
+
import { registerSuiCommands } from './commands/sui.js';
|
|
17
17
|
import { registerExportCommand } from './commands/export.js';
|
|
18
|
-
import { registerDecryptCommand } from './commands/decrypt.js';
|
|
19
18
|
import { CLI_VERSION } from './version.js';
|
|
20
19
|
|
|
21
20
|
const program = new Command();
|
|
@@ -39,7 +38,6 @@ program
|
|
|
39
38
|
});
|
|
40
39
|
|
|
41
40
|
registerAuthCommands(program);
|
|
42
|
-
registerEncryptionCommands(program);
|
|
43
41
|
registerVaultCommands(program);
|
|
44
42
|
registerFileCommands(program);
|
|
45
43
|
registerFolderCommands(program);
|
|
@@ -49,8 +47,8 @@ registerWalletCommands(program);
|
|
|
49
47
|
registerAccountCommands(program);
|
|
50
48
|
registerTuiCommand(program);
|
|
51
49
|
registerMcpCommands(program);
|
|
50
|
+
registerSuiCommands(program);
|
|
52
51
|
registerExportCommand(program);
|
|
53
|
-
registerDecryptCommand(program);
|
|
54
52
|
|
|
55
53
|
// Direct shortcuts for common operations
|
|
56
54
|
program.command('upload [paths...]')
|
package/src/lib/resolve.ts
CHANGED
|
@@ -7,12 +7,11 @@ export async function resolveVault(sdk: TuskyClient, vaultRef?: string): Promise
|
|
|
7
7
|
const defaultVault = cliConfig.get('defaultVault');
|
|
8
8
|
if (defaultVault) return defaultVault;
|
|
9
9
|
|
|
10
|
-
// Fall back to the
|
|
10
|
+
// Fall back to the first vault
|
|
11
11
|
const vaults = await sdk.vaults.list();
|
|
12
|
-
|
|
13
|
-
if (defaultV) return defaultV.id;
|
|
12
|
+
if (vaults.length > 0) return vaults[0].id;
|
|
14
13
|
|
|
15
|
-
throw new Error('No
|
|
14
|
+
throw new Error('No vault found. Create one with: tusky vault create <name>');
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
// If it looks like a UUID, use it directly
|
package/src/mcp/context.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared context for MCP tool handlers.
|
|
3
3
|
*
|
|
4
|
-
* Holds the authenticated SDK client
|
|
5
|
-
* private vault encryption, and the Sui keypair for shared vault
|
|
4
|
+
* Holds the authenticated SDK client and the Sui keypair for shared vault
|
|
6
5
|
* SEAL encryption/decryption.
|
|
7
6
|
*/
|
|
8
7
|
|
|
@@ -13,15 +12,6 @@ export interface McpContext {
|
|
|
13
12
|
/** Authenticated Tusky SDK client. */
|
|
14
13
|
sdk: TuskyClient;
|
|
15
14
|
|
|
16
|
-
/**
|
|
17
|
-
* Returns the in-memory master key, or null if encryption was not
|
|
18
|
-
* unlocked (i.e. TUSKYDP_PASSWORD was not provided or setup is incomplete).
|
|
19
|
-
*/
|
|
20
|
-
getMasterKey(): Buffer | null;
|
|
21
|
-
|
|
22
|
-
/** True when the master key is available for encrypt/decrypt operations. */
|
|
23
|
-
isEncryptionReady(): boolean;
|
|
24
|
-
|
|
25
15
|
/**
|
|
26
16
|
* Returns the Sui Ed25519 keypair for SEAL operations, or null if
|
|
27
17
|
* TUSKYDP_SUI_PRIVATE_KEY was not provided.
|