@divergeai/cli 0.1.27 → 0.1.28

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