unbound-cli 0.1.5 → 0.1.7
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/package.json +1 -1
- package/src/api.js +7 -1
- package/src/auth.js +12 -2
- package/src/commands/login.js +14 -6
- package/src/commands/logout.js +9 -5
- package/src/commands/status.js +12 -5
- package/src/commands/whoami.js +11 -8
- package/src/config.js +1 -1
- package/src/index.js +68 -20
- package/src/output.js +31 -1
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -3,7 +3,8 @@ const http = require('http');
|
|
|
3
3
|
const { URL } = require('url');
|
|
4
4
|
const config = require('./config');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const { version } = require('../package.json');
|
|
7
|
+
const USER_AGENT = `UnboundCLI/${version}`;
|
|
7
8
|
|
|
8
9
|
const STATUS_MESSAGES = {
|
|
9
10
|
400: 'Bad request. Check your input and try again.',
|
|
@@ -79,6 +80,11 @@ function request(method, path, { body, query, apiKey } = {}) {
|
|
|
79
80
|
});
|
|
80
81
|
});
|
|
81
82
|
|
|
83
|
+
req.setTimeout(30000, () => {
|
|
84
|
+
req.destroy();
|
|
85
|
+
reject(new Error(`Request to ${url.host} timed out after 30s. Please try again.`));
|
|
86
|
+
});
|
|
87
|
+
|
|
82
88
|
req.on('error', (err) => {
|
|
83
89
|
if (err.code === 'ECONNREFUSED') {
|
|
84
90
|
reject(new Error(`Could not connect to ${url.host}. Check that the server is running.`));
|
package/src/auth.js
CHANGED
|
@@ -66,7 +66,7 @@ async function loginWithBrowser(frontendUrl) {
|
|
|
66
66
|
|
|
67
67
|
const port = server.address().port;
|
|
68
68
|
const callbackUrl = `http://localhost:${port}/callback`;
|
|
69
|
-
const authUrl = `${frontendUrl}/
|
|
69
|
+
const authUrl = `${frontendUrl}/automations/api-key-callback?callback_url=${encodeURIComponent(callbackUrl)}&app_type=cli`;
|
|
70
70
|
|
|
71
71
|
const open = (await import('open')).default;
|
|
72
72
|
|
|
@@ -74,19 +74,29 @@ async function loginWithBrowser(frontendUrl) {
|
|
|
74
74
|
output.info(`If the browser does not open, visit:\n${authUrl}`);
|
|
75
75
|
output.info('Waiting for authentication...');
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
try {
|
|
78
|
+
await open(authUrl);
|
|
79
|
+
} catch {
|
|
80
|
+
// Browser failed to open — URL is already printed above
|
|
81
|
+
}
|
|
78
82
|
|
|
79
83
|
const timeout = setTimeout(() => {
|
|
80
84
|
callbackReject(new Error('Authentication timed out after 120 seconds'));
|
|
81
85
|
server.close();
|
|
82
86
|
}, 120_000);
|
|
83
87
|
|
|
88
|
+
const reminder = setInterval(() => {
|
|
89
|
+
output.info('Still waiting for browser authentication...');
|
|
90
|
+
}, 30_000);
|
|
91
|
+
|
|
84
92
|
try {
|
|
85
93
|
const result = await callbackPromise;
|
|
86
94
|
clearTimeout(timeout);
|
|
95
|
+
clearInterval(reminder);
|
|
87
96
|
return result;
|
|
88
97
|
} catch (err) {
|
|
89
98
|
clearTimeout(timeout);
|
|
99
|
+
clearInterval(reminder);
|
|
90
100
|
throw err;
|
|
91
101
|
}
|
|
92
102
|
}
|
package/src/commands/login.js
CHANGED
|
@@ -41,23 +41,31 @@ Examples:
|
|
|
41
41
|
|
|
42
42
|
if (opts.apiKey) {
|
|
43
43
|
apiKey = opts.apiKey;
|
|
44
|
+
// Validate the key before storing
|
|
45
|
+
const spin = output.spinner('Validating API key...');
|
|
46
|
+
try {
|
|
47
|
+
await api.get('/api/v1/users/privileges/', { apiKey });
|
|
48
|
+
spin.stop();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
spin.fail('Invalid API key. Check your key and try again.');
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
config.setApiKey(apiKey);
|
|
44
55
|
} else {
|
|
45
|
-
//
|
|
56
|
+
// Browser flow — key is already saved by auth.loginWithBrowser()
|
|
46
57
|
let frontendUrl;
|
|
47
58
|
if (opts.domain) {
|
|
48
59
|
frontendUrl = opts.domain.startsWith('http') ? opts.domain : `https://${opts.domain}`;
|
|
49
60
|
} else {
|
|
50
61
|
frontendUrl = config.getFrontendUrl();
|
|
51
62
|
}
|
|
52
|
-
|
|
53
63
|
await loginWithBrowser(frontendUrl);
|
|
54
64
|
apiKey = config.getApiKey();
|
|
65
|
+
// Validate the stored key
|
|
66
|
+
await api.get('/api/v1/users/privileges/');
|
|
55
67
|
}
|
|
56
68
|
|
|
57
|
-
// Store the API key and validate through the backend
|
|
58
|
-
config.setApiKey(apiKey);
|
|
59
|
-
await api.get('/api/v1/users/privileges/');
|
|
60
|
-
|
|
61
69
|
const cfg = config.readConfig();
|
|
62
70
|
const parts = [];
|
|
63
71
|
if (cfg.email) parts.push(`as ${cfg.email}`);
|
package/src/commands/logout.js
CHANGED
|
@@ -4,20 +4,24 @@ const output = require('../output');
|
|
|
4
4
|
function register(program) {
|
|
5
5
|
program
|
|
6
6
|
.command('logout')
|
|
7
|
-
.description('Log out of Unbound. Removes stored credentials
|
|
7
|
+
.description('Log out of Unbound. Removes stored credentials from ~/.unbound/config.json while preserving custom URL settings.')
|
|
8
8
|
.addHelpText('after', `
|
|
9
9
|
What this does:
|
|
10
|
-
Clears
|
|
10
|
+
Clears stored credentials (API key, email, organization) from
|
|
11
11
|
~/.unbound/config.json. Custom URL settings (base_url, frontend_url)
|
|
12
|
-
are
|
|
12
|
+
are preserved so you don't need to reconfigure them.
|
|
13
13
|
|
|
14
14
|
Examples:
|
|
15
15
|
$ unbound logout
|
|
16
16
|
`)
|
|
17
17
|
.action(() => {
|
|
18
18
|
try {
|
|
19
|
-
config.
|
|
20
|
-
|
|
19
|
+
const cfg = config.readConfig();
|
|
20
|
+
delete cfg.api_key;
|
|
21
|
+
delete cfg.email;
|
|
22
|
+
delete cfg.org_name;
|
|
23
|
+
config.writeConfig(cfg);
|
|
24
|
+
output.success('Logged out successfully. Custom URL settings preserved.');
|
|
21
25
|
} catch (err) {
|
|
22
26
|
output.error(err.message);
|
|
23
27
|
process.exitCode = 1;
|
package/src/commands/status.js
CHANGED
|
@@ -41,21 +41,28 @@ Examples:
|
|
|
41
41
|
// Check API connectivity
|
|
42
42
|
let connectivity = 'Not checked (not logged in)';
|
|
43
43
|
if (loggedIn) {
|
|
44
|
+
const spin = output.spinner('Checking API connectivity...');
|
|
44
45
|
try {
|
|
45
46
|
await api.get('/api/v1/users/privileges/');
|
|
47
|
+
spin.stop();
|
|
46
48
|
connectivity = 'Connected';
|
|
47
49
|
} catch (err) {
|
|
50
|
+
spin.stop();
|
|
48
51
|
connectivity = `Error: ${err.message}`;
|
|
49
52
|
}
|
|
50
53
|
}
|
|
51
54
|
pairs.push(['API status', connectivity]);
|
|
52
55
|
|
|
53
56
|
if (opts.json) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
output.json({
|
|
58
|
+
config_file: config.CONFIG_FILE,
|
|
59
|
+
logged_in: loggedIn,
|
|
60
|
+
email: loggedIn ? (cfg.email || null) : null,
|
|
61
|
+
organization: loggedIn ? (cfg.org_name || null) : null,
|
|
62
|
+
api_base_url: loggedIn ? config.getBaseUrl() : null,
|
|
63
|
+
frontend_url: loggedIn ? config.getFrontendUrl() : null,
|
|
64
|
+
api_status: connectivity,
|
|
65
|
+
});
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
61
68
|
|
package/src/commands/whoami.js
CHANGED
|
@@ -25,15 +25,18 @@ Examples:
|
|
|
25
25
|
`)
|
|
26
26
|
.option('--json', 'Output raw JSON')
|
|
27
27
|
.action(async (opts) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
28
|
+
if (!config.isLoggedIn()) {
|
|
29
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
const cfg = config.readConfig();
|
|
35
|
+
const spin = output.spinner('Fetching user info...');
|
|
36
|
+
try {
|
|
36
37
|
const privileges = await api.get('/api/v1/users/privileges/');
|
|
38
|
+
spin.stop();
|
|
39
|
+
|
|
37
40
|
const role = roleFromPrivileges(privileges);
|
|
38
41
|
|
|
39
42
|
if (opts.json) {
|
|
@@ -47,7 +50,7 @@ Examples:
|
|
|
47
50
|
['Role', role],
|
|
48
51
|
]);
|
|
49
52
|
} catch (err) {
|
|
50
|
-
|
|
53
|
+
spin.fail(err.message);
|
|
51
54
|
process.exitCode = 1;
|
|
52
55
|
}
|
|
53
56
|
});
|
package/src/config.js
CHANGED
|
@@ -19,7 +19,7 @@ function readConfig() {
|
|
|
19
19
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
20
20
|
}
|
|
21
21
|
} catch {
|
|
22
|
-
|
|
22
|
+
console.error(`\x1b[33mWarning: Config file at ${CONFIG_FILE} is corrupted. Using defaults.\x1b[0m`);
|
|
23
23
|
}
|
|
24
24
|
return {};
|
|
25
25
|
}
|
package/src/index.js
CHANGED
|
@@ -9,31 +9,79 @@ const program = new Command();
|
|
|
9
9
|
|
|
10
10
|
program
|
|
11
11
|
.name('unbound')
|
|
12
|
-
.description('Unbound CLI - Manage your AI Gateway from the command line.\n\nUnbound is an AI gateway that provides centralized policy management,\ncost controls, and security guardrails for AI coding tools like\nClaude Code, Cursor, Gemini CLI, and more.')
|
|
12
|
+
.description('Unbound CLI - Manage your AI Gateway from the command line.\n\nUnbound is an AI gateway that provides centralized policy management,\ncost controls, and security guardrails for AI coding tools like\nClaude Code, Cursor, Gemini CLI, Codex, and more.')
|
|
13
13
|
.version(version)
|
|
14
14
|
.addHelpText('after', `
|
|
15
|
-
|
|
16
|
-
$ unbound login
|
|
17
|
-
$ unbound login --api-key <key>
|
|
18
|
-
$ unbound
|
|
19
|
-
$ unbound
|
|
20
|
-
$ unbound
|
|
21
|
-
$ unbound
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
$ unbound setup
|
|
26
|
-
$ unbound
|
|
27
|
-
$ unbound
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
AUTHENTICATION
|
|
16
|
+
$ unbound login Sign in via browser
|
|
17
|
+
$ unbound login --api-key <key> Sign in with an API key (for CI/CD)
|
|
18
|
+
$ unbound login --domain custom.co Sign in via custom domain
|
|
19
|
+
$ unbound logout Remove stored credentials
|
|
20
|
+
$ unbound whoami Show current user and organization
|
|
21
|
+
$ unbound status Show CLI status and API connectivity
|
|
22
|
+
|
|
23
|
+
TOOL SETUP
|
|
24
|
+
Automated setup (installs hooks, sets env vars, configures tool):
|
|
25
|
+
$ unbound setup cursor Set up Cursor
|
|
26
|
+
$ unbound setup claude-code Set up Claude Code (interactive mode selection)
|
|
27
|
+
$ unbound setup claude-code --gateway Use Unbound as AI provider
|
|
28
|
+
$ unbound setup claude-code --subscription Hooks only (keep your subscription)
|
|
29
|
+
$ unbound setup gemini-cli Set up Gemini CLI
|
|
30
|
+
$ unbound setup codex Set up Codex
|
|
31
|
+
|
|
32
|
+
Instruction-only (shows config values to set manually):
|
|
33
|
+
$ unbound setup roo-code Show Roo Code config values
|
|
34
|
+
$ unbound setup cline Show Cline config values
|
|
35
|
+
$ unbound setup kilo-code Show Kilo Code config values
|
|
36
|
+
$ unbound setup custom-access Show API key and base URL
|
|
37
|
+
|
|
38
|
+
Remove configuration:
|
|
39
|
+
$ unbound setup cursor --clear Remove Unbound config for Cursor
|
|
40
|
+
$ unbound setup claude-code --clear Remove Unbound config for Claude Code
|
|
41
|
+
$ unbound setup gemini-cli --clear Remove Unbound config for Gemini CLI
|
|
42
|
+
$ unbound setup codex --clear Remove Unbound config for Codex
|
|
43
|
+
|
|
44
|
+
POLICY MANAGEMENT
|
|
45
|
+
$ unbound policy list List all policies
|
|
46
|
+
$ unbound policy list --type SECURITY Filter by type (SECURITY, MODEL, COST)
|
|
47
|
+
$ unbound policy get <id> View policy details
|
|
48
|
+
$ unbound policy create --name <n> --type <t> Create a new policy
|
|
49
|
+
$ unbound policy update <id> --name <n> Update a policy
|
|
50
|
+
$ unbound policy delete <id> Delete a policy
|
|
51
|
+
$ unbound policy effective <id> View effective policies for a user/group
|
|
52
|
+
$ unbound policy form-data Get reference data for policy creation
|
|
53
|
+
|
|
54
|
+
USER & GROUP MANAGEMENT
|
|
55
|
+
$ unbound users list List organization members
|
|
56
|
+
$ unbound users effective-policies <id> View effective policies for a user
|
|
57
|
+
$ unbound user-groups list List user groups
|
|
58
|
+
$ unbound user-groups get <id> View group details and members
|
|
59
|
+
$ unbound user-groups create --name <n> Create a user group
|
|
60
|
+
$ unbound user-groups update <id> Update a user group
|
|
61
|
+
$ unbound user-groups delete <id> Delete a user group
|
|
62
|
+
|
|
63
|
+
TOOL CONNECTIONS
|
|
64
|
+
$ unbound tools list List connected AI tools
|
|
65
|
+
$ unbound tools connect <type> Connect a new tool
|
|
66
|
+
$ unbound tools approved List approved tool types
|
|
67
|
+
|
|
68
|
+
CONFIGURATION
|
|
69
|
+
$ unbound config show Show all settings
|
|
70
|
+
$ unbound config set-url <url> Set API base URL
|
|
71
|
+
$ unbound config set-frontend-url <url> Set frontend URL
|
|
72
|
+
$ unbound config reset-url Reset API URL to default
|
|
73
|
+
$ unbound config reset-frontend-url Reset frontend URL to default
|
|
74
|
+
|
|
75
|
+
ENVIRONMENT VARIABLES
|
|
30
76
|
UNBOUND_API_URL Override the API base URL (e.g. http://localhost:8000)
|
|
31
77
|
UNBOUND_FRONTEND_URL Override the frontend URL (e.g. http://localhost:3000)
|
|
32
78
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
79
|
+
FILES
|
|
80
|
+
~/.unbound/config.json Credentials and CLI settings
|
|
81
|
+
|
|
82
|
+
LEARN MORE
|
|
83
|
+
Use "unbound <command> --help" for more information about a command.
|
|
84
|
+
Use "unbound <command> <subcommand> --help" for subcommand details.
|
|
37
85
|
`);
|
|
38
86
|
|
|
39
87
|
// Register all command modules
|
package/src/output.js
CHANGED
|
@@ -77,4 +77,34 @@ function info(msg) {
|
|
|
77
77
|
console.error(c.cyan(`\u2139 ${msg}`));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
function spinner(message) {
|
|
81
|
+
if (!process.stderr.isTTY) {
|
|
82
|
+
console.error(message);
|
|
83
|
+
return { stop: () => {}, succeed: (msg) => success(msg), fail: (msg) => error(msg) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const frames = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F'];
|
|
87
|
+
let i = 0;
|
|
88
|
+
const interval = setInterval(() => {
|
|
89
|
+
process.stderr.write(`\r${c.cyan(frames[i++ % frames.length])} ${message}`);
|
|
90
|
+
}, 80);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
stop() {
|
|
94
|
+
clearInterval(interval);
|
|
95
|
+
process.stderr.write('\r\x1b[K');
|
|
96
|
+
},
|
|
97
|
+
succeed(msg) {
|
|
98
|
+
clearInterval(interval);
|
|
99
|
+
process.stderr.write('\r\x1b[K');
|
|
100
|
+
success(msg);
|
|
101
|
+
},
|
|
102
|
+
fail(msg) {
|
|
103
|
+
clearInterval(interval);
|
|
104
|
+
process.stderr.write('\r\x1b[K');
|
|
105
|
+
error(msg);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { table, json, keyValue, success, error, warn, info, spinner };
|