unbound-cli 0.1.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/.github/workflows/publish.yml +27 -0
- package/.github/workflows/version-check.yml +35 -0
- package/LOCAL_DEV.md +126 -0
- package/README.md +128 -0
- package/package.json +27 -0
- package/src/api.js +81 -0
- package/src/auth.js +115 -0
- package/src/commands/login.js +63 -0
- package/src/commands/logout.js +28 -0
- package/src/commands/policy.js +412 -0
- package/src/commands/setup.js +341 -0
- package/src/commands/status.js +59 -0
- package/src/commands/tools.js +176 -0
- package/src/commands/user-groups.js +282 -0
- package/src/commands/users.js +88 -0
- package/src/commands/whoami.js +49 -0
- package/src/config.js +87 -0
- package/src/index.js +129 -0
- package/src/output.js +65 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
const config = require('../config');
|
|
4
|
+
const output = require('../output');
|
|
5
|
+
const { ensureLoggedIn } = require('../auth');
|
|
6
|
+
|
|
7
|
+
const SETUP_BASE_URL = 'https://raw.githubusercontent.com/websentry-ai/setup/refs/heads/main';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Runs a Python setup script from the setup repo, passing the stored API key.
|
|
11
|
+
*/
|
|
12
|
+
function runSetupScript(scriptPath, apiKey) {
|
|
13
|
+
const url = `${SETUP_BASE_URL}/${scriptPath}`;
|
|
14
|
+
const cmd = `curl -fsSL "${url}" | python3 - --api-key "${apiKey}"`;
|
|
15
|
+
|
|
16
|
+
console.log('');
|
|
17
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Ensures login, gets the stored API key, and runs the setup script(s).
|
|
22
|
+
*/
|
|
23
|
+
function makeAction(...scriptPaths) {
|
|
24
|
+
return async () => {
|
|
25
|
+
try {
|
|
26
|
+
await ensureLoggedIn();
|
|
27
|
+
const apiKey = config.getApiKey();
|
|
28
|
+
|
|
29
|
+
for (const scriptPath of scriptPaths) {
|
|
30
|
+
runSetupScript(scriptPath, apiKey);
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
output.error(err.message);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function register(program) {
|
|
40
|
+
const setup = program
|
|
41
|
+
.command('setup')
|
|
42
|
+
.description(
|
|
43
|
+
'Configure AI coding tools to use Unbound as their API gateway. ' +
|
|
44
|
+
'Supported tools: cursor, claude-code, gemini-cli, codex, roo-code, cline, kilo-code, custom-access.'
|
|
45
|
+
)
|
|
46
|
+
.addHelpText('after', `
|
|
47
|
+
Automated setup (downloads scripts, sets env vars, configures tool):
|
|
48
|
+
$ unbound setup cursor # Download hooks, set env, restart Cursor
|
|
49
|
+
$ unbound setup claude-code # Set up gateway + hooks for Claude Code
|
|
50
|
+
$ unbound setup gemini-cli # Set GEMINI_API_KEY and base URL
|
|
51
|
+
$ unbound setup codex # Set OPENAI_API_KEY and base URL
|
|
52
|
+
|
|
53
|
+
Instruction-only (shows API key and base URL to configure manually):
|
|
54
|
+
$ unbound setup roo-code # Show Roo Code config values
|
|
55
|
+
$ unbound setup cline # Show Cline config values
|
|
56
|
+
$ unbound setup kilo-code # Show Kilo Code config values
|
|
57
|
+
$ unbound setup custom-access # Show API key and base URL for direct API access
|
|
58
|
+
|
|
59
|
+
All setup commands require login. If not logged in, the browser will
|
|
60
|
+
open automatically to authenticate before proceeding.
|
|
61
|
+
`);
|
|
62
|
+
|
|
63
|
+
setup
|
|
64
|
+
.command('cursor')
|
|
65
|
+
.description(
|
|
66
|
+
'Set up Cursor to use Unbound. Downloads hook scripts, sets the ' +
|
|
67
|
+
'UNBOUND_CURSOR_API_KEY environment variable, and restarts Cursor.'
|
|
68
|
+
)
|
|
69
|
+
.addHelpText('after', `
|
|
70
|
+
What this does:
|
|
71
|
+
1. Downloads Cursor hook scripts from the Unbound setup repository
|
|
72
|
+
2. Sets the UNBOUND_CURSOR_API_KEY environment variable in your shell profile
|
|
73
|
+
3. Restarts Cursor to apply changes
|
|
74
|
+
|
|
75
|
+
Prerequisites:
|
|
76
|
+
- Must be logged in (will auto-open browser to authenticate if not)
|
|
77
|
+
- Python 3 and curl must be installed
|
|
78
|
+
- Cursor must be installed
|
|
79
|
+
|
|
80
|
+
Examples:
|
|
81
|
+
$ unbound setup cursor
|
|
82
|
+
`)
|
|
83
|
+
.action(makeAction('cursor/setup.py'));
|
|
84
|
+
|
|
85
|
+
setup
|
|
86
|
+
.command('claude-code')
|
|
87
|
+
.description(
|
|
88
|
+
'Set up Claude Code to use Unbound. Prompts whether to use your existing ' +
|
|
89
|
+
'Claude subscription or use Unbound as the AI provider.'
|
|
90
|
+
)
|
|
91
|
+
.option('--subscription', 'Use your existing Claude subscription (hooks only)')
|
|
92
|
+
.option('--gateway', 'Use Unbound as the AI provider (gateway mode)')
|
|
93
|
+
.addHelpText('after', `
|
|
94
|
+
Modes:
|
|
95
|
+
Subscription (hooks only):
|
|
96
|
+
Keep your existing Claude subscription. Installs Unbound hooks for
|
|
97
|
+
policy enforcement (security guardrails, cost limits) without changing
|
|
98
|
+
the AI provider. Runs claude-code/hooks/setup.py.
|
|
99
|
+
|
|
100
|
+
Gateway:
|
|
101
|
+
Use Unbound as the AI provider. Routes all Claude Code requests through
|
|
102
|
+
the Unbound gateway for full policy enforcement and model management.
|
|
103
|
+
Runs claude-code/gateway/setup.py.
|
|
104
|
+
|
|
105
|
+
If neither --subscription nor --gateway is provided, an interactive
|
|
106
|
+
prompt will ask you to choose.
|
|
107
|
+
|
|
108
|
+
Prerequisites:
|
|
109
|
+
- Must be logged in (will auto-open browser to authenticate if not)
|
|
110
|
+
- Python 3 and curl must be installed
|
|
111
|
+
- Claude Code must be installed
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
$ unbound setup claude-code # Interactive mode selection
|
|
115
|
+
$ unbound setup claude-code --subscription # Hooks only (keep your subscription)
|
|
116
|
+
$ unbound setup claude-code --gateway # Use Unbound as AI provider
|
|
117
|
+
`)
|
|
118
|
+
.action(async (opts) => {
|
|
119
|
+
try {
|
|
120
|
+
await ensureLoggedIn();
|
|
121
|
+
const apiKey = config.getApiKey();
|
|
122
|
+
|
|
123
|
+
let useSubscription = opts.subscription;
|
|
124
|
+
if (!opts.subscription && !opts.gateway) {
|
|
125
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
const answer = await new Promise((resolve) => {
|
|
127
|
+
console.log('\nHow do you want to use Claude Code with Unbound?\n');
|
|
128
|
+
console.log(' 1. I have my own Claude subscription (hooks only - policy enforcement)');
|
|
129
|
+
console.log(' 2. Use Unbound as the AI provider (gateway mode)\n');
|
|
130
|
+
rl.question('Choose (1 or 2): ', resolve);
|
|
131
|
+
});
|
|
132
|
+
rl.close();
|
|
133
|
+
useSubscription = answer.trim() === '1';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (useSubscription) {
|
|
137
|
+
runSetupScript('claude-code/hooks/setup.py', apiKey);
|
|
138
|
+
} else {
|
|
139
|
+
runSetupScript('claude-code/gateway/setup.py', apiKey);
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
output.error(err.message);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
setup
|
|
148
|
+
.command('gemini-cli')
|
|
149
|
+
.description(
|
|
150
|
+
'Set up Gemini CLI to use Unbound. Sets GEMINI_API_KEY and ' +
|
|
151
|
+
'GOOGLE_GEMINI_BASE_URL environment variables.'
|
|
152
|
+
)
|
|
153
|
+
.addHelpText('after', `
|
|
154
|
+
What this does:
|
|
155
|
+
1. Sets GEMINI_API_KEY to your Unbound API key in your shell profile
|
|
156
|
+
2. Sets GOOGLE_GEMINI_BASE_URL to the Unbound gateway endpoint
|
|
157
|
+
3. Gemini CLI will route all requests through Unbound
|
|
158
|
+
|
|
159
|
+
Prerequisites:
|
|
160
|
+
- Must be logged in (will auto-open browser to authenticate if not)
|
|
161
|
+
- Python 3 and curl must be installed
|
|
162
|
+
- Gemini CLI must be installed
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
$ unbound setup gemini-cli
|
|
166
|
+
`)
|
|
167
|
+
.action(makeAction('gemini-cli/gateway/setup.py'));
|
|
168
|
+
|
|
169
|
+
setup
|
|
170
|
+
.command('codex')
|
|
171
|
+
.description(
|
|
172
|
+
'Set up Codex to use Unbound. Sets OPENAI_API_KEY and ' +
|
|
173
|
+
'OPENAI_BASE_URL environment variables.'
|
|
174
|
+
)
|
|
175
|
+
.addHelpText('after', `
|
|
176
|
+
What this does:
|
|
177
|
+
1. Sets OPENAI_API_KEY to your Unbound API key in your shell profile
|
|
178
|
+
2. Sets OPENAI_BASE_URL to the Unbound gateway endpoint
|
|
179
|
+
3. Codex will route all requests through Unbound
|
|
180
|
+
|
|
181
|
+
Prerequisites:
|
|
182
|
+
- Must be logged in (will auto-open browser to authenticate if not)
|
|
183
|
+
- Python 3 and curl must be installed
|
|
184
|
+
- Codex must be installed
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
$ unbound setup codex
|
|
188
|
+
`)
|
|
189
|
+
.action(makeAction('codex/gateway/setup.py'));
|
|
190
|
+
|
|
191
|
+
// --- Instruction-only tools ---
|
|
192
|
+
|
|
193
|
+
setup
|
|
194
|
+
.command('roo-code')
|
|
195
|
+
.description('Show setup values for Roo Code (VS Code extension). Displays the API provider and API key to configure in Roo Code settings.')
|
|
196
|
+
.addHelpText('after', `
|
|
197
|
+
Output:
|
|
198
|
+
API Provider - Select "unbound" in Roo Code provider settings
|
|
199
|
+
API Key - Your Unbound API key to paste into the extension
|
|
200
|
+
|
|
201
|
+
To configure:
|
|
202
|
+
1. Open Command Palette (Ctrl/Cmd + Shift + P)
|
|
203
|
+
2. Type "Roo Code: Open Settings"
|
|
204
|
+
3. Select "unbound" as the API provider
|
|
205
|
+
4. Paste the API key shown below
|
|
206
|
+
5. Select your preferred model from the dropdown
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
$ unbound setup roo-code
|
|
210
|
+
`)
|
|
211
|
+
.action(async () => {
|
|
212
|
+
try {
|
|
213
|
+
await ensureLoggedIn();
|
|
214
|
+
const apiKey = config.getApiKey();
|
|
215
|
+
|
|
216
|
+
output.keyValue([
|
|
217
|
+
['API Provider', 'unbound'],
|
|
218
|
+
['API Key', apiKey],
|
|
219
|
+
]);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
output.error(err.message);
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
setup
|
|
227
|
+
.command('cline')
|
|
228
|
+
.description('Show setup values for Cline (VS Code extension). Displays the API provider, Base URL, and API key to configure in Cline settings.')
|
|
229
|
+
.addHelpText('after', `
|
|
230
|
+
Output:
|
|
231
|
+
API Provider - Use "OpenAI Compatible" in Cline provider settings
|
|
232
|
+
Base URL - The Unbound gateway URL to paste as the Base URL
|
|
233
|
+
API Key - Your Unbound API key to paste into the extension
|
|
234
|
+
|
|
235
|
+
To configure:
|
|
236
|
+
1. Open Cline extension in VS Code
|
|
237
|
+
2. Select "Bring your own API key"
|
|
238
|
+
3. Set API Provider to "OpenAI Compatible"
|
|
239
|
+
4. Paste the Base URL and API Key shown below
|
|
240
|
+
5. Select a model (e.g., gpt-5.1)
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
$ unbound setup cline
|
|
244
|
+
`)
|
|
245
|
+
.action(async () => {
|
|
246
|
+
try {
|
|
247
|
+
await ensureLoggedIn();
|
|
248
|
+
const apiKey = config.getApiKey();
|
|
249
|
+
const frontendUrl = config.getFrontendUrl();
|
|
250
|
+
|
|
251
|
+
output.keyValue([
|
|
252
|
+
['API Provider', 'OpenAI Compatible'],
|
|
253
|
+
['Base URL', frontendUrl],
|
|
254
|
+
['API Key', apiKey],
|
|
255
|
+
]);
|
|
256
|
+
} catch (err) {
|
|
257
|
+
output.error(err.message);
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
setup
|
|
263
|
+
.command('kilo-code')
|
|
264
|
+
.description('Show setup values for Kilo Code (VS Code extension or CLI). Displays the API provider and API key to configure in Kilo Code.')
|
|
265
|
+
.addHelpText('after', `
|
|
266
|
+
Output:
|
|
267
|
+
API Provider - Select "Unbound" in Kilo Code provider settings
|
|
268
|
+
API Key - Your Unbound API key to paste into the extension or CLI
|
|
269
|
+
|
|
270
|
+
To configure (IDE extension):
|
|
271
|
+
1. Open Extensions (Ctrl/Cmd + Shift + X), install Kilo Code
|
|
272
|
+
2. Select "Use your own API key"
|
|
273
|
+
3. Set API Provider to "Unbound"
|
|
274
|
+
4. Paste the API Key shown below
|
|
275
|
+
5. Select a model (e.g., claude-opus-4-5, gpt-5.1)
|
|
276
|
+
|
|
277
|
+
To configure (CLI):
|
|
278
|
+
1. Install: npm install -g @kilocode/cli
|
|
279
|
+
2. Run: kilocode
|
|
280
|
+
3. Set API Provider to "Unbound"
|
|
281
|
+
4. Paste the API Key shown below
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
$ unbound setup kilo-code
|
|
285
|
+
`)
|
|
286
|
+
.action(async () => {
|
|
287
|
+
try {
|
|
288
|
+
await ensureLoggedIn();
|
|
289
|
+
const apiKey = config.getApiKey();
|
|
290
|
+
|
|
291
|
+
output.keyValue([
|
|
292
|
+
['API Provider', 'Unbound'],
|
|
293
|
+
['API Key', apiKey],
|
|
294
|
+
]);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
output.error(err.message);
|
|
297
|
+
process.exitCode = 1;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
setup
|
|
302
|
+
.command('custom-access')
|
|
303
|
+
.description('Show API key and base URL for direct API access. Use this to build custom integrations with the Unbound gateway.')
|
|
304
|
+
.addHelpText('after', `
|
|
305
|
+
Output:
|
|
306
|
+
API Key - Your Unbound API key for authentication
|
|
307
|
+
Base URL - The Unbound gateway endpoint for API requests
|
|
308
|
+
|
|
309
|
+
The Base URL is OpenAI-compatible. Use it with any OpenAI SDK or HTTP client:
|
|
310
|
+
|
|
311
|
+
curl -X POST {base_url}/v1/chat/completions \\
|
|
312
|
+
-H "Authorization: Bearer {api_key}" \\
|
|
313
|
+
-H "Content-Type: application/json" \\
|
|
314
|
+
-d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Hello"}]}'
|
|
315
|
+
|
|
316
|
+
Or with the Python SDK:
|
|
317
|
+
pip install unbound-gateway
|
|
318
|
+
from unbound import Unbound
|
|
319
|
+
client = Unbound(base_url="{base_url}", api_key="{api_key}")
|
|
320
|
+
|
|
321
|
+
Examples:
|
|
322
|
+
$ unbound setup custom-access
|
|
323
|
+
`)
|
|
324
|
+
.action(async () => {
|
|
325
|
+
try {
|
|
326
|
+
await ensureLoggedIn();
|
|
327
|
+
const apiKey = config.getApiKey();
|
|
328
|
+
const frontendUrl = config.getFrontendUrl();
|
|
329
|
+
|
|
330
|
+
output.keyValue([
|
|
331
|
+
['API Key', apiKey],
|
|
332
|
+
['Base URL', frontendUrl],
|
|
333
|
+
]);
|
|
334
|
+
} catch (err) {
|
|
335
|
+
output.error(err.message);
|
|
336
|
+
process.exitCode = 1;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = { register };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const api = require('../api');
|
|
3
|
+
const output = require('../output');
|
|
4
|
+
|
|
5
|
+
function register(program) {
|
|
6
|
+
program
|
|
7
|
+
.command('status')
|
|
8
|
+
.description('Show the current CLI status including config location, login state, and API connectivity. Useful for debugging connection issues.')
|
|
9
|
+
.addHelpText('after', `
|
|
10
|
+
Output fields:
|
|
11
|
+
Config file - Path to the config file (~/.unbound/config.json)
|
|
12
|
+
Logged in - Whether credentials are stored (Yes/No)
|
|
13
|
+
Email - The authenticated user's email (if logged in)
|
|
14
|
+
Organization - The organization name (if logged in)
|
|
15
|
+
API base URL - The backend API URL being used (if logged in)
|
|
16
|
+
Frontend URL - The frontend URL for browser auth (if logged in)
|
|
17
|
+
API status - Connectivity check result (Connected / Error)
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
$ unbound status
|
|
21
|
+
`)
|
|
22
|
+
.action(async () => {
|
|
23
|
+
try {
|
|
24
|
+
const loggedIn = config.isLoggedIn();
|
|
25
|
+
const cfg = config.readConfig();
|
|
26
|
+
|
|
27
|
+
const pairs = [
|
|
28
|
+
['Config file', config.CONFIG_FILE],
|
|
29
|
+
['Logged in', loggedIn ? 'Yes' : 'No'],
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
if (loggedIn) {
|
|
33
|
+
pairs.push(['Email', cfg.email || '-']);
|
|
34
|
+
pairs.push(['Organization', cfg.org_name || '-']);
|
|
35
|
+
pairs.push(['API base URL', config.getBaseUrl()]);
|
|
36
|
+
pairs.push(['Frontend URL', config.getFrontendUrl()]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check API connectivity
|
|
40
|
+
let connectivity = 'Not checked (not logged in)';
|
|
41
|
+
if (loggedIn) {
|
|
42
|
+
try {
|
|
43
|
+
await api.get('/api/v1/users/privileges/');
|
|
44
|
+
connectivity = 'Connected';
|
|
45
|
+
} catch (err) {
|
|
46
|
+
connectivity = `Error: ${err.message}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
pairs.push(['API status', connectivity]);
|
|
50
|
+
|
|
51
|
+
output.keyValue(pairs);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
output.error(err.message);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { register };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const config = require('../config');
|
|
2
|
+
const api = require('../api');
|
|
3
|
+
const output = require('../output');
|
|
4
|
+
|
|
5
|
+
function formatDate(dateStr) {
|
|
6
|
+
if (!dateStr) return '-';
|
|
7
|
+
return new Date(dateStr).toLocaleDateString();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SUPPORTED_TOOL_TYPES = [
|
|
11
|
+
'CLAUDE_CODE',
|
|
12
|
+
'CURSOR',
|
|
13
|
+
'COPILOT',
|
|
14
|
+
'ROO_CODE',
|
|
15
|
+
'CLINE',
|
|
16
|
+
'GEMINI_CLI',
|
|
17
|
+
'CODEX',
|
|
18
|
+
'KILO_CODE',
|
|
19
|
+
'CUSTOM_ACCESS',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function register(program) {
|
|
23
|
+
const tools = program
|
|
24
|
+
.command('tools')
|
|
25
|
+
.description(
|
|
26
|
+
'Manage connected AI coding tools. List active tool connections, ' +
|
|
27
|
+
'connect new tools, and view approved tool types. ' +
|
|
28
|
+
`Supported types: ${SUPPORTED_TOOL_TYPES.join(', ')}.`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// tools list
|
|
32
|
+
tools
|
|
33
|
+
.command('list')
|
|
34
|
+
.description('List all connected tools across the organization, showing their status and usage.')
|
|
35
|
+
.option('--json', 'Output raw JSON instead of a table')
|
|
36
|
+
.addHelpText('after', `
|
|
37
|
+
Output columns:
|
|
38
|
+
Tool Type - The type of AI tool (e.g. CURSOR, CLAUDE_CODE)
|
|
39
|
+
User Email - The user who connected the tool
|
|
40
|
+
Status - Connection status
|
|
41
|
+
Monthly Cost - Cost incurred this month
|
|
42
|
+
Connected At - Date the tool was connected
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
$ unbound tools list
|
|
46
|
+
$ unbound tools list --json
|
|
47
|
+
`)
|
|
48
|
+
.action(async (opts) => {
|
|
49
|
+
try {
|
|
50
|
+
if (!config.isLoggedIn()) {
|
|
51
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = await api.get('/api/v1/agents/');
|
|
57
|
+
|
|
58
|
+
if (opts.json) {
|
|
59
|
+
output.json(data);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
output.table(data.agents, [
|
|
64
|
+
{ key: 'tool_type', header: 'Tool Type' },
|
|
65
|
+
{ key: 'user', header: 'User Email', format: (v) => (v ? v.email : '-') },
|
|
66
|
+
{ key: 'status', header: 'Status' },
|
|
67
|
+
{ key: 'monthly_cost', header: 'Monthly Cost', format: (v) => (v != null ? String(v) : '-') },
|
|
68
|
+
{ key: 'connected_at', header: 'Connected At', format: (v) => formatDate(v) },
|
|
69
|
+
]);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
output.error(err.message);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// tools connect
|
|
77
|
+
tools
|
|
78
|
+
.command('connect <tool-type>')
|
|
79
|
+
.description(`Connect a new AI coding tool. Generates an API key and connection details for the specified tool type. Supported types: ${SUPPORTED_TOOL_TYPES.join(', ')}.`)
|
|
80
|
+
.addHelpText('after', `
|
|
81
|
+
Supported tool types:
|
|
82
|
+
${SUPPORTED_TOOL_TYPES.join(', ')}
|
|
83
|
+
|
|
84
|
+
Output fields:
|
|
85
|
+
Tool Type - The connected tool type
|
|
86
|
+
Status - Connection status
|
|
87
|
+
API Key - Generated API key for the tool
|
|
88
|
+
Application ID - Application identifier
|
|
89
|
+
Application Name - Application display name
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
$ unbound tools connect CLAUDE_CODE
|
|
93
|
+
$ unbound tools connect CURSOR
|
|
94
|
+
$ unbound tools connect GEMINI_CLI
|
|
95
|
+
`)
|
|
96
|
+
.action(async (toolType) => {
|
|
97
|
+
try {
|
|
98
|
+
if (!config.isLoggedIn()) {
|
|
99
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const normalized = toolType.toUpperCase();
|
|
105
|
+
if (!SUPPORTED_TOOL_TYPES.includes(normalized)) {
|
|
106
|
+
output.error(`Unsupported tool type: ${toolType}. Supported types: ${SUPPORTED_TOOL_TYPES.join(', ')}`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = await api.post('/api/v1/agents/connect/', { body: { tool_type: normalized } });
|
|
112
|
+
|
|
113
|
+
output.success(`Connected ${normalized}.`);
|
|
114
|
+
output.keyValue([
|
|
115
|
+
['Tool Type', data.tool_type],
|
|
116
|
+
['Status', data.status],
|
|
117
|
+
['API Key', data.api_key || '-'],
|
|
118
|
+
['Application ID', data.application_id || '-'],
|
|
119
|
+
['Application Name', data.application_name || '-'],
|
|
120
|
+
]);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
output.error(err.message);
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// tools approved
|
|
128
|
+
tools
|
|
129
|
+
.command('approved')
|
|
130
|
+
.description('List approved tool types for the organization and whether tool restrictions are enforced.')
|
|
131
|
+
.option('--json', 'Output raw JSON')
|
|
132
|
+
.addHelpText('after', `
|
|
133
|
+
Output:
|
|
134
|
+
Restriction Enabled - Whether the organization restricts which tools can connect
|
|
135
|
+
Approved Tool Types - List of tool types allowed to connect
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
$ unbound tools approved
|
|
139
|
+
$ unbound tools approved --json
|
|
140
|
+
`)
|
|
141
|
+
.action(async (opts) => {
|
|
142
|
+
try {
|
|
143
|
+
if (!config.isLoggedIn()) {
|
|
144
|
+
output.error('Not logged in. Run `unbound login` first.');
|
|
145
|
+
process.exitCode = 1;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = await api.get('/api/v1/organizations/approved-tools/');
|
|
150
|
+
|
|
151
|
+
if (opts.json) {
|
|
152
|
+
output.json(data);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (data.restriction_enabled !== undefined) {
|
|
157
|
+
console.log(`Restriction Enabled: ${data.restriction_enabled}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (data.approved_tools && data.approved_tools.length > 0) {
|
|
162
|
+
console.log('Approved Tool Types:');
|
|
163
|
+
for (const tool of data.approved_tools) {
|
|
164
|
+
console.log(` ${typeof tool === 'string' ? tool : tool.tool_type || tool.name || JSON.stringify(tool)}`);
|
|
165
|
+
}
|
|
166
|
+
} else {
|
|
167
|
+
console.log('No approved tools configured.');
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
output.error(err.message);
|
|
171
|
+
process.exitCode = 1;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { register };
|