@ktmcp-cli/authentiq 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,66 @@
1
+ # AGENT.md — Authentiq CLI for AI Agents
2
+
3
+ This document explains how to use the Authentiq CLI as an AI agent.
4
+
5
+ ## Overview
6
+
7
+ The `authentiq` CLI provides access to the Authentiq Identity API. Requires a Bearer token.
8
+
9
+ ## Prerequisites
10
+
11
+ ```bash
12
+ authentiq config set token YOUR_BEARER_TOKEN
13
+ ```
14
+
15
+ ## All Commands
16
+
17
+ ### Config
18
+
19
+ ```bash
20
+ authentiq config get <key>
21
+ authentiq config set <key> <value>
22
+ authentiq config list
23
+ ```
24
+
25
+ ### Sessions
26
+
27
+ ```bash
28
+ authentiq sessions list
29
+ authentiq sessions create --scope "openid email"
30
+ authentiq sessions create --scope "openid profile" --callback https://example.com/callback
31
+ authentiq sessions get <session-id>
32
+ authentiq sessions delete <session-id>
33
+ ```
34
+
35
+ ### Keys
36
+
37
+ ```bash
38
+ authentiq keys register --key '{"kty":"EC","crv":"P-256","x":"...","y":"..."}'
39
+ authentiq keys get <pk>
40
+ authentiq keys update <pk> --data '{"status":"active"}'
41
+ authentiq keys revoke <pk>
42
+ ```
43
+
44
+ ### Scopes
45
+
46
+ ```bash
47
+ authentiq scopes list
48
+ authentiq scopes get <scope-id>
49
+ authentiq scopes request --data '{"scope":"email"}'
50
+ ```
51
+
52
+ ## JSON Output
53
+
54
+ All commands support `--json`:
55
+
56
+ ```bash
57
+ authentiq sessions list --json
58
+ authentiq keys get <pk> --json
59
+ authentiq scopes list --json
60
+ ```
61
+
62
+ ## Error Handling
63
+
64
+ The CLI exits with code 1 on error and prints to stderr.
65
+ - `Authentication failed` — Run `authentiq config set token YOUR_TOKEN`
66
+ - `Resource not found` — Check the ID is correct
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,82 @@
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
+ # Authentiq CLI
7
+
8
+ Production-ready CLI for the Authentiq Identity API. Manage sessions, keys, and scopes directly from your terminal.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install -g @ktmcp-cli/authentiq
14
+ ```
15
+
16
+ ## Configuration
17
+
18
+ ```bash
19
+ authentiq config set token YOUR_BEARER_TOKEN
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### Sessions
25
+
26
+ ```bash
27
+ # List all sessions
28
+ authentiq sessions list
29
+
30
+ # Create a new session
31
+ authentiq sessions create --scope "openid email"
32
+ authentiq sessions create --scope "openid profile" --callback https://example.com/callback
33
+
34
+ # Get session details
35
+ authentiq sessions get <session-id>
36
+
37
+ # Delete a session
38
+ authentiq sessions delete <session-id>
39
+ ```
40
+
41
+ ### Keys
42
+
43
+ ```bash
44
+ # Register a public key (JWK format)
45
+ authentiq keys register --key '{"kty":"EC","crv":"P-256","x":"...","y":"..."}'
46
+
47
+ # Get key details
48
+ authentiq keys get <pk>
49
+
50
+ # Update a key
51
+ authentiq keys update <pk> --data '{"status":"active"}'
52
+
53
+ # Revoke a key
54
+ authentiq keys revoke <pk>
55
+ ```
56
+
57
+ ### Scopes
58
+
59
+ ```bash
60
+ # List available scopes
61
+ authentiq scopes list
62
+
63
+ # Get scope details
64
+ authentiq scopes get openid
65
+
66
+ # Request additional scopes
67
+ authentiq scopes request --data '{"scope":"email"}'
68
+ ```
69
+
70
+ ### JSON Output
71
+
72
+ All commands support `--json` for machine-readable output:
73
+
74
+ ```bash
75
+ authentiq sessions list --json
76
+ authentiq sessions get <id> --json
77
+ authentiq scopes list --json
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname, join } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+
9
+ import(join(__dirname, '..', 'src', 'index.js'));
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/authentiq",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Authentiq Identity API - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "authentiq": "bin/authentiq.js"
9
+ },
10
+ "keywords": ["authentiq", "identity", "sessions", "keys", "scopes", "cli", "api", "ktmcp"],
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/authentiq.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/authentiq-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/authentiq/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,151 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const BASE_URL = 'https://6-dot-authentiqio.appspot.com';
5
+
6
+ function getClient() {
7
+ const token = getConfig('token');
8
+ return axios.create({
9
+ baseURL: BASE_URL,
10
+ headers: {
11
+ 'Accept': 'application/json',
12
+ 'Content-Type': 'application/json',
13
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {})
14
+ },
15
+ timeout: 15000
16
+ });
17
+ }
18
+
19
+ function handleApiError(error) {
20
+ if (error.response) {
21
+ const status = error.response.status;
22
+ const data = error.response.data;
23
+ if (status === 401) {
24
+ throw new Error('Authentication failed. Run: authentiq config set token YOUR_TOKEN');
25
+ } else if (status === 403) {
26
+ throw new Error('Access forbidden. Check your API permissions.');
27
+ } else if (status === 404) {
28
+ throw new Error('Resource not found.');
29
+ } else if (status === 429) {
30
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
31
+ } else {
32
+ const message = data?.message || data?.error || JSON.stringify(data);
33
+ throw new Error(`API Error (${status}): ${message}`);
34
+ }
35
+ } else if (error.request) {
36
+ throw new Error('No response from Authentiq API. Check your internet connection.');
37
+ } else {
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ // ============================================================
43
+ // SESSIONS
44
+ // ============================================================
45
+
46
+ export async function createSession(data) {
47
+ try {
48
+ const response = await getClient().post('/session', data);
49
+ return response.data;
50
+ } catch (error) {
51
+ handleApiError(error);
52
+ }
53
+ }
54
+
55
+ export async function getSession(sessionId) {
56
+ try {
57
+ const response = await getClient().get(`/session/${sessionId}`);
58
+ return response.data;
59
+ } catch (error) {
60
+ handleApiError(error);
61
+ }
62
+ }
63
+
64
+ export async function deleteSession(sessionId) {
65
+ try {
66
+ const response = await getClient().delete(`/session/${sessionId}`);
67
+ return response.data;
68
+ } catch (error) {
69
+ handleApiError(error);
70
+ }
71
+ }
72
+
73
+ export async function listSessions() {
74
+ try {
75
+ const response = await getClient().get('/session');
76
+ return response.data || [];
77
+ } catch (error) {
78
+ handleApiError(error);
79
+ }
80
+ }
81
+
82
+ // ============================================================
83
+ // KEYS
84
+ // ============================================================
85
+
86
+ export async function registerKey(keyData) {
87
+ try {
88
+ const response = await getClient().post('/key', keyData);
89
+ return response.data;
90
+ } catch (error) {
91
+ handleApiError(error);
92
+ }
93
+ }
94
+
95
+ export async function getKey(pk) {
96
+ try {
97
+ const response = await getClient().get(`/key/${pk}`);
98
+ return response.data;
99
+ } catch (error) {
100
+ handleApiError(error);
101
+ }
102
+ }
103
+
104
+ export async function revokeKey(pk) {
105
+ try {
106
+ const response = await getClient().delete(`/key/${pk}`);
107
+ return response.data;
108
+ } catch (error) {
109
+ handleApiError(error);
110
+ }
111
+ }
112
+
113
+ export async function updateKey(pk, keyData) {
114
+ try {
115
+ const response = await getClient().put(`/key/${pk}`, keyData);
116
+ return response.data;
117
+ } catch (error) {
118
+ handleApiError(error);
119
+ }
120
+ }
121
+
122
+ // ============================================================
123
+ // SCOPES
124
+ // ============================================================
125
+
126
+ export async function listScopes() {
127
+ try {
128
+ const response = await getClient().get('/scope');
129
+ return response.data || [];
130
+ } catch (error) {
131
+ handleApiError(error);
132
+ }
133
+ }
134
+
135
+ export async function getScope(scopeId) {
136
+ try {
137
+ const response = await getClient().get(`/scope/${scopeId}`);
138
+ return response.data;
139
+ } catch (error) {
140
+ handleApiError(error);
141
+ }
142
+ }
143
+
144
+ export async function requestScope(scopeData) {
145
+ try {
146
+ const response = await getClient().post('/scope', scopeData);
147
+ return response.data;
148
+ } catch (error) {
149
+ handleApiError(error);
150
+ }
151
+ }
package/src/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/authentiq' });
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
+ }
20
+
21
+ export default config;
package/src/index.js ADDED
@@ -0,0 +1,469 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, getAllConfig, isConfigured } from './config.js';
5
+ import {
6
+ createSession,
7
+ getSession,
8
+ deleteSession,
9
+ listSessions,
10
+ registerKey,
11
+ getKey,
12
+ revokeKey,
13
+ updateKey,
14
+ listScopes,
15
+ getScope,
16
+ requestScope
17
+ } from './api.js';
18
+
19
+ const program = new Command();
20
+
21
+ // ============================================================
22
+ // Helpers
23
+ // ============================================================
24
+
25
+ function printSuccess(message) {
26
+ console.log(chalk.green('✓') + ' ' + message);
27
+ }
28
+
29
+ function printError(message) {
30
+ console.error(chalk.red('✗') + ' ' + message);
31
+ }
32
+
33
+ function printTable(data, columns) {
34
+ if (!data || data.length === 0) {
35
+ console.log(chalk.yellow('No results found.'));
36
+ return;
37
+ }
38
+
39
+ const widths = {};
40
+ columns.forEach(col => {
41
+ widths[col.key] = col.label.length;
42
+ data.forEach(row => {
43
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
44
+ if (val.length > widths[col.key]) widths[col.key] = val.length;
45
+ });
46
+ widths[col.key] = Math.min(widths[col.key], 40);
47
+ });
48
+
49
+ const header = columns.map(col => col.label.padEnd(widths[col.key])).join(' ');
50
+ console.log(chalk.bold(chalk.cyan(header)));
51
+ console.log(chalk.dim('─'.repeat(header.length)));
52
+
53
+ data.forEach(row => {
54
+ const line = columns.map(col => {
55
+ const val = String(col.format ? col.format(row[col.key], row) : (row[col.key] ?? ''));
56
+ return val.substring(0, widths[col.key]).padEnd(widths[col.key]);
57
+ }).join(' ');
58
+ console.log(line);
59
+ });
60
+
61
+ console.log(chalk.dim(`\n${data.length} result(s)`));
62
+ }
63
+
64
+ function printJson(data) {
65
+ console.log(JSON.stringify(data, null, 2));
66
+ }
67
+
68
+ async function withSpinner(message, fn) {
69
+ const spinner = ora(message).start();
70
+ try {
71
+ const result = await fn();
72
+ spinner.stop();
73
+ return result;
74
+ } catch (error) {
75
+ spinner.stop();
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ function requireAuth() {
81
+ if (!isConfigured()) {
82
+ printError('Authentiq token not configured.');
83
+ console.log('\nRun the following to configure:');
84
+ console.log(chalk.cyan(' authentiq config set token YOUR_BEARER_TOKEN'));
85
+ process.exit(1);
86
+ }
87
+ }
88
+
89
+ // ============================================================
90
+ // Program metadata
91
+ // ============================================================
92
+
93
+ program
94
+ .name('authentiq')
95
+ .description(chalk.bold('Authentiq CLI') + ' - Identity and session management from your terminal')
96
+ .version('1.0.0');
97
+
98
+ // ============================================================
99
+ // CONFIG
100
+ // ============================================================
101
+
102
+ const configCmd = program.command('config').description('Manage CLI configuration');
103
+
104
+ configCmd
105
+ .command('get <key>')
106
+ .description('Get a configuration value')
107
+ .action((key) => {
108
+ const value = getConfig(key);
109
+ if (value === undefined) {
110
+ printError(`Key '${key}' not found`);
111
+ } else {
112
+ console.log(value);
113
+ }
114
+ });
115
+
116
+ configCmd
117
+ .command('set <key> <value>')
118
+ .description('Set a configuration value')
119
+ .action((key, value) => {
120
+ setConfig(key, value);
121
+ printSuccess(`Config '${key}' set`);
122
+ });
123
+
124
+ configCmd
125
+ .command('list')
126
+ .description('List all configuration values')
127
+ .action(() => {
128
+ const all = getAllConfig();
129
+ console.log(chalk.bold('\nAuthentiq CLI Configuration\n'));
130
+ if (Object.keys(all).length === 0) {
131
+ console.log(chalk.yellow('No configuration set.'));
132
+ console.log('\nRun: ' + chalk.cyan('authentiq config set token YOUR_TOKEN'));
133
+ } else {
134
+ Object.entries(all).forEach(([k, v]) => {
135
+ const displayVal = k === 'token' ? chalk.green('*'.repeat(Math.min(String(v).length, 8))) : chalk.cyan(String(v));
136
+ console.log(`${k}: ${displayVal}`);
137
+ });
138
+ }
139
+ });
140
+
141
+ // ============================================================
142
+ // SESSIONS
143
+ // ============================================================
144
+
145
+ const sessionsCmd = program.command('sessions').description('Manage identity sessions');
146
+
147
+ sessionsCmd
148
+ .command('list')
149
+ .description('List active sessions')
150
+ .option('--json', 'Output as JSON')
151
+ .action(async (options) => {
152
+ requireAuth();
153
+ try {
154
+ const sessions = await withSpinner('Fetching sessions...', () => listSessions());
155
+
156
+ if (options.json) {
157
+ printJson(sessions);
158
+ return;
159
+ }
160
+
161
+ const data = Array.isArray(sessions) ? sessions : (sessions ? [sessions] : []);
162
+ printTable(data, [
163
+ { key: 'session', label: 'Session ID', format: (v) => v ? String(v).substring(0, 16) + '...' : 'N/A' },
164
+ { key: 'status', label: 'Status' },
165
+ { key: 'created', label: 'Created' },
166
+ { key: 'scope', label: 'Scope', format: (v) => Array.isArray(v) ? v.join(' ') : (v || '') }
167
+ ]);
168
+ } catch (error) {
169
+ printError(error.message);
170
+ process.exit(1);
171
+ }
172
+ });
173
+
174
+ sessionsCmd
175
+ .command('create')
176
+ .description('Create a new session')
177
+ .option('--scope <scope>', 'Requested scope (space-separated)', 'openid')
178
+ .option('--callback <url>', 'Callback URL')
179
+ .option('--json', 'Output as JSON')
180
+ .action(async (options) => {
181
+ requireAuth();
182
+ try {
183
+ const sessionData = {
184
+ scope: options.scope.split(' '),
185
+ ...(options.callback && { callback: options.callback })
186
+ };
187
+
188
+ const session = await withSpinner('Creating session...', () => createSession(sessionData));
189
+
190
+ if (options.json) {
191
+ printJson(session);
192
+ return;
193
+ }
194
+
195
+ printSuccess('Session created');
196
+ if (session) {
197
+ console.log('Session: ', chalk.cyan(session.session || JSON.stringify(session)));
198
+ if (session.status) console.log('Status: ', session.status);
199
+ if (session.url) console.log('Auth URL: ', chalk.blue(session.url));
200
+ }
201
+ } catch (error) {
202
+ printError(error.message);
203
+ process.exit(1);
204
+ }
205
+ });
206
+
207
+ sessionsCmd
208
+ .command('get <session-id>')
209
+ .description('Get session details')
210
+ .option('--json', 'Output as JSON')
211
+ .action(async (sessionId, options) => {
212
+ requireAuth();
213
+ try {
214
+ const session = await withSpinner(`Fetching session ${sessionId}...`, () =>
215
+ getSession(sessionId)
216
+ );
217
+
218
+ if (!session) {
219
+ printError('Session not found');
220
+ process.exit(1);
221
+ }
222
+
223
+ if (options.json) {
224
+ printJson(session);
225
+ return;
226
+ }
227
+
228
+ console.log(chalk.bold('\nSession Details\n'));
229
+ console.log('Session ID: ', chalk.cyan(session.session || sessionId));
230
+ console.log('Status: ', session.status || 'N/A');
231
+ console.log('Created: ', session.created || 'N/A');
232
+ if (session.scope) console.log('Scope: ', Array.isArray(session.scope) ? session.scope.join(' ') : session.scope);
233
+ console.log('');
234
+ } catch (error) {
235
+ printError(error.message);
236
+ process.exit(1);
237
+ }
238
+ });
239
+
240
+ sessionsCmd
241
+ .command('delete <session-id>')
242
+ .description('Delete (revoke) a session')
243
+ .action(async (sessionId) => {
244
+ requireAuth();
245
+ try {
246
+ await withSpinner(`Deleting session ${sessionId}...`, () => deleteSession(sessionId));
247
+ printSuccess(`Session ${sessionId} deleted`);
248
+ } catch (error) {
249
+ printError(error.message);
250
+ process.exit(1);
251
+ }
252
+ });
253
+
254
+ // ============================================================
255
+ // KEYS
256
+ // ============================================================
257
+
258
+ const keysCmd = program.command('keys').description('Manage identity keys');
259
+
260
+ keysCmd
261
+ .command('register')
262
+ .description('Register a new public key')
263
+ .requiredOption('--key <json>', 'Public key data as JSON (JWK format)')
264
+ .option('--json', 'Output as JSON')
265
+ .action(async (options) => {
266
+ requireAuth();
267
+ let keyData;
268
+ try {
269
+ keyData = JSON.parse(options.key);
270
+ } catch {
271
+ printError('Invalid JSON for --key');
272
+ process.exit(1);
273
+ }
274
+
275
+ try {
276
+ const result = await withSpinner('Registering key...', () => registerKey(keyData));
277
+
278
+ if (options.json) {
279
+ printJson(result);
280
+ return;
281
+ }
282
+
283
+ printSuccess('Key registered');
284
+ if (result) {
285
+ console.log('Key ID: ', chalk.cyan(result.pk || result.kid || JSON.stringify(result)));
286
+ }
287
+ } catch (error) {
288
+ printError(error.message);
289
+ process.exit(1);
290
+ }
291
+ });
292
+
293
+ keysCmd
294
+ .command('get <pk>')
295
+ .description('Get key details by public key ID')
296
+ .option('--json', 'Output as JSON')
297
+ .action(async (pk, options) => {
298
+ requireAuth();
299
+ try {
300
+ const key = await withSpinner(`Fetching key ${pk}...`, () => getKey(pk));
301
+
302
+ if (!key) {
303
+ printError('Key not found');
304
+ process.exit(1);
305
+ }
306
+
307
+ if (options.json) {
308
+ printJson(key);
309
+ return;
310
+ }
311
+
312
+ console.log(chalk.bold('\nKey Details\n'));
313
+ console.log('Key ID: ', chalk.cyan(key.pk || key.kid || pk));
314
+ console.log('Type: ', key.kty || 'N/A');
315
+ console.log('Algorithm: ', key.alg || 'N/A');
316
+ console.log('Status: ', key.status || 'N/A');
317
+ console.log('');
318
+ } catch (error) {
319
+ printError(error.message);
320
+ process.exit(1);
321
+ }
322
+ });
323
+
324
+ keysCmd
325
+ .command('revoke <pk>')
326
+ .description('Revoke a public key')
327
+ .action(async (pk) => {
328
+ requireAuth();
329
+ try {
330
+ await withSpinner(`Revoking key ${pk}...`, () => revokeKey(pk));
331
+ printSuccess(`Key ${pk} revoked`);
332
+ } catch (error) {
333
+ printError(error.message);
334
+ process.exit(1);
335
+ }
336
+ });
337
+
338
+ keysCmd
339
+ .command('update <pk>')
340
+ .description('Update a public key')
341
+ .requiredOption('--data <json>', 'Updated key data as JSON')
342
+ .option('--json', 'Output as JSON')
343
+ .action(async (pk, options) => {
344
+ requireAuth();
345
+ let updateData;
346
+ try {
347
+ updateData = JSON.parse(options.data);
348
+ } catch {
349
+ printError('Invalid JSON for --data');
350
+ process.exit(1);
351
+ }
352
+
353
+ try {
354
+ const result = await withSpinner(`Updating key ${pk}...`, () => updateKey(pk, updateData));
355
+
356
+ if (options.json) {
357
+ printJson(result);
358
+ return;
359
+ }
360
+
361
+ printSuccess(`Key ${pk} updated`);
362
+ } catch (error) {
363
+ printError(error.message);
364
+ process.exit(1);
365
+ }
366
+ });
367
+
368
+ // ============================================================
369
+ // SCOPES
370
+ // ============================================================
371
+
372
+ const scopesCmd = program.command('scopes').description('Manage identity scopes');
373
+
374
+ scopesCmd
375
+ .command('list')
376
+ .description('List available scopes')
377
+ .option('--json', 'Output as JSON')
378
+ .action(async (options) => {
379
+ requireAuth();
380
+ try {
381
+ const scopes = await withSpinner('Fetching scopes...', () => listScopes());
382
+
383
+ if (options.json) {
384
+ printJson(scopes);
385
+ return;
386
+ }
387
+
388
+ const data = Array.isArray(scopes) ? scopes : (scopes ? [scopes] : []);
389
+ printTable(data, [
390
+ { key: 'name', label: 'Name' },
391
+ { key: 'essential', label: 'Essential', format: (v) => v ? 'Yes' : 'No' },
392
+ { key: 'description', label: 'Description' }
393
+ ]);
394
+ } catch (error) {
395
+ printError(error.message);
396
+ process.exit(1);
397
+ }
398
+ });
399
+
400
+ scopesCmd
401
+ .command('get <scope-id>')
402
+ .description('Get scope details')
403
+ .option('--json', 'Output as JSON')
404
+ .action(async (scopeId, options) => {
405
+ requireAuth();
406
+ try {
407
+ const scope = await withSpinner(`Fetching scope ${scopeId}...`, () => getScope(scopeId));
408
+
409
+ if (!scope) {
410
+ printError('Scope not found');
411
+ process.exit(1);
412
+ }
413
+
414
+ if (options.json) {
415
+ printJson(scope);
416
+ return;
417
+ }
418
+
419
+ console.log(chalk.bold('\nScope Details\n'));
420
+ console.log('Name: ', chalk.cyan(scope.name || scopeId));
421
+ console.log('Essential: ', scope.essential ? chalk.green('Yes') : 'No');
422
+ console.log('Description: ', scope.description || 'N/A');
423
+ console.log('');
424
+ } catch (error) {
425
+ printError(error.message);
426
+ process.exit(1);
427
+ }
428
+ });
429
+
430
+ scopesCmd
431
+ .command('request')
432
+ .description('Request additional scopes')
433
+ .requiredOption('--data <json>', 'Scope request data as JSON')
434
+ .option('--json', 'Output as JSON')
435
+ .action(async (options) => {
436
+ requireAuth();
437
+ let scopeData;
438
+ try {
439
+ scopeData = JSON.parse(options.data);
440
+ } catch {
441
+ printError('Invalid JSON for --data');
442
+ process.exit(1);
443
+ }
444
+
445
+ try {
446
+ const result = await withSpinner('Requesting scope...', () => requestScope(scopeData));
447
+
448
+ if (options.json) {
449
+ printJson(result);
450
+ return;
451
+ }
452
+
453
+ printSuccess('Scope requested');
454
+ if (result) console.log(JSON.stringify(result, null, 2));
455
+ } catch (error) {
456
+ printError(error.message);
457
+ process.exit(1);
458
+ }
459
+ });
460
+
461
+ // ============================================================
462
+ // Parse
463
+ // ============================================================
464
+
465
+ program.parse(process.argv);
466
+
467
+ if (process.argv.length <= 2) {
468
+ program.help();
469
+ }