unbound-cli 0.1.6 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbound-cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "CLI tool for Unbound - AI Gateway management",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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 USER_AGENT = 'UnboundCLI/0.1.0';
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
@@ -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
- await open(authUrl);
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
  }
@@ -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
- // Determine frontend URL: --domain flag or default
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}`);
@@ -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 and configuration from ~/.unbound/config.json.')
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 all stored credentials (API key, email, organization) from
10
+ Clears stored credentials (API key, email, organization) from
11
11
  ~/.unbound/config.json. Custom URL settings (base_url, frontend_url)
12
- are also removed.
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.clearConfig();
20
- output.success('Logged out successfully.');
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;
@@ -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
- const obj = {};
55
- for (const [key, value] of pairs) {
56
- obj[key.toLowerCase().replace(/\s+/g, '_')] = value;
57
- }
58
- output.json(obj);
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
 
@@ -25,15 +25,18 @@ Examples:
25
25
  `)
26
26
  .option('--json', 'Output raw JSON')
27
27
  .action(async (opts) => {
28
- try {
29
- if (!config.isLoggedIn()) {
30
- output.error('Not logged in. Run `unbound login` first.');
31
- process.exitCode = 1;
32
- return;
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
- const cfg = config.readConfig();
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
- output.error(err.message);
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
- // Corrupted config, start fresh
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
- Examples:
16
- $ unbound login Sign in via browser
17
- $ unbound login --api-key <key> Sign in with an API key
18
- $ unbound whoami Show current user info
19
- $ unbound policy list List all policies
20
- $ unbound policy list --type SECURITY List security policies
21
- $ unbound users list List organization users
22
- $ unbound user-groups list List user groups
23
- $ unbound tools list List connected AI tools
24
- $ unbound setup cursor Configure Cursor to use Unbound
25
- $ unbound setup claude-code Configure Claude Code to use Unbound
26
- $ unbound config set-url http://localhost:8000 Use local backend
27
- $ unbound config set-frontend-url http://localhost:3000 Use local frontend
28
-
29
- Environment Variables:
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
- Configuration:
34
- Config is stored in ~/.unbound/config.json
35
- Production backend: https://backend.getunbound.ai
36
- Production frontend: https://gateway.getunbound.ai
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
- module.exports = { table, json, keyValue, success, error, warn, info };
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 };