@tuskydp/cli 0.1.0 → 0.1.1

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 (87) hide show
  1. package/bin/tuskydp.ts +2 -0
  2. package/dist/src/commands/account.d.ts.map +1 -1
  3. package/dist/src/commands/account.js +5 -2
  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 +2 -1
  7. package/dist/src/commands/auth.js.map +1 -1
  8. package/dist/src/commands/files.d.ts.map +1 -1
  9. package/dist/src/commands/files.js +9 -4
  10. package/dist/src/commands/files.js.map +1 -1
  11. package/dist/src/commands/mcp.js +1 -1
  12. package/dist/src/commands/mcp.js.map +1 -1
  13. package/dist/src/commands/rehydrate.d.ts.map +1 -1
  14. package/dist/src/commands/rehydrate.js +5 -2
  15. package/dist/src/commands/rehydrate.js.map +1 -1
  16. package/dist/src/commands/upload.d.ts.map +1 -1
  17. package/dist/src/commands/upload.js +5 -0
  18. package/dist/src/commands/upload.js.map +1 -1
  19. package/dist/src/config.d.ts +0 -2
  20. package/dist/src/config.d.ts.map +1 -1
  21. package/dist/src/config.js +5 -4
  22. package/dist/src/config.js.map +1 -1
  23. package/dist/src/index.js +16 -2
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/lib/keyring.d.ts.map +1 -1
  26. package/dist/src/lib/keyring.js +3 -5
  27. package/dist/src/lib/keyring.js.map +1 -1
  28. package/dist/src/lib/output.js +1 -1
  29. package/dist/src/lib/output.js.map +1 -1
  30. package/dist/src/lib/resolve.js +1 -1
  31. package/dist/src/lib/resolve.js.map +1 -1
  32. package/dist/src/mcp/tools/files.d.ts.map +1 -1
  33. package/dist/src/mcp/tools/files.js +20 -0
  34. package/dist/src/mcp/tools/files.js.map +1 -1
  35. package/dist/src/mcp/tools/folders.d.ts.map +1 -1
  36. package/dist/src/mcp/tools/folders.js +15 -0
  37. package/dist/src/mcp/tools/folders.js.map +1 -1
  38. package/dist/src/mcp/tools/trash.d.ts.map +1 -1
  39. package/dist/src/mcp/tools/trash.js +14 -0
  40. package/dist/src/mcp/tools/trash.js.map +1 -1
  41. package/dist/src/sdk.d.ts +1 -1
  42. package/dist/src/sdk.d.ts.map +1 -1
  43. package/dist/src/sdk.js +3 -3
  44. package/dist/src/sdk.js.map +1 -1
  45. package/dist/src/tui/auth-screen.d.ts.map +1 -1
  46. package/dist/src/tui/auth-screen.js +7 -1
  47. package/dist/src/tui/auth-screen.js.map +1 -1
  48. package/package.json +12 -18
  49. package/src/__tests__/crypto.test.ts +315 -0
  50. package/src/commands/account.ts +82 -0
  51. package/src/commands/auth.ts +190 -0
  52. package/src/commands/decrypt.ts +276 -0
  53. package/src/commands/download.ts +82 -0
  54. package/src/commands/encryption.ts +305 -0
  55. package/src/commands/export.ts +251 -0
  56. package/src/commands/files.ts +192 -0
  57. package/src/commands/mcp.ts +220 -0
  58. package/src/commands/rehydrate.ts +37 -0
  59. package/src/commands/tui.ts +11 -0
  60. package/src/commands/upload.ts +143 -0
  61. package/src/commands/vault.ts +132 -0
  62. package/src/config.ts +38 -0
  63. package/src/crypto.ts +130 -0
  64. package/src/index.ts +79 -0
  65. package/src/lib/keyring.ts +50 -0
  66. package/src/lib/output.ts +36 -0
  67. package/src/lib/progress.ts +5 -0
  68. package/src/lib/resolve.ts +26 -0
  69. package/src/mcp/context.ts +22 -0
  70. package/src/mcp/server.ts +140 -0
  71. package/src/mcp/tools/account.ts +40 -0
  72. package/src/mcp/tools/files.ts +428 -0
  73. package/src/mcp/tools/folders.ts +109 -0
  74. package/src/mcp/tools/helpers.ts +28 -0
  75. package/src/mcp/tools/trash.ts +82 -0
  76. package/src/mcp/tools/vaults.ts +114 -0
  77. package/src/sdk.ts +115 -0
  78. package/src/tui/auth-screen.ts +176 -0
  79. package/src/tui/dialogs.ts +339 -0
  80. package/src/tui/files-panel.ts +165 -0
  81. package/src/tui/helpers.ts +206 -0
  82. package/src/tui/index.ts +420 -0
  83. package/src/tui/overview.ts +155 -0
  84. package/src/tui/status-bar.ts +61 -0
  85. package/src/tui/vaults-panel.ts +143 -0
  86. package/tsconfig.json +9 -0
  87. package/vitest.config.ts +7 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * MCP tools — Vault management
3
+ */
4
+
5
+ import { z } from 'zod';
6
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import type { McpContext } from '../context.js';
8
+ import { wrapToolError } from './helpers.js';
9
+
10
+ export function registerVaultTools(server: McpServer, ctx: McpContext) {
11
+ // ── Create vault ─────────────────────────────────────────────────────
12
+ server.tool(
13
+ 'tusky_vault_create',
14
+ 'Create a new storage vault. Visibility is immutable after creation.',
15
+ {
16
+ name: z.string().describe('Vault name'),
17
+ description: z.string().optional().describe('Vault description'),
18
+ visibility: z.enum(['public', 'private']).optional().describe(
19
+ 'Vault visibility. "private" enables client-side encryption. Defaults to "private".',
20
+ ),
21
+ },
22
+ async ({ name, description, visibility }) => {
23
+ try {
24
+ const vault = await ctx.sdk.vaults.create({
25
+ name,
26
+ description,
27
+ visibility: visibility ?? 'private',
28
+ });
29
+ return {
30
+ content: [{ type: 'text' as const, text: JSON.stringify(vault, null, 2) }],
31
+ };
32
+ } catch (err) {
33
+ return wrapToolError(err);
34
+ }
35
+ },
36
+ );
37
+
38
+ // ── List vaults ──────────────────────────────────────────────────────
39
+ server.tool(
40
+ 'tusky_vault_list',
41
+ 'List all vaults in the account',
42
+ {},
43
+ async () => {
44
+ try {
45
+ const vaults = await ctx.sdk.vaults.list();
46
+ return {
47
+ content: [{ type: 'text' as const, text: JSON.stringify(vaults, null, 2) }],
48
+ };
49
+ } catch (err) {
50
+ return wrapToolError(err);
51
+ }
52
+ },
53
+ );
54
+
55
+ // ── Get vault ────────────────────────────────────────────────────────
56
+ server.tool(
57
+ 'tusky_vault_get',
58
+ 'Get details of a specific vault by ID',
59
+ {
60
+ vaultId: z.string().describe('Vault ID'),
61
+ },
62
+ async ({ vaultId }) => {
63
+ try {
64
+ const vault = await ctx.sdk.vaults.get(vaultId);
65
+ return {
66
+ content: [{ type: 'text' as const, text: JSON.stringify(vault, null, 2) }],
67
+ };
68
+ } catch (err) {
69
+ return wrapToolError(err);
70
+ }
71
+ },
72
+ );
73
+
74
+ // ── Update vault ─────────────────────────────────────────────────────
75
+ server.tool(
76
+ 'tusky_vault_update',
77
+ 'Update a vault name or description (visibility cannot be changed)',
78
+ {
79
+ vaultId: z.string().describe('Vault ID'),
80
+ name: z.string().optional().describe('New vault name'),
81
+ description: z.string().optional().describe('New vault description'),
82
+ },
83
+ async ({ vaultId, name, description }) => {
84
+ try {
85
+ const vault = await ctx.sdk.vaults.update(vaultId, { name, description });
86
+ return {
87
+ content: [{ type: 'text' as const, text: JSON.stringify(vault, null, 2) }],
88
+ };
89
+ } catch (err) {
90
+ return wrapToolError(err);
91
+ }
92
+ },
93
+ );
94
+
95
+ // ── Delete vault ─────────────────────────────────────────────────────
96
+ server.tool(
97
+ 'tusky_vault_delete',
98
+ 'Delete a vault. Use force=true to delete even if the vault contains files.',
99
+ {
100
+ vaultId: z.string().describe('Vault ID'),
101
+ force: z.boolean().optional().describe('Force delete even if vault contains files'),
102
+ },
103
+ async ({ vaultId, force }) => {
104
+ try {
105
+ await ctx.sdk.vaults.delete(vaultId, { force });
106
+ return {
107
+ content: [{ type: 'text' as const, text: `Vault ${vaultId} deleted successfully.` }],
108
+ };
109
+ } catch (err) {
110
+ return wrapToolError(err);
111
+ }
112
+ },
113
+ );
114
+ }
package/src/sdk.ts ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * SDK client factory for CLI commands + AuthClient for pre-auth flows.
3
+ *
4
+ * The TuskyClient SDK requires an API key, so it can't be used for
5
+ * register/login/createApiKeyWithJwt flows. AuthClient handles those
6
+ * with raw fetch calls.
7
+ */
8
+
9
+ import { TuskyClient, TuskyError } from '@tuskydp/sdk';
10
+ import type { Command } from 'commander';
11
+ import { getApiUrl, getApiKey } from './config.js';
12
+
13
+ const CLI_VERSION = '0.1.0';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // SDK client factory (for authenticated commands)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Create a TuskyClient from Commander options (uses stored API key). */
20
+ export function getSDKClient(program: Command): TuskyClient {
21
+ const apiUrl = getApiUrl(program.opts().apiUrl);
22
+ const apiKey = getApiKey(program.opts().apiKey);
23
+ return new TuskyClient({ apiKey, baseUrl: apiUrl });
24
+ }
25
+
26
+ /**
27
+ * Create a TuskyClient from Commander options, resolved from a sub-command
28
+ * whose parent holds the global options.
29
+ */
30
+ export function getSDKClientFromParent(program: Command): TuskyClient {
31
+ const root = program.parent || program;
32
+ const apiUrl = getApiUrl(root.opts().apiUrl);
33
+ const apiKey = getApiKey(root.opts().apiKey);
34
+ return new TuskyClient({ apiKey, baseUrl: apiUrl });
35
+ }
36
+
37
+ /** Create a TuskyClient from explicit url + key (used in TUI). */
38
+ export function createSDKClient(apiUrl: string, apiKey: string): TuskyClient {
39
+ return new TuskyClient({ apiKey, baseUrl: apiUrl });
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // AuthClient — pre-authentication flows (no API key needed)
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /**
47
+ * Lightweight client for unauthenticated / JWT-authenticated API calls.
48
+ * Used for register, login, createApiKeyWithJwt, and getPlans.
49
+ */
50
+ export class AuthClient {
51
+ private apiUrl: string;
52
+
53
+ constructor(apiUrl: string) {
54
+ this.apiUrl = apiUrl.replace(/\/$/, '');
55
+ }
56
+
57
+ private async fetchJSON<T>(url: string, init: RequestInit): Promise<T> {
58
+ const response = await fetch(url, init);
59
+ if (!response.ok) {
60
+ const body = await response.json().catch(() => ({ error: response.statusText }));
61
+ throw new TuskyError(response.status, (body as Record<string, string>).error || 'Unknown error');
62
+ }
63
+ return response.json() as Promise<T>;
64
+ }
65
+
66
+ async register(email: string, password: string, accessCode: string, displayName?: string) {
67
+ return this.fetchJSON<{ user: { email: string; id: string }; accessToken: string; refreshToken: string }>(
68
+ `${this.apiUrl}/api/auth/register`,
69
+ {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `tusky-cli/${CLI_VERSION}` },
72
+ body: JSON.stringify({ email, password, displayName, accessCode }),
73
+ },
74
+ );
75
+ }
76
+
77
+ async login(email: string, password: string) {
78
+ return this.fetchJSON<{ user: { email: string; id: string }; accessToken: string; refreshToken: string }>(
79
+ `${this.apiUrl}/api/auth/login`,
80
+ {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `tusky-cli/${CLI_VERSION}` },
83
+ body: JSON.stringify({ email, password }),
84
+ },
85
+ );
86
+ }
87
+
88
+ async createApiKeyWithJwt(accessToken: string, name: string) {
89
+ return this.fetchJSON<{ apiKey: string; name: string; keyPrefix: string }>(
90
+ `${this.apiUrl}/api/auth/api-keys`,
91
+ {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Content-Type': 'application/json',
95
+ 'Authorization': `Bearer ${accessToken}`,
96
+ 'User-Agent': `tusky-cli/${CLI_VERSION}`,
97
+ },
98
+ body: JSON.stringify({ name }),
99
+ },
100
+ );
101
+ }
102
+
103
+ async getPlans() {
104
+ return this.fetchJSON<{ plans: any[] }>(
105
+ `${this.apiUrl}/api/plans`,
106
+ {
107
+ method: 'GET',
108
+ headers: { 'User-Agent': `tusky-cli/${CLI_VERSION}` },
109
+ },
110
+ );
111
+ }
112
+ }
113
+
114
+ // Re-export TuskyError for catch blocks that need to check error type
115
+ export { TuskyError } from '@tuskydp/sdk';
@@ -0,0 +1,176 @@
1
+ import blessed from 'blessed';
2
+ import { TuskyClient } from '@tuskydp/sdk';
3
+ import { AuthClient, createSDKClient } from '../sdk.js';
4
+ import { cliConfig, getApiUrl } from '../config.js';
5
+ import { textInputDialog, selectDialog } from './dialogs.js';
6
+ import { showError, showMessage } from './helpers.js';
7
+
8
+ export async function showAuthScreen(
9
+ screen: blessed.Widgets.Screen,
10
+ ): Promise<{ apiKey: string; apiUrl: string } | null> {
11
+ return new Promise((resolve) => {
12
+ const container = blessed.box({
13
+ parent: screen,
14
+ top: 0,
15
+ left: 0,
16
+ width: '100%',
17
+ height: '100%',
18
+ tags: true,
19
+ });
20
+
21
+ const logo = blessed.box({
22
+ parent: container,
23
+ top: 2,
24
+ left: 'center',
25
+ width: 'shrink',
26
+ height: 'shrink',
27
+ tags: true,
28
+ content: [
29
+ '{bold}{cyan-fg}╔════════════════════════════╗{/cyan-fg}{/bold}',
30
+ '{bold}{cyan-fg}║ TUSKY TUI ║{/cyan-fg}{/bold}',
31
+ '{bold}{cyan-fg}║ Decentralized Storage ║{/cyan-fg}{/bold}',
32
+ '{bold}{cyan-fg}╚════════════════════════════╝{/cyan-fg}{/bold}',
33
+ ].join('\n'),
34
+ });
35
+
36
+ const prompt = blessed.box({
37
+ parent: container,
38
+ top: 8,
39
+ left: 'center',
40
+ width: 40,
41
+ height: 3,
42
+ tags: true,
43
+ content: '{center}Not authenticated.{/center}\n{center}Choose an option to continue:{/center}',
44
+ });
45
+
46
+ const menu = blessed.list({
47
+ parent: container,
48
+ top: 12,
49
+ left: 'center',
50
+ width: 30,
51
+ height: 5,
52
+ border: { type: 'line' },
53
+ style: {
54
+ border: { fg: 'cyan' },
55
+ fg: 'white',
56
+ selected: {
57
+ bg: 'cyan',
58
+ fg: 'black',
59
+ },
60
+ },
61
+ keys: true,
62
+ vi: true,
63
+ items: ['Sign Up (email)', 'Login (email)', 'Login (API key)'],
64
+ interactive: true,
65
+ });
66
+
67
+ const quitHint = blessed.box({
68
+ parent: container,
69
+ top: 18,
70
+ left: 'center',
71
+ width: 'shrink',
72
+ height: 1,
73
+ tags: true,
74
+ content: '{gray-fg}q to quit{/gray-fg}',
75
+ });
76
+
77
+ menu.on('select', async (_item: any, index: number) => {
78
+ if (index === 0) {
79
+ // Sign Up
80
+ const email = await textInputDialog(screen, 'Enter email');
81
+ if (!email) {
82
+ menu.focus();
83
+ screen.render();
84
+ return;
85
+ }
86
+ const password = await textInputDialog(screen, 'Enter password');
87
+ if (!password) {
88
+ menu.focus();
89
+ screen.render();
90
+ return;
91
+ }
92
+ const accessCode = await textInputDialog(screen, 'Enter access code');
93
+ if (!accessCode) {
94
+ menu.focus();
95
+ screen.render();
96
+ return;
97
+ }
98
+ const apiUrl = getApiUrl();
99
+ const authClient = new AuthClient(apiUrl);
100
+ try {
101
+ const { accessToken } = await authClient.register(email, password, accessCode);
102
+ const keyResult = await authClient.createApiKeyWithJwt(accessToken, 'TUI Key');
103
+ cliConfig.set('apiKey', keyResult.apiKey);
104
+ cliConfig.set('apiUrl', apiUrl);
105
+ container.destroy();
106
+ screen.render();
107
+ resolve({ apiKey: keyResult.apiKey, apiUrl });
108
+ } catch (err: any) {
109
+ showError(screen, err.message);
110
+ menu.focus();
111
+ screen.render();
112
+ }
113
+ } else if (index === 1) {
114
+ // Login with email
115
+ const email = await textInputDialog(screen, 'Enter email');
116
+ if (!email) {
117
+ menu.focus();
118
+ screen.render();
119
+ return;
120
+ }
121
+ const password = await textInputDialog(screen, 'Enter password');
122
+ if (!password) {
123
+ menu.focus();
124
+ screen.render();
125
+ return;
126
+ }
127
+ const apiUrl = getApiUrl();
128
+ const authClient = new AuthClient(apiUrl);
129
+ try {
130
+ const { accessToken } = await authClient.login(email, password);
131
+ const keyResult = await authClient.createApiKeyWithJwt(accessToken, 'CLI Key');
132
+ cliConfig.set('apiKey', keyResult.apiKey);
133
+ cliConfig.set('apiUrl', apiUrl);
134
+ container.destroy();
135
+ screen.render();
136
+ resolve({ apiKey: keyResult.apiKey, apiUrl });
137
+ } catch (err: any) {
138
+ showError(screen, 'Login failed: ' + err.message);
139
+ menu.focus();
140
+ screen.render();
141
+ }
142
+ } else {
143
+ // Login with API key
144
+ const key = await textInputDialog(screen, 'Paste API key');
145
+ if (!key) {
146
+ menu.focus();
147
+ screen.render();
148
+ return;
149
+ }
150
+ const apiUrl = getApiUrl();
151
+ const sdk = createSDKClient(apiUrl, key);
152
+ try {
153
+ await sdk.account.get();
154
+ cliConfig.set('apiKey', key);
155
+ cliConfig.set('apiUrl', apiUrl);
156
+ container.destroy();
157
+ screen.render();
158
+ resolve({ apiKey: key, apiUrl });
159
+ } catch (err: any) {
160
+ showError(screen, 'Invalid API key: ' + err.message);
161
+ menu.focus();
162
+ screen.render();
163
+ }
164
+ }
165
+ });
166
+
167
+ menu.key(['q'], () => {
168
+ container.destroy();
169
+ screen.render();
170
+ resolve(null);
171
+ });
172
+
173
+ menu.focus();
174
+ screen.render();
175
+ });
176
+ }