unbound-cli 1.7.0 → 1.8.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/LOCAL_DEV.md +2 -3
- package/README.md +6 -4
- package/package.json +1 -1
- package/src/commands/discover.js +3 -1
- package/src/commands/onboard.js +40 -17
- package/src/commands/setup.js +11 -5
- package/src/commands/tools.js +1 -0
- package/src/index.js +4 -2
- package/src/toolHealth.js +24 -4
- package/test/onboard-cron.test.js +25 -2
- package/test/onboard-scope.test.js +41 -6
- package/test/setup-args.test.js +3 -3
- package/test/tool-health.test.js +66 -2
package/LOCAL_DEV.md
CHANGED
|
@@ -83,7 +83,7 @@ node src/index.js setup --backend-url http://localhost:8000 --frontend-url http:
|
|
|
83
83
|
sudo node src/index.js setup --api-key <ADMIN_KEY> --all --backend-url http://localhost:8000 --frontend-url http://localhost:3000
|
|
84
84
|
|
|
85
85
|
# Onboarding (combined setup + discover)
|
|
86
|
-
node src/index.js onboard --api-key <USER_KEY>
|
|
86
|
+
node src/index.js onboard --api-key <USER_KEY> \
|
|
87
87
|
--backend-url http://localhost:8000 --frontend-url http://localhost:3000
|
|
88
88
|
sudo node src/index.js onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY> \
|
|
89
89
|
--backend-url http://localhost:8000 --frontend-url http://localhost:3000
|
|
@@ -183,8 +183,7 @@ node src/index.js setup --all
|
|
|
183
183
|
node src/index.js setup --all --api-key <key>
|
|
184
184
|
|
|
185
185
|
# Onboarding (one command: login + setup --all + discover)
|
|
186
|
-
node src/index.js onboard --api-key <USER_KEY>
|
|
187
|
-
sudo node src/index.js onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
|
|
186
|
+
node src/index.js onboard --api-key <USER_KEY>
|
|
188
187
|
|
|
189
188
|
# MDM onboarding (admin device enrollment, requires root — MDM scope auto-detected from sudo)
|
|
190
189
|
sudo node src/index.js onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ For new users, the fastest path from install to a fully-configured device is the
|
|
|
37
37
|
```bash
|
|
38
38
|
# End user
|
|
39
39
|
npm install -g unbound-cli
|
|
40
|
-
unbound onboard --api-key <USER_KEY>
|
|
40
|
+
unbound onboard --api-key <USER_KEY>
|
|
41
41
|
|
|
42
42
|
# Admin enrolling a device via MDM (requires root)
|
|
43
43
|
sudo unbound onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
@@ -65,7 +65,7 @@ Interactive batch setup:
|
|
|
65
65
|
| Command | Description |
|
|
66
66
|
|---------|-------------|
|
|
67
67
|
| `unbound setup` | Select and install multiple tools interactively |
|
|
68
|
-
| `unbound setup --all` | Install the default bundle: Cursor + Claude Code hooks + Codex hooks |
|
|
68
|
+
| `unbound setup --all` | Install the default bundle: Cursor + Copilot + Augment + Claude Code hooks + Codex hooks |
|
|
69
69
|
|
|
70
70
|
Automated setup (downloads scripts, sets env vars, configures tool):
|
|
71
71
|
|
|
@@ -73,6 +73,7 @@ Automated setup (downloads scripts, sets env vars, configures tool):
|
|
|
73
73
|
|---------|-------------|
|
|
74
74
|
| `unbound setup cursor` | Download hooks, set env, restart Cursor |
|
|
75
75
|
| `unbound setup copilot` | Download hooks, set env, configure GitHub Copilot |
|
|
76
|
+
| `unbound setup augment` | Download hooks, set env, configure Augment Code |
|
|
76
77
|
| `unbound setup claude-code` | Interactive mode selection (subscription or gateway) |
|
|
77
78
|
| `unbound setup claude-code --subscription` | Hooks only (keep your Claude subscription) |
|
|
78
79
|
| `unbound setup claude-code --gateway` | Use Unbound as the AI provider |
|
|
@@ -96,6 +97,7 @@ Remove configuration:
|
|
|
96
97
|
|---------|-------------|
|
|
97
98
|
| `unbound setup cursor --clear` | Remove Unbound config for Cursor |
|
|
98
99
|
| `unbound setup copilot --clear` | Remove Unbound config for GitHub Copilot |
|
|
100
|
+
| `unbound setup augment --clear` | Remove Unbound config for Augment Code |
|
|
99
101
|
| `unbound setup claude-code --clear` | Remove Unbound config for Claude Code |
|
|
100
102
|
| `unbound setup gemini-cli --clear` | Remove Unbound config for Gemini CLI |
|
|
101
103
|
| `unbound setup codex --clear` | Remove Unbound config for Codex |
|
|
@@ -110,7 +112,7 @@ Configure all users on a device via MDM. Requires root.
|
|
|
110
112
|
| `sudo unbound setup --api-key KEY cursor codex-subscription` | Set up specific tools |
|
|
111
113
|
| `sudo unbound setup --clear cursor` | Remove config for specific tools |
|
|
112
114
|
|
|
113
|
-
Available tools: `cursor`, `copilot`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex-subscription`, `codex-gateway`
|
|
115
|
+
Available tools: `cursor`, `copilot`, `augment`, `claude-code-subscription`, `claude-code-gateway`, `gemini-cli`, `codex-subscription`, `codex-gateway`
|
|
114
116
|
|
|
115
117
|
`claude-code-subscription` and `claude-code-gateway` are mutually exclusive. `codex-subscription` and `codex-gateway` are mutually exclusive. When using `--all`, subscription mode is used by default for Claude Code and Codex.
|
|
116
118
|
|
|
@@ -252,7 +254,7 @@ Alias: `unbound groups` works the same as `unbound user-groups`.
|
|
|
252
254
|
| `unbound tools connect <type>` | Connect a tool |
|
|
253
255
|
| `unbound tools approved` | List approved tool types |
|
|
254
256
|
|
|
255
|
-
Supported tool types: `CLAUDE_CODE`, `UNBOUND_CLAUDE_CODE`, `CURSOR`, `COPILOT`, `ROO_CODE`, `CLINE`, `GEMINI_CLI`, `CODEX`, `UNBOUND_CODEX`, `KILO_CODE`, `CUSTOM_ACCESS`
|
|
257
|
+
Supported tool types: `CLAUDE_CODE`, `UNBOUND_CLAUDE_CODE`, `CURSOR`, `COPILOT`, `AUGMENT_CODE`, `ROO_CODE`, `CLINE`, `GEMINI_CLI`, `CODEX`, `UNBOUND_CODEX`, `KILO_CODE`, `CUSTOM_ACCESS`
|
|
256
258
|
|
|
257
259
|
## Configuration
|
|
258
260
|
|
package/package.json
CHANGED
package/src/commands/discover.js
CHANGED
|
@@ -199,7 +199,9 @@ Scans this device for installed AI coding tools (Cursor, Claude Code,
|
|
|
199
199
|
Gemini CLI, Codex, Windsurf, Roo Code, Cline, GitHub Copilot, JetBrains,
|
|
200
200
|
and more) and reports findings to the Unbound backend.
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
Which --api-key to use depends on scope:
|
|
203
|
+
- Single-user scan (no sudo): use your own user API key.
|
|
204
|
+
- All-users scan (sudo): use the org discovery key.
|
|
203
205
|
The --domain defaults to the backend URL configured via "unbound config set-backend-url"
|
|
204
206
|
(falls back to https://backend.getunbound.ai when unset).
|
|
205
207
|
|
package/src/commands/onboard.js
CHANGED
|
@@ -27,7 +27,7 @@ function register(program) {
|
|
|
27
27
|
)
|
|
28
28
|
.option('--api-key <key>', 'API key (user key, or admin key when run with sudo). Falls back to UNBOUND_API_KEY or a stored `unbound login` key)')
|
|
29
29
|
.addOption(new Option('--admin-api-key <key>', 'Alias for --api-key (back-compat)').hideHelp())
|
|
30
|
-
.option('--discovery-key <key>', '
|
|
30
|
+
.option('--discovery-key <key>', 'Org discovery key for the device scan — required only under sudo (MDM/org scope); ignored for per-user onboarding (or set UNBOUND_DISCOVERY_KEY)')
|
|
31
31
|
.option('--domain <url>', 'Backend URL for discovery (defaults to configured backend)')
|
|
32
32
|
.option('--set-cron', 'Set up a daily background job to keep governance up to date (under sudo, schedules a discovery scan only — not a full tool re-install)')
|
|
33
33
|
.option('--backfill', 'Seed historical Claude Code / Codex / Copilot sessions from local transcripts into Unbound analytics (Cursor skipped automatically)')
|
|
@@ -37,24 +37,27 @@ function register(program) {
|
|
|
37
37
|
.addHelpText('after', `
|
|
38
38
|
Runs the full onboarding flow, auto-detecting scope from your privileges:
|
|
39
39
|
With sudo, installs the MDM tool bundle (${MDM_ALL_TOOLS.join(', ')}) and
|
|
40
|
-
scans all users on the device. Without sudo, installs
|
|
41
|
-
(${ALL_TOOLS.join(', ')}) and scans the current user
|
|
40
|
+
scans all users on the device using --discovery-key. Without sudo, installs
|
|
41
|
+
the user bundle (${ALL_TOOLS.join(', ')}) and scans the current user using
|
|
42
|
+
your own API key.
|
|
42
43
|
|
|
43
44
|
1. Logs in / resolves the API key (or reuses a stored \`unbound login\` key
|
|
44
45
|
when --api-key is omitted).
|
|
45
46
|
2. Installs the tool bundle for the detected scope.
|
|
46
|
-
3. Runs device discovery with
|
|
47
|
-
|
|
47
|
+
3. Runs device discovery. Per-user scope scans with your API key so the
|
|
48
|
+
device is attributed to you from the first report; sudo/MDM scope scans
|
|
49
|
+
all users with --discovery-key. With --set-cron, sets up a recurring daily
|
|
50
|
+
scheduled scan (cross-platform) instead of a one-time scan.
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
Per-user onboarding needs only your API key. The separate --discovery-key is
|
|
53
|
+
required only under sudo (MDM/org enrollment), where the scan covers every user
|
|
54
|
+
on the device and cannot be attributed from a single user's key.
|
|
52
55
|
|
|
53
56
|
Examples:
|
|
54
|
-
$ unbound onboard --api-key <USER_KEY>
|
|
57
|
+
$ unbound onboard --api-key <USER_KEY>
|
|
55
58
|
$ sudo unbound onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
|
|
56
|
-
$ unbound onboard --api-key <USER_KEY> --
|
|
57
|
-
$ unbound onboard --api-key <USER_KEY> --
|
|
59
|
+
$ unbound onboard --api-key <USER_KEY> --backfill
|
|
60
|
+
$ unbound onboard --api-key <USER_KEY> --set-cron
|
|
58
61
|
`)
|
|
59
62
|
.action(telemetry.wrapAction('onboard', async (opts) => {
|
|
60
63
|
const apiKeyOpt = opts.apiKey || opts.adminApiKey || process.env.UNBOUND_API_KEY;
|
|
@@ -65,8 +68,12 @@ Examples:
|
|
|
65
68
|
telemetry.rememberSecret(discoveryKeyOpt);
|
|
66
69
|
const isMdm = hasRootPrivileges();
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
// MDM/org scope (sudo) scans every user on the device, so it still needs a
|
|
72
|
+
// dedicated org discovery key. Per-user scope scans only the current user
|
|
73
|
+
// and attributes the device via that user's own API key (see Step 2), so a
|
|
74
|
+
// separate discovery key is neither needed nor used.
|
|
75
|
+
if (isMdm && !discoveryKeyOpt) {
|
|
76
|
+
output.error('--discovery-key is required under sudo (MDM/org enrollment), or set UNBOUND_DISCOVERY_KEY env var');
|
|
70
77
|
process.exitCode = 1;
|
|
71
78
|
return;
|
|
72
79
|
}
|
|
@@ -77,6 +84,13 @@ Examples:
|
|
|
77
84
|
process.exitCode = 1;
|
|
78
85
|
return;
|
|
79
86
|
}
|
|
87
|
+
// Back-compat: --discovery-key used to be required for per-user onboarding.
|
|
88
|
+
// It is now ignored in user scope — the personal API key is used for the
|
|
89
|
+
// scan so the device is attributed to the user from the first report
|
|
90
|
+
// (WEB-4891). Warn rather than fail so existing scripts keep working.
|
|
91
|
+
if (!isMdm && discoveryKeyOpt) {
|
|
92
|
+
output.warn('--discovery-key is deprecated for per-user onboarding and is ignored; your API key is used for the device scan, so the device is attributed to you. You can drop --discovery-key (and unset UNBOUND_DISCOVERY_KEY if it is set in your environment).');
|
|
93
|
+
}
|
|
80
94
|
|
|
81
95
|
let setupSucceeded = false;
|
|
82
96
|
let discoverySucceeded = false;
|
|
@@ -148,7 +162,11 @@ Examples:
|
|
|
148
162
|
console.log('');
|
|
149
163
|
output.info('Step 2/2: Running device discovery');
|
|
150
164
|
console.log('');
|
|
151
|
-
|
|
165
|
+
// Per-user scope: scan with the user's own API key so the backend
|
|
166
|
+
// attributes the device to them at ingestion. Using the org discovery
|
|
167
|
+
// key here leaves the device unattributed until a device->user mapping
|
|
168
|
+
// resolves later (WEB-4891).
|
|
169
|
+
await runDiscoveryScan({ apiKey, domain: discoveryDomain });
|
|
152
170
|
discoverySucceeded = true;
|
|
153
171
|
skippedTools = skipped;
|
|
154
172
|
}
|
|
@@ -168,11 +186,12 @@ Examples:
|
|
|
168
186
|
skipRunAtLoad: true,
|
|
169
187
|
});
|
|
170
188
|
} else {
|
|
189
|
+
// Per-user cron re-runs `unbound onboard`, which now scans with the
|
|
190
|
+
// user's own API key — no separate discovery key to store.
|
|
171
191
|
output.info('Setting up daily scheduled run');
|
|
172
192
|
await setupScheduledRun({
|
|
173
193
|
command: 'onboard',
|
|
174
194
|
apiKey: apiKeyOpt || config.getApiKey(),
|
|
175
|
-
discoveryKey: discoveryKeyOpt,
|
|
176
195
|
domain: discoveryDomain,
|
|
177
196
|
skipRunAtLoad: true,
|
|
178
197
|
});
|
|
@@ -199,11 +218,15 @@ Examples:
|
|
|
199
218
|
if (isMdm) {
|
|
200
219
|
console.error(` Re-run just the scheduled scan with: sudo unbound discover --api-key <DISCOVERY_KEY> --set-cron${suffix}`);
|
|
201
220
|
} else {
|
|
202
|
-
console.error(` Re-run cron setup with: unbound onboard --api-key <KEY> --
|
|
221
|
+
console.error(` Re-run cron setup with: unbound onboard --api-key <KEY> --set-cron${suffix}`);
|
|
203
222
|
}
|
|
204
223
|
} else if (setupSucceeded && !discoverySucceeded) {
|
|
205
224
|
console.error(' Tool setup completed successfully — only discovery failed.');
|
|
206
|
-
|
|
225
|
+
if (isMdm) {
|
|
226
|
+
console.error(` Re-run discovery only with: sudo unbound discover --api-key <DISCOVERY_KEY>${suffix}`);
|
|
227
|
+
} else {
|
|
228
|
+
console.error(` Re-run discovery only with: unbound discover --api-key <KEY>${suffix}`);
|
|
229
|
+
}
|
|
207
230
|
}
|
|
208
231
|
process.exitCode = 1;
|
|
209
232
|
}
|
package/src/commands/setup.js
CHANGED
|
@@ -29,6 +29,7 @@ function isWindowsNative() {
|
|
|
29
29
|
const SETUP_TOOLS = [
|
|
30
30
|
{ label: 'Cursor', value: 'cursor', script: 'cursor/setup.py' },
|
|
31
31
|
{ label: 'GitHub Copilot', value: 'copilot', script: 'copilot/hooks/setup.py' },
|
|
32
|
+
{ label: 'Augment Code', value: 'augment', script: 'augment/hooks/setup.py' },
|
|
32
33
|
{ label: 'Claude Code \u2014 subscription (hooks)', value: 'claude-sub', script: 'claude-code/hooks/setup.py', group: 'claude-code' },
|
|
33
34
|
{ label: 'Claude Code \u2014 gateway (gateway)', value: 'claude-gw', script: 'claude-code/gateway/setup.py', group: 'claude-code' },
|
|
34
35
|
{ label: 'Gemini CLI', value: 'gemini', script: 'gemini-cli/gateway/setup.py' },
|
|
@@ -41,6 +42,7 @@ const SETUP_TOOLS = [
|
|
|
41
42
|
const MDM_TOOLS = {
|
|
42
43
|
'cursor': { label: 'Cursor', script: 'cursor/mdm/setup.py' },
|
|
43
44
|
'copilot': { label: 'GitHub Copilot', script: 'copilot/hooks/mdm/setup.py' },
|
|
45
|
+
'augment': { label: 'Augment Code', script: 'augment/hooks/mdm/setup.py' },
|
|
44
46
|
'claude-code-subscription': { label: 'Claude Code', script: 'claude-code/hooks/mdm/setup.py' },
|
|
45
47
|
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/mdm/setup.py' },
|
|
46
48
|
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/mdm/setup.py' },
|
|
@@ -49,21 +51,22 @@ const MDM_TOOLS = {
|
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
// Default MDM tools for `sudo unbound onboard` (subscription mode for Claude Code/Codex since only one can be active)
|
|
52
|
-
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
54
|
+
const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot', 'augment'];
|
|
53
55
|
|
|
54
56
|
// Tools for `sudo unbound setup --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
|
|
55
|
-
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
|
|
57
|
+
const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot', 'augment'];
|
|
56
58
|
|
|
57
59
|
// Default tools for `unbound onboard` (Cursor, Claude Code hooks, Codex hooks, Copilot hooks; no Gemini CLI).
|
|
58
|
-
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
60
|
+
const ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot', 'augment'];
|
|
59
61
|
|
|
60
62
|
// Tools for `unbound setup --all` — identical to ALL_TOOLS today; split kept for future flexibility.
|
|
61
|
-
const SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot'];
|
|
63
|
+
const SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot', 'augment'];
|
|
62
64
|
|
|
63
65
|
// Tool name → script mapping for automated tools
|
|
64
66
|
const SETUP_TOOL_MAP = {
|
|
65
67
|
'cursor': { label: 'Cursor', script: 'cursor/setup.py' },
|
|
66
68
|
'copilot': { label: 'GitHub Copilot', script: 'copilot/hooks/setup.py' },
|
|
69
|
+
'augment': { label: 'Augment Code', script: 'augment/hooks/setup.py' },
|
|
67
70
|
'claude-code-subscription': { label: 'Claude Code', script: 'claude-code/hooks/setup.py' },
|
|
68
71
|
'claude-code-gateway': { label: 'Claude Code (gateway)', script: 'claude-code/gateway/setup.py' },
|
|
69
72
|
'gemini-cli': { label: 'Gemini CLI', script: 'gemini-cli/gateway/setup.py' },
|
|
@@ -327,6 +330,7 @@ const NUKE_ENV_VARS = [
|
|
|
327
330
|
'UNBOUND_CODEX_API_KEY',
|
|
328
331
|
'UNBOUND_COPILOT_API_KEY',
|
|
329
332
|
'UNBOUND_CURSOR_API_KEY',
|
|
333
|
+
'UNBOUND_AUGMENT_API_KEY',
|
|
330
334
|
'ANTHROPIC_BASE_URL',
|
|
331
335
|
];
|
|
332
336
|
|
|
@@ -585,6 +589,7 @@ Scope is chosen automatically from your privileges:
|
|
|
585
589
|
Available tools:
|
|
586
590
|
cursor Cursor IDE
|
|
587
591
|
copilot GitHub Copilot
|
|
592
|
+
augment Augment Code
|
|
588
593
|
claude-code Claude Code (use --subscription or --gateway)
|
|
589
594
|
gemini-cli Gemini CLI
|
|
590
595
|
codex Codex (use --subscription or --gateway)
|
|
@@ -603,11 +608,12 @@ Examples:
|
|
|
603
608
|
Single tool:
|
|
604
609
|
$ unbound setup cursor Set up Cursor
|
|
605
610
|
$ unbound setup copilot Set up GitHub Copilot
|
|
611
|
+
$ unbound setup augment Set up Augment Code
|
|
606
612
|
$ unbound setup claude-code --gateway Claude Code gateway mode
|
|
607
613
|
$ unbound setup claude-code --subscription Claude Code hooks only
|
|
608
614
|
$ unbound setup codex --gateway Codex gateway mode
|
|
609
615
|
|
|
610
|
-
Install the default bundle (Cursor + Copilot + Claude Code hooks + Codex hooks):
|
|
616
|
+
Install the default bundle (Cursor + Copilot + Augment + Claude Code hooks + Codex hooks):
|
|
611
617
|
$ unbound setup --all Set up the default bundle
|
|
612
618
|
$ unbound setup --all --api-key <key> Login + set up the bundle
|
|
613
619
|
|
package/src/commands/tools.js
CHANGED
package/src/index.js
CHANGED
|
@@ -55,14 +55,15 @@ AUTHENTICATION
|
|
|
55
55
|
$ unbound config urls <gateway-url> <frontend-url> <backend-url>
|
|
56
56
|
|
|
57
57
|
ONBOARDING (one-step install + discover; scope auto-detected from sudo)
|
|
58
|
-
$ unbound onboard --api-key <USER_KEY>
|
|
58
|
+
$ unbound onboard --api-key <USER_KEY> Current user
|
|
59
59
|
$ sudo unbound onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY> All users (MDM)
|
|
60
60
|
|
|
61
61
|
TOOL SETUP
|
|
62
62
|
$ unbound setup Select and install multiple tools interactively
|
|
63
|
-
$ unbound setup --all Set up the default bundle (Cursor + Copilot + Claude Code hooks + Codex hooks)
|
|
63
|
+
$ unbound setup --all Set up the default bundle (Cursor + Copilot + Augment + Claude Code hooks + Codex hooks)
|
|
64
64
|
$ unbound setup cursor Set up Cursor
|
|
65
65
|
$ unbound setup copilot Set up GitHub Copilot
|
|
66
|
+
$ unbound setup augment Set up Augment Code
|
|
66
67
|
$ unbound setup claude-code Set up Claude Code (interactive mode selection)
|
|
67
68
|
$ unbound setup claude-code --gateway Use Unbound as AI provider
|
|
68
69
|
$ unbound setup claude-code --subscription Hooks only (keep your subscription)
|
|
@@ -86,6 +87,7 @@ TOOL SETUP
|
|
|
86
87
|
Remove configuration (no API key needed to clear):
|
|
87
88
|
$ unbound setup cursor --clear Remove Unbound config for Cursor
|
|
88
89
|
$ unbound setup copilot --clear Remove Unbound config for GitHub Copilot
|
|
90
|
+
$ unbound setup augment --clear Remove Unbound config for Augment Code
|
|
89
91
|
$ unbound setup claude-code --clear Remove Unbound config for Claude Code
|
|
90
92
|
$ unbound setup gemini-cli --clear Remove Unbound config for Gemini CLI
|
|
91
93
|
$ unbound setup codex --clear Remove Unbound config for Codex
|
package/src/toolHealth.js
CHANGED
|
@@ -181,10 +181,10 @@ function envCheck(label, name, expected, kind = 'aux') {
|
|
|
181
181
|
// and a managed config that points at a missing hook script is a broken (tampered)
|
|
182
182
|
// MDM install, not a healthy one. Returns { status: 'healthy'|'tampered'|null, checks }.
|
|
183
183
|
function mdmDetect(family, dirOverride, binaryPath) {
|
|
184
|
-
// Only Cursor / Claude Code / Codex have a managed (MDM) directory.
|
|
185
|
-
// Gemini have none — their org install writes the same per-user
|
|
186
|
-
// every profile, so they're checked exactly like a user-level tool.
|
|
187
|
-
const configName = { cursor: 'hooks.json', 'claude-code': 'managed-settings.json', codex: 'managed-settings.json' };
|
|
184
|
+
// Only Cursor / Claude Code / Codex / Augment have a managed (MDM) directory.
|
|
185
|
+
// Copilot and Gemini have none — their org install writes the same per-user
|
|
186
|
+
// config into every profile, so they're checked exactly like a user-level tool.
|
|
187
|
+
const configName = { cursor: 'hooks.json', 'claude-code': 'managed-settings.json', codex: 'managed-settings.json', augment: 'settings.json' };
|
|
188
188
|
if (!configName[family]) return { status: null, checks: [] };
|
|
189
189
|
|
|
190
190
|
const mac = '/Library/Application Support';
|
|
@@ -193,6 +193,9 @@ function mdmDetect(family, dirOverride, binaryPath) {
|
|
|
193
193
|
cursor: { darwin: `${mac}/Cursor`, linux: '/etc/cursor', win32: path.join(win, 'Cursor') },
|
|
194
194
|
'claude-code': { darwin: `${mac}/ClaudeCode`, linux: '/etc/claude-code', win32: path.join(win, 'ClaudeCode') },
|
|
195
195
|
codex: { darwin: `${mac}/Codex`, linux: '/etc/codex', win32: path.join(win, 'Codex') },
|
|
196
|
+
// Augment's managed dir is /etc/augment on BOTH macOS and Linux (mirrors the
|
|
197
|
+
// setup repo's get_managed_settings_dir), not Library/Application Support.
|
|
198
|
+
augment: { darwin: '/etc/augment', linux: '/etc/augment', win32: path.join(win, 'Augment') },
|
|
196
199
|
};
|
|
197
200
|
const dir = dirOverride || dirs[family][process.platform] || dirs[family].linux;
|
|
198
201
|
const configPath = path.join(dir, configName[family]);
|
|
@@ -305,6 +308,23 @@ function buildVariants(gatewayUrl, binaryPath) {
|
|
|
305
308
|
return checks;
|
|
306
309
|
},
|
|
307
310
|
},
|
|
311
|
+
{
|
|
312
|
+
key: 'augment', label: 'Augment Code', family: 'augment', mode: null,
|
|
313
|
+
checks: () => {
|
|
314
|
+
// ~/.augment/settings.json is shared with the user's own Augment config and may be
|
|
315
|
+
// JSONC, so match the Unbound hook marker on RAW TEXT (not strict JSON). Like
|
|
316
|
+
// copilot, a user install can point at the python unbound.py hook OR the shared
|
|
317
|
+
// unbound-hook binary — check whichever the config references so a binary-backed
|
|
318
|
+
// install isn't misread as tampered.
|
|
319
|
+
const cfgText = readText('~/.augment/settings.json');
|
|
320
|
+
const hasBinary = cfgText != null && cfgText.includes(BINARY_MARKER);
|
|
321
|
+
const checks = [configCheck('Config', '~/.augment/settings.json', { json: false, test: (t) => t.includes('unbound.py') || t.includes(BINARY_MARKER) })];
|
|
322
|
+
if (hasBinary) checks.push(scriptCheck('Hook binary', binaryPath || BINARY_PATH));
|
|
323
|
+
else checks.push(scriptCheck('Hook script', '~/.augment/hooks/unbound.py'));
|
|
324
|
+
checks.push(envKeyCheck('API key env', 'UNBOUND_AUGMENT_API_KEY'));
|
|
325
|
+
return checks;
|
|
326
|
+
},
|
|
327
|
+
},
|
|
308
328
|
// Gemini CLI is intentionally omitted here — it isn't part of `setup --all`
|
|
309
329
|
// and has no managed directory. Add it back when its scope is settled.
|
|
310
330
|
];
|
|
@@ -100,7 +100,9 @@ test('onboard schedules via setupScheduledRun with command onboard', () => {
|
|
|
100
100
|
);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
// WEB-4891: per-user onboarding scans with the user's own API key, so the
|
|
104
|
+
// per-user cron stores a single key — no separate discoveryKey.
|
|
105
|
+
test('per-user onboard cron passes apiKey but NOT discoveryKey to setupScheduledRun', () => {
|
|
104
106
|
const callIdx = onboardSrc.indexOf("command: 'onboard'");
|
|
105
107
|
assert.notEqual(callIdx, -1, 'expected command: onboard call site in onboard.js');
|
|
106
108
|
const open = onboardSrc.lastIndexOf('{', callIdx);
|
|
@@ -112,10 +114,31 @@ test('onboard passes both apiKey and discoveryKey to setupScheduledRun', () => {
|
|
|
112
114
|
}
|
|
113
115
|
const callArgs = onboardSrc.slice(open, close + 1);
|
|
114
116
|
assert.ok(callArgs.includes('apiKey'), 'setupScheduledRun call must include apiKey');
|
|
115
|
-
assert.ok(callArgs.includes('discoveryKey'), '
|
|
117
|
+
assert.ok(!callArgs.includes('discoveryKey'), 'per-user onboard cron must NOT pass a discoveryKey (single-key onboarding, WEB-4891)');
|
|
116
118
|
assert.ok(!callArgs.toLowerCase().includes('backfill'), 'setupScheduledRun call must not include backfill');
|
|
117
119
|
});
|
|
118
120
|
|
|
121
|
+
// WEB-4891 core fix: the per-user discovery scan must run with the user's own
|
|
122
|
+
// API key so the backend attributes the device to them at ingestion. Only the
|
|
123
|
+
// MDM (sudo) branch — which scans every user and cannot be attributed from one
|
|
124
|
+
// user's key — may scan with the org discovery key.
|
|
125
|
+
test('per-user discovery scans with the user key; only MDM uses the discovery key', () => {
|
|
126
|
+
const userBranchStart = onboardSrc.indexOf('const apiKey = config.getApiKey();');
|
|
127
|
+
assert.notEqual(userBranchStart, -1, 'expected the per-user branch in onboard.js');
|
|
128
|
+
const userBranch = onboardSrc.slice(userBranchStart);
|
|
129
|
+
assert.match(
|
|
130
|
+
userBranch,
|
|
131
|
+
/runDiscoveryScan\(\{\s*apiKey\s*,/,
|
|
132
|
+
'per-user onboarding must scan with the user apiKey, not the org discovery key'
|
|
133
|
+
);
|
|
134
|
+
const discoveryKeyScans = (onboardSrc.match(/runDiscoveryScan\(\{\s*apiKey:\s*discoveryKeyOpt/g) || []).length;
|
|
135
|
+
assert.equal(
|
|
136
|
+
discoveryKeyScans,
|
|
137
|
+
1,
|
|
138
|
+
'exactly one runDiscoveryScan (the MDM branch) may use the org discovery key'
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
119
142
|
test('onboard imports setupScheduledRun from ../scheduled', () => {
|
|
120
143
|
assert.match(
|
|
121
144
|
onboardSrc,
|
|
@@ -35,7 +35,7 @@ test('hasRootPrivileges: false when effective uid is not 0 (no sudo)', () => {
|
|
|
35
35
|
// ---- 2. onboard routes by detected scope ----
|
|
36
36
|
// Reloads the module graph with stubbed deps so we can observe which bundle
|
|
37
37
|
// helper the single `onboard` command invokes.
|
|
38
|
-
async function runOnboard(isRoot, { setCron = false } = {}) {
|
|
38
|
+
async function runOnboard(isRoot, { setCron = false, discoveryKey = 'd' } = {}) {
|
|
39
39
|
for (const m of [
|
|
40
40
|
'../src/commands/onboard',
|
|
41
41
|
'../src/commands/setup',
|
|
@@ -48,7 +48,7 @@ async function runOnboard(isRoot, { setCron = false } = {}) {
|
|
|
48
48
|
delete require.cache[require.resolve(m)];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const calls = { mdm: 0, user: 0, discovery: 0, loggedIn: 0, cron: null };
|
|
51
|
+
const calls = { mdm: 0, user: 0, discovery: 0, loggedIn: 0, cron: null, warns: [] };
|
|
52
52
|
const setup = require('../src/commands/setup');
|
|
53
53
|
const discover = require('../src/commands/discover');
|
|
54
54
|
const auth = require('../src/auth');
|
|
@@ -59,7 +59,7 @@ async function runOnboard(isRoot, { setCron = false } = {}) {
|
|
|
59
59
|
setup.hasRootPrivileges = () => isRoot;
|
|
60
60
|
setup.runMdmSetupAllBundle = async () => { calls.mdm++; return { ok: true, skipped: [] }; };
|
|
61
61
|
setup.runSetupAllBundle = async () => { calls.user++; return { ok: true, skipped: [] }; };
|
|
62
|
-
discover.runDiscoveryScan = async () => { calls.discovery++; };
|
|
62
|
+
discover.runDiscoveryScan = async (o) => { calls.discovery++; calls.discoveryArgs = o; };
|
|
63
63
|
auth.ensureLoggedIn = async () => { calls.loggedIn++; };
|
|
64
64
|
scheduled.setupScheduledRun = async (o) => { calls.cron = o; };
|
|
65
65
|
config.setUrls = () => ({});
|
|
@@ -68,13 +68,15 @@ async function runOnboard(isRoot, { setCron = false } = {}) {
|
|
|
68
68
|
config.getFrontendUrl = () => 'https://f.acme';
|
|
69
69
|
config.getGatewayUrl = () => 'https://g.acme';
|
|
70
70
|
config.isLoggedIn = () => true;
|
|
71
|
-
for (const k of ['info', 'success', 'error'
|
|
71
|
+
for (const k of ['info', 'success', 'error']) output[k] = () => {};
|
|
72
|
+
output.warn = (m) => calls.warns.push(m);
|
|
72
73
|
|
|
73
74
|
const { register } = require('../src/commands/onboard');
|
|
74
75
|
const program = new Command();
|
|
75
76
|
program.exitOverride();
|
|
76
77
|
register(program);
|
|
77
|
-
const argv = ['node', 'unbound', 'onboard', '--api-key', 'k'
|
|
78
|
+
const argv = ['node', 'unbound', 'onboard', '--api-key', 'k'];
|
|
79
|
+
if (discoveryKey) argv.push('--discovery-key', discoveryKey);
|
|
78
80
|
if (setCron) argv.push('--set-cron');
|
|
79
81
|
await program.parseAsync(argv);
|
|
80
82
|
return calls;
|
|
@@ -86,6 +88,7 @@ test('onboard with sudo/root → installs the MDM bundle (all users), no login',
|
|
|
86
88
|
assert.equal(calls.user, 0, 'user bundle NOT installed');
|
|
87
89
|
assert.equal(calls.loggedIn, 0, 'MDM scope does not run ensureLoggedIn');
|
|
88
90
|
assert.equal(calls.discovery, 1, 'discovery still runs');
|
|
91
|
+
assert.equal(calls.discoveryArgs.apiKey, 'd', 'MDM scope scans with the org discovery key');
|
|
89
92
|
});
|
|
90
93
|
|
|
91
94
|
test('onboard without sudo → logs in and installs the user bundle', async () => {
|
|
@@ -94,6 +97,9 @@ test('onboard without sudo → logs in and installs the user bundle', async () =
|
|
|
94
97
|
assert.equal(calls.mdm, 0, 'MDM bundle NOT installed');
|
|
95
98
|
assert.equal(calls.loggedIn, 1, 'user scope runs ensureLoggedIn');
|
|
96
99
|
assert.equal(calls.discovery, 1, 'discovery still runs');
|
|
100
|
+
// WEB-4891: per-user scan uses the resolved user key, NOT the org discovery key.
|
|
101
|
+
assert.equal(calls.discoveryArgs.apiKey, 'resolved-key', 'user scope scans with the user API key');
|
|
102
|
+
assert.notEqual(calls.discoveryArgs.apiKey, 'd', 'user scope must NOT scan with the discovery key');
|
|
97
103
|
});
|
|
98
104
|
|
|
99
105
|
// --set-cron under sudo schedules discovery only (not a daily full MDM
|
|
@@ -110,5 +116,34 @@ test('onboard --set-cron without sudo → schedules the full onboard run', async
|
|
|
110
116
|
const calls = await runOnboard(false, { setCron: true });
|
|
111
117
|
assert.ok(calls.cron, 'a scheduled run was set up');
|
|
112
118
|
assert.equal(calls.cron.command, 'onboard', 'user scope keeps the full onboard cron');
|
|
113
|
-
|
|
119
|
+
// WEB-4891: the per-user cron re-runs onboard, which scans with the user's own
|
|
120
|
+
// key — so the user key is stored and no separate discovery key is.
|
|
121
|
+
assert.equal(calls.cron.apiKey, 'k', 'per-user cron stores the user API key');
|
|
122
|
+
assert.equal(calls.cron.discoveryKey, undefined, 'per-user onboard cron no longer forwards a discovery key');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// WEB-4891: the clean per-user path passes NO --discovery-key. It must work
|
|
126
|
+
// without error, scan with the user key, and emit no deprecation warning.
|
|
127
|
+
test('onboard without sudo and without --discovery-key → clean single-key path', async () => {
|
|
128
|
+
const calls = await runOnboard(false, { discoveryKey: null });
|
|
129
|
+
assert.equal(calls.user, 1, 'user bundle installed');
|
|
130
|
+
assert.equal(calls.discovery, 1, 'discovery runs');
|
|
131
|
+
assert.equal(calls.discoveryArgs.apiKey, 'resolved-key', 'scans with the user API key');
|
|
132
|
+
assert.equal(
|
|
133
|
+
calls.warns.filter((w) => /discovery-key is deprecated/.test(w)).length,
|
|
134
|
+
0,
|
|
135
|
+
'no deprecation warning when --discovery-key is omitted',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Passing --discovery-key in user scope is deprecated: it is ignored (the scan
|
|
140
|
+
// still uses the user key) and exactly one deprecation warning is emitted.
|
|
141
|
+
test('onboard without sudo WITH --discovery-key → ignored + deprecation warning', async () => {
|
|
142
|
+
const calls = await runOnboard(false, { discoveryKey: 'd' });
|
|
143
|
+
assert.equal(calls.discoveryArgs.apiKey, 'resolved-key', 'still scans with the user API key, not the discovery key');
|
|
144
|
+
assert.equal(
|
|
145
|
+
calls.warns.filter((w) => /discovery-key is deprecated/.test(w)).length,
|
|
146
|
+
1,
|
|
147
|
+
'emits exactly one deprecation warning',
|
|
148
|
+
);
|
|
114
149
|
});
|
package/test/setup-args.test.js
CHANGED
|
@@ -130,8 +130,8 @@ test('buildScriptArgs: mdm clear without key emits --clear and no --api-key/--do
|
|
|
130
130
|
test('resolveSetupAllTools(false): install bundle is subscription-only, no gateway/gemini', () => {
|
|
131
131
|
const tools = resolveSetupAllTools(false);
|
|
132
132
|
// Membership + length (order-independent) so reordering SETUP_ALL_TOOLS doesn't break this.
|
|
133
|
-
assert.equal(tools.length,
|
|
134
|
-
for (const t of ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot']) {
|
|
133
|
+
assert.equal(tools.length, 5, `expected 5 tools, got ${tools.join(',')}`);
|
|
134
|
+
for (const t of ['cursor', 'claude-code-subscription', 'codex-subscription', 'copilot', 'augment']) {
|
|
135
135
|
assert.ok(tools.includes(t), `install bundle missing ${t}: ${tools.join(',')}`);
|
|
136
136
|
}
|
|
137
137
|
assert.ok(!tools.includes('claude-code-gateway'), tools.join(','));
|
|
@@ -141,7 +141,7 @@ test('resolveSetupAllTools(false): install bundle is subscription-only, no gatew
|
|
|
141
141
|
|
|
142
142
|
test('resolveSetupAllTools(true): clear-all covers every tool incl. gateway modes + gemini', () => {
|
|
143
143
|
const tools = resolveSetupAllTools(true);
|
|
144
|
-
for (const t of ['cursor', 'copilot', 'claude-code-subscription', 'claude-code-gateway',
|
|
144
|
+
for (const t of ['cursor', 'copilot', 'augment', 'claude-code-subscription', 'claude-code-gateway',
|
|
145
145
|
'gemini-cli', 'codex-subscription', 'codex-gateway']) {
|
|
146
146
|
assert.ok(tools.includes(t), `clear-all missing ${t}: ${tools.join(',')}`);
|
|
147
147
|
}
|
package/test/tool-health.test.js
CHANGED
|
@@ -188,6 +188,70 @@ test('MDM: a user-level install takes precedence over an MDM config', () => {
|
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
test('augment user install: ~/.augment settings + hook + key → healthy (user scope, not mdm)', () => {
|
|
192
|
+
withHome((tmp, th) => {
|
|
193
|
+
writeFile(path.join(tmp, '.augment', 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: path.join(tmp, '.augment', 'hooks', 'unbound.py') }] }] } }));
|
|
194
|
+
writeFile(path.join(tmp, '.augment', 'hooks', 'unbound.py'), '#');
|
|
195
|
+
process.env.UNBOUND_AUGMENT_API_KEY = 'k';
|
|
196
|
+
try {
|
|
197
|
+
const t = th.detectTools({ apiKey: 'k' }).find((x) => x.family === 'augment');
|
|
198
|
+
assert.equal(t.status, 'healthy');
|
|
199
|
+
assert.notEqual(t.scope, 'mdm');
|
|
200
|
+
} finally { delete process.env.UNBOUND_AUGMENT_API_KEY; }
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('augment user install: JSONC settings (a // comment strict JSON.parse rejects) still detected via raw-text marker → healthy', () => {
|
|
205
|
+
withHome((tmp, th) => {
|
|
206
|
+
const scriptPath = path.join(tmp, '.augment', 'hooks', 'unbound.py');
|
|
207
|
+
const jsonc = `{\n // unbound hook (JSONC comment)\n "hooks": { "PreToolUse": [{ "hooks": [{ "command": ${JSON.stringify(scriptPath)} }] }] }\n}`;
|
|
208
|
+
writeFile(path.join(tmp, '.augment', 'settings.json'), jsonc);
|
|
209
|
+
writeFile(scriptPath, '#');
|
|
210
|
+
process.env.UNBOUND_AUGMENT_API_KEY = 'k';
|
|
211
|
+
try {
|
|
212
|
+
const t = th.detectTools({ apiKey: 'k' }).find((x) => x.family === 'augment');
|
|
213
|
+
assert.equal(t.status, 'healthy');
|
|
214
|
+
} finally { delete process.env.UNBOUND_AUGMENT_API_KEY; }
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('augment user install: settings point at the unbound-hook binary + binary present → healthy (not tampered, not mdm)', () => {
|
|
219
|
+
withHome((tmp, th) => {
|
|
220
|
+
const bin = path.join(tmp, 'opt', 'unbound-hook', 'unbound-hook');
|
|
221
|
+
writeFile(bin, '#');
|
|
222
|
+
writeFile(path.join(tmp, '.augment', 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: `"${bin}" hook augment PreToolUse` }] }] } }));
|
|
223
|
+
process.env.UNBOUND_AUGMENT_API_KEY = 'k';
|
|
224
|
+
try {
|
|
225
|
+
const t = th.detectTools({ apiKey: 'k', _binaryPath: bin }).find((x) => x.family === 'augment');
|
|
226
|
+
assert.equal(t.status, 'healthy');
|
|
227
|
+
assert.notEqual(t.scope, 'mdm');
|
|
228
|
+
} finally { delete process.env.UNBOUND_AUGMENT_API_KEY; }
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('MDM: augment managed config (python) references unbound + hook present → managed by MDM', () => {
|
|
233
|
+
withHome((tmp, th) => {
|
|
234
|
+
const mdmDir = path.join(tmp, 'mdm', 'Augment');
|
|
235
|
+
writeFile(path.join(mdmDir, 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: path.join(mdmDir, 'hooks', 'unbound.py') }] }] } }));
|
|
236
|
+
writeFile(path.join(mdmDir, 'hooks', 'unbound.py'), '#');
|
|
237
|
+
const t = th.detectTools({ apiKey: 'k', _mdmDirs: { augment: mdmDir } }).find((x) => x.family === 'augment');
|
|
238
|
+
assert.equal(t.status, 'managed-by-mdm');
|
|
239
|
+
assert.equal(t.scope, 'mdm');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('MDM: augment managed config (binary) references the hook binary + binary present → managed by MDM', () => {
|
|
244
|
+
withHome((tmp, th) => {
|
|
245
|
+
const mdmDir = path.join(tmp, 'mdm', 'Augment');
|
|
246
|
+
const bin = path.join(tmp, 'opt', 'unbound-hook', 'unbound-hook');
|
|
247
|
+
writeFile(bin, '#');
|
|
248
|
+
writeFile(path.join(mdmDir, 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [{ hooks: [{ command: `"${bin}" hook augment PreToolUse` }] }] } }));
|
|
249
|
+
const t = th.detectTools({ apiKey: 'k', _mdmDirs: { augment: mdmDir }, _binaryPath: bin }).find((x) => x.family === 'augment');
|
|
250
|
+
assert.equal(t.status, 'managed-by-mdm');
|
|
251
|
+
assert.equal(t.scope, 'mdm');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
191
255
|
test('codex subscription: hooks.json + script present but the feature flag is off → tampered', () => {
|
|
192
256
|
withHome((tmp, th) => {
|
|
193
257
|
writeFile(path.join(tmp, '.codex', 'hooks.json'), JSON.stringify({ hooks: { x: [{ command: path.join(tmp, '.codex', 'hooks', 'unbound.py') }] } }));
|
|
@@ -206,10 +270,10 @@ test('copilot has no MDM scope — even with a managed dir it never reports mana
|
|
|
206
270
|
});
|
|
207
271
|
});
|
|
208
272
|
|
|
209
|
-
test('only
|
|
273
|
+
test('only five tools are reported (Gemini CLI is omitted)', () => {
|
|
210
274
|
withHome((tmp, th) => {
|
|
211
275
|
const keys = th.detectTools({ apiKey: 'k' }).map((t) => t.family);
|
|
212
|
-
assert.deepEqual(new Set(keys), new Set(['cursor', 'claude-code', 'codex', 'copilot']));
|
|
276
|
+
assert.deepEqual(new Set(keys), new Set(['cursor', 'claude-code', 'codex', 'copilot', 'augment']));
|
|
213
277
|
});
|
|
214
278
|
});
|
|
215
279
|
|