@divergeai/cli 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -79,10 +79,13 @@ Anthropic or OpenAI. Every other model, dsh included, searches through the
79
79
  platform's `search_online` tool: one key on the platform, metered and audited
80
80
  per organization. The dsh overlay wires it in as `mcp__divergent__search_online`
81
81
  (dsh's DeepSeek-backed `web_search` needs a vendor key and is off; `web_fetch`
82
- stays). In dsh's web profile the shipped agent presets still list `web_search`,
83
- where a patch cannot reach; it fails loudly and the model uses the bridge's
84
- tool listed beside it. To give Claude Code or Codex the same tool for a
85
- non-native upstream, register the bridge once:
82
+ stays). dsh's web profile composes the model's tools per agent preset, where a
83
+ patch cannot reach, so `diverge agents setup dsh` and `run dsh` also derive a
84
+ **Divergent** preset from the installed dsh's `standard` one, with the vendor
85
+ search left out of the model's tools, under `$DSH_HOME/.agent-presets/`, and
86
+ make it the default for new web sessions once in `settings.yaml` (a preset you
87
+ pick yourself there stays picked). To give Claude Code or Codex the same tool
88
+ for a non-native upstream, register the bridge once:
86
89
 
87
90
  ```bash
88
91
  claude mcp add --scope user divergent -- diverge tools bridge --tools search_online
package/dist/diverge.mjs CHANGED
@@ -6915,14 +6915,14 @@ var require_parser = __commonJS({
6915
6915
  case "scalar":
6916
6916
  case "single-quoted-scalar":
6917
6917
  case "double-quoted-scalar": {
6918
- const fs8 = this.flowScalar(this.type);
6918
+ const fs9 = this.flowScalar(this.type);
6919
6919
  if (atNextItem || it.value) {
6920
- map.items.push({ start, key: fs8, sep: [] });
6920
+ map.items.push({ start, key: fs9, sep: [] });
6921
6921
  this.onKeyLine = true;
6922
6922
  } else if (it.sep) {
6923
- this.stack.push(fs8);
6923
+ this.stack.push(fs9);
6924
6924
  } else {
6925
- Object.assign(it, { key: fs8, sep: [] });
6925
+ Object.assign(it, { key: fs9, sep: [] });
6926
6926
  this.onKeyLine = true;
6927
6927
  }
6928
6928
  return;
@@ -7050,13 +7050,13 @@ var require_parser = __commonJS({
7050
7050
  case "scalar":
7051
7051
  case "single-quoted-scalar":
7052
7052
  case "double-quoted-scalar": {
7053
- const fs8 = this.flowScalar(this.type);
7053
+ const fs9 = this.flowScalar(this.type);
7054
7054
  if (!it || it.value)
7055
- fc.items.push({ start: [], key: fs8, sep: [] });
7055
+ fc.items.push({ start: [], key: fs9, sep: [] });
7056
7056
  else if (it.sep)
7057
- this.stack.push(fs8);
7057
+ this.stack.push(fs9);
7058
7058
  else
7059
- Object.assign(it, { key: fs8, sep: [] });
7059
+ Object.assign(it, { key: fs9, sep: [] });
7060
7060
  return;
7061
7061
  }
7062
7062
  case "flow-map-end":
@@ -7382,12 +7382,12 @@ var CliError = class extends Error {
7382
7382
  };
7383
7383
 
7384
7384
  // src/commands/agents.ts
7385
- import fs6 from "node:fs";
7385
+ import fs7 from "node:fs";
7386
7386
  import path10 from "node:path";
7387
7387
  import { parseArgs } from "node:util";
7388
7388
 
7389
7389
  // src/version.ts
7390
- var VERSION = true ? "0.1.8" : "dev";
7390
+ var VERSION = true ? "0.1.10" : "dev";
7391
7391
  var USER_AGENT = `diverge/${VERSION}`;
7392
7392
 
7393
7393
  // src/http.ts
@@ -7451,19 +7451,27 @@ function openerCommand(url, env = process.env, platform = process.platform) {
7451
7451
  }
7452
7452
  return { command: "xdg-open", args: [url] };
7453
7453
  }
7454
- function openBrowser(url, env = process.env, platform = process.platform) {
7454
+ function openBrowser(url, env = process.env, platform = process.platform, report = () => {
7455
+ }) {
7455
7456
  const opener = openerCommand(url, env, platform);
7457
+ if (!env.BROWSER?.trim() && platform !== "darwin" && platform !== "win32" && !env.DISPLAY?.trim() && !env.WAYLAND_DISPLAY?.trim()) {
7458
+ report("this shell has no display (DISPLAY and WAYLAND_DISPLAY are unset)");
7459
+ return false;
7460
+ }
7456
7461
  try {
7457
7462
  const child = spawn(opener.command, opener.args, {
7458
7463
  detached: true,
7459
7464
  stdio: "ignore",
7460
7465
  ...opener.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
7461
7466
  });
7462
- child.on("error", () => {
7467
+ child.on("error", (error) => report(`${opener.command}: ${error.code === "ENOENT" ? "not found" : error.message}`));
7468
+ child.on("exit", (code2) => {
7469
+ if (code2) report(`${opener.command} exited with code ${code2}`);
7463
7470
  });
7464
7471
  child.unref();
7465
7472
  return true;
7466
- } catch {
7473
+ } catch (error) {
7474
+ report(`${opener.command}: ${error instanceof Error ? error.message : String(error)}`);
7467
7475
  return false;
7468
7476
  }
7469
7477
  }
@@ -7668,7 +7676,11 @@ function cachedFrom(payload, baseUrl, previousRefresh, nowMs) {
7668
7676
  base_url: baseUrl.replace(/\/+$/, "")
7669
7677
  };
7670
7678
  }
7671
- var defaultDeps = { startLoopback, openBrowser, now: Date.now };
7679
+ var defaultDeps = {
7680
+ startLoopback,
7681
+ openBrowser: (url, report) => openBrowser(url, process.env, process.platform, report),
7682
+ now: Date.now
7683
+ };
7672
7684
  async function login(options, deps = defaultDeps) {
7673
7685
  const verifier = generateVerifier();
7674
7686
  const state = generateState();
@@ -7686,7 +7698,7 @@ async function login(options, deps = defaultDeps) {
7686
7698
  loginHint: options.loginHint
7687
7699
  });
7688
7700
  const pending = loopback.waitForCode(state, options.timeoutMs);
7689
- if (options.openBrowser) deps.openBrowser(authorizeUrl);
7701
+ if (options.openBrowser) deps.openBrowser(authorizeUrl, (reason) => options.print(`Could not open a browser (${reason}); paste the URL below into one.`));
7690
7702
  options.print("Open this URL to sign in:");
7691
7703
  options.print(` ${authorizeUrl}`);
7692
7704
  const code2 = await pending;
@@ -7904,6 +7916,8 @@ function mergeCodexConfig(existing, block) {
7904
7916
 
7905
7917
  // src/agents/dsh.ts
7906
7918
  var import_yaml = __toESM(require_dist(), 1);
7919
+ import fs4 from "node:fs";
7920
+ import { createRequire } from "node:module";
7907
7921
  import os4 from "node:os";
7908
7922
  import path5 from "node:path";
7909
7923
  var DSH_SHIPPED_DEFAULT_MODEL = "deepseek-flash";
@@ -8016,6 +8030,83 @@ function dshSettingsDefaultModel(settingsText, surface) {
8016
8030
  function stalePinMessage(settingsPath, pinned, surface, served) {
8017
8031
  return `${settingsPath} pins ${pinned} on divergent-${surface}, which the gateway does not serve; set agent-default-model.model there to one of: ${served.join(", ")} (or remove it to use ${served[0]})`;
8018
8032
  }
8033
+ var DIVERGENT_PRESET_ID = "divergent";
8034
+ var DSH_USER_PRESETS_DIR = ".agent-presets";
8035
+ function dshPackageDir(binary) {
8036
+ if (/\.(cmd|bat)$/i.test(binary)) {
8037
+ const dir2 = path5.join(path5.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8038
+ return fs4.existsSync(path5.join(dir2, "package.json")) ? dir2 : null;
8039
+ }
8040
+ let dir;
8041
+ try {
8042
+ dir = path5.dirname(fs4.realpathSync(binary));
8043
+ } catch {
8044
+ return null;
8045
+ }
8046
+ for (; ; ) {
8047
+ try {
8048
+ if (JSON.parse(fs4.readFileSync(path5.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8049
+ } catch {
8050
+ }
8051
+ const parent = path5.dirname(dir);
8052
+ if (parent === dir) return null;
8053
+ dir = parent;
8054
+ }
8055
+ }
8056
+ function shippedPresetDir(packageDir, id = "standard") {
8057
+ try {
8058
+ const require2 = createRequire(path5.join(packageDir, "package.json"));
8059
+ const dir = path5.join(path5.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
8060
+ return fs4.existsSync(path5.join(dir, "agent.cordis.yml")) ? dir : null;
8061
+ } catch {
8062
+ return null;
8063
+ }
8064
+ }
8065
+ function divergentPreset(shippedComposition) {
8066
+ const lines = shippedComposition.split("\n");
8067
+ const start = lines.findIndex((line) => /^- id: ['"]?tool-web['"]?\s*$/.test(line));
8068
+ if (start === -1) throw new Error("the shipped preset composes no tool-web row");
8069
+ let end = lines.findIndex((line, index) => index > start && /^- /.test(line));
8070
+ if (end === -1) end = lines.length;
8071
+ const row = lines.slice(start, end);
8072
+ const search = row.findIndex((line) => /^\s+search:/.test(line));
8073
+ if (search !== -1) {
8074
+ row[search] = row[search].replace(/:.*$/, ": false");
8075
+ } else {
8076
+ const config = row.findIndex((line) => /^ config:\s*$/.test(line));
8077
+ if (config !== -1) row.splice(config + 1, 0, " search: false");
8078
+ else row.splice(1, 0, " config:", " search: false");
8079
+ }
8080
+ return [
8081
+ `# The Divergent agent preset: dsh's shipped "standard" preset with the vendor web`,
8082
+ "# search left out of the model's tools; the platform's search_online arrives over the",
8083
+ "# bridge (row mcp-divergent). Derived from the installed dsh by `diverge agents",
8084
+ "# setup dsh` and `diverge agents run dsh` on every run; edits do not survive.",
8085
+ ...lines.slice(0, start),
8086
+ ...row,
8087
+ ...lines.slice(end)
8088
+ ].join("\n");
8089
+ }
8090
+ var DIVERGENT_PRESET_METADATA = [
8091
+ "name: Divergent",
8092
+ "description: The standard coding agent, searching the web through the platform's search_online tool instead of a vendor key.",
8093
+ "order: 0",
8094
+ ""
8095
+ ].join("\n");
8096
+ function writeDivergentPreset(home, shippedDir) {
8097
+ const dir = path5.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
8098
+ writeTextAtomic(path5.join(dir, "agent.cordis.yml"), divergentPreset(fs4.readFileSync(path5.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
8099
+ writeTextAtomic(path5.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
8100
+ return dir;
8101
+ }
8102
+ function withDivergentPresetDefault(settingsText) {
8103
+ const parsed = settingsText?.trim() ? (0, import_yaml.parse)(settingsText) : null;
8104
+ const settings = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...parsed } : {};
8105
+ const presets = settings["agent-presets"];
8106
+ if (presets && typeof presets === "object" && presets.default) return null;
8107
+ settings["agent-presets"] = { ...presets ?? {}, default: DIVERGENT_PRESET_ID };
8108
+ return (0, import_yaml.stringify)(settings, { lineWidth: 0 });
8109
+ }
8019
8110
 
8020
8111
  // src/agents/opencode.ts
8021
8112
  import os5 from "node:os";
@@ -8037,7 +8128,7 @@ function mergeOpencodeConfig(existing, options) {
8037
8128
 
8038
8129
  // src/agents/run-dsh.ts
8039
8130
  import { spawn as spawn2 } from "node:child_process";
8040
- import fs4 from "node:fs";
8131
+ import fs5 from "node:fs";
8041
8132
  import path7 from "node:path";
8042
8133
  function findOnPath(name, env = process.env, platform = process.platform) {
8043
8134
  const dirs = (env.PATH ?? "").split(path7.delimiter).filter(Boolean);
@@ -8047,8 +8138,8 @@ function findOnPath(name, env = process.env, platform = process.platform) {
8047
8138
  for (const candidate of candidates) {
8048
8139
  const full = path7.join(dir, candidate);
8049
8140
  try {
8050
- fs4.accessSync(full, fs4.constants.X_OK);
8051
- if (fs4.statSync(full).isFile()) return full;
8141
+ fs5.accessSync(full, fs5.constants.X_OK);
8142
+ if (fs5.statSync(full).isFile()) return full;
8052
8143
  } catch {
8053
8144
  }
8054
8145
  }
@@ -8056,7 +8147,7 @@ function findOnPath(name, env = process.env, platform = process.platform) {
8056
8147
  return null;
8057
8148
  }
8058
8149
  function writeDshToken(credentialsPath, accessToken) {
8059
- const existing = fs4.existsSync(credentialsPath) ? fs4.readFileSync(credentialsPath, "utf8") : null;
8150
+ const existing = fs5.existsSync(credentialsPath) ? fs5.readFileSync(credentialsPath, "utf8") : null;
8060
8151
  let merged;
8061
8152
  try {
8062
8153
  merged = mergeDshCredentials(existing, accessToken);
@@ -8074,7 +8165,7 @@ function dshLaunch(binary, args, platform = process.platform, execPath = process
8074
8165
  const packageDir = path7.join(path7.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8075
8166
  let bin;
8076
8167
  try {
8077
- bin = JSON.parse(fs4.readFileSync(path7.join(packageDir, "package.json"), "utf8")).bin;
8168
+ bin = JSON.parse(fs5.readFileSync(path7.join(packageDir, "package.json"), "utf8")).bin;
8078
8169
  } catch {
8079
8170
  throw new CliError(`${binary} is not an npm shim for @deepseek-ai/dsh (no ${packageDir}); reinstall: npm install -g @deepseek-ai/dsh`);
8080
8171
  }
@@ -8165,19 +8256,19 @@ async function rotateTokenFile(loop) {
8165
8256
  }
8166
8257
 
8167
8258
  // src/agents/self-command.ts
8168
- import fs5 from "node:fs";
8259
+ import fs6 from "node:fs";
8169
8260
  import path9 from "node:path";
8170
8261
  function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
8171
8262
  const script = argv[1];
8172
8263
  if (!script) return { command: execPath, args: [] };
8173
8264
  if (platform !== "win32") {
8174
8265
  try {
8175
- fs5.accessSync(script, fs5.constants.X_OK);
8266
+ fs6.accessSync(script, fs6.constants.X_OK);
8176
8267
  return { command: path9.resolve(script), args: [] };
8177
8268
  } catch {
8178
8269
  }
8179
8270
  }
8180
- return { command: execPath, args: [fs5.realpathSync(script)] };
8271
+ return { command: execPath, args: [fs6.realpathSync(script)] };
8181
8272
  }
8182
8273
  function shellCommand(self, ...trailing) {
8183
8274
  return [self.command, ...self.args, ...trailing].map((part) => /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part).join(" ");
@@ -8204,12 +8295,29 @@ function surfaceOf(value, fallback) {
8204
8295
  throw new CliError(`--surface must be one of: ${SURFACES.join(", ")}`);
8205
8296
  }
8206
8297
  function readJson(file) {
8207
- if (!fs6.existsSync(file)) return null;
8208
- return JSON.parse(fs6.readFileSync(file, "utf8"));
8298
+ if (!fs7.existsSync(file)) return null;
8299
+ return JSON.parse(fs7.readFileSync(file, "utf8"));
8209
8300
  }
8210
8301
  async function servedModels(apiUrl, token, surface) {
8211
8302
  return (await listModels(apiUrl, token.access_token, surface)).map((entry) => entry.id);
8212
8303
  }
8304
+ function syncDivergentPreset(io, binary, home) {
8305
+ if (!binary) {
8306
+ io.stderr("dsh is not on PATH; the Divergent agent preset is derived when `diverge agents run dsh` first runs.");
8307
+ return;
8308
+ }
8309
+ const packageDir = dshPackageDir(binary);
8310
+ const shipped = packageDir ? shippedPresetDir(packageDir) : null;
8311
+ if (!shipped) {
8312
+ io.stderr(`warning: no shipped "standard" preset found behind ${binary}; dsh's own web_search stays in its web sessions and fails without a vendor key`);
8313
+ return;
8314
+ }
8315
+ const dir = writeDivergentPreset(home, shipped);
8316
+ const settingsPath = path10.join(home, "settings.yaml");
8317
+ const settings = withDivergentPresetDefault(fs7.existsSync(settingsPath) ? fs7.readFileSync(settingsPath, "utf8") : null);
8318
+ if (settings !== null) writeTextAtomic(settingsPath, settings);
8319
+ io.stderr(`Agent preset "${DIVERGENT_PRESET_ID}" (${dir}): web sessions search through the platform${settings !== null ? "; set as the default preset in settings.yaml" : ""}.`);
8320
+ }
8213
8321
  async function overlayAndDefault(apiUrl, token, surface, io, requested) {
8214
8322
  const models = await servedModels(apiUrl, token, surface);
8215
8323
  if (models.length === 0) throw new CliError(`the gateway serves no models on the ${surface} surface for this organization`);
@@ -8265,7 +8373,7 @@ async function setup(args, io) {
8265
8373
  }
8266
8374
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8267
8375
  const file = values.config ?? codexConfigPath();
8268
- const existing = fs6.existsSync(file) ? fs6.readFileSync(file, "utf8") : null;
8376
+ const existing = fs7.existsSync(file) ? fs7.readFileSync(file, "utf8") : null;
8269
8377
  writeTextAtomic(file, mergeCodexConfig(existing, block));
8270
8378
  io.stdout(`Codex provider divergent \u2192 ${openaiBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
8271
8379
  io.stdout('Select it per run with: codex -c model_provider=divergent (or set model_provider = "divergent" in the config)');
@@ -8284,6 +8392,7 @@ async function setup(args, io) {
8284
8392
  writeTextAtomic(file, overlay);
8285
8393
  io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
8286
8394
  io.stdout("Web search is the platform tool search_online over `diverge tools bridge` (row mcp-divergent); dsh's DeepSeek search is off.");
8395
+ syncDivergentPreset(io, findOnPath("dsh", io.env), dshHome(io.env));
8287
8396
  io.stdout("Boot a profile with the overlay and the token in its environment (user tokens expire; the login refreshes):");
8288
8397
  io.stdout(` DIVERGENT_TOKEN="$(diverge token)" dsh --profile headless --patch ${file} "<task>"`);
8289
8398
  io.stdout(`or let the CLI keep the token fresh for the whole session: diverge agents run dsh --surface ${surface} -- --profile web --port 3131 --no-open`);
@@ -8314,7 +8423,7 @@ ${agentsUsage}`);
8314
8423
  const settingsPath = path10.join(home, "settings.yaml");
8315
8424
  let pinned;
8316
8425
  try {
8317
- pinned = dshSettingsDefaultModel(fs6.existsSync(settingsPath) ? fs6.readFileSync(settingsPath, "utf8") : null, surface);
8426
+ pinned = dshSettingsDefaultModel(fs7.existsSync(settingsPath) ? fs7.readFileSync(settingsPath, "utf8") : null, surface);
8318
8427
  } catch (error) {
8319
8428
  throw new CliError(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
8320
8429
  }
@@ -8322,6 +8431,7 @@ ${agentsUsage}`);
8322
8431
  if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
8323
8432
  const overlayPath = path10.join(home, `divergent-${surface}.yml`);
8324
8433
  writeTextAtomic(overlayPath, overlay);
8434
+ syncDivergentPreset(io, binary, home);
8325
8435
  io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
8326
8436
  if (passthrough.length === 0) io.stderr('Starting the web UI; open the URL dsh prints. For one task without the UI: diverge agents run dsh -- "<task>"');
8327
8437
  const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
@@ -9265,7 +9375,7 @@ ${toolsUsage}`);
9265
9375
  }
9266
9376
 
9267
9377
  // src/commands/plugins.ts
9268
- import fs7 from "node:fs";
9378
+ import fs8 from "node:fs";
9269
9379
  import { parseArgs as parseArgs5 } from "node:util";
9270
9380
  var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
9271
9381
 
@@ -9292,7 +9402,7 @@ function readManifest(file) {
9292
9402
  ${pluginsUsage}`);
9293
9403
  let text;
9294
9404
  try {
9295
- text = fs7.readFileSync(file, "utf8");
9405
+ text = fs8.readFileSync(file, "utf8");
9296
9406
  } catch (error) {
9297
9407
  throw new CliError(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
9298
9408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divergeai/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Divergent platform CLI: sign in, wire coding agents to the inference gateway, call platform tools",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",