unbound-cli 1.16.7 → 1.16.9
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
package/src/commands/discover.js
CHANGED
|
@@ -200,7 +200,7 @@ function register(program) {
|
|
|
200
200
|
.addOption(new Option('--discovery-key <key>', 'Deprecated and ignored. The scan uses your API key.').hideHelp())
|
|
201
201
|
.addHelpText('after', `
|
|
202
202
|
Scans this device for installed AI coding tools (Cursor, Claude Code,
|
|
203
|
-
Gemini CLI, Codex,
|
|
203
|
+
Gemini CLI, Codex, Devin Desktop, Roo Code, Cline, GitHub Copilot, JetBrains,
|
|
204
204
|
and more) and reports findings to the Unbound backend.
|
|
205
205
|
|
|
206
206
|
Which --api-key to use depends on scope:
|
package/src/commands/setup.js
CHANGED
|
@@ -86,12 +86,19 @@ function resolveSetupAllTools(clear) {
|
|
|
86
86
|
return clear ? Object.keys(SETUP_TOOL_MAP) : [...SETUP_ALL_TOOLS];
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// OpenAI-compatible Base URL: the gateway host with the /v1 path these clients
|
|
90
|
+
// POST to. Strips a trailing slash because getGatewayUrl() returns env/config
|
|
91
|
+
// values unnormalized, which would otherwise double the slash.
|
|
92
|
+
function openAiBaseUrl(gatewayUrl) {
|
|
93
|
+
return `${String(gatewayUrl).replace(/\/+$/, '')}/v1`;
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
// Instruction-only tools (display config values, no setup scripts)
|
|
90
97
|
const INSTRUCTION_TOOLS = {
|
|
91
98
|
'roo-code': { label: 'Roo Code', values: (apiKey) => [['API Provider', 'unbound'], ['API Key', apiKey]] },
|
|
92
|
-
'cline': { label: 'Cline', values: (apiKey,
|
|
99
|
+
'cline': { label: 'Cline', values: (apiKey, gatewayUrl) => [['API Provider', 'OpenAI Compatible'], ['Base URL', openAiBaseUrl(gatewayUrl)], ['API Key', apiKey]] },
|
|
93
100
|
'kilo-code': { label: 'Kilo Code', values: (apiKey) => [['API Provider', 'Unbound'], ['API Key', apiKey]] },
|
|
94
|
-
'custom-access': { label: 'Custom Access', values: (apiKey,
|
|
101
|
+
'custom-access': { label: 'Custom Access', values: (apiKey, gatewayUrl) => [['API Key', apiKey], ['Base URL', openAiBaseUrl(gatewayUrl)]] },
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
// Tools that need interactive mode selection (claude-code, codex without suffix)
|
|
@@ -1054,7 +1061,7 @@ must update the MDM configuration.
|
|
|
1054
1061
|
if (r && r.mdmSkipped) reportMdmSkips([label]);
|
|
1055
1062
|
}
|
|
1056
1063
|
} else if (INSTRUCTION_TOOLS[toolName]) {
|
|
1057
|
-
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey,
|
|
1064
|
+
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, gatewayUrl));
|
|
1058
1065
|
}
|
|
1059
1066
|
return;
|
|
1060
1067
|
}
|
|
@@ -1110,7 +1117,7 @@ must update the MDM configuration.
|
|
|
1110
1117
|
for (const toolName of instructionResults) {
|
|
1111
1118
|
console.log('');
|
|
1112
1119
|
output.info(`${INSTRUCTION_TOOLS[toolName].label}:`);
|
|
1113
|
-
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey,
|
|
1120
|
+
output.keyValue(INSTRUCTION_TOOLS[toolName].values(apiKey, gatewayUrl));
|
|
1114
1121
|
}
|
|
1115
1122
|
|
|
1116
1123
|
} catch (err) {
|
|
@@ -1290,6 +1297,7 @@ async function runMdmSetupAllBundle(adminApiKey, { backendUrl, frontendUrl, gate
|
|
|
1290
1297
|
module.exports = {
|
|
1291
1298
|
register,
|
|
1292
1299
|
elevated,
|
|
1300
|
+
INSTRUCTION_TOOLS,
|
|
1293
1301
|
scopeNotice,
|
|
1294
1302
|
runSetupAllBundle,
|
|
1295
1303
|
runMdmSetupAllBundle,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
|
|
5
|
+
const { INSTRUCTION_TOOLS } = require('../src/commands/setup');
|
|
6
|
+
|
|
7
|
+
// Regression: `unbound setup cline` / `custom-access` printed the dashboard/
|
|
8
|
+
// frontend host as the Base URL. OpenAI-compatible clients POST to
|
|
9
|
+
// `<Base URL>/v1/chat/completions`, so the Base URL must be the GATEWAY host
|
|
10
|
+
// (with /v1), never the frontend host.
|
|
11
|
+
const GATEWAY = 'https://api.getunbound.ai';
|
|
12
|
+
const FRONTEND = 'https://gateway.getunbound.ai';
|
|
13
|
+
const API_KEY = 'ub_test_key';
|
|
14
|
+
|
|
15
|
+
function rows(tool, gatewayUrl = GATEWAY) {
|
|
16
|
+
return new Map(INSTRUCTION_TOOLS[tool].values(API_KEY, gatewayUrl));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test('cline: Base URL is the gateway /v1 endpoint, provider is OpenAI Compatible', () => {
|
|
20
|
+
const r = rows('cline');
|
|
21
|
+
assert.equal(r.get('Base URL'), `${GATEWAY}/v1`);
|
|
22
|
+
assert.equal(r.get('API Provider'), 'OpenAI Compatible');
|
|
23
|
+
assert.equal(r.get('API Key'), API_KEY);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('custom-access: Base URL is the gateway /v1 endpoint', () => {
|
|
27
|
+
assert.equal(rows('custom-access').get('Base URL'), `${GATEWAY}/v1`);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('a trailing slash on the gateway URL does not produce a double slash', () => {
|
|
31
|
+
// getGatewayUrl() returns env/config values unnormalized, so a trailing slash
|
|
32
|
+
// is reachable in practice.
|
|
33
|
+
for (const tool of ['cline', 'custom-access']) {
|
|
34
|
+
assert.equal(rows(tool, 'https://api.getunbound.ai/').get('Base URL'), `${GATEWAY}/v1`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('cline / custom-access never print the frontend host as Base URL', () => {
|
|
39
|
+
for (const tool of ['cline', 'custom-access']) {
|
|
40
|
+
for (const [, value] of INSTRUCTION_TOOLS[tool].values(API_KEY, FRONTEND)) {
|
|
41
|
+
assert.notEqual(value, FRONTEND, `${tool} leaked the frontend host as a value`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('roo-code / kilo-code print no Base URL row', () => {
|
|
47
|
+
for (const tool of ['roo-code', 'kilo-code']) {
|
|
48
|
+
const keys = INSTRUCTION_TOOLS[tool].values(API_KEY, GATEWAY).map(([k]) => k);
|
|
49
|
+
assert.ok(!keys.includes('Base URL'), `${tool} should not print a Base URL`);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ---- Call-site wiring: drive the real `setup` command and confirm it feeds
|
|
54
|
+
// the GATEWAY url (not the frontend url) into the instruction-tool callbacks.
|
|
55
|
+
// The two paths — single-tool and multi-tool — print from separate call sites,
|
|
56
|
+
// so both are exercised. gateway/frontend are deliberately distinct so a
|
|
57
|
+
// regression that reintroduces frontendUrl surfaces as a frontend-host Base URL.
|
|
58
|
+
async function driveSetup(tools) {
|
|
59
|
+
for (const m of ['../src/commands/setup', '../src/auth', '../src/config', '../src/output']) {
|
|
60
|
+
delete require.cache[require.resolve(m)];
|
|
61
|
+
}
|
|
62
|
+
const setup = require('../src/commands/setup');
|
|
63
|
+
const auth = require('../src/auth');
|
|
64
|
+
const config = require('../src/config');
|
|
65
|
+
const output = require('../src/output');
|
|
66
|
+
|
|
67
|
+
const printed = [];
|
|
68
|
+
// hasRootPrivileges() reads process.getuid; stub the uid (not the exported
|
|
69
|
+
// function, which setup.js calls locally) so scope is deterministically
|
|
70
|
+
// non-root and the run reaches the instruction-tool call sites on any host.
|
|
71
|
+
const origGetuid = process.getuid;
|
|
72
|
+
process.getuid = () => 1000;
|
|
73
|
+
auth.ensureLoggedIn = async () => {};
|
|
74
|
+
config.setUrls = () => ({});
|
|
75
|
+
config.getBaseUrl = () => 'https://backend.acme';
|
|
76
|
+
config.getFrontendUrl = () => 'https://frontend.acme';
|
|
77
|
+
config.getGatewayUrl = () => 'https://gateway.acme';
|
|
78
|
+
config.getApiKey = () => 'resolved-key';
|
|
79
|
+
config.isLoggedIn = () => true;
|
|
80
|
+
output.keyValue = (kv) => printed.push(...kv);
|
|
81
|
+
for (const k of ['info', 'success', 'error', 'warn']) output[k] = () => {};
|
|
82
|
+
|
|
83
|
+
const program = new Command();
|
|
84
|
+
program.exitOverride();
|
|
85
|
+
setup.register(program);
|
|
86
|
+
const prevExit = process.exitCode;
|
|
87
|
+
process.exitCode = 0;
|
|
88
|
+
try {
|
|
89
|
+
await program.parseAsync(['node', 'unbound', 'setup', ...tools]);
|
|
90
|
+
} finally {
|
|
91
|
+
process.exitCode = prevExit;
|
|
92
|
+
if (origGetuid) process.getuid = origGetuid; else delete process.getuid;
|
|
93
|
+
}
|
|
94
|
+
return new Map(printed);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test('setup cline (single-tool path) prints the gateway /v1 Base URL, not the frontend', async () => {
|
|
98
|
+
const r = await driveSetup(['cline']);
|
|
99
|
+
assert.equal(r.get('Base URL'), 'https://gateway.acme/v1');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('setup cline custom-access (multi-tool path) prints the gateway /v1 Base URL, not the frontend', async () => {
|
|
103
|
+
const r = await driveSetup(['cline', 'custom-access']);
|
|
104
|
+
assert.equal(r.get('Base URL'), 'https://gateway.acme/v1');
|
|
105
|
+
});
|