@divergeai/cli 0.1.29 → 0.1.30

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 +15 -2
  2. package/dist/diverge.mjs +138 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -117,7 +117,8 @@ after `--` goes to dsh as given. Each launch, in order:
117
117
  `$DIVERGE_HOME/harness/dsh/<version>`: about a minute and nearly 1 GB, and
118
118
  Node 24.9 or later. A dsh you installed yourself is neither used nor touched.
119
119
  A version no launch has used for a week is deleted.
120
- 4. brings your skills up to date in `$DSH_HOME/skills`.
120
+ 4. brings the launch organization's skills and the CLI's up to date (see
121
+ [Skills](#skills)) and names both roots in the overlay.
121
122
  5. writes the overlay `$DSH_HOME/divergent-<surface>.yml`: the models the
122
123
  gateway serves, each with the input, context window and output cap the
123
124
  gateway lists for it; dsh's DeepSeek adapter, telemetry upload and vendor
@@ -178,7 +179,7 @@ a shell.
178
179
 
179
180
  ```bash
180
181
  diverge skills list # what the platform serves you, per organization
181
- diverge skills install # into ~/.claude/skills, ~/.codex/skills and $DSH_HOME/skills
182
+ diverge skills install # into ~/.claude/skills, ~/.codex/skills and $DIVERGE_HOME/skills/dsh
182
183
  diverge skills refresh # the same, best effort and quiet: what sign-in and a session start run
183
184
  diverge skills status # what each folder holds against the platform
184
185
  ```
@@ -213,6 +214,18 @@ shell with `diverge tools`, which costs nothing until a tool is called, and
213
214
  the `diverge` skill shows it how, arguments piped through stdin and batches
214
215
  included.
215
216
 
217
+ A dsh session is bound to one organization by the launch that starts it, so
218
+ dsh's skills do not sit side by side: the CLI writes them under its own root,
219
+ `$DIVERGE_HOME/skills/dsh/<organization id>/<folder>/SKILL.md`, and the CLI's
220
+ `diverge` and `drive-chrome` under `$DIVERGE_HOME/skills/dsh/cli/`. A `diverge
221
+ dsh` launch refreshes its organization's folder and names it and the CLI's as
222
+ `customSkillDirs` on dsh's skill provider, in the host row and in every
223
+ shipped agent preset that mounts one, so its catalog offers only the skills
224
+ its token can run. dsh's default roots stay on: your own `$DSH_HOME/skills`
225
+ and a project's `.agents/skills` still load. A CLI before 0.1.30 wrote every
226
+ organization's skills into `$DSH_HOME/skills`; the first install or launch
227
+ after it removes exactly the folders it recorded there, and nothing of yours.
228
+
216
229
  dsh also gets the browser. The overlay mounts Chrome DevTools MCP, attached
217
230
  to a Chrome the person starts in debug mode (`--remote-debugging-port=9222`,
218
231
  on a profile of its own), and the CLI's `drive-chrome` skill says how to use
package/dist/diverge.mjs CHANGED
@@ -7553,7 +7553,7 @@ function tokenSource(env, baseUrl) {
7553
7553
  }
7554
7554
 
7555
7555
  // src/version.ts
7556
- var VERSION = true ? "0.1.29" : "dev";
7556
+ var VERSION = true ? "0.1.30" : "dev";
7557
7557
  var USER_AGENT = `diverge/${VERSION}`;
7558
7558
 
7559
7559
  // src/http.ts
@@ -8601,9 +8601,18 @@ function dshPatchYaml(options) {
8601
8601
  ...dshShellRows(),
8602
8602
  ...dshBrowserRows(),
8603
8603
  ...options.hooks ? dshHookRows(options.hooks) : [],
8604
+ ...options.skillDirs?.length ? dshSkillRows(options.skillDirs) : [],
8604
8605
  ""
8605
8606
  ].join("\n");
8606
8607
  }
8608
+ function dshSkillRows(skillDirs) {
8609
+ return [
8610
+ "- id: skill-filesystem",
8611
+ " config:",
8612
+ " customSkillDirs:",
8613
+ ...skillDirs.map((dir) => ` - ${JSON.stringify(dir)}`)
8614
+ ];
8615
+ }
8607
8616
  function repositoryHookConfig(cwd) {
8608
8617
  for (let dir = path4.resolve(cwd); ; dir = path4.dirname(dir)) {
8609
8618
  if (fs5.existsSync(path4.join(dir, ".git"))) {
@@ -8688,48 +8697,77 @@ function dshSavedModel(patchText, surface) {
8688
8697
  return { model: String(config.model), ...config.reasoningEffort ? { reasoningEffort: String(config.reasoningEffort) } : {} };
8689
8698
  }
8690
8699
  var dshPresetsDir = (prefix) => path4.join(prefix, "node_modules", "@deepseek-ai", "dsh-web-app", "presets");
8691
- function dshPresetOverride(shipped) {
8692
- const lines = shipped.split("\n");
8693
- const insert = lines.indexOf("- insert:");
8694
- if (insert === -1) throw new Error("a shipped preset is not one inserted row");
8695
- const row = lines.slice(insert + 1).map((line) => line.replace(/^ {4}/, ""));
8696
- const start = row.findIndex((line) => /^\s+- id: ['"]?tool-web['"]?\s*$/.test(line));
8697
- if (start === -1) return null;
8700
+ function editPresetPlugin(row, id, edit) {
8701
+ const start = row.findIndex((line) => new RegExp(`^\\s+- id: ['"]?${id}['"]?\\s*$`).test(line));
8702
+ if (start === -1) return false;
8698
8703
  const indent = row[start].indexOf("-");
8699
8704
  const body = " ".repeat(indent + 2);
8700
8705
  let end = row.findIndex((line, index) => index > start && line.trim() !== "" && line.search(/\S/) <= indent);
8701
8706
  if (end === -1) end = row.length;
8702
8707
  const block = row.slice(start, end);
8703
- const search = block.findIndex((line) => line.startsWith(`${body} search:`));
8704
- if (search !== -1) {
8705
- block[search] = `${body} search: false`;
8706
- } else {
8707
- const config = block.indexOf(`${body}config:`);
8708
- if (config !== -1) block.splice(config + 1, 0, `${body} search: false`);
8709
- else block.splice(1, 0, `${body}config:`, `${body} search: false`);
8710
- }
8711
- return [...row.slice(0, start), ...block, ...row.slice(end)].join("\n").trimEnd() + "\n";
8708
+ edit(block, body);
8709
+ row.splice(start, end - start, ...block);
8710
+ return true;
8711
+ }
8712
+ function dshPresetOverride(shipped, skillDirs = []) {
8713
+ const lines = shipped.split("\n");
8714
+ const insert = lines.indexOf("- insert:");
8715
+ if (insert === -1) throw new Error("a shipped preset is not one inserted row");
8716
+ const row = lines.slice(insert + 1).map((line) => line.replace(/^ {4}/, ""));
8717
+ const web = editPresetPlugin(row, "tool-web", (block, body) => {
8718
+ const search = block.findIndex((line) => line.startsWith(`${body} search:`));
8719
+ if (search !== -1) {
8720
+ block[search] = `${body} search: false`;
8721
+ } else {
8722
+ const config = block.indexOf(`${body}config:`);
8723
+ if (config !== -1) block.splice(config + 1, 0, `${body} search: false`);
8724
+ else block.splice(1, 0, `${body}config:`, `${body} search: false`);
8725
+ }
8726
+ });
8727
+ const skills = skillDirs.length > 0 && editPresetPlugin(row, "skill-filesystem", (block, body) => {
8728
+ const items = skillDirs.map((dir) => `${body} - ${JSON.stringify(dir)}`);
8729
+ const list = block.indexOf(`${body} customSkillDirs:`);
8730
+ if (list !== -1) {
8731
+ let last = list + 1;
8732
+ while (last < block.length && block[last].startsWith(`${body} `)) last += 1;
8733
+ block.splice(last, 0, ...items);
8734
+ } else {
8735
+ const config = block.indexOf(`${body}config:`);
8736
+ if (config !== -1) block.splice(config + 1, 0, `${body} customSkillDirs:`, ...items);
8737
+ else block.splice(1, 0, `${body}config:`, `${body} customSkillDirs:`, ...items);
8738
+ }
8739
+ });
8740
+ if (!web && !skills) return null;
8741
+ return row.join("\n").trimEnd() + "\n";
8712
8742
  }
8713
- function dshPresetOverrides(prefix) {
8743
+ function dshPresetOverrides(prefix, skillDirs = []) {
8714
8744
  const dir = dshPresetsDir(prefix);
8715
- return fs5.readdirSync(dir).filter((name) => name.endsWith(".patch.yml")).sort().flatMap((name) => dshPresetOverride(fs5.readFileSync(path4.join(dir, name), "utf8")) ?? []).join("");
8745
+ return fs5.readdirSync(dir).filter((name) => name.endsWith(".patch.yml")).sort().flatMap((name) => dshPresetOverride(fs5.readFileSync(path4.join(dir, name), "utf8"), skillDirs) ?? []).join("");
8716
8746
  }
8717
8747
 
8718
8748
  // src/skills/install.ts
8719
8749
  var HARNESSES = ["claude", "codex", "dsh"];
8720
8750
  var SKILLS_MANIFEST = ".diverge-skills.json";
8721
- function skillsRoot(harness, env = process.env) {
8751
+ var DSH_CLI_SKILLS = "cli";
8752
+ function harnessHome(harness, env) {
8722
8753
  switch (harness) {
8723
8754
  case "claude":
8724
- return path5.join(env.CLAUDE_CONFIG_DIR?.trim() || path5.join(os3.homedir(), ".claude"), "skills");
8755
+ return env.CLAUDE_CONFIG_DIR?.trim() || path5.join(os3.homedir(), ".claude");
8725
8756
  case "codex":
8726
- return path5.join(env.CODEX_HOME?.trim() || path5.join(os3.homedir(), ".codex"), "skills");
8757
+ return env.CODEX_HOME?.trim() || path5.join(os3.homedir(), ".codex");
8727
8758
  case "dsh":
8728
- return path5.join(dshHome(env), "skills");
8759
+ return dshHome(env);
8729
8760
  }
8730
8761
  }
8762
+ function skillsRoot(harness, env = process.env) {
8763
+ return harness === "dsh" ? path5.join(configDir(env), "skills", "dsh") : path5.join(harnessHome(harness, env), "skills");
8764
+ }
8765
+ function dshSkillDirs(organizationId, env = process.env) {
8766
+ const root = skillsRoot("dsh", env);
8767
+ return [path5.join(root, organizationId), path5.join(root, DSH_CLI_SKILLS)];
8768
+ }
8731
8769
  function presentHarnesses(env = process.env) {
8732
- return HARNESSES.filter((harness) => fs6.existsSync(path5.dirname(skillsRoot(harness, env))));
8770
+ return HARNESSES.filter((harness) => fs6.existsSync(harnessHome(harness, env)));
8733
8771
  }
8734
8772
  function isRecord(value) {
8735
8773
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -8866,6 +8904,66 @@ function skillsStatus(root, set2) {
8866
8904
  const retired = [...new Set(retirable(manifest, set2))].filter((folder) => !servedFolders.has(folder));
8867
8905
  return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired, skipped };
8868
8906
  }
8907
+ function isOrganizationFolder(id) {
8908
+ return id !== DSH_CLI_SKILLS && /^[A-Za-z0-9_-]{1,64}$/.test(id);
8909
+ }
8910
+ function targets(harness, root, set2) {
8911
+ if (harness !== "dsh") return [{ root, set: set2 }];
8912
+ return [
8913
+ ...set2.organizations.filter((organization) => isOrganizationFolder(organization.id)).map((organization) => ({ root: path5.join(root, organization.id), set: { cli: [], organizations: [organization], complete: true } })),
8914
+ { root: path5.join(root, DSH_CLI_SKILLS), set: { cli: set2.cli, organizations: [], complete: true } }
8915
+ ];
8916
+ }
8917
+ function retiredOrganizations(root, set2) {
8918
+ if (!set2.complete || !fs6.existsSync(root)) return [];
8919
+ const listed = new Set(set2.organizations.map((organization) => organization.id));
8920
+ return fs6.readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isOrganizationFolder(entry.name) && !listed.has(entry.name) && readSkillsManifest(path5.join(root, entry.name))).map((entry) => path5.join(root, entry.name));
8921
+ }
8922
+ function legacyDshFolders(env) {
8923
+ const root = path5.join(dshHome(env), "skills");
8924
+ return { root, folders: [...heldFolders(readSkillsManifest(root)).keys()].filter(isSkillFolder) };
8925
+ }
8926
+ function installHarness(harness, root, set2, source, env = process.env, now = () => /* @__PURE__ */ new Date()) {
8927
+ const results = targets(harness, root, set2).map((target) => installSkills(target.root, target.set, source, now));
8928
+ const merged = {
8929
+ root,
8930
+ written: results.flatMap((result) => result.written),
8931
+ added: results.flatMap((result) => result.added),
8932
+ updated: results.flatMap((result) => result.updated),
8933
+ removed: results.flatMap((result) => result.removed),
8934
+ skipped: results.flatMap((result) => result.skipped),
8935
+ counts: results.flatMap((result) => result.counts)
8936
+ };
8937
+ if (harness !== "dsh") return merged;
8938
+ for (const folder of retiredOrganizations(root, set2)) {
8939
+ merged.removed.push(...heldFolders(readSkillsManifest(folder)).keys());
8940
+ fs6.rmSync(folder, { recursive: true, force: true });
8941
+ }
8942
+ if (root !== skillsRoot("dsh", env)) return merged;
8943
+ const legacy = legacyDshFolders(env);
8944
+ for (const folder of legacy.folders) fs6.rmSync(path5.join(legacy.root, folder), { recursive: true, force: true });
8945
+ fs6.rmSync(path5.join(legacy.root, SKILLS_MANIFEST), { force: true });
8946
+ merged.removed.push(...legacy.folders);
8947
+ return merged;
8948
+ }
8949
+ function harnessStatus(harness, root, set2, env = process.env) {
8950
+ const statuses = targets(harness, root, set2).map((target) => skillsStatus(target.root, target.set));
8951
+ const retired = statuses.flatMap((status2) => status2.retired);
8952
+ if (harness === "dsh") {
8953
+ for (const folder of retiredOrganizations(root, set2)) retired.push(...heldFolders(readSkillsManifest(folder)).keys());
8954
+ if (root === skillsRoot("dsh", env)) retired.push(...legacyDshFolders(env).folders);
8955
+ }
8956
+ const installed = statuses.map((status2) => status2.installedAt).filter((at) => at !== null).sort();
8957
+ return {
8958
+ root,
8959
+ installedAt: installed[0] ?? null,
8960
+ current: statuses.flatMap((status2) => status2.current),
8961
+ behind: statuses.flatMap((status2) => status2.behind),
8962
+ missing: statuses.flatMap((status2) => status2.missing),
8963
+ retired,
8964
+ skipped: statuses.flatMap((status2) => status2.skipped)
8965
+ };
8966
+ }
8869
8967
 
8870
8968
  // src/skills/installable.ts
8871
8969
  async function fetchInstallableSkills(baseUrl, accessToken) {
@@ -8913,9 +9011,11 @@ async function collectLoginSkills(deps, options = {}) {
8913
9011
  var skillsUsage = `diverge skills <list|install|refresh|status> [--organization-id <id|name>] [--harness claude,codex,dsh] [--into <dir>]
8914
9012
 
8915
9013
  list the skills the platform serves you: in every organization you can act in, the skills whose tools you can run
8916
- install write them, and the CLI's own skills ("diverge"; "drive-chrome" for dsh), into each harness's user skill folder
8917
- (~/.claude/skills, ~/.codex/skills, $DSH_HOME/skills), side by side under each organization's prefix;
8918
- retires what an organization stopped serving; reports what changed \u2014 the full list is \`skills list\`
9014
+ install write them, and the CLI's own skills ("diverge"; "drive-chrome" for dsh), into each harness's skill folder:
9015
+ ~/.claude/skills and ~/.codex/skills side by side under each organization's prefix; for dsh one folder
9016
+ per organization and one for the CLI's under ~/.diverge/skills/dsh, of which a diverge dsh launch names
9017
+ its organization's and the CLI's; retires what an organization stopped serving; reports what changed \u2014
9018
+ the full list is \`skills list\`
8919
9019
  refresh the same, best effort: no login, no network or nothing changed prints nothing and never fails.
8920
9020
  Runs at sign-in and at every session start, so the folders hold what the organizations serve now
8921
9021
  status what each folder holds against what the platform serves now
@@ -8980,7 +9080,7 @@ function installReport(harness, result) {
8980
9080
  return `${harness}: ${result.written.length} skills in ${result.root} (${holdings}${changes.length ? `; ${changes.join("; ")}` : "; unchanged"})`;
8981
9081
  }
8982
9082
  function installForHarness(io, harness, skills, apiUrl, into) {
8983
- const result = installSkills(into ?? skillsRoot(harness, io.env), installSet(harness, skills), apiUrl);
9083
+ const result = installHarness(harness, into ?? skillsRoot(harness, io.env), installSet(harness, skills), apiUrl, io.env);
8984
9084
  io.stdout(installReport(harness, result));
8985
9085
  return result;
8986
9086
  }
@@ -8999,7 +9099,7 @@ async function refreshSkills(io, apiUrl, options = {}) {
8999
9099
  }
9000
9100
  const results = [];
9001
9101
  for (const harness of harnesses) {
9002
- const result = installSkills(skillsRoot(harness, io.env), installSet(harness, skills), apiUrl);
9102
+ const result = installHarness(harness, skillsRoot(harness, io.env), installSet(harness, skills), apiUrl, io.env);
9003
9103
  if (!options.quiet || result.added.length || result.updated.length || result.removed.length) io.stdout(installReport(harness, result));
9004
9104
  results.push(result);
9005
9105
  }
@@ -9037,7 +9137,7 @@ async function runSkills(args, io) {
9037
9137
  const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9038
9138
  let behind = false;
9039
9139
  for (const harness of harnessesOf(values.harness, io.env)) {
9040
- const status2 = skillsStatus(values.into ?? skillsRoot(harness, io.env), installSet(harness, skills));
9140
+ const status2 = harnessStatus(harness, values.into ?? skillsRoot(harness, io.env), installSet(harness, skills), io.env);
9041
9141
  const parts = [`${status2.current.length} current`];
9042
9142
  if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
9043
9143
  if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
@@ -10327,7 +10427,7 @@ function savedModel(home, surface) {
10327
10427
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
10328
10428
  }
10329
10429
  }
10330
- async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, launch, hooks) {
10430
+ async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, launch, hooks, skillDirs) {
10331
10431
  const served = (await listModels(apiUrl, token.access_token, surface)).map(servedModel);
10332
10432
  const models = served.map((model) => model.id);
10333
10433
  if (models.length === 0) throw new CliError(`the gateway serves no models on the ${surface} surface for this organization`);
@@ -10335,7 +10435,7 @@ async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, l
10335
10435
  const usable = !requested && saved && models.includes(saved.model) ? saved : null;
10336
10436
  const defaultModel = dshDefaultModel(models, requested ?? usable?.model);
10337
10437
  if (!requested && saved && !usable) io.stderr(`The model you last chose, ${saved.model}, is no longer served here; using ${defaultModel}.`);
10338
- return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel, reasoningEffort: usable?.reasoningEffort, launch, hooks }), defaultModel };
10438
+ return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel, reasoningEffort: usable?.reasoningEffort, launch, hooks, skillDirs }), defaultModel };
10339
10439
  }
10340
10440
  async function runDshCommand(args, io) {
10341
10441
  const terminator = args.indexOf("--");
@@ -10354,21 +10454,22 @@ async function runDshCommand(args, io) {
10354
10454
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
10355
10455
  const dsh = await ensureDsh(io);
10356
10456
  const prefix = path18.join(dshInstallRoot(io.env), TESTED_DSH_VERSION);
10357
- await skillsFor(io, apiUrl, "dsh", values["organization-id"]);
10457
+ const organizationId = tokenClaims(token.access_token).organizationId ?? values["organization-id"];
10458
+ if (!organizationId) throw new CliError("the gateway token names no organization; pass --organization-id");
10459
+ await skillsFor(io, apiUrl, "dsh", organizationId);
10460
+ const skillDirs = dshSkillDirs(organizationId, io.env);
10358
10461
  const launch = { id: randomUUID(), client: `dsh/${TESTED_DSH_VERSION}` };
10359
10462
  const home = dshHome(io.env);
10360
10463
  const bridge = dshHookBridgeIn(prefix);
10361
10464
  const configPath = bridge ? repositoryHookConfig(process.cwd()) : null;
10362
10465
  const hooks = bridge && configPath ? { bridge, configPath } : null;
10363
- const { overlay, defaultModel } = await overlayAndDefault(io, apiUrl, token, surface, values.model, savedModel(home, surface), launch, hooks);
10466
+ const { overlay, defaultModel } = await overlayAndDefault(io, apiUrl, token, surface, values.model, savedModel(home, surface), launch, hooks, skillDirs);
10364
10467
  const overlayPath = path18.join(home, `divergent-${surface}.yml`);
10365
- writeTextAtomic(overlayPath, `${overlay}${dshPresetOverrides(prefix)}`);
10468
+ writeTextAtomic(overlayPath, `${overlay}${dshPresetOverrides(prefix, skillDirs)}`);
10366
10469
  io.stderr(`dsh ${TESTED_DSH_VERSION} on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
10367
10470
  if (hooks) io.stderr(`Running this repository's hooks (${hooks.configPath}).`);
10368
10471
  if (passthrough.length === 0) io.stderr('Starting the web UI; open the URL dsh prints. For one task without the UI: diverge dsh -- "<task>"');
10369
10472
  const source = tokenSource(io.env, apiUrl);
10370
- const organizationId = tokenClaims(token.access_token).organizationId ?? values["organization-id"];
10371
- if (!organizationId) throw new CliError("the gateway token names no organization; pass --organization-id");
10372
10473
  io.stderr(`This session is organization ${organizationId}'s own record at the level its administrators set \u2014 off, metadata (tools, tokens, models; no content) or the whole conversation \u2014 named once its record opens. Yours to read: diverge tools call ${AGENT_SESSION_LIST_TOOL} (ADR 0121).`);
10373
10474
  const tools = new ToolsClient({ apiUrl, organizationId, mode: "operator", clientId: clientId(io.env) }, io.env);
10374
10475
  const publisher = new DshSessionPublisher({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divergeai/cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
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",