unbound-cli 1.17.0 → 1.18.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/package.json +1 -1
- package/src/commands/setup.js +1 -0
- package/src/config.js +5 -1
- package/src/toolHealth.js +13 -5
- package/test/device-token.test.js +46 -0
- package/test/tool-health.test.js +31 -0
package/package.json
CHANGED
package/src/commands/setup.js
CHANGED
|
@@ -377,6 +377,7 @@ async function runSetupScriptReported(script, stepName, apiKey, opts = {}) {
|
|
|
377
377
|
// — it's a generic user-owned var that can have non-Unbound uses.
|
|
378
378
|
const NUKE_ENV_VARS = [
|
|
379
379
|
'UNBOUND_API_KEY',
|
|
380
|
+
'UNBOUND_DEVICE_TOKEN',
|
|
380
381
|
'UNBOUND_CLAUDE_API_KEY',
|
|
381
382
|
'UNBOUND_CODEX_API_KEY',
|
|
382
383
|
'UNBOUND_COPILOT_API_KEY',
|
package/src/config.js
CHANGED
|
@@ -77,11 +77,15 @@ function clearApiKey() {
|
|
|
77
77
|
|
|
78
78
|
// The send-only device key, where setup scripts and hooks read it.
|
|
79
79
|
function getDeviceKey() {
|
|
80
|
-
|
|
80
|
+
const config = readConfig();
|
|
81
|
+
// api_key first: every writer still sets it, and older writers set only it.
|
|
82
|
+
return config.api_key || config.device_token || null;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
function setDeviceKey(apiKey) {
|
|
84
86
|
const config = readConfig();
|
|
87
|
+
config.device_token = apiKey;
|
|
88
|
+
// Older CLIs and hooks read only `api_key`; stop writing it once they are gone.
|
|
85
89
|
config.api_key = apiKey;
|
|
86
90
|
writeConfig(config);
|
|
87
91
|
}
|
package/src/toolHealth.js
CHANGED
|
@@ -206,6 +206,14 @@ function envKeyCheck(label, name, kind = 'aux') {
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// Setup writes UNBOUND_DEVICE_TOKEN; installs from before it carry only the per-tool name.
|
|
210
|
+
function deviceTokenCheck(legacyName) {
|
|
211
|
+
const check = envKeyCheck('API key env', 'UNBOUND_DEVICE_TOKEN');
|
|
212
|
+
if (check.ok) return check;
|
|
213
|
+
const legacy = envKeyCheck('API key env', legacyName);
|
|
214
|
+
return legacy.ok ? legacy : check;
|
|
215
|
+
}
|
|
216
|
+
|
|
209
217
|
function envCheck(label, name, expected, kind = 'aux') {
|
|
210
218
|
const base = label.replace(/ env$/, '');
|
|
211
219
|
const r = readEnvVar(name);
|
|
@@ -334,7 +342,7 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
334
342
|
checks: () => [
|
|
335
343
|
configCheck('Config', '~/.cursor/hooks.json', { json: true, test: refsUnbound }),
|
|
336
344
|
scriptCheck('Hook script', '~/.cursor/hooks/unbound.py'),
|
|
337
|
-
|
|
345
|
+
deviceTokenCheck('UNBOUND_CURSOR_API_KEY'),
|
|
338
346
|
],
|
|
339
347
|
},
|
|
340
348
|
{
|
|
@@ -342,7 +350,7 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
342
350
|
checks: () => [
|
|
343
351
|
configCheck('Config', claudePath('settings.json'), { json: true, test: refsUnbound }),
|
|
344
352
|
scriptCheck('Hook script', claudePath('hooks', 'unbound.py')),
|
|
345
|
-
|
|
353
|
+
deviceTokenCheck('UNBOUND_CLAUDE_API_KEY'),
|
|
346
354
|
],
|
|
347
355
|
},
|
|
348
356
|
{
|
|
@@ -373,7 +381,7 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
373
381
|
return features.hooks === undefined ? features.codex_hooks === true : features.hooks === true;
|
|
374
382
|
} }, { short: 'codex hooks not enabled', kind: 'aux' }), // aux: an org install leaves it set with no user hook
|
|
375
383
|
scriptCheck('Hook script', '~/.codex/hooks/unbound.py'),
|
|
376
|
-
|
|
384
|
+
deviceTokenCheck('UNBOUND_CODEX_API_KEY'),
|
|
377
385
|
],
|
|
378
386
|
},
|
|
379
387
|
{
|
|
@@ -397,7 +405,7 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
397
405
|
const checks = [configCheck('Config', '~/.copilot/hooks/unbound.json', { json: true, test: refsUnbound })];
|
|
398
406
|
if (hasBinary) checks.push(scriptCheck('Hook binary', binaryPath || BINARY_PATH));
|
|
399
407
|
else checks.push(scriptCheck('Hook script', '~/.copilot/hooks/unbound.py'));
|
|
400
|
-
checks.push(
|
|
408
|
+
checks.push(deviceTokenCheck('UNBOUND_COPILOT_API_KEY'));
|
|
401
409
|
return checks;
|
|
402
410
|
},
|
|
403
411
|
},
|
|
@@ -414,7 +422,7 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
414
422
|
const checks = [configCheck('Config', '~/.augment/settings.json', { json: false, test: (t) => t.includes('unbound.py') || t.includes(BINARY_MARKER) })];
|
|
415
423
|
if (hasBinary) checks.push(scriptCheck('Hook binary', binaryPath || BINARY_PATH));
|
|
416
424
|
else checks.push(scriptCheck('Hook script', '~/.augment/hooks/unbound.py'));
|
|
417
|
-
checks.push(
|
|
425
|
+
checks.push(deviceTokenCheck('UNBOUND_AUGMENT_API_KEY'));
|
|
418
426
|
return checks;
|
|
419
427
|
},
|
|
420
428
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function withConfig(contents, fn) {
|
|
8
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'unbound-token-'));
|
|
9
|
+
const origHome = process.env.HOME;
|
|
10
|
+
process.env.HOME = tmp;
|
|
11
|
+
const file = path.join(tmp, '.unbound', 'config.json');
|
|
12
|
+
if (contents) {
|
|
13
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
14
|
+
fs.writeFileSync(file, JSON.stringify(contents));
|
|
15
|
+
}
|
|
16
|
+
delete require.cache[require.resolve('../src/config')];
|
|
17
|
+
try {
|
|
18
|
+
return fn(require('../src/config'), file);
|
|
19
|
+
} finally {
|
|
20
|
+
process.env.HOME = origHome;
|
|
21
|
+
delete require.cache[require.resolve('../src/config')];
|
|
22
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('getDeviceKey prefers api_key, which an older writer may have updated alone', () => {
|
|
27
|
+
withConfig({ device_token: 'stale', api_key: 'relogged' }, (config) => {
|
|
28
|
+
assert.equal(config.getDeviceKey(), 'relogged');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('getDeviceKey falls back to device_token', () => {
|
|
33
|
+
withConfig({ device_token: 'new' }, (config) => {
|
|
34
|
+
assert.equal(config.getDeviceKey(), 'new');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('setDeviceKey writes both fields so older CLIs and hooks still read it', () => {
|
|
39
|
+
withConfig({ api_key: 'old', base_url: 'https://api.acme.com' }, (config, file) => {
|
|
40
|
+
config.setDeviceKey('k2');
|
|
41
|
+
const written = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
42
|
+
assert.equal(written.device_token, 'k2');
|
|
43
|
+
assert.equal(written.api_key, 'k2');
|
|
44
|
+
assert.equal(written.base_url, 'https://api.acme.com');
|
|
45
|
+
});
|
|
46
|
+
});
|
package/test/tool-health.test.js
CHANGED
|
@@ -1085,3 +1085,34 @@ test('isUnboundKeyHelper: a helper under a relocated CLAUDE_CONFIG_DIR is still
|
|
|
1085
1085
|
delete require.cache[require.resolve('../src/toolHealth')];
|
|
1086
1086
|
}
|
|
1087
1087
|
});
|
|
1088
|
+
|
|
1089
|
+
// The per-tool env check accepts the shared UNBOUND_DEVICE_TOKEN or the per-tool name from older installs.
|
|
1090
|
+
function claudeWithEnv(env) {
|
|
1091
|
+
return withHome((tmp, th) => {
|
|
1092
|
+
writeFile(path.join(tmp, '.claude', 'settings.json'), JSON.stringify({ hooks: { x: [{ command: path.join(tmp, '.claude', 'hooks', 'unbound.py') }] } }));
|
|
1093
|
+
writeFile(path.join(tmp, '.claude', 'hooks', 'unbound.py'), '#');
|
|
1094
|
+
const names = ['UNBOUND_DEVICE_TOKEN', 'UNBOUND_CLAUDE_API_KEY'];
|
|
1095
|
+
const saved = names.map((n) => process.env[n]);
|
|
1096
|
+
names.forEach((n) => delete process.env[n]);
|
|
1097
|
+
Object.assign(process.env, env);
|
|
1098
|
+
try {
|
|
1099
|
+
return th.detectTools({ apiKey: 'k' }).find((x) => x.family === 'claude-code');
|
|
1100
|
+
} finally {
|
|
1101
|
+
names.forEach((n, i) => { if (saved[i] === undefined) delete process.env[n]; else process.env[n] = saved[i]; });
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
test('claude-code: UNBOUND_DEVICE_TOKEN alone → healthy', () => {
|
|
1107
|
+
assert.equal(claudeWithEnv({ UNBOUND_DEVICE_TOKEN: 'k' }).status, 'healthy');
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
test('claude-code: only the per-tool UNBOUND_CLAUDE_API_KEY → healthy', () => {
|
|
1111
|
+
assert.equal(claudeWithEnv({ UNBOUND_CLAUDE_API_KEY: 'k' }).status, 'healthy');
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
test('claude-code: neither env set → tampered, naming UNBOUND_DEVICE_TOKEN', () => {
|
|
1115
|
+
const t = claudeWithEnv({});
|
|
1116
|
+
assert.equal(t.status, 'tampered');
|
|
1117
|
+
assert.ok(t.checks.some((c) => !c.ok && /UNBOUND_DEVICE_TOKEN is not set/.test(c.detail)));
|
|
1118
|
+
});
|