@divergeai/cli 0.1.26 → 0.1.28

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 +60 -18
  2. package/dist/diverge.mjs +1089 -936
  3. package/package.json +3 -2
package/dist/diverge.mjs CHANGED
@@ -114,17 +114,17 @@ var require_visit = __commonJS({
114
114
  visit.BREAK = BREAK;
115
115
  visit.SKIP = SKIP;
116
116
  visit.REMOVE = REMOVE;
117
- function visit_(key, node, visitor, path18) {
118
- const ctrl = callVisitor(key, node, visitor, path18);
117
+ function visit_(key, node, visitor, path20) {
118
+ const ctrl = callVisitor(key, node, visitor, path20);
119
119
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
120
- replaceNode(key, path18, ctrl);
121
- return visit_(key, ctrl, visitor, path18);
120
+ replaceNode(key, path20, ctrl);
121
+ return visit_(key, ctrl, visitor, path20);
122
122
  }
123
123
  if (typeof ctrl !== "symbol") {
124
124
  if (identity.isCollection(node)) {
125
- path18 = Object.freeze(path18.concat(node));
125
+ path20 = Object.freeze(path20.concat(node));
126
126
  for (let i = 0; i < node.items.length; ++i) {
127
- const ci = visit_(i, node.items[i], visitor, path18);
127
+ const ci = visit_(i, node.items[i], visitor, path20);
128
128
  if (typeof ci === "number")
129
129
  i = ci - 1;
130
130
  else if (ci === BREAK)
@@ -135,13 +135,13 @@ var require_visit = __commonJS({
135
135
  }
136
136
  }
137
137
  } else if (identity.isPair(node)) {
138
- path18 = Object.freeze(path18.concat(node));
139
- const ck = visit_("key", node.key, visitor, path18);
138
+ path20 = Object.freeze(path20.concat(node));
139
+ const ck = visit_("key", node.key, visitor, path20);
140
140
  if (ck === BREAK)
141
141
  return BREAK;
142
142
  else if (ck === REMOVE)
143
143
  node.key = null;
144
- const cv = visit_("value", node.value, visitor, path18);
144
+ const cv = visit_("value", node.value, visitor, path20);
145
145
  if (cv === BREAK)
146
146
  return BREAK;
147
147
  else if (cv === REMOVE)
@@ -162,17 +162,17 @@ var require_visit = __commonJS({
162
162
  visitAsync.BREAK = BREAK;
163
163
  visitAsync.SKIP = SKIP;
164
164
  visitAsync.REMOVE = REMOVE;
165
- async function visitAsync_(key, node, visitor, path18) {
166
- const ctrl = await callVisitor(key, node, visitor, path18);
165
+ async function visitAsync_(key, node, visitor, path20) {
166
+ const ctrl = await callVisitor(key, node, visitor, path20);
167
167
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
168
- replaceNode(key, path18, ctrl);
169
- return visitAsync_(key, ctrl, visitor, path18);
168
+ replaceNode(key, path20, ctrl);
169
+ return visitAsync_(key, ctrl, visitor, path20);
170
170
  }
171
171
  if (typeof ctrl !== "symbol") {
172
172
  if (identity.isCollection(node)) {
173
- path18 = Object.freeze(path18.concat(node));
173
+ path20 = Object.freeze(path20.concat(node));
174
174
  for (let i = 0; i < node.items.length; ++i) {
175
- const ci = await visitAsync_(i, node.items[i], visitor, path18);
175
+ const ci = await visitAsync_(i, node.items[i], visitor, path20);
176
176
  if (typeof ci === "number")
177
177
  i = ci - 1;
178
178
  else if (ci === BREAK)
@@ -183,13 +183,13 @@ var require_visit = __commonJS({
183
183
  }
184
184
  }
185
185
  } else if (identity.isPair(node)) {
186
- path18 = Object.freeze(path18.concat(node));
187
- const ck = await visitAsync_("key", node.key, visitor, path18);
186
+ path20 = Object.freeze(path20.concat(node));
187
+ const ck = await visitAsync_("key", node.key, visitor, path20);
188
188
  if (ck === BREAK)
189
189
  return BREAK;
190
190
  else if (ck === REMOVE)
191
191
  node.key = null;
192
- const cv = await visitAsync_("value", node.value, visitor, path18);
192
+ const cv = await visitAsync_("value", node.value, visitor, path20);
193
193
  if (cv === BREAK)
194
194
  return BREAK;
195
195
  else if (cv === REMOVE)
@@ -216,23 +216,23 @@ var require_visit = __commonJS({
216
216
  }
217
217
  return visitor;
218
218
  }
219
- function callVisitor(key, node, visitor, path18) {
219
+ function callVisitor(key, node, visitor, path20) {
220
220
  if (typeof visitor === "function")
221
- return visitor(key, node, path18);
221
+ return visitor(key, node, path20);
222
222
  if (identity.isMap(node))
223
- return visitor.Map?.(key, node, path18);
223
+ return visitor.Map?.(key, node, path20);
224
224
  if (identity.isSeq(node))
225
- return visitor.Seq?.(key, node, path18);
225
+ return visitor.Seq?.(key, node, path20);
226
226
  if (identity.isPair(node))
227
- return visitor.Pair?.(key, node, path18);
227
+ return visitor.Pair?.(key, node, path20);
228
228
  if (identity.isScalar(node))
229
- return visitor.Scalar?.(key, node, path18);
229
+ return visitor.Scalar?.(key, node, path20);
230
230
  if (identity.isAlias(node))
231
- return visitor.Alias?.(key, node, path18);
231
+ return visitor.Alias?.(key, node, path20);
232
232
  return void 0;
233
233
  }
234
- function replaceNode(key, path18, node) {
235
- const parent = path18[path18.length - 1];
234
+ function replaceNode(key, path20, node) {
235
+ const parent = path20[path20.length - 1];
236
236
  if (identity.isCollection(parent)) {
237
237
  parent.items[key] = node;
238
238
  } else if (identity.isPair(parent)) {
@@ -844,10 +844,10 @@ var require_Collection = __commonJS({
844
844
  var createNode = require_createNode();
845
845
  var identity = require_identity();
846
846
  var Node = require_Node();
847
- function collectionFromPath(schema, path18, value) {
847
+ function collectionFromPath(schema, path20, value) {
848
848
  let v = value;
849
- for (let i = path18.length - 1; i >= 0; --i) {
850
- const k = path18[i];
849
+ for (let i = path20.length - 1; i >= 0; --i) {
850
+ const k = path20[i];
851
851
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
852
852
  const a = [];
853
853
  a[k] = v;
@@ -866,7 +866,7 @@ var require_Collection = __commonJS({
866
866
  sourceObjects: /* @__PURE__ */ new Map()
867
867
  });
868
868
  }
869
- var isEmptyPath = (path18) => path18 == null || typeof path18 === "object" && !!path18[Symbol.iterator]().next().done;
869
+ var isEmptyPath = (path20) => path20 == null || typeof path20 === "object" && !!path20[Symbol.iterator]().next().done;
870
870
  var Collection = class extends Node.NodeBase {
871
871
  constructor(type, schema) {
872
872
  super(type);
@@ -896,11 +896,11 @@ var require_Collection = __commonJS({
896
896
  * be a Pair instance or a `{ key, value }` object, which may not have a key
897
897
  * that already exists in the map.
898
898
  */
899
- addIn(path18, value) {
900
- if (isEmptyPath(path18))
899
+ addIn(path20, value) {
900
+ if (isEmptyPath(path20))
901
901
  this.add(value);
902
902
  else {
903
- const [key, ...rest] = path18;
903
+ const [key, ...rest] = path20;
904
904
  const node = this.get(key, true);
905
905
  if (identity.isCollection(node))
906
906
  node.addIn(rest, value);
@@ -914,8 +914,8 @@ var require_Collection = __commonJS({
914
914
  * Removes a value from the collection.
915
915
  * @returns `true` if the item was found and removed.
916
916
  */
917
- deleteIn(path18) {
918
- const [key, ...rest] = path18;
917
+ deleteIn(path20) {
918
+ const [key, ...rest] = path20;
919
919
  if (rest.length === 0)
920
920
  return this.delete(key);
921
921
  const node = this.get(key, true);
@@ -929,8 +929,8 @@ var require_Collection = __commonJS({
929
929
  * scalar values from their surrounding node; to disable set `keepScalar` to
930
930
  * `true` (collections are always returned intact).
931
931
  */
932
- getIn(path18, keepScalar) {
933
- const [key, ...rest] = path18;
932
+ getIn(path20, keepScalar) {
933
+ const [key, ...rest] = path20;
934
934
  const node = this.get(key, true);
935
935
  if (rest.length === 0)
936
936
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -948,8 +948,8 @@ var require_Collection = __commonJS({
948
948
  /**
949
949
  * Checks if the collection includes a value with the key `key`.
950
950
  */
951
- hasIn(path18) {
952
- const [key, ...rest] = path18;
951
+ hasIn(path20) {
952
+ const [key, ...rest] = path20;
953
953
  if (rest.length === 0)
954
954
  return this.has(key);
955
955
  const node = this.get(key, true);
@@ -959,8 +959,8 @@ var require_Collection = __commonJS({
959
959
  * Sets a value in this collection. For `!!set`, `value` needs to be a
960
960
  * boolean to add/remove the item from the set.
961
961
  */
962
- setIn(path18, value) {
963
- const [key, ...rest] = path18;
962
+ setIn(path20, value) {
963
+ const [key, ...rest] = path20;
964
964
  if (rest.length === 0) {
965
965
  this.set(key, value);
966
966
  } else {
@@ -3475,9 +3475,9 @@ var require_Document = __commonJS({
3475
3475
  this.contents.add(value);
3476
3476
  }
3477
3477
  /** Adds a value to the document. */
3478
- addIn(path18, value) {
3478
+ addIn(path20, value) {
3479
3479
  if (assertCollection(this.contents))
3480
- this.contents.addIn(path18, value);
3480
+ this.contents.addIn(path20, value);
3481
3481
  }
3482
3482
  /**
3483
3483
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -3552,14 +3552,14 @@ var require_Document = __commonJS({
3552
3552
  * Removes a value from the document.
3553
3553
  * @returns `true` if the item was found and removed.
3554
3554
  */
3555
- deleteIn(path18) {
3556
- if (Collection.isEmptyPath(path18)) {
3555
+ deleteIn(path20) {
3556
+ if (Collection.isEmptyPath(path20)) {
3557
3557
  if (this.contents == null)
3558
3558
  return false;
3559
3559
  this.contents = null;
3560
3560
  return true;
3561
3561
  }
3562
- return assertCollection(this.contents) ? this.contents.deleteIn(path18) : false;
3562
+ return assertCollection(this.contents) ? this.contents.deleteIn(path20) : false;
3563
3563
  }
3564
3564
  /**
3565
3565
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -3574,10 +3574,10 @@ var require_Document = __commonJS({
3574
3574
  * scalar values from their surrounding node; to disable set `keepScalar` to
3575
3575
  * `true` (collections are always returned intact).
3576
3576
  */
3577
- getIn(path18, keepScalar) {
3578
- if (Collection.isEmptyPath(path18))
3577
+ getIn(path20, keepScalar) {
3578
+ if (Collection.isEmptyPath(path20))
3579
3579
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3580
- return identity.isCollection(this.contents) ? this.contents.getIn(path18, keepScalar) : void 0;
3580
+ return identity.isCollection(this.contents) ? this.contents.getIn(path20, keepScalar) : void 0;
3581
3581
  }
3582
3582
  /**
3583
3583
  * Checks if the document includes a value with the key `key`.
@@ -3588,10 +3588,10 @@ var require_Document = __commonJS({
3588
3588
  /**
3589
3589
  * Checks if the document includes a value at `path`.
3590
3590
  */
3591
- hasIn(path18) {
3592
- if (Collection.isEmptyPath(path18))
3591
+ hasIn(path20) {
3592
+ if (Collection.isEmptyPath(path20))
3593
3593
  return this.contents !== void 0;
3594
- return identity.isCollection(this.contents) ? this.contents.hasIn(path18) : false;
3594
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path20) : false;
3595
3595
  }
3596
3596
  /**
3597
3597
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -3608,13 +3608,13 @@ var require_Document = __commonJS({
3608
3608
  * Sets a value in this document. For `!!set`, `value` needs to be a
3609
3609
  * boolean to add/remove the item from the set.
3610
3610
  */
3611
- setIn(path18, value) {
3612
- if (Collection.isEmptyPath(path18)) {
3611
+ setIn(path20, value) {
3612
+ if (Collection.isEmptyPath(path20)) {
3613
3613
  this.contents = value;
3614
3614
  } else if (this.contents == null) {
3615
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path18), value);
3615
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path20), value);
3616
3616
  } else if (assertCollection(this.contents)) {
3617
- this.contents.setIn(path18, value);
3617
+ this.contents.setIn(path20, value);
3618
3618
  }
3619
3619
  }
3620
3620
  /**
@@ -5575,9 +5575,9 @@ var require_cst_visit = __commonJS({
5575
5575
  visit.BREAK = BREAK;
5576
5576
  visit.SKIP = SKIP;
5577
5577
  visit.REMOVE = REMOVE;
5578
- visit.itemAtPath = (cst, path18) => {
5578
+ visit.itemAtPath = (cst, path20) => {
5579
5579
  let item = cst;
5580
- for (const [field, index] of path18) {
5580
+ for (const [field, index] of path20) {
5581
5581
  const tok = item?.[field];
5582
5582
  if (tok && "items" in tok) {
5583
5583
  item = tok.items[index];
@@ -5586,23 +5586,23 @@ var require_cst_visit = __commonJS({
5586
5586
  }
5587
5587
  return item;
5588
5588
  };
5589
- visit.parentCollection = (cst, path18) => {
5590
- const parent = visit.itemAtPath(cst, path18.slice(0, -1));
5591
- const field = path18[path18.length - 1][0];
5589
+ visit.parentCollection = (cst, path20) => {
5590
+ const parent = visit.itemAtPath(cst, path20.slice(0, -1));
5591
+ const field = path20[path20.length - 1][0];
5592
5592
  const coll = parent?.[field];
5593
5593
  if (coll && "items" in coll)
5594
5594
  return coll;
5595
5595
  throw new Error("Parent collection not found");
5596
5596
  };
5597
- function _visit(path18, item, visitor) {
5598
- let ctrl = visitor(item, path18);
5597
+ function _visit(path20, item, visitor) {
5598
+ let ctrl = visitor(item, path20);
5599
5599
  if (typeof ctrl === "symbol")
5600
5600
  return ctrl;
5601
5601
  for (const field of ["key", "value"]) {
5602
5602
  const token = item[field];
5603
5603
  if (token && "items" in token) {
5604
5604
  for (let i = 0; i < token.items.length; ++i) {
5605
- const ci = _visit(Object.freeze(path18.concat([[field, i]])), token.items[i], visitor);
5605
+ const ci = _visit(Object.freeze(path20.concat([[field, i]])), token.items[i], visitor);
5606
5606
  if (typeof ci === "number")
5607
5607
  i = ci - 1;
5608
5608
  else if (ci === BREAK)
@@ -5613,10 +5613,10 @@ var require_cst_visit = __commonJS({
5613
5613
  }
5614
5614
  }
5615
5615
  if (typeof ctrl === "function" && field === "key")
5616
- ctrl = ctrl(item, path18);
5616
+ ctrl = ctrl(item, path20);
5617
5617
  }
5618
5618
  }
5619
- return typeof ctrl === "function" ? ctrl(item, path18) : ctrl;
5619
+ return typeof ctrl === "function" ? ctrl(item, path20) : ctrl;
5620
5620
  }
5621
5621
  exports.visit = visit;
5622
5622
  }
@@ -6918,14 +6918,14 @@ var require_parser = __commonJS({
6918
6918
  case "scalar":
6919
6919
  case "single-quoted-scalar":
6920
6920
  case "double-quoted-scalar": {
6921
- const fs16 = this.flowScalar(this.type);
6921
+ const fs19 = this.flowScalar(this.type);
6922
6922
  if (atNextItem || it.value) {
6923
- map.items.push({ start, key: fs16, sep: [] });
6923
+ map.items.push({ start, key: fs19, sep: [] });
6924
6924
  this.onKeyLine = true;
6925
6925
  } else if (it.sep) {
6926
- this.stack.push(fs16);
6926
+ this.stack.push(fs19);
6927
6927
  } else {
6928
- Object.assign(it, { key: fs16, sep: [] });
6928
+ Object.assign(it, { key: fs19, sep: [] });
6929
6929
  this.onKeyLine = true;
6930
6930
  }
6931
6931
  return;
@@ -7053,13 +7053,13 @@ var require_parser = __commonJS({
7053
7053
  case "scalar":
7054
7054
  case "single-quoted-scalar":
7055
7055
  case "double-quoted-scalar": {
7056
- const fs16 = this.flowScalar(this.type);
7056
+ const fs19 = this.flowScalar(this.type);
7057
7057
  if (!it || it.value)
7058
- fc.items.push({ start: [], key: fs16, sep: [] });
7058
+ fc.items.push({ start: [], key: fs19, sep: [] });
7059
7059
  else if (it.sep)
7060
- this.stack.push(fs16);
7060
+ this.stack.push(fs19);
7061
7061
  else
7062
- Object.assign(it, { key: fs16, sep: [] });
7062
+ Object.assign(it, { key: fs19, sep: [] });
7063
7063
  return;
7064
7064
  }
7065
7065
  case "flow-map-end":
@@ -7392,10 +7392,7 @@ var NotGrantedError = class extends CliError {
7392
7392
  };
7393
7393
 
7394
7394
  // src/commands/agents.ts
7395
- import { randomUUID } from "node:crypto";
7396
- import fs11 from "node:fs";
7397
- import os8 from "node:os";
7398
- import path14 from "node:path";
7395
+ import fs8 from "node:fs";
7399
7396
  import { parseArgs as parseArgs3 } from "node:util";
7400
7397
 
7401
7398
  // src/auth/oauth-client.ts
@@ -7556,7 +7553,7 @@ function tokenSource(env, baseUrl) {
7556
7553
  }
7557
7554
 
7558
7555
  // src/version.ts
7559
- var VERSION = true ? "0.1.26" : "dev";
7556
+ var VERSION = true ? "0.1.28" : "dev";
7560
7557
  var USER_AGENT = `diverge/${VERSION}`;
7561
7558
 
7562
7559
  // src/http.ts
@@ -7939,11 +7936,11 @@ function verbPath(usage2, args) {
7939
7936
  function groupOptions(usage2) {
7940
7937
  return usage2.split("\n", 1)[0].match(/\[--[^\]]+\]/g) ?? [];
7941
7938
  }
7942
- function verbUsage(command, usage2, path18) {
7943
- const lines = usageEntries(usage2).filter((entry) => startsWith(entry.verbs, path18)).flatMap((entry) => entry.lines);
7944
- if (path18.length === 0 || lines.length === 0) return usage2;
7939
+ function verbUsage(command, usage2, path20) {
7940
+ const lines = usageEntries(usage2).filter((entry) => startsWith(entry.verbs, path20)).flatMap((entry) => entry.lines);
7941
+ if (path20.length === 0 || lines.length === 0) return usage2;
7945
7942
  const shared = groupOptions(usage2);
7946
- return [`diverge ${command} ${path18.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
7943
+ return [`diverge ${command} ${path20.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
7947
7944
  }
7948
7945
  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"]);
7949
7946
  function distance(a, b) {
@@ -7995,12 +7992,12 @@ function didYouMean(matches) {
7995
7992
  return ` Did you mean one of:
7996
7993
  ${matches.map((match) => ` ${shown(match)}`).join("\n")}`;
7997
7994
  }
7998
- function verbCandidates(command, usage2, path18) {
7995
+ function verbCandidates(command, usage2, path20) {
7999
7996
  const byName = /* @__PURE__ */ new Map();
8000
7997
  for (const entry of usageEntries(usage2)) {
8001
- if (!startsWith(entry.verbs, path18)) continue;
8002
- const name = entry.verbs[path18.length] ?? "";
8003
- const invocation = ["diverge", command, ...path18, ...name ? [name] : []].join(" ");
7998
+ if (!startsWith(entry.verbs, path20)) continue;
7999
+ const name = entry.verbs[path20.length] ?? "";
8000
+ const invocation = ["diverge", command, ...path20, ...name ? [name] : []].join(" ");
8004
8001
  const lines = [entry.summary, ...entry.lines.slice(1).map((line) => line.trim())].filter(Boolean);
8005
8002
  const known = byName.get(name);
8006
8003
  if (known) known.lines.push(...lines);
@@ -8008,9 +8005,9 @@ function verbCandidates(command, usage2, path18) {
8008
8005
  }
8009
8006
  return [...byName.values()];
8010
8007
  }
8011
- function unknownVerb(command, usage2, path18, verb, rest = []) {
8012
- const candidates = verbCandidates(command, usage2, path18);
8013
- const where = [command, ...path18].join(" ");
8008
+ function unknownVerb(command, usage2, path20, verb, rest = []) {
8009
+ const candidates = verbCandidates(command, usage2, path20);
8010
+ const where = [command, ...path20].join(" ");
8014
8011
  if (!verb) return new CliError(`${where} needs one of: ${candidates.map((candidate) => candidate.name).filter(Boolean).join(", ")}
8015
8012
 
8016
8013
  ${usage2}`);
@@ -8018,10 +8015,10 @@ ${usage2}`);
8018
8015
 
8019
8016
  ${usage2}`);
8020
8017
  }
8021
- function optionCandidates(usage2, path18) {
8018
+ function optionCandidates(usage2, path20) {
8022
8019
  const entries = usageEntries(usage2);
8023
8020
  const [header = "", ...body] = usage2.split("\n");
8024
- const described = entries.length === 0 || path18.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path18)).flatMap((entry) => entry.lines);
8021
+ const described = entries.length === 0 || path20.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path20)).flatMap((entry) => entry.lines);
8025
8022
  const byName = /* @__PURE__ */ new Map();
8026
8023
  const add = (line, describes) => {
8027
8024
  for (const [flag] of line.matchAll(/--[a-z][a-z-]*/g)) {
@@ -8038,8 +8035,8 @@ function unknownOption(error, usage2, args) {
8038
8035
  if (!(error instanceof Error) || error.name !== "TypeError") return null;
8039
8036
  const option = /^Unknown option '([^']+)'/.exec(error.message)?.[1];
8040
8037
  if (!option) return null;
8041
- const path18 = verbPath(usage2, args);
8042
- return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path18)))}`;
8038
+ const path20 = verbPath(usage2, args);
8039
+ return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path20)))}`;
8043
8040
  }
8044
8041
 
8045
8042
  // src/commands/login.ts
@@ -8158,19 +8155,23 @@ unwraps the platform's envelope and nothing inside. Its shape is the tool's
8158
8155
  own and differs by tool and plugin \u2014 one returns its records under \`data\`,
8159
8156
  another at the top level \u2014 so read a tool's shape from its own answer, not
8160
8157
  from the last tool's. Parse stdout alone; stderr only carries the scope and
8161
- identity lines. Without \`--json\`, errors are text on stderr.
8158
+ identity lines. A shell tool that shows both streams in one view prints those
8159
+ two lines above the JSON, but a pipe or \`$(...)\` never sees them: pipe
8160
+ straight to \`jq\` and strip nothing. Without \`--json\`, errors are text on
8161
+ stderr.
8162
8162
 
8163
8163
  ## Several related writes: one set, one decision
8164
8164
 
8165
- When two or more writes serve one intent \u2014 assign a role to several groups,
8166
- deactivate several users, set several notification routes \u2014 raise them as
8167
- one set, never one call per write: the person approves the set once instead
8168
- of clicking through one card each.
8165
+ When two or more writes serve one intent \u2014 deactivate several users, set
8166
+ several notification routes \u2014 raise them as one set, never one call per
8167
+ write: the person approves the set once instead of clicking through one card
8168
+ each. A change to one thing's access is already one call: a role's holders,
8169
+ a group's members, a person's groups.
8169
8170
 
8170
8171
  \`\`\`bash
8171
- diverge tools set - --summary "Coordinators get the field role" --json <<'JSON'
8172
- [{"tool": "rbac_assign_role_to_group", "args": {"roleId": "r1", "groupId": "g1"}},
8173
- {"tool": "rbac_assign_role_to_group", "args": {"roleId": "r1", "groupId": "g2"}}]
8172
+ diverge tools set - --summary "Coordinators who left" --json <<'JSON'
8173
+ [{"tool": "rbac_deactivate_user", "args": {"userId": "u1"}},
8174
+ {"tool": "rbac_deactivate_user", "args": {"userId": "u2"}}]
8174
8175
  JSON
8175
8176
  \`\`\`
8176
8177
 
@@ -8276,7 +8277,7 @@ var DRIVE_CHROME_SKILL = {
8276
8277
  "tool call connects; there is nothing to restart.",
8277
8278
  "",
8278
8279
  "If no `mcp__chrome-devtools__` tool is listed at all, dsh was started without",
8279
- "the Divergent overlay: `diverge agents run dsh` starts it with one.",
8280
+ "the Divergent overlay: `diverge dsh` starts it with one.",
8280
8281
  ""
8281
8282
  ].join("\n")
8282
8283
  };
@@ -8292,7 +8293,6 @@ import path5 from "node:path";
8292
8293
  // src/agents/dsh.ts
8293
8294
  var import_yaml = __toESM(require_dist(), 1);
8294
8295
  import fs5 from "node:fs";
8295
- import { createRequire } from "node:module";
8296
8296
  import os2 from "node:os";
8297
8297
  import path4 from "node:path";
8298
8298
 
@@ -8366,6 +8366,20 @@ var PENDING_INVOCATION_STATUSES = ["pending", "approved", "executing"];
8366
8366
  function isPending(result) {
8367
8367
  return !!result && typeof result === "object" && result.status === "pending_approval";
8368
8368
  }
8369
+ function devTaskContinuation(env) {
8370
+ const raw = env.DIVERGENT_DEV_TASK?.trim();
8371
+ if (!raw) return void 0;
8372
+ let task;
8373
+ try {
8374
+ task = JSON.parse(raw);
8375
+ } catch {
8376
+ throw new CliError('DIVERGENT_DEV_TASK is not JSON; the launcher writes {"organizationId","host","worktreePath"}');
8377
+ }
8378
+ if (!task || typeof task !== "object" || Array.isArray(task)) {
8379
+ throw new CliError('DIVERGENT_DEV_TASK names no task; the launcher writes {"organizationId","host","worktreePath"}');
8380
+ }
8381
+ return { kind: "forge.dev-board.task-resume", payload: task };
8382
+ }
8369
8383
  var REQUEST_TIMEOUT_MS = 63e4;
8370
8384
  var TOKEN_REFRESH_MARGIN_SECONDS = 60;
8371
8385
  var ToolsClient = class {
@@ -8412,10 +8426,10 @@ var ToolsClient = class {
8412
8426
  if (this.settings.mode === "service") headers["x-divergent-user-id"] = this.settings.delegatedUserId;
8413
8427
  return headers;
8414
8428
  }
8415
- scoped(path18) {
8429
+ scoped(path20) {
8416
8430
  const query = new URLSearchParams({ organizationId: this.settings.organizationId });
8417
8431
  if (this.settings.projectId) query.set("projectId", this.settings.projectId);
8418
- return `${this.settings.apiUrl}${path18}?${query.toString()}`;
8432
+ return `${this.settings.apiUrl}${path20}?${query.toString()}`;
8419
8433
  }
8420
8434
  async send(method, url, body) {
8421
8435
  return this.request(method, url, body, void 0);
@@ -8451,7 +8465,7 @@ var ToolsClient = class {
8451
8465
  if (typeof parsed.error === "string" && parsed.error) message = parsed.error;
8452
8466
  } catch {
8453
8467
  }
8454
- if (response.status === 403 && /scope/i.test(message)) {
8468
+ if (response.status === 403 && /^insufficient scope$/i.test(message.trim())) {
8455
8469
  throw new CliError(`${response.status}: ${message}
8456
8470
  The signed-in token is missing a required scope. Re-run: ${LOGIN_HINT}`);
8457
8471
  }
@@ -8487,7 +8501,12 @@ var ToolsClient = class {
8487
8501
  * unwrapped here, once — a pending approval passes through as it is.
8488
8502
  */
8489
8503
  async callTool(name, args) {
8490
- const body = await this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), { toolName: name, arguments: args ?? {} });
8504
+ const continuation = devTaskContinuation(this.env);
8505
+ const body = await this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), {
8506
+ toolName: name,
8507
+ arguments: args ?? {},
8508
+ ...continuation ? { continuation } : {}
8509
+ });
8491
8510
  const outcome = body.result;
8492
8511
  if (outcome?.status !== "succeeded") return body;
8493
8512
  return { ...body, result: outcome.result };
@@ -8499,9 +8518,11 @@ var ToolsClient = class {
8499
8518
  * carries every call's outcome in `result.calls`.
8500
8519
  */
8501
8520
  async callSet(calls, summary) {
8521
+ const continuation = devTaskContinuation(this.env);
8502
8522
  const body = await this.send("POST", this.scoped("/api/plugins/functions/tools/sets"), {
8503
8523
  ...summary ? { summary } : {},
8504
- calls: calls.map((call2) => ({ toolName: call2.tool, arguments: call2.args ?? {} }))
8524
+ calls: calls.map((call2) => ({ toolName: call2.tool, arguments: call2.args ?? {} })),
8525
+ ...continuation ? { continuation } : {}
8505
8526
  });
8506
8527
  const outcome = body.result;
8507
8528
  if (outcome?.status !== "succeeded") return body;
@@ -8512,36 +8533,16 @@ var ToolsClient = class {
8512
8533
  return this.send("GET", this.scoped(`/api/plugins/functions/tools/invocations/${encodeURIComponent(invocationId)}`));
8513
8534
  }
8514
8535
  // ── Tenant plugins (RFC: Tenant Plugins) ──────────────────────────────
8515
- descriptorsUrl(pluginId) {
8516
- const base = `${this.settings.apiUrl}/api/plugins/forge/plugins/descriptors${pluginId ? `/${encodeURIComponent(pluginId)}` : ""}`;
8517
- return `${base}?${new URLSearchParams({ targetOrganizationId: this.settings.organizationId }).toString()}`;
8518
- }
8519
- /** Validate (dryRun) or publish a manifest for the organization the login is issued to. */
8520
- publishPlugin(manifest, options) {
8521
- return this.request(
8522
- "POST",
8523
- this.descriptorsUrl(),
8524
- { targetOrganizationId: this.settings.organizationId, manifest, dryRun: options.dryRun },
8525
- (status2, text2) => {
8526
- if (status2 !== 400) return void 0;
8527
- try {
8528
- const parsed = JSON.parse(text2);
8529
- if (Array.isArray(parsed.details)) return { ok: false, details: parsed.details.map(String) };
8530
- return { ok: false, details: [parsed.error ?? text2] };
8531
- } catch {
8532
- return { ok: false, details: [text2] };
8533
- }
8534
- }
8535
- ).then((outcome) => "ok" in outcome ? outcome : { ok: true, result: outcome });
8536
- }
8536
+ /**
8537
+ * The descriptors the organization published: the one read the CLI makes
8538
+ * outside Functions. Validating, publishing, reshaping and retiring are
8539
+ * tool invocations through `callTool` (ADR 0124).
8540
+ */
8537
8541
  async listPlugins() {
8538
- const body = await this.send("GET", this.descriptorsUrl());
8542
+ const query = new URLSearchParams({ targetOrganizationId: this.settings.organizationId });
8543
+ const body = await this.send("GET", `${this.settings.apiUrl}/api/plugins/forge/plugins/descriptors?${query.toString()}`);
8539
8544
  return body.plugins ?? [];
8540
8545
  }
8541
- async retirePlugin(pluginId) {
8542
- const body = await this.send("DELETE", this.descriptorsUrl(pluginId), { targetOrganizationId: this.settings.organizationId });
8543
- return body.plugin;
8544
- }
8545
8546
  };
8546
8547
 
8547
8548
  // src/agents/dsh.ts
@@ -8559,8 +8560,7 @@ function dshPatchYaml(options) {
8559
8560
  const api = options.surface === "openai" ? "openai-completions" : "anthropic-messages";
8560
8561
  const endpoint = options.surface === "openai" ? `${options.baseUrl}/v1` : options.baseUrl;
8561
8562
  return [
8562
- "# Divergent gateway overlay for dsh; written by `diverge agents setup dsh`.",
8563
- `# Boot with: ${tokenEnv}="$(diverge token)" dsh --profile <profile> --patch <this file>`,
8563
+ "# Divergent gateway overlay for dsh; written by `diverge dsh` on every launch.",
8564
8564
  "- id: llm-pi-ai",
8565
8565
  " config:",
8566
8566
  " providers:",
@@ -8581,6 +8581,7 @@ function dshPatchYaml(options) {
8581
8581
  " config:",
8582
8582
  ` provider: ${route}`,
8583
8583
  ` model: ${JSON.stringify(options.defaultModel)}`,
8584
+ ...options.reasoningEffort ? [` reasoningEffort: ${JSON.stringify(options.reasoningEffort)}`] : [],
8584
8585
  // dsh's native DeepSeek adapter needs a vendor key nobody here holds and,
8585
8586
  // where one exists, would send the organization's code straight to the
8586
8587
  // vendor past the gateway, its ledger, budget and policy. Its session
@@ -8591,6 +8592,11 @@ function dshPatchYaml(options) {
8591
8592
  " disabled: true",
8592
8593
  "- id: session-telemetry-otel",
8593
8594
  " disabled: true",
8595
+ // The headless profile turns dsh's credential store off, for a job given a
8596
+ // key in its environment. A launch keeps its token fresh in that file, so
8597
+ // it stays on in every profile.
8598
+ "- id: credentials",
8599
+ " disabled: false",
8594
8600
  ...dshSearchRows(),
8595
8601
  ...dshShellRows(),
8596
8602
  ...dshBrowserRows(),
@@ -8606,16 +8612,6 @@ function dshLaunchHeaderRows(launch) {
8606
8612
  ` ${DSH_LAUNCH_ID_HEADER}: ${JSON.stringify(launch.id)}`
8607
8613
  ];
8608
8614
  }
8609
- function dshVersionOf(binary) {
8610
- const dir = dshPackageDir(binary);
8611
- if (!dir) return null;
8612
- try {
8613
- const version = JSON.parse(fs5.readFileSync(path4.join(dir, "package.json"), "utf8")).version;
8614
- return typeof version === "string" && version.length > 0 ? version : null;
8615
- } catch {
8616
- return null;
8617
- }
8618
- }
8619
8615
  function dshShellRows() {
8620
8616
  return ["bash-sandbox", "pwsh-sandbox"].flatMap((id) => [
8621
8617
  `- id: ${id}`,
@@ -8649,11 +8645,6 @@ function dshSearchRows() {
8649
8645
  " searchTimeoutMs: 60000"
8650
8646
  ];
8651
8647
  }
8652
- function servedIds(overlay) {
8653
- const rows = (0, import_yaml.parse)(overlay) ?? [];
8654
- const route = rows.find((row) => row.id === "llm-pi-ai");
8655
- return Object.values(route?.config?.providers ?? {}).flatMap((provider) => (provider.models ?? []).map((entry) => entry.id));
8656
- }
8657
8648
  function mergeDshCredentials(existing, token, ref = "DIVERGENT_TOKEN") {
8658
8649
  const parsed = existing?.trim() ? (0, import_yaml.parse)(existing) : null;
8659
8650
  let document = {};
@@ -8668,95 +8659,41 @@ function mergeDshCredentials(existing, token, ref = "DIVERGENT_TOKEN") {
8668
8659
  if (document.records && Object.keys(document.records).length > 0) ordered.records = document.records;
8669
8660
  return (0, import_yaml.stringify)(ordered, { lineWidth: 0 });
8670
8661
  }
8671
- function dshSettingsDefaultModel(settingsText, surface) {
8672
- if (!settingsText?.trim()) return null;
8673
- const settings = (0, import_yaml.parse)(settingsText);
8674
- if (!settings || typeof settings !== "object") return null;
8675
- const pinned = settings["agent-default-model"];
8676
- if (!pinned || typeof pinned !== "object") return null;
8677
- const record = pinned;
8678
- if (record.provider !== `divergent-${surface}`) return null;
8679
- return record.model ? String(record.model) : null;
8680
- }
8681
- function stalePinMessage(settingsPath, pinned, surface, served) {
8682
- 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]})`;
8683
- }
8684
- var DIVERGENT_PRESET_ID = "divergent";
8685
- var DSH_USER_PRESETS_DIR = ".agent-presets";
8686
- function dshPackageDir(binary) {
8687
- if (/\.(cmd|bat)$/i.test(binary)) {
8688
- const dir2 = path4.join(path4.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8689
- return fs5.existsSync(path4.join(dir2, "package.json")) ? dir2 : null;
8690
- }
8691
- let dir;
8692
- try {
8693
- dir = path4.dirname(fs5.realpathSync(binary));
8694
- } catch {
8695
- return null;
8696
- }
8697
- for (; ; ) {
8698
- try {
8699
- if (JSON.parse(fs5.readFileSync(path4.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8700
- } catch {
8701
- }
8702
- const parent = path4.dirname(dir);
8703
- if (parent === dir) return null;
8704
- dir = parent;
8705
- }
8706
- }
8707
- function shippedPresetDir(packageDir, id = "standard") {
8708
- try {
8709
- const require2 = createRequire(path4.join(packageDir, "package.json"));
8710
- const dir = path4.join(path4.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
8711
- return fs5.existsSync(path4.join(dir, "agent.cordis.yml")) ? dir : null;
8712
- } catch {
8713
- return null;
8714
- }
8715
- }
8716
- function divergentPreset(shippedComposition) {
8717
- const lines = shippedComposition.split("\n");
8718
- const start = lines.findIndex((line) => /^- id: ['"]?tool-web['"]?\s*$/.test(line));
8719
- if (start === -1) throw new Error("the shipped preset composes no tool-web row");
8720
- let end = lines.findIndex((line, index) => index > start && /^- /.test(line));
8721
- if (end === -1) end = lines.length;
8722
- const row = lines.slice(start, end);
8723
- const search = row.findIndex((line) => /^\s+search:/.test(line));
8662
+ function dshSavedModel(patchText, surface) {
8663
+ if (!patchText?.trim()) return null;
8664
+ const rows = (0, import_yaml.parse)(patchText, { logLevel: "silent" });
8665
+ if (!Array.isArray(rows)) return null;
8666
+ const row = rows.find((entry) => entry?.id === "agent-default-model");
8667
+ const config = row?.config;
8668
+ if (config?.provider !== `divergent-${surface}` || !config.model) return null;
8669
+ return { model: String(config.model), ...config.reasoningEffort ? { reasoningEffort: String(config.reasoningEffort) } : {} };
8670
+ }
8671
+ 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;
8679
+ const indent = row[start].indexOf("-");
8680
+ const body = " ".repeat(indent + 2);
8681
+ let end = row.findIndex((line, index) => index > start && line.trim() !== "" && line.search(/\S/) <= indent);
8682
+ if (end === -1) end = row.length;
8683
+ const block = row.slice(start, end);
8684
+ const search = block.findIndex((line) => line.startsWith(`${body} search:`));
8724
8685
  if (search !== -1) {
8725
- row[search] = row[search].replace(/:.*$/, ": false");
8686
+ block[search] = `${body} search: false`;
8726
8687
  } else {
8727
- const config = row.findIndex((line) => /^ config:\s*$/.test(line));
8728
- if (config !== -1) row.splice(config + 1, 0, " search: false");
8729
- else row.splice(1, 0, " config:", " search: false");
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`);
8730
8691
  }
8731
- return [
8732
- `# The Divergent agent preset: dsh's shipped "standard" preset with the vendor web`,
8733
- "# search left out of the model's tools; platform tools, search included, are reached",
8734
- "# from the shell through `diverge tools`. Derived from the installed dsh by `diverge agents",
8735
- "# setup dsh` and `diverge agents run dsh` on every run; edits do not survive.",
8736
- ...lines.slice(0, start),
8737
- ...row,
8738
- ...lines.slice(end)
8739
- ].join("\n");
8692
+ return [...row.slice(0, start), ...block, ...row.slice(end)].join("\n").trimEnd() + "\n";
8740
8693
  }
8741
- var DIVERGENT_PRESET_METADATA = [
8742
- "name: Divergent",
8743
- "description: The standard coding agent, searching the web through the platform's search_online tool instead of a vendor key.",
8744
- "order: 0",
8745
- ""
8746
- ].join("\n");
8747
- function writeDivergentPreset(home, shippedDir) {
8748
- const dir = path4.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
8749
- writeTextAtomic(path4.join(dir, "agent.cordis.yml"), divergentPreset(fs5.readFileSync(path4.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
8750
- writeTextAtomic(path4.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
8751
- return dir;
8752
- }
8753
- function withDivergentPresetDefault(settingsText) {
8754
- const parsed = settingsText?.trim() ? (0, import_yaml.parse)(settingsText) : null;
8755
- const settings = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? { ...parsed } : {};
8756
- const presets = settings["agent-presets"];
8757
- if (presets && typeof presets === "object" && presets.default) return null;
8758
- settings["agent-presets"] = { ...presets ?? {}, default: DIVERGENT_PRESET_ID };
8759
- return (0, import_yaml.stringify)(settings, { lineWidth: 0 });
8694
+ function dshPresetOverrides(prefix) {
8695
+ 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("");
8760
8697
  }
8761
8698
 
8762
8699
  // src/skills/install.ts
@@ -8802,6 +8739,19 @@ function readSkillsManifest(root) {
8802
8739
  function few(names, limit = 6) {
8803
8740
  return names.length <= limit ? names.join(", ") : `${names.slice(0, limit).join(", ")} and ${names.length - limit} more`;
8804
8741
  }
8742
+ function isSkillFolder(folder) {
8743
+ return /^[a-z0-9][a-z0-9-]{0,63}$/.test(folder);
8744
+ }
8745
+ function retainedFolders(manifest, set2) {
8746
+ const retained = /* @__PURE__ */ new Map();
8747
+ for (const organization of set2.organizations) {
8748
+ const skipped = organization.skills.filter((skill) => !isSkillFolder(skill.folder));
8749
+ const held = Object.keys(manifest?.organizations[organization.id]?.skills ?? {});
8750
+ const kept = held.filter((folder) => skipped.some((skill) => folder.endsWith(`-${skill.name}`)));
8751
+ if (kept.length) retained.set(organization.id, kept);
8752
+ }
8753
+ return retained;
8754
+ }
8805
8755
  function heldFolders(manifest) {
8806
8756
  const held = /* @__PURE__ */ new Map();
8807
8757
  for (const [folder, version] of Object.entries(manifest?.flat ?? {})) held.set(folder, version);
@@ -8811,9 +8761,10 @@ function heldFolders(manifest) {
8811
8761
  }
8812
8762
  function retirable(manifest, set2) {
8813
8763
  const refreshed = new Set(set2.organizations.map((organization) => organization.id));
8764
+ const retained = new Set([...retainedFolders(manifest, set2).values()].flat());
8814
8765
  const folders = [...Object.keys(manifest?.flat ?? {}), ...Object.keys(manifest?.cli ?? {})];
8815
8766
  for (const [id, section] of Object.entries(manifest?.organizations ?? {})) {
8816
- if (set2.complete || refreshed.has(id)) folders.push(...Object.keys(section.skills));
8767
+ if (set2.complete || refreshed.has(id)) folders.push(...Object.keys(section.skills).filter((folder) => !retained.has(folder)));
8817
8768
  }
8818
8769
  return folders;
8819
8770
  }
@@ -8823,21 +8774,28 @@ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date(
8823
8774
  const written = [];
8824
8775
  const added = [];
8825
8776
  const updated = [];
8777
+ const skipped = [];
8826
8778
  const counts = [];
8827
8779
  const write = (skill) => {
8828
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(skill.folder)) throw new Error(`skill folder ${JSON.stringify(skill.folder)} is not a folder name`);
8780
+ if (!isSkillFolder(skill.folder)) {
8781
+ skipped.push(skill.folder);
8782
+ return;
8783
+ }
8829
8784
  writeTextAtomic(path5.join(root, skill.folder, "SKILL.md"), skill.content, 420);
8830
8785
  written.push(skill.folder);
8831
- const before = held.get(skill.folder);
8832
- if (before === void 0) added.push(skill.folder);
8833
- else if (before !== skill.updatedAt) updated.push(skill.folder);
8786
+ const before2 = held.get(skill.folder);
8787
+ if (before2 === void 0) added.push(skill.folder);
8788
+ else if (before2 !== skill.updatedAt) updated.push(skill.folder);
8834
8789
  };
8835
8790
  for (const organization of set2.organizations) {
8791
+ const before2 = written.length;
8836
8792
  organization.skills.forEach(write);
8837
- counts.push({ label: organization.name, count: organization.skills.length });
8793
+ counts.push({ label: organization.name, count: written.length - before2 });
8838
8794
  }
8795
+ const before = written.length;
8839
8796
  set2.cli.forEach(write);
8840
- if (set2.cli.length) counts.push({ label: "CLI", count: set2.cli.length });
8797
+ if (set2.cli.length) counts.push({ label: "CLI", count: written.length - before });
8798
+ const retained = retainedFolders(previous, set2);
8841
8799
  const removed = [];
8842
8800
  const kept = new Set(written);
8843
8801
  for (const folder of retirable(previous, set2)) {
@@ -8855,7 +8813,10 @@ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date(
8855
8813
  organizations[organization.id] = {
8856
8814
  name: organization.name,
8857
8815
  installedAt: stamp,
8858
- skills: Object.fromEntries(organization.skills.map((skill) => [skill.folder, skill.updatedAt]))
8816
+ skills: Object.fromEntries([
8817
+ ...(retained.get(organization.id) ?? []).map((folder) => [folder, previous.organizations[organization.id].skills[folder]]),
8818
+ ...organization.skills.filter((skill) => isSkillFolder(skill.folder)).map((skill) => [skill.folder, skill.updatedAt])
8819
+ ])
8859
8820
  };
8860
8821
  }
8861
8822
  const manifest = {
@@ -8865,7 +8826,7 @@ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date(
8865
8826
  organizations
8866
8827
  };
8867
8828
  writeJsonAtomic(path5.join(root, SKILLS_MANIFEST), manifest, 420);
8868
- return { root, written, added, updated, removed, counts };
8829
+ return { root, written, added, updated, removed, skipped, counts };
8869
8830
  }
8870
8831
  function skillsStatus(root, set2) {
8871
8832
  const manifest = readSkillsManifest(root);
@@ -8873,7 +8834,9 @@ function skillsStatus(root, set2) {
8873
8834
  const current = [];
8874
8835
  const behind = [];
8875
8836
  const missing = [];
8876
- const served = [...set2.organizations.flatMap((organization) => organization.skills), ...set2.cli];
8837
+ const all = [...set2.organizations.flatMap((organization) => organization.skills), ...set2.cli];
8838
+ const served = all.filter((skill) => isSkillFolder(skill.folder));
8839
+ const skipped = all.filter((skill) => !isSkillFolder(skill.folder)).map((skill) => skill.folder);
8877
8840
  for (const skill of served) {
8878
8841
  const before = held.get(skill.folder);
8879
8842
  if (before === void 0 || !fs6.existsSync(path5.join(root, skill.folder, "SKILL.md"))) missing.push(skill.folder);
@@ -8882,7 +8845,7 @@ function skillsStatus(root, set2) {
8882
8845
  }
8883
8846
  const servedFolders = new Set(served.map((skill) => skill.folder));
8884
8847
  const retired = [...new Set(retirable(manifest, set2))].filter((folder) => !servedFolders.has(folder));
8885
- return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired };
8848
+ return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired, skipped };
8886
8849
  }
8887
8850
 
8888
8851
  // src/skills/installable.ts
@@ -8940,8 +8903,11 @@ var skillsUsage = `diverge skills <list|install|refresh|status> [--organization-
8940
8903
 
8941
8904
  A skill is a procedure over the tools it names; you get the ones whose every
8942
8905
  tool you may run, in every organization the login can act in. Each lands as
8943
- <organization prefix>-<name> \u2014 the organization's short code, or a slug of
8944
- its name \u2014 and its SKILL.md opens with the organization its tools run in.
8906
+ <organization prefix>-<name> \u2014 the organization's handle, or a slug of
8907
+ its name; the platform shortens a longer one to the 64 characters a harness
8908
+ accepts \u2014 and its SKILL.md opens with the organization its tools run in.
8909
+ A folder that is not a skill name is skipped with a warning naming it, and
8910
+ the folder that skill was installed under before stays.
8945
8911
  --organization-id refreshes one organization and leaves the others as they
8946
8912
  are; without it the whole login is refreshed, and an organization the login
8947
8913
  no longer lists is retired. Without --harness, the harnesses present on this
@@ -8980,6 +8946,11 @@ function harnessesOf(flag, env) {
8980
8946
  function installSet(harness, skills) {
8981
8947
  return { cli: bundledSkills(harness), organizations: skills.organizations, complete: skills.complete };
8982
8948
  }
8949
+ var SKIPPED = "not installed, the folder is not a skill name (at most 64 lowercase letters, digits and hyphens)";
8950
+ function skippedNote(results) {
8951
+ const skipped = [...new Set(results.flatMap((result) => result.skipped))];
8952
+ return skipped.length ? [`${skipped.length} ${SKIPPED}: ${few(skipped)}`] : [];
8953
+ }
8983
8954
  function installReport(harness, result) {
8984
8955
  const holdings = result.counts.map((entry) => `${entry.label} ${entry.count}`).join(", ");
8985
8956
  const changes = [
@@ -9007,11 +8978,13 @@ async function refreshSkills(io, apiUrl, options = {}) {
9007
8978
  io.stderr(`warning: skills not refreshed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
9008
8979
  return;
9009
8980
  }
8981
+ const results = [];
9010
8982
  for (const harness of harnesses) {
9011
8983
  const result = installSkills(skillsRoot(harness, io.env), installSet(harness, skills), apiUrl);
9012
8984
  if (!options.quiet || result.added.length || result.updated.length || result.removed.length) io.stdout(installReport(harness, result));
8985
+ results.push(result);
9013
8986
  }
9014
- for (const note of skills.notes) io.stderr(`warning: ${note}`);
8987
+ for (const note of [...skills.notes, ...skippedNote(results)]) io.stderr(`warning: ${note}`);
9015
8988
  }
9016
8989
  async function runSkills(args, io) {
9017
8990
  const [verb, ...rest] = args;
@@ -9033,8 +9006,8 @@ async function runSkills(args, io) {
9033
9006
  const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9034
9007
  const harnesses = values.into ? [harnessesOf(values.harness ?? HARNESSES[0], io.env)[0]] : harnessesOf(values.harness, io.env);
9035
9008
  if (harnesses.length === 0) throw new CliError("No coding-agent harness on this machine yet (Claude Code, Codex or dsh); name one with --harness to write its folder anyway.");
9036
- for (const harness of harnesses) installForHarness(io, harness, skills, apiUrl, values.into);
9037
- for (const note of skills.notes) io.stderr(`warning: ${note}`);
9009
+ const results = harnesses.map((harness) => installForHarness(io, harness, skills, apiUrl, values.into));
9010
+ for (const note of [...skills.notes, ...skippedNote(results)]) io.stderr(`warning: ${note}`);
9038
9011
  return ExitCode.Ok;
9039
9012
  }
9040
9013
  case "refresh": {
@@ -9050,6 +9023,7 @@ async function runSkills(args, io) {
9050
9023
  if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
9051
9024
  if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
9052
9025
  if (status2.retired.length) parts.push(`${status2.retired.length} retired (${few(status2.retired)})`);
9026
+ if (status2.skipped.length) parts.push(`${status2.skipped.length} ${SKIPPED} (${few(status2.skipped)})`);
9053
9027
  behind ||= status2.behind.length + status2.missing.length + status2.retired.length > 0;
9054
9028
  io.stdout(`${harness}: ${parts.join(", ")} in ${status2.root}${status2.installedAt ? ` (installed ${status2.installedAt})` : " (never installed)"}`);
9055
9029
  }
@@ -9076,7 +9050,9 @@ Signing in also installs the login's skills \u2014 every organization it can act
9076
9050
  in, side by side \u2014 into the coding-agent harnesses on this machine.
9077
9051
  --as <email> names the account: the platform signs that account in instead of
9078
9052
  reusing whatever session the browser holds, and issues nothing to any other.
9079
- Without --as, the account of the last login is used; switching is explicit.
9053
+ Without --as, the account of the last login is used; a browser that signs in
9054
+ any account but the expected one (the --as one, else the last login's) is
9055
+ refused and changes nothing, so this host's shared login switches only by --as.
9080
9056
  Without --scopes, the login asks for the scopes the platform registers for
9081
9057
  this CLI \u2014 everything it and forge use \u2014 and holds what your roles grant; a
9082
9058
  scope the platform adds later reaches this login on its next refresh.
@@ -9094,6 +9070,16 @@ function resolveLoginHint(flag, previous) {
9094
9070
  const remembered = previous.account?.trim().toLowerCase();
9095
9071
  return remembered || void 0;
9096
9072
  }
9073
+ function refuseAnotherAccount(signedIn, expected, context) {
9074
+ const account = signedIn.trim().toLowerCase();
9075
+ if (account === expected) return;
9076
+ const asked = context.named ? `You asked to sign in as ${expected}` : `This host's login for ${context.apiUrl} is ${expected}`;
9077
+ throw new CliError(
9078
+ `${asked}, but the browser signed in ${account}; nothing was changed.
9079
+ To replace this host's login, for every session on it: diverge login --as ${account}
9080
+ To keep a second identity beside it: DIVERGE_HOME=<another directory> diverge login --as ${account}`
9081
+ );
9082
+ }
9097
9083
  async function runLogin(args, io) {
9098
9084
  refuseInSession(io.env, "login");
9099
9085
  const { values } = parseArgs2({
@@ -9129,6 +9115,7 @@ async function runLogin(args, io) {
9129
9115
  } catch (error) {
9130
9116
  throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
9131
9117
  }
9118
+ if (loginHint) refuseAnotherAccount(info.email, loginHint, { named: Boolean(values.as), apiUrl });
9132
9119
  io.stdout(`Signed in as: ${info.email}`);
9133
9120
  const organizationId = await chooseDefaultOrganization(io, values["organization-id"], previous?.organization, info);
9134
9121
  const issued = await issueToken(source, { base_url: source.baseUrl, refresh_token: signIn.refreshToken, account: info.email }, organizationId);
@@ -9320,211 +9307,415 @@ function mergeOpencodeConfig(existing, options) {
9320
9307
  return config;
9321
9308
  }
9322
9309
 
9323
- // src/agents/run-dsh.ts
9324
- import { spawn as spawn2 } from "node:child_process";
9325
- import fs7 from "node:fs";
9310
+ // src/agents/rotate.ts
9311
+ import os7 from "node:os";
9326
9312
  import path9 from "node:path";
9327
- function writeDshToken(credentialsPath, accessToken) {
9328
- const existing = fs7.existsSync(credentialsPath) ? fs7.readFileSync(credentialsPath, "utf8") : null;
9329
- let merged;
9330
- try {
9331
- merged = mergeDshCredentials(existing, accessToken);
9332
- } catch (error) {
9333
- throw new CliError(`${credentialsPath}: ${error instanceof Error ? error.message : String(error)}`);
9334
- }
9335
- writeTextAtomic(credentialsPath, merged, 384);
9336
- }
9337
- function writeDivergeShim(binDir, cli, sessionTokenPath, platform = process.platform) {
9338
- if (platform === "win32") {
9339
- const file2 = path9.join(binDir, "diverge.cmd");
9340
- const run3 = [cli.command, ...cli.args].map((part) => `"${part}"`).join(" ");
9341
- writeTextAtomic(file2, `@setlocal\r
9342
- @set "${SESSION_TOKEN_VARIABLE}=${sessionTokenPath}"\r
9343
- @${run3} %*\r
9344
- `, 493);
9345
- return file2;
9346
- }
9347
- const quote = (part) => `'${part.replace(/'/g, `'\\''`)}'`;
9348
- const file = path9.join(binDir, "diverge");
9349
- writeTextAtomic(file, `#!/bin/sh
9350
- ${SESSION_TOKEN_VARIABLE}=${quote(sessionTokenPath)}
9351
- export ${SESSION_TOKEN_VARIABLE}
9352
- exec ${[cli.command, ...cli.args].map(quote).join(" ")} "$@"
9353
- `, 493);
9354
- return file;
9355
- }
9356
- function dshEnv(env, binDir) {
9357
- const out = {};
9358
- for (const [key, value] of Object.entries(env)) {
9359
- if (value !== void 0 && key !== "DIVERGENT_TOKEN" && key !== "DIVERGENT_ORG_ID") out[key] = value;
9360
- }
9361
- out.PATH = env.PATH ? `${binDir}${path9.delimiter}${env.PATH}` : binDir;
9362
- return out;
9363
- }
9364
- function dshArgs(overlayPath, args) {
9365
- const rest = args.includes("--profile") ? args : args.length === 0 ? ["--profile", "web"] : ["--profile", "headless", ...args];
9366
- return ["--patch", overlayPath, ...rest];
9313
+ function defaultTokenFilePath() {
9314
+ return path9.join(os7.homedir(), ".divergent", "token");
9367
9315
  }
9368
- function dshLaunch(binary, args, platform = process.platform, execPath = process.execPath) {
9369
- if (platform !== "win32" || !/\.(cmd|bat)$/i.test(binary)) return { command: binary, args };
9370
- const packageDir = path9.join(path9.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
9371
- let bin;
9372
- try {
9373
- bin = JSON.parse(fs7.readFileSync(path9.join(packageDir, "package.json"), "utf8")).bin;
9374
- } catch {
9375
- throw new CliError(`${binary} is not an npm shim for @deepseek-ai/dsh (no ${packageDir}); reinstall: npm install -g @deepseek-ai/dsh`);
9316
+ var MIN_ROTATE_INTERVAL_SECONDS = 60;
9317
+ async function rotateTokenFile(loop) {
9318
+ const write = loop.write ?? ((file, token) => writeTextAtomic(file, token, 384));
9319
+ const interval = Math.max(loop.intervalSeconds, MIN_ROTATE_INTERVAL_SECONDS) * 1e3;
9320
+ for (; ; ) {
9321
+ write(loop.tokenFile, await loop.freshToken());
9322
+ loop.report(`${(loop.now ?? (() => /* @__PURE__ */ new Date()))().toISOString().slice(11, 19)} token written to ${loop.tokenFile}`);
9323
+ if (loop.once) return 0;
9324
+ await loop.sleep(interval);
9376
9325
  }
9377
- const script = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null ? bin.dsh : void 0;
9378
- if (typeof script !== "string") throw new CliError(`${packageDir}/package.json declares no dsh entry; reinstall: npm install -g @deepseek-ai/dsh`);
9379
- return { command: execPath, args: [path9.join(packageDir, script), ...args] };
9380
9326
  }
9381
- async function keepTokenFresh(loop) {
9382
- let written = null;
9383
- while (loop.isRunning()) {
9384
- const token = await loop.freshToken();
9385
- if (token && token !== written) {
9386
- loop.write(token);
9387
- written = token;
9388
- }
9389
- for (let i = 0; i < 30; i += 1) {
9390
- if (!loop.isRunning()) return;
9391
- await loop.sleep(1e3);
9327
+
9328
+ // src/agents/self-command.ts
9329
+ import fs7 from "node:fs";
9330
+ import path10 from "node:path";
9331
+ function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
9332
+ const script = argv[1];
9333
+ if (!script) return { command: execPath, args: [] };
9334
+ if (platform !== "win32") {
9335
+ try {
9336
+ fs7.accessSync(script, fs7.constants.X_OK);
9337
+ return { command: path10.resolve(script), args: [] };
9338
+ } catch {
9392
9339
  }
9393
9340
  }
9341
+ return { command: execPath, args: [fs7.realpathSync(script)] };
9394
9342
  }
9395
- function runDsh(options) {
9396
- const writeToken = (token) => {
9397
- writeDshToken(options.credentialsPath, token);
9398
- writeTextAtomic(options.sessionTokenPath, `${token}
9399
- `, 384);
9400
- };
9401
- writeToken(options.initialToken);
9402
- writeDivergeShim(options.binDir, options.cli, options.sessionTokenPath);
9403
- const env = dshEnv(options.env, options.binDir);
9404
- const launch = dshLaunch(options.binary, dshArgs(options.overlayPath, options.args));
9405
- const child = spawn2(launch.command, launch.args, { env, stdio: "inherit" });
9406
- let exited = false;
9407
- let wake = null;
9408
- const finish = () => {
9409
- exited = true;
9410
- wake?.();
9411
- };
9412
- const done = new Promise((resolve, reject) => {
9413
- child.on("error", (error) => {
9414
- finish();
9415
- reject(error);
9416
- });
9417
- child.on("exit", (code2, signal) => {
9418
- finish();
9419
- resolve(code2 ?? (signal ? 128 + 15 : 1));
9420
- });
9421
- });
9422
- const ignore = () => {
9423
- };
9424
- process.on("SIGINT", ignore);
9425
- const terminate = () => child.kill("SIGTERM");
9426
- process.on("SIGTERM", terminate);
9427
- const loop = keepTokenFresh({
9428
- freshToken: options.freshToken,
9429
- write: writeToken,
9430
- isRunning: () => !exited,
9431
- sleep: (ms) => new Promise((resolve) => {
9432
- const timer = setTimeout(() => {
9433
- wake = null;
9434
- resolve();
9435
- }, ms);
9436
- wake = () => {
9437
- clearTimeout(timer);
9438
- wake = null;
9439
- resolve();
9440
- };
9441
- })
9442
- });
9443
- return done.finally(async () => {
9444
- process.off("SIGINT", ignore);
9445
- process.off("SIGTERM", terminate);
9446
- await loop;
9447
- });
9343
+ function shellCommand(self, ...trailing) {
9344
+ return [self.command, ...self.args, ...trailing].map((part) => /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part).join(" ");
9448
9345
  }
9449
9346
 
9450
- // src/agents/dsh-session-publisher.ts
9451
- import fs8 from "node:fs";
9452
- import path10 from "node:path";
9347
+ // src/commands/agents.ts
9348
+ var agentsUsage = `diverge agents <setup|rotate|status> ...
9453
9349
 
9454
- // src/agents/dsh-session-log.ts
9455
- import zlib from "node:zlib";
9456
- var DSH_SESSION_LOG_FILE = "session.jsonl.zstd";
9457
- var DSH_DELTA_RECORD_TYPES = /* @__PURE__ */ new Set(["assistant/chunk", "reasoning-chunks", "text-chunks", "tool-call-chunks"]);
9458
- function dshProjectKey(cwd) {
9459
- if (cwd.length === 0) throw new Error("cannot encode an empty project path");
9460
- let readable = "";
9461
- let separatorRun = false;
9462
- for (let i = 0; i < cwd.length; i += 1) {
9463
- const code2 = cwd.charCodeAt(i);
9464
- const ch = String.fromCharCode(code2);
9465
- if (ch === "/" || ch === "\\" || ch === ":") {
9466
- if (!separatorRun) readable += "-";
9467
- separatorRun = true;
9468
- } else if (ch !== "~" && /^[A-Za-z0-9._-]$/.test(ch)) {
9469
- readable += ch;
9470
- separatorRun = false;
9471
- } else {
9472
- readable += `~${code2.toString(16).toUpperCase().padStart(4, "0")}`;
9473
- separatorRun = false;
9474
- }
9475
- }
9476
- return `--${(readable.replace(/^-+/, "") || "root").slice(0, 251)}--`;
9350
+ setup claude [--scope user|project] [--project-id <id>] [--model-discovery] [--organization-id <id>]
9351
+ setup opencode [--surface anthropic|openai] [--config <path>] [--organization-id <id>]
9352
+ setup codex [--project-id <id>] [--config <path>] [--print] [--organization-id <id>]
9353
+ rotate [--into <path>] [--interval <seconds>] [--once] [--organization-id <id>]
9354
+ keep a token file fresh for a client that reads a file, not a helper (default ~/.divergent/token, every 600 s)
9355
+ status [--organization-id <id>]
9356
+
9357
+ Point a coding agent at the platform's model gateway. A sign-in is started
9358
+ when the cached login does not serve the gateway for the organization. dsh
9359
+ needs no setup: diverge dsh starts it on the gateway.`;
9360
+ function surfaceOf(value, fallback) {
9361
+ if (value === void 0) return fallback;
9362
+ if (SURFACES.includes(value)) return value;
9363
+ throw new CliError(`--surface must be one of: ${SURFACES.join(", ")}`);
9477
9364
  }
9478
- function zstdAvailable() {
9479
- return typeof zlib.zstdDecompressSync === "function";
9365
+ function readJson(file) {
9366
+ if (!fs8.existsSync(file)) return null;
9367
+ return JSON.parse(fs8.readFileSync(file, "utf8"));
9480
9368
  }
9481
- var ZSTD_MAGIC = 4247762216;
9482
- var SKIPPABLE_MAGIC_LOW = 407710288;
9483
- var SKIPPABLE_MAGIC_HIGH = 407710303;
9484
- function zstdFrameEnd(buffer, start) {
9485
- if (buffer.length - start < 4) return null;
9486
- const magic = buffer.readUInt32LE(start);
9487
- if (magic >= SKIPPABLE_MAGIC_LOW && magic <= SKIPPABLE_MAGIC_HIGH) {
9488
- if (buffer.length - start < 8) return null;
9489
- const end = start + 8 + buffer.readUInt32LE(start + 4);
9490
- return end <= buffer.length ? end : null;
9491
- }
9492
- if (magic !== ZSTD_MAGIC) return null;
9493
- if (buffer.length - start < 5) return null;
9494
- const descriptor = buffer[start + 4];
9495
- const contentSizeFlag = descriptor >> 6;
9496
- const singleSegment = descriptor >> 5 & 1;
9497
- const checksum = descriptor >> 2 & 1;
9498
- const dictionaryFlag = descriptor & 3;
9499
- let pos = start + 5;
9500
- if (!singleSegment) pos += 1;
9501
- pos += [0, 1, 2, 4][dictionaryFlag];
9502
- pos += contentSizeFlag === 0 ? singleSegment : [0, 2, 4, 8][contentSizeFlag];
9503
- for (; ; ) {
9504
- if (pos + 3 > buffer.length) return null;
9505
- const header = buffer[pos] | buffer[pos + 1] << 8 | buffer[pos + 2] << 16;
9506
- const last = header & 1;
9507
- const type = header >> 1 & 3;
9508
- const size = header >> 3;
9509
- if (type === 3) return null;
9510
- pos += 3 + (type === 1 ? 1 : size);
9511
- if (pos > buffer.length) return null;
9512
- if (last) break;
9369
+ async function servedModels(apiUrl, token, surface) {
9370
+ return (await listModels(apiUrl, token.access_token, surface)).map(servedModel);
9371
+ }
9372
+ async function skillsFor(io, apiUrl, harness, organizationId) {
9373
+ try {
9374
+ const skills = await loginSkillSet(apiUrl, io.env, organizationId?.trim() || void 0);
9375
+ const result = installForHarness(io, harness, skills, apiUrl);
9376
+ for (const note of [...skills.notes, ...skippedNote([result])]) io.stderr(`warning: ${note}`);
9377
+ } catch (error) {
9378
+ io.stderr(`warning: skills not installed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
9513
9379
  }
9514
- if (checksum) pos += 4;
9515
- return pos <= buffer.length ? pos : null;
9516
9380
  }
9517
- function decodeZstdFrames(buffer) {
9518
- const parts = [];
9519
- let offset = 0;
9520
- while (offset < buffer.length) {
9521
- const end = zstdFrameEnd(buffer, offset);
9522
- if (end === null) break;
9523
- if (buffer.readUInt32LE(offset) === ZSTD_MAGIC) {
9524
- try {
9525
- parts.push(zlib.zstdDecompressSync(buffer.subarray(offset, end)));
9526
- } catch (error) {
9527
- return { text: Buffer.concat(parts).toString("utf8"), consumed: offset, failed: { at: offset, error: error instanceof Error ? error.message : String(error) } };
9381
+ function organizationArgs(organizationId) {
9382
+ const trimmed = organizationId?.trim();
9383
+ return trimmed ? ["--organization-id", trimmed] : [];
9384
+ }
9385
+ async function setup(args, io) {
9386
+ const [agent, ...rest] = args;
9387
+ const apiUrl = resolveApiUrl(void 0, io.env);
9388
+ switch (agent) {
9389
+ case "claude": {
9390
+ const { values } = parseArgs3({ args: rest, options: { scope: { type: "string", default: "user" }, "project-id": { type: "string" }, "organization-id": { type: "string" }, "model-discovery": { type: "boolean", default: false } }, strict: true });
9391
+ if (values.scope !== "user" && values.scope !== "project") throw new CliError("--scope must be user or project");
9392
+ const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9393
+ const file = claudeSettingsPath(values.scope);
9394
+ const { settings, warnings } = mergeClaudeSettings(readJson(file), {
9395
+ baseUrl: anthropicBaseUrl(apiUrl),
9396
+ tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
9397
+ projectId: values["project-id"],
9398
+ modelDiscovery: values["model-discovery"],
9399
+ // Claude Code scans its skill folder when a session starts, so that is when the login's skills are brought current.
9400
+ skillsRefresh: values.scope === "user" ? shellCommand(resolveSelfCommand(), "skills", "refresh") : void 0
9401
+ });
9402
+ writeJsonAtomic(file, settings);
9403
+ io.stdout(`Claude Code \u2192 ${anthropicBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
9404
+ for (const warning of warnings) io.stderr(`warning: ${warning}`);
9405
+ await skillsFor(io, apiUrl, "claude", values["organization-id"]);
9406
+ return ExitCode.Ok;
9407
+ }
9408
+ case "opencode": {
9409
+ const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
9410
+ const surface = surfaceOf(values.surface, "anthropic");
9411
+ const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9412
+ const baseUrl = surfaceBaseUrl(apiUrl, surface);
9413
+ let models = [];
9414
+ try {
9415
+ models = await servedModels(apiUrl, token, surface);
9416
+ } catch (error) {
9417
+ io.stderr(`warning: ${error instanceof Error ? error.message : String(error)}; writing the provider without a model list`);
9418
+ }
9419
+ const file = values.config ?? opencodeConfigPath();
9420
+ writeJsonAtomic(file, mergeOpencodeConfig(readJson(file), { baseUrl, surface, models }));
9421
+ io.stdout(`OpenCode provider divergent-${surface} \u2192 ${baseUrl}. Wrote ${file}.`);
9422
+ io.stdout("Keep DIVERGENT_TOKEN fresh for the session (user tokens expire; the login refreshes):");
9423
+ io.stdout(` export DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})"`);
9424
+ return ExitCode.Ok;
9425
+ }
9426
+ case "codex": {
9427
+ const { values } = parseArgs3({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
9428
+ const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"], organizationId: values["organization-id"] });
9429
+ if (values.print) {
9430
+ io.stdout(block.trimEnd());
9431
+ return ExitCode.Ok;
9432
+ }
9433
+ const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9434
+ const file = values.config ?? codexConfigPath();
9435
+ const existing = fs8.existsSync(file) ? fs8.readFileSync(file, "utf8") : null;
9436
+ writeTextAtomic(file, mergeCodexConfig(existing, block));
9437
+ io.stdout(`Codex provider divergent \u2192 ${openaiBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
9438
+ io.stdout('Select it per run with: codex -c model_provider=divergent (or set model_provider = "divergent" in the config)');
9439
+ await skillsFor(io, apiUrl, "codex", values["organization-id"]);
9440
+ return ExitCode.Ok;
9441
+ }
9442
+ default:
9443
+ throw unknownVerb("agents", agentsUsage, ["setup"], agent, rest);
9444
+ }
9445
+ }
9446
+ async function status(args, io) {
9447
+ const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
9448
+ const apiUrl = resolveApiUrl(void 0, io.env);
9449
+ const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9450
+ const claims = tokenClaims(token.access_token);
9451
+ io.stdout(`Gateway: ${apiUrl}/api/plugins/inference/{anthropic,openai}`);
9452
+ io.stdout(`Organization: ${claims.organizationId}`);
9453
+ io.stdout(`Scopes: ${(claims.scopes ?? []).join(" ")}`);
9454
+ for (const surface of SURFACES) {
9455
+ const models = (await servedModels(apiUrl, token, surface)).map((model) => model.id);
9456
+ io.stdout(`Models (${surface}): ${models.length ? models.join(", ") : "(none served on this surface)"}`);
9457
+ }
9458
+ return ExitCode.Ok;
9459
+ }
9460
+ async function rotate(args, io) {
9461
+ const { values } = parseArgs3({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
9462
+ const interval = Number(values.interval);
9463
+ if (!Number.isFinite(interval) || interval <= 0) throw new CliError("--interval must be a positive number of seconds");
9464
+ const apiUrl = resolveApiUrl(void 0, io.env);
9465
+ const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9466
+ const source = tokenSource(io.env, apiUrl);
9467
+ const tokenFilePath = values.into ?? defaultTokenFilePath();
9468
+ io.stderr(`Keeping ${tokenFilePath} fresh as you (organization ${tokenClaims(token.access_token).organizationId})`);
9469
+ return rotateTokenFile({
9470
+ tokenFile: tokenFilePath,
9471
+ intervalSeconds: interval,
9472
+ once: values.once,
9473
+ freshToken: async () => (await getUserToken(source, { freshForSeconds: Math.max(interval, 60) + 60, organizationId: values["organization-id"] })).access_token,
9474
+ report: (line) => io.stderr(line),
9475
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
9476
+ });
9477
+ }
9478
+ async function runAgents(args, io) {
9479
+ const [verb, ...rest] = args;
9480
+ switch (verb) {
9481
+ case "setup":
9482
+ return setup(rest, io);
9483
+ case "rotate":
9484
+ return rotate(rest, io);
9485
+ case "status":
9486
+ return status(rest, io);
9487
+ default:
9488
+ throw unknownVerb("agents", agentsUsage, [], verb, rest);
9489
+ }
9490
+ }
9491
+
9492
+ // src/commands/dsh.ts
9493
+ import { spawn as spawn5 } from "node:child_process";
9494
+ import { randomUUID } from "node:crypto";
9495
+ import fs15 from "node:fs";
9496
+ import os8 from "node:os";
9497
+ import path18 from "node:path";
9498
+ import { parseArgs as parseArgs4 } from "node:util";
9499
+
9500
+ // src/agents/dsh-install.ts
9501
+ import fs10 from "node:fs";
9502
+ import path13 from "node:path";
9503
+
9504
+ // src/npm.ts
9505
+ import { spawn as spawn2 } from "node:child_process";
9506
+ import path12 from "node:path";
9507
+
9508
+ // src/fs/path.ts
9509
+ import fs9 from "node:fs";
9510
+ import path11 from "node:path";
9511
+ function findOnPath(name, env = process.env, platform = process.platform) {
9512
+ const dirs = (env.PATH ?? "").split(path11.delimiter).filter(Boolean);
9513
+ const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
9514
+ const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
9515
+ for (const dir of dirs) {
9516
+ for (const candidate of candidates) {
9517
+ const full = path11.join(dir, candidate);
9518
+ try {
9519
+ fs9.accessSync(full, fs9.constants.X_OK);
9520
+ if (fs9.statSync(full).isFile()) return full;
9521
+ } catch {
9522
+ }
9523
+ }
9524
+ }
9525
+ return null;
9526
+ }
9527
+
9528
+ // src/npm.ts
9529
+ function runNpm(args, env = process.env, platform = process.platform, stdio = "inherit") {
9530
+ let command = "npm";
9531
+ const argv = [...args];
9532
+ if (platform === "win32") {
9533
+ const npm = findOnPath("npm", env, platform);
9534
+ if (!npm) throw new CliError("npm was not found on PATH; install Node.js with npm and try again.");
9535
+ command = npm;
9536
+ if (/\.(cmd|bat)$/i.test(npm)) {
9537
+ command = process.execPath;
9538
+ argv.unshift(path12.join(path12.dirname(npm), "node_modules", "npm", "bin", "npm-cli.js"));
9539
+ }
9540
+ }
9541
+ return new Promise((resolve, reject) => {
9542
+ const child = spawn2(command, argv, { env, stdio });
9543
+ child.on("error", reject);
9544
+ child.on("exit", (code2) => resolve(code2 ?? 1));
9545
+ });
9546
+ }
9547
+
9548
+ // src/agents/dsh-version.ts
9549
+ var TESTED_DSH_VERSION = "0.2.1-rc.5";
9550
+
9551
+ // src/agents/dsh-install.ts
9552
+ var DSH_PACKAGE = "@deepseek-ai/dsh";
9553
+ var REGISTRY = "https://registry.npmjs.org";
9554
+ var RETIRED_AFTER_MS = 7 * 24 * 60 * 60 * 1e3;
9555
+ function dshInstallRoot(env) {
9556
+ return path13.join(configDir(env), "harness", "dsh");
9557
+ }
9558
+ function dshPackageIn(prefix) {
9559
+ return path13.join(prefix, "node_modules", "@deepseek-ai", "dsh");
9560
+ }
9561
+ function readManifest(prefix) {
9562
+ try {
9563
+ return JSON.parse(fs10.readFileSync(path13.join(dshPackageIn(prefix), "package.json"), "utf8"));
9564
+ } catch {
9565
+ return null;
9566
+ }
9567
+ }
9568
+ function nodeSatisfies(range, nodeVersion) {
9569
+ const floor = /^>=\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(range?.trim() ?? "");
9570
+ if (!floor) return true;
9571
+ const have = nodeVersion.replace(/^v/, "").split(".").map(Number);
9572
+ for (let i = 0; i < 3; i += 1) {
9573
+ const want = Number(floor[i + 1] ?? 0);
9574
+ if ((have[i] ?? 0) !== want) return (have[i] ?? 0) > want;
9575
+ }
9576
+ return true;
9577
+ }
9578
+ function requireNode(version, range, nodeVersion) {
9579
+ if (!nodeSatisfies(range, nodeVersion)) {
9580
+ throw new CliError(`dsh ${version} needs Node ${range}; this is Node ${nodeVersion}. Install a newer Node, then run diverge dsh again.`);
9581
+ }
9582
+ }
9583
+ var realDeps = (env) => ({
9584
+ // npm 11 runs only the install scripts a project approves. dsh's own restores
9585
+ // the executable bit on node-pty's helper, without which a macOS shell cannot
9586
+ // start; the rest check prebuilt binaries or print a notice. An older npm
9587
+ // ignores the field and runs them all. stdout goes to stderr: a headless
9588
+ // 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]);
9592
+ },
9593
+ engines: async (version) => (await fetchJson(`${REGISTRY}/${DSH_PACKAGE}/${version}`, { timeoutMs: 15e3 })).engines?.node,
9594
+ nodeVersion: process.version,
9595
+ now: Date.now
9596
+ });
9597
+ async function ensureDsh(io, version = TESTED_DSH_VERSION, deps = realDeps(io.env)) {
9598
+ const root = dshInstallRoot(io.env);
9599
+ const prefix = path13.join(root, version);
9600
+ let manifest = readManifest(prefix);
9601
+ if (manifest) {
9602
+ requireNode(version, manifest.engines?.node, deps.nodeVersion);
9603
+ } else {
9604
+ let engines;
9605
+ try {
9606
+ engines = await deps.engines(version);
9607
+ } catch {
9608
+ }
9609
+ requireNode(version, engines, deps.nodeVersion);
9610
+ io.stderr(`Installing dsh ${version} (about a minute and nearly 1 GB, once per dsh release)\u2026`);
9611
+ const staging = `${prefix}.installing-${process.pid}`;
9612
+ fs10.mkdirSync(staging, { recursive: true });
9613
+ try {
9614
+ const code2 = await deps.npmInstall(staging, `${DSH_PACKAGE}@${version}`);
9615
+ if (code2 !== 0) throw new CliError(`Installing dsh ${version} failed: npm exited with ${code2}. Check the network and run diverge dsh again.`);
9616
+ try {
9617
+ fs10.renameSync(staging, prefix);
9618
+ } catch (error) {
9619
+ if (!readManifest(prefix)) throw error;
9620
+ }
9621
+ } finally {
9622
+ fs10.rmSync(staging, { recursive: true, force: true });
9623
+ }
9624
+ manifest = readManifest(prefix);
9625
+ if (!manifest) throw new CliError(`Installing dsh ${version} left no package at ${dshPackageIn(prefix)}`);
9626
+ }
9627
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.dsh;
9628
+ if (!bin) throw new CliError(`dsh ${version} declares no entry script`);
9629
+ retireOldVersions(root, version, deps.now());
9630
+ return { command: process.execPath, args: [path13.join(dshPackageIn(prefix), bin)] };
9631
+ }
9632
+ function retireOldVersions(root, current, now) {
9633
+ try {
9634
+ const moment = new Date(now);
9635
+ fs10.utimesSync(path13.join(root, current), moment, moment);
9636
+ for (const name of fs10.readdirSync(root)) {
9637
+ if (name === current) continue;
9638
+ const dir = path13.join(root, name);
9639
+ if (now - fs10.statSync(dir).mtimeMs > RETIRED_AFTER_MS) fs10.rmSync(dir, { recursive: true, force: true });
9640
+ }
9641
+ } catch {
9642
+ }
9643
+ }
9644
+
9645
+ // src/agents/dsh-session-publisher.ts
9646
+ import fs12 from "node:fs";
9647
+ import path15 from "node:path";
9648
+
9649
+ // src/agents/dsh-session-log.ts
9650
+ import fs11 from "node:fs";
9651
+ import path14 from "node:path";
9652
+ import zlib from "node:zlib";
9653
+ var DSH_SESSION_LOG_FILE = /^session\.v(\d+)\.jsonl\.zstd$/;
9654
+ function dshSessionLogFile(dir) {
9655
+ let names;
9656
+ try {
9657
+ names = fs11.readdirSync(dir);
9658
+ } catch {
9659
+ return null;
9660
+ }
9661
+ let newest = null;
9662
+ for (const name of names) {
9663
+ const format = Number(DSH_SESSION_LOG_FILE.exec(name)?.[1]);
9664
+ if (!Number.isNaN(format) && (!newest || format > newest.format)) newest = { name, format };
9665
+ }
9666
+ return newest && path14.join(dir, newest.name);
9667
+ }
9668
+ var DSH_DELTA_RECORD_TYPES = /* @__PURE__ */ new Set(["assistant/chunk", "reasoning-chunks", "text-chunks", "tool-call-chunks"]);
9669
+ function zstdAvailable() {
9670
+ return typeof zlib.zstdDecompressSync === "function";
9671
+ }
9672
+ var ZSTD_MAGIC = 4247762216;
9673
+ var SKIPPABLE_MAGIC_LOW = 407710288;
9674
+ var SKIPPABLE_MAGIC_HIGH = 407710303;
9675
+ function zstdFrameEnd(buffer, start) {
9676
+ if (buffer.length - start < 4) return null;
9677
+ const magic = buffer.readUInt32LE(start);
9678
+ if (magic >= SKIPPABLE_MAGIC_LOW && magic <= SKIPPABLE_MAGIC_HIGH) {
9679
+ if (buffer.length - start < 8) return null;
9680
+ const end = start + 8 + buffer.readUInt32LE(start + 4);
9681
+ return end <= buffer.length ? end : null;
9682
+ }
9683
+ if (magic !== ZSTD_MAGIC) return null;
9684
+ if (buffer.length - start < 5) return null;
9685
+ const descriptor = buffer[start + 4];
9686
+ const contentSizeFlag = descriptor >> 6;
9687
+ const singleSegment = descriptor >> 5 & 1;
9688
+ const checksum = descriptor >> 2 & 1;
9689
+ const dictionaryFlag = descriptor & 3;
9690
+ let pos = start + 5;
9691
+ if (!singleSegment) pos += 1;
9692
+ pos += [0, 1, 2, 4][dictionaryFlag];
9693
+ pos += contentSizeFlag === 0 ? singleSegment : [0, 2, 4, 8][contentSizeFlag];
9694
+ for (; ; ) {
9695
+ if (pos + 3 > buffer.length) return null;
9696
+ const header = buffer[pos] | buffer[pos + 1] << 8 | buffer[pos + 2] << 16;
9697
+ const last = header & 1;
9698
+ const type = header >> 1 & 3;
9699
+ const size = header >> 3;
9700
+ if (type === 3) return null;
9701
+ pos += 3 + (type === 1 ? 1 : size);
9702
+ if (pos > buffer.length) return null;
9703
+ if (last) break;
9704
+ }
9705
+ if (checksum) pos += 4;
9706
+ return pos <= buffer.length ? pos : null;
9707
+ }
9708
+ function decodeZstdFrames(buffer) {
9709
+ const parts = [];
9710
+ let offset = 0;
9711
+ while (offset < buffer.length) {
9712
+ const end = zstdFrameEnd(buffer, offset);
9713
+ if (end === null) break;
9714
+ if (buffer.readUInt32LE(offset) === ZSTD_MAGIC) {
9715
+ try {
9716
+ parts.push(zlib.zstdDecompressSync(buffer.subarray(offset, end)));
9717
+ } catch (error) {
9718
+ return { text: Buffer.concat(parts).toString("utf8"), consumed: offset, failed: { at: offset, error: error instanceof Error ? error.message : String(error) } };
9528
9719
  }
9529
9720
  }
9530
9721
  offset = end;
@@ -9587,11 +9778,18 @@ var STALLED_TICKS = 30;
9587
9778
  var CAPTURE_NOTICES = {
9588
9779
  off: "This organization does not record agent sessions (Forge installation config agentSessions.capture is off); nothing is sent.",
9589
9780
  metadata: "Recorded: metadata only \u2014 which tools ran and how many failed, tokens, models and timing; no prompt, reply, tool argument or result.",
9590
- conversation: "Recorded: the whole conversation \u2014 every prompt, reply, tool call and its result. Yours to read; anyone else in the organization needs the Agent conversations role, and every such read is recorded on the session."
9781
+ conversation: "Recorded: the whole conversation \u2014 every prompt, reply, tool call and its result. Yours to read; anyone else in the organization needs the Agent Conversations role, and every such read is recorded on the session."
9591
9782
  };
9592
- function eventBytes(event) {
9593
- return Buffer.byteLength(JSON.stringify(event), "utf8");
9594
- }
9783
+ function readDirectories(dir) {
9784
+ try {
9785
+ return fs12.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? [entry.name] : []);
9786
+ } catch {
9787
+ return [];
9788
+ }
9789
+ }
9790
+ function eventBytes(event) {
9791
+ return Buffer.byteLength(JSON.stringify(event), "utf8");
9792
+ }
9595
9793
  function nextBatch(pending) {
9596
9794
  const batch = [];
9597
9795
  let bytes = 0;
@@ -9620,10 +9818,6 @@ var DshSessionPublisher = class {
9620
9818
  /** The level the person was last told. */
9621
9819
  announced = null;
9622
9820
  ticking = null;
9623
- /** The directory dsh writes this launch's sessions under. */
9624
- get projectDir() {
9625
- return path10.join(this.deps.sessionsRoot, dshProjectKey(this.deps.cwd));
9626
- }
9627
9821
  start() {
9628
9822
  if (this.running) return;
9629
9823
  if (!zstdAvailable()) {
@@ -9676,54 +9870,63 @@ var DshSessionPublisher = class {
9676
9870
  await this.flush(session);
9677
9871
  }
9678
9872
  }
9873
+ /**
9874
+ * Every session this launch opened or continued, in any project: dsh files a
9875
+ * session under its workspace, and the web UI's workspace is the person's
9876
+ * choice (its default is a folder of dsh's own), not where the launch was
9877
+ * started; and it reopens the last conversation, which is older than the
9878
+ * launch and writes to its log again. dsh's home holds one launch's sessions
9879
+ * at a time in practice; a second launch on the same home would record the
9880
+ * first one's as well.
9881
+ */
9679
9882
  discover() {
9680
- let entries;
9681
- try {
9682
- entries = fs8.readdirSync(this.projectDir, { withFileTypes: true });
9683
- } catch {
9684
- return;
9685
- }
9686
- for (const entry of entries) {
9687
- if (!entry.isDirectory() || this.sessions.has(entry.name)) continue;
9688
- const dir = path10.join(this.projectDir, entry.name);
9689
- const file = path10.join(dir, DSH_SESSION_LOG_FILE);
9690
- let stat;
9691
- try {
9692
- stat = fs8.statSync(dir);
9693
- } catch {
9694
- continue;
9883
+ const cutoff = this.deps.launchedAt - LAUNCH_SLACK_MS;
9884
+ for (const project of readDirectories(this.deps.sessionsRoot)) {
9885
+ const projectDir = path15.join(this.deps.sessionsRoot, project);
9886
+ for (const name of readDirectories(projectDir)) {
9887
+ if (this.sessions.has(name)) continue;
9888
+ const dir = path15.join(projectDir, name);
9889
+ const log = dshSessionLogFile(dir);
9890
+ let touched;
9891
+ try {
9892
+ const stat = fs12.statSync(dir);
9893
+ touched = Math.max(Math.min(stat.birthtimeMs || Number.POSITIVE_INFINITY, stat.mtimeMs), log ? fs12.statSync(log).mtimeMs : 0);
9894
+ } catch {
9895
+ continue;
9896
+ }
9897
+ if (touched < cutoff) continue;
9898
+ this.sessions.set(name, {
9899
+ dir,
9900
+ file: null,
9901
+ offset: 0,
9902
+ header: null,
9903
+ pending: [],
9904
+ opened: false,
9905
+ lastSeq: -1,
9906
+ reportedFailure: null,
9907
+ unreadableReported: false,
9908
+ stalledAt: 0,
9909
+ stalledTicks: 0,
9910
+ dead: false
9911
+ });
9695
9912
  }
9696
- const born = Math.min(stat.birthtimeMs || Number.POSITIVE_INFINITY, stat.mtimeMs);
9697
- if (born < this.deps.launchedAt - LAUNCH_SLACK_MS) continue;
9698
- this.sessions.set(entry.name, {
9699
- dir,
9700
- file,
9701
- offset: 0,
9702
- header: null,
9703
- pending: [],
9704
- opened: false,
9705
- lastSeq: -1,
9706
- reportedFailure: null,
9707
- unreadableReported: false,
9708
- stalledAt: 0,
9709
- stalledTicks: 0,
9710
- dead: false
9711
- });
9712
9913
  }
9713
9914
  }
9714
9915
  read(session) {
9715
9916
  if (session.dead) return;
9917
+ session.file ??= dshSessionLogFile(session.dir);
9918
+ if (!session.file) return;
9716
9919
  let fd;
9717
9920
  try {
9718
- fd = fs8.openSync(session.file, "r");
9921
+ fd = fs12.openSync(session.file, "r");
9719
9922
  } catch {
9720
9923
  return;
9721
9924
  }
9722
9925
  try {
9723
- const size = fs8.fstatSync(fd).size;
9926
+ const size = fs12.fstatSync(fd).size;
9724
9927
  if (size <= session.offset) return;
9725
9928
  const buffer = Buffer.alloc(size - session.offset);
9726
- const read = fs8.readSync(fd, buffer, 0, buffer.length, session.offset);
9929
+ const read = fs12.readSync(fd, buffer, 0, buffer.length, session.offset);
9727
9930
  const decoded = decodeZstdFrames(buffer.subarray(0, read));
9728
9931
  if (decoded.failed) {
9729
9932
  this.giveUp(session, `the frame at byte ${session.offset + decoded.failed.at} does not decode: ${decoded.failed.error}`);
@@ -9751,7 +9954,7 @@ var DshSessionPublisher = class {
9751
9954
  this.deps.report(`warning: the platform has not taken ${session.file} for a while; ${dropped} oldest unsent event(s) were dropped`);
9752
9955
  }
9753
9956
  } finally {
9754
- fs8.closeSync(fd);
9957
+ fs12.closeSync(fd);
9755
9958
  }
9756
9959
  }
9757
9960
  /** The rest of this session's log cannot be read; what was sent stands, and the person is told once. */
@@ -9765,7 +9968,7 @@ var DshSessionPublisher = class {
9765
9968
  harnessSessionId: header.id,
9766
9969
  ...this.deps.harnessVersion ? { harnessVersion: this.deps.harnessVersion } : {},
9767
9970
  host: this.deps.host,
9768
- workingDirectory: header.cwd ?? this.deps.cwd,
9971
+ workingDirectory: header.cwd ?? void 0,
9769
9972
  ...header.createdAt !== null ? { startedAt: new Date(header.createdAt).toISOString() } : {},
9770
9973
  launchId: this.deps.launchId,
9771
9974
  ...header.parentSession ? { parentHarnessSessionId: header.parentSession } : {},
@@ -9807,250 +10010,332 @@ var DshSessionPublisher = class {
9807
10010
  var AGENT_SESSION_APPEND_TOOL = "forge_append_agent_session_events";
9808
10011
  var AGENT_SESSION_LIST_TOOL = "forge_list_agent_sessions";
9809
10012
 
9810
- // src/fs/path.ts
9811
- import fs9 from "node:fs";
9812
- import path11 from "node:path";
9813
- function findOnPath(name, env = process.env, platform = process.platform) {
9814
- const dirs = (env.PATH ?? "").split(path11.delimiter).filter(Boolean);
9815
- const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
9816
- const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
9817
- for (const dir of dirs) {
9818
- for (const candidate of candidates) {
9819
- const full = path11.join(dir, candidate);
9820
- try {
9821
- fs9.accessSync(full, fs9.constants.X_OK);
9822
- if (fs9.statSync(full).isFile()) return full;
9823
- } catch {
9824
- }
10013
+ // src/agents/run-dsh.ts
10014
+ import { spawn as spawn3 } from "node:child_process";
10015
+ import fs13 from "node:fs";
10016
+ import path16 from "node:path";
10017
+ function writeDshToken(credentialsPath, accessToken) {
10018
+ const existing = fs13.existsSync(credentialsPath) ? fs13.readFileSync(credentialsPath, "utf8") : null;
10019
+ let merged;
10020
+ try {
10021
+ merged = mergeDshCredentials(existing, accessToken);
10022
+ } catch (error) {
10023
+ throw new CliError(`${credentialsPath}: ${error instanceof Error ? error.message : String(error)}`);
10024
+ }
10025
+ writeTextAtomic(credentialsPath, merged, 384);
10026
+ }
10027
+ function writeDivergeShim(binDir, cli, sessionTokenPath, platform = process.platform) {
10028
+ if (platform === "win32") {
10029
+ const file2 = path16.join(binDir, "diverge.cmd");
10030
+ const run2 = [cli.command, ...cli.args].map((part) => `"${part}"`).join(" ");
10031
+ writeTextAtomic(file2, `@setlocal\r
10032
+ @set "${SESSION_TOKEN_VARIABLE}=${sessionTokenPath}"\r
10033
+ @${run2} %*\r
10034
+ `, 493);
10035
+ return file2;
10036
+ }
10037
+ const quote = (part) => `'${part.replace(/'/g, `'\\''`)}'`;
10038
+ const file = path16.join(binDir, "diverge");
10039
+ writeTextAtomic(file, `#!/bin/sh
10040
+ ${SESSION_TOKEN_VARIABLE}=${quote(sessionTokenPath)}
10041
+ export ${SESSION_TOKEN_VARIABLE}
10042
+ exec ${[cli.command, ...cli.args].map(quote).join(" ")} "$@"
10043
+ `, 493);
10044
+ return file;
10045
+ }
10046
+ function dshEnv(env, binDir) {
10047
+ const out = {};
10048
+ for (const [key, value] of Object.entries(env)) {
10049
+ if (value !== void 0 && key !== "DIVERGENT_TOKEN" && key !== "DIVERGENT_ORG_ID") out[key] = value;
10050
+ }
10051
+ out.PATH = env.PATH ? `${binDir}${path16.delimiter}${env.PATH}` : binDir;
10052
+ return out;
10053
+ }
10054
+ function dshArgs(overlayPath, args) {
10055
+ const rest = args.includes("--profile") ? args : args.length === 0 ? ["--profile", "web"] : ["--profile", "headless", ...args];
10056
+ return ["--patch", overlayPath, ...rest];
10057
+ }
10058
+ async function keepTokenFresh(loop) {
10059
+ let written = null;
10060
+ while (loop.isRunning()) {
10061
+ const token = await loop.freshToken();
10062
+ if (token && token !== written) {
10063
+ loop.write(token);
10064
+ written = token;
10065
+ }
10066
+ for (let i = 0; i < 30; i += 1) {
10067
+ if (!loop.isRunning()) return;
10068
+ await loop.sleep(1e3);
9825
10069
  }
9826
10070
  }
9827
- return null;
10071
+ }
10072
+ function runDsh(options) {
10073
+ const writeToken = (token) => {
10074
+ writeDshToken(options.credentialsPath, token);
10075
+ writeTextAtomic(options.sessionTokenPath, `${token}
10076
+ `, 384);
10077
+ };
10078
+ writeToken(options.initialToken);
10079
+ writeDivergeShim(options.binDir, options.cli, options.sessionTokenPath);
10080
+ const env = dshEnv(options.env, options.binDir);
10081
+ const child = spawn3(options.dsh.command, [...options.dsh.args, ...dshArgs(options.overlayPath, options.args)], { env, stdio: "inherit" });
10082
+ let exited = false;
10083
+ let wake = null;
10084
+ const finish = () => {
10085
+ exited = true;
10086
+ wake?.();
10087
+ };
10088
+ const done = new Promise((resolve, reject) => {
10089
+ child.on("error", (error) => {
10090
+ finish();
10091
+ reject(error);
10092
+ });
10093
+ child.on("exit", (code2, signal) => {
10094
+ finish();
10095
+ resolve(code2 ?? (signal ? 128 + 15 : 1));
10096
+ });
10097
+ });
10098
+ const ignore = () => {
10099
+ };
10100
+ process.on("SIGINT", ignore);
10101
+ const terminate = () => child.kill("SIGTERM");
10102
+ process.on("SIGTERM", terminate);
10103
+ const loop = keepTokenFresh({
10104
+ freshToken: options.freshToken,
10105
+ write: writeToken,
10106
+ isRunning: () => !exited,
10107
+ sleep: (ms) => new Promise((resolve) => {
10108
+ const timer = setTimeout(() => {
10109
+ wake = null;
10110
+ resolve();
10111
+ }, ms);
10112
+ wake = () => {
10113
+ clearTimeout(timer);
10114
+ wake = null;
10115
+ resolve();
10116
+ };
10117
+ })
10118
+ });
10119
+ return done.finally(async () => {
10120
+ process.off("SIGINT", ignore);
10121
+ process.off("SIGTERM", terminate);
10122
+ await loop;
10123
+ });
9828
10124
  }
9829
10125
 
9830
- // src/agents/rotate.ts
9831
- import os7 from "node:os";
9832
- import path12 from "node:path";
9833
- function defaultTokenFilePath() {
9834
- return path12.join(os7.homedir(), ".divergent", "token");
10126
+ // src/update-notice.ts
10127
+ import { spawn as spawn4 } from "node:child_process";
10128
+ import fs14 from "node:fs";
10129
+ import path17 from "node:path";
10130
+ var UPDATE_CHECK_FILE = "update-check.json";
10131
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10132
+ function updateCheckFile(env) {
10133
+ return path17.join(configDir(env), UPDATE_CHECK_FILE);
9835
10134
  }
9836
- var MIN_ROTATE_INTERVAL_SECONDS = 60;
9837
- async function rotateTokenFile(loop) {
9838
- const write = loop.write ?? ((file, token) => writeTextAtomic(file, token, 384));
9839
- const interval = Math.max(loop.intervalSeconds, MIN_ROTATE_INTERVAL_SECONDS) * 1e3;
9840
- for (; ; ) {
9841
- write(loop.tokenFile, await loop.freshToken());
9842
- loop.report(`${(loop.now ?? (() => /* @__PURE__ */ new Date()))().toISOString().slice(11, 19)} token written to ${loop.tokenFile}`);
9843
- if (loop.once) return 0;
9844
- await loop.sleep(interval);
10135
+ function readUpdateCheck(env) {
10136
+ try {
10137
+ const parsed = JSON.parse(fs14.readFileSync(updateCheckFile(env), "utf8"));
10138
+ if (parsed && typeof parsed === "object" && typeof parsed.checkedAt === "number" && typeof parsed.latest === "string") return parsed;
10139
+ } catch {
10140
+ }
10141
+ return null;
10142
+ }
10143
+ function writeUpdateCheck(env, check) {
10144
+ writeJsonAtomic(updateCheckFile(env), check);
10145
+ }
10146
+ function noticeFor(check, version = VERSION) {
10147
+ if (!check || !isNewer(check.latest, version)) return null;
10148
+ return `diverge ${check.latest} is available (this is ${version}). Update with: diverge update (or run npx @divergeai/cli@latest)`;
10149
+ }
10150
+ function noticeApplies(argv, io, version = VERSION) {
10151
+ if (version === "dev" || !io.interactive) return false;
10152
+ if (io.env.DIVERGE_NO_UPDATE_CHECK?.trim() || io.env.CI?.trim() || sessionTokenFile(io.env)) return false;
10153
+ if (argv[0] === "update") return false;
10154
+ if (argv[0] === "tools" && argv[1] === "bridge") return false;
10155
+ return true;
10156
+ }
10157
+ function scheduleUpdateCheck(spawnImpl = spawn4) {
10158
+ try {
10159
+ const child = spawnImpl(process.execPath, [process.argv[1], "update", "--record"], { detached: true, stdio: "ignore" });
10160
+ child.on("error", () => {
10161
+ });
10162
+ child.unref();
10163
+ } catch {
9845
10164
  }
9846
10165
  }
10166
+ function updateNotice(argv, io, deps = { now: Date.now, schedule: scheduleUpdateCheck, read: readUpdateCheck, version: VERSION }) {
10167
+ if (!noticeApplies(argv, io, deps.version)) return;
10168
+ const check = deps.read(io.env);
10169
+ const notice = noticeFor(check, deps.version);
10170
+ if (notice) io.stderr(notice);
10171
+ if (!check || deps.now() - check.checkedAt > UPDATE_CHECK_INTERVAL_MS) deps.schedule();
10172
+ }
9847
10173
 
9848
- // src/agents/self-command.ts
9849
- import fs10 from "node:fs";
9850
- import path13 from "node:path";
9851
- function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
9852
- const script = argv[1];
9853
- if (!script) return { command: execPath, args: [] };
9854
- if (platform !== "win32") {
10174
+ // src/commands/update.ts
10175
+ var PACKAGE_NAME = "@divergeai/cli";
10176
+ var updateUsage = "diverge update\n\nInstall the latest release with the npm that installed this one.";
10177
+ async function latestVersion(registry = "https://registry.npmjs.org", timeoutMs) {
10178
+ const body = await fetchJson(`${registry}/${PACKAGE_NAME}/latest`, { timeoutMs });
10179
+ if (!body.version) throw new CliError("The registry answered without a version");
10180
+ return body.version;
10181
+ }
10182
+ function isNewer(a, b) {
10183
+ const parse = (v) => {
10184
+ const [core, pre] = v.split("-", 2);
10185
+ return { nums: core.split(".").map((n) => Number(n) || 0), pre: pre ?? "" };
10186
+ };
10187
+ const x = parse(a);
10188
+ const y = parse(b);
10189
+ for (let i = 0; i < 3; i += 1) {
10190
+ const dx = x.nums[i] ?? 0;
10191
+ const dy = y.nums[i] ?? 0;
10192
+ if (dx !== dy) return dx > dy;
10193
+ }
10194
+ if (x.pre === y.pre) return false;
10195
+ if (!x.pre) return true;
10196
+ if (!y.pre) return false;
10197
+ return x.pre > y.pre;
10198
+ }
10199
+ async function runUpdate(args, io, deps = { latestVersion, install: installLatest, record: writeUpdateCheck, now: Date.now }) {
10200
+ refuseInSession(io.env, "update");
10201
+ if (args[0] === "--record") {
9855
10202
  try {
9856
- fs10.accessSync(script, fs10.constants.X_OK);
9857
- return { command: path13.resolve(script), args: [] };
10203
+ deps.record(io.env, { checkedAt: deps.now(), latest: await deps.latestVersion() });
9858
10204
  } catch {
9859
10205
  }
10206
+ return ExitCode.Ok;
9860
10207
  }
9861
- return { command: execPath, args: [fs10.realpathSync(script)] };
10208
+ if (VERSION === "dev") throw new CliError("This is a source checkout, not an installed release; pull the repository instead.");
10209
+ const latest = await deps.latestVersion();
10210
+ if (!isNewer(latest, VERSION)) {
10211
+ io.stdout(`diverge ${VERSION} is up to date.`);
10212
+ return ExitCode.Ok;
10213
+ }
10214
+ io.stdout(`Updating diverge ${VERSION} \u2192 ${latest}\u2026`);
10215
+ const code2 = await deps.install(latest);
10216
+ if (code2 !== 0) throw new CliError(`npm install exited with ${code2}`);
10217
+ io.stdout(`diverge ${latest} installed.`);
10218
+ return ExitCode.Ok;
9862
10219
  }
9863
- function shellCommand(self, ...trailing) {
9864
- return [self.command, ...self.args, ...trailing].map((part) => /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part).join(" ");
10220
+ function installLatest(version, env = process.env, platform = process.platform, stdio = "inherit") {
10221
+ return runNpm(["install", "-g", `${PACKAGE_NAME}@${version}`], env, platform, stdio);
9865
10222
  }
9866
10223
 
9867
- // src/commands/agents.ts
9868
- var agentsUsage = `diverge agents <setup|run|rotate|status> ...
10224
+ // src/commands/dsh.ts
10225
+ var dshUsage = `diverge dsh [--surface anthropic|openai] [--model <id>] [--organization-id <id>] [-- <dsh args>]
9869
10226
 
9870
- setup claude [--scope user|project] [--project-id <id>] [--model-discovery] [--organization-id <id>]
9871
- setup opencode [--surface anthropic|openai] [--config <path>] [--organization-id <id>]
9872
- setup codex [--project-id <id>] [--config <path>] [--print] [--organization-id <id>]
9873
- setup dsh [--surface anthropic|openai] [--model <id>] [--into <path>] [--print] [--organization-id <id>]
9874
- run dsh [--surface anthropic|openai] [--model <id>] [--organization-id <id>] [-- <dsh args>]
9875
- nothing after --: the interactive web UI; a task after --: one headless run
9876
- rotate [--into <path>] [--interval <seconds>] [--once] [--organization-id <id>]
9877
- keep a token file fresh for a client that reads a file, not a helper (default ~/.divergent/token, every 600 s)
9878
- status [--organization-id <id>]
10227
+ Start DeepSeek Harness on the organization's model gateway. Nothing after --
10228
+ opens the web UI; a task after -- answers it headless and exits; any other dsh
10229
+ arguments go after -- as given.
9879
10230
 
9880
- Point a coding agent at the platform's model gateway. A sign-in is started
9881
- when the cached login does not serve the gateway for the organization.`;
9882
- function surfaceOf(value, fallback) {
9883
- if (value === void 0) return fallback;
9884
- if (SURFACES.includes(value)) return value;
9885
- throw new CliError(`--surface must be one of: ${SURFACES.join(", ")}`);
9886
- }
9887
- function readJson(file) {
9888
- if (!fs11.existsSync(file)) return null;
9889
- return JSON.parse(fs11.readFileSync(file, "utf8"));
9890
- }
9891
- async function servedModels(apiUrl, token, surface) {
9892
- return (await listModels(apiUrl, token.access_token, surface)).map(servedModel);
9893
- }
9894
- function syncDivergentPreset(io, binary, home) {
9895
- if (!binary) {
9896
- io.stderr("dsh is not on PATH; the Divergent agent preset is derived when `diverge agents run dsh` first runs.");
9897
- return;
10231
+ Every launch brings the CLI, the dsh release it is tested with and your skills
10232
+ up to date first. DIVERGE_NO_UPDATE_CHECK skips the CLI's update.`;
10233
+ var RELAUNCHED = "DIVERGE_DSH_RELAUNCHED";
10234
+ var LATEST_TIMEOUT_MS = 5e3;
10235
+ async function updateThenRelaunch(io, deps) {
10236
+ const env = io.env;
10237
+ if (deps.version === "dev" || env[RELAUNCHED] || env.DIVERGE_NO_UPDATE_CHECK?.trim() || env.CI?.trim() || sessionTokenFile(env)) return null;
10238
+ let latest;
10239
+ try {
10240
+ latest = await deps.latest();
10241
+ } catch {
10242
+ return null;
9898
10243
  }
9899
- const packageDir = dshPackageDir(binary);
9900
- const shipped = packageDir ? shippedPresetDir(packageDir) : null;
9901
- if (!shipped) {
9902
- 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`);
9903
- return;
10244
+ if (!isNewer(latest, deps.version)) return null;
10245
+ io.stderr(`Updating diverge ${deps.version} \u2192 ${latest}\u2026`);
10246
+ let code2;
10247
+ try {
10248
+ code2 = await deps.install(latest);
10249
+ } catch (error) {
10250
+ io.stderr(`warning: could not update diverge (${error instanceof Error ? error.message : String(error)}); going on with ${deps.version}. To retry: diverge update`);
10251
+ return null;
10252
+ }
10253
+ if (code2 !== 0) {
10254
+ io.stderr(`warning: could not update diverge (npm exited with ${code2}); going on with ${deps.version}. To retry: diverge update`);
10255
+ return null;
9904
10256
  }
9905
- const dir = writeDivergentPreset(home, shipped);
9906
- const settingsPath = path14.join(home, "settings.yaml");
9907
- const settings = withDivergentPresetDefault(fs11.existsSync(settingsPath) ? fs11.readFileSync(settingsPath, "utf8") : null);
9908
- if (settings !== null) writeTextAtomic(settingsPath, settings);
9909
- io.stderr(`Agent preset "${DIVERGENT_PRESET_ID}" (${dir}): web sessions search through the platform${settings !== null ? "; set as the default preset in settings.yaml" : ""}.`);
10257
+ return deps.relaunch();
10258
+ }
10259
+ function relaunch(args, env) {
10260
+ return new Promise((resolve, reject) => {
10261
+ const child = spawn5(process.execPath, [process.argv[1], "dsh", ...args], { env: { ...env, [RELAUNCHED]: "1" }, stdio: "inherit" });
10262
+ const ignore = () => {
10263
+ };
10264
+ const terminate = () => child.kill("SIGTERM");
10265
+ process.on("SIGINT", ignore);
10266
+ process.on("SIGTERM", terminate);
10267
+ const settle = () => {
10268
+ process.off("SIGINT", ignore);
10269
+ process.off("SIGTERM", terminate);
10270
+ };
10271
+ child.on("error", (error) => {
10272
+ settle();
10273
+ reject(error);
10274
+ });
10275
+ child.on("exit", (code2, signal) => {
10276
+ settle();
10277
+ resolve(code2 ?? (signal ? 128 + 15 : 1));
10278
+ });
10279
+ });
9910
10280
  }
9911
- async function skillsFor(io, apiUrl, harness, organizationId) {
10281
+ function surfaceOf2(value) {
10282
+ if (value === void 0) return "openai";
10283
+ if (SURFACES.includes(value)) return value;
10284
+ throw new CliError(`--surface must be one of: ${SURFACES.join(", ")}`);
10285
+ }
10286
+ function savedModel(home, surface) {
10287
+ const file = path18.join(home, "profiles", "web", "cordis.patch.yml");
9912
10288
  try {
9913
- const skills = await loginSkillSet(apiUrl, io.env, organizationId?.trim() || void 0);
9914
- installForHarness(io, harness, skills, apiUrl);
9915
- for (const note of skills.notes) io.stderr(`warning: ${note}`);
10289
+ return dshSavedModel(fs15.existsSync(file) ? fs15.readFileSync(file, "utf8") : null, surface);
9916
10290
  } catch (error) {
9917
- io.stderr(`warning: skills not installed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
10291
+ throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
9918
10292
  }
9919
10293
  }
9920
- async function overlayAndDefault(apiUrl, token, surface, io, requested, launch) {
9921
- const served = await servedModels(apiUrl, token, surface);
10294
+ async function overlayAndDefault(io, apiUrl, token, surface, requested, saved, launch) {
10295
+ const served = (await listModels(apiUrl, token.access_token, surface)).map(servedModel);
9922
10296
  const models = served.map((model) => model.id);
9923
10297
  if (models.length === 0) throw new CliError(`the gateway serves no models on the ${surface} surface for this organization`);
9924
10298
  if (requested && !models.includes(requested)) throw new CliError(`${requested} is not served on the ${surface} surface; choose one of: ${models.join(", ")}`);
9925
- const defaultModel = dshDefaultModel(models, requested);
9926
- return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel, launch }), defaultModel, models };
9927
- }
9928
- function organizationArgs(organizationId) {
9929
- const trimmed = organizationId?.trim();
9930
- return trimmed ? ["--organization-id", trimmed] : [];
9931
- }
9932
- async function setup(args, io) {
9933
- const [agent, ...rest] = args;
9934
- const apiUrl = resolveApiUrl(void 0, io.env);
9935
- switch (agent) {
9936
- case "claude": {
9937
- const { values } = parseArgs3({ args: rest, options: { scope: { type: "string", default: "user" }, "project-id": { type: "string" }, "organization-id": { type: "string" }, "model-discovery": { type: "boolean", default: false } }, strict: true });
9938
- if (values.scope !== "user" && values.scope !== "project") throw new CliError("--scope must be user or project");
9939
- const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9940
- const file = claudeSettingsPath(values.scope);
9941
- const { settings, warnings } = mergeClaudeSettings(readJson(file), {
9942
- baseUrl: anthropicBaseUrl(apiUrl),
9943
- tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
9944
- projectId: values["project-id"],
9945
- modelDiscovery: values["model-discovery"],
9946
- // Claude Code scans its skill folder when a session starts, so that is when the login's skills are brought current.
9947
- skillsRefresh: values.scope === "user" ? shellCommand(resolveSelfCommand(), "skills", "refresh") : void 0
9948
- });
9949
- writeJsonAtomic(file, settings);
9950
- io.stdout(`Claude Code \u2192 ${anthropicBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
9951
- for (const warning of warnings) io.stderr(`warning: ${warning}`);
9952
- await skillsFor(io, apiUrl, "claude", values["organization-id"]);
9953
- return ExitCode.Ok;
9954
- }
9955
- case "opencode": {
9956
- const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
9957
- const surface = surfaceOf(values.surface, "anthropic");
9958
- const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9959
- const baseUrl = surfaceBaseUrl(apiUrl, surface);
9960
- let models = [];
9961
- try {
9962
- models = await servedModels(apiUrl, token, surface);
9963
- } catch (error) {
9964
- io.stderr(`warning: ${error instanceof Error ? error.message : String(error)}; writing the provider without a model list`);
9965
- }
9966
- const file = values.config ?? opencodeConfigPath();
9967
- writeJsonAtomic(file, mergeOpencodeConfig(readJson(file), { baseUrl, surface, models }));
9968
- io.stdout(`OpenCode provider divergent-${surface} \u2192 ${baseUrl}. Wrote ${file}.`);
9969
- io.stdout("Keep DIVERGENT_TOKEN fresh for the session (user tokens expire; the login refreshes):");
9970
- io.stdout(` export DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})"`);
9971
- return ExitCode.Ok;
9972
- }
9973
- case "codex": {
9974
- const { values } = parseArgs3({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
9975
- const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"], organizationId: values["organization-id"] });
9976
- if (values.print) {
9977
- io.stdout(block.trimEnd());
9978
- return ExitCode.Ok;
9979
- }
9980
- const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9981
- const file = values.config ?? codexConfigPath();
9982
- const existing = fs11.existsSync(file) ? fs11.readFileSync(file, "utf8") : null;
9983
- writeTextAtomic(file, mergeCodexConfig(existing, block));
9984
- io.stdout(`Codex provider divergent \u2192 ${openaiBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
9985
- io.stdout('Select it per run with: codex -c model_provider=divergent (or set model_provider = "divergent" in the config)');
9986
- await skillsFor(io, apiUrl, "codex", values["organization-id"]);
9987
- return ExitCode.Ok;
9988
- }
9989
- case "dsh": {
9990
- const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" }, into: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
9991
- const surface = surfaceOf(values.surface, "openai");
9992
- const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9993
- if (!values.print) await skillsFor(io, apiUrl, "dsh", values["organization-id"]);
9994
- const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
9995
- if (values.print) {
9996
- io.stdout(overlay.trimEnd());
9997
- return ExitCode.Ok;
9998
- }
9999
- const file = values.into ?? path14.join(dshHome(io.env), `divergent-${surface}.yml`);
10000
- writeTextAtomic(file, overlay);
10001
- io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
10002
- io.stdout("Platform tools, web search included, are reached from dsh's shell through `diverge tools`; the installed skills say when and how. dsh's DeepSeek search is off.");
10003
- syncDivergentPreset(io, findOnPath("dsh", io.env), dshHome(io.env));
10004
- io.stdout("Boot a profile with the overlay and the token in its environment (user tokens expire; the login refreshes):");
10005
- io.stdout(` DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})" dsh --profile headless --patch ${file} "<task>"`);
10006
- 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`);
10007
- return ExitCode.Ok;
10008
- }
10009
- default:
10010
- throw unknownVerb("agents", agentsUsage, ["setup"], agent, rest);
10011
- }
10012
- }
10013
- async function run(args, io) {
10014
- const [agent, ...rest] = args;
10015
- if (agent !== "dsh") throw unknownVerb("agents", agentsUsage, ["run"], agent, rest);
10016
- const terminator = rest.indexOf("--");
10017
- const flags = terminator === -1 ? rest : rest.slice(0, terminator);
10018
- const passthrough = terminator === -1 ? [] : rest.slice(terminator + 1);
10019
- const { values } = parseArgs3({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
10020
- const surface = surfaceOf(values.surface, "openai");
10299
+ const usable = !requested && saved && models.includes(saved.model) ? saved : null;
10300
+ const defaultModel = dshDefaultModel(models, requested ?? usable?.model);
10301
+ 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 };
10303
+ }
10304
+ async function runDshCommand(args, io) {
10305
+ const terminator = args.indexOf("--");
10306
+ const flags = terminator === -1 ? args : args.slice(0, terminator);
10307
+ const passthrough = terminator === -1 ? [] : args.slice(terminator + 1);
10308
+ const { values } = parseArgs4({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
10309
+ const surface = surfaceOf2(values.surface);
10310
+ const relaunched = await updateThenRelaunch(io, {
10311
+ version: VERSION,
10312
+ latest: () => latestVersion(void 0, LATEST_TIMEOUT_MS),
10313
+ install: (version) => installLatest(version, io.env, process.platform, ["ignore", 2, 2]),
10314
+ relaunch: () => relaunch(args, io.env)
10315
+ });
10316
+ if (relaunched !== null) return relaunched;
10021
10317
  const apiUrl = resolveApiUrl(void 0, io.env);
10022
10318
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
10023
- const binary = findOnPath("dsh", io.env);
10024
- if (!binary) throw new CliError("dsh is not on PATH (install: npm install -g @deepseek-ai/dsh)");
10319
+ const dsh = await ensureDsh(io);
10320
+ const prefix = path18.join(dshInstallRoot(io.env), TESTED_DSH_VERSION);
10025
10321
  await skillsFor(io, apiUrl, "dsh", values["organization-id"]);
10026
- const launch = { id: randomUUID(), client: `dsh/${dshVersionOf(binary) ?? "unknown"}` };
10027
- const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model, launch);
10322
+ const launch = { id: randomUUID(), client: `dsh/${TESTED_DSH_VERSION}` };
10028
10323
  const home = dshHome(io.env);
10029
- const settingsPath = path14.join(home, "settings.yaml");
10030
- let pinned;
10031
- try {
10032
- pinned = dshSettingsDefaultModel(fs11.existsSync(settingsPath) ? fs11.readFileSync(settingsPath, "utf8") : null, surface);
10033
- } catch (error) {
10034
- throw new CliError(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
10035
- }
10036
- const served = servedIds(overlay);
10037
- if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
10038
- const overlayPath = path14.join(home, `divergent-${surface}.yml`);
10039
- writeTextAtomic(overlayPath, overlay);
10040
- syncDivergentPreset(io, binary, home);
10041
- io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
10042
- 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>"');
10324
+ const { overlay, defaultModel } = await overlayAndDefault(io, apiUrl, token, surface, values.model, savedModel(home, surface), launch);
10325
+ const overlayPath = path18.join(home, `divergent-${surface}.yml`);
10326
+ writeTextAtomic(overlayPath, `${overlay}${dshPresetOverrides(prefix)}`);
10327
+ io.stderr(`dsh ${TESTED_DSH_VERSION} on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
10328
+ if (passthrough.length === 0) io.stderr('Starting the web UI; open the URL dsh prints. For one task without the UI: diverge dsh -- "<task>"');
10043
10329
  const source = tokenSource(io.env, apiUrl);
10044
10330
  const organizationId = tokenClaims(token.access_token).organizationId ?? values["organization-id"];
10045
10331
  if (!organizationId) throw new CliError("the gateway token names no organization; pass --organization-id");
10046
10332
  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).`);
10047
10333
  const tools = new ToolsClient({ apiUrl, organizationId, mode: "operator", clientId: clientId(io.env) }, io.env);
10048
10334
  const publisher = new DshSessionPublisher({
10049
- sessionsRoot: path14.join(home, "sessions"),
10050
- cwd: process.cwd(),
10335
+ sessionsRoot: path18.join(home, "sessions"),
10051
10336
  launchedAt: Date.now(),
10052
10337
  launchId: launch.id,
10053
- harnessVersion: dshVersionOf(binary),
10338
+ harnessVersion: TESTED_DSH_VERSION,
10054
10339
  host: os8.hostname(),
10055
10340
  append: (input) => appendSession(tools, input),
10056
10341
  report: (line) => io.stderr(line),
@@ -10059,12 +10344,12 @@ async function run(args, io) {
10059
10344
  publisher.start();
10060
10345
  try {
10061
10346
  return await runDsh({
10062
- binary,
10347
+ dsh,
10063
10348
  overlayPath,
10064
- credentialsPath: path14.join(home, ".credentials.yaml"),
10065
- sessionTokenPath: path14.join(home, "session-token"),
10066
- binDir: path14.join(home, "bin"),
10067
- cli: { command: process.execPath, args: [path14.resolve(process.argv[1])] },
10349
+ credentialsPath: path18.join(home, ".credentials.yaml"),
10350
+ sessionTokenPath: path18.join(home, "session-token"),
10351
+ binDir: path18.join(home, "bin"),
10352
+ cli: { command: process.execPath, args: [path18.resolve(process.argv[1])] },
10068
10353
  args: passthrough,
10069
10354
  env: io.env,
10070
10355
  initialToken: token.access_token,
@@ -10091,56 +10376,9 @@ async function appendSession(tools, input) {
10091
10376
  }
10092
10377
  return { capture: result.capture, sessionId: result.sessionId ?? null, stored: result.stored ?? 0, skipped: result.skipped ?? 0, lastSeq: typeof result.lastSeq === "number" ? result.lastSeq : -1 };
10093
10378
  }
10094
- async function status(args, io) {
10095
- const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
10096
- const apiUrl = resolveApiUrl(void 0, io.env);
10097
- const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
10098
- const claims = tokenClaims(token.access_token);
10099
- io.stdout(`Gateway: ${apiUrl}/api/plugins/inference/{anthropic,openai}`);
10100
- io.stdout(`Organization: ${claims.organizationId}`);
10101
- io.stdout(`Scopes: ${(claims.scopes ?? []).join(" ")}`);
10102
- for (const surface of SURFACES) {
10103
- const models = (await servedModels(apiUrl, token, surface)).map((model) => model.id);
10104
- io.stdout(`Models (${surface}): ${models.length ? models.join(", ") : "(none served on this surface)"}`);
10105
- }
10106
- return ExitCode.Ok;
10107
- }
10108
- async function rotate(args, io) {
10109
- const { values } = parseArgs3({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
10110
- const interval = Number(values.interval);
10111
- if (!Number.isFinite(interval) || interval <= 0) throw new CliError("--interval must be a positive number of seconds");
10112
- const apiUrl = resolveApiUrl(void 0, io.env);
10113
- const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
10114
- const source = tokenSource(io.env, apiUrl);
10115
- const tokenFilePath = values.into ?? defaultTokenFilePath();
10116
- io.stderr(`Keeping ${tokenFilePath} fresh as you (organization ${tokenClaims(token.access_token).organizationId})`);
10117
- return rotateTokenFile({
10118
- tokenFile: tokenFilePath,
10119
- intervalSeconds: interval,
10120
- once: values.once,
10121
- freshToken: async () => (await getUserToken(source, { freshForSeconds: Math.max(interval, 60) + 60, organizationId: values["organization-id"] })).access_token,
10122
- report: (line) => io.stderr(line),
10123
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
10124
- });
10125
- }
10126
- async function runAgents(args, io) {
10127
- const [verb, ...rest] = args;
10128
- switch (verb) {
10129
- case "setup":
10130
- return setup(rest, io);
10131
- case "run":
10132
- return run(rest, io);
10133
- case "rotate":
10134
- return rotate(rest, io);
10135
- case "status":
10136
- return status(rest, io);
10137
- default:
10138
- throw unknownVerb("agents", agentsUsage, [], verb, rest);
10139
- }
10140
- }
10141
10379
 
10142
10380
  // src/commands/org.ts
10143
- import { parseArgs as parseArgs4 } from "node:util";
10381
+ import { parseArgs as parseArgs5 } from "node:util";
10144
10382
  var orgUsage = `diverge org [use <id|name>]
10145
10383
 
10146
10384
  (no verb) list the organizations you may act in, the default marked
@@ -10153,7 +10391,7 @@ there, which proves you may act in it. For one command, pass
10153
10391
  act in, and so sees, every organization.`;
10154
10392
  async function runOrg(args, io) {
10155
10393
  refuseInSession(io.env, "org");
10156
- const { positionals } = parseArgs4({ args, options: {}, strict: true, allowPositionals: true });
10394
+ const { positionals } = parseArgs5({ args, options: {}, strict: true, allowPositionals: true });
10157
10395
  const verb = positionals[0];
10158
10396
  if (verb && verb !== "use") throw unknownVerb("org", orgUsage, [], verb, positionals.slice(1));
10159
10397
  const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
@@ -10200,7 +10438,7 @@ async function runLogout(_args, io) {
10200
10438
  }
10201
10439
 
10202
10440
  // src/commands/status.ts
10203
- import path15 from "node:path";
10441
+ import path19 from "node:path";
10204
10442
  var statusUsage = "diverge status\n\nShow the deployment, the cached login (or the session token), whose account it is, what it carries, and who a tool call would run as. One request: the account behind the login.";
10205
10443
  async function runStatus(_args, io) {
10206
10444
  const apiUrl = resolveApiUrl(void 0, io.env);
@@ -10216,7 +10454,7 @@ async function runStatus(_args, io) {
10216
10454
  io.stdout(describeToolExecution(io.env));
10217
10455
  return 0;
10218
10456
  }
10219
- io.stdout(`State: ${path15.dirname(source.loginFile)}`);
10457
+ io.stdout(`State: ${path19.dirname(source.loginFile)}`);
10220
10458
  const record = loadLogin(source);
10221
10459
  if (!record) {
10222
10460
  io.stdout(`Not logged in. Run: diverge login${apiUrl === DEFAULT_API_URL ? "" : ` --api-url ${apiUrl}`}`);
@@ -10264,7 +10502,7 @@ function describeSignIn(record, nowMs = Date.now()) {
10264
10502
  }
10265
10503
 
10266
10504
  // src/commands/token.ts
10267
- import { parseArgs as parseArgs5 } from "node:util";
10505
+ import { parseArgs as parseArgs6 } from "node:util";
10268
10506
  var tokenUsage = `diverge token [--organization-id <id>]
10269
10507
 
10270
10508
  Print a fresh access token and nothing else: the credential helper for Claude
@@ -10273,7 +10511,7 @@ Code, Codex and forge. The token is for the organization named by
10273
10511
  DIVERGENT_TOKEN_FILE set, it prints the session token in that file, and an
10274
10512
  organization other than the token's is refused.`;
10275
10513
  async function runToken(args, io) {
10276
- const { values } = parseArgs5({ args, options: { "organization-id": { type: "string" } }, strict: true, allowPositionals: false });
10514
+ const { values } = parseArgs6({ args, options: { "organization-id": { type: "string" } }, strict: true, allowPositionals: false });
10277
10515
  const organizationId = values["organization-id"]?.trim() || io.env.DIVERGENT_ORG_ID?.trim() || void 0;
10278
10516
  const token = await getUserToken(tokenSource(io.env, resolveApiUrl(void 0, io.env)), { organizationId });
10279
10517
  io.stdout(token.access_token);
@@ -10281,7 +10519,7 @@ async function runToken(args, io) {
10281
10519
  }
10282
10520
 
10283
10521
  // src/commands/tools.ts
10284
- import { parseArgs as parseArgs6 } from "node:util";
10522
+ import { parseArgs as parseArgs7 } from "node:util";
10285
10523
 
10286
10524
  // src/mcp/bridge.ts
10287
10525
  import readline2 from "node:readline";
@@ -10508,7 +10746,7 @@ async function waitForInvocation(id, options, deps) {
10508
10746
  }
10509
10747
 
10510
10748
  // src/tools/call-args.ts
10511
- import fs12 from "node:fs";
10749
+ import fs16 from "node:fs";
10512
10750
  async function resolveCallArgs(options, stdin = readStdin) {
10513
10751
  if (options.argsJson !== void 0 && options.argsJson !== "{}" && options.argsFile !== void 0) {
10514
10752
  throw new CliError("give the arguments once: --args-json or --args-file");
@@ -10518,7 +10756,7 @@ async function resolveCallArgs(options, stdin = readStdin) {
10518
10756
  if (options.argsFile !== void 0) {
10519
10757
  source = `--args-file ${options.argsFile}`;
10520
10758
  try {
10521
- text2 = fs12.readFileSync(options.argsFile, "utf8");
10759
+ text2 = fs16.readFileSync(options.argsFile, "utf8");
10522
10760
  } catch (error) {
10523
10761
  throw new CliError(`${source}: ${error instanceof Error ? error.message : String(error)}`);
10524
10762
  }
@@ -10610,7 +10848,7 @@ function worstExitCode(outcomes) {
10610
10848
  }
10611
10849
 
10612
10850
  // src/commands/tools.ts
10613
- import fs13 from "node:fs";
10851
+ import fs17 from "node:fs";
10614
10852
 
10615
10853
  // src/tools/print.ts
10616
10854
  function matchesCapability(selector, capability) {
@@ -10735,7 +10973,7 @@ async function call(rest, io) {
10735
10973
  let waited = "";
10736
10974
  let outcome;
10737
10975
  try {
10738
- const { values, positionals } = parseArgs6({ args: rest, options: CALL_OPTIONS, strict: true, allowPositionals: true });
10976
+ const { values, positionals } = parseArgs7({ args: rest, options: CALL_OPTIONS, strict: true, allowPositionals: true });
10739
10977
  tool = positionals[0] ?? "";
10740
10978
  if (!tool) throw new CliError("call needs a tool name");
10741
10979
  const args = await resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
@@ -10762,12 +11000,12 @@ async function set(rest, io) {
10762
11000
  let waited = "";
10763
11001
  let outcome;
10764
11002
  try {
10765
- const { values, positionals } = parseArgs6({ args: rest, options: SET_OPTIONS, strict: true, allowPositionals: true });
11003
+ const { values, positionals } = parseArgs7({ args: rest, options: SET_OPTIONS, strict: true, allowPositionals: true });
10766
11004
  const file = positionals[0];
10767
11005
  if (!file) throw new CliError("set needs a file of calls, or - for stdin");
10768
11006
  let entries;
10769
11007
  try {
10770
- entries = parseBatch(file === "-" ? await readStdin() : fs13.readFileSync(file, "utf8"));
11008
+ entries = parseBatch(file === "-" ? await readStdin() : fs17.readFileSync(file, "utf8"));
10771
11009
  } catch (error) {
10772
11010
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
10773
11011
  }
@@ -10811,7 +11049,7 @@ async function runTools(args, io) {
10811
11049
  const [verb, ...rest] = args;
10812
11050
  switch (verb) {
10813
11051
  case "list": {
10814
- const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
11052
+ const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
10815
11053
  const { client: tools } = await client(values, io);
10816
11054
  let list = await tools.listTools();
10817
11055
  if (values.filter) list = list.filter((tool) => matchesFilter(values.filter, tool));
@@ -10821,7 +11059,7 @@ async function runTools(args, io) {
10821
11059
  return ExitCode.Ok;
10822
11060
  }
10823
11061
  case "capabilities": {
10824
- const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
11062
+ const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
10825
11063
  const { client: tools } = await client(values, io);
10826
11064
  const list = await tools.listTools();
10827
11065
  if (values.json) io.stdout(JSON.stringify(capabilityClusters(list), null, 2));
@@ -10829,7 +11067,7 @@ async function runTools(args, io) {
10829
11067
  return ExitCode.Ok;
10830
11068
  }
10831
11069
  case "describe": {
10832
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
11070
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
10833
11071
  const name = positionals[0];
10834
11072
  if (!name) throw new CliError("describe needs a tool name");
10835
11073
  const { settings, client: tools } = await client(values, io);
@@ -10849,12 +11087,12 @@ async function runTools(args, io) {
10849
11087
  case "set":
10850
11088
  return set(rest, io);
10851
11089
  case "run": {
10852
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
11090
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
10853
11091
  const file = positionals[0];
10854
11092
  if (!file) throw new CliError("run needs a file of calls, or - for stdin");
10855
11093
  let entries;
10856
11094
  try {
10857
- entries = parseBatch(file === "-" ? await readStdin() : fs13.readFileSync(file, "utf8"));
11095
+ entries = parseBatch(file === "-" ? await readStdin() : fs17.readFileSync(file, "utf8"));
10858
11096
  } catch (error) {
10859
11097
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
10860
11098
  }
@@ -10869,7 +11107,7 @@ async function runTools(args, io) {
10869
11107
  return exitCode;
10870
11108
  }
10871
11109
  case "invocation": {
10872
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" }, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
11110
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" }, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
10873
11111
  const id = positionals[0];
10874
11112
  if (!id) throw new CliError("invocation needs an id");
10875
11113
  const { client: tools } = await client(values, io);
@@ -10887,7 +11125,7 @@ async function runTools(args, io) {
10887
11125
  return worstExitCode([outcome]);
10888
11126
  }
10889
11127
  case "bridge": {
10890
- const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
11128
+ const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
10891
11129
  const { client: tools } = await client(values, io);
10892
11130
  const allow = (values.tools ?? "").split(",").map((name) => name.trim()).filter(Boolean);
10893
11131
  return serveBridge(new BridgeServer(tools, { tools: allow }), process.stdin, process.stdout);
@@ -10898,34 +11136,60 @@ async function runTools(args, io) {
10898
11136
  }
10899
11137
 
10900
11138
  // src/commands/plugins.ts
10901
- import fs14 from "node:fs";
10902
- import { parseArgs as parseArgs7 } from "node:util";
11139
+ import fs18 from "node:fs";
11140
+ import { parseArgs as parseArgs8 } from "node:util";
10903
11141
  var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
10904
11142
 
10905
- validate <manifest.json> (dry run: the platform's full validation, nothing stored)
10906
- publish <manifest.json> [--json] (forge_plugins:create in the signed-in organization)
11143
+ validate <manifest.json> [--json] (forge_validate_plugin: the platform's full check, nothing stored;
11144
+ says whether publishing needs a person's decision)
11145
+ publish <manifest.json> [--json] (forge_publish_plugin: raised as you, decided in Compass;
11146
+ exits 3 while it waits)
10907
11147
  list [--json]
10908
- retire <pluginId>
11148
+ retire <pluginId> [--json] (forge_retire_plugin: raised as you, decided in Compass;
11149
+ exits 3 while it waits)
10909
11150
 
10910
11151
  Register a tenant plugin: a plugin whose code runs in your organization's own
10911
11152
  runtime and never inside the platform. The manifest lists the tools the
10912
- platform registers, pinned; a change is a new version published again. Enable
10913
- it with forge plugins (or forge_set_plugin_state) and bind the runtime with a
10914
- Forge bridge binding whose runtimeType matches the manifest's runtime.type.`;
11153
+ platform registers, pinned; a change is a new version published again.
11154
+
11155
+ Every write is a tool invocation decided by your organization's policy: publish
11156
+ and retire print the invocation and its Compass link and exit 3 while they
11157
+ wait; a person decides there, and diverge tools invocation <id> reads the
11158
+ outcome. A version that only reshapes tools already served is what the deploy
11159
+ pipeline stores unattended with forge_reshape_plugin; validate says which a
11160
+ version needs. Enable the plugin with forge_set_plugin_state and bind the
11161
+ runtime with a Forge bridge binding whose runtimeType matches the manifest's
11162
+ runtime.type.`;
10915
11163
  var SCOPE_OPTIONS2 = { "organization-id": { type: "string" } };
10916
11164
  async function client2(values, io) {
10917
11165
  const settings = loadToolsSettings({ organizationId: values["organization-id"] }, io.env);
10918
11166
  io.stderr(scopeLine(settings));
10919
11167
  io.stderr(await actingAsLine(settings, io.env));
10920
- return new ToolsClient(settings, io.env);
11168
+ return { settings, tools: new ToolsClient(settings, io.env) };
11169
+ }
11170
+ function printProblems(details, io) {
11171
+ io.stderr(`Invalid manifest (${details.length} problem${details.length === 1 ? "" : "s"}):`);
11172
+ for (const detail of details) io.stderr(` - ${detail}`);
11173
+ return ExitCode.Failure;
11174
+ }
11175
+ async function raise(entry, context, io) {
11176
+ const { settings, tools, json } = context;
11177
+ const outcome = await runOne(entry, {
11178
+ callTool: (name, args) => tools.callTool(name, args),
11179
+ decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
11180
+ ...json ? {} : { onPending: (pending) => formatPending(pending, settings.apiUrl, settings.organizationId).forEach((line) => io.stdout(line)) }
11181
+ });
11182
+ if (json) io.stdout(JSON.stringify(outcome, null, 2));
11183
+ else if (outcome.status === "failed") throw new CliError(`${entry.tool} failed: ${outcome.error}`);
11184
+ return outcome;
10921
11185
  }
10922
- function readManifest(file) {
11186
+ function readManifest2(file) {
10923
11187
  if (!file) throw new CliError(`A manifest file is required.
10924
11188
 
10925
11189
  ${pluginsUsage}`);
10926
11190
  let text2;
10927
11191
  try {
10928
- text2 = fs14.readFileSync(file, "utf8");
11192
+ text2 = fs18.readFileSync(file, "utf8");
10929
11193
  } catch (error) {
10930
11194
  throw new CliError(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
10931
11195
  }
@@ -10952,43 +11216,45 @@ async function runPlugins(args, io) {
10952
11216
  const [verb, ...rest] = args;
10953
11217
  switch (verb) {
10954
11218
  case "validate": {
10955
- const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
10956
- const manifest = readManifest(positionals[0]);
10957
- const tools = await client2(values, io);
10958
- const outcome = await tools.publishPlugin(manifest, { dryRun: true });
10959
- if (!outcome.ok) {
10960
- io.stderr(`Invalid manifest (${outcome.details.length} problem${outcome.details.length === 1 ? "" : "s"}):`);
10961
- for (const detail of outcome.details) io.stderr(` - ${detail}`);
10962
- return ExitCode.Failure;
10963
- }
10964
- if (values.json) io.stdout(JSON.stringify(outcome.result, null, 2));
11219
+ const { values, positionals } = parseArgs8({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
11220
+ const manifest = readManifest2(positionals[0]);
11221
+ const { tools } = await client2(values, io);
11222
+ const body = await tools.callTool("forge_validate_plugin", { manifest });
11223
+ if (body.success === false) throw new CliError(`forge_validate_plugin failed: ${body.error ?? "unknown error"}`);
11224
+ const result = body.result;
11225
+ if (!result.valid) {
11226
+ if (values.json) io.stdout(JSON.stringify(result, null, 2));
11227
+ return printProblems(result.problems, io);
11228
+ }
11229
+ if (values.json) io.stdout(JSON.stringify(result, null, 2));
10965
11230
  else {
10966
- io.stdout(`Valid. ${outcome.result.changed ? "Publishing would register" : "Already published at this digest:"}`);
10967
- for (const line of formatPlugin(outcome.result.plugin)) io.stdout(line);
11231
+ io.stdout(`Valid. ${result.changed ? "Publishing would register" : "Already published at this digest:"}`);
11232
+ for (const line of formatPlugin(result.plugin)) io.stdout(line);
11233
+ if (result.authorityChanges.length) {
11234
+ io.stdout("Publishing it changes authority, so forge_publish_plugin stores it once a person decides in Compass:");
11235
+ for (const change of result.authorityChanges) io.stdout(` - ${change}`);
11236
+ } else if (result.changed) {
11237
+ io.stdout("It keeps the serving version's authority: forge_reshape_plugin stores it without a decision.");
11238
+ }
10968
11239
  }
10969
11240
  return ExitCode.Ok;
10970
11241
  }
10971
11242
  case "publish": {
10972
- const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
10973
- const manifest = readManifest(positionals[0]);
10974
- const tools = await client2(values, io);
10975
- const outcome = await tools.publishPlugin(manifest, { dryRun: false });
10976
- if (!outcome.ok) {
10977
- io.stderr(`Invalid manifest (${outcome.details.length} problem${outcome.details.length === 1 ? "" : "s"}):`);
10978
- for (const detail of outcome.details) io.stderr(` - ${detail}`);
10979
- return ExitCode.Failure;
10980
- }
10981
- if (values.json) io.stdout(JSON.stringify(outcome.result, null, 2));
10982
- else {
10983
- io.stdout(outcome.result.changed ? "Published." : "Unchanged: this digest was already published.");
10984
- for (const line of formatPlugin(outcome.result.plugin)) io.stdout(line);
10985
- io.stdout("Enable it for the organization with forge plugins (or forge_set_plugin_state); the tools run through the bridge binding of the manifest's runtime.type.");
11243
+ const { values, positionals } = parseArgs8({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
11244
+ const manifest = readManifest2(positionals[0]);
11245
+ const { settings, tools } = await client2(values, io);
11246
+ const outcome = await raise({ tool: "forge_publish_plugin", args: { manifest } }, { settings, tools, json: values.json }, io);
11247
+ if (!values.json && outcome.status === "executed") {
11248
+ const result = outcome.result;
11249
+ io.stdout(result.changed ? "Published." : "Unchanged: this digest was already published.");
11250
+ for (const line of formatPlugin(result.plugin)) io.stdout(line);
11251
+ io.stdout("Enable it for the organization with forge_set_plugin_state; the tools run through the bridge binding of the manifest's runtime.type.");
10986
11252
  }
10987
- return ExitCode.Ok;
11253
+ return worstExitCode([outcome]);
10988
11254
  }
10989
11255
  case "list": {
10990
- const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
10991
- const tools = await client2(values, io);
11256
+ const { values } = parseArgs8({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
11257
+ const { tools } = await client2(values, io);
10992
11258
  const plugins = await tools.listPlugins();
10993
11259
  if (values.json) io.stdout(JSON.stringify(plugins, null, 2));
10994
11260
  else if (!plugins.length) io.stdout("No tenant plugins published for this organization.");
@@ -10996,138 +11262,24 @@ async function runPlugins(args, io) {
10996
11262
  return ExitCode.Ok;
10997
11263
  }
10998
11264
  case "retire": {
10999
- const { values, positionals } = parseArgs7({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
11265
+ const { values, positionals } = parseArgs8({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
11000
11266
  const pluginId = positionals[0];
11001
11267
  if (!pluginId) throw new CliError(`A plugin id is required.
11002
11268
 
11003
11269
  ${pluginsUsage}`);
11004
- const tools = await client2(values, io);
11005
- const plugin = await tools.retirePlugin(pluginId);
11006
- io.stdout(`Retired ${plugin.pluginId}@${plugin.version}; its ${plugin.toolNames.length} tool(s) are gone. Installation records stay for a re-publish.`);
11007
- return ExitCode.Ok;
11270
+ const { settings, tools } = await client2(values, io);
11271
+ const outcome = await raise({ tool: "forge_retire_plugin", args: { pluginId } }, { settings, tools, json: values.json }, io);
11272
+ if (!values.json && outcome.status === "executed") {
11273
+ const { plugin } = outcome.result;
11274
+ io.stdout(`Retired ${plugin.pluginId}@${plugin.version}; its ${plugin.toolNames.length} tool(s) are gone. Installation records stay for a re-publish.`);
11275
+ }
11276
+ return worstExitCode([outcome]);
11008
11277
  }
11009
11278
  default:
11010
11279
  throw unknownVerb("plugins", pluginsUsage, [], verb, rest);
11011
11280
  }
11012
11281
  }
11013
11282
 
11014
- // src/commands/update.ts
11015
- import { spawn as spawn4 } from "node:child_process";
11016
- import path17 from "node:path";
11017
-
11018
- // src/update-notice.ts
11019
- import { spawn as spawn3 } from "node:child_process";
11020
- import fs15 from "node:fs";
11021
- import path16 from "node:path";
11022
- var UPDATE_CHECK_FILE = "update-check.json";
11023
- var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
11024
- function updateCheckFile(env) {
11025
- return path16.join(configDir(env), UPDATE_CHECK_FILE);
11026
- }
11027
- function readUpdateCheck(env) {
11028
- try {
11029
- const parsed = JSON.parse(fs15.readFileSync(updateCheckFile(env), "utf8"));
11030
- if (parsed && typeof parsed === "object" && typeof parsed.checkedAt === "number" && typeof parsed.latest === "string") return parsed;
11031
- } catch {
11032
- }
11033
- return null;
11034
- }
11035
- function writeUpdateCheck(env, check) {
11036
- writeJsonAtomic(updateCheckFile(env), check);
11037
- }
11038
- function noticeFor(check, version = VERSION) {
11039
- if (!check || !isNewer(check.latest, version)) return null;
11040
- return `diverge ${check.latest} is available (this is ${version}). Update with: diverge update (or run npx @divergeai/cli@latest)`;
11041
- }
11042
- function noticeApplies(argv, io, version = VERSION) {
11043
- if (version === "dev" || !io.interactive) return false;
11044
- if (io.env.DIVERGE_NO_UPDATE_CHECK?.trim() || io.env.CI?.trim() || sessionTokenFile(io.env)) return false;
11045
- if (argv[0] === "update") return false;
11046
- if (argv[0] === "tools" && argv[1] === "bridge") return false;
11047
- return true;
11048
- }
11049
- function scheduleUpdateCheck(spawnImpl = spawn3) {
11050
- try {
11051
- const child = spawnImpl(process.execPath, [process.argv[1], "update", "--record"], { detached: true, stdio: "ignore" });
11052
- child.on("error", () => {
11053
- });
11054
- child.unref();
11055
- } catch {
11056
- }
11057
- }
11058
- function updateNotice(argv, io, deps = { now: Date.now, schedule: scheduleUpdateCheck, read: readUpdateCheck, version: VERSION }) {
11059
- if (!noticeApplies(argv, io, deps.version)) return;
11060
- const check = deps.read(io.env);
11061
- const notice = noticeFor(check, deps.version);
11062
- if (notice) io.stderr(notice);
11063
- if (!check || deps.now() - check.checkedAt > UPDATE_CHECK_INTERVAL_MS) deps.schedule();
11064
- }
11065
-
11066
- // src/commands/update.ts
11067
- var PACKAGE_NAME = "@divergeai/cli";
11068
- var updateUsage = "diverge update\n\nInstall the latest release with the npm that installed this one.";
11069
- async function latestVersion(registry = "https://registry.npmjs.org") {
11070
- const body = await fetchJson(`${registry}/${PACKAGE_NAME}/latest`);
11071
- if (!body.version) throw new CliError("The registry answered without a version");
11072
- return body.version;
11073
- }
11074
- function isNewer(a, b) {
11075
- const parse = (v) => {
11076
- const [core, pre] = v.split("-", 2);
11077
- return { nums: core.split(".").map((n) => Number(n) || 0), pre: pre ?? "" };
11078
- };
11079
- const x = parse(a);
11080
- const y = parse(b);
11081
- for (let i = 0; i < 3; i += 1) {
11082
- const dx = x.nums[i] ?? 0;
11083
- const dy = y.nums[i] ?? 0;
11084
- if (dx !== dy) return dx > dy;
11085
- }
11086
- if (x.pre === y.pre) return false;
11087
- if (!x.pre) return true;
11088
- if (!y.pre) return false;
11089
- return x.pre > y.pre;
11090
- }
11091
- async function runUpdate(args, io, deps = { latestVersion, install: installLatest, record: writeUpdateCheck, now: Date.now }) {
11092
- refuseInSession(io.env, "update");
11093
- if (args[0] === "--record") {
11094
- try {
11095
- deps.record(io.env, { checkedAt: deps.now(), latest: await deps.latestVersion() });
11096
- } catch {
11097
- }
11098
- return ExitCode.Ok;
11099
- }
11100
- if (VERSION === "dev") throw new CliError("This is a source checkout, not an installed release; pull the repository instead.");
11101
- const latest = await deps.latestVersion();
11102
- if (!isNewer(latest, VERSION)) {
11103
- io.stdout(`diverge ${VERSION} is up to date.`);
11104
- return ExitCode.Ok;
11105
- }
11106
- io.stdout(`Updating diverge ${VERSION} \u2192 ${latest}\u2026`);
11107
- const code2 = await deps.install(latest);
11108
- if (code2 !== 0) throw new CliError(`npm install exited with ${code2}`);
11109
- io.stdout(`diverge ${latest} installed.`);
11110
- return ExitCode.Ok;
11111
- }
11112
- function installLatest(version, env = process.env, platform = process.platform) {
11113
- let command = "npm";
11114
- const args = ["install", "-g", `${PACKAGE_NAME}@${version}`];
11115
- if (platform === "win32") {
11116
- const npm = findOnPath("npm", env, platform);
11117
- if (!npm) throw new CliError("npm was not found on PATH; install Node.js with npm and try again.");
11118
- command = npm;
11119
- if (/\.(cmd|bat)$/i.test(npm)) {
11120
- command = process.execPath;
11121
- args.unshift(path17.join(path17.dirname(npm), "node_modules", "npm", "bin", "npm-cli.js"));
11122
- }
11123
- }
11124
- return new Promise((resolve, reject) => {
11125
- const child = spawn4(command, args, { env, stdio: "inherit" });
11126
- child.on("error", reject);
11127
- child.on("exit", (code2) => resolve(code2 ?? 1));
11128
- });
11129
- }
11130
-
11131
11283
  // src/cli.ts
11132
11284
  var COMMANDS = [
11133
11285
  { name: "login", summary: "Sign in (authenticate) through the browser", usage: loginUsage, run: runLogin },
@@ -11135,7 +11287,8 @@ var COMMANDS = [
11135
11287
  { name: "status", summary: "Show who you are signed in as: the deployment, the account and what the login carries", usage: statusUsage, run: runStatus },
11136
11288
  { name: "org", summary: "Show or switch the organization the login acts in", usage: orgUsage, run: runOrg },
11137
11289
  { name: "token", summary: "Print a fresh access token", usage: tokenUsage, run: runToken },
11138
- { name: "agents", summary: "Point a coding agent at the model gateway (setup, run, status)", usage: agentsUsage, run: runAgents },
11290
+ { name: "dsh", summary: "Start DeepSeek Harness on the model gateway, up to date, with your skills", usage: dshUsage, run: (args, io) => runDshCommand(args, io) },
11291
+ { name: "agents", summary: "Point a coding agent at the model gateway (setup, status)", usage: agentsUsage, run: runAgents },
11139
11292
  { name: "tools", summary: "Discover and execute platform tools; raise and read approvals", usage: toolsUsage, run: runTools },
11140
11293
  { name: "plugins", summary: "Validate, publish, list and retire tenant plugins for your organization", usage: pluginsUsage, run: runPlugins },
11141
11294
  { name: "skills", summary: "Install the skills of every organization you can act in into Claude Code, Codex and dsh", usage: skillsUsage, run: runSkills },
@@ -11156,7 +11309,7 @@ function usage() {
11156
11309
  "Environment: DIVERGENT_API_URL (deployment; production when unset), DIVERGE_HOME (state directory, default ~/.diverge), DIVERGENT_ORG_ID and DIVERGENT_PROJECT_ID (the organization and project a command acts in), DIVERGENT_TOKEN_FILE (a file holding the token this session runs on, in place of any login)."
11157
11310
  ].join("\n");
11158
11311
  }
11159
- async function run2(argv, io) {
11312
+ async function run(argv, io) {
11160
11313
  const [name, ...rest] = argv;
11161
11314
  if (!name || name === "--help" || name === "-h" || name === "help") {
11162
11315
  io.stdout(usage());
@@ -11176,9 +11329,9 @@ ${usage()}`);
11176
11329
  }
11177
11330
  const own = rest.includes("--") ? rest.slice(0, rest.indexOf("--")) : rest;
11178
11331
  if (own.includes("--help") || own.includes("-h")) {
11179
- const path18 = verbPath(command.usage, own);
11180
- if (path18.length > 0 || own[0].startsWith("-")) {
11181
- io.stdout(verbUsage(command.name, command.usage, path18));
11332
+ const path20 = verbPath(command.usage, own);
11333
+ if (path20.length > 0 || own[0].startsWith("-")) {
11334
+ io.stdout(verbUsage(command.name, command.usage, path20));
11182
11335
  return ExitCode.Ok;
11183
11336
  }
11184
11337
  }
@@ -11206,7 +11359,7 @@ ${verbUsage(command.name, command.usage, verbPath(command.usage, rest))}`);
11206
11359
  }
11207
11360
 
11208
11361
  // src/index.ts
11209
- var code = await run2(process.argv.slice(2), {
11362
+ var code = await run(process.argv.slice(2), {
11210
11363
  stdout: (line) => process.stdout.write(`${line}
11211
11364
  `),
11212
11365
  stderr: (line) => process.stderr.write(`${line}