@divergeai/cli 0.1.28 → 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 +28 -2
  2. package/dist/diverge.mjs +182 -41
  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
@@ -222,6 +235,19 @@ command in a fresh sandbox. Its tool definitions ride on every request
222
235
  (about 5k tokens, cached after the first). Without a Chrome on the port the
223
236
  server waits; the first tool call connects.
224
237
 
238
+ dsh runs the repository's hooks. Started inside a git checkout whose root
239
+ holds a Claude Code hook config (`.claude/settings.json`), `diverge dsh`
240
+ mounts dsh's own hook bridge (`@deepseek-ai/dsh-hooks-claude-code`, installed
241
+ beside the harness) on that file, and says so at launch; anywhere else
242
+ nothing is mounted. The bridge reads the file once at start and runs each
243
+ command hook at the matching moment: session start, prompt, tool use, stop.
244
+ Three things differ from Claude Code, and a hook meant for both allows for
245
+ them: a hook runs inside dsh's sandbox (the working directory is writable,
246
+ the home directory is not, `/tmp` does not outlive the command); context is
247
+ read only as JSON (`hookSpecificOutput.additionalContext`), never as plain
248
+ output; and a stop hook is never told the stop was already continued, so it
249
+ keeps its own record of what it held for.
250
+
225
251
  ## Platform tools
226
252
 
227
253
  Every command takes the scope it acts in: `--organization-id` (or
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.28" : "dev";
7556
+ var VERSION = true ? "0.1.30" : "dev";
7557
7557
  var USER_AGENT = `diverge/${VERSION}`;
7558
7558
 
7559
7559
  // src/http.ts
@@ -8600,9 +8600,37 @@ function dshPatchYaml(options) {
8600
8600
  ...dshSearchRows(),
8601
8601
  ...dshShellRows(),
8602
8602
  ...dshBrowserRows(),
8603
+ ...options.hooks ? dshHookRows(options.hooks) : [],
8604
+ ...options.skillDirs?.length ? dshSkillRows(options.skillDirs) : [],
8603
8605
  ""
8604
8606
  ].join("\n");
8605
8607
  }
8608
+ function dshSkillRows(skillDirs) {
8609
+ return [
8610
+ "- id: skill-filesystem",
8611
+ " config:",
8612
+ " customSkillDirs:",
8613
+ ...skillDirs.map((dir) => ` - ${JSON.stringify(dir)}`)
8614
+ ];
8615
+ }
8616
+ function repositoryHookConfig(cwd) {
8617
+ for (let dir = path4.resolve(cwd); ; dir = path4.dirname(dir)) {
8618
+ if (fs5.existsSync(path4.join(dir, ".git"))) {
8619
+ const file = path4.join(dir, ".claude", "settings.json");
8620
+ return fs5.existsSync(file) ? file : null;
8621
+ }
8622
+ if (dir === path4.dirname(dir)) return null;
8623
+ }
8624
+ }
8625
+ function dshHookRows(hooks) {
8626
+ return [
8627
+ "- insert:",
8628
+ " - id: hooks-claude-code",
8629
+ ` name: ${JSON.stringify(hooks.bridge)}`,
8630
+ " config:",
8631
+ ` configPath: ${JSON.stringify(hooks.configPath)}`
8632
+ ];
8633
+ }
8606
8634
  var DSH_LAUNCH_ID_HEADER = "x-divergent-launch-id";
8607
8635
  var DSH_CLIENT_HEADER = "x-divergent-client";
8608
8636
  function dshLaunchHeaderRows(launch) {
@@ -8669,48 +8697,77 @@ function dshSavedModel(patchText, surface) {
8669
8697
  return { model: String(config.model), ...config.reasoningEffort ? { reasoningEffort: String(config.reasoningEffort) } : {} };
8670
8698
  }
8671
8699
  var dshPresetsDir = (prefix) => path4.join(prefix, "node_modules", "@deepseek-ai", "dsh-web-app", "presets");
8672
- function dshPresetOverride(shipped) {
8673
- const lines = shipped.split("\n");
8674
- const insert = lines.indexOf("- insert:");
8675
- if (insert === -1) throw new Error("a shipped preset is not one inserted row");
8676
- const row = lines.slice(insert + 1).map((line) => line.replace(/^ {4}/, ""));
8677
- const start = row.findIndex((line) => /^\s+- id: ['"]?tool-web['"]?\s*$/.test(line));
8678
- 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;
8679
8703
  const indent = row[start].indexOf("-");
8680
8704
  const body = " ".repeat(indent + 2);
8681
8705
  let end = row.findIndex((line, index) => index > start && line.trim() !== "" && line.search(/\S/) <= indent);
8682
8706
  if (end === -1) end = row.length;
8683
8707
  const block = row.slice(start, end);
8684
- const search = block.findIndex((line) => line.startsWith(`${body} search:`));
8685
- if (search !== -1) {
8686
- block[search] = `${body} search: false`;
8687
- } else {
8688
- const config = block.indexOf(`${body}config:`);
8689
- if (config !== -1) block.splice(config + 1, 0, `${body} search: false`);
8690
- else block.splice(1, 0, `${body}config:`, `${body} search: false`);
8691
- }
8692
- 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";
8693
8742
  }
8694
- function dshPresetOverrides(prefix) {
8743
+ function dshPresetOverrides(prefix, skillDirs = []) {
8695
8744
  const dir = dshPresetsDir(prefix);
8696
- 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("");
8697
8746
  }
8698
8747
 
8699
8748
  // src/skills/install.ts
8700
8749
  var HARNESSES = ["claude", "codex", "dsh"];
8701
8750
  var SKILLS_MANIFEST = ".diverge-skills.json";
8702
- function skillsRoot(harness, env = process.env) {
8751
+ var DSH_CLI_SKILLS = "cli";
8752
+ function harnessHome(harness, env) {
8703
8753
  switch (harness) {
8704
8754
  case "claude":
8705
- 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");
8706
8756
  case "codex":
8707
- return path5.join(env.CODEX_HOME?.trim() || path5.join(os3.homedir(), ".codex"), "skills");
8757
+ return env.CODEX_HOME?.trim() || path5.join(os3.homedir(), ".codex");
8708
8758
  case "dsh":
8709
- return path5.join(dshHome(env), "skills");
8759
+ return dshHome(env);
8710
8760
  }
8711
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
+ }
8712
8769
  function presentHarnesses(env = process.env) {
8713
- return HARNESSES.filter((harness) => fs6.existsSync(path5.dirname(skillsRoot(harness, env))));
8770
+ return HARNESSES.filter((harness) => fs6.existsSync(harnessHome(harness, env)));
8714
8771
  }
8715
8772
  function isRecord(value) {
8716
8773
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -8847,6 +8904,66 @@ function skillsStatus(root, set2) {
8847
8904
  const retired = [...new Set(retirable(manifest, set2))].filter((folder) => !servedFolders.has(folder));
8848
8905
  return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired, skipped };
8849
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
+ }
8850
8967
 
8851
8968
  // src/skills/installable.ts
8852
8969
  async function fetchInstallableSkills(baseUrl, accessToken) {
@@ -8894,9 +9011,11 @@ async function collectLoginSkills(deps, options = {}) {
8894
9011
  var skillsUsage = `diverge skills <list|install|refresh|status> [--organization-id <id|name>] [--harness claude,codex,dsh] [--into <dir>]
8895
9012
 
8896
9013
  list the skills the platform serves you: in every organization you can act in, the skills whose tools you can run
8897
- install write them, and the CLI's own skills ("diverge"; "drive-chrome" for dsh), into each harness's user skill folder
8898
- (~/.claude/skills, ~/.codex/skills, $DSH_HOME/skills), side by side under each organization's prefix;
8899
- 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\`
8900
9019
  refresh the same, best effort: no login, no network or nothing changed prints nothing and never fails.
8901
9020
  Runs at sign-in and at every session start, so the folders hold what the organizations serve now
8902
9021
  status what each folder holds against what the platform serves now
@@ -8961,7 +9080,7 @@ function installReport(harness, result) {
8961
9080
  return `${harness}: ${result.written.length} skills in ${result.root} (${holdings}${changes.length ? `; ${changes.join("; ")}` : "; unchanged"})`;
8962
9081
  }
8963
9082
  function installForHarness(io, harness, skills, apiUrl, into) {
8964
- 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);
8965
9084
  io.stdout(installReport(harness, result));
8966
9085
  return result;
8967
9086
  }
@@ -8980,7 +9099,7 @@ async function refreshSkills(io, apiUrl, options = {}) {
8980
9099
  }
8981
9100
  const results = [];
8982
9101
  for (const harness of harnesses) {
8983
- 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);
8984
9103
  if (!options.quiet || result.added.length || result.updated.length || result.removed.length) io.stdout(installReport(harness, result));
8985
9104
  results.push(result);
8986
9105
  }
@@ -9018,7 +9137,7 @@ async function runSkills(args, io) {
9018
9137
  const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9019
9138
  let behind = false;
9020
9139
  for (const harness of harnessesOf(values.harness, io.env)) {
9021
- 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);
9022
9141
  const parts = [`${status2.current.length} current`];
9023
9142
  if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
9024
9143
  if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
@@ -9550,6 +9669,7 @@ var TESTED_DSH_VERSION = "0.2.1-rc.5";
9550
9669
 
9551
9670
  // src/agents/dsh-install.ts
9552
9671
  var DSH_PACKAGE = "@deepseek-ai/dsh";
9672
+ var DSH_HOOK_BRIDGE_PACKAGE = "@deepseek-ai/dsh-hooks-claude-code";
9553
9673
  var REGISTRY = "https://registry.npmjs.org";
9554
9674
  var RETIRED_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
9555
9675
  function dshInstallRoot(env) {
@@ -9558,6 +9678,15 @@ function dshInstallRoot(env) {
9558
9678
  function dshPackageIn(prefix) {
9559
9679
  return path13.join(prefix, "node_modules", "@deepseek-ai", "dsh");
9560
9680
  }
9681
+ function dshHookBridgeIn(prefix) {
9682
+ const dir = path13.join(prefix, "node_modules", ...DSH_HOOK_BRIDGE_PACKAGE.split("/"));
9683
+ try {
9684
+ const manifest = JSON.parse(fs10.readFileSync(path13.join(dir, "package.json"), "utf8"));
9685
+ return path13.join(dir, manifest.main ?? "lib/index.js");
9686
+ } catch {
9687
+ return null;
9688
+ }
9689
+ }
9561
9690
  function readManifest(prefix) {
9562
9691
  try {
9563
9692
  return JSON.parse(fs10.readFileSync(path13.join(dshPackageIn(prefix), "package.json"), "utf8"));
@@ -9586,9 +9715,12 @@ var realDeps = (env) => ({
9586
9715
  // start; the rest check prebuilt binaries or print a notice. An older npm
9587
9716
  // ignores the field and runs them all. stdout goes to stderr: a headless
9588
9717
  // run's stdout is the answer.
9589
- npmInstall: (prefix, spec) => {
9590
- fs10.writeFileSync(path13.join(prefix, "package.json"), JSON.stringify({ private: true, allowScripts: { "@deepseek-ai/dsh-subprocess-local": true } }));
9591
- return runNpm(["install", "--prefix", prefix, "--no-audit", "--no-fund", "--loglevel=error", spec], env, process.platform, ["ignore", 2, 2]);
9718
+ // A prefix that holds a version already keeps its manifest: npm removes what
9719
+ // a manifest stops naming.
9720
+ npmInstall: (prefix, specs) => {
9721
+ const manifest = path13.join(prefix, "package.json");
9722
+ if (!fs10.existsSync(manifest)) fs10.writeFileSync(manifest, JSON.stringify({ private: true, allowScripts: { "@deepseek-ai/dsh-subprocess-local": true } }));
9723
+ return runNpm(["install", "--prefix", prefix, "--no-audit", "--no-fund", "--loglevel=error", ...specs], env, process.platform, ["ignore", 2, 2]);
9592
9724
  },
9593
9725
  engines: async (version) => (await fetchJson(`${REGISTRY}/${DSH_PACKAGE}/${version}`, { timeoutMs: 15e3 })).engines?.node,
9594
9726
  nodeVersion: process.version,
@@ -9600,6 +9732,10 @@ async function ensureDsh(io, version = TESTED_DSH_VERSION, deps = realDeps(io.en
9600
9732
  let manifest = readManifest(prefix);
9601
9733
  if (manifest) {
9602
9734
  requireNode(version, manifest.engines?.node, deps.nodeVersion);
9735
+ if (!dshHookBridgeIn(prefix)) {
9736
+ const code2 = await deps.npmInstall(prefix, [`${DSH_HOOK_BRIDGE_PACKAGE}@${version}`]).catch(() => 1);
9737
+ if (code2 !== 0) io.stderr(`warning: could not add dsh's hook bridge (npm exited with ${code2}); this session runs without the repository's hooks.`);
9738
+ }
9603
9739
  } else {
9604
9740
  let engines;
9605
9741
  try {
@@ -9611,7 +9747,7 @@ async function ensureDsh(io, version = TESTED_DSH_VERSION, deps = realDeps(io.en
9611
9747
  const staging = `${prefix}.installing-${process.pid}`;
9612
9748
  fs10.mkdirSync(staging, { recursive: true });
9613
9749
  try {
9614
- const code2 = await deps.npmInstall(staging, `${DSH_PACKAGE}@${version}`);
9750
+ const code2 = await deps.npmInstall(staging, [`${DSH_PACKAGE}@${version}`, `${DSH_HOOK_BRIDGE_PACKAGE}@${version}`]);
9615
9751
  if (code2 !== 0) throw new CliError(`Installing dsh ${version} failed: npm exited with ${code2}. Check the network and run diverge dsh again.`);
9616
9752
  try {
9617
9753
  fs10.renameSync(staging, prefix);
@@ -10291,7 +10427,7 @@ function savedModel(home, surface) {
10291
10427
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
10292
10428
  }
10293
10429
  }
10294
- async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, launch) {
10430
+ async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, launch, hooks, skillDirs) {
10295
10431
  const served = (await listModels(apiUrl, token.access_token, surface)).map(servedModel);
10296
10432
  const models = served.map((model) => model.id);
10297
10433
  if (models.length === 0) throw new CliError(`the gateway serves no models on the ${surface} surface for this organization`);
@@ -10299,7 +10435,7 @@ async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, l
10299
10435
  const usable = !requested && saved && models.includes(saved.model) ? saved : null;
10300
10436
  const defaultModel = dshDefaultModel(models, requested ?? usable?.model);
10301
10437
  if (!requested && saved && !usable) io.stderr(`The model you last chose, ${saved.model}, is no longer served here; using ${defaultModel}.`);
10302
- return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel, reasoningEffort: usable?.reasoningEffort, launch }), defaultModel };
10438
+ return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel, reasoningEffort: usable?.reasoningEffort, launch, hooks, skillDirs }), defaultModel };
10303
10439
  }
10304
10440
  async function runDshCommand(args, io) {
10305
10441
  const terminator = args.indexOf("--");
@@ -10318,17 +10454,22 @@ async function runDshCommand(args, io) {
10318
10454
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
10319
10455
  const dsh = await ensureDsh(io);
10320
10456
  const prefix = path18.join(dshInstallRoot(io.env), TESTED_DSH_VERSION);
10321
- 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);
10322
10461
  const launch = { id: randomUUID(), client: `dsh/${TESTED_DSH_VERSION}` };
10323
10462
  const home = dshHome(io.env);
10324
- const { overlay, defaultModel } = await overlayAndDefault(io, apiUrl, token, surface, values.model, savedModel(home, surface), launch);
10463
+ const bridge = dshHookBridgeIn(prefix);
10464
+ const configPath = bridge ? repositoryHookConfig(process.cwd()) : null;
10465
+ const hooks = bridge && configPath ? { bridge, configPath } : null;
10466
+ const { overlay, defaultModel } = await overlayAndDefault(io, apiUrl, token, surface, values.model, savedModel(home, surface), launch, hooks, skillDirs);
10325
10467
  const overlayPath = path18.join(home, `divergent-${surface}.yml`);
10326
- writeTextAtomic(overlayPath, `${overlay}${dshPresetOverrides(prefix)}`);
10468
+ writeTextAtomic(overlayPath, `${overlay}${dshPresetOverrides(prefix, skillDirs)}`);
10327
10469
  io.stderr(`dsh ${TESTED_DSH_VERSION} on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
10470
+ if (hooks) io.stderr(`Running this repository's hooks (${hooks.configPath}).`);
10328
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>"');
10329
10472
  const source = tokenSource(io.env, apiUrl);
10330
- const organizationId = tokenClaims(token.access_token).organizationId ?? values["organization-id"];
10331
- if (!organizationId) throw new CliError("the gateway token names no organization; pass --organization-id");
10332
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).`);
10333
10474
  const tools = new ToolsClient({ apiUrl, organizationId, mode: "operator", clientId: clientId(io.env) }, io.env);
10334
10475
  const publisher = new DshSessionPublisher({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@divergeai/cli",
3
- "version": "0.1.28",
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",