@ktmcp-cli/1password 1.0.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/AGENT.md ADDED
@@ -0,0 +1,48 @@
1
+ # 1Password CLI - AI Agent Guide
2
+
3
+ This CLI provides programmatic access to the 1Password Secrets/Connect API.
4
+
5
+ ## Quick Start for AI Agents
6
+
7
+ ```bash
8
+ 1password config set --token YOUR_CONNECT_TOKEN
9
+ 1password config set --base-url https://your-connect-server.example.com
10
+ 1password vaults list
11
+ ```
12
+
13
+ ## Available Commands
14
+
15
+ ### config
16
+ - `1password config set --token <token>` - Set Connect API token
17
+ - `1password config set --base-url <url>` - Set Connect server URL
18
+ - `1password config get <key>` - Get a config value
19
+ - `1password config list` - List all config values
20
+
21
+ ### vaults
22
+ - `1password vaults list` - List all vaults
23
+ - `1password vaults get <vault-id>` - Get vault details
24
+
25
+ ### items
26
+ - `1password items list <vault-id>` - List items in a vault
27
+ - `1password items list <vault-id> --filter <query>` - Filter items by title
28
+ - `1password items get <vault-id> <item-id>` - Get item details
29
+ - `1password items create <vault-id> --title <title> --category <category>` - Create item
30
+ - `1password items update <vault-id> <item-id> --title <title>` - Update item
31
+ - `1password items delete <vault-id> <item-id>` - Delete item
32
+
33
+ ### files
34
+ - `1password files list <vault-id> <item-id>` - List files on an item
35
+ - `1password files get <vault-id> <item-id> <file-id>` - Get file metadata
36
+
37
+ ## Output Format
38
+
39
+ All commands output formatted tables by default. Use `--json` flag for machine-readable JSON output.
40
+
41
+ ```bash
42
+ 1password vaults list --json
43
+ 1password items get <vault-id> <item-id> --json
44
+ ```
45
+
46
+ ## Authentication
47
+
48
+ This CLI uses 1Password Connect API tokens. You need a 1Password Connect server running and a token configured.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,92 @@
1
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
2
+ >
3
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
4
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
5
+
6
+ # 1Password CLI
7
+
8
+ Production-ready CLI for 1Password Secrets/Connect API.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/1password
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ 1password config set --token YOUR_CONNECT_TOKEN
20
+ 1password config set --base-url https://your-connect-server.example.com
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Vaults
26
+
27
+ ```bash
28
+ # List all vaults
29
+ 1password vaults list
30
+
31
+ # Get vault details
32
+ 1password vaults get <vault-id>
33
+ ```
34
+
35
+ ### Items
36
+
37
+ ```bash
38
+ # List items in a vault
39
+ 1password items list <vault-id>
40
+
41
+ # Filter items by title
42
+ 1password items list <vault-id> --filter "database"
43
+
44
+ # Get a specific item
45
+ 1password items get <vault-id> <item-id>
46
+
47
+ # Create a new item
48
+ 1password items create <vault-id> --title "My Login" --category LOGIN \
49
+ --fields '[{"label":"username","value":"myuser","type":"STRING"},{"label":"password","value":"secret","type":"CONCEALED"}]'
50
+
51
+ # Update an item
52
+ 1password items update <vault-id> <item-id> --title "Updated Title"
53
+
54
+ # Delete an item
55
+ 1password items delete <vault-id> <item-id>
56
+ ```
57
+
58
+ ### Files
59
+
60
+ ```bash
61
+ # List files attached to an item
62
+ 1password files list <vault-id> <item-id>
63
+
64
+ # Get file metadata
65
+ 1password files get <vault-id> <item-id> <file-id>
66
+ ```
67
+
68
+ ### Configuration
69
+
70
+ ```bash
71
+ # Set a config value
72
+ 1password config set --token YOUR_TOKEN
73
+
74
+ # Get a config value
75
+ 1password config get token
76
+
77
+ # List all config
78
+ 1password config list
79
+ ```
80
+
81
+ ## JSON Output
82
+
83
+ All commands support `--json` flag for machine-readable output:
84
+
85
+ ```bash
86
+ 1password vaults list --json
87
+ 1password items get <vault-id> <item-id> --json
88
+ ```
89
+
90
+ ## License
91
+
92
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/1password",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for 1Password Secrets/Connect API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "1password": "bin/1password.js"
9
+ },
10
+ "keywords": ["1password", "cli", "api", "ktmcp", "secrets", "vault"],
11
+ "author": "KTMCP",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "axios": "^1.6.7",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.1",
18
+ "conf": "^12.0.0"
19
+ },
20
+ "engines": { "node": ">=18.0.0" },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ktmcp-cli/1password.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/1password-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/1password/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,142 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const DEFAULT_BASE_URL = 'https://connect.1password.com';
5
+
6
+ function getClient() {
7
+ const token = getConfig('token');
8
+ const baseURL = getConfig('baseUrl') || DEFAULT_BASE_URL;
9
+
10
+ if (!token) {
11
+ throw new Error('API token not configured. Run: 1password config set --token YOUR_TOKEN');
12
+ }
13
+
14
+ return axios.create({
15
+ baseURL,
16
+ headers: {
17
+ 'Authorization': `Bearer ${token}`,
18
+ 'Content-Type': 'application/json',
19
+ 'Accept': 'application/json'
20
+ }
21
+ });
22
+ }
23
+
24
+ function handleApiError(error) {
25
+ if (error.response) {
26
+ const status = error.response.status;
27
+ const data = error.response.data;
28
+ if (status === 401) throw new Error('Authentication failed. Check your Connect token.');
29
+ if (status === 403) throw new Error('Access forbidden. Check your API permissions.');
30
+ if (status === 404) throw new Error('Resource not found.');
31
+ if (status === 429) throw new Error('Rate limit exceeded. Please wait before retrying.');
32
+ const message = data?.message || data?.detail || JSON.stringify(data);
33
+ throw new Error(`API Error (${status}): ${message}`);
34
+ } else if (error.request) {
35
+ throw new Error('No response from 1Password Connect API. Check your server URL and connection.');
36
+ } else {
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ // ============================================================
42
+ // VAULTS
43
+ // ============================================================
44
+
45
+ export async function listVaults() {
46
+ try {
47
+ const client = getClient();
48
+ const response = await client.get('/v1/vaults');
49
+ return response.data;
50
+ } catch (error) {
51
+ handleApiError(error);
52
+ }
53
+ }
54
+
55
+ export async function getVault(vaultId) {
56
+ try {
57
+ const client = getClient();
58
+ const response = await client.get(`/v1/vaults/${vaultId}`);
59
+ return response.data;
60
+ } catch (error) {
61
+ handleApiError(error);
62
+ }
63
+ }
64
+
65
+ // ============================================================
66
+ // ITEMS
67
+ // ============================================================
68
+
69
+ export async function listItems(vaultId, { filter } = {}) {
70
+ try {
71
+ const client = getClient();
72
+ const params = {};
73
+ if (filter) params.filter = filter;
74
+ const response = await client.get(`/v1/vaults/${vaultId}/items`, { params });
75
+ return response.data;
76
+ } catch (error) {
77
+ handleApiError(error);
78
+ }
79
+ }
80
+
81
+ export async function getItem(vaultId, itemId) {
82
+ try {
83
+ const client = getClient();
84
+ const response = await client.get(`/v1/vaults/${vaultId}/items/${itemId}`);
85
+ return response.data;
86
+ } catch (error) {
87
+ handleApiError(error);
88
+ }
89
+ }
90
+
91
+ export async function createItem(vaultId, itemData) {
92
+ try {
93
+ const client = getClient();
94
+ const response = await client.post(`/v1/vaults/${vaultId}/items`, itemData);
95
+ return response.data;
96
+ } catch (error) {
97
+ handleApiError(error);
98
+ }
99
+ }
100
+
101
+ export async function updateItem(vaultId, itemId, itemData) {
102
+ try {
103
+ const client = getClient();
104
+ const response = await client.put(`/v1/vaults/${vaultId}/items/${itemId}`, itemData);
105
+ return response.data;
106
+ } catch (error) {
107
+ handleApiError(error);
108
+ }
109
+ }
110
+
111
+ export async function deleteItem(vaultId, itemId) {
112
+ try {
113
+ const client = getClient();
114
+ await client.delete(`/v1/vaults/${vaultId}/items/${itemId}`);
115
+ } catch (error) {
116
+ handleApiError(error);
117
+ }
118
+ }
119
+
120
+ // ============================================================
121
+ // FILES
122
+ // ============================================================
123
+
124
+ export async function listFiles(vaultId, itemId) {
125
+ try {
126
+ const client = getClient();
127
+ const response = await client.get(`/v1/vaults/${vaultId}/items/${itemId}/files`);
128
+ return response.data;
129
+ } catch (error) {
130
+ handleApiError(error);
131
+ }
132
+ }
133
+
134
+ export async function getFile(vaultId, itemId, fileId) {
135
+ try {
136
+ const client = getClient();
137
+ const response = await client.get(`/v1/vaults/${vaultId}/items/${itemId}/files/${fileId}`);
138
+ return response.data;
139
+ } catch (error) {
140
+ handleApiError(error);
141
+ }
142
+ }
package/src/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/1password' });
4
+
5
+ export function getConfig(key) {
6
+ return config.get(key);
7
+ }
8
+
9
+ export function setConfig(key, value) {
10
+ config.set(key, value);
11
+ }
12
+
13
+ export function isConfigured() {
14
+ return !!config.get('token');
15
+ }
16
+
17
+ export function getAllConfig() {
18
+ return config.store;
19
+ }
package/src/index.js ADDED
@@ -0,0 +1,382 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, isConfigured, getAllConfig } from './config.js';
5
+ import {
6
+ listVaults, getVault,
7
+ listItems, getItem, createItem, updateItem, deleteItem,
8
+ listFiles, getFile
9
+ } from './api.js';
10
+
11
+ const program = new Command();
12
+
13
+ // ============================================================
14
+ // Helpers
15
+ // ============================================================
16
+
17
+ function printSuccess(message) {
18
+ console.log(chalk.green('✓') + ' ' + message);
19
+ }
20
+
21
+ function printError(message) {
22
+ console.error(chalk.red('✗') + ' ' + message);
23
+ }
24
+
25
+ function printTable(data, columns) {
26
+ if (!data || data.length === 0) {
27
+ console.log(chalk.yellow('No results found.'));
28
+ return;
29
+ }
30
+ const widths = {};
31
+ columns.forEach(col => {
32
+ widths[col.key] = col.label.length;
33
+ data.forEach(row => {
34
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
35
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
36
+ });
37
+ widths[col.key] = Math.min(widths[col.key], 40);
38
+ });
39
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
40
+ console.log(chalk.bold(chalk.cyan(header)));
41
+ console.log(chalk.dim('─'.repeat(header.length)));
42
+ data.forEach(row => {
43
+ const line = columns.map(col => {
44
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
45
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
46
+ }).join(' ');
47
+ console.log(line);
48
+ });
49
+ console.log(chalk.dim(`\n${data.length} result(s)`));
50
+ }
51
+
52
+ function printJson(data) {
53
+ console.log(JSON.stringify(data, null, 2));
54
+ }
55
+
56
+ async function withSpinner(message, fn) {
57
+ const spinner = ora(message).start();
58
+ try {
59
+ const result = await fn();
60
+ spinner.stop();
61
+ return result;
62
+ } catch (error) {
63
+ spinner.stop();
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ function requireAuth() {
69
+ if (!isConfigured()) {
70
+ printError('1Password Connect token not configured.');
71
+ console.log('\nRun the following to configure:');
72
+ console.log(chalk.cyan(' 1password config set --token YOUR_CONNECT_TOKEN'));
73
+ console.log(chalk.cyan(' 1password config set --base-url https://your-connect-server.example.com'));
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ // ============================================================
79
+ // Program metadata
80
+ // ============================================================
81
+
82
+ program
83
+ .name('1password')
84
+ .description(chalk.bold('1Password CLI') + ' - Secrets and vault management from your terminal')
85
+ .version('1.0.0');
86
+
87
+ // ============================================================
88
+ // CONFIG
89
+ // ============================================================
90
+
91
+ const configCmd = program.command('config').description('Manage CLI configuration');
92
+
93
+ configCmd
94
+ .command('set')
95
+ .description('Set configuration values')
96
+ .option('--token <token>', '1Password Connect API token')
97
+ .option('--base-url <url>', '1Password Connect server URL', 'https://connect.1password.com')
98
+ .action((options) => {
99
+ if (options.token) {
100
+ setConfig('token', options.token);
101
+ printSuccess('Connect token set');
102
+ }
103
+ if (options.baseUrl) {
104
+ setConfig('baseUrl', options.baseUrl);
105
+ printSuccess(`Base URL set to: ${options.baseUrl}`);
106
+ }
107
+ if (!options.token && !options.baseUrl) {
108
+ printError('No options provided. Use --token or --base-url');
109
+ }
110
+ });
111
+
112
+ configCmd
113
+ .command('get <key>')
114
+ .description('Get a configuration value')
115
+ .action((key) => {
116
+ const value = getConfig(key);
117
+ if (value === undefined) {
118
+ printError(`Key "${key}" not found`);
119
+ } else {
120
+ console.log(value);
121
+ }
122
+ });
123
+
124
+ configCmd
125
+ .command('list')
126
+ .description('List all configuration values')
127
+ .action(() => {
128
+ const all = getAllConfig();
129
+ console.log(chalk.bold('\n1Password CLI Configuration\n'));
130
+ const token = all.token;
131
+ const baseUrl = all.baseUrl;
132
+ console.log('Token: ', token ? chalk.green('*'.repeat(16) + token.slice(-4)) : chalk.red('not set'));
133
+ console.log('Base URL: ', baseUrl ? chalk.green(baseUrl) : chalk.yellow('using default: https://connect.1password.com'));
134
+ console.log('');
135
+ });
136
+
137
+ // ============================================================
138
+ // VAULTS
139
+ // ============================================================
140
+
141
+ const vaultsCmd = program.command('vaults').description('Manage vaults');
142
+
143
+ vaultsCmd
144
+ .command('list')
145
+ .description('List all vaults')
146
+ .option('--json', 'Output as JSON')
147
+ .action(async (options) => {
148
+ requireAuth();
149
+ try {
150
+ const vaults = await withSpinner('Fetching vaults...', () => listVaults());
151
+ if (options.json) { printJson(vaults); return; }
152
+ printTable(vaults, [
153
+ { key: 'id', label: 'ID', format: (v) => v?.substring(0, 16) + '...' },
154
+ { key: 'name', label: 'Name' },
155
+ { key: 'type', label: 'Type' },
156
+ { key: 'items', label: 'Items' },
157
+ { key: 'description', label: 'Description' }
158
+ ]);
159
+ } catch (error) {
160
+ printError(error.message);
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ vaultsCmd
166
+ .command('get <vault-id>')
167
+ .description('Get details of a specific vault')
168
+ .option('--json', 'Output as JSON')
169
+ .action(async (vaultId, options) => {
170
+ requireAuth();
171
+ try {
172
+ const vault = await withSpinner('Fetching vault...', () => getVault(vaultId));
173
+ if (options.json) { printJson(vault); return; }
174
+ console.log(chalk.bold('\nVault Details\n'));
175
+ console.log('ID: ', chalk.cyan(vault.id));
176
+ console.log('Name: ', chalk.bold(vault.name));
177
+ console.log('Type: ', vault.type || 'N/A');
178
+ console.log('Items: ', vault.items || 0);
179
+ console.log('Description: ', vault.description || 'N/A');
180
+ console.log('Created: ', vault.createdAt || 'N/A');
181
+ console.log('Updated: ', vault.updatedAt || 'N/A');
182
+ console.log('');
183
+ } catch (error) {
184
+ printError(error.message);
185
+ process.exit(1);
186
+ }
187
+ });
188
+
189
+ // ============================================================
190
+ // ITEMS
191
+ // ============================================================
192
+
193
+ const itemsCmd = program.command('items').description('Manage vault items');
194
+
195
+ itemsCmd
196
+ .command('list <vault-id>')
197
+ .description('List items in a vault')
198
+ .option('--filter <query>', 'Filter items by title')
199
+ .option('--json', 'Output as JSON')
200
+ .action(async (vaultId, options) => {
201
+ requireAuth();
202
+ try {
203
+ const items = await withSpinner('Fetching items...', () =>
204
+ listItems(vaultId, { filter: options.filter })
205
+ );
206
+ if (options.json) { printJson(items); return; }
207
+ printTable(items, [
208
+ { key: 'id', label: 'ID', format: (v) => v?.substring(0, 16) + '...' },
209
+ { key: 'title', label: 'Title' },
210
+ { key: 'category', label: 'Category' },
211
+ { key: 'updatedAt', label: 'Updated' },
212
+ { key: 'createdAt', label: 'Created' }
213
+ ]);
214
+ } catch (error) {
215
+ printError(error.message);
216
+ process.exit(1);
217
+ }
218
+ });
219
+
220
+ itemsCmd
221
+ .command('get <vault-id> <item-id>')
222
+ .description('Get a specific item from a vault')
223
+ .option('--json', 'Output as JSON')
224
+ .action(async (vaultId, itemId, options) => {
225
+ requireAuth();
226
+ try {
227
+ const item = await withSpinner('Fetching item...', () => getItem(vaultId, itemId));
228
+ if (options.json) { printJson(item); return; }
229
+ console.log(chalk.bold('\nItem Details\n'));
230
+ console.log('ID: ', chalk.cyan(item.id));
231
+ console.log('Title: ', chalk.bold(item.title));
232
+ console.log('Category: ', item.category);
233
+ console.log('Created: ', item.createdAt || 'N/A');
234
+ console.log('Updated: ', item.updatedAt || 'N/A');
235
+ if (item.fields && item.fields.length > 0) {
236
+ console.log(chalk.bold('\nFields:\n'));
237
+ printTable(item.fields.filter(f => f.value), [
238
+ { key: 'label', label: 'Label' },
239
+ { key: 'type', label: 'Type' },
240
+ { key: 'value', label: 'Value', format: (v, row) => row.type === 'CONCEALED' ? '****' : (v || 'N/A') }
241
+ ]);
242
+ }
243
+ } catch (error) {
244
+ printError(error.message);
245
+ process.exit(1);
246
+ }
247
+ });
248
+
249
+ itemsCmd
250
+ .command('create <vault-id>')
251
+ .description('Create a new item in a vault')
252
+ .requiredOption('--title <title>', 'Item title')
253
+ .requiredOption('--category <category>', 'Item category (LOGIN|PASSWORD|SERVER|DATABASE|CREDIT_CARD|MEMBERSHIP|PASSPORT|OUTDOOR_LICENSE|WIRELESS_ROUTER|BANK_ACCOUNT|DRIVER_LICENSE|IDENTITY|REWARD_PROGRAM|DOCUMENT|EMAIL_ACCOUNT|SOCIAL_SECURITY_NUMBER|MEDICAL_RECORD|SSH_KEY|API_CREDENTIAL)')
254
+ .option('--fields <json>', 'Item fields as JSON array, e.g. \'[{"label":"username","value":"myuser","type":"STRING"}]\'')
255
+ .option('--json', 'Output as JSON')
256
+ .action(async (vaultId, options) => {
257
+ requireAuth();
258
+ let fields = [];
259
+ if (options.fields) {
260
+ try { fields = JSON.parse(options.fields); } catch {
261
+ printError('Invalid JSON for --fields'); process.exit(1);
262
+ }
263
+ }
264
+ try {
265
+ const item = await withSpinner('Creating item...', () =>
266
+ createItem(vaultId, {
267
+ vault: { id: vaultId },
268
+ title: options.title,
269
+ category: options.category,
270
+ fields
271
+ })
272
+ );
273
+ if (options.json) { printJson(item); return; }
274
+ printSuccess(`Item created: ${chalk.bold(item.title)}`);
275
+ console.log('ID: ', item.id);
276
+ console.log('Category: ', item.category);
277
+ } catch (error) {
278
+ printError(error.message);
279
+ process.exit(1);
280
+ }
281
+ });
282
+
283
+ itemsCmd
284
+ .command('update <vault-id> <item-id>')
285
+ .description('Update an existing item')
286
+ .option('--title <title>', 'New item title')
287
+ .option('--fields <json>', 'Updated fields as JSON array')
288
+ .option('--json', 'Output as JSON')
289
+ .action(async (vaultId, itemId, options) => {
290
+ requireAuth();
291
+ let fields;
292
+ if (options.fields) {
293
+ try { fields = JSON.parse(options.fields); } catch {
294
+ printError('Invalid JSON for --fields'); process.exit(1);
295
+ }
296
+ }
297
+ try {
298
+ const existing = await withSpinner('Fetching current item...', () => getItem(vaultId, itemId));
299
+ const updateData = { ...existing };
300
+ if (options.title) updateData.title = options.title;
301
+ if (fields) updateData.fields = fields;
302
+
303
+ const item = await withSpinner('Updating item...', () => updateItem(vaultId, itemId, updateData));
304
+ if (options.json) { printJson(item); return; }
305
+ printSuccess(`Item updated: ${chalk.bold(item.title)}`);
306
+ } catch (error) {
307
+ printError(error.message);
308
+ process.exit(1);
309
+ }
310
+ });
311
+
312
+ itemsCmd
313
+ .command('delete <vault-id> <item-id>')
314
+ .description('Delete an item from a vault')
315
+ .action(async (vaultId, itemId) => {
316
+ requireAuth();
317
+ try {
318
+ await withSpinner('Deleting item...', () => deleteItem(vaultId, itemId));
319
+ printSuccess('Item deleted successfully');
320
+ } catch (error) {
321
+ printError(error.message);
322
+ process.exit(1);
323
+ }
324
+ });
325
+
326
+ // ============================================================
327
+ // FILES
328
+ // ============================================================
329
+
330
+ const filesCmd = program.command('files').description('Manage files attached to items');
331
+
332
+ filesCmd
333
+ .command('list <vault-id> <item-id>')
334
+ .description('List files attached to an item')
335
+ .option('--json', 'Output as JSON')
336
+ .action(async (vaultId, itemId, options) => {
337
+ requireAuth();
338
+ try {
339
+ const files = await withSpinner('Fetching files...', () => listFiles(vaultId, itemId));
340
+ if (options.json) { printJson(files); return; }
341
+ printTable(files, [
342
+ { key: 'id', label: 'ID', format: (v) => v?.substring(0, 16) + '...' },
343
+ { key: 'name', label: 'Name' },
344
+ { key: 'size', label: 'Size', format: (v) => v ? `${(v / 1024).toFixed(1)} KB` : 'N/A' },
345
+ { key: 'content_path', label: 'Content Path' }
346
+ ]);
347
+ } catch (error) {
348
+ printError(error.message);
349
+ process.exit(1);
350
+ }
351
+ });
352
+
353
+ filesCmd
354
+ .command('get <vault-id> <item-id> <file-id>')
355
+ .description('Get file metadata')
356
+ .option('--json', 'Output as JSON')
357
+ .action(async (vaultId, itemId, fileId, options) => {
358
+ requireAuth();
359
+ try {
360
+ const file = await withSpinner('Fetching file...', () => getFile(vaultId, itemId, fileId));
361
+ if (options.json) { printJson(file); return; }
362
+ console.log(chalk.bold('\nFile Details\n'));
363
+ console.log('ID: ', chalk.cyan(file.id));
364
+ console.log('Name: ', chalk.bold(file.name));
365
+ console.log('Size: ', file.size ? `${(file.size / 1024).toFixed(1)} KB` : 'N/A');
366
+ console.log('Content Path: ', file.content_path || 'N/A');
367
+ console.log('');
368
+ } catch (error) {
369
+ printError(error.message);
370
+ process.exit(1);
371
+ }
372
+ });
373
+
374
+ // ============================================================
375
+ // Parse
376
+ // ============================================================
377
+
378
+ program.parse(process.argv);
379
+
380
+ if (process.argv.length <= 2) {
381
+ program.help();
382
+ }