@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.
Files changed (160) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/src/commands/account.d.ts.map +1 -1
  3. package/dist/src/commands/account.js +0 -1
  4. package/dist/src/commands/account.js.map +1 -1
  5. package/dist/src/commands/auth.d.ts.map +1 -1
  6. package/dist/src/commands/auth.js +8 -5
  7. package/dist/src/commands/auth.js.map +1 -1
  8. package/dist/src/commands/download.d.ts +1 -0
  9. package/dist/src/commands/download.d.ts.map +1 -1
  10. package/dist/src/commands/download.js +35 -22
  11. package/dist/src/commands/download.js.map +1 -1
  12. package/dist/src/commands/export.d.ts +9 -24
  13. package/dist/src/commands/export.d.ts.map +1 -1
  14. package/dist/src/commands/export.js +31 -59
  15. package/dist/src/commands/export.js.map +1 -1
  16. package/dist/src/commands/files.d.ts.map +1 -1
  17. package/dist/src/commands/files.js +91 -12
  18. package/dist/src/commands/files.js.map +1 -1
  19. package/dist/src/commands/folder.d.ts +3 -0
  20. package/dist/src/commands/folder.d.ts.map +1 -0
  21. package/dist/src/commands/folder.js +151 -0
  22. package/dist/src/commands/folder.js.map +1 -0
  23. package/dist/src/commands/mcp.d.ts.map +1 -1
  24. package/dist/src/commands/mcp.js +15 -9
  25. package/dist/src/commands/mcp.js.map +1 -1
  26. package/dist/src/commands/rehydrate.d.ts +1 -0
  27. package/dist/src/commands/rehydrate.d.ts.map +1 -1
  28. package/dist/src/commands/rehydrate.js +15 -7
  29. package/dist/src/commands/rehydrate.js.map +1 -1
  30. package/dist/src/commands/sui.d.ts +3 -0
  31. package/dist/src/commands/sui.d.ts.map +1 -0
  32. package/dist/src/commands/sui.js +64 -0
  33. package/dist/src/commands/sui.js.map +1 -0
  34. package/dist/src/commands/trash.d.ts +3 -0
  35. package/dist/src/commands/trash.d.ts.map +1 -0
  36. package/dist/src/commands/trash.js +109 -0
  37. package/dist/src/commands/trash.js.map +1 -0
  38. package/dist/src/commands/upload.d.ts +4 -0
  39. package/dist/src/commands/upload.d.ts.map +1 -1
  40. package/dist/src/commands/upload.js +82 -27
  41. package/dist/src/commands/upload.js.map +1 -1
  42. package/dist/src/commands/vault.d.ts.map +1 -1
  43. package/dist/src/commands/vault.js +2 -24
  44. package/dist/src/commands/vault.js.map +1 -1
  45. package/dist/src/commands/wallet.d.ts +3 -0
  46. package/dist/src/commands/wallet.d.ts.map +1 -0
  47. package/dist/src/commands/wallet.js +126 -0
  48. package/dist/src/commands/wallet.js.map +1 -0
  49. package/dist/src/commands/webhook.d.ts +3 -0
  50. package/dist/src/commands/webhook.d.ts.map +1 -0
  51. package/dist/src/commands/webhook.js +172 -0
  52. package/dist/src/commands/webhook.js.map +1 -0
  53. package/dist/src/config.d.ts +2 -2
  54. package/dist/src/config.d.ts.map +1 -1
  55. package/dist/src/config.js +2 -3
  56. package/dist/src/config.js.map +1 -1
  57. package/dist/src/index.js +19 -9
  58. package/dist/src/index.js.map +1 -1
  59. package/dist/src/lib/resolve.d.ts.map +1 -1
  60. package/dist/src/lib/resolve.js +4 -5
  61. package/dist/src/lib/resolve.js.map +1 -1
  62. package/dist/src/mcp/context.d.ts +1 -9
  63. package/dist/src/mcp/context.d.ts.map +1 -1
  64. package/dist/src/mcp/context.js +1 -2
  65. package/dist/src/mcp/context.js.map +1 -1
  66. package/dist/src/mcp/server.d.ts.map +1 -1
  67. package/dist/src/mcp/server.js +2 -59
  68. package/dist/src/mcp/server.js.map +1 -1
  69. package/dist/src/mcp/tools/account.d.ts.map +1 -1
  70. package/dist/src/mcp/tools/account.js +1 -3
  71. package/dist/src/mcp/tools/account.js.map +1 -1
  72. package/dist/src/mcp/tools/files.d.ts +2 -3
  73. package/dist/src/mcp/tools/files.d.ts.map +1 -1
  74. package/dist/src/mcp/tools/files.js +46 -49
  75. package/dist/src/mcp/tools/files.js.map +1 -1
  76. package/dist/src/mcp/tools/vaults.js +2 -2
  77. package/dist/src/mcp/tools/vaults.js.map +1 -1
  78. package/dist/src/seal.d.ts +16 -0
  79. package/dist/src/seal.d.ts.map +1 -1
  80. package/dist/src/seal.js +23 -0
  81. package/dist/src/seal.js.map +1 -1
  82. package/dist/src/tui/files-panel.d.ts +31 -2
  83. package/dist/src/tui/files-panel.d.ts.map +1 -1
  84. package/dist/src/tui/files-panel.js +119 -13
  85. package/dist/src/tui/files-panel.js.map +1 -1
  86. package/dist/src/tui/index.d.ts.map +1 -1
  87. package/dist/src/tui/index.js +252 -48
  88. package/dist/src/tui/index.js.map +1 -1
  89. package/dist/src/tui/overview.d.ts.map +1 -1
  90. package/dist/src/tui/overview.js +21 -9
  91. package/dist/src/tui/overview.js.map +1 -1
  92. package/dist/src/tui/trash-screen.d.ts +4 -0
  93. package/dist/src/tui/trash-screen.d.ts.map +1 -0
  94. package/dist/src/tui/trash-screen.js +190 -0
  95. package/dist/src/tui/trash-screen.js.map +1 -0
  96. package/dist/src/tui/vaults-panel.d.ts +8 -0
  97. package/dist/src/tui/vaults-panel.d.ts.map +1 -1
  98. package/dist/src/tui/vaults-panel.js +45 -6
  99. package/dist/src/tui/vaults-panel.js.map +1 -1
  100. package/dist/src/version.d.ts +2 -0
  101. package/dist/src/version.d.ts.map +1 -0
  102. package/dist/src/version.js +21 -0
  103. package/dist/src/version.js.map +1 -0
  104. package/package.json +3 -3
  105. package/src/__tests__/seal.test.ts +7 -54
  106. package/src/commands/account.ts +0 -1
  107. package/src/commands/auth.ts +7 -5
  108. package/src/commands/download.ts +38 -28
  109. package/src/commands/export.ts +37 -81
  110. package/src/commands/files.ts +95 -11
  111. package/src/commands/folder.ts +169 -0
  112. package/src/commands/mcp.ts +16 -10
  113. package/src/commands/rehydrate.ts +15 -8
  114. package/src/commands/sui.ts +69 -0
  115. package/src/commands/trash.ts +121 -0
  116. package/src/commands/upload.ts +98 -31
  117. package/src/commands/vault.ts +2 -23
  118. package/src/commands/wallet.ts +183 -0
  119. package/src/commands/webhook.ts +193 -0
  120. package/src/config.ts +3 -4
  121. package/src/index.ts +19 -10
  122. package/src/lib/resolve.ts +3 -4
  123. package/src/mcp/context.ts +1 -11
  124. package/src/mcp/server.ts +2 -70
  125. package/src/mcp/tools/account.ts +1 -3
  126. package/src/mcp/tools/files.ts +50 -63
  127. package/src/mcp/tools/vaults.ts +3 -3
  128. package/src/seal.ts +34 -1
  129. package/src/tui/files-panel.ts +140 -14
  130. package/src/tui/index.ts +264 -52
  131. package/src/tui/overview.ts +20 -9
  132. package/src/tui/trash-screen.ts +203 -0
  133. package/src/tui/vaults-panel.ts +55 -6
  134. package/src/version.ts +21 -0
  135. package/vitest.config.ts +1 -0
  136. package/dist/src/client.d.ts +0 -120
  137. package/dist/src/client.d.ts.map +0 -1
  138. package/dist/src/client.js +0 -152
  139. package/dist/src/client.js.map +0 -1
  140. package/dist/src/commands/decrypt.d.ts +0 -15
  141. package/dist/src/commands/decrypt.d.ts.map +0 -1
  142. package/dist/src/commands/decrypt.js +0 -224
  143. package/dist/src/commands/decrypt.js.map +0 -1
  144. package/dist/src/commands/encryption.d.ts +0 -3
  145. package/dist/src/commands/encryption.d.ts.map +0 -1
  146. package/dist/src/commands/encryption.js +0 -254
  147. package/dist/src/commands/encryption.js.map +0 -1
  148. package/dist/src/crypto.d.ts +0 -16
  149. package/dist/src/crypto.d.ts.map +0 -1
  150. package/dist/src/crypto.js +0 -95
  151. package/dist/src/crypto.js.map +0 -1
  152. package/dist/src/lib/keyring.d.ts +0 -4
  153. package/dist/src/lib/keyring.d.ts.map +0 -1
  154. package/dist/src/lib/keyring.js +0 -49
  155. package/dist/src/lib/keyring.js.map +0 -1
  156. package/src/__tests__/crypto.test.ts +0 -315
  157. package/src/commands/decrypt.ts +0 -276
  158. package/src/commands/encryption.ts +0 -305
  159. package/src/crypto.ts +0 -130
  160. package/src/lib/keyring.ts +0 -50
@@ -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
- // Placeholder for password user fills in
104
- env.TUSKYDP_PASSWORD = '';
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
- .action(async (options: { target: string; apiKey?: string; apiUrl?: string }) => {
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
- console.log(chalk.yellow(' Remember to set TUSKYDP_PASSWORD in the config for private vault encryption:'));
209
- console.log(chalk.dim(` Edit the env block in ${target.configPath}`));
210
- console.log('');
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.'));
@@ -1,14 +1,17 @@
1
1
  import type { Command } from 'commander';
2
2
  import { writeFileSync } from 'fs';
3
3
  import { join } from 'path';
4
+ import ora from 'ora';
4
5
  import { resolveWalrusConfig } from '@tuskydp/shared/walrus-networks.js';
5
6
  import { formatBytes } from '../lib/output.js';
6
- import { createSpinner } from '../lib/progress.js';
7
7
 
8
8
  export async function rehydrateCommand(blobId: string, options: {
9
9
  output?: string;
10
+ stdout?: boolean;
10
11
  }, _program: Command) {
11
- const spinner = createSpinner('Fetching blob from Walrus...');
12
+ // When piping to stdout, send spinner to stderr to avoid polluting the binary stream
13
+ const spinnerStream = options.stdout ? process.stderr : process.stdout;
14
+ const spinner = ora({ text: 'Fetching blob from Walrus...', stream: spinnerStream as NodeJS.WritableStream });
12
15
  spinner.start();
13
16
 
14
17
  try {
@@ -24,12 +27,16 @@ export async function rehydrateCommand(blobId: string, options: {
24
27
  const arrayBuf = await response.arrayBuffer();
25
28
  const fileBuffer = Buffer.from(new Uint8Array(arrayBuf));
26
29
 
27
- // Sanitize blob ID for use as filename (replace non-filesystem-safe characters)
28
- const safeBlobId = blobId.replace(/[^a-zA-Z0-9_-]/g, '_');
29
- const outputPath = options.output || join(process.cwd(), `blob-${safeBlobId}`);
30
- writeFileSync(outputPath, fileBuffer);
31
-
32
- spinner.succeed(`Downloaded -> ${outputPath} (${formatBytes(fileBuffer.length)})`);
30
+ if (options.stdout) {
31
+ spinner.stop();
32
+ process.stdout.write(fileBuffer);
33
+ } else {
34
+ // Sanitize blob ID for use as filename (replace non-filesystem-safe characters)
35
+ const safeBlobId = blobId.replace(/[^a-zA-Z0-9_-]/g, '_');
36
+ const outputPath = options.output || join(process.cwd(), `blob-${safeBlobId}`);
37
+ writeFileSync(outputPath, fileBuffer);
38
+ spinner.succeed(`Downloaded -> ${outputPath} (${formatBytes(fileBuffer.length)})`);
39
+ }
33
40
  } catch (err: any) {
34
41
  spinner.fail(`Rehydrate failed: ${err.message}`);
35
42
  process.exit(1);
@@ -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
+ }
@@ -0,0 +1,121 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { cliConfig } from '../config.js';
5
+ import { getSDKClientFromParent } from '../sdk.js';
6
+ import { createTable, formatBytes, formatDate, shortId } from '../lib/output.js';
7
+
8
+ export function registerTrashCommands(program: Command) {
9
+ const trash = program.command('trash').description('Manage trashed items');
10
+
11
+ // ── list ────────────────────────────────────────────────────────────
12
+ trash.command('list')
13
+ .description('List trashed files and vaults')
14
+ .action(async () => {
15
+ const sdk = getSDKClientFromParent(trash);
16
+ const root = trash.parent || trash;
17
+ const format = root.opts().format || cliConfig.get('outputFormat');
18
+ const trashed = await sdk.trash.list();
19
+
20
+ if (format === 'json') {
21
+ console.log(JSON.stringify(trashed, null, 2));
22
+ return;
23
+ }
24
+
25
+ const totalItems = trashed.files.length + trashed.vaults.length;
26
+ if (totalItems === 0) {
27
+ console.log(chalk.dim('Trash is empty.'));
28
+ return;
29
+ }
30
+
31
+ if (trashed.vaults.length > 0) {
32
+ console.log(chalk.bold('Trashed Vaults:'));
33
+ const table = createTable(['Name', 'Visibility', 'Files', 'Size', 'Deleted', 'ID']);
34
+ for (const v of trashed.vaults) {
35
+ table.push([
36
+ v.name,
37
+ v.visibility,
38
+ String(v.fileCount),
39
+ formatBytes(v.totalSizeBytes),
40
+ v.deletedAt ? formatDate(v.deletedAt) : 'N/A',
41
+ chalk.dim(shortId(v.id)),
42
+ ]);
43
+ }
44
+ console.log(table.toString());
45
+ }
46
+
47
+ if (trashed.files.length > 0) {
48
+ if (trashed.vaults.length > 0) console.log('');
49
+ console.log(chalk.bold('Trashed Files:'));
50
+ const table = createTable(['Name', 'Size', 'Status', 'Deleted', 'ID']);
51
+ for (const f of trashed.files) {
52
+ table.push([
53
+ f.name,
54
+ formatBytes(f.sizeBytes),
55
+ f.status,
56
+ f.deletedAt ? formatDate(f.deletedAt) : 'N/A',
57
+ chalk.dim(shortId(f.id)),
58
+ ]);
59
+ }
60
+ console.log(table.toString());
61
+ }
62
+
63
+ console.log(chalk.dim(`\n${totalItems} item(s) in trash. Items are permanently deleted after 7 days.`));
64
+ });
65
+
66
+ // ── restore ─────────────────────────────────────────────────────────
67
+ trash.command('restore <id>')
68
+ .description('Restore a trashed item (file or vault)')
69
+ .action(async (id: string) => {
70
+ const sdk = getSDKClientFromParent(trash);
71
+ const result = await sdk.trash.restore(id);
72
+ console.log(chalk.green(`Restored ${result.type || 'item'}: ${id}`));
73
+ });
74
+
75
+ // ── delete ──────────────────────────────────────────────────────────
76
+ trash.command('delete <id>')
77
+ .description('Permanently delete a single trashed item')
78
+ .option('--force', 'Skip confirmation prompt')
79
+ .action(async (id: string, options) => {
80
+ const sdk = getSDKClientFromParent(trash);
81
+
82
+ if (!options.force) {
83
+ const answers = await inquirer.prompt([{
84
+ type: 'confirm',
85
+ name: 'confirm',
86
+ message: 'Permanently delete this item? This cannot be undone.',
87
+ default: false,
88
+ }]);
89
+ if (!answers.confirm) return;
90
+ }
91
+
92
+ await sdk.trash.delete(id);
93
+ console.log(chalk.green(`Permanently deleted: ${id}`));
94
+ });
95
+
96
+ // ── empty ───────────────────────────────────────────────────────────
97
+ trash.command('empty')
98
+ .description('Permanently delete ALL trashed items')
99
+ .option('--force', 'Skip confirmation prompt')
100
+ .action(async (options) => {
101
+ const sdk = getSDKClientFromParent(trash);
102
+
103
+ if (!options.force) {
104
+ const answers = await inquirer.prompt([{
105
+ type: 'confirm',
106
+ name: 'confirm',
107
+ message: 'Permanently delete ALL trashed items? This cannot be undone.',
108
+ default: false,
109
+ }]);
110
+ if (!answers.confirm) return;
111
+ }
112
+
113
+ await sdk.trash.empty();
114
+ console.log(chalk.green('Trash emptied.'));
115
+ });
116
+
117
+ // Default action for `tusky trash` with no subcommand → run list
118
+ trash.action(async () => {
119
+ await trash.commands.find(c => c.name() === 'list')?.parseAsync([], { from: 'user' });
120
+ });
121
+ }
@@ -9,9 +9,16 @@ 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 } from '../crypto.js';
13
- import { loadMasterKey } from '../lib/keyring.js';
14
- import { isSealConfigured, sealEncrypt, getSuiKeypair } from '../seal.js';
12
+ import { isSealConfigured, sealEncrypt, sealEncryptMetadata, getSuiKeypair } from '../seal.js';
13
+
14
+ async function readStdin(): Promise<Buffer> {
15
+ return new Promise((resolve, reject) => {
16
+ const chunks: Buffer[] = [];
17
+ process.stdin.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
18
+ process.stdin.on('end', () => resolve(Buffer.concat(chunks)));
19
+ process.stdin.on('error', reject);
20
+ });
21
+ }
15
22
 
16
23
  async function expandPaths(paths: string[], recursive?: boolean): Promise<string[]> {
17
24
  const result: string[] = [];
@@ -44,25 +51,97 @@ async function expandPaths(paths: string[], recursive?: boolean): Promise<string
44
51
  export async function uploadCommand(paths: string[], options: {
45
52
  vault?: string;
46
53
  recursive?: boolean;
54
+ folder?: string;
55
+ content?: string;
56
+ stdin?: boolean;
57
+ name?: string;
47
58
  }, program: Command) {
48
59
  const apiUrl = getApiUrl(program.opts().apiUrl);
49
60
  const apiKey = getApiKey(program.opts().apiKey);
50
61
  const sdk = createSDKClient(apiUrl, apiKey);
51
62
 
52
- const vaultId = await resolveVault(sdk, options.vault);
53
- const vault = await sdk.vaults.get(vaultId);
54
- const isPrivate = vault.visibility === 'private';
55
- const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
56
-
57
- let masterKey: Buffer | null = null;
58
- if (isPrivate) {
59
- masterKey = loadMasterKey();
60
- if (!masterKey) {
61
- console.error(chalk.red('Encryption session not unlocked. Run: tusky encryption unlock'));
63
+ // ── Inline content / stdin mode ──────────────────────────────────
64
+ if (options.content !== undefined || options.stdin) {
65
+ if (!options.name) {
66
+ console.error(chalk.red('--name <filename> is required when using --content or --stdin'));
62
67
  process.exit(1);
63
68
  }
69
+
70
+ const fileName = options.name;
71
+ let fileBuffer: Buffer;
72
+ if (options.stdin) {
73
+ fileBuffer = await readStdin();
74
+ } else {
75
+ fileBuffer = Buffer.from(options.content as string, 'utf8');
76
+ }
77
+
78
+ const vaultId = await resolveVault(sdk, options.vault);
79
+ const vault = await sdk.vaults.get(vaultId);
80
+ const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
81
+
82
+ const spinner = createSpinner(`Uploading ${fileName}`);
83
+ spinner.start();
84
+
85
+ try {
86
+ const mimeType = lookup(fileName) || 'application/octet-stream';
87
+ let uploadBody: Buffer;
88
+ let encryptionMeta: {
89
+ sealIdentity?: string;
90
+ sealEncryptedObject?: string;
91
+ encryptedMetadata?: string;
92
+ } = {};
93
+
94
+ if (isShared) {
95
+ spinner.text = `SEAL encrypting ${fileName}...`;
96
+ const fileNonce = randomUUID();
97
+ const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
98
+ uploadBody = Buffer.from(sealResult.encryptedData);
99
+ encryptionMeta = {
100
+ sealIdentity: sealResult.sealIdentity,
101
+ sealEncryptedObject: sealResult.sealEncryptedObject,
102
+ encryptedMetadata: await sealEncryptMetadata(fileName, mimeType, vault, fileNonce),
103
+ };
104
+ } else {
105
+ uploadBody = fileBuffer;
106
+ }
107
+
108
+ spinner.text = `Requesting upload URL for ${fileName}...`;
109
+ const { fileId, uploadUrl } = await sdk.files.requestUpload({
110
+ name: fileName,
111
+ mimeType,
112
+ sizeBytes: uploadBody.length,
113
+ vaultId,
114
+ ...(options.folder ? { folderId: options.folder } : {}),
115
+ ...encryptionMeta,
116
+ });
117
+
118
+ spinner.text = `Uploading ${fileName} (${formatBytes(uploadBody.length)})...`;
119
+ const uploadResponse = await fetch(uploadUrl, {
120
+ method: 'PUT',
121
+ headers: { 'Content-Type': 'application/octet-stream' },
122
+ body: new Uint8Array(uploadBody),
123
+ });
124
+ if (!uploadResponse.ok) throw new Error(`Upload failed: ${uploadResponse.status}`);
125
+
126
+ spinner.text = `Confirming ${fileName}...`;
127
+ await sdk.files.confirmUpload(fileId);
128
+
129
+ spinner.succeed(`${fileName} -> ${fileId} (${formatBytes(uploadBody.length)})`);
130
+ } catch (err: any) {
131
+ spinner.fail(`Failed: ${fileName} — ${err.message}`);
132
+ if (err.statusCode === 402) {
133
+ console.error(chalk.yellow(' This may require topping up your wallet balance.'));
134
+ console.error(chalk.dim(' Check your wallet: tusky wallet info'));
135
+ }
136
+ }
137
+ return;
64
138
  }
65
139
 
140
+ // ── File path mode (original behavior) ───────────────────────────
141
+ const vaultId = await resolveVault(sdk, options.vault);
142
+ const vault = await sdk.vaults.get(vaultId);
143
+ const isShared = vault.visibility === 'shared' && isSealConfigured(vault);
144
+
66
145
  if (isShared) {
67
146
  const keypair = getSuiKeypair();
68
147
  if (!keypair) {
@@ -91,25 +170,12 @@ export async function uploadCommand(paths: string[], options: {
91
170
 
92
171
  let uploadBody: Buffer;
93
172
  let encryptionMeta: {
94
- wrappedKey?: string;
95
- encryptionIv?: string;
96
- plaintextSizeBytes?: number;
97
- plaintextChecksumSha256?: string;
98
173
  sealIdentity?: string;
99
174
  sealEncryptedObject?: string;
175
+ encryptedMetadata?: string;
100
176
  } = {};
101
177
 
102
- if (isPrivate && masterKey) {
103
- spinner.text = `Encrypting ${basename(filePath)}...`;
104
- const { ciphertext, wrappedKey, iv, plaintextChecksum } = encryptBuffer(fileBuffer, masterKey);
105
- uploadBody = ciphertext;
106
- encryptionMeta = {
107
- wrappedKey,
108
- encryptionIv: iv,
109
- plaintextSizeBytes: stat.size,
110
- plaintextChecksumSha256: plaintextChecksum,
111
- };
112
- } else if (isShared) {
178
+ if (isShared) {
113
179
  spinner.text = `SEAL encrypting ${basename(filePath)}...`;
114
180
  const fileNonce = randomUUID();
115
181
  const sealResult = await sealEncrypt(new Uint8Array(fileBuffer), vault, fileNonce);
@@ -117,7 +183,7 @@ export async function uploadCommand(paths: string[], options: {
117
183
  encryptionMeta = {
118
184
  sealIdentity: sealResult.sealIdentity,
119
185
  sealEncryptedObject: sealResult.sealEncryptedObject,
120
- plaintextSizeBytes: stat.size,
186
+ encryptedMetadata: await sealEncryptMetadata(basename(filePath), mimeType, vault, fileNonce),
121
187
  };
122
188
  } else {
123
189
  uploadBody = fileBuffer;
@@ -130,6 +196,7 @@ export async function uploadCommand(paths: string[], options: {
130
196
  mimeType,
131
197
  sizeBytes: uploadBody.length,
132
198
  vaultId,
199
+ ...(options.folder ? { folderId: options.folder } : {}),
133
200
  ...encryptionMeta,
134
201
  });
135
202
 
@@ -154,13 +221,13 @@ export async function uploadCommand(paths: string[], options: {
154
221
  // Surface PPU payment details if present (402 errors)
155
222
  if (err.statusCode === 402) {
156
223
  console.error(chalk.yellow(' This may require topping up your wallet balance.'));
157
- console.error(chalk.dim(' Check your wallet: tusky account usage'));
224
+ console.error(chalk.dim(' Check your wallet: tusky wallet info'));
158
225
  }
159
226
  }
160
227
  }
161
228
 
162
229
  if (filePaths.length > 1) {
163
- const label = isPrivate ? 'private' : isShared ? 'shared' : 'public';
230
+ const label = isShared ? 'shared' : 'public';
164
231
  console.log(`\nUploaded ${successCount} file(s) (${formatBytes(totalSize)}) to vault "${vault.name}" [${label}]`);
165
232
  }
166
233
  }
@@ -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 (private/encrypted by default)')
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' | 'private' | 'shared' = 'private';
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();