@dosu/cli 0.41.0 → 0.43.0-alpha.1

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.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/bin/dosu.js +163 -231
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -54,13 +54,16 @@ dosu setup
54
54
  brew install dosu-ai/dosu/dosu
55
55
  ```
56
56
 
57
- Or tap first:
57
+ The fully qualified name works on every Homebrew version: it taps the repo if needed and, on Homebrew 6.0+, records trust for just the `dosu` formula. To use tapped short names instead:
58
58
 
59
59
  ```bash
60
60
  brew tap dosu-ai/dosu
61
+ brew trust dosu-ai/dosu # Homebrew 6.0+ only — skip on older versions
61
62
  brew install dosu
62
63
  ```
63
64
 
65
+ **Homebrew 6.0+ only** ([tap trust](https://docs.brew.sh/Tap-Trust), since June 2026): third-party taps start untrusted, so short-name installs or upgrades — including on machines that tapped `dosu-ai/dosu` before upgrading Homebrew — fail with `Error: Refusing to load formula dosu-ai/dosu/dosu from untrusted tap dosu-ai/dosu` until a one-time `brew trust dosu-ai/dosu` (the tap also ships [decant](https://github.com/dosu-ai/decant)). Homebrew 5 and earlier has no trust step and needs none. Tap trust is Homebrew's own consent step, separate from the macOS Gatekeeper warning below — Homebrew installs never trigger Gatekeeper.
66
+
64
67
  ### Manual Download
65
68
 
66
69
  Download the appropriate archive from the [Releases](https://github.com/dosu-ai/dosu-cli/releases) page.
package/bin/dosu.js CHANGED
@@ -4759,7 +4759,7 @@ var init_dist4 = __esm(() => {
4759
4759
  function getVersionString() {
4760
4760
  return `v${VERSION}`;
4761
4761
  }
4762
- var VERSION = "0.41.0", INSTALL_CHANNEL = "npm";
4762
+ var VERSION = "0.43.0-alpha.1", INSTALL_CHANNEL = "npm";
4763
4763
 
4764
4764
  // src/debug/logger.ts
4765
4765
  import {
@@ -8744,6 +8744,25 @@ function mcpHeaders(apiKey) {
8744
8744
  }
8745
8745
  return { "X-Dosu-API-Key": apiKey };
8746
8746
  }
8747
+ function mcpRemoteServer(url, apiKey) {
8748
+ const env = {};
8749
+ const headerArgs = Object.entries(mcpHeaders(apiKey)).flatMap(([key, value]) => {
8750
+ const envKey = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
8751
+ env[envKey] = value;
8752
+ return ["--header", `${key}:\${${envKey}}`];
8753
+ });
8754
+ return {
8755
+ args: [
8756
+ "-y",
8757
+ `mcp-remote@${MCP_REMOTE_VERSION}`,
8758
+ url,
8759
+ ...headerArgs,
8760
+ "--transport",
8761
+ "http-only"
8762
+ ],
8763
+ env
8764
+ };
8765
+ }
8747
8766
  function loadJSONConfig(path3) {
8748
8767
  if (!existsSync6(path3))
8749
8768
  return {};
@@ -8845,7 +8864,7 @@ function removeJSONServer(configPath, topKey) {
8845
8864
  }
8846
8865
  saveJSONConfig(configPath, jsonCfg);
8847
8866
  }
8848
- var import_write_file_atomic, writeFileAtomic;
8867
+ var import_write_file_atomic, writeFileAtomic, MCP_REMOTE_VERSION = "0.1.38";
8849
8868
  var init_config_helpers = __esm(() => {
8850
8869
  import_write_file_atomic = __toESM(require_lib(), 1);
8851
8870
  writeFileAtomic = import_write_file_atomic.default;
@@ -10610,7 +10629,7 @@ var MCP_PROVIDER_SLUG = "dosu_mcp";
10610
10629
  // src/mcp/detect.ts
10611
10630
  import { existsSync as existsSync13 } from "node:fs";
10612
10631
  import { homedir as homedir2, platform as platform2 } from "node:os";
10613
- import { join as join12 } from "node:path";
10632
+ import { delimiter, dirname as dirname5, join as join12 } from "node:path";
10614
10633
  function isInstalled(paths) {
10615
10634
  return paths.some((p2) => existsSync13(expandHome(p2)));
10616
10635
  }
@@ -10635,6 +10654,20 @@ function appSupportDir() {
10635
10654
  }
10636
10655
  }
10637
10656
  }
10657
+ function findNpx() {
10658
+ const bin = platform2() === "win32" ? "npx.cmd" : "npx";
10659
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
10660
+ if (!dir)
10661
+ continue;
10662
+ const candidate = join12(dir, bin);
10663
+ if (existsSync13(candidate))
10664
+ return candidate;
10665
+ }
10666
+ throw new Error("npx not found on PATH — Node.js is required (the MCP entry runs `npx mcp-remote`).");
10667
+ }
10668
+ function npxPathEnv(npx) {
10669
+ return [dirname5(npx), "/usr/bin", "/bin"].join(delimiter);
10670
+ }
10638
10671
  var init_detect = () => {};
10639
10672
 
10640
10673
  // src/mcp/providers/base.ts
@@ -10729,6 +10762,9 @@ var init_claude = __esm(() => {
10729
10762
 
10730
10763
  // src/mcp/providers/claude-desktop.ts
10731
10764
  import { join as join14 } from "node:path";
10765
+ function configPath() {
10766
+ return join14(appSupportDir(), "Claude", "claude_desktop_config.json");
10767
+ }
10732
10768
  var ClaudeDesktopProvider = () => ({
10733
10769
  name: () => "Claude Desktop",
10734
10770
  id: () => "claude-desktop",
@@ -10736,16 +10772,31 @@ var ClaudeDesktopProvider = () => ({
10736
10772
  priority: () => 2,
10737
10773
  detectPaths: () => [join14(appSupportDir(), "Claude")],
10738
10774
  isInstalled: () => isInstalled([join14(appSupportDir(), "Claude")]),
10739
- globalConfigPath: () => join14(appSupportDir(), "Claude", "claude_desktop_config.json"),
10740
- isConfigured: () => false,
10741
- install() {
10742
- throw new Error("this tool only supports local (stdio) servers and cannot be configured for remote MCP");
10775
+ globalConfigPath: () => configPath(),
10776
+ isConfigured: () => isJSONKeyConfigured(configPath(), "mcpServers"),
10777
+ install(cfg, global) {
10778
+ if (!global)
10779
+ throw new Error("Claude Desktop does not support local installation");
10780
+ if (cfg.mode !== MODE_OSS && !cfg.active_account?.target?.deployment_id)
10781
+ throw new Error("deployment ID is required");
10782
+ const url = cfg.mode === MODE_OSS ? mcpBaseURL() : mcpURL(cfg.active_account.target.deployment_id);
10783
+ const npx = findNpx();
10784
+ const remote = mcpRemoteServer(url, cfg.active_account?.target?.api_key);
10785
+ installJSONServer(configPath(), "mcpServers", {
10786
+ command: npx,
10787
+ args: remote.args,
10788
+ env: { PATH: npxPathEnv(npx), ...remote.env }
10789
+ });
10743
10790
  },
10744
- remove() {
10745
- throw new Error("this tool only supports local (stdio) servers and cannot be configured for remote MCP");
10791
+ remove(global) {
10792
+ if (!global)
10793
+ throw new Error("Claude Desktop does not support local removal");
10794
+ removeJSONServer(configPath(), "mcpServers");
10746
10795
  }
10747
10796
  });
10748
10797
  var init_claude_desktop = __esm(() => {
10798
+ init_config();
10799
+ init_config_helpers();
10749
10800
  init_detect();
10750
10801
  });
10751
10802
 
@@ -10824,20 +10875,25 @@ function mcpEndpoint(cfg) {
10824
10875
  throw new Error("deployment ID is required");
10825
10876
  return mcpURL(cfg.active_account?.target?.deployment_id);
10826
10877
  }
10878
+ function tomlString(value) {
10879
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
10880
+ }
10827
10881
  function installDosuToTOML(path3, cfg) {
10828
10882
  let content = readTOML(path3);
10829
10883
  content = removeDosuFromTOML(content);
10830
- const url = mcpEndpoint(cfg);
10831
- const headers = mcpHeaders(cfg.active_account?.target?.api_key);
10832
- const headerEntries = Object.entries(headers).map(([k, v]) => `${k} = "${v}"`).join(`
10884
+ const npx = findNpx();
10885
+ const remote = mcpRemoteServer(mcpEndpoint(cfg), cfg.active_account?.target?.api_key);
10886
+ const env = { PATH: npxPathEnv(npx), ...remote.env };
10887
+ const envEntries = Object.entries(env).map(([key, value]) => `${key} = ${tomlString(value)}`).join(`
10833
10888
  `);
10889
+ const args = remote.args.map(tomlString).join(", ");
10834
10890
  const section = `
10835
10891
  [mcp_servers.dosu]
10836
- type = "http"
10837
- url = "${url}"
10838
-
10839
- [mcp_servers.dosu.http_headers]
10840
- ${headerEntries}
10892
+ command = ${tomlString(npx)}
10893
+ args = [${args}]
10894
+ ` + `
10895
+ [mcp_servers.dosu.env]
10896
+ ${envEntries}
10841
10897
  `;
10842
10898
  content += section;
10843
10899
  writeTOML(path3, content);
@@ -10864,7 +10920,7 @@ function removeDosuFromTOML(content) {
10864
10920
  `);
10865
10921
  }
10866
10922
  var CodexProvider = () => ({
10867
- name: () => "Codex CLI",
10923
+ name: () => "Codex (CLI + Desktop)",
10868
10924
  id: () => "codex",
10869
10925
  supportsLocal: () => true,
10870
10926
  priority: () => 8,
@@ -10928,21 +10984,21 @@ var CopilotProvider = () => ({
10928
10984
  };
10929
10985
  installJSONServer(globalPath(), "mcpServers", server);
10930
10986
  } else {
10931
- const configPath = join18(process.cwd(), ".vscode", "mcp.json");
10987
+ const configPath2 = join18(process.cwd(), ".vscode", "mcp.json");
10932
10988
  const server = {
10933
10989
  type: "http",
10934
10990
  url,
10935
10991
  headers: mcpHeaders(cfg.active_account.target.api_key)
10936
10992
  };
10937
- installJSONServer(configPath, "servers", server);
10993
+ installJSONServer(configPath2, "servers", server);
10938
10994
  }
10939
10995
  },
10940
10996
  remove(global) {
10941
10997
  if (global) {
10942
10998
  removeJSONServer(globalPath(), "mcpServers");
10943
10999
  } else {
10944
- const configPath = join18(process.cwd(), ".vscode", "mcp.json");
10945
- removeJSONServer(configPath, "servers");
11000
+ const configPath2 = join18(process.cwd(), ".vscode", "mcp.json");
11001
+ removeJSONServer(configPath2, "servers");
10946
11002
  }
10947
11003
  }
10948
11004
  });
@@ -11077,17 +11133,17 @@ var MCPorterProvider = () => ({
11077
11133
  globalConfigPath: () => resolveGlobalConfigPath(),
11078
11134
  isConfigured: () => isJSONKeyConfigured(resolveGlobalConfigPath(), "mcpServers"),
11079
11135
  install(cfg, global) {
11080
- const configPath = global ? resolveGlobalConfigPath() : join22(process.cwd(), "config", "mcporter.json");
11136
+ const configPath2 = global ? resolveGlobalConfigPath() : join22(process.cwd(), "config", "mcporter.json");
11081
11137
  const server = {
11082
11138
  type: "http",
11083
11139
  url: mcpEndpoint4(cfg),
11084
11140
  headers: mcpHeaders(cfg.active_account.target.api_key)
11085
11141
  };
11086
- installJSONServer(configPath, "mcpServers", server);
11142
+ installJSONServer(configPath2, "mcpServers", server);
11087
11143
  },
11088
11144
  remove(global) {
11089
- const configPath = global ? resolveGlobalConfigPath() : join22(process.cwd(), "config", "mcporter.json");
11090
- removeJSONServer(configPath, "mcpServers");
11145
+ const configPath2 = global ? resolveGlobalConfigPath() : join22(process.cwd(), "config", "mcporter.json");
11146
+ removeJSONServer(configPath2, "mcpServers");
11091
11147
  }
11092
11148
  });
11093
11149
  var init_mcporter = __esm(() => {
@@ -11236,7 +11292,7 @@ var init_providers = __esm(() => {
11236
11292
  // src/rules/installer.ts
11237
11293
  import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync14, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
11238
11294
  import { homedir as homedir4 } from "node:os";
11239
- import { dirname as dirname5, join as join27 } from "node:path";
11295
+ import { dirname as dirname6, join as join27 } from "node:path";
11240
11296
  function claudeConfigDir() {
11241
11297
  return process.env.CLAUDE_CONFIG_DIR || join27(homedir4(), ".claude");
11242
11298
  }
@@ -11267,7 +11323,7 @@ async function fetchDosuRule(fetchImpl = globalThis.fetch) {
11267
11323
  return normalizeRule(FALLBACK_DOSU_RULE);
11268
11324
  }
11269
11325
  function ensureParent(path3) {
11270
- mkdirSync8(dirname5(path3), { recursive: true });
11326
+ mkdirSync8(dirname6(path3), { recursive: true });
11271
11327
  }
11272
11328
  function writeIfChanged(path3, content) {
11273
11329
  if (!existsSync16(path3)) {
@@ -11802,46 +11858,9 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background:
11802
11858
  </body>
11803
11859
  </html>`;
11804
11860
  }
11805
- function buildNextPollScript(settleMsg) {
11806
- return `<script>
11807
- (function () {
11808
- var tries = 0;
11809
- var misses = 0;
11810
- var setCopy = function (heading, msg) {
11811
- var h = document.getElementById('heading');
11812
- var m = document.getElementById('close-msg');
11813
- if (h && heading) h.textContent = heading;
11814
- if (m && msg) m.textContent = msg;
11815
- };
11816
- var settle = function () { setCopy(null, ${JSON.stringify(settleMsg)}); };
11817
- var timer = setInterval(function () {
11818
- tries += 1;
11819
- if (tries > 720) { clearInterval(timer); settle(); return; }
11820
- fetch('/next').then(function (res) {
11821
- misses = 0;
11822
- if (res.status === 200) {
11823
- clearInterval(timer);
11824
- res.json().then(function (body) {
11825
- if (body && body.url) {
11826
- setCopy("You're ready to connect your data sources", 'Redirecting...');
11827
- window.location.replace(body.url);
11828
- }
11829
- });
11830
- } else if (res.status === 410) {
11831
- clearInterval(timer);
11832
- settle();
11833
- }
11834
- }).catch(function () {
11835
- misses += 1;
11836
- if (misses >= 8) { clearInterval(timer); settle(); }
11837
- });
11838
- }, 250);
11839
- })();
11840
- </script>`;
11841
- }
11842
- function buildSuccessHtml(email, variant = "auth", withNextPoll = false) {
11861
+ function buildSuccessHtml(email, variant = "auth") {
11843
11862
  const copy2 = SUCCESS_COPY[variant];
11844
- const closeMsg = withNextPoll ? "Hang tight — this tab will continue automatically." : copy2.close;
11863
+ const closeMsg = copy2.close;
11845
11864
  const emailLine = email ? `<p class="email">Signed in as <strong>${email.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")}</strong></p>` : "";
11846
11865
  return `<!DOCTYPE html>
11847
11866
  <html lang="en">
@@ -11959,7 +11978,6 @@ h1 {
11959
11978
  <span class="tip-label">Did you know?</span>
11960
11979
  You can use Dosu to make your coding agents faster and cheaper. Just ask your agent to use Dosu to update your AGENTS.md.
11961
11980
  </div>
11962
- ${withNextPoll ? buildNextPollScript(copy2.close) : ""}
11963
11981
  </body>
11964
11982
  </html>`;
11965
11983
  }
@@ -11970,27 +11988,12 @@ async function startCallbackServer(opts = {}) {
11970
11988
  resolveToken = resolve;
11971
11989
  rejectToken = reject;
11972
11990
  });
11973
- let nextTarget;
11974
11991
  const http = __require("node:http");
11975
11992
  const httpServer = http.createServer((req, res) => {
11976
11993
  const url = new URL(req.url ?? "/", `http://localhost`);
11977
11994
  const cookieLen = (req.headers.cookie ?? "").length;
11978
11995
  const ua = req.headers["user-agent"] ?? "none";
11979
11996
  logger.debug("auth.server", `Request: ${req.method} ${url.pathname} cookie-len=${cookieLen} ua=${ua} has-token=${url.searchParams.has("access_token")}`);
11980
- if (opts.nextHold && url.pathname === "/next") {
11981
- if (nextTarget === undefined) {
11982
- res.writeHead(204);
11983
- res.end();
11984
- } else if (nextTarget === null) {
11985
- res.writeHead(410);
11986
- res.end();
11987
- } else {
11988
- logger.info("auth.server", "Handing tab onward via /next");
11989
- res.writeHead(200, { "Content-Type": "application/json" });
11990
- res.end(JSON.stringify({ url: nextTarget }));
11991
- }
11992
- return;
11993
- }
11994
11997
  if (url.pathname !== "/callback") {
11995
11998
  logger.debug("auth.server", `404: ${url.pathname}`);
11996
11999
  res.writeHead(404, { "Content-Type": "text/plain" });
@@ -12038,7 +12041,7 @@ async function startCallbackServer(opts = {}) {
12038
12041
  });
12039
12042
  logger.info("auth.server", "Served success HTML");
12040
12043
  res.writeHead(200, { "Content-Type": "text/html" });
12041
- res.end(buildSuccessHtml(email ?? undefined, opts.successVariant ?? "auth", Boolean(opts.nextHold)));
12044
+ res.end(buildSuccessHtml(email ?? undefined, opts.successVariant ?? "auth"));
12042
12045
  });
12043
12046
  await new Promise((resolve) => {
12044
12047
  httpServer.listen(0, "localhost", () => resolve());
@@ -12052,9 +12055,6 @@ async function startCallbackServer(opts = {}) {
12052
12055
  close: () => {
12053
12056
  httpServer.close();
12054
12057
  httpServer.closeAllConnections?.();
12055
- },
12056
- setNext: (url) => {
12057
- nextTarget = url;
12058
12058
  }
12059
12059
  },
12060
12060
  tokenPromise
@@ -12149,11 +12149,9 @@ __export(exports_flow, {
12149
12149
  });
12150
12150
  async function startOAuthFlow(signal, path3 = "/cli/auth", params = {}, onAuthURL, options = {}) {
12151
12151
  const { server, tokenPromise } = await startCallbackServer({
12152
- nextHold: options.holdNext,
12153
12152
  successVariant: options.successVariant
12154
12153
  });
12155
12154
  let timeoutId;
12156
- let handedOver = false;
12157
12155
  try {
12158
12156
  const callbackURL = `http://localhost:${server.port}/callback`;
12159
12157
  logger.debug("auth.flow", `Callback URL: ${callbackURL}`);
@@ -12161,29 +12159,26 @@ async function startOAuthFlow(signal, path3 = "/cli/auth", params = {}, onAuthUR
12161
12159
  logger.info("auth.flow", `Auth URL: ${authURL}`);
12162
12160
  options.onAuthURL?.(authURL);
12163
12161
  let browserOpened = false;
12164
- if (options.suppressBrowserOpen) {
12162
+ const open2 = await Promise.resolve().then(() => (init_open(), exports_open));
12163
+ try {
12164
+ await open2.default(authURL);
12165
12165
  browserOpened = true;
12166
- logger.info("auth.flow", "Browser open suppressed — caller steers an existing tab");
12167
- } else {
12168
- const open2 = await Promise.resolve().then(() => (init_open(), exports_open));
12169
- try {
12170
- await open2.default(authURL);
12171
- browserOpened = true;
12172
- logger.info("auth.flow", "Browser open command executed");
12173
- onAuthURL?.(authURL);
12174
- } catch (openErr) {
12175
- logger.warn("auth.flow", `Could not open browser automatically: ${openErr instanceof Error ? openErr.message : String(openErr)}`);
12176
- }
12166
+ logger.info("auth.flow", "Browser open command executed");
12167
+ onAuthURL?.(authURL);
12168
+ } catch (openErr) {
12169
+ logger.warn("auth.flow", `Could not open browser automatically: ${openErr instanceof Error ? openErr.message : String(openErr)}`);
12177
12170
  }
12178
12171
  if (!browserOpened && !options.waitWithoutBrowser) {
12179
12172
  logger.debug("auth.flow", "Browser unavailable — returning to caller for fallback");
12180
12173
  return { browserOpened: false };
12181
12174
  }
12175
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12176
+ const timeoutMinutes = Math.round(timeoutMs / 60000);
12182
12177
  const timeout = new Promise((_2, reject) => {
12183
12178
  timeoutId = setTimeout(() => {
12184
- logger.warn("auth.flow", "Authentication timed out (8min)");
12185
- reject(new Error("Authentication did not complete within 8 minutes. The OAuth state may have expired — please run `dosu login` again."));
12186
- }, 480000);
12179
+ logger.warn("auth.flow", `Authentication timed out (${timeoutMinutes}min)`);
12180
+ reject(new Error(`Authentication did not complete within ${timeoutMinutes} minutes. If you finished in the browser after the timeout, just re-run the command.`));
12181
+ }, timeoutMs);
12187
12182
  });
12188
12183
  const abort = signal ? new Promise((_2, reject) => {
12189
12184
  signal.addEventListener("abort", () => {
@@ -12193,17 +12188,11 @@ async function startOAuthFlow(signal, path3 = "/cli/auth", params = {}, onAuthUR
12193
12188
  }) : new Promise(() => {});
12194
12189
  const token = await Promise.race([tokenPromise, timeout, abort]);
12195
12190
  logger.info("auth.flow", "Token received");
12196
- if (options.holdNext) {
12197
- handedOver = true;
12198
- return { browserOpened: true, token, server };
12199
- }
12200
12191
  return { browserOpened: true, token };
12201
12192
  } finally {
12202
12193
  clearTimeout(timeoutId);
12203
- if (!handedOver) {
12204
- server.close();
12205
- logger.debug("auth.flow", "Cleaning up: timeout cleared, server closed");
12206
- }
12194
+ server.close();
12195
+ logger.debug("auth.flow", "Cleaning up: timeout cleared, server closed");
12207
12196
  }
12208
12197
  }
12209
12198
  function buildAuthURL(callbackURL, path3, extraParams) {
@@ -12211,9 +12200,11 @@ function buildAuthURL(callbackURL, path3, extraParams) {
12211
12200
  const params = new URLSearchParams({ callback: callbackURL, ...extraParams });
12212
12201
  return `${webAppURL}${path3}?${params}`;
12213
12202
  }
12203
+ var DEFAULT_TIMEOUT_MS;
12214
12204
  var init_flow = __esm(() => {
12215
12205
  init_logger();
12216
12206
  init_server();
12207
+ DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
12217
12208
  });
12218
12209
 
12219
12210
  // src/setup/flow.ts
@@ -12223,8 +12214,7 @@ __export(exports_flow2, {
12223
12214
  stepDetectTools: () => stepDetectTools,
12224
12215
  stepConfigureTools: () => stepConfigureTools,
12225
12216
  runSetup: () => runSetup,
12226
- runInstallSkill: () => runInstallSkill,
12227
- isStdioOnly: () => isStdioOnly
12217
+ runInstallSkill: () => runInstallSkill
12228
12218
  });
12229
12219
  import { randomUUID } from "node:crypto";
12230
12220
  async function runSetup(opts = {}) {
@@ -12245,11 +12235,6 @@ async function runSetup(opts = {}) {
12245
12235
  if (!authed)
12246
12236
  return;
12247
12237
  cfg = authed.cfg;
12248
- const authTab = authed.authTab;
12249
- const releaseAuthTab = () => {
12250
- authTab?.setNext(null);
12251
- authTab?.close();
12252
- };
12253
12238
  await trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_auth_completed");
12254
12239
  let apiClient = new Client(cfg);
12255
12240
  let cloudSetupContext = null;
@@ -12262,36 +12247,41 @@ async function runSetup(opts = {}) {
12262
12247
  reason: "cloud_setup_context_failed"
12263
12248
  });
12264
12249
  s.stop("Workspace load failed");
12265
- releaseAuthTab();
12266
12250
  return;
12267
12251
  }
12268
12252
  s.stop("Workspace loaded");
12269
12253
  }
12270
12254
  await trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_started", {
12271
- flow_kind: cloudSetupContext?.kind ?? "oss"
12255
+ flow_kind: cloudSetupContext ? cloudSetupContext.finishedOnboarding ? "setup" : "onboarding" : "oss"
12272
12256
  });
12273
- const firstRunOnboarding = cfg.mode !== MODE_OSS && cloudSetupContext?.kind === "onboarding" && !opts.deploymentID;
12274
- if (!firstRunOnboarding) {
12275
- releaseAuthTab();
12276
- }
12277
- if (firstRunOnboarding && cloudSetupContext) {
12278
- const onboarded = await stepWebOnboarding(cfg, onboardingRunID, authTab);
12279
- if (!onboarded) {
12257
+ const needsHandshake = cfg.mode !== MODE_OSS && cloudSetupContext !== null && !cloudSetupContext.finishedOnboarding && !opts.deploymentID;
12258
+ if (needsHandshake) {
12259
+ const handshook = await stepSetupHandshake(cfg, onboardingRunID);
12260
+ if (!handshook) {
12280
12261
  await trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_failed", {
12281
12262
  reason: "web_onboarding_incomplete"
12282
12263
  });
12283
12264
  return;
12284
12265
  }
12285
12266
  apiClient = new Client(cfg);
12286
- const targetOrg = await resolveCurrentOnboardingTargetOrg(cfg);
12287
- const ok = await bindOnboardingDeployment(apiClient, cfg, targetOrg);
12288
- if (!ok) {
12267
+ const refreshed = await resolveCloudSetupContext(cfg);
12268
+ if (refreshed === null) {
12289
12269
  await trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_failed", {
12290
- reason: "onboarding_deployment_failed"
12270
+ reason: "cloud_setup_context_failed"
12291
12271
  });
12292
12272
  return;
12293
12273
  }
12294
- } else if (!cfg.active_account?.target?.deployment_id || opts.deploymentID) {
12274
+ if (!refreshed.finishedOnboarding) {
12275
+ log.warn("Your account still needs onboarding. Finish it in the browser at the Dosu app, then re-run `dosu setup`.");
12276
+ await trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_failed", {
12277
+ reason: "onboarding_incomplete_after_handshake"
12278
+ });
12279
+ process.exitCode = 1;
12280
+ return;
12281
+ }
12282
+ cloudSetupContext = refreshed;
12283
+ }
12284
+ if (!cfg.active_account?.target?.deployment_id || opts.deploymentID) {
12295
12285
  const ok = await resolveDeployment(apiClient, cfg, opts);
12296
12286
  if (!ok) {
12297
12287
  await trackCliOnboardingEvent(cfg, onboardingRunID, "cli_onboarding_failed", {
@@ -12466,9 +12456,13 @@ async function openBrowserForSetup(cfg, onboardingRunID) {
12466
12456
  try {
12467
12457
  const { startOAuthFlow: startOAuthFlow2 } = await Promise.resolve().then(() => (init_flow(), exports_flow));
12468
12458
  const s = spinner();
12469
- const result = await startOAuthFlow2(undefined, "/cli/auth", onboardingRunID ? { onboarding_run_id: onboardingRunID } : {}, undefined, {
12459
+ const isCloud = cfg.mode !== MODE_OSS;
12460
+ const result = await startOAuthFlow2(undefined, "/cli/auth", {
12461
+ ...isCloud ? { intent: "setup" } : {},
12462
+ ...onboardingRunID ? { onboarding_run_id: onboardingRunID } : {}
12463
+ }, undefined, {
12470
12464
  waitWithoutBrowser: true,
12471
- holdNext: true,
12465
+ ...isCloud ? { timeoutMs: SETUP_HANDSHAKE_TIMEOUT_MS } : {},
12472
12466
  onAuthURL: (url) => {
12473
12467
  log.message(browserFallbackHint(url));
12474
12468
  s.start("Waiting for authentication...");
@@ -12479,7 +12473,7 @@ async function openBrowserForSetup(cfg, onboardingRunID) {
12479
12473
  return null;
12480
12474
  }
12481
12475
  const token = result.token;
12482
- s.stop("Authenticated");
12476
+ s.stop(token.email ? `Authenticated as ${token.email}` : "Authenticated");
12483
12477
  logger.info("setup", "Browser auth completed");
12484
12478
  replaceLoginSession(cfg, {
12485
12479
  access_token: token.access_token,
@@ -12487,7 +12481,7 @@ async function openBrowserForSetup(cfg, onboardingRunID) {
12487
12481
  expires_at: Math.floor(Date.now() / 1000) + token.expires_in
12488
12482
  });
12489
12483
  saveConfig(cfg);
12490
- return { cfg, authTab: result.server };
12484
+ return { cfg };
12491
12485
  } catch (err) {
12492
12486
  const msg = err instanceof Error ? err.stack ?? err.message : String(err);
12493
12487
  logger.error("setup", `Auth failed: ${msg}`);
@@ -12510,20 +12504,19 @@ async function openBrowserForSetup(cfg, onboardingRunID) {
12510
12504
  return null;
12511
12505
  }
12512
12506
  }
12513
- async function stepWebOnboarding(cfg, onboardingRunID, authTab) {
12514
- logger.info("setup", "Step: web onboarding handoff");
12515
- log.info("Almost there — connect your repos in the browser and we'll pick up from here.");
12507
+ async function stepSetupHandshake(cfg, onboardingRunID) {
12508
+ logger.info("setup", "Step: setup handshake");
12509
+ log.info("Almost there — finish setting up in the browser and we'll pick up from here.");
12516
12510
  const s = spinner();
12517
12511
  try {
12518
12512
  const { startOAuthFlow: startOAuthFlow2 } = await Promise.resolve().then(() => (init_flow(), exports_flow));
12519
- const result = await startOAuthFlow2(undefined, "/onboarding/connections", { source: "cli", onboarding_run_id: onboardingRunID }, undefined, {
12513
+ const result = await startOAuthFlow2(undefined, "/cli/auth", { intent: "setup", onboarding_run_id: onboardingRunID }, undefined, {
12520
12514
  waitWithoutBrowser: true,
12521
- suppressBrowserOpen: Boolean(authTab),
12522
12515
  successVariant: "onboarding",
12516
+ timeoutMs: SETUP_HANDSHAKE_TIMEOUT_MS,
12523
12517
  onAuthURL: (url) => {
12524
- authTab?.setNext(url);
12525
12518
  log.message(browserFallbackHint(url));
12526
- s.start("Waiting for onboarding to finish in the browser...");
12519
+ s.start("Waiting for the browser...");
12527
12520
  }
12528
12521
  });
12529
12522
  if (!result.browserOpened) {
@@ -12536,17 +12529,20 @@ async function stepWebOnboarding(cfg, onboardingRunID, authTab) {
12536
12529
  expires_at: Math.floor(Date.now() / 1000) + result.token.expires_in
12537
12530
  });
12538
12531
  saveConfig(cfg);
12539
- s.stop("Onboarding finished in the browser");
12540
- logger.info("setup", "Web onboarding handoff completed");
12532
+ s.stop(result.token.email ? `Authenticated as ${result.token.email}` : "Browser setup finished");
12533
+ logger.info("setup", "Setup handshake completed");
12541
12534
  return true;
12542
12535
  } catch (err) {
12543
12536
  const msg = err instanceof Error ? err.message : String(err);
12544
- logger.warn("setup", `Web onboarding handoff did not complete: ${msg}`);
12545
- s.stop("Onboarding not completed");
12546
- log.warn("Didn't hear back from the browser. Finish onboarding there, then re-run `dosu setup`.");
12537
+ logger.warn("setup", `Setup handshake did not complete: ${msg}`);
12538
+ s.stop("Setup not completed");
12539
+ const { OAuthCallbackError: OAuthCallbackError2 } = await Promise.resolve().then(() => (init_errors(), exports_errors));
12540
+ if (err instanceof OAuthCallbackError2) {
12541
+ log.error(err.userMessage);
12542
+ return false;
12543
+ }
12544
+ log.warn("Didn't hear back from the browser. If it opened the Dosu app instead of the setup " + "wizard, your CLI and browser may be signed in to different accounts — run " + "`dosu logout`, then retry. Otherwise finish in the browser and re-run `dosu setup`.");
12547
12545
  return false;
12548
- } finally {
12549
- authTab?.close();
12550
12546
  }
12551
12547
  }
12552
12548
  async function resolveCloudSetupContext(cfg) {
@@ -12560,22 +12556,9 @@ async function resolveCloudSetupContext(cfg) {
12560
12556
  }
12561
12557
  bindAccountIdentity(cfg, profile.user_id);
12562
12558
  saveConfig(cfg);
12563
- if (profile.finished_onboarding === true) {
12564
- return {
12565
- kind: "setup",
12566
- profileUserID: profile.user_id
12567
- };
12568
- }
12569
- const targetOrg = await resolveOnboardingTargetOrg(trpc);
12570
- if (!targetOrg) {
12571
- log.error("Could not determine your onboarding organization.");
12572
- return null;
12573
- }
12574
- logger.info("setup", `First-run onboarding detected for org ${targetOrg.org_id}`);
12575
12559
  return {
12576
- kind: "onboarding",
12577
12560
  profileUserID: profile.user_id,
12578
- targetOrg
12561
+ finishedOnboarding: profile.finished_onboarding === true
12579
12562
  };
12580
12563
  } catch (err) {
12581
12564
  const msg = err instanceof Error ? err.message : String(err);
@@ -12584,53 +12567,6 @@ async function resolveCloudSetupContext(cfg) {
12584
12567
  return null;
12585
12568
  }
12586
12569
  }
12587
- async function resolveOnboardingTargetOrg(trpc) {
12588
- const accessibleOrgs = await trpc.organization.getOrganizations.query();
12589
- const ownerOrg = accessibleOrgs.find((org) => org.user_role === "OWNER");
12590
- if (ownerOrg) {
12591
- return ownerOrg;
12592
- }
12593
- return accessibleOrgs[0] ?? null;
12594
- }
12595
- async function resolveCurrentOnboardingTargetOrg(cfg) {
12596
- try {
12597
- const { createTypedClient: createTypedClient2 } = await Promise.resolve().then(() => (init_trpc(), exports_trpc));
12598
- const trpc = createTypedClient2(cfg);
12599
- const profile = await trpc.user.getCliOnboardingContext.query();
12600
- if (!profile?.user_id)
12601
- return null;
12602
- bindAccountIdentity(cfg, profile.user_id);
12603
- saveConfig(cfg);
12604
- return await resolveOnboardingTargetOrg(trpc);
12605
- } catch (err) {
12606
- const msg = err instanceof Error ? err.message : String(err);
12607
- logger.error("setup", `Failed to resolve post-onboarding organization: ${msg}`);
12608
- log.error(`Could not determine your onboarding organization: ${msg}`);
12609
- return null;
12610
- }
12611
- }
12612
- async function bindOnboardingDeployment(apiClient, cfg, targetOrg) {
12613
- if (!targetOrg) {
12614
- log.error("Could not determine your onboarding organization.");
12615
- return false;
12616
- }
12617
- const deployment = await resolveOnboardingDeployment(apiClient, targetOrg);
12618
- if (!deployment) {
12619
- log.error(`No MCP found for ${targetOrg.name}.`);
12620
- return false;
12621
- }
12622
- cfg.mode = undefined;
12623
- applyDeployment(cfg, deployment);
12624
- logger.info("setup", `Bound onboarding context org=${targetOrg.org_id} deployment=${deployment.deployment_id}`);
12625
- log.success(`Organization
12626
- ${dim(targetOrg.name)}`);
12627
- return true;
12628
- }
12629
- async function resolveOnboardingDeployment(apiClient, targetOrg) {
12630
- const deployments = await fetchDeployments(apiClient);
12631
- const orgDeployments = deployments.filter((deployment) => deployment.org_id === targetOrg.org_id);
12632
- return orgDeployments.find((deployment) => deployment.provider_slug === MCP_PROVIDER_SLUG) ?? orgDeployments[0] ?? null;
12633
- }
12634
12570
  async function resolveDeployment(apiClient, cfg, opts) {
12635
12571
  if (opts.deploymentID) {
12636
12572
  const d2 = await stepResolveDeployment(apiClient, opts.deploymentID);
@@ -12728,6 +12664,13 @@ async function stepSelectDeployment(apiClient, org) {
12728
12664
  ${dim(deployments[0].name)}`);
12729
12665
  return deployments[0];
12730
12666
  }
12667
+ const mcpDeployments = deployments.filter((d2) => d2.provider_slug === MCP_PROVIDER_SLUG);
12668
+ if (mcpDeployments.length === 1) {
12669
+ logger.info("setup", `Selected deployment: ${mcpDeployments[0].name} (auto, single MCP)`);
12670
+ log.success(`Using MCP
12671
+ ${dim(mcpDeployments[0].name)}`);
12672
+ return mcpDeployments[0];
12673
+ }
12731
12674
  const selected = await select({
12732
12675
  message: "Select an MCP",
12733
12676
  options: deployments.map((d2) => ({ label: d2.name, value: d2.deployment_id }))
@@ -12771,11 +12714,8 @@ ${dim("created")}`);
12771
12714
  return null;
12772
12715
  }
12773
12716
  }
12774
- function isStdioOnly(p2) {
12775
- return p2.id() === "claude-desktop";
12776
- }
12777
12717
  function stepDetectTools() {
12778
- return allSetupProviders().filter((p2) => p2.isInstalled() && !isStdioOnly(p2));
12718
+ return allSetupProviders().filter((p2) => p2.isInstalled());
12779
12719
  }
12780
12720
  async function stepSelectTools(detected) {
12781
12721
  const configuredMap = new Map;
@@ -12861,6 +12801,7 @@ function stepShowSummary(results) {
12861
12801
  log.success("All agents already configured. No changes needed.");
12862
12802
  }
12863
12803
  }
12804
+ var SETUP_HANDSHAKE_TIMEOUT_MS = 1800000;
12864
12805
  var init_flow2 = __esm(() => {
12865
12806
  init_dist8();
12866
12807
  init_client();
@@ -12877,7 +12818,7 @@ var init_flow2 = __esm(() => {
12877
12818
 
12878
12819
  // src/commands/insights.ts
12879
12820
  import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
12880
- import { dirname as dirname6, join as join29 } from "node:path";
12821
+ import { dirname as dirname7, join as join29 } from "node:path";
12881
12822
  function shuffled(arr) {
12882
12823
  const copy2 = [...arr];
12883
12824
  for (let i2 = copy2.length - 1;i2 > 0; i2--) {
@@ -13075,7 +13016,7 @@ function defaultRunner(cfg) {
13075
13016
  build: buildInsights,
13076
13017
  render: renderHTML,
13077
13018
  writeFile: (path3, content) => {
13078
- const dir = dirname6(path3);
13019
+ const dir = dirname7(path3);
13079
13020
  if (!existsSync18(dir))
13080
13021
  mkdirSync9(dir, { recursive: true, mode: 448 });
13081
13022
  writeFileSync9(path3, content, { mode: 384 });
@@ -13519,7 +13460,7 @@ __export(exports_flow3, {
13519
13460
  async function runAgentSetup(opts) {
13520
13461
  const provider = allSetupProviders().find((p2) => p2.id() === opts.tool.toLowerCase());
13521
13462
  if (!provider) {
13522
- const available = allSetupProviders().filter((p2) => !isStdioOnly(p2)).map((p2) => p2.id()).join(", ");
13463
+ const available = allSetupProviders().map((p2) => p2.id()).join(", ");
13523
13464
  emitError({
13524
13465
  step: "setup",
13525
13466
  reason: "unknown_tool",
@@ -13527,14 +13468,6 @@ async function runAgentSetup(opts) {
13527
13468
  });
13528
13469
  return 2;
13529
13470
  }
13530
- if (isStdioOnly(provider)) {
13531
- emitError({
13532
- step: "setup",
13533
- reason: "tool_unsupported_in_agent_mode",
13534
- agent_next_steps: `${provider.name()} is not supported by agent setup. Tell the user to run 'dosu mcp add ${provider.id()}' manually after signing in.`
13535
- });
13536
- return 2;
13537
- }
13538
13471
  let cfg = loadConfig();
13539
13472
  if (opts.loginTicket) {
13540
13473
  const redeemed = await redeemTicket(opts.loginTicket, cfg);
@@ -13764,7 +13697,7 @@ async function resolveDeployment2(client, cfg, opts) {
13764
13697
  emitError({
13765
13698
  step: "deployment",
13766
13699
  reason: "no_deployments",
13767
- agent_next_steps: "No Dosu deployments are accessible to this account. Tell the user to create one at https://app.dosu.dev before retrying."
13700
+ agent_next_steps: "No Dosu deployments are accessible to this account. The CLI may be signed in to a " + "different account than the user expects — have them run 'dosu logout' and retry, or " + "create a deployment at https://app.dosu.dev before retrying."
13768
13701
  });
13769
13702
  return { code: 1, cfg };
13770
13703
  }
@@ -13773,7 +13706,7 @@ async function resolveDeployment2(client, cfg, opts) {
13773
13706
  emitError({
13774
13707
  step: "deployment",
13775
13708
  reason: "no_mcp_deployment",
13776
- agent_next_steps: "Account has deployments but none of them back an MCP server. Tell the user to create an MCP deployment at https://app.dosu.dev, or pass --deployment <id> to target a specific deployment."
13709
+ agent_next_steps: "Account has deployments but none of them back an MCP server. The CLI may be signed in " + "to a different account than the user expects — have them run 'dosu logout' and retry, " + "create an MCP deployment at https://app.dosu.dev, or pass --deployment <id> to target " + "a specific deployment."
13777
13710
  });
13778
13711
  return { code: 1, cfg };
13779
13712
  }
@@ -13862,7 +13795,7 @@ function buildResumeCommand(tool, ticket, deploymentID) {
13862
13795
  return parts.join(" ");
13863
13796
  }
13864
13797
  function listAgentSupportedToolIDs() {
13865
- return allSetupProviders().filter((p2) => !isStdioOnly(p2)).map((p2) => p2.id());
13798
+ return allSetupProviders().map((p2) => p2.id());
13866
13799
  }
13867
13800
  var NPX_INVOCATION = "npx @dosu/cli@latest";
13868
13801
  var init_flow3 = __esm(() => {
@@ -13874,7 +13807,6 @@ var init_flow3 = __esm(() => {
13874
13807
  init_providers();
13875
13808
  init_installer();
13876
13809
  init_agents_md_step();
13877
- init_flow2();
13878
13810
  });
13879
13811
 
13880
13812
  // src/cli/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dosu/cli",
3
- "version": "0.41.0",
3
+ "version": "0.43.0-alpha.1",
4
4
  "type": "module",
5
5
  "description": "Dosu CLI - Manage MCP servers for AI tools",
6
6
  "license": "MIT",