unbound-cli 0.2.1 → 0.3.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/README.md +1 -0
- package/package.json +1 -1
- package/src/auth.js +47 -3
- package/src/commands/login.js +4 -9
- package/src/commands/setup.js +225 -377
- package/src/index.js +6 -0
package/README.md
CHANGED
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -6,6 +6,7 @@ const http = require('http');
|
|
|
6
6
|
const { URL } = require('url');
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
const output = require('./output');
|
|
9
|
+
const api = require('./api');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Opens the browser for authentication and waits for the callback.
|
|
@@ -102,10 +103,53 @@ async function loginWithBrowser(frontendUrl) {
|
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
/**
|
|
105
|
-
*
|
|
106
|
+
* Validates an API key against the backend and stores it.
|
|
107
|
+
* Shows a spinner during validation and a success message with user info.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} apiKey - The API key to validate and store
|
|
110
|
+
* @returns {Promise<true>}
|
|
111
|
+
*/
|
|
112
|
+
async function loginWithApiKey(apiKey) {
|
|
113
|
+
const spin = output.spinner('Authenticating...');
|
|
114
|
+
let response;
|
|
115
|
+
try {
|
|
116
|
+
response = await api.get('/api/v1/users/privileges/', { apiKey });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
let msg;
|
|
119
|
+
if (err.statusCode === 401 || err.statusCode === 403) {
|
|
120
|
+
msg = 'Authentication failed. The API key may be invalid or expired.';
|
|
121
|
+
} else if (err.statusCode) {
|
|
122
|
+
msg = `Authentication failed (HTTP ${err.statusCode}). Please try again.`;
|
|
123
|
+
} else {
|
|
124
|
+
msg = 'Unable to reach the server. Please check your internet connection.';
|
|
125
|
+
}
|
|
126
|
+
spin.fail(msg);
|
|
127
|
+
err.displayed = true;
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
config.setApiKey(apiKey);
|
|
131
|
+
config.backfillUserInfo(response);
|
|
132
|
+
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (response.email) parts.push(response.email);
|
|
135
|
+
const orgName = response.org_name || response.organization_name || response.organization;
|
|
136
|
+
if (orgName) parts.push(orgName);
|
|
137
|
+
spin.succeed(parts.length > 0 ? `Authenticated as ${parts.join(' \u00b7 ')}` : 'Authenticated');
|
|
138
|
+
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Ensures the user is logged in. If an apiKey is provided, validates and
|
|
144
|
+
* stores it first. If not logged in, triggers browser-based login.
|
|
106
145
|
* Returns true if logged in (or just logged in), false on failure.
|
|
107
146
|
*/
|
|
108
|
-
async function ensureLoggedIn() {
|
|
147
|
+
async function ensureLoggedIn({ apiKey } = {}) {
|
|
148
|
+
if (apiKey) {
|
|
149
|
+
await loginWithApiKey(apiKey);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
109
153
|
if (config.isLoggedIn()) {
|
|
110
154
|
return true;
|
|
111
155
|
}
|
|
@@ -122,4 +166,4 @@ async function ensureLoggedIn() {
|
|
|
122
166
|
return true;
|
|
123
167
|
}
|
|
124
168
|
|
|
125
|
-
module.exports = { loginWithBrowser, ensureLoggedIn };
|
|
169
|
+
module.exports = { loginWithBrowser, loginWithApiKey, ensureLoggedIn };
|
package/src/commands/login.js
CHANGED
|
@@ -2,7 +2,7 @@ const { Option } = require('commander');
|
|
|
2
2
|
const config = require('../config');
|
|
3
3
|
const output = require('../output');
|
|
4
4
|
const api = require('../api');
|
|
5
|
-
const { loginWithBrowser } = require('../auth');
|
|
5
|
+
const { loginWithBrowser, loginWithApiKey } = require('../auth');
|
|
6
6
|
|
|
7
7
|
function register(program) {
|
|
8
8
|
program
|
|
@@ -39,18 +39,13 @@ Examples:
|
|
|
39
39
|
let apiKey;
|
|
40
40
|
|
|
41
41
|
if (opts.apiKey) {
|
|
42
|
-
apiKey = opts.apiKey;
|
|
43
|
-
// Validate the key before storing
|
|
44
|
-
const spin = output.spinner('Validating API key...');
|
|
45
42
|
try {
|
|
46
|
-
await
|
|
47
|
-
|
|
48
|
-
} catch (err) {
|
|
49
|
-
spin.fail('Invalid API key. Check your key and try again.');
|
|
43
|
+
await loginWithApiKey(opts.apiKey);
|
|
44
|
+
} catch {
|
|
50
45
|
process.exitCode = 1;
|
|
51
46
|
return;
|
|
52
47
|
}
|
|
53
|
-
|
|
48
|
+
return;
|
|
54
49
|
} else {
|
|
55
50
|
// Browser flow — key is already saved by auth.loginWithBrowser()
|
|
56
51
|
let frontendUrl;
|
package/src/commands/setup.js
CHANGED
|
@@ -27,6 +27,54 @@ const MDM_TOOLS = {
|
|
|
27
27
|
// Default tools for --all (uses subscription mode for Claude Code and Codex since only one can be active)
|
|
28
28
|
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription'];
|
|
29
29
|
|
|
30
|
+
// Tool name → script mapping for automated tools
|
|
31
|
+
const SETUP_TOOL_MAP = {
|
|
32
|
+
'cursor': { label: 'Cursor', script: 'cursor/setup.py' },
|
|
33
|
+
'claude-code-subscription': { label: 'Claude Code (subscription)', script: 'claude-code/hooks/setup.py' },
|
|
34
|
+
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/setup.py' },
|
|
35
|
+
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/setup.py' },
|
|
36
|
+
'codex-subscription': { label: 'Codex (subscription)', script: 'codex/hooks/setup.py' },
|
|
37
|
+
'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/setup.py' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Instruction-only tools (display config values, no setup scripts)
|
|
41
|
+
const INSTRUCTION_TOOLS = {
|
|
42
|
+
'roo-code': { label: 'Roo Code', values: (apiKey) => [['API Provider', 'unbound'], ['API Key', apiKey]] },
|
|
43
|
+
'cline': { label: 'Cline', values: (apiKey, frontendUrl) => [['API Provider', 'OpenAI Compatible'], ['Base URL', frontendUrl], ['API Key', apiKey]] },
|
|
44
|
+
'kilo-code': { label: 'Kilo Code', values: (apiKey) => [['API Provider', 'Unbound'], ['API Key', apiKey]] },
|
|
45
|
+
'custom-access': { label: 'Custom Access', values: (apiKey, frontendUrl) => [['API Key', apiKey], ['Base URL', frontendUrl]] },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Tools that need interactive mode selection (claude-code, codex without suffix)
|
|
49
|
+
const MODE_TOOLS = {
|
|
50
|
+
'claude-code': {
|
|
51
|
+
prompt: 'How do you want to use Claude Code with Unbound?',
|
|
52
|
+
options: [
|
|
53
|
+
{ label: 'Use my Claude subscription (hooks)', value: 'subscription' },
|
|
54
|
+
{ label: 'Use Unbound as the AI provider (gateway)', value: 'gateway' },
|
|
55
|
+
],
|
|
56
|
+
subscription: 'claude-code-subscription',
|
|
57
|
+
gateway: 'claude-code-gateway',
|
|
58
|
+
},
|
|
59
|
+
'codex': {
|
|
60
|
+
prompt: 'How do you want to use Codex with Unbound?',
|
|
61
|
+
options: [
|
|
62
|
+
{ label: 'Use my OpenAI subscription (hooks)', value: 'subscription' },
|
|
63
|
+
{ label: 'Use Unbound as the AI provider (gateway)', value: 'gateway' },
|
|
64
|
+
],
|
|
65
|
+
subscription: 'codex-subscription',
|
|
66
|
+
gateway: 'codex-gateway',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Escapes a string for safe embedding in a shell command.
|
|
72
|
+
* Wraps in single quotes and escapes any embedded single quotes.
|
|
73
|
+
*/
|
|
74
|
+
function shellEscape(str) {
|
|
75
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
76
|
+
}
|
|
77
|
+
|
|
30
78
|
/**
|
|
31
79
|
* Builds a shell command that curls a setup script and pipes it to python3.
|
|
32
80
|
*/
|
|
@@ -39,7 +87,7 @@ function buildSetupCommand(scriptPath, args) {
|
|
|
39
87
|
* Runs a Python setup script from the setup repo with inherited stdio (live output).
|
|
40
88
|
*/
|
|
41
89
|
function runSetupScript(scriptPath, apiKey, { clear = false } = {}) {
|
|
42
|
-
const args = `--api-key
|
|
90
|
+
const args = `--api-key ${shellEscape(apiKey)}${clear ? ' --clear' : ''}`;
|
|
43
91
|
console.log('');
|
|
44
92
|
try {
|
|
45
93
|
execSync(buildSetupCommand(scriptPath, args), { stdio: 'inherit' });
|
|
@@ -111,418 +159,218 @@ async function runBatch(tools, runFn, { clear = false } = {}) {
|
|
|
111
159
|
return true;
|
|
112
160
|
}
|
|
113
161
|
|
|
114
|
-
/**
|
|
115
|
-
* Ensures login, gets the stored API key, and runs the setup script(s).
|
|
116
|
-
*/
|
|
117
|
-
function makeAction(...scriptPaths) {
|
|
118
|
-
return async (opts) => {
|
|
119
|
-
try {
|
|
120
|
-
await ensureLoggedIn();
|
|
121
|
-
const apiKey = config.getApiKey();
|
|
122
|
-
|
|
123
|
-
for (const scriptPath of scriptPaths) {
|
|
124
|
-
runSetupScript(scriptPath, apiKey, { clear: opts.clear });
|
|
125
|
-
}
|
|
126
|
-
} catch (err) {
|
|
127
|
-
output.error(err.message);
|
|
128
|
-
process.exitCode = 1;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
162
|
function register(program) {
|
|
134
163
|
const setup = program
|
|
135
164
|
.command('setup')
|
|
165
|
+
.argument('[tools...]', 'Tools to set up')
|
|
136
166
|
.description(
|
|
137
167
|
'Configure AI coding tools to use Unbound as their API gateway. ' +
|
|
138
|
-
'
|
|
168
|
+
'Run with no arguments for interactive setup, or specify tools directly.'
|
|
139
169
|
)
|
|
170
|
+
.option('--api-key <key>', 'Authenticate with an API key (skips browser login)')
|
|
171
|
+
.option('--clear', 'Remove Unbound configuration for the specified tools')
|
|
172
|
+
.option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
|
|
173
|
+
.option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
|
|
140
174
|
.addHelpText('after', `
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
All setup commands require login. If not logged in, the browser will
|
|
157
|
-
open automatically to authenticate before proceeding.
|
|
158
|
-
`)
|
|
159
|
-
// Parent action: runs when `unbound setup` is invoked with no subcommand.
|
|
160
|
-
// Subcommand actions (cursor, claude-code, etc.) take precedence when specified.
|
|
161
|
-
.action(async () => {
|
|
162
|
-
try {
|
|
163
|
-
await ensureLoggedIn();
|
|
164
|
-
const apiKey = config.getApiKey();
|
|
165
|
-
|
|
166
|
-
const selected = await output.multiSelect(
|
|
167
|
-
'Select tools to set up with Unbound:',
|
|
168
|
-
SETUP_TOOLS
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
if (selected.length === 0) {
|
|
172
|
-
output.warn('No tools selected.');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const selectedTools = SETUP_TOOLS.filter(t => selected.includes(t.value));
|
|
177
|
-
console.log('');
|
|
178
|
-
|
|
179
|
-
const ok = await runBatch(selectedTools, (tool) =>
|
|
180
|
-
runScriptPiped(tool.script, `--api-key "${apiKey}"`)
|
|
181
|
-
);
|
|
182
|
-
if (!ok) return;
|
|
183
|
-
|
|
184
|
-
console.log('');
|
|
185
|
-
output.success('All tools configured');
|
|
186
|
-
} catch (err) {
|
|
187
|
-
if (err.message === 'Selection cancelled') return;
|
|
188
|
-
output.error(err.message);
|
|
189
|
-
process.exitCode = 1;
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
setup
|
|
194
|
-
.command('cursor')
|
|
195
|
-
.description(
|
|
196
|
-
'Set up Cursor to use Unbound. Downloads hook scripts, sets the ' +
|
|
197
|
-
'UNBOUND_CURSOR_API_KEY environment variable, and restarts Cursor.'
|
|
198
|
-
)
|
|
199
|
-
.option('--clear', 'Remove Unbound configuration for Cursor')
|
|
200
|
-
.addHelpText('after', `
|
|
201
|
-
What this does:
|
|
202
|
-
1. Downloads Cursor hook scripts from the Unbound setup repository
|
|
203
|
-
2. Sets the UNBOUND_CURSOR_API_KEY environment variable in your shell profile
|
|
204
|
-
3. Restarts Cursor to apply changes
|
|
205
|
-
|
|
206
|
-
Prerequisites:
|
|
207
|
-
- Must be logged in (will auto-open browser to authenticate if not)
|
|
208
|
-
- Python 3 and curl must be installed
|
|
209
|
-
- Cursor must be installed
|
|
175
|
+
Available tools:
|
|
176
|
+
cursor Cursor IDE
|
|
177
|
+
claude-code Claude Code (use --subscription or --gateway)
|
|
178
|
+
gemini-cli Gemini CLI
|
|
179
|
+
codex Codex (use --subscription or --gateway)
|
|
180
|
+
roo-code Roo Code (shows config values)
|
|
181
|
+
cline Cline (shows config values)
|
|
182
|
+
kilo-code Kilo Code (shows config values)
|
|
183
|
+
custom-access Direct API access (shows config values)
|
|
184
|
+
|
|
185
|
+
For multi-tool setup, use explicit mode names:
|
|
186
|
+
claude-code-subscription Claude Code with your own subscription (hooks)
|
|
187
|
+
claude-code-gateway Claude Code with Unbound as AI provider
|
|
188
|
+
codex-subscription Codex with your own subscription (hooks)
|
|
189
|
+
codex-gateway Codex with Unbound as AI provider
|
|
210
190
|
|
|
211
191
|
Examples:
|
|
212
|
-
|
|
213
|
-
$ unbound setup cursor
|
|
192
|
+
Single tool:
|
|
193
|
+
$ unbound setup cursor Set up Cursor
|
|
194
|
+
$ unbound setup claude-code --gateway Claude Code gateway mode
|
|
195
|
+
$ unbound setup claude-code --subscription Claude Code hooks only
|
|
196
|
+
$ unbound setup codex --gateway Codex gateway mode
|
|
197
|
+
|
|
198
|
+
One-step login and setup:
|
|
199
|
+
$ unbound setup cursor --api-key <key> Login + set up Cursor
|
|
200
|
+
$ unbound setup cursor claude-code-gateway --api-key <key>
|
|
201
|
+
Login + set up multiple tools
|
|
202
|
+
|
|
203
|
+
Remove configuration:
|
|
204
|
+
$ unbound setup cursor --clear Remove Cursor config
|
|
205
|
+
$ unbound setup claude-code --clear Remove Claude Code config
|
|
206
|
+
|
|
207
|
+
Interactive:
|
|
208
|
+
$ unbound setup Select tools interactively
|
|
209
|
+
$ unbound setup --api-key <key> Login, then select interactively
|
|
210
|
+
|
|
211
|
+
If not logged in and --api-key is not provided, the browser will open
|
|
212
|
+
automatically to authenticate before proceeding.
|
|
214
213
|
`)
|
|
215
|
-
.action(
|
|
214
|
+
.action(async (tools, opts) => {
|
|
215
|
+
try {
|
|
216
|
+
await ensureLoggedIn({ apiKey: opts.apiKey });
|
|
217
|
+
const apiKey = config.getApiKey();
|
|
218
|
+
const frontendUrl = config.getFrontendUrl();
|
|
216
219
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.option('--subscription', 'Use your existing Claude subscription (hooks only)')
|
|
224
|
-
.option('--gateway', 'Use Unbound as the AI provider (gateway mode)')
|
|
225
|
-
.option('--clear', 'Remove Unbound configuration for Claude Code')
|
|
226
|
-
.addHelpText('after', `
|
|
227
|
-
Modes:
|
|
228
|
-
Subscription (hooks only):
|
|
229
|
-
Keep your existing Claude subscription. Installs Unbound hooks for
|
|
230
|
-
policy enforcement (security guardrails, cost limits) without changing
|
|
231
|
-
the AI provider. Runs claude-code/hooks/setup.py.
|
|
220
|
+
// No tools specified → interactive multi-select (existing flow)
|
|
221
|
+
if (tools.length === 0) {
|
|
222
|
+
const selected = await output.multiSelect(
|
|
223
|
+
'Select tools to set up with Unbound:',
|
|
224
|
+
SETUP_TOOLS
|
|
225
|
+
);
|
|
232
226
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
227
|
+
if (selected.length === 0) {
|
|
228
|
+
output.warn('No tools selected.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
237
231
|
|
|
238
|
-
|
|
239
|
-
|
|
232
|
+
const selectedTools = SETUP_TOOLS.filter(t => selected.includes(t.value));
|
|
233
|
+
console.log('');
|
|
240
234
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
const ok = await runBatch(selectedTools, (tool) =>
|
|
236
|
+
runScriptPiped(tool.script, `--api-key ${shellEscape(apiKey)}`)
|
|
237
|
+
);
|
|
238
|
+
if (!ok) return;
|
|
245
239
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
$ unbound setup claude-code --clear # Remove Unbound configuration
|
|
251
|
-
`)
|
|
252
|
-
.action(async (opts) => {
|
|
253
|
-
try {
|
|
254
|
-
await ensureLoggedIn();
|
|
255
|
-
const apiKey = config.getApiKey();
|
|
256
|
-
const scriptOpts = { clear: opts.clear };
|
|
240
|
+
console.log('');
|
|
241
|
+
output.success('All tools configured');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
257
244
|
|
|
245
|
+
// Validate --subscription and --gateway mutual exclusivity
|
|
258
246
|
if (opts.subscription && opts.gateway) {
|
|
259
247
|
output.error('Cannot use both --subscription and --gateway. Choose one.');
|
|
260
248
|
process.exitCode = 1;
|
|
261
249
|
return;
|
|
262
250
|
}
|
|
263
251
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const mode = await output.select('How do you want to use Claude Code with Unbound?', [
|
|
267
|
-
{ label: 'Use my Claude subscription (hooks)', value: 'subscription' },
|
|
268
|
-
{ label: 'Use Unbound as the AI provider (gateway)', value: 'gateway' },
|
|
269
|
-
]);
|
|
270
|
-
useSubscription = mode === 'subscription';
|
|
271
|
-
}
|
|
252
|
+
// Deduplicate tool names
|
|
253
|
+
tools = [...new Set(tools)];
|
|
272
254
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
255
|
+
// Validate all tool names
|
|
256
|
+
const allKnown = { ...SETUP_TOOL_MAP, ...MODE_TOOLS, ...INSTRUCTION_TOOLS };
|
|
257
|
+
const invalid = tools.filter(t => !allKnown[t]);
|
|
258
|
+
if (invalid.length > 0) {
|
|
259
|
+
output.error(`Unknown tool(s): ${invalid.join(', ')}`);
|
|
260
|
+
console.error(' Run "unbound setup --help" to see available tools.');
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
return;
|
|
280
263
|
}
|
|
281
|
-
} catch (err) {
|
|
282
|
-
output.error(err.message);
|
|
283
|
-
process.exitCode = 1;
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
264
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
'Set up Gemini CLI to use Unbound. Sets GEMINI_API_KEY and ' +
|
|
291
|
-
'GOOGLE_GEMINI_BASE_URL environment variables.'
|
|
292
|
-
)
|
|
293
|
-
.option('--clear', 'Remove Unbound configuration for Gemini CLI')
|
|
294
|
-
.addHelpText('after', `
|
|
295
|
-
What this does:
|
|
296
|
-
1. Sets GEMINI_API_KEY to your Unbound API key in your shell profile
|
|
297
|
-
2. Sets GOOGLE_GEMINI_BASE_URL to the Unbound gateway endpoint
|
|
298
|
-
3. Gemini CLI will route all requests through Unbound
|
|
299
|
-
|
|
300
|
-
Prerequisites:
|
|
301
|
-
- Must be logged in (will auto-open browser to authenticate if not)
|
|
302
|
-
- Python 3 and curl must be installed
|
|
303
|
-
- Gemini CLI must be installed
|
|
304
|
-
|
|
305
|
-
Examples:
|
|
306
|
-
$ unbound setup gemini-cli
|
|
307
|
-
$ unbound setup gemini-cli --clear # Remove Unbound configuration
|
|
308
|
-
`)
|
|
309
|
-
.action(makeAction('gemini-cli/gateway/setup.py'));
|
|
310
|
-
|
|
311
|
-
setup
|
|
312
|
-
.command('codex')
|
|
313
|
-
.description(
|
|
314
|
-
'Set up Codex to use Unbound. Prompts whether to use your existing ' +
|
|
315
|
-
'OpenAI subscription or use Unbound as the AI provider.'
|
|
316
|
-
)
|
|
317
|
-
.option('--subscription', 'Use your existing OpenAI subscription (hooks only)')
|
|
318
|
-
.option('--gateway', 'Use Unbound as the AI provider (gateway mode)')
|
|
319
|
-
.option('--clear', 'Remove Unbound configuration for Codex')
|
|
320
|
-
.addHelpText('after', `
|
|
321
|
-
Modes:
|
|
322
|
-
Subscription (hooks only):
|
|
323
|
-
Keep your existing OpenAI subscription. Installs Unbound hooks for
|
|
324
|
-
policy enforcement (security guardrails, cost limits) without changing
|
|
325
|
-
the AI provider. Runs codex/hooks/setup.py.
|
|
326
|
-
|
|
327
|
-
Gateway:
|
|
328
|
-
Use Unbound as the AI provider. Routes all Codex requests through
|
|
329
|
-
the Unbound gateway for full policy enforcement and model management.
|
|
330
|
-
Runs codex/gateway/setup.py.
|
|
331
|
-
|
|
332
|
-
If neither --subscription nor --gateway is provided, an interactive
|
|
333
|
-
prompt will ask you to choose.
|
|
334
|
-
|
|
335
|
-
Prerequisites:
|
|
336
|
-
- Must be logged in (will auto-open browser to authenticate if not)
|
|
337
|
-
- Python 3 and curl must be installed
|
|
338
|
-
- Codex must be installed
|
|
339
|
-
|
|
340
|
-
Examples:
|
|
341
|
-
$ unbound setup codex # Interactive mode selection
|
|
342
|
-
$ unbound setup codex --subscription # Hooks only (keep your subscription)
|
|
343
|
-
$ unbound setup codex --gateway # Use Unbound as AI provider
|
|
344
|
-
$ unbound setup codex --clear # Remove Unbound configuration
|
|
345
|
-
`)
|
|
346
|
-
.action(async (opts) => {
|
|
347
|
-
try {
|
|
348
|
-
await ensureLoggedIn();
|
|
349
|
-
const apiKey = config.getApiKey();
|
|
350
|
-
const scriptOpts = { clear: opts.clear };
|
|
351
|
-
|
|
352
|
-
if (opts.subscription && opts.gateway) {
|
|
353
|
-
output.error('Cannot use both --subscription and --gateway. Choose one.');
|
|
265
|
+
// Validate mutual exclusivity
|
|
266
|
+
if (tools.includes('claude-code-subscription') && tools.includes('claude-code-gateway')) {
|
|
267
|
+
output.error('Cannot set up both claude-code-subscription and claude-code-gateway. Choose one.');
|
|
354
268
|
process.exitCode = 1;
|
|
355
269
|
return;
|
|
356
270
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
{ label: 'Use my OpenAI subscription (hooks)', value: 'subscription' },
|
|
362
|
-
{ label: 'Use Unbound as the AI provider (gateway)', value: 'gateway' },
|
|
363
|
-
]);
|
|
364
|
-
useSubscription = mode === 'subscription';
|
|
271
|
+
if (tools.includes('codex-subscription') && tools.includes('codex-gateway')) {
|
|
272
|
+
output.error('Cannot set up both codex-subscription and codex-gateway. Choose one.');
|
|
273
|
+
process.exitCode = 1;
|
|
274
|
+
return;
|
|
365
275
|
}
|
|
366
276
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
277
|
+
// Validate no bare + suffixed name conflicts
|
|
278
|
+
if (tools.includes('claude-code') && (tools.includes('claude-code-subscription') || tools.includes('claude-code-gateway'))) {
|
|
279
|
+
output.error('Cannot combine claude-code with claude-code-subscription or claude-code-gateway.');
|
|
280
|
+
console.error(' Use --subscription or --gateway with claude-code, or use the explicit name directly.');
|
|
281
|
+
process.exitCode = 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (tools.includes('codex') && (tools.includes('codex-subscription') || tools.includes('codex-gateway'))) {
|
|
285
|
+
output.error('Cannot combine codex with codex-subscription or codex-gateway.');
|
|
286
|
+
console.error(' Use --subscription or --gateway with codex, or use the explicit name directly.');
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
return;
|
|
374
289
|
}
|
|
375
|
-
} catch (err) {
|
|
376
|
-
output.error(err.message);
|
|
377
|
-
process.exitCode = 1;
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// --- Instruction-only tools ---
|
|
382
|
-
|
|
383
|
-
setup
|
|
384
|
-
.command('roo-code')
|
|
385
|
-
.description('Show setup values for Roo Code (VS Code extension). Displays the API provider and API key to configure in Roo Code settings.')
|
|
386
|
-
.addHelpText('after', `
|
|
387
|
-
Output:
|
|
388
|
-
API Provider - Select "unbound" in Roo Code provider settings
|
|
389
|
-
API Key - Your Unbound API key to paste into the extension
|
|
390
|
-
|
|
391
|
-
To configure:
|
|
392
|
-
1. Open Command Palette (Ctrl/Cmd + Shift + P)
|
|
393
|
-
2. Type "Roo Code: Open Settings"
|
|
394
|
-
3. Select "unbound" as the API provider
|
|
395
|
-
4. Paste the API key shown below
|
|
396
|
-
5. Select your preferred model from the dropdown
|
|
397
|
-
|
|
398
|
-
Examples:
|
|
399
|
-
$ unbound setup roo-code
|
|
400
|
-
`)
|
|
401
|
-
.action(async () => {
|
|
402
|
-
try {
|
|
403
|
-
await ensureLoggedIn();
|
|
404
|
-
const apiKey = config.getApiKey();
|
|
405
|
-
|
|
406
|
-
output.keyValue([
|
|
407
|
-
['API Provider', 'unbound'],
|
|
408
|
-
['API Key', apiKey],
|
|
409
|
-
]);
|
|
410
|
-
} catch (err) {
|
|
411
|
-
output.error(err.message);
|
|
412
|
-
process.exitCode = 1;
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
setup
|
|
417
|
-
.command('cline')
|
|
418
|
-
.description('Show setup values for Cline (VS Code extension). Displays the API provider, Base URL, and API key to configure in Cline settings.')
|
|
419
|
-
.addHelpText('after', `
|
|
420
|
-
Output:
|
|
421
|
-
API Provider - Use "OpenAI Compatible" in Cline provider settings
|
|
422
|
-
Base URL - The Unbound gateway URL to paste as the Base URL
|
|
423
|
-
API Key - Your Unbound API key to paste into the extension
|
|
424
|
-
|
|
425
|
-
To configure:
|
|
426
|
-
1. Open Cline extension in VS Code
|
|
427
|
-
2. Select "Bring your own API key"
|
|
428
|
-
3. Set API Provider to "OpenAI Compatible"
|
|
429
|
-
4. Paste the Base URL and API Key shown below
|
|
430
|
-
5. Select a model (e.g., gpt-5.1)
|
|
431
|
-
|
|
432
|
-
Examples:
|
|
433
|
-
$ unbound setup cline
|
|
434
|
-
`)
|
|
435
|
-
.action(async () => {
|
|
436
|
-
try {
|
|
437
|
-
await ensureLoggedIn();
|
|
438
|
-
const apiKey = config.getApiKey();
|
|
439
|
-
const frontendUrl = config.getFrontendUrl();
|
|
440
|
-
|
|
441
|
-
output.keyValue([
|
|
442
|
-
['API Provider', 'OpenAI Compatible'],
|
|
443
|
-
['Base URL', frontendUrl],
|
|
444
|
-
['API Key', apiKey],
|
|
445
|
-
]);
|
|
446
|
-
} catch (err) {
|
|
447
|
-
output.error(err.message);
|
|
448
|
-
process.exitCode = 1;
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
setup
|
|
453
|
-
.command('kilo-code')
|
|
454
|
-
.description('Show setup values for Kilo Code (VS Code extension or CLI). Displays the API provider and API key to configure in Kilo Code.')
|
|
455
|
-
.addHelpText('after', `
|
|
456
|
-
Output:
|
|
457
|
-
API Provider - Select "Unbound" in Kilo Code provider settings
|
|
458
|
-
API Key - Your Unbound API key to paste into the extension or CLI
|
|
459
|
-
|
|
460
|
-
To configure (IDE extension):
|
|
461
|
-
1. Open Extensions (Ctrl/Cmd + Shift + X), install Kilo Code
|
|
462
|
-
2. Select "Use your own API key"
|
|
463
|
-
3. Set API Provider to "Unbound"
|
|
464
|
-
4. Paste the API Key shown below
|
|
465
|
-
5. Select a model (e.g., claude-opus-4-5, gpt-5.1)
|
|
466
|
-
|
|
467
|
-
To configure (CLI):
|
|
468
|
-
1. Install: npm install -g @kilocode/cli
|
|
469
|
-
2. Run: kilocode
|
|
470
|
-
3. Set API Provider to "Unbound"
|
|
471
|
-
4. Paste the API Key shown below
|
|
472
|
-
|
|
473
|
-
Examples:
|
|
474
|
-
$ unbound setup kilo-code
|
|
475
|
-
`)
|
|
476
|
-
.action(async () => {
|
|
477
|
-
try {
|
|
478
|
-
await ensureLoggedIn();
|
|
479
|
-
const apiKey = config.getApiKey();
|
|
480
|
-
|
|
481
|
-
output.keyValue([
|
|
482
|
-
['API Provider', 'Unbound'],
|
|
483
|
-
['API Key', apiKey],
|
|
484
|
-
]);
|
|
485
|
-
} catch (err) {
|
|
486
|
-
output.error(err.message);
|
|
487
|
-
process.exitCode = 1;
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
290
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
Output:
|
|
496
|
-
API Key - Your Unbound API key for authentication
|
|
497
|
-
Base URL - The Unbound gateway endpoint for API requests
|
|
291
|
+
// Validate --subscription/--gateway only with tools that need them
|
|
292
|
+
if ((opts.subscription || opts.gateway) && !tools.some(t => MODE_TOOLS[t])) {
|
|
293
|
+
output.warn('--subscription and --gateway only apply to claude-code and codex. Ignoring.');
|
|
294
|
+
}
|
|
498
295
|
|
|
499
|
-
|
|
296
|
+
// Single tool → live output (inherited stdio)
|
|
297
|
+
if (tools.length === 1) {
|
|
298
|
+
const toolName = tools[0];
|
|
299
|
+
|
|
300
|
+
if (SETUP_TOOL_MAP[toolName]) {
|
|
301
|
+
runSetupScript(SETUP_TOOL_MAP[toolName].script, apiKey, { clear: opts.clear });
|
|
302
|
+
} else if (MODE_TOOLS[toolName]) {
|
|
303
|
+
const mode = MODE_TOOLS[toolName];
|
|
304
|
+
if (opts.clear) {
|
|
305
|
+
// Clear both modes
|
|
306
|
+
runSetupScript(SETUP_TOOL_MAP[mode.subscription].script, apiKey, { clear: true });
|
|
307
|
+
runSetupScript(SETUP_TOOL_MAP[mode.gateway].script, apiKey, { clear: true });
|
|
308
|
+
} else {
|
|
309
|
+
let useSubscription = opts.subscription;
|
|
310
|
+
if (!opts.subscription && !opts.gateway) {
|
|
311
|
+
const choice = await output.select(mode.prompt, mode.options);
|
|
312
|
+
useSubscription = choice === 'subscription';
|
|
313
|
+
}
|
|
314
|
+
const resolved = useSubscription ? mode.subscription : mode.gateway;
|
|
315
|
+
runSetupScript(SETUP_TOOL_MAP[resolved].script, apiKey, {});
|
|
316
|
+
}
|
|
317
|
+
} else if (INSTRUCTION_TOOLS[toolName]) {
|
|
318
|
+
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, frontendUrl));
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
500
322
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
323
|
+
// Multi-tool → resolve all tools, then batch with spinners
|
|
324
|
+
const resolvedScripts = []; // { label, script } for automated tools
|
|
325
|
+
const instructionResults = []; // toolName for instruction-only
|
|
326
|
+
|
|
327
|
+
for (const toolName of tools) {
|
|
328
|
+
if (SETUP_TOOL_MAP[toolName]) {
|
|
329
|
+
resolvedScripts.push({ ...SETUP_TOOL_MAP[toolName], name: toolName });
|
|
330
|
+
} else if (MODE_TOOLS[toolName]) {
|
|
331
|
+
const mode = MODE_TOOLS[toolName];
|
|
332
|
+
if (opts.clear) {
|
|
333
|
+
// Expand to both scripts for clearing
|
|
334
|
+
resolvedScripts.push({ ...SETUP_TOOL_MAP[mode.subscription], name: mode.subscription });
|
|
335
|
+
resolvedScripts.push({ ...SETUP_TOOL_MAP[mode.gateway], name: mode.gateway });
|
|
336
|
+
} else {
|
|
337
|
+
let useSubscription = opts.subscription;
|
|
338
|
+
if (!opts.subscription && !opts.gateway) {
|
|
339
|
+
const choice = await output.select(mode.prompt, mode.options);
|
|
340
|
+
useSubscription = choice === 'subscription';
|
|
341
|
+
}
|
|
342
|
+
const resolved = useSubscription ? mode.subscription : mode.gateway;
|
|
343
|
+
resolvedScripts.push({ ...SETUP_TOOL_MAP[resolved], name: resolved });
|
|
344
|
+
}
|
|
345
|
+
} else if (INSTRUCTION_TOOLS[toolName]) {
|
|
346
|
+
instructionResults.push(toolName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
505
349
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
350
|
+
// Run automated tools with spinners
|
|
351
|
+
if (resolvedScripts.length > 0) {
|
|
352
|
+
console.log('');
|
|
353
|
+
const args = `--api-key ${shellEscape(apiKey)}${opts.clear ? ' --clear' : ''}`;
|
|
354
|
+
const ok = await runBatch(resolvedScripts, (tool) =>
|
|
355
|
+
runScriptPiped(tool.script, args)
|
|
356
|
+
, { clear: opts.clear });
|
|
357
|
+
if (!ok) return;
|
|
358
|
+
}
|
|
510
359
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const apiKey = config.getApiKey();
|
|
518
|
-
const frontendUrl = config.getFrontendUrl();
|
|
360
|
+
// Display instruction-only tool values
|
|
361
|
+
for (const toolName of instructionResults) {
|
|
362
|
+
console.log('');
|
|
363
|
+
output.info(`${INSTRUCTION_TOOLS[toolName].label}:`);
|
|
364
|
+
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, frontendUrl));
|
|
365
|
+
}
|
|
519
366
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
367
|
+
if (resolvedScripts.length > 0) {
|
|
368
|
+
console.log('');
|
|
369
|
+
output.success(opts.clear ? 'All tools cleared' : 'All tools configured');
|
|
370
|
+
}
|
|
524
371
|
} catch (err) {
|
|
525
|
-
|
|
372
|
+
if (err.message === 'Selection cancelled') return;
|
|
373
|
+
if (!err.displayed) output.error(err.message);
|
|
526
374
|
process.exitCode = 1;
|
|
527
375
|
}
|
|
528
376
|
});
|
|
@@ -607,8 +455,8 @@ Examples:
|
|
|
607
455
|
console.log('');
|
|
608
456
|
|
|
609
457
|
const mdmArgs = (tool) => {
|
|
610
|
-
let args = `--api_key
|
|
611
|
-
if (opts.url) args += ` --url
|
|
458
|
+
let args = `--api_key ${shellEscape(opts.adminApiKey)}`;
|
|
459
|
+
if (opts.url) args += ` --url ${shellEscape(opts.url)}`;
|
|
612
460
|
if (opts.clear) args += ' --clear';
|
|
613
461
|
return args;
|
|
614
462
|
};
|
package/src/index.js
CHANGED
|
@@ -34,6 +34,12 @@ TOOL SETUP
|
|
|
34
34
|
$ unbound setup codex --gateway Use Unbound as AI provider
|
|
35
35
|
$ unbound setup codex --subscription Hooks only (keep your subscription)
|
|
36
36
|
|
|
37
|
+
One-step login and setup (--api-key):
|
|
38
|
+
$ unbound setup cursor --api-key <key> Login and set up Cursor
|
|
39
|
+
$ unbound setup cursor claude-code-gateway --api-key <key>
|
|
40
|
+
Login and set up multiple tools
|
|
41
|
+
$ unbound setup --api-key <key> Login, then select interactively
|
|
42
|
+
|
|
37
43
|
Instruction-only (shows config values to set manually):
|
|
38
44
|
$ unbound setup roo-code Show Roo Code config values
|
|
39
45
|
$ unbound setup cline Show Cline config values
|