unbound-cli 1.1.8 → 1.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.
@@ -45,10 +45,10 @@ const MDM_TOOLS = {
45
45
  'codex-gateway': { label: 'Codex (gateway)', script: 'codex/gateway/mdm/setup.py' },
46
46
  };
47
47
 
48
- // Default MDM tools for `unbound onboard-mdm` (subscription mode for Claude Code/Codex since only one can be active)
48
+ // Default MDM tools for `sudo unbound onboard` (subscription mode for Claude Code/Codex since only one can be active)
49
49
  const MDM_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
50
50
 
51
- // Tools for `unbound setup mdm --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
51
+ // Tools for `sudo unbound setup --all` — identical to MDM_ALL_TOOLS today; split kept for future flexibility.
52
52
  const MDM_SETUP_ALL_TOOLS = ['cursor', 'claude-code-subscription', 'gemini-cli', 'codex-subscription', 'copilot'];
53
53
 
54
54
  // Default tools for `unbound onboard` (Cursor, Claude Code hooks, Codex hooks, Copilot hooks; no Gemini CLI).
@@ -73,7 +73,7 @@ const SETUP_TOOL_MAP = {
73
73
  * bundle (one mode per tool, no Gemini CLI), but clearing must remove EVERY
74
74
  * tool Unbound can configure — both modes plus Gemini CLI — so a prior
75
75
  * gateway-mode or Gemini setup isn't silently left behind. Mirrors the
76
- * `setup mdm --all` split.
76
+ * MDM `--all` split.
77
77
  */
78
78
  function resolveSetupAllTools(clear) {
79
79
  return clear ? Object.keys(SETUP_TOOL_MAP) : [...SETUP_ALL_TOOLS];
@@ -338,18 +338,6 @@ function runScriptPiped(scriptPath, args) {
338
338
  });
339
339
  }
340
340
 
341
- /**
342
- * Checks that the process is running as root (macOS/Linux).
343
- * Windows admin check is handled by the Python MDM scripts themselves.
344
- * Pass a customized hint for the calling command (defaults to "setup mdm").
345
- */
346
- function checkRoot(commandHint = 'setup mdm') {
347
- if (process.platform === 'win32') return;
348
- if (typeof process.getuid !== 'function' || process.getuid() !== 0) {
349
- throw new Error(`MDM setup requires root. Run with: sudo unbound ${commandHint} ...`);
350
- }
351
- }
352
-
353
341
  /**
354
342
  * Returns true when the process has the privileges needed to touch system-level
355
343
  * (MDM) configuration. On Windows, `net session` succeeds only when elevated, so
@@ -455,9 +443,12 @@ function register(program) {
455
443
  .argument('[tools...]', 'Tools to set up')
456
444
  .description(
457
445
  'Configure AI coding tools to use Unbound as their API gateway. ' +
458
- 'Run with no arguments for interactive setup, or specify tools directly.'
446
+ 'Run with no arguments for interactive setup, or specify tools directly. ' +
447
+ 'Run with sudo to configure every user on the device (MDM/org scope); ' +
448
+ 'without sudo, only the current user.'
459
449
  )
460
450
  .option('--api-key <key>', 'Authenticate with an API key (skips browser login)')
451
+ .addOption(new Option('--admin-api-key <key>', 'Alias for --api-key for MDM enrollment (back-compat)').hideHelp())
461
452
  .option('--clear', 'Remove Unbound configuration for the specified tools (no login or API key required)')
462
453
  .option('--subscription', 'Use subscription mode for Claude Code / Codex (hooks only)')
463
454
  .option('--gateway', 'Use gateway mode for Claude Code / Codex (Unbound as AI provider)')
@@ -467,6 +458,10 @@ function register(program) {
467
458
  .addOption(new Option('--frontend-url <url>', 'Override frontend URL for setup scripts (dev only)').hideHelp())
468
459
  .addOption(new Option('--gateway-url <url>', 'Override gateway URL for setup scripts (dev only)').hideHelp())
469
460
  .addHelpText('after', `
461
+ Scope is chosen automatically from your privileges:
462
+ Run with sudo to configure every user on the device (MDM/org scope);
463
+ without sudo, only the current user is configured.
464
+
470
465
  Available tools:
471
466
  cursor Cursor IDE
472
467
  copilot GitHub Copilot
@@ -496,6 +491,11 @@ Examples:
496
491
  $ unbound setup --all Set up the default bundle
497
492
  $ unbound setup --all --api-key <key> Login + set up the bundle
498
493
 
494
+ Configure all users on the device (MDM/org scope — run with sudo):
495
+ $ sudo unbound setup --all Configure all users (MDM)
496
+ $ sudo unbound setup cursor codex-subscription Configure specific tools for all users
497
+ $ sudo unbound setup --clear --all Remove config for all users
498
+
499
499
  Seed historical sessions (Claude Code / Codex subscription mode + Copilot):
500
500
  $ unbound setup claude-code --subscription --backfill Install hooks AND backfill local history
501
501
  $ unbound setup codex --subscription --backfill Install hooks AND backfill local history
@@ -545,6 +545,95 @@ must update the MDM configuration.
545
545
  const frontendUrl = written.frontend_url || config.getFrontendUrl();
546
546
  const gatewayUrl = written.gateway_url || config.getGatewayUrl();
547
547
 
548
+ // Scope is auto-detected from privileges: with sudo/root we configure
549
+ // every user on the device (MDM scope); without it, just the current
550
+ // user. URLs were already persisted above and apply to both scopes.
551
+ const isMdm = hasRootPrivileges();
552
+ if (isMdm) {
553
+ const mdmToolNames = [...Object.keys(MDM_TOOLS), 'claude-code', 'codex'].join(', ');
554
+ const adminApiKey = opts.adminApiKey || opts.apiKey || config.getApiKey();
555
+ if (!opts.clear && !adminApiKey) {
556
+ output.error('--api-key is required to set up tools (or run `unbound login` first).');
557
+ process.exitCode = 1;
558
+ return;
559
+ }
560
+ if (opts.all && tools.length > 0) {
561
+ output.error('Cannot combine --all with specific tool names. Use one or the other.');
562
+ process.exitCode = 1;
563
+ return;
564
+ }
565
+ let toolNames;
566
+ if (opts.all) {
567
+ // --clear --all wipes every tool (both modes); setup --all uses the
568
+ // subscription-default bundle (can't enroll both modes at once).
569
+ toolNames = opts.clear ? Object.keys(MDM_TOOLS) : MDM_SETUP_ALL_TOOLS;
570
+ } else if (tools.length > 0) {
571
+ toolNames = tools;
572
+ } else {
573
+ output.error('Specify tools to set up, or use --all.');
574
+ console.error(' Available: ' + mdmToolNames);
575
+ process.exitCode = 1;
576
+ return;
577
+ }
578
+ // Bare claude-code/codex have no interactive mode prompt under MDM:
579
+ // --clear removes both modes; setup honors --gateway/--subscription
580
+ // (matching user scope) and defaults to subscription.
581
+ if (!opts.clear && opts.subscription && opts.gateway) {
582
+ output.error('Cannot use both --subscription and --gateway. Choose one.');
583
+ process.exitCode = 1;
584
+ return;
585
+ }
586
+ toolNames = [...new Set(toolNames.flatMap(name => {
587
+ const mode = MODE_TOOLS[name];
588
+ if (!mode) return [name];
589
+ if (opts.clear) return [mode.subscription, mode.gateway];
590
+ return opts.gateway ? [mode.gateway] : [mode.subscription];
591
+ }))];
592
+ const invalid = toolNames.filter(t => !MDM_TOOLS[t]);
593
+ if (invalid.length > 0) {
594
+ output.error(`Unknown tool(s): ${invalid.join(', ')}`);
595
+ console.error(' Available: ' + mdmToolNames);
596
+ process.exitCode = 1;
597
+ return;
598
+ }
599
+ if (!opts.clear) {
600
+ if (toolNames.includes('claude-code-subscription') && toolNames.includes('claude-code-gateway')) {
601
+ output.error('Cannot use both claude-code-subscription and claude-code-gateway. Choose one.');
602
+ process.exitCode = 1;
603
+ return;
604
+ }
605
+ if (toolNames.includes('codex-subscription') && toolNames.includes('codex-gateway')) {
606
+ output.error('Cannot use both codex-subscription and codex-gateway. Choose one.');
607
+ process.exitCode = 1;
608
+ return;
609
+ }
610
+ }
611
+ const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
612
+ console.log('');
613
+ if (opts.backfill) {
614
+ for (const tool of resolvedTools) {
615
+ if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
616
+ }
617
+ }
618
+ const { ok } = await runBatch(
619
+ resolvedTools,
620
+ (tool) => {
621
+ const toolArgs = buildScriptArgs(adminApiKey, {
622
+ backendUrl,
623
+ frontendUrl,
624
+ gatewayUrl,
625
+ clear: opts.clear,
626
+ mdm: true,
627
+ backfill: opts.backfill && scriptSupportsBackfill(tool.script),
628
+ });
629
+ return runScriptPiped(tool.script, toolArgs);
630
+ },
631
+ { clear: opts.clear, summary: opts.clear ? 'All tools cleared' : 'All tools configured' }
632
+ );
633
+ if (!ok) return;
634
+ return;
635
+ }
636
+
548
637
  // Clearing config needs no credentials — the setup scripts remove
549
638
  // files without calling the API — so don't force a login for --clear.
550
639
  if (!opts.clear) {
@@ -747,169 +836,6 @@ must update the MDM configuration.
747
836
  }
748
837
  });
749
838
 
750
- // --- MDM setup ---
751
-
752
- // Bare claude-code/codex are accepted too: with --clear they remove both
753
- // modes; for setup they default to subscription (MDM has no mode prompt).
754
- const mdmToolNames = [...Object.keys(MDM_TOOLS), 'claude-code', 'codex'].join(', ');
755
-
756
- setup
757
- .command('mdm')
758
- .description(
759
- 'MDM setup: configure all users on this device. Requires root. ' +
760
- 'Used by organization admins to enroll devices via MDM.'
761
- )
762
- .argument('[tools...]', 'Tools to set up: ' + mdmToolNames)
763
- .option('--admin-api-key <key>', 'Admin API key for MDM enrollment (falls back to your stored `unbound login` key; not required with --clear)')
764
- .option('--clear', 'Remove Unbound configuration for the specified tools (no API key required)')
765
- .option('--all', 'Set up all available tools')
766
- .option('--backfill', 'Seed historical Claude Code / Codex / Copilot sessions from local transcripts into Unbound analytics (subscription/hooks mode only; Cursor unsupported)')
767
- .addHelpText('after', `
768
- Available tools:
769
- cursor Cursor IDE
770
- copilot GitHub Copilot
771
- claude-code-subscription Claude Code with your own subscription (hooks only)
772
- claude-code-gateway Claude Code with Unbound as AI provider
773
- claude-code Both Claude Code modes (clears both; sets up subscription)
774
- gemini-cli Gemini CLI
775
- codex-subscription Codex with your own subscription (hooks only)
776
- codex-gateway Codex with Unbound as AI provider
777
- codex Both Codex modes (clears both; sets up subscription)
778
-
779
- Note: claude-code-subscription and claude-code-gateway are mutually exclusive when
780
- setting up; same for codex. Bare claude-code/codex set up subscription mode.
781
- When using --all, subscription mode is used by default for Claude Code and Codex.
782
-
783
- Setup examples (need an admin key — pass --admin-api-key, or omit it to reuse the
784
- key stored by a prior \`unbound login\`):
785
- $ sudo unbound setup mdm --admin-api-key KEY cursor
786
- $ sudo unbound setup mdm --admin-api-key KEY cursor claude-code-subscription codex-subscription
787
- $ sudo unbound setup mdm --admin-api-key KEY --all
788
- $ sudo unbound setup mdm --all Reuse the stored \`unbound login\` key
789
- $ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription --backfill
790
- Install hooks AND backfill local history
791
- $ sudo unbound setup mdm --admin-api-key KEY copilot --backfill
792
- Install Copilot hooks AND backfill local history
793
-
794
- Clear examples (no API key required):
795
- $ sudo unbound setup mdm --clear cursor
796
- $ sudo unbound setup mdm --clear claude-code Clears BOTH Claude Code modes
797
- $ sudo unbound setup mdm --clear codex Clears BOTH Codex modes
798
- $ sudo unbound setup mdm --clear --all Clears every tool
799
- `)
800
- .action(async (tools, opts, command) => {
801
- try {
802
- checkRoot();
803
- // --all and --clear are defined on both this command and the parent `setup` command;
804
- // --backend-url, --frontend-url, --gateway-url are defined only on the parent `setup` command.
805
- // Use optsWithGlobals() so they all work regardless of position relative to `mdm`.
806
- const globalOpts = command.optsWithGlobals();
807
- // Clearing removes config without calling the API, so a key is only
808
- // required when actually enrolling tools. Fall back to the API key
809
- // stored by `unbound login` so admins who are already logged in don't
810
- // have to pass --admin-api-key again.
811
- const adminApiKey = opts.adminApiKey || config.getApiKey();
812
- if (!globalOpts.clear && !adminApiKey) {
813
- output.error('--admin-api-key is required to set up tools (or run `unbound login` first).');
814
- process.exitCode = 1;
815
- return;
816
- }
817
- // Persist URLs first so this MDM run wires tools at the new tenant
818
- // and any subsequent non-MDM command on the same machine inherits.
819
- // Prefer just-persisted values over env-var-aware getters so a stale
820
- // UNBOUND_*_URL can't shadow the explicit --*-url flag.
821
- const written = config.setUrls({
822
- backend: globalOpts.backendUrl,
823
- frontend: globalOpts.frontendUrl,
824
- gateway: globalOpts.gatewayUrl,
825
- });
826
- const backendUrl = written.base_url || config.getBaseUrl();
827
- const frontendUrl = written.frontend_url || config.getFrontendUrl();
828
- const gatewayUrl = written.gateway_url || config.getGatewayUrl();
829
-
830
- if (globalOpts.all && tools.length > 0) {
831
- output.error('Cannot combine --all with specific tool names. Use one or the other.');
832
- process.exitCode = 1;
833
- return;
834
- }
835
-
836
- let toolNames;
837
- if (globalOpts.all) {
838
- // --clear --all wipes every tool, including both Claude Code/Codex modes.
839
- // Setup --all uses the subscription-default bundle (can't enroll both modes).
840
- toolNames = globalOpts.clear ? Object.keys(MDM_TOOLS) : MDM_SETUP_ALL_TOOLS;
841
- } else if (tools.length > 0) {
842
- toolNames = tools;
843
- } else {
844
- output.error('Specify tools to set up, or use --all.');
845
- console.error(' Available: ' + mdmToolNames);
846
- process.exitCode = 1;
847
- return;
848
- }
849
-
850
- // Expand bare claude-code/codex (MDM has no interactive mode prompt):
851
- // --clear removes both modes; setup defaults to subscription, matching --all.
852
- toolNames = [...new Set(toolNames.flatMap(name => {
853
- const mode = MODE_TOOLS[name];
854
- if (!mode) return [name];
855
- return globalOpts.clear ? [mode.subscription, mode.gateway] : [mode.subscription];
856
- }))];
857
-
858
- const invalid = toolNames.filter(t => !MDM_TOOLS[t]);
859
- if (invalid.length > 0) {
860
- output.error(`Unknown tool(s): ${invalid.join(', ')}`);
861
- console.error(' Available: ' + mdmToolNames);
862
- process.exitCode = 1;
863
- return;
864
- }
865
-
866
- // Mode mutual-exclusivity only applies when setting up — clearing both
867
- // modes at once is valid (and is what bare claude-code/codex --clear does).
868
- if (!globalOpts.clear) {
869
- if (toolNames.includes('claude-code-subscription') && toolNames.includes('claude-code-gateway')) {
870
- output.error('Cannot use both claude-code-subscription and claude-code-gateway. Choose one.');
871
- process.exitCode = 1;
872
- return;
873
- }
874
-
875
- if (toolNames.includes('codex-subscription') && toolNames.includes('codex-gateway')) {
876
- output.error('Cannot use both codex-subscription and codex-gateway. Choose one.');
877
- process.exitCode = 1;
878
- return;
879
- }
880
- }
881
-
882
- const resolvedTools = toolNames.map(name => ({ name, ...MDM_TOOLS[name] }));
883
- console.log('');
884
-
885
- if (globalOpts.backfill) {
886
- for (const tool of resolvedTools) {
887
- if (!scriptSupportsBackfill(tool.script)) noteBackfillUnsupported(tool.label, tool.script);
888
- }
889
- }
890
-
891
- const { ok } = await runBatch(
892
- resolvedTools,
893
- (tool) => {
894
- const toolArgs = buildScriptArgs(adminApiKey, {
895
- backendUrl,
896
- frontendUrl,
897
- gatewayUrl,
898
- clear: globalOpts.clear,
899
- mdm: true,
900
- backfill: globalOpts.backfill && scriptSupportsBackfill(tool.script),
901
- });
902
- return runScriptPiped(tool.script, toolArgs);
903
- },
904
- { clear: globalOpts.clear, summary: globalOpts.clear ? 'All tools cleared' : 'All tools configured' }
905
- );
906
- if (!ok) return;
907
- } catch (err) {
908
- output.error(err.message);
909
- process.exitCode = 1;
910
- }
911
- });
912
-
913
839
  // --- Full uninstall ---
914
840
 
915
841
  program
@@ -1054,7 +980,7 @@ module.exports = {
1054
980
  register,
1055
981
  runSetupAllBundle,
1056
982
  runMdmSetupAllBundle,
1057
- checkRoot,
983
+ hasRootPrivileges,
1058
984
  ALL_TOOLS,
1059
985
  MDM_ALL_TOOLS,
1060
986
  buildScriptArgs,
@@ -2,21 +2,36 @@ const config = require('../config');
2
2
  const api = require('../api');
3
3
  const output = require('../output');
4
4
  const { getDeviceSerial } = require('../device-serial');
5
+ const { detectTools } = require('../toolHealth');
6
+
7
+ function roleFromPrivileges(p) {
8
+ if (!p) return null;
9
+ if (p.is_admin) return 'Admin';
10
+ if (p.is_manager) return 'Manager';
11
+ if (p.is_member) return 'Member';
12
+ return 'Unknown';
13
+ }
14
+
5
15
 
6
16
  function register(program) {
7
17
  program
8
18
  .command('status')
9
- .description('Show the current CLI status including config location, login state, and API connectivity. Useful for debugging connection issues.')
19
+ .description('Show the current CLI status: config location, login state, role, connected tools, and API connectivity. Useful for debugging connection issues.')
10
20
  .addHelpText('after', `
11
21
  Output fields:
12
22
  Config file - Path to the config file (~/.unbound/config.json)
13
23
  Logged in - Whether credentials are stored (Yes/No)
14
24
  Email - The authenticated user's email (if logged in)
15
25
  Organization - The organization name (if logged in)
26
+ Role - Admin / Manager / Member (if logged in)
16
27
  Backend URL - REST API host (configurable for tenant deployments)
17
28
  Frontend URL - Browser login host
18
29
  Gateway URL - AI gateway host (used by tool setup)
19
30
  API status - Connectivity check result (Connected / Error)
31
+ Connected tools - AI tools wired through Unbound on this device, with mode
32
+
33
+ For a deep per-tool health check (config, hook script, env wiring), run
34
+ \`unbound doctor\`.
20
35
 
21
36
  Examples:
22
37
  $ unbound status
@@ -32,8 +47,8 @@ Examples:
32
47
  ['Logged in', loggedIn ? 'Yes' : 'No'],
33
48
  ];
34
49
 
35
- // Check API connectivity
36
50
  let connectivity = 'Not checked (not logged in)';
51
+ let role = null;
37
52
  if (loggedIn) {
38
53
  const spin = output.spinner('Checking API connectivity...');
39
54
  try {
@@ -42,6 +57,7 @@ Examples:
42
57
  query: { device_serial: deviceSerial },
43
58
  });
44
59
  config.backfillUserInfo(privileges);
60
+ role = roleFromPrivileges(privileges);
45
61
  spin.stop();
46
62
  connectivity = 'Connected';
47
63
  } catch (err) {
@@ -52,12 +68,17 @@ Examples:
52
68
  const cfg = config.readConfig();
53
69
  pairs.push(['Email', cfg.email || '-']);
54
70
  pairs.push(['Organization', cfg.org_name || '-']);
71
+ pairs.push(['Role', role || '-']);
55
72
  }
56
73
  pairs.push(['Backend URL', config.getBaseUrl()]);
57
74
  pairs.push(['Frontend URL', config.getFrontendUrl()]);
58
75
  pairs.push(['Gateway URL', config.getGatewayUrl()]);
59
76
  pairs.push(['API status', connectivity]);
60
77
 
78
+ // Locally detected AI tools wired through Unbound, with their mode.
79
+ const connected = detectTools({ gatewayUrl: config.getGatewayUrl(), apiKey: config.getApiKey() })
80
+ .filter((t) => t.status !== 'not-installed');
81
+
61
82
  if (opts.json) {
62
83
  const cfg = loggedIn ? config.readConfig() : {};
63
84
  output.json({
@@ -65,15 +86,34 @@ Examples:
65
86
  logged_in: loggedIn,
66
87
  email: loggedIn ? (cfg.email || null) : null,
67
88
  organization: loggedIn ? (cfg.org_name || null) : null,
89
+ role: role,
68
90
  backend_url: config.getBaseUrl(),
69
91
  frontend_url: config.getFrontendUrl(),
70
92
  gateway_url: config.getGatewayUrl(),
71
93
  api_status: connectivity,
94
+ connected_tools: connected.map((t) => ({ tool: t.key, label: t.label, mode: t.mode, status: t.status })),
72
95
  });
73
96
  return;
74
97
  }
75
98
 
76
99
  output.keyValue(pairs);
100
+
101
+ console.log('');
102
+ output.info('Connected tools');
103
+ const C = output.colors;
104
+ if (connected.length === 0) {
105
+ console.log(` ${C.dim('None set up yet.')} Run ${C.bold('unbound setup')} to wire a tool.`);
106
+ } else {
107
+ for (const t of connected) {
108
+ const mode = t.mode ? C.dim(` (${t.mode})`) : '';
109
+ let mark = C.green('✓');
110
+ let note = '';
111
+ if (t.status === 'tampered') { mark = C.red('✗'); note = C.dim(' — run `unbound doctor`'); }
112
+ else if (t.status === 'managed-by-mdm') { note = C.dim(' (managed by MDM)'); }
113
+ console.log(` ${mark} ${t.label}${mode}${note}`);
114
+ }
115
+ }
116
+ console.log('');
77
117
  } catch (err) {
78
118
  output.error(err.message);
79
119
  process.exitCode = 1;
package/src/config.js CHANGED
@@ -141,7 +141,7 @@ function isLoggedIn() {
141
141
  * Refreshes cached user identity (email, org_name) from a backend response.
142
142
  * Always overwrites when the response carries a non-empty value so that
143
143
  * switching tenants under the same API key (or rotating the key to a new org)
144
- * shows the correct organization in `whoami` / `status` instead of stale data.
144
+ * shows the correct organization in `status` instead of stale data.
145
145
  * Defensive: leaves an existing cached value untouched if the response field
146
146
  * is missing or empty, so a partial API response can't blank the local config.
147
147
  */
package/src/index.js CHANGED
@@ -29,8 +29,8 @@ AUTHENTICATION
29
29
  $ unbound login --api-key <key> Sign in with an API key (for CI/CD)
30
30
  $ unbound login --domain custom.co Sign in via custom domain
31
31
  $ unbound logout Remove stored credentials
32
- $ unbound whoami Show current user and organization
33
- $ unbound status Show CLI status and API connectivity
32
+ $ unbound status Show CLI status, role, and connected tools
33
+ $ unbound doctor Diagnose per-tool health and API key
34
34
 
35
35
  Tenant deployments — pass URL flags on login; they persist to ~/.unbound/config.json:
36
36
  $ unbound login --api-key <YOUR_API_KEY> \\
@@ -44,9 +44,9 @@ AUTHENTICATION
44
44
  Or set URLs separately (any time):
45
45
  $ unbound config urls <gateway-url> <frontend-url> <backend-url>
46
46
 
47
- ONBOARDING (one-step install + discover)
48
- $ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY>
49
- $ sudo unbound onboard-mdm --admin-api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY>
47
+ ONBOARDING (one-step install + discover; scope auto-detected from sudo)
48
+ $ unbound onboard --api-key <USER_KEY> --discovery-key <DISCOVERY_KEY> Current user
49
+ $ sudo unbound onboard --api-key <ADMIN_KEY> --discovery-key <DISCOVERY_KEY> All users (MDM)
50
50
 
51
51
  TOOL SETUP
52
52
  $ unbound setup Select and install multiple tools interactively
@@ -85,11 +85,11 @@ TOOL SETUP
85
85
  $ sudo unbound nuke Wipe everything on the device (MDM + user)
86
86
  $ unbound nuke Wipe just your tools + credentials (no sudo)
87
87
 
88
- MDM SETUP (admin, requires root)
89
- $ sudo unbound setup mdm --admin-api-key KEY --all
90
- $ sudo unbound setup mdm --admin-api-key KEY cursor copilot codex-subscription
91
- $ sudo unbound setup mdm --admin-api-key KEY claude-code-subscription codex-subscription gemini-cli
92
- $ sudo unbound setup mdm --clear cursor codex-subscription (no API key needed to clear)
88
+ MDM SETUP (all users on the device — run setup with sudo)
89
+ $ sudo unbound setup --all --api-key KEY
90
+ $ sudo unbound setup cursor copilot codex-subscription --api-key KEY
91
+ $ sudo unbound setup claude-code-subscription codex-subscription gemini-cli --api-key KEY
92
+ $ sudo unbound setup --clear cursor codex-subscription (no API key needed to clear)
93
93
 
94
94
  MDM AI TOOLS DISCOVERY
95
95
  --domain defaults to the configured backend URL (set via "unbound config set-backend-url")
@@ -178,8 +178,8 @@ LEARN MORE
178
178
  // Register all command modules
179
179
  require('./commands/login').register(program);
180
180
  require('./commands/logout').register(program);
181
- require('./commands/whoami').register(program);
182
181
  require('./commands/status').register(program);
182
+ require('./commands/doctor').register(program);
183
183
  require('./commands/policy').register(program);
184
184
  require('./commands/users').register(program);
185
185
  require('./commands/user-groups').register(program);
@@ -241,7 +241,7 @@ Use this on a fresh install for tenant deployments. Positional order is fixed:
241
241
  2. <frontend-url> — Frontend host (e.g. https://gateway.acme.com)
242
242
  Used by the browser login flow.
243
243
  3. <backend-url> — REST API host (e.g. https://backend.acme.com)
244
- All "unbound *" commands (whoami, status, policies, ...) hit this.
244
+ All "unbound *" commands (status, doctor, policies, ...) hit this.
245
245
 
246
246
  Bare hostnames are accepted; "https://" is added automatically.
247
247
  The three values are written atomically to ~/.unbound/config.json.