@divergeai/cli 0.1.24 → 0.1.25

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 +31 -15
  2. package/dist/diverge.mjs +1497 -908
  3. package/package.json +1 -1
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, path17) {
118
- const ctrl = callVisitor(key, node, visitor, path17);
117
+ function visit_(key, node, visitor, path18) {
118
+ const ctrl = callVisitor(key, node, visitor, path18);
119
119
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
120
- replaceNode(key, path17, ctrl);
121
- return visit_(key, ctrl, visitor, path17);
120
+ replaceNode(key, path18, ctrl);
121
+ return visit_(key, ctrl, visitor, path18);
122
122
  }
123
123
  if (typeof ctrl !== "symbol") {
124
124
  if (identity.isCollection(node)) {
125
- path17 = Object.freeze(path17.concat(node));
125
+ path18 = Object.freeze(path18.concat(node));
126
126
  for (let i = 0; i < node.items.length; ++i) {
127
- const ci = visit_(i, node.items[i], visitor, path17);
127
+ const ci = visit_(i, node.items[i], visitor, path18);
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
- path17 = Object.freeze(path17.concat(node));
139
- const ck = visit_("key", node.key, visitor, path17);
138
+ path18 = Object.freeze(path18.concat(node));
139
+ const ck = visit_("key", node.key, visitor, path18);
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, path17);
144
+ const cv = visit_("value", node.value, visitor, path18);
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, path17) {
166
- const ctrl = await callVisitor(key, node, visitor, path17);
165
+ async function visitAsync_(key, node, visitor, path18) {
166
+ const ctrl = await callVisitor(key, node, visitor, path18);
167
167
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
168
- replaceNode(key, path17, ctrl);
169
- return visitAsync_(key, ctrl, visitor, path17);
168
+ replaceNode(key, path18, ctrl);
169
+ return visitAsync_(key, ctrl, visitor, path18);
170
170
  }
171
171
  if (typeof ctrl !== "symbol") {
172
172
  if (identity.isCollection(node)) {
173
- path17 = Object.freeze(path17.concat(node));
173
+ path18 = Object.freeze(path18.concat(node));
174
174
  for (let i = 0; i < node.items.length; ++i) {
175
- const ci = await visitAsync_(i, node.items[i], visitor, path17);
175
+ const ci = await visitAsync_(i, node.items[i], visitor, path18);
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
- path17 = Object.freeze(path17.concat(node));
187
- const ck = await visitAsync_("key", node.key, visitor, path17);
186
+ path18 = Object.freeze(path18.concat(node));
187
+ const ck = await visitAsync_("key", node.key, visitor, path18);
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, path17);
192
+ const cv = await visitAsync_("value", node.value, visitor, path18);
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, path17) {
219
+ function callVisitor(key, node, visitor, path18) {
220
220
  if (typeof visitor === "function")
221
- return visitor(key, node, path17);
221
+ return visitor(key, node, path18);
222
222
  if (identity.isMap(node))
223
- return visitor.Map?.(key, node, path17);
223
+ return visitor.Map?.(key, node, path18);
224
224
  if (identity.isSeq(node))
225
- return visitor.Seq?.(key, node, path17);
225
+ return visitor.Seq?.(key, node, path18);
226
226
  if (identity.isPair(node))
227
- return visitor.Pair?.(key, node, path17);
227
+ return visitor.Pair?.(key, node, path18);
228
228
  if (identity.isScalar(node))
229
- return visitor.Scalar?.(key, node, path17);
229
+ return visitor.Scalar?.(key, node, path18);
230
230
  if (identity.isAlias(node))
231
- return visitor.Alias?.(key, node, path17);
231
+ return visitor.Alias?.(key, node, path18);
232
232
  return void 0;
233
233
  }
234
- function replaceNode(key, path17, node) {
235
- const parent = path17[path17.length - 1];
234
+ function replaceNode(key, path18, node) {
235
+ const parent = path18[path18.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, path17, value) {
847
+ function collectionFromPath(schema, path18, value) {
848
848
  let v = value;
849
- for (let i = path17.length - 1; i >= 0; --i) {
850
- const k = path17[i];
849
+ for (let i = path18.length - 1; i >= 0; --i) {
850
+ const k = path18[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 = (path17) => path17 == null || typeof path17 === "object" && !!path17[Symbol.iterator]().next().done;
869
+ var isEmptyPath = (path18) => path18 == null || typeof path18 === "object" && !!path18[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(path17, value) {
900
- if (isEmptyPath(path17))
899
+ addIn(path18, value) {
900
+ if (isEmptyPath(path18))
901
901
  this.add(value);
902
902
  else {
903
- const [key, ...rest] = path17;
903
+ const [key, ...rest] = path18;
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(path17) {
918
- const [key, ...rest] = path17;
917
+ deleteIn(path18) {
918
+ const [key, ...rest] = path18;
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(path17, keepScalar) {
933
- const [key, ...rest] = path17;
932
+ getIn(path18, keepScalar) {
933
+ const [key, ...rest] = path18;
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(path17) {
952
- const [key, ...rest] = path17;
951
+ hasIn(path18) {
952
+ const [key, ...rest] = path18;
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(path17, value) {
963
- const [key, ...rest] = path17;
962
+ setIn(path18, value) {
963
+ const [key, ...rest] = path18;
964
964
  if (rest.length === 0) {
965
965
  this.set(key, value);
966
966
  } else {
@@ -1004,14 +1004,14 @@ var require_foldFlowLines = __commonJS({
1004
1004
  var FOLD_FLOW = "flow";
1005
1005
  var FOLD_BLOCK = "block";
1006
1006
  var FOLD_QUOTED = "quoted";
1007
- function foldFlowLines(text, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) {
1007
+ function foldFlowLines(text2, indent, mode = "flow", { indentAtStart, lineWidth = 80, minContentWidth = 20, onFold, onOverflow } = {}) {
1008
1008
  if (!lineWidth || lineWidth < 0)
1009
- return text;
1009
+ return text2;
1010
1010
  if (lineWidth < minContentWidth)
1011
1011
  minContentWidth = 0;
1012
1012
  const endStep = Math.max(1 + minContentWidth, 1 + lineWidth - indent.length);
1013
- if (text.length <= endStep)
1014
- return text;
1013
+ if (text2.length <= endStep)
1014
+ return text2;
1015
1015
  const folds = [];
1016
1016
  const escapedFolds = {};
1017
1017
  let end = lineWidth - indent.length;
@@ -1028,14 +1028,14 @@ var require_foldFlowLines = __commonJS({
1028
1028
  let escStart = -1;
1029
1029
  let escEnd = -1;
1030
1030
  if (mode === FOLD_BLOCK) {
1031
- i = consumeMoreIndentedLines(text, i, indent.length);
1031
+ i = consumeMoreIndentedLines(text2, i, indent.length);
1032
1032
  if (i !== -1)
1033
1033
  end = i + endStep;
1034
1034
  }
1035
- for (let ch; ch = text[i += 1]; ) {
1035
+ for (let ch; ch = text2[i += 1]; ) {
1036
1036
  if (mode === FOLD_QUOTED && ch === "\\") {
1037
1037
  escStart = i;
1038
- switch (text[i + 1]) {
1038
+ switch (text2[i + 1]) {
1039
1039
  case "x":
1040
1040
  i += 3;
1041
1041
  break;
@@ -1052,12 +1052,12 @@ var require_foldFlowLines = __commonJS({
1052
1052
  }
1053
1053
  if (ch === "\n") {
1054
1054
  if (mode === FOLD_BLOCK)
1055
- i = consumeMoreIndentedLines(text, i, indent.length);
1055
+ i = consumeMoreIndentedLines(text2, i, indent.length);
1056
1056
  end = i + indent.length + endStep;
1057
1057
  split = void 0;
1058
1058
  } else {
1059
1059
  if (ch === " " && prev && prev !== " " && prev !== "\n" && prev !== " ") {
1060
- const next = text[i + 1];
1060
+ const next = text2[i + 1];
1061
1061
  if (next && next !== " " && next !== "\n" && next !== " ")
1062
1062
  split = i;
1063
1063
  }
@@ -1069,12 +1069,12 @@ var require_foldFlowLines = __commonJS({
1069
1069
  } else if (mode === FOLD_QUOTED) {
1070
1070
  while (prev === " " || prev === " ") {
1071
1071
  prev = ch;
1072
- ch = text[i += 1];
1072
+ ch = text2[i += 1];
1073
1073
  overflow = true;
1074
1074
  }
1075
1075
  const j = i > escEnd + 1 ? i - 2 : escStart - 1;
1076
1076
  if (escapedFolds[j])
1077
- return text;
1077
+ return text2;
1078
1078
  folds.push(j);
1079
1079
  escapedFolds[j] = true;
1080
1080
  end = j + endStep;
@@ -1089,39 +1089,39 @@ var require_foldFlowLines = __commonJS({
1089
1089
  if (overflow && onOverflow)
1090
1090
  onOverflow();
1091
1091
  if (folds.length === 0)
1092
- return text;
1092
+ return text2;
1093
1093
  if (onFold)
1094
1094
  onFold();
1095
- let res = text.slice(0, folds[0]);
1095
+ let res = text2.slice(0, folds[0]);
1096
1096
  for (let i2 = 0; i2 < folds.length; ++i2) {
1097
1097
  const fold = folds[i2];
1098
- const end2 = folds[i2 + 1] || text.length;
1098
+ const end2 = folds[i2 + 1] || text2.length;
1099
1099
  if (fold === 0)
1100
1100
  res = `
1101
- ${indent}${text.slice(0, end2)}`;
1101
+ ${indent}${text2.slice(0, end2)}`;
1102
1102
  else {
1103
1103
  if (mode === FOLD_QUOTED && escapedFolds[fold])
1104
- res += `${text[fold]}\\`;
1104
+ res += `${text2[fold]}\\`;
1105
1105
  res += `
1106
- ${indent}${text.slice(fold + 1, end2)}`;
1106
+ ${indent}${text2.slice(fold + 1, end2)}`;
1107
1107
  }
1108
1108
  }
1109
1109
  return res;
1110
1110
  }
1111
- function consumeMoreIndentedLines(text, i, indent) {
1111
+ function consumeMoreIndentedLines(text2, i, indent) {
1112
1112
  let end = i;
1113
1113
  let start = i + 1;
1114
- let ch = text[start];
1114
+ let ch = text2[start];
1115
1115
  while (ch === " " || ch === " ") {
1116
1116
  if (i < start + indent) {
1117
- ch = text[++i];
1117
+ ch = text2[++i];
1118
1118
  } else {
1119
1119
  do {
1120
- ch = text[++i];
1120
+ ch = text2[++i];
1121
1121
  } while (ch && ch !== "\n");
1122
1122
  end = i;
1123
1123
  start = i + 1;
1124
- ch = text[start];
1124
+ ch = text2[start];
1125
1125
  }
1126
1126
  }
1127
1127
  return end;
@@ -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(path17, value) {
3478
+ addIn(path18, value) {
3479
3479
  if (assertCollection(this.contents))
3480
- this.contents.addIn(path17, value);
3480
+ this.contents.addIn(path18, 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(path17) {
3556
- if (Collection.isEmptyPath(path17)) {
3555
+ deleteIn(path18) {
3556
+ if (Collection.isEmptyPath(path18)) {
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(path17) : false;
3562
+ return assertCollection(this.contents) ? this.contents.deleteIn(path18) : 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(path17, keepScalar) {
3578
- if (Collection.isEmptyPath(path17))
3577
+ getIn(path18, keepScalar) {
3578
+ if (Collection.isEmptyPath(path18))
3579
3579
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3580
- return identity.isCollection(this.contents) ? this.contents.getIn(path17, keepScalar) : void 0;
3580
+ return identity.isCollection(this.contents) ? this.contents.getIn(path18, 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(path17) {
3592
- if (Collection.isEmptyPath(path17))
3591
+ hasIn(path18) {
3592
+ if (Collection.isEmptyPath(path18))
3593
3593
  return this.contents !== void 0;
3594
- return identity.isCollection(this.contents) ? this.contents.hasIn(path17) : false;
3594
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path18) : 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(path17, value) {
3612
- if (Collection.isEmptyPath(path17)) {
3611
+ setIn(path18, value) {
3612
+ if (Collection.isEmptyPath(path18)) {
3613
3613
  this.contents = value;
3614
3614
  } else if (this.contents == null) {
3615
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path17), value);
3615
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path18), value);
3616
3616
  } else if (assertCollection(this.contents)) {
3617
- this.contents.setIn(path17, value);
3617
+ this.contents.setIn(path18, 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, path17) => {
5578
+ visit.itemAtPath = (cst, path18) => {
5579
5579
  let item = cst;
5580
- for (const [field, index] of path17) {
5580
+ for (const [field, index] of path18) {
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, path17) => {
5590
- const parent = visit.itemAtPath(cst, path17.slice(0, -1));
5591
- const field = path17[path17.length - 1][0];
5589
+ visit.parentCollection = (cst, path18) => {
5590
+ const parent = visit.itemAtPath(cst, path18.slice(0, -1));
5591
+ const field = path18[path18.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(path17, item, visitor) {
5598
- let ctrl = visitor(item, path17);
5597
+ function _visit(path18, item, visitor) {
5598
+ let ctrl = visitor(item, path18);
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(path17.concat([[field, i]])), token.items[i], visitor);
5605
+ const ci = _visit(Object.freeze(path18.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, path17);
5616
+ ctrl = ctrl(item, path18);
5617
5617
  }
5618
5618
  }
5619
- return typeof ctrl === "function" ? ctrl(item, path17) : ctrl;
5619
+ return typeof ctrl === "function" ? ctrl(item, path18) : 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 fs15 = this.flowScalar(this.type);
6921
+ const fs16 = this.flowScalar(this.type);
6922
6922
  if (atNextItem || it.value) {
6923
- map.items.push({ start, key: fs15, sep: [] });
6923
+ map.items.push({ start, key: fs16, sep: [] });
6924
6924
  this.onKeyLine = true;
6925
6925
  } else if (it.sep) {
6926
- this.stack.push(fs15);
6926
+ this.stack.push(fs16);
6927
6927
  } else {
6928
- Object.assign(it, { key: fs15, sep: [] });
6928
+ Object.assign(it, { key: fs16, 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 fs15 = this.flowScalar(this.type);
7056
+ const fs16 = this.flowScalar(this.type);
7057
7057
  if (!it || it.value)
7058
- fc.items.push({ start: [], key: fs15, sep: [] });
7058
+ fc.items.push({ start: [], key: fs16, sep: [] });
7059
7059
  else if (it.sep)
7060
- this.stack.push(fs15);
7060
+ this.stack.push(fs16);
7061
7061
  else
7062
- Object.assign(it, { key: fs15, sep: [] });
7062
+ Object.assign(it, { key: fs16, sep: [] });
7063
7063
  return;
7064
7064
  }
7065
7065
  case "flow-map-end":
@@ -7384,10 +7384,18 @@ var CliError = class extends Error {
7384
7384
  }
7385
7385
  exitCode;
7386
7386
  };
7387
+ var NotGrantedError = class extends CliError {
7388
+ constructor(message) {
7389
+ super(message);
7390
+ this.name = "NotGrantedError";
7391
+ }
7392
+ };
7387
7393
 
7388
7394
  // src/commands/agents.ts
7389
- import fs10 from "node:fs";
7390
- import path13 from "node:path";
7395
+ import { randomUUID } from "node:crypto";
7396
+ import fs11 from "node:fs";
7397
+ import os8 from "node:os";
7398
+ import path14 from "node:path";
7391
7399
  import { parseArgs as parseArgs3 } from "node:util";
7392
7400
 
7393
7401
  // src/auth/oauth-client.ts
@@ -7407,13 +7415,13 @@ import fs2 from "node:fs";
7407
7415
  // src/fs/atomic.ts
7408
7416
  import fs from "node:fs";
7409
7417
  import path from "node:path";
7410
- function writeTextAtomic(filePath, text, mode = 384) {
7418
+ function writeTextAtomic(filePath, text2, mode = 384) {
7411
7419
  const dir = path.dirname(filePath);
7412
7420
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
7413
7421
  const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
7414
7422
  const fd = fs.openSync(tmp, "wx", mode);
7415
7423
  try {
7416
- fs.writeFileSync(fd, text);
7424
+ fs.writeFileSync(fd, text2);
7417
7425
  fs.closeSync(fd);
7418
7426
  fs.renameSync(tmp, filePath);
7419
7427
  } catch (error) {
@@ -7449,14 +7457,14 @@ function saveLoginRecord(file, record) {
7449
7457
  writeJsonAtomic(file, record, 384);
7450
7458
  }
7451
7459
  function loadUserToken(file) {
7452
- let text;
7460
+ let text2;
7453
7461
  try {
7454
- text = fs2.readFileSync(file, "utf8");
7462
+ text2 = fs2.readFileSync(file, "utf8");
7455
7463
  } catch {
7456
7464
  return null;
7457
7465
  }
7458
7466
  try {
7459
- const parsed = JSON.parse(text);
7467
+ const parsed = JSON.parse(text2);
7460
7468
  if (typeof parsed.access_token !== "string" || typeof parsed.base_url !== "string" || typeof parsed.expires_at !== "number") {
7461
7469
  return null;
7462
7470
  }
@@ -7488,14 +7496,14 @@ function sessionTokenFile(env) {
7488
7496
  return env[SESSION_TOKEN_VARIABLE]?.trim() || void 0;
7489
7497
  }
7490
7498
  function readSessionToken(file) {
7491
- let text;
7499
+ let text2;
7492
7500
  try {
7493
- text = fs3.readFileSync(file, "utf8");
7501
+ text2 = fs3.readFileSync(file, "utf8");
7494
7502
  } catch (error) {
7495
7503
  const reason = error.code ?? (error instanceof Error ? error.message : String(error));
7496
7504
  throw new CliError(`Cannot read the session token file ${file} named by ${SESSION_TOKEN_VARIABLE} (${reason}).`);
7497
7505
  }
7498
- const token = text.trim();
7506
+ const token = text2.trim();
7499
7507
  if (!token) throw new CliError(`The session token file ${file} named by ${SESSION_TOKEN_VARIABLE} is empty.`);
7500
7508
  return token;
7501
7509
  }
@@ -7548,7 +7556,7 @@ function tokenSource(env, baseUrl) {
7548
7556
  }
7549
7557
 
7550
7558
  // src/version.ts
7551
- var VERSION = true ? "0.1.24" : "dev";
7559
+ var VERSION = true ? "0.1.25" : "dev";
7552
7560
  var USER_AGENT = `diverge/${VERSION}`;
7553
7561
 
7554
7562
  // src/http.ts
@@ -7587,10 +7595,10 @@ async function fetchJson(url, init = {}) {
7587
7595
  body: init.body,
7588
7596
  signal: AbortSignal.timeout(init.timeoutMs ?? 3e4)
7589
7597
  });
7590
- const text = await response.text();
7591
- let body = text;
7598
+ const text2 = await response.text();
7599
+ let body = text2;
7592
7600
  try {
7593
- body = text ? JSON.parse(text) : null;
7601
+ body = text2 ? JSON.parse(text2) : null;
7594
7602
  } catch {
7595
7603
  }
7596
7604
  if (!response.ok) {
@@ -7814,7 +7822,7 @@ async function issueToken(source, record, organizationId, nowMs = Date.now()) {
7814
7822
  },
7815
7823
  (error) => {
7816
7824
  if (error.code === "invalid_grant") return loginRequired(`Your diverge login is no longer valid (${error.message}). Run: diverge login`);
7817
- if (error.code === "invalid_scope") return new CliError(`Not granted${organizationId ? ` at organization ${organizationId}` : ""}: ${error.message}`);
7825
+ if (error.code === "invalid_scope") return new NotGrantedError(`Not granted${organizationId ? ` at organization ${organizationId}` : ""}: ${error.message}`);
7818
7826
  return void 0;
7819
7827
  }
7820
7828
  );
@@ -7931,11 +7939,11 @@ function verbPath(usage2, args) {
7931
7939
  function groupOptions(usage2) {
7932
7940
  return usage2.split("\n", 1)[0].match(/\[--[^\]]+\]/g) ?? [];
7933
7941
  }
7934
- function verbUsage(command, usage2, path17) {
7935
- const lines = usageEntries(usage2).filter((entry) => startsWith(entry.verbs, path17)).flatMap((entry) => entry.lines);
7936
- if (path17.length === 0 || lines.length === 0) return usage2;
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;
7937
7945
  const shared = groupOptions(usage2);
7938
- return [`diverge ${command} ${path17.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
7946
+ return [`diverge ${command} ${path18.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
7939
7947
  }
7940
7948
  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"]);
7941
7949
  function distance(a, b) {
@@ -7972,7 +7980,7 @@ function closest(guess, candidates, rest = [], limit = 3) {
7972
7980
  matches.push({ candidate, score: Math.min(edits, threshold), hint: first });
7973
7981
  continue;
7974
7982
  }
7975
- const line = candidate.lines.find((text) => (prose(text).match(/[a-z]+/g) ?? []).some((word) => namesIt(word, target)));
7983
+ const line = candidate.lines.find((text2) => (prose(text2).match(/[a-z]+/g) ?? []).some((word) => namesIt(word, target)));
7976
7984
  if (line) matches.push({ candidate, score: threshold + 1, hint: line });
7977
7985
  }
7978
7986
  return matches.sort((a, b) => a.score - b.score).slice(0, limit);
@@ -7987,12 +7995,12 @@ function didYouMean(matches) {
7987
7995
  return ` Did you mean one of:
7988
7996
  ${matches.map((match) => ` ${shown(match)}`).join("\n")}`;
7989
7997
  }
7990
- function verbCandidates(command, usage2, path17) {
7998
+ function verbCandidates(command, usage2, path18) {
7991
7999
  const byName = /* @__PURE__ */ new Map();
7992
8000
  for (const entry of usageEntries(usage2)) {
7993
- if (!startsWith(entry.verbs, path17)) continue;
7994
- const name = entry.verbs[path17.length] ?? "";
7995
- const invocation = ["diverge", command, ...path17, ...name ? [name] : []].join(" ");
8001
+ if (!startsWith(entry.verbs, path18)) continue;
8002
+ const name = entry.verbs[path18.length] ?? "";
8003
+ const invocation = ["diverge", command, ...path18, ...name ? [name] : []].join(" ");
7996
8004
  const lines = [entry.summary, ...entry.lines.slice(1).map((line) => line.trim())].filter(Boolean);
7997
8005
  const known = byName.get(name);
7998
8006
  if (known) known.lines.push(...lines);
@@ -8000,9 +8008,9 @@ function verbCandidates(command, usage2, path17) {
8000
8008
  }
8001
8009
  return [...byName.values()];
8002
8010
  }
8003
- function unknownVerb(command, usage2, path17, verb, rest = []) {
8004
- const candidates = verbCandidates(command, usage2, path17);
8005
- const where = [command, ...path17].join(" ");
8011
+ function unknownVerb(command, usage2, path18, verb, rest = []) {
8012
+ const candidates = verbCandidates(command, usage2, path18);
8013
+ const where = [command, ...path18].join(" ");
8006
8014
  if (!verb) return new CliError(`${where} needs one of: ${candidates.map((candidate) => candidate.name).filter(Boolean).join(", ")}
8007
8015
 
8008
8016
  ${usage2}`);
@@ -8010,10 +8018,10 @@ ${usage2}`);
8010
8018
 
8011
8019
  ${usage2}`);
8012
8020
  }
8013
- function optionCandidates(usage2, path17) {
8021
+ function optionCandidates(usage2, path18) {
8014
8022
  const entries = usageEntries(usage2);
8015
8023
  const [header = "", ...body] = usage2.split("\n");
8016
- const described = entries.length === 0 || path17.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path17)).flatMap((entry) => entry.lines);
8024
+ const described = entries.length === 0 || path18.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path18)).flatMap((entry) => entry.lines);
8017
8025
  const byName = /* @__PURE__ */ new Map();
8018
8026
  const add = (line, describes) => {
8019
8027
  for (const [flag] of line.matchAll(/--[a-z][a-z-]*/g)) {
@@ -8030,12 +8038,12 @@ function unknownOption(error, usage2, args) {
8030
8038
  if (!(error instanceof Error) || error.name !== "TypeError") return null;
8031
8039
  const option = /^Unknown option '([^']+)'/.exec(error.message)?.[1];
8032
8040
  if (!option) return null;
8033
- const path17 = verbPath(usage2, args);
8034
- return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path17)))}`;
8041
+ const path18 = verbPath(usage2, args);
8042
+ return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path18)))}`;
8035
8043
  }
8036
8044
 
8037
8045
  // src/commands/login.ts
8038
- import { parseArgs } from "node:util";
8046
+ import { parseArgs as parseArgs2 } from "node:util";
8039
8047
 
8040
8048
  // src/auth/organizations.ts
8041
8049
  function chooseOrganization(input) {
@@ -8090,244 +8098,219 @@ function ask(question) {
8090
8098
  }));
8091
8099
  }
8092
8100
 
8093
- // src/commands/login.ts
8094
- var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
8101
+ // src/commands/skills.ts
8102
+ import { parseArgs } from "node:util";
8095
8103
 
8096
- Sign in through the browser, then choose where to act. The browser only
8097
- authenticates the person; the organization is chosen here afterwards \u2014 the
8098
- one named by --organization-id (id or name), else the last login's, else the
8099
- only membership, else a menu. The login itself is yours, not an
8100
- organization's; the default is a local preference.
8101
- That is the default: commands act there unless they name another with
8102
- --organization-id or DIVERGENT_ORG_ID. Change it with: diverge org use <id|name>.
8103
- --as <email> names the account: the platform signs that account in instead of
8104
- reusing whatever session the browser holds, and issues nothing to any other.
8105
- Without --as, the account of the last login is used; switching is explicit.
8106
- Without --scopes, the login asks for the scopes the platform registers for
8107
- this CLI \u2014 everything it and forge use \u2014 and holds what your roles grant; a
8108
- scope the platform adds later reaches this login on its next refresh.
8109
- --scopes asks for less, deliberately, and the login stays that narrow.
8110
- --api-url signs in to another deployment (a test stack, say). Each deployment
8111
- keeps its own login, so this never replaces another one; commands reach it
8112
- when DIVERGENT_API_URL names it.`;
8113
- function resolveScopes(flag) {
8114
- const explicit = flag?.trim();
8115
- return explicit ? explicit.split(/\s+/).join(" ") : void 0;
8116
- }
8117
- function resolveLoginHint(flag, previous) {
8118
- const explicit = flag?.trim().toLowerCase();
8119
- if (explicit) return explicit;
8120
- const remembered = previous.account?.trim().toLowerCase();
8121
- return remembered || void 0;
8122
- }
8123
- async function runLogin(args, io) {
8124
- refuseInSession(io.env, "login");
8125
- const { values } = parseArgs({
8126
- args,
8127
- options: {
8128
- "organization-id": { type: "string" },
8129
- scopes: { type: "string" },
8130
- as: { type: "string" },
8131
- "no-browser": { type: "boolean", default: false },
8132
- "api-url": { type: "string" }
8133
- },
8134
- strict: true,
8135
- allowPositionals: false
8136
- });
8137
- const apiUrl = resolveApiUrl(values["api-url"], io.env);
8138
- const source = tokenSource(io.env, apiUrl);
8139
- const previous = loadLogin(source);
8140
- const loginHint = resolveLoginHint(values.as, { account: previous?.account });
8141
- if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
8142
- const scopes = resolveScopes(values.scopes);
8143
- io.stdout(scopes ? `Asking for scopes: ${scopes}.` : "Asking for the scopes the platform registers for this CLI.");
8144
- const signIn = await login({
8145
- baseUrl: apiUrl,
8146
- clientId: source.clientId,
8147
- scopes,
8148
- openBrowser: !values["no-browser"],
8149
- loginHint,
8150
- print: io.stdout
8151
- });
8152
- let info;
8153
- try {
8154
- info = await fetchUserInfo(apiUrl, signIn.token.access_token);
8155
- } catch (error) {
8156
- throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
8157
- }
8158
- io.stdout(`Signed in as: ${info.email}`);
8159
- const organizationId = await chooseDefaultOrganization(io, values["organization-id"], previous?.organization, info);
8160
- const issued = await issueToken(source, { base_url: source.baseUrl, refresh_token: signIn.refreshToken, account: info.email }, organizationId);
8161
- saveLogin(source, { ...issued.record, organization: organizationId });
8162
- saveOrganizationToken(source, organizationId, issued.token);
8163
- io.stdout(`Logged in to ${apiUrl}.`);
8164
- if (apiUrl !== resolveApiUrl(void 0, io.env)) io.stdout(`Commands reach this deployment with DIVERGENT_API_URL=${apiUrl}.`);
8165
- const granted = new Set(tokenClaims(issued.token.access_token).scopes ?? []);
8166
- io.stdout(`Scopes: ${[...granted].join(" ") || "none"}`);
8167
- const missing = (scopes ?? "").split(" ").filter((scope) => scope && !granted.has(scope));
8168
- if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
8169
- return 0;
8170
- }
8171
- async function chooseDefaultOrganization(io, flag, remembered, info) {
8172
- let choice = chooseOrganization({ flag, remembered, organizations: info.organizations ?? [] });
8173
- if (choice.kind === "none") {
8174
- throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
8175
- }
8176
- if (choice.kind === "ask") {
8177
- if (!io.interactive) {
8178
- throw new CliError(`You may act in several organizations; pass --organization-id <id|name>:
8179
- ${organizationMenu(choice.options).join("\n")}`);
8180
- }
8181
- io.stdout("Choose the organization to act in:");
8182
- for (const line of organizationMenu(choice.options)) io.stdout(line);
8183
- const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
8184
- choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
8185
- }
8186
- const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
8187
- const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
8188
- io.stdout(`Organization: ${label}${why}`);
8189
- return choice.organizationId;
8190
- }
8104
+ // src/skills/bundled.ts
8105
+ var DIVERGE_SKILL = {
8106
+ name: "diverge",
8107
+ folder: "diverge",
8108
+ description: "Reach the Divergent platform's tools from a shell with the diverge CLI: discover, read one schema, call, and read back a call that waits on a person. Use when a task needs the organization's records, diagnostics, documents or governed actions, which the platform holds and this skill's siblings name.",
8109
+ tools: [],
8110
+ updatedAt: `diverge/${VERSION}`,
8111
+ content: `---
8112
+ name: diverge
8113
+ description: Reach the Divergent platform's tools from a shell with the diverge CLI - discover, read one schema, call, and read back a call that waits on a person. Use when a task needs the organization's records, diagnostics, documents or governed actions, which the platform holds and this skill's siblings name.
8114
+ ---
8191
8115
 
8192
- // src/gateway/urls.ts
8193
- var SURFACES = ["anthropic", "openai"];
8194
- var INFERENCE_SCOPE = "inference:execute";
8195
- function anthropicBaseUrl(apiUrl) {
8196
- return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/anthropic`;
8197
- }
8198
- function openaiBaseUrl(apiUrl) {
8199
- return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/openai`;
8200
- }
8201
- function surfaceBaseUrl(apiUrl, surface) {
8202
- return surface === "openai" ? openaiBaseUrl(apiUrl) : anthropicBaseUrl(apiUrl);
8203
- }
8116
+ # diverge: the platform's tools from a shell
8204
8117
 
8205
- // src/gateway/models.ts
8206
- function servedModel(record) {
8207
- return {
8208
- id: record.id,
8209
- input: record.input_modalities?.includes("image") ? ["text", "image"] : ["text"],
8210
- ...record.context_window ? { contextWindow: record.context_window } : {},
8211
- ...record.max_output_tokens ? { maxOutputTokens: record.max_output_tokens } : {}
8212
- };
8213
- }
8214
- async function listModels(apiUrl, accessToken, surface) {
8215
- try {
8216
- const body = await fetchJson(`${surfaceBaseUrl(apiUrl, surface)}/v1/models`, {
8217
- headers: { Authorization: `Bearer ${accessToken}` }
8218
- });
8219
- return body.data ?? [];
8220
- } catch (error) {
8221
- if (error instanceof HttpError) {
8222
- const hint = error.status === 403 && /scope/i.test(error.message) ? " \u2014 the token needs inference:execute and your role needs inference_models:execute" : "";
8223
- throw new CliError(`Gateway refused (${error.status}): ${error.message}${hint}`);
8224
- }
8225
- throw error;
8226
- }
8227
- }
8228
- async function ensureInferenceLogin(io, apiUrl, organizationId) {
8229
- const source = tokenSource(io.env, apiUrl);
8230
- if (!source.sessionTokenFile && !loadLogin(source)) {
8231
- if (!io.interactive) throw new CliError(`Not signed in to ${apiUrl}. Run: diverge login${organizationId ? ` --organization-id ${organizationId}` : ""}`);
8232
- await runLogin(organizationId ? ["--organization-id", organizationId] : [], io);
8233
- }
8234
- const token = await getUserToken(source, { organizationId });
8235
- const claims = tokenClaims(token.access_token);
8236
- if (!claims.organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
8237
- if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) {
8238
- throw new CliError(`The token for organization ${claims.organizationId} carries no ${INFERENCE_SCOPE}: your roles there lack inference_models:execute, or the login asked for less (re-run: diverge login).`);
8239
- }
8240
- return token;
8241
- }
8118
+ The organization's tools run on the platform, as the signed-in person, with
8119
+ their permissions and audit trail. Nothing runs locally; the CLI is the
8120
+ transport. Keep the catalog out of context: pull the one schema you need.
8242
8121
 
8243
- // src/agents/claude.ts
8244
- import os2 from "node:os";
8245
- import path4 from "node:path";
8246
- var CLAUDE_MODEL_DISCOVERY_FLAG = "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
8247
- function claudeSettingsPath(scope, cwd = process.cwd()) {
8248
- return scope === "user" ? path4.join(os2.homedir(), ".claude", "settings.json") : path4.join(cwd, ".claude", "settings.local.json");
8249
- }
8250
- function mergeClaudeSettings(existing, options) {
8251
- const settings = structuredClone(existing ?? {});
8252
- const env = { ...settings.env ?? {} };
8253
- const warnings = [];
8254
- env.ANTHROPIC_BASE_URL = options.baseUrl;
8255
- if (options.projectId) env.ANTHROPIC_CUSTOM_HEADERS = `x-project-id: ${options.projectId}`;
8256
- else delete env.ANTHROPIC_CUSTOM_HEADERS;
8257
- if (options.modelDiscovery) env[CLAUDE_MODEL_DISCOVERY_FLAG] = "1";
8258
- else delete env[CLAUDE_MODEL_DISCOVERY_FLAG];
8259
- if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) {
8260
- warnings.push("env.ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN is set in these settings and would shadow the platform token; remove it");
8261
- }
8262
- settings.env = env;
8263
- settings.apiKeyHelper = options.tokenHelper;
8264
- return { settings, warnings };
8265
- }
8122
+ \`\`\`bash
8123
+ diverge tools list --filter <word> # find candidates by name or description
8124
+ diverge tools describe <tool> # the one schema, on demand
8125
+ diverge tools call <tool> --args-file args.json --json # arguments from a file, one JSON outcome out
8126
+ diverge tools call search_online --args-json '{"query": "..."}' # web search is a platform tool too
8127
+ \`\`\`
8266
8128
 
8267
- // src/agents/codex.ts
8268
- import os3 from "node:os";
8269
- import path5 from "node:path";
8270
- var CODEX_BLOCK_START = "# >>> divergent >>>";
8271
- var CODEX_BLOCK_END = "# <<< divergent <<<";
8272
- function codexConfigPath() {
8273
- return path5.join(os3.homedir(), ".codex", "config.toml");
8274
- }
8275
- var tomlString = (value) => JSON.stringify(value);
8276
- function codexProviderToml(options) {
8277
- const lines = [
8278
- CODEX_BLOCK_START,
8279
- "[model_providers.divergent]",
8280
- 'name = "Divergent"',
8281
- `base_url = ${tomlString(`${options.openaiBaseUrl}/v1`)}`,
8282
- 'wire_api = "responses"'
8283
- ];
8284
- if (options.projectId) lines.push(`http_headers = { "x-project-id" = ${tomlString(options.projectId)} }`);
8285
- lines.push(
8286
- "",
8287
- "[model_providers.divergent.auth]",
8288
- `command = ${tomlString(options.self.command)}`,
8289
- `args = [${[...options.self.args, "token", ...options.organizationId ? ["--organization-id", options.organizationId] : []].map(tomlString).join(", ")}]`,
8290
- "refresh_interval_ms = 600000",
8291
- CODEX_BLOCK_END,
8292
- ""
8293
- );
8294
- return lines.join("\n");
8295
- }
8296
- function mergeCodexConfig(existing, block) {
8297
- const text = existing ?? "";
8298
- const start = text.indexOf(CODEX_BLOCK_START);
8299
- const endIndex = text.indexOf(CODEX_BLOCK_END);
8300
- if (start !== -1 && endIndex !== -1 && endIndex > start) {
8301
- let end = endIndex + CODEX_BLOCK_END.length;
8302
- if (text[end] === "\n") end += 1;
8303
- return text.slice(0, start) + block + text.slice(end);
8304
- }
8305
- const separator = !text || text.endsWith("\n") ? "" : "\n";
8306
- return text + separator + (text ? "\n" : "") + block;
8307
- }
8129
+ The other skills installed beside this one are the organization's own
8130
+ procedures; each names the tools it is about. Prefer a procedure over
8131
+ guessing a tool.
8308
8132
 
8309
- // src/agents/dsh.ts
8310
- var import_yaml = __toESM(require_dist(), 1);
8311
- import fs5 from "node:fs";
8312
- import { createRequire } from "node:module";
8313
- import os4 from "node:os";
8314
- import path6 from "node:path";
8133
+ ## Arguments: pipe them through stdin
8315
8134
 
8316
- // src/tools/settings.ts
8317
- var BRIDGE_SCOPES = "mcp:read mcp:execute";
8318
- var LOGIN_HINT = "diverge login";
8319
- function loadToolsSettings(options, env = process.env) {
8320
- const apiUrl = resolveApiUrl(void 0, env);
8321
- const projectId = options.projectId?.trim() || env.DIVERGENT_PROJECT_ID?.trim() || void 0;
8322
- let organizationId = options.organizationId?.trim() || env.DIVERGENT_ORG_ID?.trim() || "";
8323
- const tokenFile = sessionTokenFile(env);
8324
- if (tokenFile) {
8325
- organizationId = sessionOrganization(tokenFile, readSessionToken(tokenFile), organizationId || void 0);
8326
- return { apiUrl, organizationId, projectId, mode: "session", tokenFile };
8327
- }
8328
- const cached = loadLogin(tokenSource(env, apiUrl));
8329
- if (cached) {
8330
- organizationId ||= cached.organization ?? "";
8135
+ Shells mangle JSON. Windows PowerShell strips the double quotes inside an
8136
+ inline argument, so \`--args-json '{"a":"b"}'\` can arrive as \`{a:b}\`.
8137
+ Pipe the object to \`--args-json -\` from a heredoc (a here-string on
8138
+ PowerShell): exact on every shell, and no file to keep. A worked call:
8139
+
8140
+ \`\`\`bash
8141
+ diverge tools describe forge_query_issues
8142
+ diverge tools call forge_query_issues --args-json - --json <<'JSON'
8143
+ {"query": "sync failed", "limit": 20}
8144
+ JSON
8145
+ \`\`\`
8146
+
8147
+ \`--args-file <path>\` reads a file you already have. Keep anything a later
8148
+ command needs under the working directory: a sandboxed harness runs each
8149
+ command in a fresh sandbox, and its \`/tmp\` is gone when the command ends.
8150
+
8151
+ ## Read the outcome from stdout with --json
8152
+
8153
+ \`call --json\` prints exactly one JSON object on stdout whatever happens,
8154
+ errors included: \`{"tool", "status", "result"?, "error"?, "invocationId"?,
8155
+ "decideAt"?}\`, where status is executed, pending, rejected, expired or failed.
8156
+ \`result\` is the tool's payload exactly as the tool returned it: the CLI
8157
+ unwraps the platform's envelope and nothing inside. Its shape is the tool's
8158
+ own and differs by tool and plugin \u2014 one returns its records under \`data\`,
8159
+ another at the top level \u2014 so read a tool's shape from its own answer, not
8160
+ from the last tool's. Parse stdout alone; stderr only carries the scope and
8161
+ identity lines. Without \`--json\`, errors are text on stderr.
8162
+
8163
+ ## Several related writes: one set, one decision
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.
8169
+
8170
+ \`\`\`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"}}]
8174
+ JSON
8175
+ \`\`\`
8176
+
8177
+ The set answers like one call (exit 3 while it waits, one invocation id).
8178
+ Once approved its calls run in order and stop at the first failure; read
8179
+ every call's outcome \u2014 succeeded, failed, or not_run \u2014 from \`result.calls\`
8180
+ with \`diverge tools invocation <id>\`. Arguments are frozen when you raise
8181
+ the set, so a call cannot use an id an earlier call creates: raise that one
8182
+ after the first ran. A set carries writes only; reads, and prepared writes
8183
+ that propose a Forge Change, are called on their own.
8184
+
8185
+ ## Several independent calls at once
8186
+
8187
+ When a task needs a sequence of reads or unrelated calls, pipe them to
8188
+ \`diverge tools run -\` once instead of one round trip each (or name a file):
8189
+ a JSON array of \`{"tool": "...", "args": {...}}\`, or one per line. Calls run
8190
+ in order; each prints one JSON line of outcome, the same shape as
8191
+ \`call --json\`; the exit code is the worst of them. Each write in a run is
8192
+ its own decision, so related writes belong in a set.
8193
+
8194
+ ## Exit codes are the contract
8195
+
8196
+ | Code | Meaning | Do |
8197
+ | --- | --- | --- |
8198
+ | 0 | executed | use the result |
8199
+ | 3 | pending: a person must decide it in Compass | hand over the printed link, keep working, read it back later |
8200
+ | 4 | rejected, with the person's reason | re-plan; the reason is data |
8201
+ | 5 | expired: nobody decided in time | raise a fresh call if still needed |
8202
+ | 1 | executed but failed, or a transport error | read the message; do not re-raise blindly |
8203
+
8204
+ A pending call prints its invocation id and a link. Read the outcome with
8205
+ \`diverge tools invocation <id>\` (add \`--wait\` to poll at the platform's
8206
+ cadence). Never decide your own invocation from any surface you can drive.
8207
+
8208
+ ## Session
8209
+
8210
+ - \`diverge status\` says who is signed in and at which organization; a call
8211
+ that names none runs there. To act in another organization, pass
8212
+ \`--organization-id <id>\` (or set \`DIVERGENT_ORG_ID\`) on the call; that
8213
+ never changes the default. \`diverge org use <id|name>\` changes it for
8214
+ every session on the machine, so leave it to the person.
8215
+ - A missing login or scope prints the command to run; \`diverge login\` opens
8216
+ the browser.
8217
+ - \`--json\` on \`list\`, \`describe\` and \`invocation\` gives raw records.
8218
+ `
8219
+ };
8220
+ var DRIVE_CHROME_DESCRIPTION = "Control the Chrome the person runs in debug mode (remote debugging on port 9222) through the mcp__chrome-devtools__ tools - list and pick tabs, open and navigate pages, read a page, click, fill forms, run JavaScript, read console and network, take screenshots and performance traces. Use when a task needs to look at or act in a web page, debug a UI, or reproduce a browser problem.";
8221
+ var DRIVE_CHROME_SKILL = {
8222
+ name: "drive-chrome",
8223
+ folder: "drive-chrome",
8224
+ description: DRIVE_CHROME_DESCRIPTION,
8225
+ tools: [],
8226
+ updatedAt: `diverge/${VERSION}`,
8227
+ content: [
8228
+ "---",
8229
+ "name: drive-chrome",
8230
+ `description: ${DRIVE_CHROME_DESCRIPTION}`,
8231
+ "---",
8232
+ "",
8233
+ "# drive-chrome: the person's Chrome",
8234
+ "",
8235
+ "The `mcp__chrome-devtools__*` tools act in a Chrome the person started with",
8236
+ "remote debugging: their own tabs, signed in as them.",
8237
+ "",
8238
+ "## Find the page",
8239
+ "",
8240
+ "1. `list_pages` names every tab with a page id; the selected one is marked.",
8241
+ "2. `select_page` with that id makes it the page every other tool acts on.",
8242
+ "3. To load something new, `new_page` with the url. Do not `navigate_page` a tab",
8243
+ " the person already had open: that replaces what they were looking at.",
8244
+ "",
8245
+ "## Read before acting",
8246
+ "",
8247
+ "- `take_snapshot` returns the page as text, each element with a `uid`; prefer",
8248
+ " it to a screenshot. `click`, `fill`, `hover` and `fill_form` take those uids.",
8249
+ "- A uid is valid for the snapshot it came from: after an action changes the",
8250
+ " page, take a fresh snapshot before the next one.",
8251
+ "- `evaluate_script` runs a function in the page, for what a snapshot does not",
8252
+ " show: `() => document.title`.",
8253
+ "- `list_console_messages` and `list_network_requests` read what the page logged",
8254
+ " and fetched; `get_network_request` reads one body.",
8255
+ "- `take_screenshot` when the layout itself is the question.",
8256
+ "",
8257
+ "## The person's sessions",
8258
+ "",
8259
+ "Reading is free. Before a click or a script that submits, sends, buys,",
8260
+ "deletes, approves or changes a setting, say what it will do and wait for the",
8261
+ "person. Close the pages you opened with `close_page`; leave theirs.",
8262
+ "",
8263
+ "## When there is no Chrome",
8264
+ "",
8265
+ '"Could not connect to Chrome" means nothing listens on port 9222. Ask the',
8266
+ "person to start Chrome in debug mode. Chrome refuses remote debugging on its",
8267
+ "default profile, so it runs a profile of its own (signed in once, kept after):",
8268
+ "",
8269
+ "```bash",
8270
+ 'google-chrome --user-data-dir="$HOME/.config/google-chrome-debug" --remote-debugging-port=9222 # Linux',
8271
+ 'open -na "Google Chrome" --args --user-data-dir="$HOME/.chrome-debug" --remote-debugging-port=9222 # macOS',
8272
+ "```",
8273
+ "",
8274
+ "On Windows: `chrome.exe --user-data-dir=%LOCALAPPDATA%\\chrome-debug --remote-debugging-port=9222`.",
8275
+ "`curl -s http://127.0.0.1:9222/json/version` answers once it is up. The next",
8276
+ "tool call connects; there is nothing to restart.",
8277
+ "",
8278
+ "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
+ ""
8281
+ ].join("\n")
8282
+ };
8283
+ function bundledSkills(harness) {
8284
+ return harness === "dsh" ? [DIVERGE_SKILL, DRIVE_CHROME_SKILL] : [DIVERGE_SKILL];
8285
+ }
8286
+
8287
+ // src/skills/install.ts
8288
+ import fs6 from "node:fs";
8289
+ import os3 from "node:os";
8290
+ import path5 from "node:path";
8291
+
8292
+ // src/agents/dsh.ts
8293
+ var import_yaml = __toESM(require_dist(), 1);
8294
+ import fs5 from "node:fs";
8295
+ import { createRequire } from "node:module";
8296
+ import os2 from "node:os";
8297
+ import path4 from "node:path";
8298
+
8299
+ // src/tools/settings.ts
8300
+ var BRIDGE_SCOPES = "mcp:read mcp:execute";
8301
+ var LOGIN_HINT = "diverge login";
8302
+ function loadToolsSettings(options, env = process.env) {
8303
+ const apiUrl = resolveApiUrl(void 0, env);
8304
+ const projectId = options.projectId?.trim() || env.DIVERGENT_PROJECT_ID?.trim() || void 0;
8305
+ let organizationId = options.organizationId?.trim() || env.DIVERGENT_ORG_ID?.trim() || "";
8306
+ const tokenFile = sessionTokenFile(env);
8307
+ if (tokenFile) {
8308
+ organizationId = sessionOrganization(tokenFile, readSessionToken(tokenFile), organizationId || void 0);
8309
+ return { apiUrl, organizationId, projectId, mode: "session", tokenFile };
8310
+ }
8311
+ const cached = loadLogin(tokenSource(env, apiUrl));
8312
+ if (cached) {
8313
+ organizationId ||= cached.organization ?? "";
8331
8314
  if (!organizationId) throw new CliError(`The login has no default organization. Pass --organization-id or choose one: diverge org use <id|name>`);
8332
8315
  return { apiUrl, organizationId, projectId, mode: "operator", clientId: clientId(env) };
8333
8316
  }
@@ -8429,10 +8412,10 @@ var ToolsClient = class {
8429
8412
  if (this.settings.mode === "service") headers["x-divergent-user-id"] = this.settings.delegatedUserId;
8430
8413
  return headers;
8431
8414
  }
8432
- scoped(path17) {
8415
+ scoped(path18) {
8433
8416
  const query = new URLSearchParams({ organizationId: this.settings.organizationId });
8434
8417
  if (this.settings.projectId) query.set("projectId", this.settings.projectId);
8435
- return `${this.settings.apiUrl}${path17}?${query.toString()}`;
8418
+ return `${this.settings.apiUrl}${path18}?${query.toString()}`;
8436
8419
  }
8437
8420
  async send(method, url, body) {
8438
8421
  return this.request(method, url, body, void 0);
@@ -8458,13 +8441,13 @@ var ToolsClient = class {
8458
8441
  this.serviceToken = null;
8459
8442
  continue;
8460
8443
  }
8461
- const text = await response.text();
8444
+ const text2 = await response.text();
8462
8445
  if (!response.ok) {
8463
- const handled = refused?.(response.status, text);
8446
+ const handled = refused?.(response.status, text2);
8464
8447
  if (handled !== void 0) return handled;
8465
- let message = text;
8448
+ let message = text2;
8466
8449
  try {
8467
- const parsed = JSON.parse(text);
8450
+ const parsed = JSON.parse(text2);
8468
8451
  if (typeof parsed.error === "string" && parsed.error) message = parsed.error;
8469
8452
  } catch {
8470
8453
  }
@@ -8474,7 +8457,7 @@ var ToolsClient = class {
8474
8457
  }
8475
8458
  throw new CliError(`${response.status}: ${message}`.slice(0, 2e3));
8476
8459
  }
8477
- return text ? JSON.parse(text) : {};
8460
+ return text2 ? JSON.parse(text2) : {};
8478
8461
  }
8479
8462
  throw new CliError("Request failed after re-authentication");
8480
8463
  }
@@ -8487,10 +8470,10 @@ var ToolsClient = class {
8487
8470
  * a tool deployed after its server's catalog was read. A 404 is an answer.
8488
8471
  */
8489
8472
  describeTool(name) {
8490
- return this.request("GET", this.scoped(`/api/plugins/functions/tools/${encodeURIComponent(name)}`), void 0, (status2, text) => {
8473
+ return this.request("GET", this.scoped(`/api/plugins/functions/tools/${encodeURIComponent(name)}`), void 0, (status2, text2) => {
8491
8474
  if (status2 !== 404) return void 0;
8492
8475
  try {
8493
- const parsed = JSON.parse(text);
8476
+ const parsed = JSON.parse(text2);
8494
8477
  if (typeof parsed.error === "string" && parsed.error) return { notFound: parsed.error };
8495
8478
  } catch {
8496
8479
  }
@@ -8539,14 +8522,14 @@ var ToolsClient = class {
8539
8522
  "POST",
8540
8523
  this.descriptorsUrl(),
8541
8524
  { targetOrganizationId: this.settings.organizationId, manifest, dryRun: options.dryRun },
8542
- (status2, text) => {
8525
+ (status2, text2) => {
8543
8526
  if (status2 !== 400) return void 0;
8544
8527
  try {
8545
- const parsed = JSON.parse(text);
8528
+ const parsed = JSON.parse(text2);
8546
8529
  if (Array.isArray(parsed.details)) return { ok: false, details: parsed.details.map(String) };
8547
- return { ok: false, details: [parsed.error ?? text] };
8530
+ return { ok: false, details: [parsed.error ?? text2] };
8548
8531
  } catch {
8549
- return { ok: false, details: [text] };
8532
+ return { ok: false, details: [text2] };
8550
8533
  }
8551
8534
  }
8552
8535
  ).then((outcome) => "ok" in outcome ? outcome : { ok: true, result: outcome });
@@ -8564,7 +8547,7 @@ var ToolsClient = class {
8564
8547
  // src/agents/dsh.ts
8565
8548
  var DSH_SHIPPED_DEFAULT_MODEL = "deepseek-flash";
8566
8549
  function dshHome(env = process.env) {
8567
- return env.DSH_HOME?.trim() || path6.join(os4.homedir(), ".dsh");
8550
+ return env.DSH_HOME?.trim() || path4.join(os2.homedir(), ".dsh");
8568
8551
  }
8569
8552
  function dshDefaultModel(models, requested) {
8570
8553
  if (requested) return requested;
@@ -8586,6 +8569,7 @@ function dshPatchYaml(options) {
8586
8569
  ` api: ${api}`,
8587
8570
  ` baseURL: ${endpoint}`,
8588
8571
  ` apiKeyEnv: ${tokenEnv}`,
8572
+ ...options.launch ? dshLaunchHeaderRows(options.launch) : [],
8589
8573
  " models:",
8590
8574
  ...options.models.flatMap((model) => [
8591
8575
  ` - id: ${JSON.stringify(model.id)}`,
@@ -8613,6 +8597,25 @@ function dshPatchYaml(options) {
8613
8597
  ""
8614
8598
  ].join("\n");
8615
8599
  }
8600
+ var DSH_LAUNCH_ID_HEADER = "x-divergent-launch-id";
8601
+ var DSH_CLIENT_HEADER = "x-divergent-client";
8602
+ function dshLaunchHeaderRows(launch) {
8603
+ return [
8604
+ " headers:",
8605
+ ` ${DSH_CLIENT_HEADER}: ${JSON.stringify(launch.client)}`,
8606
+ ` ${DSH_LAUNCH_ID_HEADER}: ${JSON.stringify(launch.id)}`
8607
+ ];
8608
+ }
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
+ }
8616
8619
  function dshShellRows() {
8617
8620
  return ["bash-sandbox", "pwsh-sandbox"].flatMap((id) => [
8618
8621
  `- id: ${id}`,
@@ -8682,30 +8685,30 @@ var DIVERGENT_PRESET_ID = "divergent";
8682
8685
  var DSH_USER_PRESETS_DIR = ".agent-presets";
8683
8686
  function dshPackageDir(binary) {
8684
8687
  if (/\.(cmd|bat)$/i.test(binary)) {
8685
- const dir2 = path6.join(path6.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8686
- return fs5.existsSync(path6.join(dir2, "package.json")) ? dir2 : null;
8688
+ const dir2 = path4.join(path4.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8689
+ return fs5.existsSync(path4.join(dir2, "package.json")) ? dir2 : null;
8687
8690
  }
8688
8691
  let dir;
8689
8692
  try {
8690
- dir = path6.dirname(fs5.realpathSync(binary));
8693
+ dir = path4.dirname(fs5.realpathSync(binary));
8691
8694
  } catch {
8692
8695
  return null;
8693
8696
  }
8694
8697
  for (; ; ) {
8695
8698
  try {
8696
- if (JSON.parse(fs5.readFileSync(path6.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8699
+ if (JSON.parse(fs5.readFileSync(path4.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8697
8700
  } catch {
8698
8701
  }
8699
- const parent = path6.dirname(dir);
8702
+ const parent = path4.dirname(dir);
8700
8703
  if (parent === dir) return null;
8701
8704
  dir = parent;
8702
8705
  }
8703
8706
  }
8704
8707
  function shippedPresetDir(packageDir, id = "standard") {
8705
8708
  try {
8706
- const require2 = createRequire(path6.join(packageDir, "package.json"));
8707
- const dir = path6.join(path6.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
8708
- return fs5.existsSync(path6.join(dir, "agent.cordis.yml")) ? dir : null;
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;
8709
8712
  } catch {
8710
8713
  return null;
8711
8714
  }
@@ -8742,9 +8745,9 @@ var DIVERGENT_PRESET_METADATA = [
8742
8745
  ""
8743
8746
  ].join("\n");
8744
8747
  function writeDivergentPreset(home, shippedDir) {
8745
- const dir = path6.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
8746
- writeTextAtomic(path6.join(dir, "agent.cordis.yml"), divergentPreset(fs5.readFileSync(path6.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
8747
- writeTextAtomic(path6.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
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);
8748
8751
  return dir;
8749
8752
  }
8750
8753
  function withDivergentPresetDefault(settingsText) {
@@ -8756,515 +8759,1069 @@ function withDivergentPresetDefault(settingsText) {
8756
8759
  return (0, import_yaml.stringify)(settings, { lineWidth: 0 });
8757
8760
  }
8758
8761
 
8759
- // src/agents/opencode.ts
8760
- import os5 from "node:os";
8761
- import path7 from "node:path";
8762
- function opencodeConfigPath() {
8763
- return path7.join(os5.homedir(), ".config", "opencode", "opencode.json");
8762
+ // src/skills/install.ts
8763
+ var HARNESSES = ["claude", "codex", "dsh"];
8764
+ var SKILLS_MANIFEST = ".diverge-skills.json";
8765
+ function skillsRoot(harness, env = process.env) {
8766
+ switch (harness) {
8767
+ case "claude":
8768
+ return path5.join(env.CLAUDE_CONFIG_DIR?.trim() || path5.join(os3.homedir(), ".claude"), "skills");
8769
+ case "codex":
8770
+ return path5.join(env.CODEX_HOME?.trim() || path5.join(os3.homedir(), ".codex"), "skills");
8771
+ case "dsh":
8772
+ return path5.join(dshHome(env), "skills");
8773
+ }
8764
8774
  }
8765
- function mergeOpencodeConfig(existing, options) {
8766
- const tokenEnv = options.tokenEnv ?? "DIVERGENT_TOKEN";
8767
- const config = structuredClone(existing ?? {});
8768
- config.$schema ??= "https://opencode.ai/config.json";
8769
- const providers = { ...config.provider ?? {} };
8770
- const models = Object.fromEntries(options.models.map((model) => [model.id, {
8771
- name: model.id,
8772
- attachment: model.input.includes("image"),
8773
- modalities: { input: model.input, output: ["text"] },
8774
- ...model.contextWindow && model.maxOutputTokens ? { limit: { context: model.contextWindow, output: model.maxOutputTokens } } : {}
8775
- }]));
8776
- const options_ = { baseURL: `${options.baseUrl}/v1`, apiKey: `{env:${tokenEnv}}` };
8777
- providers[`divergent-${options.surface}`] = options.surface === "openai" ? { npm: "@ai-sdk/openai-compatible", name: "Divergent (OpenAI)", options: options_, models } : { npm: "@ai-sdk/anthropic", name: "Divergent (Anthropic)", options: options_, models };
8778
- config.provider = providers;
8779
- return config;
8775
+ function presentHarnesses(env = process.env) {
8776
+ return HARNESSES.filter((harness) => fs6.existsSync(path5.dirname(skillsRoot(harness, env))));
8780
8777
  }
8781
-
8782
- // src/agents/run-dsh.ts
8783
- import { spawn as spawn2 } from "node:child_process";
8784
- import fs6 from "node:fs";
8785
- import path8 from "node:path";
8786
- function writeDshToken(credentialsPath, accessToken) {
8787
- const existing = fs6.existsSync(credentialsPath) ? fs6.readFileSync(credentialsPath, "utf8") : null;
8788
- let merged;
8778
+ function isRecord(value) {
8779
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8780
+ }
8781
+ function readSkillsManifest(root) {
8782
+ let parsed;
8789
8783
  try {
8790
- merged = mergeDshCredentials(existing, accessToken);
8791
- } catch (error) {
8792
- throw new CliError(`${credentialsPath}: ${error instanceof Error ? error.message : String(error)}`);
8784
+ parsed = JSON.parse(fs6.readFileSync(path5.join(root, SKILLS_MANIFEST), "utf8"));
8785
+ } catch {
8786
+ return null;
8793
8787
  }
8794
- writeTextAtomic(credentialsPath, merged, 384);
8788
+ if (!isRecord(parsed)) return null;
8789
+ const installedAt = typeof parsed.installedAt === "string" ? parsed.installedAt : "";
8790
+ const source = typeof parsed.source === "string" ? parsed.source : "";
8791
+ if (isRecord(parsed.organizations)) {
8792
+ return {
8793
+ installedAt,
8794
+ source,
8795
+ cli: isRecord(parsed.cli) ? parsed.cli : {},
8796
+ organizations: parsed.organizations
8797
+ };
8798
+ }
8799
+ if (isRecord(parsed.skills)) return { installedAt, source, cli: {}, organizations: {}, flat: parsed.skills };
8800
+ return null;
8795
8801
  }
8796
- function writeDivergeShim(binDir, cli, sessionTokenPath, platform = process.platform) {
8797
- if (platform === "win32") {
8798
- const file2 = path8.join(binDir, "diverge.cmd");
8799
- const run3 = [cli.command, ...cli.args].map((part) => `"${part}"`).join(" ");
8800
- writeTextAtomic(file2, `@setlocal\r
8801
- @set "${SESSION_TOKEN_VARIABLE}=${sessionTokenPath}"\r
8802
- @${run3} %*\r
8803
- `, 493);
8804
- return file2;
8802
+ function few(names, limit = 6) {
8803
+ return names.length <= limit ? names.join(", ") : `${names.slice(0, limit).join(", ")} and ${names.length - limit} more`;
8804
+ }
8805
+ function heldFolders(manifest) {
8806
+ const held = /* @__PURE__ */ new Map();
8807
+ for (const [folder, version] of Object.entries(manifest?.flat ?? {})) held.set(folder, version);
8808
+ for (const [folder, version] of Object.entries(manifest?.cli ?? {})) held.set(folder, version);
8809
+ for (const section of Object.values(manifest?.organizations ?? {})) for (const [folder, version] of Object.entries(section.skills)) held.set(folder, version);
8810
+ return held;
8811
+ }
8812
+ function retirable(manifest, set2) {
8813
+ const refreshed = new Set(set2.organizations.map((organization) => organization.id));
8814
+ const folders = [...Object.keys(manifest?.flat ?? {}), ...Object.keys(manifest?.cli ?? {})];
8815
+ for (const [id, section] of Object.entries(manifest?.organizations ?? {})) {
8816
+ if (set2.complete || refreshed.has(id)) folders.push(...Object.keys(section.skills));
8805
8817
  }
8806
- const quote = (part) => `'${part.replace(/'/g, `'\\''`)}'`;
8807
- const file = path8.join(binDir, "diverge");
8808
- writeTextAtomic(file, `#!/bin/sh
8809
- ${SESSION_TOKEN_VARIABLE}=${quote(sessionTokenPath)}
8810
- export ${SESSION_TOKEN_VARIABLE}
8811
- exec ${[cli.command, ...cli.args].map(quote).join(" ")} "$@"
8812
- `, 493);
8813
- return file;
8818
+ return folders;
8814
8819
  }
8815
- function dshEnv(env, binDir) {
8816
- const out = {};
8817
- for (const [key, value] of Object.entries(env)) {
8818
- if (value !== void 0 && key !== "DIVERGENT_TOKEN" && key !== "DIVERGENT_ORG_ID") out[key] = value;
8820
+ function installSkills(root, set2, source, now = () => /* @__PURE__ */ new Date()) {
8821
+ const previous = readSkillsManifest(root);
8822
+ const held = heldFolders(previous);
8823
+ const written = [];
8824
+ const added = [];
8825
+ const updated = [];
8826
+ const counts = [];
8827
+ 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`);
8829
+ writeTextAtomic(path5.join(root, skill.folder, "SKILL.md"), skill.content, 420);
8830
+ 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);
8834
+ };
8835
+ for (const organization of set2.organizations) {
8836
+ organization.skills.forEach(write);
8837
+ counts.push({ label: organization.name, count: organization.skills.length });
8819
8838
  }
8820
- out.PATH = env.PATH ? `${binDir}${path8.delimiter}${env.PATH}` : binDir;
8821
- return out;
8822
- }
8823
- function dshArgs(overlayPath, args) {
8824
- const rest = args.includes("--profile") ? args : args.length === 0 ? ["--profile", "web"] : ["--profile", "headless", ...args];
8825
- return ["--patch", overlayPath, ...rest];
8826
- }
8827
- function dshLaunch(binary, args, platform = process.platform, execPath = process.execPath) {
8828
- if (platform !== "win32" || !/\.(cmd|bat)$/i.test(binary)) return { command: binary, args };
8829
- const packageDir = path8.join(path8.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8830
- let bin;
8831
- try {
8832
- bin = JSON.parse(fs6.readFileSync(path8.join(packageDir, "package.json"), "utf8")).bin;
8833
- } catch {
8834
- throw new CliError(`${binary} is not an npm shim for @deepseek-ai/dsh (no ${packageDir}); reinstall: npm install -g @deepseek-ai/dsh`);
8839
+ set2.cli.forEach(write);
8840
+ if (set2.cli.length) counts.push({ label: "CLI", count: set2.cli.length });
8841
+ const removed = [];
8842
+ const kept = new Set(written);
8843
+ for (const folder of retirable(previous, set2)) {
8844
+ if (kept.has(folder) || removed.includes(folder)) continue;
8845
+ fs6.rmSync(path5.join(root, folder), { recursive: true, force: true });
8846
+ removed.push(folder);
8847
+ }
8848
+ const stamp = now().toISOString();
8849
+ const refreshed = new Set(set2.organizations.map((organization) => organization.id));
8850
+ const organizations = {};
8851
+ if (!set2.complete) {
8852
+ for (const [id, section] of Object.entries(previous?.organizations ?? {})) if (!refreshed.has(id)) organizations[id] = section;
8853
+ }
8854
+ for (const organization of set2.organizations) {
8855
+ organizations[organization.id] = {
8856
+ name: organization.name,
8857
+ installedAt: stamp,
8858
+ skills: Object.fromEntries(organization.skills.map((skill) => [skill.folder, skill.updatedAt]))
8859
+ };
8835
8860
  }
8836
- const script = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null ? bin.dsh : void 0;
8837
- if (typeof script !== "string") throw new CliError(`${packageDir}/package.json declares no dsh entry; reinstall: npm install -g @deepseek-ai/dsh`);
8838
- return { command: execPath, args: [path8.join(packageDir, script), ...args] };
8861
+ const manifest = {
8862
+ installedAt: stamp,
8863
+ source,
8864
+ cli: Object.fromEntries(set2.cli.map((skill) => [skill.folder, skill.updatedAt])),
8865
+ organizations
8866
+ };
8867
+ writeJsonAtomic(path5.join(root, SKILLS_MANIFEST), manifest, 420);
8868
+ return { root, written, added, updated, removed, counts };
8839
8869
  }
8840
- async function keepTokenFresh(loop) {
8841
- let written = null;
8842
- while (loop.isRunning()) {
8843
- const token = await loop.freshToken();
8844
- if (token && token !== written) {
8845
- loop.write(token);
8846
- written = token;
8847
- }
8848
- for (let i = 0; i < 30; i += 1) {
8849
- if (!loop.isRunning()) return;
8850
- await loop.sleep(1e3);
8851
- }
8870
+ function skillsStatus(root, set2) {
8871
+ const manifest = readSkillsManifest(root);
8872
+ const held = heldFolders(manifest);
8873
+ const current = [];
8874
+ const behind = [];
8875
+ const missing = [];
8876
+ const served = [...set2.organizations.flatMap((organization) => organization.skills), ...set2.cli];
8877
+ for (const skill of served) {
8878
+ const before = held.get(skill.folder);
8879
+ if (before === void 0 || !fs6.existsSync(path5.join(root, skill.folder, "SKILL.md"))) missing.push(skill.folder);
8880
+ else if (before !== skill.updatedAt) behind.push(skill.folder);
8881
+ else current.push(skill.folder);
8852
8882
  }
8883
+ const servedFolders = new Set(served.map((skill) => skill.folder));
8884
+ const retired = [...new Set(retirable(manifest, set2))].filter((folder) => !servedFolders.has(folder));
8885
+ return { root, installedAt: manifest?.installedAt || null, current, behind, missing, retired };
8853
8886
  }
8854
- function runDsh(options) {
8855
- const writeToken = (token) => {
8856
- writeDshToken(options.credentialsPath, token);
8857
- writeTextAtomic(options.sessionTokenPath, `${token}
8858
- `, 384);
8859
- };
8860
- writeToken(options.initialToken);
8861
- writeDivergeShim(options.binDir, options.cli, options.sessionTokenPath);
8862
- const env = dshEnv(options.env, options.binDir);
8863
- const launch = dshLaunch(options.binary, dshArgs(options.overlayPath, options.args));
8864
- const child = spawn2(launch.command, launch.args, { env, stdio: "inherit" });
8865
- let exited = false;
8866
- let wake = null;
8867
- const finish = () => {
8868
- exited = true;
8869
- wake?.();
8870
- };
8871
- const done = new Promise((resolve, reject) => {
8872
- child.on("error", (error) => {
8873
- finish();
8874
- reject(error);
8875
- });
8876
- child.on("exit", (code2, signal) => {
8877
- finish();
8878
- resolve(code2 ?? (signal ? 128 + 15 : 1));
8879
- });
8880
- });
8881
- const ignore = () => {
8882
- };
8883
- process.on("SIGINT", ignore);
8884
- const terminate = () => child.kill("SIGTERM");
8885
- process.on("SIGTERM", terminate);
8886
- const loop = keepTokenFresh({
8887
- freshToken: options.freshToken,
8888
- write: writeToken,
8889
- isRunning: () => !exited,
8890
- sleep: (ms) => new Promise((resolve) => {
8891
- const timer = setTimeout(() => {
8892
- wake = null;
8893
- resolve();
8894
- }, ms);
8895
- wake = () => {
8896
- clearTimeout(timer);
8897
- wake = null;
8898
- resolve();
8899
- };
8900
- })
8901
- });
8902
- return done.finally(async () => {
8903
- process.off("SIGINT", ignore);
8904
- process.off("SIGTERM", terminate);
8905
- await loop;
8906
- });
8887
+
8888
+ // src/skills/installable.ts
8889
+ async function fetchInstallableSkills(baseUrl, accessToken) {
8890
+ const body = await fetchJson(
8891
+ `${baseUrl.replace(/\/+$/, "")}/api/plugins/symphony/skills/installable`,
8892
+ { headers: { Authorization: `Bearer ${accessToken}` } }
8893
+ );
8894
+ if (!body.success || !body.data) throw new CliError(body.error ?? "The platform answered without skills");
8895
+ if (!body.data.organization) throw new CliError("The platform answered without the organization of its skills: it runs an older release than this CLI; refresh again after its next deploy");
8896
+ return body.data;
8907
8897
  }
8908
8898
 
8909
- // src/fs/path.ts
8910
- import fs7 from "node:fs";
8911
- import path9 from "node:path";
8912
- function findOnPath(name, env = process.env, platform = process.platform) {
8913
- const dirs = (env.PATH ?? "").split(path9.delimiter).filter(Boolean);
8914
- const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
8915
- const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
8916
- for (const dir of dirs) {
8917
- for (const candidate of candidates) {
8918
- const full = path9.join(dir, candidate);
8919
- try {
8920
- fs7.accessSync(full, fs7.constants.X_OK);
8921
- if (fs7.statSync(full).isFile()) return full;
8922
- } catch {
8899
+ // src/skills/login-skills.ts
8900
+ async function collectLoginSkills(deps, options = {}) {
8901
+ const reachable = await deps.organizations();
8902
+ let chosen = reachable;
8903
+ const narrow = options.organizationId?.trim();
8904
+ if (narrow) {
8905
+ const choice = chooseOrganization({ flag: narrow, organizations: reachable });
8906
+ if (choice.kind !== "chosen") throw new CliError(`Not one of your organizations: ${narrow}`);
8907
+ chosen = reachable.filter((organization) => organization.id === choice.organizationId);
8908
+ }
8909
+ const organizations = [];
8910
+ const notes = [];
8911
+ let unanswered = 0;
8912
+ for (const organization of chosen) {
8913
+ try {
8914
+ const served = await deps.installable(organization.id);
8915
+ organizations.push({ id: organization.id, name: served.organization.name, skills: served.skills });
8916
+ } catch (error) {
8917
+ const message = error instanceof Error ? error.message : String(error);
8918
+ if (deps.refused(error)) {
8919
+ organizations.push({ id: organization.id, name: organization.name, skills: [] });
8920
+ notes.push(`${organization.name}: not served (${message})`);
8921
+ } else {
8922
+ unanswered += 1;
8923
+ notes.push(`${organization.name}: not refreshed (${message})`);
8923
8924
  }
8924
8925
  }
8925
8926
  }
8926
- return null;
8927
+ return { organizations, notes, complete: !narrow && unanswered === 0 };
8927
8928
  }
8928
8929
 
8929
8930
  // src/commands/skills.ts
8930
- import { parseArgs as parseArgs2 } from "node:util";
8931
-
8932
- // src/skills/bundled.ts
8933
- var DIVERGE_SKILL = {
8934
- name: "diverge",
8935
- description: "Reach the Divergent platform's tools from a shell with the diverge CLI: discover, read one schema, call, and read back a call that waits on a person. Use when a task needs the organization's records, diagnostics, documents or governed actions, which the platform holds and this skill's siblings name.",
8936
- tools: [],
8937
- updatedAt: `diverge/${VERSION}`,
8938
- content: `---
8939
- name: diverge
8940
- description: Reach the Divergent platform's tools from a shell with the diverge CLI - discover, read one schema, call, and read back a call that waits on a person. Use when a task needs the organization's records, diagnostics, documents or governed actions, which the platform holds and this skill's siblings name.
8941
- ---
8942
-
8943
- # diverge: the platform's tools from a shell
8944
-
8945
- The organization's tools run on the platform, as the signed-in person, with
8946
- their permissions and audit trail. Nothing runs locally; the CLI is the
8947
- transport. Keep the catalog out of context: pull the one schema you need.
8948
-
8949
- \`\`\`bash
8950
- diverge tools list --filter <word> # find candidates by name or description
8951
- diverge tools describe <tool> # the one schema, on demand
8952
- diverge tools call <tool> --args-file args.json --json # arguments from a file, one JSON outcome out
8953
- diverge tools call search_online --args-json '{"query": "..."}' # web search is a platform tool too
8954
- \`\`\`
8955
-
8956
- The other skills installed beside this one are the organization's own
8957
- procedures; each names the tools it is about. Prefer a procedure over
8958
- guessing a tool.
8959
-
8960
- ## Arguments: pipe them through stdin
8961
-
8962
- Shells mangle JSON. Windows PowerShell strips the double quotes inside an
8963
- inline argument, so \`--args-json '{"a":"b"}'\` can arrive as \`{a:b}\`.
8964
- Pipe the object to \`--args-json -\` from a heredoc (a here-string on
8965
- PowerShell): exact on every shell, and no file to keep. A worked call:
8966
-
8967
- \`\`\`bash
8968
- diverge tools describe forge_query_issues
8969
- diverge tools call forge_query_issues --args-json - --json <<'JSON'
8970
- {"query": "sync failed", "limit": 20}
8971
- JSON
8972
- \`\`\`
8931
+ var skillsUsage = `diverge skills <list|install|refresh|status> [--organization-id <id|name>] [--harness claude,codex,dsh] [--into <dir>]
8973
8932
 
8974
- \`--args-file <path>\` reads a file you already have. Keep anything a later
8975
- command needs under the working directory: a sandboxed harness runs each
8976
- command in a fresh sandbox, and its \`/tmp\` is gone when the command ends.
8977
-
8978
- ## Read the outcome from stdout with --json
8979
-
8980
- \`call --json\` prints exactly one JSON object on stdout whatever happens,
8981
- errors included: \`{"tool", "status", "result"?, "error"?, "invocationId"?,
8982
- "decideAt"?}\`, where status is executed, pending, rejected, expired or failed.
8983
- \`result\` is the tool's payload exactly as the tool returned it: the CLI
8984
- unwraps the platform's envelope and nothing inside. Its shape is the tool's
8985
- own and differs by tool and plugin \u2014 one returns its records under \`data\`,
8986
- another at the top level \u2014 so read a tool's shape from its own answer, not
8987
- from the last tool's. Parse stdout alone; stderr only carries the scope and
8988
- identity lines. Without \`--json\`, errors are text on stderr.
8989
-
8990
- ## Several related writes: one set, one decision
8991
-
8992
- When two or more writes serve one intent \u2014 assign a role to several groups,
8993
- deactivate several users, set several notification routes \u2014 raise them as
8994
- one set, never one call per write: the person approves the set once instead
8995
- of clicking through one card each.
8933
+ list the skills the platform serves you: in every organization you can act in, the skills whose tools you can run
8934
+ install write them, and the CLI's own skills ("diverge"; "drive-chrome" for dsh), into each harness's user skill folder
8935
+ (~/.claude/skills, ~/.codex/skills, $DSH_HOME/skills), side by side under each organization's prefix;
8936
+ retires what an organization stopped serving; reports what changed \u2014 the full list is \`skills list\`
8937
+ refresh the same, best effort: no login, no network or nothing changed prints nothing and never fails.
8938
+ Runs at sign-in and at every session start, so the folders hold what the organizations serve now
8939
+ status what each folder holds against what the platform serves now
8996
8940
 
8997
- \`\`\`bash
8998
- diverge tools set - --summary "Coordinators get the field role" --json <<'JSON'
8999
- [{"tool": "rbac_assign_role_to_group", "args": {"roleId": "r1", "groupId": "g1"}},
9000
- {"tool": "rbac_assign_role_to_group", "args": {"roleId": "r1", "groupId": "g2"}}]
9001
- JSON
9002
- \`\`\`
8941
+ A skill is a procedure over the tools it names; you get the ones whose every
8942
+ 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.
8945
+ --organization-id refreshes one organization and leaves the others as they
8946
+ are; without it the whole login is refreshed, and an organization the login
8947
+ no longer lists is retired. Without --harness, the harnesses present on this
8948
+ machine are written; name one to write it anyway.`;
8949
+ function narrowedOrganization(flag, env) {
8950
+ return flag?.trim() || env.DIVERGENT_ORG_ID?.trim() || void 0;
8951
+ }
8952
+ function loginDeps(apiUrl, env) {
8953
+ const source = tokenSource(env, apiUrl);
8954
+ return {
8955
+ organizations: async () => {
8956
+ const token = await getUserToken(source);
8957
+ if (source.sessionTokenFile) {
8958
+ const organizationId = tokenClaims(token.access_token).organizationId;
8959
+ if (!organizationId) throw new CliError("The session token names no organization");
8960
+ return [{ id: organizationId, name: organizationId }];
8961
+ }
8962
+ return (await fetchUserInfo(source.baseUrl, token.access_token)).organizations ?? [];
8963
+ },
8964
+ installable: async (organizationId) => {
8965
+ const token = await getUserToken(source, { organizationId });
8966
+ return fetchInstallableSkills(apiUrl, token.access_token);
8967
+ },
8968
+ refused: (error) => error instanceof NotGrantedError || error instanceof HttpError && error.status === 403
8969
+ };
8970
+ }
8971
+ async function loginSkillSet(apiUrl, env, organizationId) {
8972
+ return collectLoginSkills(loginDeps(apiUrl, env), { organizationId });
8973
+ }
8974
+ function harnessesOf(flag, env) {
8975
+ if (!flag) return presentHarnesses(env);
8976
+ const chosen = flag.split(",").map((name) => name.trim()).filter(Boolean);
8977
+ for (const name of chosen) if (!HARNESSES.includes(name)) throw new CliError(`--harness must name some of: ${HARNESSES.join(", ")}`);
8978
+ return chosen;
8979
+ }
8980
+ function installSet(harness, skills) {
8981
+ return { cli: bundledSkills(harness), organizations: skills.organizations, complete: skills.complete };
8982
+ }
8983
+ function installReport(harness, result) {
8984
+ const holdings = result.counts.map((entry) => `${entry.label} ${entry.count}`).join(", ");
8985
+ const changes = [
8986
+ ...result.added.length ? [`${result.added.length} new: ${few(result.added)}`] : [],
8987
+ ...result.updated.length ? [`${result.updated.length} updated: ${few(result.updated)}`] : [],
8988
+ ...result.removed.length ? [`${result.removed.length} retired: ${few(result.removed)}`] : []
8989
+ ];
8990
+ return `${harness}: ${result.written.length} skills in ${result.root} (${holdings}${changes.length ? `; ${changes.join("; ")}` : "; unchanged"})`;
8991
+ }
8992
+ function installForHarness(io, harness, skills, apiUrl, into) {
8993
+ const result = installSkills(into ?? skillsRoot(harness, io.env), installSet(harness, skills), apiUrl);
8994
+ io.stdout(installReport(harness, result));
8995
+ return result;
8996
+ }
8997
+ async function refreshSkills(io, apiUrl, options = {}) {
8998
+ const harnesses = presentHarnesses(io.env);
8999
+ if (harnesses.length === 0) {
9000
+ if (!options.quiet) io.stdout("No coding-agent harness on this machine yet (Claude Code, Codex or dsh); run `diverge skills install --harness <name>` once one is.");
9001
+ return;
9002
+ }
9003
+ let skills;
9004
+ try {
9005
+ skills = await loginSkillSet(apiUrl, io.env, options.organizationId);
9006
+ } catch (error) {
9007
+ io.stderr(`warning: skills not refreshed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
9008
+ return;
9009
+ }
9010
+ for (const harness of harnesses) {
9011
+ const result = installSkills(skillsRoot(harness, io.env), installSet(harness, skills), apiUrl);
9012
+ if (!options.quiet || result.added.length || result.updated.length || result.removed.length) io.stdout(installReport(harness, result));
9013
+ }
9014
+ for (const note of skills.notes) io.stderr(`warning: ${note}`);
9015
+ }
9016
+ async function runSkills(args, io) {
9017
+ const [verb, ...rest] = args;
9018
+ const { values } = parseArgs({ args: rest, options: { "organization-id": { type: "string" }, harness: { type: "string" }, into: { type: "string" } }, strict: true });
9019
+ const apiUrl = resolveApiUrl(void 0, io.env);
9020
+ const organizationId = narrowedOrganization(values["organization-id"], io.env);
9021
+ switch (verb) {
9022
+ case "list": {
9023
+ const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9024
+ io.stdout(`Skills the platform serves you, after the CLI's own (${bundledSkills("dsh").map((skill) => skill.name).join("; ")}):`);
9025
+ for (const organization of skills.organizations) {
9026
+ io.stdout(`${organization.name} (${organization.id}): ${organization.skills.length} skills`);
9027
+ for (const skill of organization.skills) io.stdout(` ${skill.folder.padEnd(40)} ${skill.tools.join(", ")}`);
9028
+ }
9029
+ for (const note of skills.notes) io.stdout(note);
9030
+ return ExitCode.Ok;
9031
+ }
9032
+ case "install": {
9033
+ const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9034
+ const harnesses = values.into ? [harnessesOf(values.harness ?? HARNESSES[0], io.env)[0]] : harnessesOf(values.harness, io.env);
9035
+ 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}`);
9038
+ return ExitCode.Ok;
9039
+ }
9040
+ case "refresh": {
9041
+ await refreshSkills(io, apiUrl, { organizationId, quiet: true });
9042
+ return ExitCode.Ok;
9043
+ }
9044
+ case "status": {
9045
+ const skills = await loginSkillSet(apiUrl, io.env, organizationId);
9046
+ let behind = false;
9047
+ for (const harness of harnessesOf(values.harness, io.env)) {
9048
+ const status2 = skillsStatus(values.into ?? skillsRoot(harness, io.env), installSet(harness, skills));
9049
+ const parts = [`${status2.current.length} current`];
9050
+ if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
9051
+ if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
9052
+ if (status2.retired.length) parts.push(`${status2.retired.length} retired (${few(status2.retired)})`);
9053
+ behind ||= status2.behind.length + status2.missing.length + status2.retired.length > 0;
9054
+ io.stdout(`${harness}: ${parts.join(", ")} in ${status2.root}${status2.installedAt ? ` (installed ${status2.installedAt})` : " (never installed)"}`);
9055
+ }
9056
+ for (const note of skills.notes) io.stdout(note);
9057
+ if (behind) io.stdout("Run: diverge skills install");
9058
+ return ExitCode.Ok;
9059
+ }
9060
+ default:
9061
+ throw unknownVerb("skills", skillsUsage, [], verb, rest);
9062
+ }
9063
+ }
9003
9064
 
9004
- The set answers like one call (exit 3 while it waits, one invocation id).
9005
- Once approved its calls run in order and stop at the first failure; read
9006
- every call's outcome \u2014 succeeded, failed, or not_run \u2014 from \`result.calls\`
9007
- with \`diverge tools invocation <id>\`. Arguments are frozen when you raise
9008
- the set, so a call cannot use an id an earlier call creates: raise that one
9009
- after the first ran. A set carries writes only; reads, and prepared writes
9010
- that propose a Forge Change, are called on their own.
9065
+ // src/commands/login.ts
9066
+ var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
9011
9067
 
9012
- ## Several independent calls at once
9013
-
9014
- When a task needs a sequence of reads or unrelated calls, pipe them to
9015
- \`diverge tools run -\` once instead of one round trip each (or name a file):
9016
- a JSON array of \`{"tool": "...", "args": {...}}\`, or one per line. Calls run
9017
- in order; each prints one JSON line of outcome, the same shape as
9018
- \`call --json\`; the exit code is the worst of them. Each write in a run is
9019
- its own decision, so related writes belong in a set.
9020
-
9021
- ## Exit codes are the contract
9068
+ Sign in through the browser, then choose where to act. The browser only
9069
+ authenticates the person; the organization is chosen here afterwards \u2014 the
9070
+ one named by --organization-id (id or name), else the last login's, else the
9071
+ only membership, else a menu. The login itself is yours, not an
9072
+ organization's; the default is a local preference.
9073
+ That is the default: commands act there unless they name another with
9074
+ --organization-id or DIVERGENT_ORG_ID. Change it with: diverge org use <id|name>.
9075
+ Signing in also installs the login's skills \u2014 every organization it can act
9076
+ in, side by side \u2014 into the coding-agent harnesses on this machine.
9077
+ --as <email> names the account: the platform signs that account in instead of
9078
+ 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.
9080
+ Without --scopes, the login asks for the scopes the platform registers for
9081
+ this CLI \u2014 everything it and forge use \u2014 and holds what your roles grant; a
9082
+ scope the platform adds later reaches this login on its next refresh.
9083
+ --scopes asks for less, deliberately, and the login stays that narrow.
9084
+ --api-url signs in to another deployment (a test stack, say). Each deployment
9085
+ keeps its own login, so this never replaces another one; commands reach it
9086
+ when DIVERGENT_API_URL names it.`;
9087
+ function resolveScopes(flag) {
9088
+ const explicit = flag?.trim();
9089
+ return explicit ? explicit.split(/\s+/).join(" ") : void 0;
9090
+ }
9091
+ function resolveLoginHint(flag, previous) {
9092
+ const explicit = flag?.trim().toLowerCase();
9093
+ if (explicit) return explicit;
9094
+ const remembered = previous.account?.trim().toLowerCase();
9095
+ return remembered || void 0;
9096
+ }
9097
+ async function runLogin(args, io) {
9098
+ refuseInSession(io.env, "login");
9099
+ const { values } = parseArgs2({
9100
+ args,
9101
+ options: {
9102
+ "organization-id": { type: "string" },
9103
+ scopes: { type: "string" },
9104
+ as: { type: "string" },
9105
+ "no-browser": { type: "boolean", default: false },
9106
+ "api-url": { type: "string" }
9107
+ },
9108
+ strict: true,
9109
+ allowPositionals: false
9110
+ });
9111
+ const apiUrl = resolveApiUrl(values["api-url"], io.env);
9112
+ const source = tokenSource(io.env, apiUrl);
9113
+ const previous = loadLogin(source);
9114
+ const loginHint = resolveLoginHint(values.as, { account: previous?.account });
9115
+ if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
9116
+ const scopes = resolveScopes(values.scopes);
9117
+ io.stdout(scopes ? `Asking for scopes: ${scopes}.` : "Asking for the scopes the platform registers for this CLI.");
9118
+ const signIn = await login({
9119
+ baseUrl: apiUrl,
9120
+ clientId: source.clientId,
9121
+ scopes,
9122
+ openBrowser: !values["no-browser"],
9123
+ loginHint,
9124
+ print: io.stdout
9125
+ });
9126
+ let info;
9127
+ try {
9128
+ info = await fetchUserInfo(apiUrl, signIn.token.access_token);
9129
+ } catch (error) {
9130
+ 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
+ }
9132
+ io.stdout(`Signed in as: ${info.email}`);
9133
+ const organizationId = await chooseDefaultOrganization(io, values["organization-id"], previous?.organization, info);
9134
+ const issued = await issueToken(source, { base_url: source.baseUrl, refresh_token: signIn.refreshToken, account: info.email }, organizationId);
9135
+ saveLogin(source, { ...issued.record, organization: organizationId });
9136
+ saveOrganizationToken(source, organizationId, issued.token);
9137
+ io.stdout(`Logged in to ${apiUrl}.`);
9138
+ if (apiUrl !== resolveApiUrl(void 0, io.env)) io.stdout(`Commands reach this deployment with DIVERGENT_API_URL=${apiUrl}.`);
9139
+ const granted = new Set(tokenClaims(issued.token.access_token).scopes ?? []);
9140
+ io.stdout(`Scopes: ${[...granted].join(" ") || "none"}`);
9141
+ const missing = (scopes ?? "").split(" ").filter((scope) => scope && !granted.has(scope));
9142
+ if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
9143
+ await refreshSkills(io, apiUrl);
9144
+ return 0;
9145
+ }
9146
+ async function chooseDefaultOrganization(io, flag, remembered, info) {
9147
+ let choice = chooseOrganization({ flag, remembered, organizations: info.organizations ?? [] });
9148
+ if (choice.kind === "none") {
9149
+ throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
9150
+ }
9151
+ if (choice.kind === "ask") {
9152
+ if (!io.interactive) {
9153
+ throw new CliError(`You may act in several organizations; pass --organization-id <id|name>:
9154
+ ${organizationMenu(choice.options).join("\n")}`);
9155
+ }
9156
+ io.stdout("Choose the organization to act in:");
9157
+ for (const line of organizationMenu(choice.options)) io.stdout(line);
9158
+ const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
9159
+ choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
9160
+ }
9161
+ const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
9162
+ const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
9163
+ io.stdout(`Organization: ${label}${why}`);
9164
+ return choice.organizationId;
9165
+ }
9022
9166
 
9023
- | Code | Meaning | Do |
9024
- | --- | --- | --- |
9025
- | 0 | executed | use the result |
9026
- | 3 | pending: a person must decide it in Compass | hand over the printed link, keep working, read it back later |
9027
- | 4 | rejected, with the person's reason | re-plan; the reason is data |
9028
- | 5 | expired: nobody decided in time | raise a fresh call if still needed |
9029
- | 1 | executed but failed, or a transport error | read the message; do not re-raise blindly |
9167
+ // src/gateway/urls.ts
9168
+ var SURFACES = ["anthropic", "openai"];
9169
+ var INFERENCE_SCOPE = "inference:execute";
9170
+ function anthropicBaseUrl(apiUrl) {
9171
+ return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/anthropic`;
9172
+ }
9173
+ function openaiBaseUrl(apiUrl) {
9174
+ return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/openai`;
9175
+ }
9176
+ function surfaceBaseUrl(apiUrl, surface) {
9177
+ return surface === "openai" ? openaiBaseUrl(apiUrl) : anthropicBaseUrl(apiUrl);
9178
+ }
9030
9179
 
9031
- A pending call prints its invocation id and a link. Read the outcome with
9032
- \`diverge tools invocation <id>\` (add \`--wait\` to poll at the platform's
9033
- cadence). Never decide your own invocation from any surface you can drive.
9180
+ // src/gateway/models.ts
9181
+ function servedModel(record) {
9182
+ return {
9183
+ id: record.id,
9184
+ input: record.input_modalities?.includes("image") ? ["text", "image"] : ["text"],
9185
+ ...record.context_window ? { contextWindow: record.context_window } : {},
9186
+ ...record.max_output_tokens ? { maxOutputTokens: record.max_output_tokens } : {}
9187
+ };
9188
+ }
9189
+ async function listModels(apiUrl, accessToken, surface) {
9190
+ try {
9191
+ const body = await fetchJson(`${surfaceBaseUrl(apiUrl, surface)}/v1/models`, {
9192
+ headers: { Authorization: `Bearer ${accessToken}` }
9193
+ });
9194
+ return body.data ?? [];
9195
+ } catch (error) {
9196
+ if (error instanceof HttpError) {
9197
+ const hint = error.status === 403 && /scope/i.test(error.message) ? " \u2014 the token needs inference:execute and your role needs inference_models:execute" : "";
9198
+ throw new CliError(`Gateway refused (${error.status}): ${error.message}${hint}`);
9199
+ }
9200
+ throw error;
9201
+ }
9202
+ }
9203
+ async function ensureInferenceLogin(io, apiUrl, organizationId) {
9204
+ const source = tokenSource(io.env, apiUrl);
9205
+ if (!source.sessionTokenFile && !loadLogin(source)) {
9206
+ if (!io.interactive) throw new CliError(`Not signed in to ${apiUrl}. Run: diverge login${organizationId ? ` --organization-id ${organizationId}` : ""}`);
9207
+ await runLogin(organizationId ? ["--organization-id", organizationId] : [], io);
9208
+ }
9209
+ const token = await getUserToken(source, { organizationId });
9210
+ const claims = tokenClaims(token.access_token);
9211
+ if (!claims.organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
9212
+ if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) {
9213
+ throw new CliError(`The token for organization ${claims.organizationId} carries no ${INFERENCE_SCOPE}: your roles there lack inference_models:execute, or the login asked for less (re-run: diverge login).`);
9214
+ }
9215
+ return token;
9216
+ }
9034
9217
 
9035
- ## Session
9218
+ // src/agents/claude.ts
9219
+ import os4 from "node:os";
9220
+ import path6 from "node:path";
9221
+ var CLAUDE_MODEL_DISCOVERY_FLAG = "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
9222
+ function claudeSettingsPath(scope, cwd = process.cwd()) {
9223
+ return scope === "user" ? path6.join(os4.homedir(), ".claude", "settings.json") : path6.join(cwd, ".claude", "settings.local.json");
9224
+ }
9225
+ var SKILLS_REFRESH_MATCHER = "startup|resume";
9226
+ function isSkillsRefreshHook(hook) {
9227
+ return typeof hook === "object" && hook !== null && typeof hook.command === "string" && /(^|\s)skills refresh$/.test(hook.command);
9228
+ }
9229
+ function withSkillsRefreshHook(hooks, command) {
9230
+ const all = typeof hooks === "object" && hooks !== null ? structuredClone(hooks) : {};
9231
+ const entries = Array.isArray(all.SessionStart) ? all.SessionStart : [];
9232
+ const kept = entries.map((entry) => {
9233
+ if (typeof entry !== "object" || entry === null || !Array.isArray(entry.hooks)) return entry;
9234
+ return { ...entry, hooks: entry.hooks.filter((hook) => !isSkillsRefreshHook(hook)) };
9235
+ }).filter((entry) => !(typeof entry === "object" && entry !== null && Array.isArray(entry.hooks) && entry.hooks.length === 0));
9236
+ kept.push({ matcher: SKILLS_REFRESH_MATCHER, hooks: [{ type: "command", command, timeout: 30 }] });
9237
+ all.SessionStart = kept;
9238
+ return all;
9239
+ }
9240
+ function mergeClaudeSettings(existing, options) {
9241
+ const settings = structuredClone(existing ?? {});
9242
+ const env = { ...settings.env ?? {} };
9243
+ const warnings = [];
9244
+ env.ANTHROPIC_BASE_URL = options.baseUrl;
9245
+ if (options.projectId) env.ANTHROPIC_CUSTOM_HEADERS = `x-project-id: ${options.projectId}`;
9246
+ else delete env.ANTHROPIC_CUSTOM_HEADERS;
9247
+ if (options.modelDiscovery) env[CLAUDE_MODEL_DISCOVERY_FLAG] = "1";
9248
+ else delete env[CLAUDE_MODEL_DISCOVERY_FLAG];
9249
+ if (env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN) {
9250
+ warnings.push("env.ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN is set in these settings and would shadow the platform token; remove it");
9251
+ }
9252
+ settings.env = env;
9253
+ settings.apiKeyHelper = options.tokenHelper;
9254
+ if (options.skillsRefresh) settings.hooks = withSkillsRefreshHook(settings.hooks, options.skillsRefresh);
9255
+ return { settings, warnings };
9256
+ }
9036
9257
 
9037
- - \`diverge status\` says who is signed in and at which organization; a call
9038
- that names none runs there. To act in another organization, pass
9039
- \`--organization-id <id>\` (or set \`DIVERGENT_ORG_ID\`) on the call; that
9040
- never changes the default. \`diverge org use <id|name>\` changes it for
9041
- every session on the machine, so leave it to the person.
9042
- - A missing login or scope prints the command to run; \`diverge login\` opens
9043
- the browser.
9044
- - \`--json\` on \`list\`, \`describe\` and \`invocation\` gives raw records.
9045
- `
9046
- };
9047
- var DRIVE_CHROME_DESCRIPTION = "Control the Chrome the person runs in debug mode (remote debugging on port 9222) through the mcp__chrome-devtools__ tools - list and pick tabs, open and navigate pages, read a page, click, fill forms, run JavaScript, read console and network, take screenshots and performance traces. Use when a task needs to look at or act in a web page, debug a UI, or reproduce a browser problem.";
9048
- var DRIVE_CHROME_SKILL = {
9049
- name: "drive-chrome",
9050
- description: DRIVE_CHROME_DESCRIPTION,
9051
- tools: [],
9052
- updatedAt: `diverge/${VERSION}`,
9053
- content: [
9054
- "---",
9055
- "name: drive-chrome",
9056
- `description: ${DRIVE_CHROME_DESCRIPTION}`,
9057
- "---",
9058
- "",
9059
- "# drive-chrome: the person's Chrome",
9060
- "",
9061
- "The `mcp__chrome-devtools__*` tools act in a Chrome the person started with",
9062
- "remote debugging: their own tabs, signed in as them.",
9063
- "",
9064
- "## Find the page",
9065
- "",
9066
- "1. `list_pages` names every tab with a page id; the selected one is marked.",
9067
- "2. `select_page` with that id makes it the page every other tool acts on.",
9068
- "3. To load something new, `new_page` with the url. Do not `navigate_page` a tab",
9069
- " the person already had open: that replaces what they were looking at.",
9070
- "",
9071
- "## Read before acting",
9072
- "",
9073
- "- `take_snapshot` returns the page as text, each element with a `uid`; prefer",
9074
- " it to a screenshot. `click`, `fill`, `hover` and `fill_form` take those uids.",
9075
- "- A uid is valid for the snapshot it came from: after an action changes the",
9076
- " page, take a fresh snapshot before the next one.",
9077
- "- `evaluate_script` runs a function in the page, for what a snapshot does not",
9078
- " show: `() => document.title`.",
9079
- "- `list_console_messages` and `list_network_requests` read what the page logged",
9080
- " and fetched; `get_network_request` reads one body.",
9081
- "- `take_screenshot` when the layout itself is the question.",
9082
- "",
9083
- "## The person's sessions",
9084
- "",
9085
- "Reading is free. Before a click or a script that submits, sends, buys,",
9086
- "deletes, approves or changes a setting, say what it will do and wait for the",
9087
- "person. Close the pages you opened with `close_page`; leave theirs.",
9088
- "",
9089
- "## When there is no Chrome",
9090
- "",
9091
- '"Could not connect to Chrome" means nothing listens on port 9222. Ask the',
9092
- "person to start Chrome in debug mode. Chrome refuses remote debugging on its",
9093
- "default profile, so it runs a profile of its own (signed in once, kept after):",
9094
- "",
9095
- "```bash",
9096
- 'google-chrome --user-data-dir="$HOME/.config/google-chrome-debug" --remote-debugging-port=9222 # Linux',
9097
- 'open -na "Google Chrome" --args --user-data-dir="$HOME/.chrome-debug" --remote-debugging-port=9222 # macOS',
9098
- "```",
9099
- "",
9100
- "On Windows: `chrome.exe --user-data-dir=%LOCALAPPDATA%\\chrome-debug --remote-debugging-port=9222`.",
9101
- "`curl -s http://127.0.0.1:9222/json/version` answers once it is up. The next",
9102
- "tool call connects; there is nothing to restart.",
9258
+ // src/agents/codex.ts
9259
+ import os5 from "node:os";
9260
+ import path7 from "node:path";
9261
+ var CODEX_BLOCK_START = "# >>> divergent >>>";
9262
+ var CODEX_BLOCK_END = "# <<< divergent <<<";
9263
+ function codexConfigPath() {
9264
+ return path7.join(os5.homedir(), ".codex", "config.toml");
9265
+ }
9266
+ var tomlString = (value) => JSON.stringify(value);
9267
+ function codexProviderToml(options) {
9268
+ const lines = [
9269
+ CODEX_BLOCK_START,
9270
+ "[model_providers.divergent]",
9271
+ 'name = "Divergent"',
9272
+ `base_url = ${tomlString(`${options.openaiBaseUrl}/v1`)}`,
9273
+ 'wire_api = "responses"'
9274
+ ];
9275
+ if (options.projectId) lines.push(`http_headers = { "x-project-id" = ${tomlString(options.projectId)} }`);
9276
+ lines.push(
9103
9277
  "",
9104
- "If no `mcp__chrome-devtools__` tool is listed at all, dsh was started without",
9105
- "the Divergent overlay: `diverge agents run dsh` starts it with one.",
9278
+ "[model_providers.divergent.auth]",
9279
+ `command = ${tomlString(options.self.command)}`,
9280
+ `args = [${[...options.self.args, "token", ...options.organizationId ? ["--organization-id", options.organizationId] : []].map(tomlString).join(", ")}]`,
9281
+ "refresh_interval_ms = 600000",
9282
+ CODEX_BLOCK_END,
9106
9283
  ""
9107
- ].join("\n")
9108
- };
9109
- function bundledSkills(harness) {
9110
- return harness === "dsh" ? [DIVERGE_SKILL, DRIVE_CHROME_SKILL] : [DIVERGE_SKILL];
9284
+ );
9285
+ return lines.join("\n");
9286
+ }
9287
+ function mergeCodexConfig(existing, block) {
9288
+ const text2 = existing ?? "";
9289
+ const start = text2.indexOf(CODEX_BLOCK_START);
9290
+ const endIndex = text2.indexOf(CODEX_BLOCK_END);
9291
+ if (start !== -1 && endIndex !== -1 && endIndex > start) {
9292
+ let end = endIndex + CODEX_BLOCK_END.length;
9293
+ if (text2[end] === "\n") end += 1;
9294
+ return text2.slice(0, start) + block + text2.slice(end);
9295
+ }
9296
+ const separator = !text2 || text2.endsWith("\n") ? "" : "\n";
9297
+ return text2 + separator + (text2 ? "\n" : "") + block;
9111
9298
  }
9112
9299
 
9113
- // src/skills/install.ts
9114
- import fs8 from "node:fs";
9300
+ // src/agents/opencode.ts
9115
9301
  import os6 from "node:os";
9116
- import path10 from "node:path";
9117
- var HARNESSES = ["claude", "codex", "dsh"];
9118
- var SKILLS_MANIFEST = ".diverge-skills.json";
9119
- function skillsRoot(harness, env = process.env) {
9120
- switch (harness) {
9121
- case "claude":
9122
- return path10.join(env.CLAUDE_CONFIG_DIR?.trim() || path10.join(os6.homedir(), ".claude"), "skills");
9123
- case "codex":
9124
- return path10.join(env.CODEX_HOME?.trim() || path10.join(os6.homedir(), ".codex"), "skills");
9125
- case "dsh":
9126
- return path10.join(dshHome(env), "skills");
9302
+ import path8 from "node:path";
9303
+ function opencodeConfigPath() {
9304
+ return path8.join(os6.homedir(), ".config", "opencode", "opencode.json");
9305
+ }
9306
+ function mergeOpencodeConfig(existing, options) {
9307
+ const tokenEnv = options.tokenEnv ?? "DIVERGENT_TOKEN";
9308
+ const config = structuredClone(existing ?? {});
9309
+ config.$schema ??= "https://opencode.ai/config.json";
9310
+ const providers = { ...config.provider ?? {} };
9311
+ const models = Object.fromEntries(options.models.map((model) => [model.id, {
9312
+ name: model.id,
9313
+ attachment: model.input.includes("image"),
9314
+ modalities: { input: model.input, output: ["text"] },
9315
+ ...model.contextWindow && model.maxOutputTokens ? { limit: { context: model.contextWindow, output: model.maxOutputTokens } } : {}
9316
+ }]));
9317
+ const options_ = { baseURL: `${options.baseUrl}/v1`, apiKey: `{env:${tokenEnv}}` };
9318
+ providers[`divergent-${options.surface}`] = options.surface === "openai" ? { npm: "@ai-sdk/openai-compatible", name: "Divergent (OpenAI)", options: options_, models } : { npm: "@ai-sdk/anthropic", name: "Divergent (Anthropic)", options: options_, models };
9319
+ config.provider = providers;
9320
+ return config;
9321
+ }
9322
+
9323
+ // src/agents/run-dsh.ts
9324
+ import { spawn as spawn2 } from "node:child_process";
9325
+ import fs7 from "node:fs";
9326
+ 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;
9127
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];
9128
9367
  }
9129
- function readSkillsManifest(root) {
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;
9130
9372
  try {
9131
- const parsed = JSON.parse(fs8.readFileSync(path10.join(root, SKILLS_MANIFEST), "utf8"));
9132
- if (parsed && typeof parsed === "object" && typeof parsed.skills === "object") return parsed;
9373
+ bin = JSON.parse(fs7.readFileSync(path9.join(packageDir, "package.json"), "utf8")).bin;
9133
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`);
9134
9376
  }
9135
- return null;
9136
- }
9137
- function few(names, limit = 6) {
9138
- return names.length <= limit ? names.join(", ") : `${names.slice(0, limit).join(", ")} and ${names.length - limit} more`;
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] };
9139
9380
  }
9140
- function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Date()) {
9141
- const previous = readSkillsManifest(root);
9142
- const held = previous?.skills ?? {};
9143
- const written = [];
9144
- const added = [];
9145
- const updated = [];
9146
- for (const skill of skills) {
9147
- if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(skill.name)) throw new Error(`skill name ${JSON.stringify(skill.name)} is not a folder name`);
9148
- writeTextAtomic(path10.join(root, skill.name, "SKILL.md"), skill.content, 420);
9149
- written.push(skill.name);
9150
- if (!(skill.name in held)) added.push(skill.name);
9151
- else if (held[skill.name] !== skill.updatedAt) updated.push(skill.name);
9152
- }
9153
- const removed = [];
9154
- for (const name of Object.keys(previous?.skills ?? {})) {
9155
- if (skills.some((skill) => skill.name === name)) continue;
9156
- fs8.rmSync(path10.join(root, name), { recursive: true, force: true });
9157
- removed.push(name);
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);
9392
+ }
9158
9393
  }
9159
- const manifest = {
9160
- installedAt: now().toISOString(),
9161
- source,
9162
- skills: Object.fromEntries(skills.map((skill) => [skill.name, skill.updatedAt]))
9394
+ }
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 = () => {
9163
9423
  };
9164
- writeJsonAtomic(path10.join(root, SKILLS_MANIFEST), manifest, 420);
9165
- return { root, written, added, updated, removed };
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
+ });
9166
9448
  }
9167
- function skillsStatus(root, skills) {
9168
- const manifest = readSkillsManifest(root);
9169
- const held = manifest?.skills ?? {};
9170
- const current = [];
9171
- const behind = [];
9172
- const missing = [];
9173
- for (const skill of skills) {
9174
- if (!(skill.name in held) || !fs8.existsSync(path10.join(root, skill.name, "SKILL.md"))) missing.push(skill.name);
9175
- else if (held[skill.name] !== skill.updatedAt) behind.push(skill.name);
9176
- else current.push(skill.name);
9449
+
9450
+ // src/agents/dsh-session-publisher.ts
9451
+ import fs8 from "node:fs";
9452
+ import path10 from "node:path";
9453
+
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)}--`;
9477
+ }
9478
+ function zstdAvailable() {
9479
+ return typeof zlib.zstdDecompressSync === "function";
9480
+ }
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;
9513
+ }
9514
+ if (checksum) pos += 4;
9515
+ return pos <= buffer.length ? pos : null;
9516
+ }
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) } };
9528
+ }
9529
+ }
9530
+ offset = end;
9177
9531
  }
9178
- const retired = Object.keys(held).filter((name) => !skills.some((skill) => skill.name === name));
9179
- return { root, installedAt: manifest?.installedAt ?? null, current, behind, missing, retired };
9532
+ return { text: Buffer.concat(parts).toString("utf8"), consumed: offset };
9180
9533
  }
9181
-
9182
- // src/skills/installable.ts
9183
- async function fetchInstallableSkills(baseUrl, accessToken) {
9184
- const body = await fetchJson(
9185
- `${baseUrl.replace(/\/+$/, "")}/api/plugins/symphony/skills/installable`,
9186
- { headers: { Authorization: `Bearer ${accessToken}` } }
9187
- );
9188
- if (!body.success || !body.data) throw new CliError(body.error ?? "The platform answered without skills");
9189
- return body.data;
9534
+ function text(value) {
9535
+ return typeof value === "string" && value.length > 0 ? value : null;
9190
9536
  }
9191
-
9192
- // src/commands/skills.ts
9193
- var skillsUsage = `diverge skills <list|install|status> [--harness claude,codex,dsh] [--into <dir>]
9194
-
9195
- list the skills the platform serves you: your organization's skills whose tools you can run
9196
- install write them, and the CLI's own skills ("diverge"; "drive-chrome" for dsh), into each harness's user skill folder
9197
- (~/.claude/skills, ~/.codex/skills, $DSH_HOME/skills); retires the ones the organization retired;
9198
- reports what changed \u2014 the full list is \`skills list\`
9199
- status what each folder holds against what the platform serves now
9200
-
9201
- A skill is a procedure over the tools it names; you get the ones whose every
9202
- tool you may run. Re-run install after the organization changes its skills.`;
9203
- async function installableSkillSet(apiUrl, env) {
9204
- const token = await getUserToken(tokenSource(env, apiUrl));
9205
- const organizationId = tokenClaims(token.access_token).organizationId;
9206
- if (!organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
9207
- const served = await fetchInstallableSkills(apiUrl, token.access_token);
9208
- return { skills: served.skills, organizationId };
9209
- }
9210
- function harnessesOf(flag) {
9211
- if (!flag) return [...HARNESSES];
9212
- const chosen = flag.split(",").map((name) => name.trim()).filter(Boolean);
9213
- for (const name of chosen) if (!HARNESSES.includes(name)) throw new CliError(`--harness must name some of: ${HARNESSES.join(", ")}`);
9214
- return chosen;
9537
+ function instant(value, fallback) {
9538
+ const ms = typeof value === "number" && Number.isFinite(value) ? value : fallback;
9539
+ return new Date(ms).toISOString();
9215
9540
  }
9216
- function installForHarness(io, harness, skills, apiUrl, into) {
9217
- const root = into ?? skillsRoot(harness, io.env);
9218
- const result = installSkills(root, [...bundledSkills(harness), ...skills], apiUrl);
9219
- const changes = [
9220
- ...result.added.length ? [`${result.added.length} new: ${few(result.added)}`] : [],
9221
- ...result.updated.length ? [`${result.updated.length} updated: ${few(result.updated)}`] : [],
9222
- ...result.removed.length ? [`${result.removed.length} retired: ${few(result.removed)}`] : []
9223
- ];
9224
- io.stdout(`${harness}: ${result.written.length} skills in ${root}${changes.length ? ` (${changes.join("; ")})` : " (unchanged)"}`);
9541
+ function parseDshRecords(logText, now = Date.now) {
9542
+ const parsed = { header: null, events: [], unreadable: 0 };
9543
+ for (const line of logText.split("\n")) {
9544
+ if (line.trim().length === 0) continue;
9545
+ let record;
9546
+ try {
9547
+ record = JSON.parse(line);
9548
+ } catch {
9549
+ parsed.unreadable += 1;
9550
+ continue;
9551
+ }
9552
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
9553
+ parsed.unreadable += 1;
9554
+ continue;
9555
+ }
9556
+ const row = record;
9557
+ const type = text(row.type);
9558
+ if (type === "session" && text(row.id)) {
9559
+ parsed.header = {
9560
+ id: row.id,
9561
+ createdAt: typeof row.createdAt === "number" ? row.createdAt : null,
9562
+ cwd: text(row.cwd),
9563
+ parentSession: text(row.parentSession),
9564
+ origin: text(row.origin),
9565
+ delegationDepth: typeof row.delegationDepth === "number" ? row.delegationDepth : null
9566
+ };
9567
+ continue;
9568
+ }
9569
+ if (!type || DSH_DELTA_RECORD_TYPES.has(type)) continue;
9570
+ if (!Number.isInteger(row.seq) || row.seq < 0) {
9571
+ parsed.unreadable += 1;
9572
+ continue;
9573
+ }
9574
+ parsed.events.push({ seq: row.seq, type, time: instant(row.time, now()), data: row.data === void 0 ? {} : row.data });
9575
+ }
9576
+ return parsed;
9225
9577
  }
9226
- async function runSkills(args, io) {
9227
- const [verb, ...rest] = args;
9228
- const { values } = parseArgs2({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
9229
- const apiUrl = resolveApiUrl(void 0, io.env);
9230
- switch (verb) {
9231
- case "list": {
9232
- const { skills, organizationId } = await installableSkillSet(apiUrl, io.env);
9233
- io.stdout(`Skills the platform serves you at organization ${organizationId}, after the CLI's own:`);
9234
- for (const skill of [...bundledSkills("dsh"), ...skills]) io.stdout(` ${skill.name.padEnd(36)} ${skill.tools.length ? skill.tools.join(", ") : "(ships with the CLI)"}`);
9235
- return ExitCode.Ok;
9578
+
9579
+ // src/agents/dsh-session-publisher.ts
9580
+ var DSH_HARNESS = "dsh";
9581
+ var EVENTS_PER_APPEND = 400;
9582
+ var BYTES_PER_APPEND = 15e5;
9583
+ var MAX_PENDING_EVENTS = 2e4;
9584
+ var DEFAULT_POLL_MS = 2e3;
9585
+ var LAUNCH_SLACK_MS = 5e3;
9586
+ var STALLED_TICKS = 30;
9587
+ function eventBytes(event) {
9588
+ return Buffer.byteLength(JSON.stringify(event), "utf8");
9589
+ }
9590
+ function nextBatch(pending) {
9591
+ const batch = [];
9592
+ let bytes = 0;
9593
+ for (const event of pending) {
9594
+ const size = eventBytes(event);
9595
+ if (batch.length > 0 && (batch.length >= EVENTS_PER_APPEND || bytes + size > BYTES_PER_APPEND)) break;
9596
+ batch.push(event);
9597
+ bytes += size;
9598
+ }
9599
+ return batch;
9600
+ }
9601
+ var DshSessionPublisher = class {
9602
+ constructor(deps) {
9603
+ this.deps = deps;
9604
+ this.pollMs = deps.pollMs ?? DEFAULT_POLL_MS;
9605
+ this.now = deps.now ?? Date.now;
9606
+ }
9607
+ deps;
9608
+ sessions = /* @__PURE__ */ new Map();
9609
+ pollMs;
9610
+ now;
9611
+ running = false;
9612
+ loop = null;
9613
+ /** The organization does not record: nothing more is read or sent. */
9614
+ disabled = false;
9615
+ ticking = null;
9616
+ /** The directory dsh writes this launch's sessions under. */
9617
+ get projectDir() {
9618
+ return path10.join(this.deps.sessionsRoot, dshProjectKey(this.deps.cwd));
9619
+ }
9620
+ start() {
9621
+ if (this.running) return;
9622
+ if (!zstdAvailable()) {
9623
+ this.deps.report(`warning: this Node (${process.versions.node}) cannot decode dsh's session log (Zstandard needs Node 22.15 or later); the session is not recorded`);
9624
+ this.disabled = true;
9625
+ return;
9236
9626
  }
9237
- case "install": {
9238
- const { skills } = await installableSkillSet(apiUrl, io.env);
9239
- const harnesses = values.into ? [harnessesOf(values.harness)[0]] : harnessesOf(values.harness);
9240
- for (const harness of harnesses) installForHarness(io, harness, skills, apiUrl, values.into);
9241
- return ExitCode.Ok;
9627
+ this.running = true;
9628
+ this.loop = (async () => {
9629
+ while (this.running) {
9630
+ await this.tick();
9631
+ if (!this.running) break;
9632
+ await this.deps.sleep(this.pollMs);
9633
+ }
9634
+ })();
9635
+ }
9636
+ /** The harness exited: publish what is left and close every session this launch opened. */
9637
+ async stop() {
9638
+ this.running = false;
9639
+ await this.loop;
9640
+ if (this.disabled) return;
9641
+ await this.tick();
9642
+ const at = new Date(this.now()).toISOString();
9643
+ for (const session of this.sessions.values()) {
9644
+ if (!session.opened || !session.header) continue;
9645
+ try {
9646
+ await this.deps.append({ ...this.openInput(session.header), events: [], ended: { at, reason: "exit" } });
9647
+ } catch (error) {
9648
+ this.deps.report(`warning: could not close the record of dsh session ${session.header.id}: ${error instanceof Error ? error.message : String(error)}`);
9649
+ }
9242
9650
  }
9243
- case "status": {
9244
- const { skills } = await installableSkillSet(apiUrl, io.env);
9245
- let behind = false;
9246
- for (const harness of harnessesOf(values.harness)) {
9247
- const status2 = skillsStatus(values.into ?? skillsRoot(harness, io.env), [...bundledSkills(harness), ...skills]);
9248
- const parts = [`${status2.current.length} current`];
9249
- if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
9250
- if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
9251
- if (status2.retired.length) parts.push(`${status2.retired.length} retired (${few(status2.retired)})`);
9252
- behind ||= status2.behind.length + status2.missing.length + status2.retired.length > 0;
9253
- io.stdout(`${harness}: ${parts.join(", ")} in ${status2.root}${status2.installedAt ? ` (installed ${status2.installedAt})` : " (never installed)"}`);
9651
+ }
9652
+ /** One pass: find new sessions, read what each appended, send what is pending. Serialized; a slow platform never overlaps ticks. */
9653
+ async tick() {
9654
+ if (this.disabled) return;
9655
+ if (this.ticking) {
9656
+ await this.ticking;
9657
+ return;
9658
+ }
9659
+ this.ticking = this.pass().finally(() => {
9660
+ this.ticking = null;
9661
+ });
9662
+ await this.ticking;
9663
+ }
9664
+ async pass() {
9665
+ this.discover();
9666
+ for (const session of this.sessions.values()) {
9667
+ if (this.disabled) return;
9668
+ this.read(session);
9669
+ await this.flush(session);
9670
+ }
9671
+ }
9672
+ discover() {
9673
+ let entries;
9674
+ try {
9675
+ entries = fs8.readdirSync(this.projectDir, { withFileTypes: true });
9676
+ } catch {
9677
+ return;
9678
+ }
9679
+ for (const entry of entries) {
9680
+ if (!entry.isDirectory() || this.sessions.has(entry.name)) continue;
9681
+ const dir = path10.join(this.projectDir, entry.name);
9682
+ const file = path10.join(dir, DSH_SESSION_LOG_FILE);
9683
+ let stat;
9684
+ try {
9685
+ stat = fs8.statSync(dir);
9686
+ } catch {
9687
+ continue;
9254
9688
  }
9255
- if (behind) io.stdout("Run: diverge skills install");
9256
- return ExitCode.Ok;
9689
+ const born = Math.min(stat.birthtimeMs || Number.POSITIVE_INFINITY, stat.mtimeMs);
9690
+ if (born < this.deps.launchedAt - LAUNCH_SLACK_MS) continue;
9691
+ this.sessions.set(entry.name, {
9692
+ dir,
9693
+ file,
9694
+ offset: 0,
9695
+ header: null,
9696
+ pending: [],
9697
+ opened: false,
9698
+ lastSeq: -1,
9699
+ reportedFailure: null,
9700
+ unreadableReported: false,
9701
+ stalledAt: 0,
9702
+ stalledTicks: 0,
9703
+ dead: false
9704
+ });
9705
+ }
9706
+ }
9707
+ read(session) {
9708
+ if (session.dead) return;
9709
+ let fd;
9710
+ try {
9711
+ fd = fs8.openSync(session.file, "r");
9712
+ } catch {
9713
+ return;
9714
+ }
9715
+ try {
9716
+ const size = fs8.fstatSync(fd).size;
9717
+ if (size <= session.offset) return;
9718
+ const buffer = Buffer.alloc(size - session.offset);
9719
+ const read = fs8.readSync(fd, buffer, 0, buffer.length, session.offset);
9720
+ const decoded = decodeZstdFrames(buffer.subarray(0, read));
9721
+ if (decoded.failed) {
9722
+ this.giveUp(session, `the frame at byte ${session.offset + decoded.failed.at} does not decode: ${decoded.failed.error}`);
9723
+ } else if (decoded.consumed === 0) {
9724
+ session.stalledTicks = size === session.stalledAt ? session.stalledTicks + 1 : 1;
9725
+ session.stalledAt = size;
9726
+ if (session.stalledTicks >= STALLED_TICKS) this.giveUp(session, `${size - session.offset} byte(s) at byte ${session.offset} complete no frame`);
9727
+ return;
9728
+ }
9729
+ if (decoded.consumed === 0) return;
9730
+ session.stalledTicks = 0;
9731
+ session.offset += decoded.consumed;
9732
+ const parsed = parseDshRecords(decoded.text, this.now);
9733
+ if (parsed.header) session.header = parsed.header;
9734
+ if (parsed.unreadable > 0 && !session.unreadableReported) {
9735
+ session.unreadableReported = true;
9736
+ this.deps.report(`warning: ${parsed.unreadable} line(s) of ${session.file} could not be read as dsh events and were left out`);
9737
+ }
9738
+ for (const event of parsed.events) {
9739
+ if (event.seq > session.lastSeq) session.pending.push(event);
9740
+ }
9741
+ if (session.pending.length > MAX_PENDING_EVENTS) {
9742
+ const dropped = session.pending.length - MAX_PENDING_EVENTS;
9743
+ session.pending.splice(0, dropped);
9744
+ this.deps.report(`warning: the platform has not taken ${session.file} for a while; ${dropped} oldest unsent event(s) were dropped`);
9745
+ }
9746
+ } finally {
9747
+ fs8.closeSync(fd);
9257
9748
  }
9258
- default:
9259
- throw unknownVerb("skills", skillsUsage, [], verb, rest);
9260
9749
  }
9750
+ /** The rest of this session's log cannot be read; what was sent stands, and the person is told once. */
9751
+ giveUp(session, why) {
9752
+ session.dead = true;
9753
+ this.deps.report(`warning: ${session.file} is not read further (${why}); what was recorded so far stands`);
9754
+ }
9755
+ openInput(header) {
9756
+ return {
9757
+ harness: DSH_HARNESS,
9758
+ harnessSessionId: header.id,
9759
+ ...this.deps.harnessVersion ? { harnessVersion: this.deps.harnessVersion } : {},
9760
+ host: this.deps.host,
9761
+ workingDirectory: header.cwd ?? this.deps.cwd,
9762
+ ...header.createdAt !== null ? { startedAt: new Date(header.createdAt).toISOString() } : {},
9763
+ launchId: this.deps.launchId,
9764
+ ...header.parentSession ? { parentHarnessSessionId: header.parentSession } : {},
9765
+ ...header.origin ? { origin: header.origin } : {}
9766
+ };
9767
+ }
9768
+ async flush(session) {
9769
+ if (!session.header) return;
9770
+ while (session.pending.length > 0 && !this.disabled) {
9771
+ const batch = nextBatch(session.pending);
9772
+ let result;
9773
+ try {
9774
+ result = await this.deps.append({ ...this.openInput(session.header), events: batch });
9775
+ } catch (error) {
9776
+ const message = error instanceof Error ? error.message : String(error);
9777
+ if (session.reportedFailure !== message) {
9778
+ session.reportedFailure = message;
9779
+ this.deps.report(`warning: the platform did not take dsh session ${session.header.id}'s events (kept for the next try): ${message}`);
9780
+ }
9781
+ return;
9782
+ }
9783
+ session.reportedFailure = null;
9784
+ if (!result.recording) {
9785
+ this.disabled = true;
9786
+ this.deps.report("This organization does not record agent sessions (Forge installation config agentSessions.recording is off); nothing is sent.");
9787
+ return;
9788
+ }
9789
+ session.opened = true;
9790
+ session.lastSeq = Math.max(session.lastSeq, result.lastSeq, batch[batch.length - 1].seq);
9791
+ session.pending.splice(0, batch.length);
9792
+ }
9793
+ }
9794
+ };
9795
+
9796
+ // src/agents/dsh-session-tools.ts
9797
+ var AGENT_SESSION_APPEND_TOOL = "forge_append_agent_session_events";
9798
+ var AGENT_SESSION_LIST_TOOL = "forge_list_agent_sessions";
9799
+
9800
+ // src/fs/path.ts
9801
+ import fs9 from "node:fs";
9802
+ import path11 from "node:path";
9803
+ function findOnPath(name, env = process.env, platform = process.platform) {
9804
+ const dirs = (env.PATH ?? "").split(path11.delimiter).filter(Boolean);
9805
+ const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
9806
+ const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
9807
+ for (const dir of dirs) {
9808
+ for (const candidate of candidates) {
9809
+ const full = path11.join(dir, candidate);
9810
+ try {
9811
+ fs9.accessSync(full, fs9.constants.X_OK);
9812
+ if (fs9.statSync(full).isFile()) return full;
9813
+ } catch {
9814
+ }
9815
+ }
9816
+ }
9817
+ return null;
9261
9818
  }
9262
9819
 
9263
9820
  // src/agents/rotate.ts
9264
9821
  import os7 from "node:os";
9265
- import path11 from "node:path";
9822
+ import path12 from "node:path";
9266
9823
  function defaultTokenFilePath() {
9267
- return path11.join(os7.homedir(), ".divergent", "token");
9824
+ return path12.join(os7.homedir(), ".divergent", "token");
9268
9825
  }
9269
9826
  var MIN_ROTATE_INTERVAL_SECONDS = 60;
9270
9827
  async function rotateTokenFile(loop) {
@@ -9279,19 +9836,19 @@ async function rotateTokenFile(loop) {
9279
9836
  }
9280
9837
 
9281
9838
  // src/agents/self-command.ts
9282
- import fs9 from "node:fs";
9283
- import path12 from "node:path";
9839
+ import fs10 from "node:fs";
9840
+ import path13 from "node:path";
9284
9841
  function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
9285
9842
  const script = argv[1];
9286
9843
  if (!script) return { command: execPath, args: [] };
9287
9844
  if (platform !== "win32") {
9288
9845
  try {
9289
- fs9.accessSync(script, fs9.constants.X_OK);
9290
- return { command: path12.resolve(script), args: [] };
9846
+ fs10.accessSync(script, fs10.constants.X_OK);
9847
+ return { command: path13.resolve(script), args: [] };
9291
9848
  } catch {
9292
9849
  }
9293
9850
  }
9294
- return { command: execPath, args: [fs9.realpathSync(script)] };
9851
+ return { command: execPath, args: [fs10.realpathSync(script)] };
9295
9852
  }
9296
9853
  function shellCommand(self, ...trailing) {
9297
9854
  return [self.command, ...self.args, ...trailing].map((part) => /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part).join(" ");
@@ -9318,8 +9875,8 @@ function surfaceOf(value, fallback) {
9318
9875
  throw new CliError(`--surface must be one of: ${SURFACES.join(", ")}`);
9319
9876
  }
9320
9877
  function readJson(file) {
9321
- if (!fs10.existsSync(file)) return null;
9322
- return JSON.parse(fs10.readFileSync(file, "utf8"));
9878
+ if (!fs11.existsSync(file)) return null;
9879
+ return JSON.parse(fs11.readFileSync(file, "utf8"));
9323
9880
  }
9324
9881
  async function servedModels(apiUrl, token, surface) {
9325
9882
  return (await listModels(apiUrl, token.access_token, surface)).map(servedModel);
@@ -9336,26 +9893,27 @@ function syncDivergentPreset(io, binary, home) {
9336
9893
  return;
9337
9894
  }
9338
9895
  const dir = writeDivergentPreset(home, shipped);
9339
- const settingsPath = path13.join(home, "settings.yaml");
9340
- const settings = withDivergentPresetDefault(fs10.existsSync(settingsPath) ? fs10.readFileSync(settingsPath, "utf8") : null);
9896
+ const settingsPath = path14.join(home, "settings.yaml");
9897
+ const settings = withDivergentPresetDefault(fs11.existsSync(settingsPath) ? fs11.readFileSync(settingsPath, "utf8") : null);
9341
9898
  if (settings !== null) writeTextAtomic(settingsPath, settings);
9342
9899
  io.stderr(`Agent preset "${DIVERGENT_PRESET_ID}" (${dir}): web sessions search through the platform${settings !== null ? "; set as the default preset in settings.yaml" : ""}.`);
9343
9900
  }
9344
- async function skillsFor(io, apiUrl, harness) {
9901
+ async function skillsFor(io, apiUrl, harness, organizationId) {
9345
9902
  try {
9346
- const { skills } = await installableSkillSet(apiUrl, io.env);
9903
+ const skills = await loginSkillSet(apiUrl, io.env, organizationId?.trim() || void 0);
9347
9904
  installForHarness(io, harness, skills, apiUrl);
9905
+ for (const note of skills.notes) io.stderr(`warning: ${note}`);
9348
9906
  } catch (error) {
9349
9907
  io.stderr(`warning: skills not installed (${error instanceof Error ? error.message : String(error)}); run: diverge skills install`);
9350
9908
  }
9351
9909
  }
9352
- async function overlayAndDefault(apiUrl, token, surface, io, requested) {
9910
+ async function overlayAndDefault(apiUrl, token, surface, io, requested, launch) {
9353
9911
  const served = await servedModels(apiUrl, token, surface);
9354
9912
  const models = served.map((model) => model.id);
9355
9913
  if (models.length === 0) throw new CliError(`the gateway serves no models on the ${surface} surface for this organization`);
9356
9914
  if (requested && !models.includes(requested)) throw new CliError(`${requested} is not served on the ${surface} surface; choose one of: ${models.join(", ")}`);
9357
9915
  const defaultModel = dshDefaultModel(models, requested);
9358
- return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel }), defaultModel, models };
9916
+ return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models: served, defaultModel, launch }), defaultModel, models };
9359
9917
  }
9360
9918
  function organizationArgs(organizationId) {
9361
9919
  const trimmed = organizationId?.trim();
@@ -9374,12 +9932,14 @@ async function setup(args, io) {
9374
9932
  baseUrl: anthropicBaseUrl(apiUrl),
9375
9933
  tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
9376
9934
  projectId: values["project-id"],
9377
- modelDiscovery: values["model-discovery"]
9935
+ modelDiscovery: values["model-discovery"],
9936
+ // Claude Code scans its skill folder when a session starts, so that is when the login's skills are brought current.
9937
+ skillsRefresh: values.scope === "user" ? shellCommand(resolveSelfCommand(), "skills", "refresh") : void 0
9378
9938
  });
9379
9939
  writeJsonAtomic(file, settings);
9380
9940
  io.stdout(`Claude Code \u2192 ${anthropicBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
9381
9941
  for (const warning of warnings) io.stderr(`warning: ${warning}`);
9382
- await skillsFor(io, apiUrl, "claude");
9942
+ await skillsFor(io, apiUrl, "claude", values["organization-id"]);
9383
9943
  return ExitCode.Ok;
9384
9944
  }
9385
9945
  case "opencode": {
@@ -9409,24 +9969,24 @@ async function setup(args, io) {
9409
9969
  }
9410
9970
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9411
9971
  const file = values.config ?? codexConfigPath();
9412
- const existing = fs10.existsSync(file) ? fs10.readFileSync(file, "utf8") : null;
9972
+ const existing = fs11.existsSync(file) ? fs11.readFileSync(file, "utf8") : null;
9413
9973
  writeTextAtomic(file, mergeCodexConfig(existing, block));
9414
9974
  io.stdout(`Codex provider divergent \u2192 ${openaiBaseUrl(apiUrl)} as you, organization ${tokenClaims(token.access_token).organizationId}. Wrote ${file}.`);
9415
9975
  io.stdout('Select it per run with: codex -c model_provider=divergent (or set model_provider = "divergent" in the config)');
9416
- await skillsFor(io, apiUrl, "codex");
9976
+ await skillsFor(io, apiUrl, "codex", values["organization-id"]);
9417
9977
  return ExitCode.Ok;
9418
9978
  }
9419
9979
  case "dsh": {
9420
9980
  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 });
9421
9981
  const surface = surfaceOf(values.surface, "openai");
9422
9982
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9423
- if (!values.print) await skillsFor(io, apiUrl, "dsh");
9983
+ if (!values.print) await skillsFor(io, apiUrl, "dsh", values["organization-id"]);
9424
9984
  const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
9425
9985
  if (values.print) {
9426
9986
  io.stdout(overlay.trimEnd());
9427
9987
  return ExitCode.Ok;
9428
9988
  }
9429
- const file = values.into ?? path13.join(dshHome(io.env), `divergent-${surface}.yml`);
9989
+ const file = values.into ?? path14.join(dshHome(io.env), `divergent-${surface}.yml`);
9430
9990
  writeTextAtomic(file, overlay);
9431
9991
  io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
9432
9992
  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.");
@@ -9452,43 +10012,72 @@ async function run(args, io) {
9452
10012
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
9453
10013
  const binary = findOnPath("dsh", io.env);
9454
10014
  if (!binary) throw new CliError("dsh is not on PATH (install: npm install -g @deepseek-ai/dsh)");
9455
- await skillsFor(io, apiUrl, "dsh");
9456
- const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
10015
+ await skillsFor(io, apiUrl, "dsh", values["organization-id"]);
10016
+ const launch = { id: randomUUID(), client: `dsh/${dshVersionOf(binary) ?? "unknown"}` };
10017
+ const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model, launch);
9457
10018
  const home = dshHome(io.env);
9458
- const settingsPath = path13.join(home, "settings.yaml");
10019
+ const settingsPath = path14.join(home, "settings.yaml");
9459
10020
  let pinned;
9460
10021
  try {
9461
- pinned = dshSettingsDefaultModel(fs10.existsSync(settingsPath) ? fs10.readFileSync(settingsPath, "utf8") : null, surface);
10022
+ pinned = dshSettingsDefaultModel(fs11.existsSync(settingsPath) ? fs11.readFileSync(settingsPath, "utf8") : null, surface);
9462
10023
  } catch (error) {
9463
10024
  throw new CliError(`${settingsPath}: ${error instanceof Error ? error.message : String(error)}`);
9464
10025
  }
9465
10026
  const served = servedIds(overlay);
9466
10027
  if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
9467
- const overlayPath = path13.join(home, `divergent-${surface}.yml`);
10028
+ const overlayPath = path14.join(home, `divergent-${surface}.yml`);
9468
10029
  writeTextAtomic(overlayPath, overlay);
9469
10030
  syncDivergentPreset(io, binary, home);
9470
10031
  io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
9471
10032
  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>"');
9472
10033
  const source = tokenSource(io.env, apiUrl);
9473
- return runDsh({
9474
- binary,
9475
- overlayPath,
9476
- credentialsPath: path13.join(home, ".credentials.yaml"),
9477
- sessionTokenPath: path13.join(home, "session-token"),
9478
- binDir: path13.join(home, "bin"),
9479
- cli: { command: process.execPath, args: [path13.resolve(process.argv[1])] },
9480
- args: passthrough,
9481
- env: io.env,
9482
- initialToken: token.access_token,
9483
- freshToken: async () => {
9484
- try {
9485
- return (await getUserToken(source, { freshForSeconds: 120, organizationId: values["organization-id"] })).access_token;
9486
- } catch (error) {
9487
- io.stderr(`warning: could not refresh the platform token: ${error instanceof Error ? error.message : String(error)}`);
9488
- return null;
9489
- }
9490
- }
10034
+ const organizationId = tokenClaims(token.access_token).organizationId ?? values["organization-id"];
10035
+ if (!organizationId) throw new CliError("the gateway token names no organization; pass --organization-id");
10036
+ io.stderr(`This session is recorded as organization ${organizationId}'s own record \u2014 the conversation, every tool call and its result \u2014 unless the organization turned recording off; its administrators set whether and for how long. Yours to read: diverge tools call ${AGENT_SESSION_LIST_TOOL} (ADR 0121).`);
10037
+ const tools = new ToolsClient({ apiUrl, organizationId, mode: "operator", clientId: clientId(io.env) }, io.env);
10038
+ const publisher = new DshSessionPublisher({
10039
+ sessionsRoot: path14.join(home, "sessions"),
10040
+ cwd: process.cwd(),
10041
+ launchedAt: Date.now(),
10042
+ launchId: launch.id,
10043
+ harnessVersion: dshVersionOf(binary),
10044
+ host: os8.hostname(),
10045
+ append: (input) => appendSession(tools, input),
10046
+ report: (line) => io.stderr(line),
10047
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
9491
10048
  });
10049
+ publisher.start();
10050
+ try {
10051
+ return await runDsh({
10052
+ binary,
10053
+ overlayPath,
10054
+ credentialsPath: path14.join(home, ".credentials.yaml"),
10055
+ sessionTokenPath: path14.join(home, "session-token"),
10056
+ binDir: path14.join(home, "bin"),
10057
+ cli: { command: process.execPath, args: [path14.resolve(process.argv[1])] },
10058
+ args: passthrough,
10059
+ env: io.env,
10060
+ initialToken: token.access_token,
10061
+ freshToken: async () => {
10062
+ try {
10063
+ return (await getUserToken(source, { freshForSeconds: 120, organizationId: values["organization-id"] })).access_token;
10064
+ } catch (error) {
10065
+ io.stderr(`warning: could not refresh the platform token: ${error instanceof Error ? error.message : String(error)}`);
10066
+ return null;
10067
+ }
10068
+ }
10069
+ });
10070
+ } finally {
10071
+ await publisher.stop();
10072
+ }
10073
+ }
10074
+ async function appendSession(tools, input) {
10075
+ const response = await tools.callTool(AGENT_SESSION_APPEND_TOOL, input);
10076
+ if (response.success === false || response.error) throw new CliError(response.error ?? "the platform refused the append");
10077
+ if (isPending(response.result)) throw new CliError("the append was raised for a decision, which an inert record never needs");
10078
+ const result = response.result;
10079
+ if (!result || typeof result.recording !== "boolean") throw new CliError("the platform answered the append without a result");
10080
+ return { recording: result.recording, sessionId: result.sessionId ?? null, stored: result.stored ?? 0, skipped: result.skipped ?? 0, lastSeq: typeof result.lastSeq === "number" ? result.lastSeq : -1 };
9492
10081
  }
9493
10082
  async function status(args, io) {
9494
10083
  const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
@@ -9599,7 +10188,7 @@ async function runLogout(_args, io) {
9599
10188
  }
9600
10189
 
9601
10190
  // src/commands/status.ts
9602
- import path14 from "node:path";
10191
+ import path15 from "node:path";
9603
10192
  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.";
9604
10193
  async function runStatus(_args, io) {
9605
10194
  const apiUrl = resolveApiUrl(void 0, io.env);
@@ -9615,7 +10204,7 @@ async function runStatus(_args, io) {
9615
10204
  io.stdout(describeToolExecution(io.env));
9616
10205
  return 0;
9617
10206
  }
9618
- io.stdout(`State: ${path14.dirname(source.loginFile)}`);
10207
+ io.stdout(`State: ${path15.dirname(source.loginFile)}`);
9619
10208
  const record = loadLogin(source);
9620
10209
  if (!record) {
9621
10210
  io.stdout(`Not logged in. Run: diverge login${apiUrl === DEFAULT_API_URL ? "" : ` --api-url ${apiUrl}`}`);
@@ -9751,14 +10340,14 @@ function pendingResult(result) {
9751
10340
  summary: result.invocation?.preview?.summary
9752
10341
  };
9753
10342
  const who = result.decision?.outcome === "second_principal" ? "another person (a holder of the decide permission, never the proposer)" : "the operator";
9754
- const text = [
10343
+ const text2 = [
9755
10344
  "Pending human approval \u2014 nothing has executed.",
9756
10345
  `invocationId: ${structured.invocationId}`,
9757
10346
  `decision: ${result.decision?.outcome ?? "pending"}`,
9758
10347
  `Ask ${who} to decide it at: ${structured.approvalUrl}`,
9759
10348
  `Then read the outcome with ${INVOCATION_STATUS_TOOL}, no sooner than ${structured.pollAfterMs} ms between reads; expires ${structured.expiresAt}.`
9760
10349
  ].join("\n");
9761
- return { content: textContent(text), structuredContent: structured, _meta: { "net.divergeai/invocationId": structured.invocationId } };
10350
+ return { content: textContent(text2), structuredContent: structured, _meta: { "net.divergeai/invocationId": structured.invocationId } };
9762
10351
  }
9763
10352
  var BridgeServer = class {
9764
10353
  constructor(client3, options = {}) {
@@ -9907,30 +10496,30 @@ async function waitForInvocation(id, options, deps) {
9907
10496
  }
9908
10497
 
9909
10498
  // src/tools/call-args.ts
9910
- import fs11 from "node:fs";
10499
+ import fs12 from "node:fs";
9911
10500
  async function resolveCallArgs(options, stdin = readStdin) {
9912
10501
  if (options.argsJson !== void 0 && options.argsJson !== "{}" && options.argsFile !== void 0) {
9913
10502
  throw new CliError("give the arguments once: --args-json or --args-file");
9914
10503
  }
9915
- let text;
10504
+ let text2;
9916
10505
  let source;
9917
10506
  if (options.argsFile !== void 0) {
9918
10507
  source = `--args-file ${options.argsFile}`;
9919
10508
  try {
9920
- text = fs11.readFileSync(options.argsFile, "utf8");
10509
+ text2 = fs12.readFileSync(options.argsFile, "utf8");
9921
10510
  } catch (error) {
9922
10511
  throw new CliError(`${source}: ${error instanceof Error ? error.message : String(error)}`);
9923
10512
  }
9924
10513
  } else if (options.argsJson === "-") {
9925
10514
  source = "--args-json - (stdin)";
9926
- text = await stdin();
10515
+ text2 = await stdin();
9927
10516
  } else {
9928
10517
  source = "--args-json";
9929
- text = options.argsJson ?? "{}";
10518
+ text2 = options.argsJson ?? "{}";
9930
10519
  }
9931
10520
  let parsed;
9932
10521
  try {
9933
- parsed = JSON.parse(text.trim() === "" ? "{}" : text);
10522
+ parsed = JSON.parse(text2.trim() === "" ? "{}" : text2);
9934
10523
  } catch (error) {
9935
10524
  throw new CliError(`${source} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
9936
10525
  }
@@ -9944,8 +10533,8 @@ async function readStdin() {
9944
10533
  }
9945
10534
 
9946
10535
  // src/tools/batch.ts
9947
- function parseBatch(text) {
9948
- const trimmed = text.trim();
10536
+ function parseBatch(text2) {
10537
+ const trimmed = text2.trim();
9949
10538
  if (!trimmed) throw new Error("the batch file is empty");
9950
10539
  const raw = trimmed.startsWith("[") ? JSON.parse(trimmed) : trimmed.split("\n").filter((line) => line.trim()).map((line) => JSON.parse(line));
9951
10540
  if (!Array.isArray(raw)) throw new Error("the batch file must hold a JSON array or one JSON object per line");
@@ -10009,7 +10598,7 @@ function worstExitCode(outcomes) {
10009
10598
  }
10010
10599
 
10011
10600
  // src/commands/tools.ts
10012
- import fs12 from "node:fs";
10601
+ import fs13 from "node:fs";
10013
10602
 
10014
10603
  // src/tools/print.ts
10015
10604
  function matchesCapability(selector, capability) {
@@ -10166,7 +10755,7 @@ async function set(rest, io) {
10166
10755
  if (!file) throw new CliError("set needs a file of calls, or - for stdin");
10167
10756
  let entries;
10168
10757
  try {
10169
- entries = parseBatch(file === "-" ? await readStdin() : fs12.readFileSync(file, "utf8"));
10758
+ entries = parseBatch(file === "-" ? await readStdin() : fs13.readFileSync(file, "utf8"));
10170
10759
  } catch (error) {
10171
10760
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
10172
10761
  }
@@ -10253,7 +10842,7 @@ async function runTools(args, io) {
10253
10842
  if (!file) throw new CliError("run needs a file of calls, or - for stdin");
10254
10843
  let entries;
10255
10844
  try {
10256
- entries = parseBatch(file === "-" ? await readStdin() : fs12.readFileSync(file, "utf8"));
10845
+ entries = parseBatch(file === "-" ? await readStdin() : fs13.readFileSync(file, "utf8"));
10257
10846
  } catch (error) {
10258
10847
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
10259
10848
  }
@@ -10297,7 +10886,7 @@ async function runTools(args, io) {
10297
10886
  }
10298
10887
 
10299
10888
  // src/commands/plugins.ts
10300
- import fs13 from "node:fs";
10889
+ import fs14 from "node:fs";
10301
10890
  import { parseArgs as parseArgs7 } from "node:util";
10302
10891
  var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
10303
10892
 
@@ -10322,15 +10911,15 @@ function readManifest(file) {
10322
10911
  if (!file) throw new CliError(`A manifest file is required.
10323
10912
 
10324
10913
  ${pluginsUsage}`);
10325
- let text;
10914
+ let text2;
10326
10915
  try {
10327
- text = fs13.readFileSync(file, "utf8");
10916
+ text2 = fs14.readFileSync(file, "utf8");
10328
10917
  } catch (error) {
10329
10918
  throw new CliError(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
10330
10919
  }
10331
10920
  let parsed;
10332
10921
  try {
10333
- parsed = JSON.parse(text);
10922
+ parsed = JSON.parse(text2);
10334
10923
  } catch (error) {
10335
10924
  throw new CliError(`${file} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
10336
10925
  }
@@ -10412,20 +11001,20 @@ ${pluginsUsage}`);
10412
11001
 
10413
11002
  // src/commands/update.ts
10414
11003
  import { spawn as spawn4 } from "node:child_process";
10415
- import path16 from "node:path";
11004
+ import path17 from "node:path";
10416
11005
 
10417
11006
  // src/update-notice.ts
10418
11007
  import { spawn as spawn3 } from "node:child_process";
10419
- import fs14 from "node:fs";
10420
- import path15 from "node:path";
11008
+ import fs15 from "node:fs";
11009
+ import path16 from "node:path";
10421
11010
  var UPDATE_CHECK_FILE = "update-check.json";
10422
11011
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
10423
11012
  function updateCheckFile(env) {
10424
- return path15.join(configDir(env), UPDATE_CHECK_FILE);
11013
+ return path16.join(configDir(env), UPDATE_CHECK_FILE);
10425
11014
  }
10426
11015
  function readUpdateCheck(env) {
10427
11016
  try {
10428
- const parsed = JSON.parse(fs14.readFileSync(updateCheckFile(env), "utf8"));
11017
+ const parsed = JSON.parse(fs15.readFileSync(updateCheckFile(env), "utf8"));
10429
11018
  if (parsed && typeof parsed === "object" && typeof parsed.checkedAt === "number" && typeof parsed.latest === "string") return parsed;
10430
11019
  } catch {
10431
11020
  }
@@ -10517,7 +11106,7 @@ function installLatest(version, env = process.env, platform = process.platform)
10517
11106
  command = npm;
10518
11107
  if (/\.(cmd|bat)$/i.test(npm)) {
10519
11108
  command = process.execPath;
10520
- args.unshift(path16.join(path16.dirname(npm), "node_modules", "npm", "bin", "npm-cli.js"));
11109
+ args.unshift(path17.join(path17.dirname(npm), "node_modules", "npm", "bin", "npm-cli.js"));
10521
11110
  }
10522
11111
  }
10523
11112
  return new Promise((resolve, reject) => {
@@ -10537,7 +11126,7 @@ var COMMANDS = [
10537
11126
  { name: "agents", summary: "Point a coding agent at the model gateway (setup, run, status)", usage: agentsUsage, run: runAgents },
10538
11127
  { name: "tools", summary: "Discover and execute platform tools; raise and read approvals", usage: toolsUsage, run: runTools },
10539
11128
  { name: "plugins", summary: "Validate, publish, list and retire tenant plugins for your organization", usage: pluginsUsage, run: runPlugins },
10540
- { name: "skills", summary: "Install your organization's skills into Claude Code, Codex and dsh", usage: skillsUsage, run: runSkills },
11129
+ { name: "skills", summary: "Install the skills of every organization you can act in into Claude Code, Codex and dsh", usage: skillsUsage, run: runSkills },
10541
11130
  { name: "update", summary: "Install the latest release", usage: updateUsage, run: (args, io) => runUpdate(args, io) }
10542
11131
  ];
10543
11132
  function usage() {
@@ -10575,9 +11164,9 @@ ${usage()}`);
10575
11164
  }
10576
11165
  const own = rest.includes("--") ? rest.slice(0, rest.indexOf("--")) : rest;
10577
11166
  if (own.includes("--help") || own.includes("-h")) {
10578
- const path17 = verbPath(command.usage, own);
10579
- if (path17.length > 0 || own[0].startsWith("-")) {
10580
- io.stdout(verbUsage(command.name, command.usage, path17));
11167
+ const path18 = verbPath(command.usage, own);
11168
+ if (path18.length > 0 || own[0].startsWith("-")) {
11169
+ io.stdout(verbUsage(command.name, command.usage, path18));
10581
11170
  return ExitCode.Ok;
10582
11171
  }
10583
11172
  }