@divergeai/cli 0.1.22 → 0.1.23

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
@@ -13,6 +13,12 @@ diverge tools list --organization-id <id>
13
13
 
14
14
  Node 22 or later. Linux and macOS are supported; Windows is best-effort.
15
15
 
16
+ `diverge --help`, `diverge <command> --help` and `diverge <command> <verb>
17
+ --help` print the usage, and that usage is the CLI's command table: a
18
+ command, verb or option that does not exist is answered with the closest ones
19
+ it lists, by spelling or by what their description says (`diverge whoami` →
20
+ `diverge status`, `tools search` → `tools list --filter`), and exits 1.
21
+
16
22
  ## Sign in
17
23
 
18
24
  `diverge login` runs the browser sign-in (authorization code with PKCE on a
package/dist/diverge.mjs CHANGED
@@ -7423,7 +7423,7 @@ function tokenSource(env, baseUrl) {
7423
7423
  }
7424
7424
 
7425
7425
  // src/version.ts
7426
- var VERSION = true ? "0.1.22" : "dev";
7426
+ var VERSION = true ? "0.1.23" : "dev";
7427
7427
  var USER_AGENT = `diverge/${VERSION}`;
7428
7428
 
7429
7429
  // src/http.ts
@@ -7849,6 +7849,146 @@ async function revokeRefreshToken(baseUrl, clientId2, refreshToken) {
7849
7849
  if (!response.ok) throw new CliError(`Revoke endpoint returned HTTP ${response.status}`);
7850
7850
  }
7851
7851
 
7852
+ // src/usage.ts
7853
+ function usageEntries(usage2) {
7854
+ const entries = [];
7855
+ let current = null;
7856
+ for (const line of usage2.split("\n")) {
7857
+ if (/^ {2}\S/.test(line)) {
7858
+ const label = line.trim().split(/ {2,}/)[0];
7859
+ const verbs = [];
7860
+ for (const word of label.split(" ")) {
7861
+ if (!/^[a-z][a-z-]*$/.test(word)) break;
7862
+ verbs.push(word);
7863
+ }
7864
+ current = { verbs, summary: line.trim().slice(label.length).trim(), lines: [line] };
7865
+ entries.push(current);
7866
+ } else if (current && /^ {3,}\S/.test(line)) {
7867
+ current.lines.push(line);
7868
+ } else {
7869
+ current = null;
7870
+ }
7871
+ }
7872
+ return entries;
7873
+ }
7874
+ function startsWith(words, prefix) {
7875
+ return prefix.every((word, index) => words[index] === word);
7876
+ }
7877
+ function verbPath(usage2, args) {
7878
+ let best = [];
7879
+ for (const entry of usageEntries(usage2)) {
7880
+ let shared = 0;
7881
+ while (shared < entry.verbs.length && args[shared] === entry.verbs[shared]) shared++;
7882
+ if (shared > best.length) best = entry.verbs.slice(0, shared);
7883
+ }
7884
+ return best;
7885
+ }
7886
+ function groupOptions(usage2) {
7887
+ return usage2.split("\n", 1)[0].match(/\[--[^\]]+\]/g) ?? [];
7888
+ }
7889
+ function verbUsage(command, usage2, path15) {
7890
+ const lines = usageEntries(usage2).filter((entry) => startsWith(entry.verbs, path15)).flatMap((entry) => entry.lines);
7891
+ if (path15.length === 0 || lines.length === 0) return usage2;
7892
+ const shared = groupOptions(usage2);
7893
+ return [`diverge ${command} ${path15.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
7894
+ }
7895
+ var COMMON_WORDS = /* @__PURE__ */ new Set(["the", "and", "you", "for", "its", "with", "not", "one", "are", "all", "any", "from", "that", "this", "own", "per", "into", "out", "else", "each", "what", "when", "your"]);
7896
+ function distance(a, b) {
7897
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
7898
+ for (let i = 1; i <= a.length; i++) {
7899
+ for (let j = 1; j <= b.length; j++) {
7900
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
7901
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
7902
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
7903
+ }
7904
+ }
7905
+ return d[a.length][b.length];
7906
+ }
7907
+ function namesIt(word, guess) {
7908
+ if (word.length < 3 || COMMON_WORDS.has(word)) return false;
7909
+ return word === guess || guess.length >= 3 && word.startsWith(guess) || guess.startsWith(word) || guess.endsWith(word);
7910
+ }
7911
+ function prose(line) {
7912
+ return line.toLowerCase().replace(/\[[^\]]*\]|--?[a-z][a-z-]*/g, " ");
7913
+ }
7914
+ function closest(guess, candidates, rest = [], limit = 3) {
7915
+ const target = guess.toLowerCase().replace(/^-+/, "");
7916
+ const threshold = Math.max(1, Math.floor(target.length / 3));
7917
+ const matches = [];
7918
+ for (const candidate of candidates) {
7919
+ const first = candidate.summary ?? "";
7920
+ const name = candidate.name.toLowerCase();
7921
+ if (name && rest.includes(candidate.name)) {
7922
+ matches.push({ candidate, score: 0, hint: first });
7923
+ continue;
7924
+ }
7925
+ const edits = name ? distance(target, name) : Infinity;
7926
+ if (edits <= threshold || name && target.length >= 2 && name.startsWith(target)) {
7927
+ matches.push({ candidate, score: Math.min(edits, threshold), hint: first });
7928
+ continue;
7929
+ }
7930
+ const line = candidate.lines.find((text) => (prose(text).match(/[a-z]+/g) ?? []).some((word) => namesIt(word, target)));
7931
+ if (line) matches.push({ candidate, score: threshold + 1, hint: line });
7932
+ }
7933
+ return matches.sort((a, b) => a.score - b.score).slice(0, limit);
7934
+ }
7935
+ function didYouMean(matches) {
7936
+ if (matches.length === 0) return "";
7937
+ const shown = (match) => {
7938
+ const hint = match.hint.trim();
7939
+ return hint && hint !== match.candidate.invocation ? `${match.candidate.invocation} (${hint})` : match.candidate.invocation;
7940
+ };
7941
+ if (matches.length === 1) return ` Did you mean: ${shown(matches[0])}?`;
7942
+ return ` Did you mean one of:
7943
+ ${matches.map((match) => ` ${shown(match)}`).join("\n")}`;
7944
+ }
7945
+ function verbCandidates(command, usage2, path15) {
7946
+ const byName = /* @__PURE__ */ new Map();
7947
+ for (const entry of usageEntries(usage2)) {
7948
+ if (!startsWith(entry.verbs, path15)) continue;
7949
+ const name = entry.verbs[path15.length] ?? "";
7950
+ const invocation = ["diverge", command, ...path15, ...name ? [name] : []].join(" ");
7951
+ const lines = [entry.summary, ...entry.lines.slice(1).map((line) => line.trim())].filter(Boolean);
7952
+ const known = byName.get(name);
7953
+ if (known) known.lines.push(...lines);
7954
+ else byName.set(name, { invocation, name, summary: entry.summary, lines });
7955
+ }
7956
+ return [...byName.values()];
7957
+ }
7958
+ function unknownVerb(command, usage2, path15, verb, rest = []) {
7959
+ const candidates = verbCandidates(command, usage2, path15);
7960
+ const where = [command, ...path15].join(" ");
7961
+ if (!verb) return new CliError(`${where} needs one of: ${candidates.map((candidate) => candidate.name).filter(Boolean).join(", ")}
7962
+
7963
+ ${usage2}`);
7964
+ return new CliError(`Unknown ${where} verb: ${verb}.${didYouMean(closest(verb, candidates, rest))}
7965
+
7966
+ ${usage2}`);
7967
+ }
7968
+ function optionCandidates(usage2, path15) {
7969
+ const entries = usageEntries(usage2);
7970
+ const [header = "", ...body] = usage2.split("\n");
7971
+ const described = entries.length === 0 || path15.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path15)).flatMap((entry) => entry.lines);
7972
+ const byName = /* @__PURE__ */ new Map();
7973
+ const add = (line, describes) => {
7974
+ for (const [flag] of line.matchAll(/--[a-z][a-z-]*/g)) {
7975
+ const candidate = byName.get(flag) ?? { invocation: flag, name: flag.slice(2), lines: [] };
7976
+ byName.set(flag, candidate);
7977
+ if (describes && !candidate.lines.includes(line.trim())) candidate.lines.push(line.trim());
7978
+ }
7979
+ };
7980
+ add(header, false);
7981
+ for (const line of described) add(line, true);
7982
+ return [...byName.values()];
7983
+ }
7984
+ function unknownOption(error, usage2, args) {
7985
+ if (!(error instanceof Error) || error.name !== "TypeError") return null;
7986
+ const option = /^Unknown option '([^']+)'/.exec(error.message)?.[1];
7987
+ if (!option) return null;
7988
+ const path15 = verbPath(usage2, args);
7989
+ return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path15)))}`;
7990
+ }
7991
+
7852
7992
  // src/commands/login.ts
7853
7993
  import { parseArgs } from "node:util";
7854
7994
 
@@ -8647,9 +8787,7 @@ async function runSkills(args, io) {
8647
8787
  return ExitCode.Ok;
8648
8788
  }
8649
8789
  default:
8650
- throw new CliError(`skills needs one of: list, install, status
8651
-
8652
- ${skillsUsage}`);
8790
+ throw unknownVerb("skills", skillsUsage, [], verb, rest);
8653
8791
  }
8654
8792
  }
8655
8793
 
@@ -8829,16 +8967,12 @@ async function setup(args, io) {
8829
8967
  return ExitCode.Ok;
8830
8968
  }
8831
8969
  default:
8832
- throw new CliError(`agents setup needs one of: claude, opencode, codex, dsh
8833
-
8834
- ${agentsUsage}`);
8970
+ throw unknownVerb("agents", agentsUsage, ["setup"], agent, rest);
8835
8971
  }
8836
8972
  }
8837
8973
  async function run(args, io) {
8838
8974
  const [agent, ...rest] = args;
8839
- if (agent !== "dsh") throw new CliError(`agents run supports: dsh
8840
-
8841
- ${agentsUsage}`);
8975
+ if (agent !== "dsh") throw unknownVerb("agents", agentsUsage, ["run"], agent, rest);
8842
8976
  const terminator = rest.indexOf("--");
8843
8977
  const flags = terminator === -1 ? rest : rest.slice(0, terminator);
8844
8978
  const passthrough = terminator === -1 ? [] : rest.slice(terminator + 1);
@@ -8915,18 +9049,8 @@ async function rotate(args, io) {
8915
9049
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
8916
9050
  });
8917
9051
  }
8918
- function agentsVerbUsage(verb) {
8919
- const lines = agentsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `) || line.trimStart().startsWith(`${verb} `));
8920
- return lines.length ? `diverge agents ${verb} ...
8921
-
8922
- ${lines.join("\n")}` : agentsUsage;
8923
- }
8924
9052
  async function runAgents(args, io) {
8925
9053
  const [verb, ...rest] = args;
8926
- if (verb && (rest.includes("--help") || rest.includes("-h")) && !rest.includes("--")) {
8927
- io.stdout(agentsVerbUsage(verb));
8928
- return ExitCode.Ok;
8929
- }
8930
9054
  switch (verb) {
8931
9055
  case "setup":
8932
9056
  return setup(rest, io);
@@ -8937,9 +9061,7 @@ async function runAgents(args, io) {
8937
9061
  case "status":
8938
9062
  return status(rest, io);
8939
9063
  default:
8940
- throw new CliError(`agents needs one of: setup, run, rotate, status
8941
-
8942
- ${agentsUsage}`);
9064
+ throw unknownVerb("agents", agentsUsage, [], verb, rest);
8943
9065
  }
8944
9066
  }
8945
9067
 
@@ -8947,20 +9069,22 @@ ${agentsUsage}`);
8947
9069
  import { parseArgs as parseArgs4 } from "node:util";
8948
9070
  var orgUsage = `diverge org [use <id|name>]
8949
9071
 
8950
- Without a verb: the organizations you may act in, the default marked.
8951
- use <id|name>: make it the default \u2014 where every command that names no
8952
- organization acts, in every shell on this machine that shares the login. It
8953
- is a local preference: the platform is asked only for a token there, which
8954
- proves you may act in it. For one command, pass --organization-id <id> (or
8955
- set DIVERGENT_ORG_ID) instead. A platform actor may act in, and so sees, every
8956
- organization.`;
9072
+ (no verb) list the organizations you may act in, the default marked
9073
+ use <id|name> make it the default: where every command that names no organization acts,
9074
+ in every shell on this machine that shares the login
9075
+
9076
+ The default is a local preference: the platform is asked only for a token
9077
+ there, which proves you may act in it. For one command, pass
9078
+ --organization-id <id> (or set DIVERGENT_ORG_ID) instead. A platform actor may
9079
+ act in, and so sees, every organization.`;
8957
9080
  async function runOrg(args, io) {
8958
9081
  const { positionals } = parseArgs4({ args, options: {}, strict: true, allowPositionals: true });
9082
+ const verb = positionals[0];
9083
+ if (verb && verb !== "use") throw unknownVerb("org", orgUsage, [], verb, positionals.slice(1));
8959
9084
  const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
8960
9085
  const info = await fetchUserInfo(source.baseUrl, (await getUserToken(source)).access_token);
8961
9086
  const organizations = info.organizations ?? [];
8962
9087
  const current = requireLogin(source).organization;
8963
- const verb = positionals[0];
8964
9088
  if (!verb) {
8965
9089
  if (organizations.length === 0) {
8966
9090
  io.stdout("You are not a member of any organization.");
@@ -8969,9 +9093,6 @@ async function runOrg(args, io) {
8969
9093
  for (const organization of organizations) io.stdout(`${organization.id === current ? "*" : " "} ${organization.name} (${organization.id})`);
8970
9094
  return 0;
8971
9095
  }
8972
- if (verb !== "use") throw new CliError(`Unknown verb: ${verb}
8973
-
8974
- ${orgUsage}`);
8975
9096
  const target = positionals[1];
8976
9097
  if (!target) throw new CliError("org use needs an organization id or name");
8977
9098
  const choice = chooseOrganization({ flag: target, organizations });
@@ -9659,7 +9780,7 @@ function formatPending(result, apiUrl, organizationId) {
9659
9780
  var toolsUsage = `diverge tools <list|capabilities|describe|call|run|invocation|bridge> [--organization-id <id>] [--project-id <id>] ...
9660
9781
 
9661
9782
  list [--filter <text>] [--capability <selector>] [--json]
9662
- --filter matches tool names and descriptions, ignoring case
9783
+ --filter searches tool names and descriptions, ignoring case
9663
9784
  capabilities [--json]
9664
9785
  describe <name> [--json]
9665
9786
  call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>] [--json]
@@ -9718,7 +9839,7 @@ async function call(rest, io) {
9718
9839
  });
9719
9840
  } catch (error) {
9720
9841
  if (!json) throw error;
9721
- outcome = { tool, status: "failed", error: error instanceof Error ? error.message : String(error) };
9842
+ outcome = { tool, status: "failed", error: unknownOption(error, toolsUsage, ["call"]) ?? (error instanceof Error ? error.message : String(error)) };
9722
9843
  }
9723
9844
  if (json) io.stdout(JSON.stringify(outcome, null, 2));
9724
9845
  else printOutcome(outcome, waited, io);
@@ -9742,26 +9863,8 @@ function printOutcome(outcome, waited, io) {
9742
9863
  throw new CliError(outcome.invocationId ? `Approved, but execution failed: ${outcome.error}` : outcome.error || "Tool execution failed");
9743
9864
  }
9744
9865
  }
9745
- function toolsVerbUsage(verb) {
9746
- const lines = [];
9747
- let inVerb = false;
9748
- for (const line of toolsUsage.split("\n")) {
9749
- if (/^ {2}\S/.test(line)) inVerb = line.trimStart().startsWith(`${verb} `);
9750
- else if (!/^ {3,}\S/.test(line)) inVerb = false;
9751
- if (inVerb) lines.push(line);
9752
- }
9753
- return lines.length ? `diverge tools ${verb} ...
9754
-
9755
- ${lines.join("\n")}
9756
-
9757
- Every tools command takes --organization-id <id> (or DIVERGENT_ORG_ID, else the signed-in login's organization) and --project-id <id> (or DIVERGENT_PROJECT_ID).` : toolsUsage;
9758
- }
9759
9866
  async function runTools(args, io) {
9760
9867
  const [verb, ...rest] = args;
9761
- if (verb && (rest.includes("--help") || rest.includes("-h"))) {
9762
- io.stdout(toolsVerbUsage(verb));
9763
- return ExitCode.Ok;
9764
- }
9765
9868
  switch (verb) {
9766
9869
  case "list": {
9767
9870
  const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
@@ -9844,9 +9947,7 @@ async function runTools(args, io) {
9844
9947
  return serveBridge(new BridgeServer(tools, { tools: allow }), process.stdin, process.stdout);
9845
9948
  }
9846
9949
  default:
9847
- throw new CliError(`tools needs one of: list, capabilities, describe, call, run, invocation, bridge
9848
-
9849
- ${toolsUsage}`);
9950
+ throw unknownVerb("tools", toolsUsage, [], verb, rest);
9850
9951
  }
9851
9952
  }
9852
9953
 
@@ -9900,20 +10001,8 @@ function formatPlugin(plugin) {
9900
10001
  ...plugin.refusals?.length ? [" NOT SERVED: the platform refuses this version; publish a fixed one:", ...plugin.refusals.map((reason) => ` - ${reason}`)] : []
9901
10002
  ];
9902
10003
  }
9903
- function pluginsVerbUsage(verb) {
9904
- const lines = pluginsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `));
9905
- return lines.length ? `diverge plugins ${verb} ...
9906
-
9907
- ${lines.join("\n")}
9908
-
9909
- Every plugins command takes --organization-id <id> (or DIVERGENT_ORG_ID, else the signed-in login's organization).` : pluginsUsage;
9910
- }
9911
10004
  async function runPlugins(args, io) {
9912
10005
  const [verb, ...rest] = args;
9913
- if (verb && (rest.includes("--help") || rest.includes("-h"))) {
9914
- io.stdout(pluginsVerbUsage(verb));
9915
- return ExitCode.Ok;
9916
- }
9917
10006
  switch (verb) {
9918
10007
  case "validate": {
9919
10008
  const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
@@ -9971,9 +10060,7 @@ ${pluginsUsage}`);
9971
10060
  return ExitCode.Ok;
9972
10061
  }
9973
10062
  default:
9974
- throw new CliError(`plugins needs one of: validate, publish, list, retire
9975
-
9976
- ${pluginsUsage}`);
10063
+ throw unknownVerb("plugins", pluginsUsage, [], verb, rest);
9977
10064
  }
9978
10065
  }
9979
10066
 
@@ -10084,9 +10171,9 @@ function installLatest(version) {
10084
10171
 
10085
10172
  // src/cli.ts
10086
10173
  var COMMANDS = [
10087
- { name: "login", summary: "Sign in through the browser", usage: loginUsage, run: runLogin },
10174
+ { name: "login", summary: "Sign in (authenticate) through the browser", usage: loginUsage, run: runLogin },
10088
10175
  { name: "logout", summary: "Revoke the login and forget it", usage: logoutUsage, run: runLogout },
10089
- { name: "status", summary: "Show the deployment and the cached login", usage: statusUsage, run: runStatus },
10176
+ { name: "status", summary: "Show who you are signed in as: the deployment, the account and what the login carries", usage: statusUsage, run: runStatus },
10090
10177
  { name: "org", summary: "Show or switch the organization the login acts in", usage: orgUsage, run: runOrg },
10091
10178
  { name: "token", summary: "Print a fresh access token", usage: tokenUsage, run: runToken },
10092
10179
  { name: "agents", summary: "Point a coding agent at the model gateway (setup, run, status)", usage: agentsUsage, run: runAgents },
@@ -10122,15 +10209,19 @@ async function run2(argv, io) {
10122
10209
  }
10123
10210
  const command = COMMANDS.find((candidate) => candidate.name === name);
10124
10211
  if (!command) {
10125
- io.stderr(`Unknown command: ${name}
10212
+ const candidates = COMMANDS.map((known) => ({ invocation: `diverge ${known.name}`, name: known.name, summary: known.summary, lines: [known.summary] }));
10213
+ io.stderr(`Unknown command: ${name}.${didYouMean(closest(name, candidates, rest))}
10126
10214
 
10127
10215
  ${usage()}`);
10128
10216
  return ExitCode.Failure;
10129
10217
  }
10130
10218
  const own = rest.includes("--") ? rest.slice(0, rest.indexOf("--")) : rest;
10131
- if ((own.includes("--help") || own.includes("-h")) && (own.length === 1 || own[0].startsWith("-"))) {
10132
- io.stdout(command.usage);
10133
- return ExitCode.Ok;
10219
+ if (own.includes("--help") || own.includes("-h")) {
10220
+ const path15 = verbPath(command.usage, own);
10221
+ if (path15.length > 0 || own[0].startsWith("-")) {
10222
+ io.stdout(verbUsage(command.name, command.usage, path15));
10223
+ return ExitCode.Ok;
10224
+ }
10134
10225
  }
10135
10226
  const code2 = await runCommand(command, rest, io);
10136
10227
  updateNotice(argv, io);
@@ -10145,9 +10236,9 @@ async function runCommand(command, rest, io) {
10145
10236
  return error.exitCode;
10146
10237
  }
10147
10238
  if (error instanceof Error && error.name === "TypeError" && /Unknown option|Option .* argument|Unexpected argument/.test(error.message)) {
10148
- io.stderr(`Error: ${error.message}
10239
+ io.stderr(`Error: ${unknownOption(error, command.usage, rest) ?? error.message}
10149
10240
 
10150
- ${command.usage}`);
10241
+ ${verbUsage(command.name, command.usage, verbPath(command.usage, rest))}`);
10151
10242
  return ExitCode.Failure;
10152
10243
  }
10153
10244
  io.stderr(`Error: ${error instanceof Error ? error.message : String(error)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divergeai/cli",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
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",