@divergeai/cli 0.1.13 → 0.1.22

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 +39 -19
  2. package/dist/diverge.mjs +740 -585
  3. package/package.json +1 -1
package/dist/diverge.mjs CHANGED
@@ -114,17 +114,17 @@ var require_visit = __commonJS({
114
114
  visit.BREAK = BREAK;
115
115
  visit.SKIP = SKIP;
116
116
  visit.REMOVE = REMOVE;
117
- function visit_(key, node, visitor, path13) {
118
- const ctrl = callVisitor(key, node, visitor, path13);
117
+ function visit_(key, node, visitor, path15) {
118
+ const ctrl = callVisitor(key, node, visitor, path15);
119
119
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
120
- replaceNode(key, path13, ctrl);
121
- return visit_(key, ctrl, visitor, path13);
120
+ replaceNode(key, path15, ctrl);
121
+ return visit_(key, ctrl, visitor, path15);
122
122
  }
123
123
  if (typeof ctrl !== "symbol") {
124
124
  if (identity.isCollection(node)) {
125
- path13 = Object.freeze(path13.concat(node));
125
+ path15 = Object.freeze(path15.concat(node));
126
126
  for (let i = 0; i < node.items.length; ++i) {
127
- const ci = visit_(i, node.items[i], visitor, path13);
127
+ const ci = visit_(i, node.items[i], visitor, path15);
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
- path13 = Object.freeze(path13.concat(node));
139
- const ck = visit_("key", node.key, visitor, path13);
138
+ path15 = Object.freeze(path15.concat(node));
139
+ const ck = visit_("key", node.key, visitor, path15);
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, path13);
144
+ const cv = visit_("value", node.value, visitor, path15);
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, path13) {
166
- const ctrl = await callVisitor(key, node, visitor, path13);
165
+ async function visitAsync_(key, node, visitor, path15) {
166
+ const ctrl = await callVisitor(key, node, visitor, path15);
167
167
  if (identity.isNode(ctrl) || identity.isPair(ctrl)) {
168
- replaceNode(key, path13, ctrl);
169
- return visitAsync_(key, ctrl, visitor, path13);
168
+ replaceNode(key, path15, ctrl);
169
+ return visitAsync_(key, ctrl, visitor, path15);
170
170
  }
171
171
  if (typeof ctrl !== "symbol") {
172
172
  if (identity.isCollection(node)) {
173
- path13 = Object.freeze(path13.concat(node));
173
+ path15 = Object.freeze(path15.concat(node));
174
174
  for (let i = 0; i < node.items.length; ++i) {
175
- const ci = await visitAsync_(i, node.items[i], visitor, path13);
175
+ const ci = await visitAsync_(i, node.items[i], visitor, path15);
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
- path13 = Object.freeze(path13.concat(node));
187
- const ck = await visitAsync_("key", node.key, visitor, path13);
186
+ path15 = Object.freeze(path15.concat(node));
187
+ const ck = await visitAsync_("key", node.key, visitor, path15);
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, path13);
192
+ const cv = await visitAsync_("value", node.value, visitor, path15);
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, path13) {
219
+ function callVisitor(key, node, visitor, path15) {
220
220
  if (typeof visitor === "function")
221
- return visitor(key, node, path13);
221
+ return visitor(key, node, path15);
222
222
  if (identity.isMap(node))
223
- return visitor.Map?.(key, node, path13);
223
+ return visitor.Map?.(key, node, path15);
224
224
  if (identity.isSeq(node))
225
- return visitor.Seq?.(key, node, path13);
225
+ return visitor.Seq?.(key, node, path15);
226
226
  if (identity.isPair(node))
227
- return visitor.Pair?.(key, node, path13);
227
+ return visitor.Pair?.(key, node, path15);
228
228
  if (identity.isScalar(node))
229
- return visitor.Scalar?.(key, node, path13);
229
+ return visitor.Scalar?.(key, node, path15);
230
230
  if (identity.isAlias(node))
231
- return visitor.Alias?.(key, node, path13);
231
+ return visitor.Alias?.(key, node, path15);
232
232
  return void 0;
233
233
  }
234
- function replaceNode(key, path13, node) {
235
- const parent = path13[path13.length - 1];
234
+ function replaceNode(key, path15, node) {
235
+ const parent = path15[path15.length - 1];
236
236
  if (identity.isCollection(parent)) {
237
237
  parent.items[key] = node;
238
238
  } else if (identity.isPair(parent)) {
@@ -842,10 +842,10 @@ var require_Collection = __commonJS({
842
842
  var createNode = require_createNode();
843
843
  var identity = require_identity();
844
844
  var Node = require_Node();
845
- function collectionFromPath(schema, path13, value) {
845
+ function collectionFromPath(schema, path15, value) {
846
846
  let v = value;
847
- for (let i = path13.length - 1; i >= 0; --i) {
848
- const k = path13[i];
847
+ for (let i = path15.length - 1; i >= 0; --i) {
848
+ const k = path15[i];
849
849
  if (typeof k === "number" && Number.isInteger(k) && k >= 0) {
850
850
  const a = [];
851
851
  a[k] = v;
@@ -864,7 +864,7 @@ var require_Collection = __commonJS({
864
864
  sourceObjects: /* @__PURE__ */ new Map()
865
865
  });
866
866
  }
867
- var isEmptyPath = (path13) => path13 == null || typeof path13 === "object" && !!path13[Symbol.iterator]().next().done;
867
+ var isEmptyPath = (path15) => path15 == null || typeof path15 === "object" && !!path15[Symbol.iterator]().next().done;
868
868
  var Collection = class extends Node.NodeBase {
869
869
  constructor(type, schema) {
870
870
  super(type);
@@ -894,11 +894,11 @@ var require_Collection = __commonJS({
894
894
  * be a Pair instance or a `{ key, value }` object, which may not have a key
895
895
  * that already exists in the map.
896
896
  */
897
- addIn(path13, value) {
898
- if (isEmptyPath(path13))
897
+ addIn(path15, value) {
898
+ if (isEmptyPath(path15))
899
899
  this.add(value);
900
900
  else {
901
- const [key, ...rest] = path13;
901
+ const [key, ...rest] = path15;
902
902
  const node = this.get(key, true);
903
903
  if (identity.isCollection(node))
904
904
  node.addIn(rest, value);
@@ -912,8 +912,8 @@ var require_Collection = __commonJS({
912
912
  * Removes a value from the collection.
913
913
  * @returns `true` if the item was found and removed.
914
914
  */
915
- deleteIn(path13) {
916
- const [key, ...rest] = path13;
915
+ deleteIn(path15) {
916
+ const [key, ...rest] = path15;
917
917
  if (rest.length === 0)
918
918
  return this.delete(key);
919
919
  const node = this.get(key, true);
@@ -927,8 +927,8 @@ var require_Collection = __commonJS({
927
927
  * scalar values from their surrounding node; to disable set `keepScalar` to
928
928
  * `true` (collections are always returned intact).
929
929
  */
930
- getIn(path13, keepScalar) {
931
- const [key, ...rest] = path13;
930
+ getIn(path15, keepScalar) {
931
+ const [key, ...rest] = path15;
932
932
  const node = this.get(key, true);
933
933
  if (rest.length === 0)
934
934
  return !keepScalar && identity.isScalar(node) ? node.value : node;
@@ -946,8 +946,8 @@ var require_Collection = __commonJS({
946
946
  /**
947
947
  * Checks if the collection includes a value with the key `key`.
948
948
  */
949
- hasIn(path13) {
950
- const [key, ...rest] = path13;
949
+ hasIn(path15) {
950
+ const [key, ...rest] = path15;
951
951
  if (rest.length === 0)
952
952
  return this.has(key);
953
953
  const node = this.get(key, true);
@@ -957,8 +957,8 @@ var require_Collection = __commonJS({
957
957
  * Sets a value in this collection. For `!!set`, `value` needs to be a
958
958
  * boolean to add/remove the item from the set.
959
959
  */
960
- setIn(path13, value) {
961
- const [key, ...rest] = path13;
960
+ setIn(path15, value) {
961
+ const [key, ...rest] = path15;
962
962
  if (rest.length === 0) {
963
963
  this.set(key, value);
964
964
  } else {
@@ -3473,9 +3473,9 @@ var require_Document = __commonJS({
3473
3473
  this.contents.add(value);
3474
3474
  }
3475
3475
  /** Adds a value to the document. */
3476
- addIn(path13, value) {
3476
+ addIn(path15, value) {
3477
3477
  if (assertCollection(this.contents))
3478
- this.contents.addIn(path13, value);
3478
+ this.contents.addIn(path15, value);
3479
3479
  }
3480
3480
  /**
3481
3481
  * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
@@ -3550,14 +3550,14 @@ var require_Document = __commonJS({
3550
3550
  * Removes a value from the document.
3551
3551
  * @returns `true` if the item was found and removed.
3552
3552
  */
3553
- deleteIn(path13) {
3554
- if (Collection.isEmptyPath(path13)) {
3553
+ deleteIn(path15) {
3554
+ if (Collection.isEmptyPath(path15)) {
3555
3555
  if (this.contents == null)
3556
3556
  return false;
3557
3557
  this.contents = null;
3558
3558
  return true;
3559
3559
  }
3560
- return assertCollection(this.contents) ? this.contents.deleteIn(path13) : false;
3560
+ return assertCollection(this.contents) ? this.contents.deleteIn(path15) : false;
3561
3561
  }
3562
3562
  /**
3563
3563
  * Returns item at `key`, or `undefined` if not found. By default unwraps
@@ -3572,10 +3572,10 @@ var require_Document = __commonJS({
3572
3572
  * scalar values from their surrounding node; to disable set `keepScalar` to
3573
3573
  * `true` (collections are always returned intact).
3574
3574
  */
3575
- getIn(path13, keepScalar) {
3576
- if (Collection.isEmptyPath(path13))
3575
+ getIn(path15, keepScalar) {
3576
+ if (Collection.isEmptyPath(path15))
3577
3577
  return !keepScalar && identity.isScalar(this.contents) ? this.contents.value : this.contents;
3578
- return identity.isCollection(this.contents) ? this.contents.getIn(path13, keepScalar) : void 0;
3578
+ return identity.isCollection(this.contents) ? this.contents.getIn(path15, keepScalar) : void 0;
3579
3579
  }
3580
3580
  /**
3581
3581
  * Checks if the document includes a value with the key `key`.
@@ -3586,10 +3586,10 @@ var require_Document = __commonJS({
3586
3586
  /**
3587
3587
  * Checks if the document includes a value at `path`.
3588
3588
  */
3589
- hasIn(path13) {
3590
- if (Collection.isEmptyPath(path13))
3589
+ hasIn(path15) {
3590
+ if (Collection.isEmptyPath(path15))
3591
3591
  return this.contents !== void 0;
3592
- return identity.isCollection(this.contents) ? this.contents.hasIn(path13) : false;
3592
+ return identity.isCollection(this.contents) ? this.contents.hasIn(path15) : false;
3593
3593
  }
3594
3594
  /**
3595
3595
  * Sets a value in this document. For `!!set`, `value` needs to be a
@@ -3606,13 +3606,13 @@ var require_Document = __commonJS({
3606
3606
  * Sets a value in this document. For `!!set`, `value` needs to be a
3607
3607
  * boolean to add/remove the item from the set.
3608
3608
  */
3609
- setIn(path13, value) {
3610
- if (Collection.isEmptyPath(path13)) {
3609
+ setIn(path15, value) {
3610
+ if (Collection.isEmptyPath(path15)) {
3611
3611
  this.contents = value;
3612
3612
  } else if (this.contents == null) {
3613
- this.contents = Collection.collectionFromPath(this.schema, Array.from(path13), value);
3613
+ this.contents = Collection.collectionFromPath(this.schema, Array.from(path15), value);
3614
3614
  } else if (assertCollection(this.contents)) {
3615
- this.contents.setIn(path13, value);
3615
+ this.contents.setIn(path15, value);
3616
3616
  }
3617
3617
  }
3618
3618
  /**
@@ -5572,9 +5572,9 @@ var require_cst_visit = __commonJS({
5572
5572
  visit.BREAK = BREAK;
5573
5573
  visit.SKIP = SKIP;
5574
5574
  visit.REMOVE = REMOVE;
5575
- visit.itemAtPath = (cst, path13) => {
5575
+ visit.itemAtPath = (cst, path15) => {
5576
5576
  let item = cst;
5577
- for (const [field, index] of path13) {
5577
+ for (const [field, index] of path15) {
5578
5578
  const tok = item?.[field];
5579
5579
  if (tok && "items" in tok) {
5580
5580
  item = tok.items[index];
@@ -5583,23 +5583,23 @@ var require_cst_visit = __commonJS({
5583
5583
  }
5584
5584
  return item;
5585
5585
  };
5586
- visit.parentCollection = (cst, path13) => {
5587
- const parent = visit.itemAtPath(cst, path13.slice(0, -1));
5588
- const field = path13[path13.length - 1][0];
5586
+ visit.parentCollection = (cst, path15) => {
5587
+ const parent = visit.itemAtPath(cst, path15.slice(0, -1));
5588
+ const field = path15[path15.length - 1][0];
5589
5589
  const coll = parent?.[field];
5590
5590
  if (coll && "items" in coll)
5591
5591
  return coll;
5592
5592
  throw new Error("Parent collection not found");
5593
5593
  };
5594
- function _visit(path13, item, visitor) {
5595
- let ctrl = visitor(item, path13);
5594
+ function _visit(path15, item, visitor) {
5595
+ let ctrl = visitor(item, path15);
5596
5596
  if (typeof ctrl === "symbol")
5597
5597
  return ctrl;
5598
5598
  for (const field of ["key", "value"]) {
5599
5599
  const token = item[field];
5600
5600
  if (token && "items" in token) {
5601
5601
  for (let i = 0; i < token.items.length; ++i) {
5602
- const ci = _visit(Object.freeze(path13.concat([[field, i]])), token.items[i], visitor);
5602
+ const ci = _visit(Object.freeze(path15.concat([[field, i]])), token.items[i], visitor);
5603
5603
  if (typeof ci === "number")
5604
5604
  i = ci - 1;
5605
5605
  else if (ci === BREAK)
@@ -5610,10 +5610,10 @@ var require_cst_visit = __commonJS({
5610
5610
  }
5611
5611
  }
5612
5612
  if (typeof ctrl === "function" && field === "key")
5613
- ctrl = ctrl(item, path13);
5613
+ ctrl = ctrl(item, path15);
5614
5614
  }
5615
5615
  }
5616
- return typeof ctrl === "function" ? ctrl(item, path13) : ctrl;
5616
+ return typeof ctrl === "function" ? ctrl(item, path15) : ctrl;
5617
5617
  }
5618
5618
  exports.visit = visit;
5619
5619
  }
@@ -7370,7 +7370,8 @@ var ExitCode = {
7370
7370
  Failure: 1,
7371
7371
  Pending: 3,
7372
7372
  Rejected: 4,
7373
- Expired: 5
7373
+ Expired: 5,
7374
+ LoginRequired: 6
7374
7375
  };
7375
7376
  var CliError = class extends Error {
7376
7377
  constructor(message, exitCode = ExitCode.Failure) {
@@ -7383,11 +7384,46 @@ var CliError = class extends Error {
7383
7384
 
7384
7385
  // src/commands/agents.ts
7385
7386
  import fs8 from "node:fs";
7386
- import path11 from "node:path";
7387
- import { parseArgs as parseArgs2 } from "node:util";
7387
+ import path12 from "node:path";
7388
+ import { parseArgs as parseArgs3 } from "node:util";
7389
+
7390
+ // src/auth/oauth-client.ts
7391
+ import fs3 from "node:fs";
7392
+ import path3 from "node:path";
7393
+
7394
+ // src/config.ts
7395
+ import os from "node:os";
7396
+ import path from "node:path";
7397
+ var DEFAULT_API_URL = "https://divergeai.net";
7398
+ var DEFAULT_CLIENT_ID = "diverge";
7399
+ function configDir(env = process.env) {
7400
+ return env.DIVERGE_HOME?.trim() || path.join(os.homedir(), ".diverge");
7401
+ }
7402
+ function resolveApiUrl(flag, env = process.env) {
7403
+ const chosen = flag?.trim() || env.DIVERGENT_API_URL?.trim() || DEFAULT_API_URL;
7404
+ return chosen.replace(/\/+$/, "");
7405
+ }
7406
+ function clientId(env = process.env) {
7407
+ return env.DIVERGENT_DIVERGE_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID;
7408
+ }
7409
+ function deploymentKey(baseUrl) {
7410
+ const url = new URL(baseUrl);
7411
+ const pathPart = url.pathname.replace(/\/+$/, "");
7412
+ return `${url.protocol === "http:" ? "http_" : ""}${url.host}${pathPart}`.replace(/[^A-Za-z0-9.-]/g, "_");
7413
+ }
7414
+ function tokenSource(env, baseUrl) {
7415
+ const normalized = baseUrl.replace(/\/+$/, "");
7416
+ const dir = path.join(configDir(env), "logins", deploymentKey(normalized));
7417
+ return {
7418
+ loginFile: path.join(dir, "login.json"),
7419
+ tokensDir: path.join(dir, "tokens"),
7420
+ baseUrl: normalized,
7421
+ clientId: clientId(env)
7422
+ };
7423
+ }
7388
7424
 
7389
7425
  // src/version.ts
7390
- var VERSION = true ? "0.1.13" : "dev";
7426
+ var VERSION = true ? "0.1.22" : "dev";
7391
7427
  var USER_AGENT = `diverge/${VERSION}`;
7392
7428
 
7393
7429
  // src/http.ts
@@ -7397,9 +7433,13 @@ var HttpError = class extends Error {
7397
7433
  this.status = status2;
7398
7434
  this.body = body;
7399
7435
  this.name = "HttpError";
7436
+ const error = body && typeof body === "object" ? body.error : void 0;
7437
+ this.code = typeof error === "string" ? error : void 0;
7400
7438
  }
7401
7439
  status;
7402
7440
  body;
7441
+ /** The OAuth error code (RFC 6749 §5.2) when the body carries one: branch on it, never on the message. */
7442
+ code;
7403
7443
  };
7404
7444
  function errorMessageFrom(body, fallback) {
7405
7445
  if (body && typeof body === "object") {
@@ -7566,7 +7606,7 @@ function buildAuthorizeUrl(params) {
7566
7606
  url.searchParams.set("redirect_uri", params.redirectUri);
7567
7607
  url.searchParams.set("code_challenge", params.codeChallenge);
7568
7608
  url.searchParams.set("code_challenge_method", "S256");
7569
- url.searchParams.set("scope", params.scopes);
7609
+ if (params.scopes) url.searchParams.set("scope", params.scopes);
7570
7610
  url.searchParams.set("state", params.state);
7571
7611
  if (params.organizationId) {
7572
7612
  url.searchParams.set("organization_id", params.organizationId);
@@ -7582,11 +7622,11 @@ import fs2 from "node:fs";
7582
7622
 
7583
7623
  // src/fs/atomic.ts
7584
7624
  import fs from "node:fs";
7585
- import path from "node:path";
7625
+ import path2 from "node:path";
7586
7626
  function writeTextAtomic(filePath, text, mode = 384) {
7587
- const dir = path.dirname(filePath);
7627
+ const dir = path2.dirname(filePath);
7588
7628
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
7589
- const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
7629
+ const tmp = path2.join(dir, `.${path2.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
7590
7630
  const fd = fs.openSync(tmp, "wx", mode);
7591
7631
  try {
7592
7632
  fs.writeFileSync(fd, text);
@@ -7607,6 +7647,23 @@ function writeJsonAtomic(filePath, value, mode = 384) {
7607
7647
  }
7608
7648
 
7609
7649
  // src/auth/token-cache.ts
7650
+ function loadLoginRecord(file) {
7651
+ try {
7652
+ const parsed = JSON.parse(fs2.readFileSync(file, "utf8"));
7653
+ if (typeof parsed.base_url !== "string" || typeof parsed.refresh_token !== "string") return null;
7654
+ return {
7655
+ base_url: parsed.base_url,
7656
+ refresh_token: parsed.refresh_token,
7657
+ ...typeof parsed.account === "string" && parsed.account ? { account: parsed.account } : {},
7658
+ ...typeof parsed.organization === "string" && parsed.organization ? { organization: parsed.organization } : {}
7659
+ };
7660
+ } catch {
7661
+ return null;
7662
+ }
7663
+ }
7664
+ function saveLoginRecord(file, record) {
7665
+ writeJsonAtomic(file, record, 384);
7666
+ }
7610
7667
  function loadUserToken(file) {
7611
7668
  let text;
7612
7669
  try {
@@ -7619,12 +7676,7 @@ function loadUserToken(file) {
7619
7676
  if (typeof parsed.access_token !== "string" || typeof parsed.base_url !== "string" || typeof parsed.expires_at !== "number") {
7620
7677
  return null;
7621
7678
  }
7622
- return {
7623
- access_token: parsed.access_token,
7624
- refresh_token: typeof parsed.refresh_token === "string" ? parsed.refresh_token : null,
7625
- expires_at: parsed.expires_at,
7626
- base_url: parsed.base_url
7627
- };
7679
+ return { access_token: parsed.access_token, expires_at: parsed.expires_at, base_url: parsed.base_url };
7628
7680
  } catch {
7629
7681
  return null;
7630
7682
  }
@@ -7632,14 +7684,6 @@ function loadUserToken(file) {
7632
7684
  function saveUserToken(file, token) {
7633
7685
  writeJsonAtomic(file, token, 384);
7634
7686
  }
7635
- function deleteUserToken(file) {
7636
- try {
7637
- fs2.rmSync(file);
7638
- return true;
7639
- } catch {
7640
- return false;
7641
- }
7642
- }
7643
7687
  function isFresh(token, freshForSeconds, nowMs = Date.now()) {
7644
7688
  return token.expires_at - nowMs / 1e3 > freshForSeconds;
7645
7689
  }
@@ -7656,7 +7700,10 @@ function tokenClaims(accessToken) {
7656
7700
 
7657
7701
  // src/auth/oauth-client.ts
7658
7702
  var DEFAULT_EXPIRES_IN_SECONDS = 3600;
7659
- async function postToken(baseUrl, body) {
7703
+ function loginRequired(message) {
7704
+ return new CliError(message, ExitCode.LoginRequired);
7705
+ }
7706
+ async function postToken(baseUrl, body, refused) {
7660
7707
  try {
7661
7708
  return await fetchJson(tokenEndpoint(baseUrl), {
7662
7709
  method: "POST",
@@ -7664,14 +7711,13 @@ async function postToken(baseUrl, body) {
7664
7711
  body: JSON.stringify(body)
7665
7712
  });
7666
7713
  } catch (error) {
7667
- if (error instanceof HttpError) throw new CliError(`Token endpoint returned HTTP ${error.status}: ${error.message}`);
7668
- throw error;
7714
+ if (error instanceof HttpError) throw refused?.(error) ?? new CliError(`Token endpoint returned HTTP ${error.status}: ${error.message}`);
7715
+ throw new CliError(`Cannot reach ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
7669
7716
  }
7670
7717
  }
7671
- function cachedFrom(payload, baseUrl, previousRefresh, nowMs) {
7718
+ function accessFrom(payload, baseUrl, nowMs) {
7672
7719
  return {
7673
7720
  access_token: payload.access_token,
7674
- refresh_token: payload.refresh_token ?? previousRefresh,
7675
7721
  expires_at: nowMs / 1e3 + Number(payload.expires_in ?? DEFAULT_EXPIRES_IN_SECONDS),
7676
7722
  base_url: baseUrl.replace(/\/+$/, "")
7677
7723
  };
@@ -7693,8 +7739,6 @@ async function login(options, deps = defaultDeps) {
7693
7739
  codeChallenge: challengeS256(verifier),
7694
7740
  scopes: options.scopes,
7695
7741
  state,
7696
- organizationId: options.organizationId,
7697
- selectOrganization: options.selectOrganization,
7698
7742
  loginHint: options.loginHint
7699
7743
  });
7700
7744
  const pending = loopback.waitForCode(state, options.timeoutMs);
@@ -7709,41 +7753,90 @@ async function login(options, deps = defaultDeps) {
7709
7753
  redirect_uri: loopback.redirectUri,
7710
7754
  code_verifier: verifier
7711
7755
  });
7712
- const token = cachedFrom(payload, options.baseUrl, null, deps.now());
7713
- saveUserToken(options.tokenFile, token);
7714
- return token;
7756
+ if (!payload.refresh_token) throw new CliError("The platform issued no refresh token for this sign-in; run: diverge login");
7757
+ return { refreshToken: payload.refresh_token, token: accessFrom(payload, options.baseUrl, deps.now()) };
7715
7758
  } finally {
7716
7759
  await loopback.close();
7717
7760
  }
7718
7761
  }
7719
- async function refreshUserToken(baseUrl, clientId2, token, nowMs = Date.now(), organizationId) {
7720
- if (!token.refresh_token) throw new CliError("Cached login expired. Run: diverge login");
7721
- const payload = await postToken(baseUrl, {
7722
- grant_type: "refresh_token",
7723
- refresh_token: token.refresh_token,
7724
- client_id: clientId2,
7725
- ...organizationId ? { organization_id: organizationId } : {}
7726
- });
7727
- return cachedFrom(payload, token.base_url, token.refresh_token, nowMs);
7728
- }
7729
- async function bindOrganization(source, organizationId, nowMs = Date.now()) {
7730
- const token = loadUserToken(source.tokenFile);
7731
- if (!token || token.base_url !== source.baseUrl.replace(/\/+$/, "")) {
7732
- throw new CliError("No cached login for this deployment. Run: diverge login");
7733
- }
7734
- const bound = await refreshUserToken(source.baseUrl, source.clientId, token, nowMs, organizationId);
7735
- saveUserToken(source.tokenFile, bound);
7736
- return bound;
7762
+ async function issueToken(source, record, organizationId, nowMs = Date.now()) {
7763
+ const payload = await postToken(
7764
+ source.baseUrl,
7765
+ {
7766
+ grant_type: "refresh_token",
7767
+ refresh_token: record.refresh_token,
7768
+ client_id: source.clientId,
7769
+ ...organizationId ? { organization_id: organizationId } : {}
7770
+ },
7771
+ (error) => {
7772
+ if (error.code === "invalid_grant") return loginRequired(`Your diverge login is no longer valid (${error.message}). Run: diverge login`);
7773
+ if (error.code === "invalid_scope") return new CliError(`Not granted${organizationId ? ` at organization ${organizationId}` : ""}: ${error.message}`);
7774
+ return void 0;
7775
+ }
7776
+ );
7777
+ return {
7778
+ record: { ...record, refresh_token: payload.refresh_token ?? record.refresh_token },
7779
+ token: accessFrom(payload, source.baseUrl, nowMs)
7780
+ };
7737
7781
  }
7738
- async function getUserToken(source, freshForSeconds = 30, nowMs = Date.now()) {
7739
- const token = loadUserToken(source.tokenFile);
7740
- if (!token || token.base_url !== source.baseUrl.replace(/\/+$/, "")) {
7741
- throw new CliError("No cached login for this deployment. Run: diverge login");
7742
- }
7743
- if (isFresh(token, freshForSeconds, nowMs)) return token;
7744
- const refreshed = await refreshUserToken(source.baseUrl, source.clientId, token, nowMs);
7745
- saveUserToken(source.tokenFile, refreshed);
7746
- return refreshed;
7782
+ function loadLogin(source) {
7783
+ const record = loadLoginRecord(source.loginFile);
7784
+ return record && record.base_url === source.baseUrl ? record : null;
7785
+ }
7786
+ function saveLogin(source, record) {
7787
+ saveLoginRecord(source.loginFile, record);
7788
+ }
7789
+ function tokenFileFor(source, organizationId) {
7790
+ return path3.join(source.tokensDir, `${organizationId ? organizationId.replace(/[^A-Za-z0-9._-]/g, "_") : "_platform"}.json`);
7791
+ }
7792
+ function saveOrganizationToken(source, organizationId, token) {
7793
+ saveUserToken(tokenFileFor(source, organizationId), token);
7794
+ }
7795
+ function saveRotated(source, presentedRefresh, next) {
7796
+ const current = loadLoginRecord(source.loginFile);
7797
+ if (current && current.refresh_token !== presentedRefresh) return;
7798
+ saveLogin(source, next);
7799
+ }
7800
+ function forgetLogin(source) {
7801
+ fs3.rmSync(path3.dirname(source.loginFile), { recursive: true, force: true });
7802
+ }
7803
+ function noLogin(baseUrl, alternative) {
7804
+ const command = `diverge login${baseUrl === DEFAULT_API_URL ? "" : ` --api-url ${baseUrl}`}`;
7805
+ return loginRequired(`No cached login for ${baseUrl}. Run: ${command}${alternative ? `
7806
+ ${alternative}` : ""}`);
7807
+ }
7808
+ function requireLogin(source) {
7809
+ const record = loadLogin(source);
7810
+ if (!record) throw noLogin(source.baseUrl);
7811
+ return record;
7812
+ }
7813
+ async function useOrganization(source, organizationId, nowMs = Date.now()) {
7814
+ const token = await getUserToken(source, { organizationId, freshForSeconds: 0, nowMs });
7815
+ const record = requireLogin(source);
7816
+ saveLogin(source, { ...record, organization: organizationId });
7817
+ return token;
7818
+ }
7819
+ async function getUserToken(source, options = {}) {
7820
+ const nowMs = options.nowMs ?? Date.now();
7821
+ const record = requireLogin(source);
7822
+ const organizationId = options.organizationId?.trim() || record.organization;
7823
+ const file = tokenFileFor(source, organizationId);
7824
+ const cached = loadUserToken(file);
7825
+ if (cached && cached.base_url === source.baseUrl && ofThisLogin(cached, record) && isFresh(cached, options.freshForSeconds ?? 30, nowMs)) return cached;
7826
+ const issued = await issueToken(source, record, organizationId, nowMs);
7827
+ saveRotated(source, record.refresh_token, issued.record);
7828
+ saveOrganizationToken(source, organizationId, issued.token);
7829
+ return issued.token;
7830
+ }
7831
+ function ofThisLogin(token, record) {
7832
+ return tokenClaims(token.access_token).sid === tokenClaims(record.refresh_token).sid;
7833
+ }
7834
+ function loginExpiry(record) {
7835
+ const claims = tokenClaims(record.refresh_token);
7836
+ return {
7837
+ idle: typeof claims.exp === "number" ? claims.exp : null,
7838
+ cap: typeof claims.loginEndsAt === "number" ? claims.loginEndsAt : null
7839
+ };
7747
7840
  }
7748
7841
  async function revokeRefreshToken(baseUrl, clientId2, refreshToken) {
7749
7842
  const body = new URLSearchParams({ token: refreshToken, token_type_hint: "refresh_token", client_id: clientId2 });
@@ -7756,43 +7849,162 @@ async function revokeRefreshToken(baseUrl, clientId2, refreshToken) {
7756
7849
  if (!response.ok) throw new CliError(`Revoke endpoint returned HTTP ${response.status}`);
7757
7850
  }
7758
7851
 
7759
- // src/config.ts
7760
- import fs3 from "node:fs";
7761
- import os from "node:os";
7762
- import path2 from "node:path";
7763
- var DEFAULT_API_URL = "https://divergeai.net";
7764
- var DEFAULT_CLIENT_ID = "diverge";
7765
- function configDir(env = process.env) {
7766
- return env.DIVERGE_HOME?.trim() || path2.join(os.homedir(), ".diverge");
7852
+ // src/commands/login.ts
7853
+ import { parseArgs } from "node:util";
7854
+
7855
+ // src/auth/organizations.ts
7856
+ function chooseOrganization(input) {
7857
+ const organizations = input.organizations;
7858
+ const flag = input.flag?.trim();
7859
+ if (flag) {
7860
+ const match = organizations.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
7861
+ if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
7862
+ throw new CliError(`Not one of your organizations: ${flag}. Yours: ${organizations.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
7863
+ }
7864
+ const remembered = input.remembered?.trim();
7865
+ if (remembered) {
7866
+ const match = organizations.find((organization) => organization.id === remembered);
7867
+ if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
7868
+ }
7869
+ if (organizations.length === 1) return { kind: "chosen", organizationId: organizations[0].id, name: organizations[0].name, reason: "only" };
7870
+ if (organizations.length === 0) return { kind: "none" };
7871
+ return { kind: "ask", options: organizations };
7767
7872
  }
7768
- function tokenFile(env = process.env) {
7769
- return path2.join(configDir(env), "token.json");
7873
+ function organizationMenu(options) {
7874
+ return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
7770
7875
  }
7771
- function configFile(env = process.env) {
7772
- return path2.join(configDir(env), "config.json");
7876
+ function pickFromMenu(answer, options) {
7877
+ const trimmed = answer.trim();
7878
+ const index = Number(trimmed);
7879
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
7880
+ const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
7881
+ if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
7882
+ return match;
7773
7883
  }
7774
- function readConfig(env = process.env) {
7884
+
7885
+ // src/auth/userinfo.ts
7886
+ async function fetchUserInfo(baseUrl, accessToken) {
7887
+ return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
7888
+ }
7889
+ async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
7775
7890
  try {
7776
- const parsed = JSON.parse(fs3.readFileSync(configFile(env), "utf8"));
7777
- return parsed && typeof parsed === "object" ? parsed : {};
7778
- } catch {
7779
- return {};
7891
+ const info = await fetchUserInfo(baseUrl, accessToken);
7892
+ return options.withSub ? `${info.email} (${info.sub})` : info.email;
7893
+ } catch (error) {
7894
+ return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
7780
7895
  }
7781
7896
  }
7782
- function writeConfig(config, env = process.env) {
7783
- writeJsonAtomic(configFile(env), config);
7897
+
7898
+ // src/prompt.ts
7899
+ import readline from "node:readline";
7900
+ function ask(question) {
7901
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
7902
+ return new Promise((resolve) => rl.question(question, (answer) => {
7903
+ rl.close();
7904
+ resolve(answer);
7905
+ }));
7784
7906
  }
7785
- function resolveApiUrl(flag, env = process.env, config = readConfig(env)) {
7786
- const chosen = flag?.trim() || env.DIVERGENT_API_URL?.trim() || config.apiUrl?.trim() || DEFAULT_API_URL;
7787
- return chosen.replace(/\/+$/, "");
7907
+
7908
+ // src/commands/login.ts
7909
+ var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
7910
+
7911
+ Sign in through the browser, then choose where to act. The browser only
7912
+ authenticates the person; the organization is chosen here afterwards \u2014 the
7913
+ one named by --organization-id (id or name), else the last login's, else the
7914
+ only membership, else a menu. The login itself is yours, not an
7915
+ organization's; the default is a local preference.
7916
+ That is the default: commands act there unless they name another with
7917
+ --organization-id or DIVERGENT_ORG_ID. Change it with: diverge org use <id|name>.
7918
+ --as <email> names the account: the platform signs that account in instead of
7919
+ reusing whatever session the browser holds, and issues nothing to any other.
7920
+ Without --as, the account of the last login is used; switching is explicit.
7921
+ Without --scopes, the login asks for the scopes the platform registers for
7922
+ this CLI \u2014 everything it and forge use \u2014 and holds what your roles grant; a
7923
+ scope the platform adds later reaches this login on its next refresh.
7924
+ --scopes asks for less, deliberately, and the login stays that narrow.
7925
+ --api-url signs in to another deployment (a test stack, say). Each deployment
7926
+ keeps its own login, so this never replaces another one; commands reach it
7927
+ when DIVERGENT_API_URL names it.`;
7928
+ function resolveScopes(flag) {
7929
+ const explicit = flag?.trim();
7930
+ return explicit ? explicit.split(/\s+/).join(" ") : void 0;
7788
7931
  }
7789
- function clientId(env = process.env) {
7790
- return env.DIVERGENT_DIVERGE_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID;
7932
+ function resolveLoginHint(flag, previous) {
7933
+ const explicit = flag?.trim().toLowerCase();
7934
+ if (explicit) return explicit;
7935
+ const remembered = previous.account?.trim().toLowerCase();
7936
+ return remembered || void 0;
7937
+ }
7938
+ async function runLogin(args, io) {
7939
+ const { values } = parseArgs({
7940
+ args,
7941
+ options: {
7942
+ "organization-id": { type: "string" },
7943
+ scopes: { type: "string" },
7944
+ as: { type: "string" },
7945
+ "no-browser": { type: "boolean", default: false },
7946
+ "api-url": { type: "string" }
7947
+ },
7948
+ strict: true,
7949
+ allowPositionals: false
7950
+ });
7951
+ const apiUrl = resolveApiUrl(values["api-url"], io.env);
7952
+ const source = tokenSource(io.env, apiUrl);
7953
+ const previous = loadLogin(source);
7954
+ const loginHint = resolveLoginHint(values.as, { account: previous?.account });
7955
+ if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
7956
+ const scopes = resolveScopes(values.scopes);
7957
+ io.stdout(scopes ? `Asking for scopes: ${scopes}.` : "Asking for the scopes the platform registers for this CLI.");
7958
+ const signIn = await login({
7959
+ baseUrl: apiUrl,
7960
+ clientId: source.clientId,
7961
+ scopes,
7962
+ openBrowser: !values["no-browser"],
7963
+ loginHint,
7964
+ print: io.stdout
7965
+ });
7966
+ let info;
7967
+ try {
7968
+ info = await fetchUserInfo(apiUrl, signIn.token.access_token);
7969
+ } catch (error) {
7970
+ throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
7971
+ }
7972
+ io.stdout(`Signed in as: ${info.email}`);
7973
+ const organizationId = await chooseDefaultOrganization(io, values["organization-id"], previous?.organization, info);
7974
+ const issued = await issueToken(source, { base_url: source.baseUrl, refresh_token: signIn.refreshToken, account: info.email }, organizationId);
7975
+ saveLogin(source, { ...issued.record, organization: organizationId });
7976
+ saveOrganizationToken(source, organizationId, issued.token);
7977
+ io.stdout(`Logged in to ${apiUrl}.`);
7978
+ if (apiUrl !== resolveApiUrl(void 0, io.env)) io.stdout(`Commands reach this deployment with DIVERGENT_API_URL=${apiUrl}.`);
7979
+ const granted = new Set(tokenClaims(issued.token.access_token).scopes ?? []);
7980
+ io.stdout(`Scopes: ${[...granted].join(" ") || "none"}`);
7981
+ const missing = (scopes ?? "").split(" ").filter((scope) => scope && !granted.has(scope));
7982
+ if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
7983
+ return 0;
7984
+ }
7985
+ async function chooseDefaultOrganization(io, flag, remembered, info) {
7986
+ let choice = chooseOrganization({ flag, remembered, organizations: info.organizations ?? [] });
7987
+ if (choice.kind === "none") {
7988
+ throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
7989
+ }
7990
+ if (choice.kind === "ask") {
7991
+ if (!io.interactive) {
7992
+ throw new CliError(`You may act in several organizations; pass --organization-id <id|name>:
7993
+ ${organizationMenu(choice.options).join("\n")}`);
7994
+ }
7995
+ io.stdout("Choose the organization to act in:");
7996
+ for (const line of organizationMenu(choice.options)) io.stdout(line);
7997
+ const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
7998
+ choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
7999
+ }
8000
+ const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
8001
+ const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
8002
+ io.stdout(`Organization: ${label}${why}`);
8003
+ return choice.organizationId;
7791
8004
  }
7792
8005
 
7793
8006
  // src/gateway/urls.ts
7794
8007
  var SURFACES = ["anthropic", "openai"];
7795
- var GATEWAY_SCOPES = "mcp:read mcp:execute inference:execute";
7796
8008
  var INFERENCE_SCOPE = "inference:execute";
7797
8009
  function anthropicBaseUrl(apiUrl) {
7798
8010
  return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/anthropic`;
@@ -7803,12 +8015,6 @@ function openaiBaseUrl(apiUrl) {
7803
8015
  function surfaceBaseUrl(apiUrl, surface) {
7804
8016
  return surface === "openai" ? openaiBaseUrl(apiUrl) : anthropicBaseUrl(apiUrl);
7805
8017
  }
7806
- function loginServesGateway(claims, organizationId) {
7807
- if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) return false;
7808
- const org = claims.organizationId;
7809
- if (!org) return false;
7810
- return !organizationId || org === organizationId;
7811
- }
7812
8018
 
7813
8019
  // src/gateway/models.ts
7814
8020
  async function listModels(apiUrl, accessToken, surface) {
@@ -7825,35 +8031,27 @@ async function listModels(apiUrl, accessToken, surface) {
7825
8031
  throw error;
7826
8032
  }
7827
8033
  }
7828
- async function ensureInferenceLogin(io, apiUrl, organizationId, openBrowser2 = true) {
7829
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
7830
- try {
7831
- const cached = await getUserToken(source);
7832
- if (loginServesGateway(tokenClaims(cached.access_token), organizationId)) return cached;
7833
- } catch (error) {
7834
- if (!(error instanceof CliError)) throw error;
8034
+ async function ensureInferenceLogin(io, apiUrl, organizationId) {
8035
+ const source = tokenSource(io.env, apiUrl);
8036
+ if (!loadLogin(source)) {
8037
+ if (!io.interactive) throw new CliError(`Not signed in to ${apiUrl}. Run: diverge login${organizationId ? ` --organization-id ${organizationId}` : ""}`);
8038
+ await runLogin(organizationId ? ["--organization-id", organizationId] : [], io);
7835
8039
  }
7836
- if (!io.interactive) {
7837
- throw new CliError(`Not signed in for the gateway${organizationId ? ` at organization ${organizationId}` : ""}. Run: diverge login${organizationId ? ` --organization-id ${organizationId}` : ""}`);
8040
+ const token = await getUserToken(source, { organizationId });
8041
+ const claims = tokenClaims(token.access_token);
8042
+ if (!claims.organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
8043
+ if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) {
8044
+ throw new CliError(`The token for organization ${claims.organizationId} carries no ${INFERENCE_SCOPE}: your roles there lack inference_models:execute, or the login asked for less (re-run: diverge login).`);
7838
8045
  }
7839
- return login({
7840
- baseUrl: apiUrl,
7841
- clientId: source.clientId,
7842
- scopes: GATEWAY_SCOPES,
7843
- organizationId,
7844
- selectOrganization: !organizationId,
7845
- openBrowser: openBrowser2,
7846
- tokenFile: source.tokenFile,
7847
- print: io.stdout
7848
- });
8046
+ return token;
7849
8047
  }
7850
8048
 
7851
8049
  // src/agents/claude.ts
7852
8050
  import os2 from "node:os";
7853
- import path3 from "node:path";
8051
+ import path4 from "node:path";
7854
8052
  var CLAUDE_MODEL_DISCOVERY_FLAG = "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
7855
8053
  function claudeSettingsPath(scope, cwd = process.cwd()) {
7856
- return scope === "user" ? path3.join(os2.homedir(), ".claude", "settings.json") : path3.join(cwd, ".claude", "settings.local.json");
8054
+ return scope === "user" ? path4.join(os2.homedir(), ".claude", "settings.json") : path4.join(cwd, ".claude", "settings.local.json");
7857
8055
  }
7858
8056
  function mergeClaudeSettings(existing, options) {
7859
8057
  const settings = structuredClone(existing ?? {});
@@ -7874,11 +8072,11 @@ function mergeClaudeSettings(existing, options) {
7874
8072
 
7875
8073
  // src/agents/codex.ts
7876
8074
  import os3 from "node:os";
7877
- import path4 from "node:path";
8075
+ import path5 from "node:path";
7878
8076
  var CODEX_BLOCK_START = "# >>> divergent >>>";
7879
8077
  var CODEX_BLOCK_END = "# <<< divergent <<<";
7880
8078
  function codexConfigPath() {
7881
- return path4.join(os3.homedir(), ".codex", "config.toml");
8079
+ return path5.join(os3.homedir(), ".codex", "config.toml");
7882
8080
  }
7883
8081
  var tomlString = (value) => JSON.stringify(value);
7884
8082
  function codexProviderToml(options) {
@@ -7894,7 +8092,7 @@ function codexProviderToml(options) {
7894
8092
  "",
7895
8093
  "[model_providers.divergent.auth]",
7896
8094
  `command = ${tomlString(options.self.command)}`,
7897
- `args = [${[...options.self.args, "token"].map(tomlString).join(", ")}]`,
8095
+ `args = [${[...options.self.args, "token", ...options.organizationId ? ["--organization-id", options.organizationId] : []].map(tomlString).join(", ")}]`,
7898
8096
  "refresh_interval_ms = 600000",
7899
8097
  CODEX_BLOCK_END,
7900
8098
  ""
@@ -7919,10 +8117,10 @@ var import_yaml = __toESM(require_dist(), 1);
7919
8117
  import fs4 from "node:fs";
7920
8118
  import { createRequire } from "node:module";
7921
8119
  import os4 from "node:os";
7922
- import path5 from "node:path";
8120
+ import path6 from "node:path";
7923
8121
  var DSH_SHIPPED_DEFAULT_MODEL = "deepseek-flash";
7924
8122
  function dshHome(env = process.env) {
7925
- return env.DSH_HOME?.trim() || path5.join(os4.homedir(), ".dsh");
8123
+ return env.DSH_HOME?.trim() || path6.join(os4.homedir(), ".dsh");
7926
8124
  }
7927
8125
  function dshDefaultModel(models, requested) {
7928
8126
  if (requested) return requested;
@@ -8011,30 +8209,30 @@ var DIVERGENT_PRESET_ID = "divergent";
8011
8209
  var DSH_USER_PRESETS_DIR = ".agent-presets";
8012
8210
  function dshPackageDir(binary) {
8013
8211
  if (/\.(cmd|bat)$/i.test(binary)) {
8014
- const dir2 = path5.join(path5.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8015
- return fs4.existsSync(path5.join(dir2, "package.json")) ? dir2 : null;
8212
+ const dir2 = path6.join(path6.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8213
+ return fs4.existsSync(path6.join(dir2, "package.json")) ? dir2 : null;
8016
8214
  }
8017
8215
  let dir;
8018
8216
  try {
8019
- dir = path5.dirname(fs4.realpathSync(binary));
8217
+ dir = path6.dirname(fs4.realpathSync(binary));
8020
8218
  } catch {
8021
8219
  return null;
8022
8220
  }
8023
8221
  for (; ; ) {
8024
8222
  try {
8025
- if (JSON.parse(fs4.readFileSync(path5.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8223
+ if (JSON.parse(fs4.readFileSync(path6.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8026
8224
  } catch {
8027
8225
  }
8028
- const parent = path5.dirname(dir);
8226
+ const parent = path6.dirname(dir);
8029
8227
  if (parent === dir) return null;
8030
8228
  dir = parent;
8031
8229
  }
8032
8230
  }
8033
8231
  function shippedPresetDir(packageDir, id = "standard") {
8034
8232
  try {
8035
- const require2 = createRequire(path5.join(packageDir, "package.json"));
8036
- const dir = path5.join(path5.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
8037
- return fs4.existsSync(path5.join(dir, "agent.cordis.yml")) ? dir : null;
8233
+ const require2 = createRequire(path6.join(packageDir, "package.json"));
8234
+ const dir = path6.join(path6.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
8235
+ return fs4.existsSync(path6.join(dir, "agent.cordis.yml")) ? dir : null;
8038
8236
  } catch {
8039
8237
  return null;
8040
8238
  }
@@ -8071,9 +8269,9 @@ var DIVERGENT_PRESET_METADATA = [
8071
8269
  ""
8072
8270
  ].join("\n");
8073
8271
  function writeDivergentPreset(home, shippedDir) {
8074
- const dir = path5.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
8075
- writeTextAtomic(path5.join(dir, "agent.cordis.yml"), divergentPreset(fs4.readFileSync(path5.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
8076
- writeTextAtomic(path5.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
8272
+ const dir = path6.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
8273
+ writeTextAtomic(path6.join(dir, "agent.cordis.yml"), divergentPreset(fs4.readFileSync(path6.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
8274
+ writeTextAtomic(path6.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
8077
8275
  return dir;
8078
8276
  }
8079
8277
  function withDivergentPresetDefault(settingsText) {
@@ -8087,9 +8285,9 @@ function withDivergentPresetDefault(settingsText) {
8087
8285
 
8088
8286
  // src/agents/opencode.ts
8089
8287
  import os5 from "node:os";
8090
- import path6 from "node:path";
8288
+ import path7 from "node:path";
8091
8289
  function opencodeConfigPath() {
8092
- return path6.join(os5.homedir(), ".config", "opencode", "opencode.json");
8290
+ return path7.join(os5.homedir(), ".config", "opencode", "opencode.json");
8093
8291
  }
8094
8292
  function mergeOpencodeConfig(existing, options) {
8095
8293
  const tokenEnv = options.tokenEnv ?? "DIVERGENT_TOKEN";
@@ -8106,14 +8304,14 @@ function mergeOpencodeConfig(existing, options) {
8106
8304
  // src/agents/run-dsh.ts
8107
8305
  import { spawn as spawn2 } from "node:child_process";
8108
8306
  import fs5 from "node:fs";
8109
- import path7 from "node:path";
8307
+ import path8 from "node:path";
8110
8308
  function findOnPath(name, env = process.env, platform = process.platform) {
8111
- const dirs = (env.PATH ?? "").split(path7.delimiter).filter(Boolean);
8309
+ const dirs = (env.PATH ?? "").split(path8.delimiter).filter(Boolean);
8112
8310
  const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
8113
8311
  const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
8114
8312
  for (const dir of dirs) {
8115
8313
  for (const candidate of candidates) {
8116
- const full = path7.join(dir, candidate);
8314
+ const full = path8.join(dir, candidate);
8117
8315
  try {
8118
8316
  fs5.accessSync(full, fs5.constants.X_OK);
8119
8317
  if (fs5.statSync(full).isFile()) return full;
@@ -8139,16 +8337,16 @@ function dshArgs(overlayPath, args) {
8139
8337
  }
8140
8338
  function dshLaunch(binary, args, platform = process.platform, execPath = process.execPath) {
8141
8339
  if (platform !== "win32" || !/\.(cmd|bat)$/i.test(binary)) return { command: binary, args };
8142
- const packageDir = path7.join(path7.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8340
+ const packageDir = path8.join(path8.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8143
8341
  let bin;
8144
8342
  try {
8145
- bin = JSON.parse(fs5.readFileSync(path7.join(packageDir, "package.json"), "utf8")).bin;
8343
+ bin = JSON.parse(fs5.readFileSync(path8.join(packageDir, "package.json"), "utf8")).bin;
8146
8344
  } catch {
8147
8345
  throw new CliError(`${binary} is not an npm shim for @deepseek-ai/dsh (no ${packageDir}); reinstall: npm install -g @deepseek-ai/dsh`);
8148
8346
  }
8149
8347
  const script = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null ? bin.dsh : void 0;
8150
8348
  if (typeof script !== "string") throw new CliError(`${packageDir}/package.json declares no dsh entry; reinstall: npm install -g @deepseek-ai/dsh`);
8151
- return { command: execPath, args: [path7.join(packageDir, script), ...args] };
8349
+ return { command: execPath, args: [path8.join(packageDir, script), ...args] };
8152
8350
  }
8153
8351
  async function keepTokenFresh(loop) {
8154
8352
  let written = null;
@@ -8215,7 +8413,7 @@ function runDsh(options) {
8215
8413
  }
8216
8414
 
8217
8415
  // src/commands/skills.ts
8218
- import { parseArgs } from "node:util";
8416
+ import { parseArgs as parseArgs2 } from "node:util";
8219
8417
 
8220
8418
  // src/skills/bundled.ts
8221
8419
  var DIVERGE_SKILL = {
@@ -8235,9 +8433,9 @@ their permissions and audit trail. Nothing runs locally; the CLI is the
8235
8433
  transport. Keep the catalog out of context: pull the one schema you need.
8236
8434
 
8237
8435
  \`\`\`bash
8238
- diverge tools list --filter <word> # find candidates by name or capability
8436
+ diverge tools list --filter <word> # find candidates by name or description
8239
8437
  diverge tools describe <tool> # the one schema, on demand
8240
- diverge tools call <tool> --args-file args.json # arguments from a file (see below)
8438
+ diverge tools call <tool> --args-file args.json --json # arguments from a file, one JSON outcome out
8241
8439
  diverge tools call search_online --args-json '{"query": "..."}' # web search is a platform tool too
8242
8440
  \`\`\`
8243
8441
 
@@ -8253,20 +8451,28 @@ Write the object to a file and pass \`--args-file\`, or pipe it with
8253
8451
  \`--args-json -\`; both are exact on every shell. A worked call:
8254
8452
 
8255
8453
  \`\`\`bash
8256
- diverge tools describe divix-mcp_implementation_search_source
8454
+ diverge tools describe divix-data-sync_implementation_search_source
8257
8455
  cat > /tmp/args.json <<'JSON'
8258
8456
  {"query": "cr9b5_serial", "limit": 20}
8259
8457
  JSON
8260
- diverge tools call divix-mcp_implementation_search_source --args-file /tmp/args.json
8458
+ diverge tools call divix-data-sync_implementation_search_source --args-file /tmp/args.json
8261
8459
  \`\`\`
8262
8460
 
8461
+ ## Read the outcome from stdout with --json
8462
+
8463
+ \`call --json\` prints exactly one JSON object on stdout whatever happens,
8464
+ errors included: \`{"tool", "status", "result"?, "error"?, "invocationId"?,
8465
+ "decideAt"?}\`, where status is executed, pending, rejected, expired or failed.
8466
+ Parse stdout alone; stderr only carries the scope and identity lines. Without
8467
+ \`--json\`, errors are text on stderr.
8468
+
8263
8469
  ## Several calls at once
8264
8470
 
8265
8471
  When a task needs a sequence of calls, write them to one file and run it
8266
- once instead of one round trip each: \`diverge tools run calls.json\`, a JSON
8472
+ once instead of one round trip each: \`diverge tools run calls.json\` (or \`run -\` for stdin), a JSON
8267
8473
  array of \`{"tool": "...", "args": {...}}\` (or one per line). Calls run in
8268
- order; each prints one JSON line of outcome; the exit code is the worst of
8269
- them.
8474
+ order; each prints one JSON line of outcome, the same shape as \`call --json\`;
8475
+ the exit code is the worst of them.
8270
8476
 
8271
8477
  ## Exit codes are the contract
8272
8478
 
@@ -8284,8 +8490,11 @@ cadence). Never decide your own invocation from any surface you can drive.
8284
8490
 
8285
8491
  ## Session
8286
8492
 
8287
- - \`diverge status\` says who is signed in and at which organization; every
8288
- call runs there. \`diverge org use <id|name>\` moves the session.
8493
+ - \`diverge status\` says who is signed in and at which organization; a call
8494
+ that names none runs there. To act in another organization, pass
8495
+ \`--organization-id <id>\` (or set \`DIVERGENT_ORG_ID\`) on the call; that
8496
+ never changes the default. \`diverge org use <id|name>\` changes it for
8497
+ every session on the machine, so leave it to the person.
8289
8498
  - A missing login or scope prints the command to run; \`diverge login\` opens
8290
8499
  the browser.
8291
8500
  - \`--json\` on \`list\`, \`describe\` and \`invocation\` gives raw records.
@@ -8295,39 +8504,47 @@ cadence). Never decide your own invocation from any surface you can drive.
8295
8504
  // src/skills/install.ts
8296
8505
  import fs6 from "node:fs";
8297
8506
  import os6 from "node:os";
8298
- import path8 from "node:path";
8507
+ import path9 from "node:path";
8299
8508
  var HARNESSES = ["claude", "codex", "dsh"];
8300
8509
  var SKILLS_MANIFEST = ".diverge-skills.json";
8301
8510
  function skillsRoot(harness, env = process.env) {
8302
8511
  switch (harness) {
8303
8512
  case "claude":
8304
- return path8.join(env.CLAUDE_CONFIG_DIR?.trim() || path8.join(os6.homedir(), ".claude"), "skills");
8513
+ return path9.join(env.CLAUDE_CONFIG_DIR?.trim() || path9.join(os6.homedir(), ".claude"), "skills");
8305
8514
  case "codex":
8306
- return path8.join(env.CODEX_HOME?.trim() || path8.join(os6.homedir(), ".codex"), "skills");
8515
+ return path9.join(env.CODEX_HOME?.trim() || path9.join(os6.homedir(), ".codex"), "skills");
8307
8516
  case "dsh":
8308
- return path8.join(dshHome(env), "skills");
8517
+ return path9.join(dshHome(env), "skills");
8309
8518
  }
8310
8519
  }
8311
8520
  function readSkillsManifest(root) {
8312
8521
  try {
8313
- const parsed = JSON.parse(fs6.readFileSync(path8.join(root, SKILLS_MANIFEST), "utf8"));
8522
+ const parsed = JSON.parse(fs6.readFileSync(path9.join(root, SKILLS_MANIFEST), "utf8"));
8314
8523
  if (parsed && typeof parsed === "object" && typeof parsed.skills === "object") return parsed;
8315
8524
  } catch {
8316
8525
  }
8317
8526
  return null;
8318
8527
  }
8528
+ function few(names, limit = 6) {
8529
+ return names.length <= limit ? names.join(", ") : `${names.slice(0, limit).join(", ")} and ${names.length - limit} more`;
8530
+ }
8319
8531
  function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Date()) {
8320
8532
  const previous = readSkillsManifest(root);
8533
+ const held = previous?.skills ?? {};
8321
8534
  const written = [];
8535
+ const added = [];
8536
+ const updated = [];
8322
8537
  for (const skill of skills) {
8323
8538
  if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(skill.name)) throw new Error(`skill name ${JSON.stringify(skill.name)} is not a folder name`);
8324
- writeTextAtomic(path8.join(root, skill.name, "SKILL.md"), skill.content, 420);
8539
+ writeTextAtomic(path9.join(root, skill.name, "SKILL.md"), skill.content, 420);
8325
8540
  written.push(skill.name);
8541
+ if (!(skill.name in held)) added.push(skill.name);
8542
+ else if (held[skill.name] !== skill.updatedAt) updated.push(skill.name);
8326
8543
  }
8327
8544
  const removed = [];
8328
8545
  for (const name of Object.keys(previous?.skills ?? {})) {
8329
8546
  if (skills.some((skill) => skill.name === name)) continue;
8330
- fs6.rmSync(path8.join(root, name), { recursive: true, force: true });
8547
+ fs6.rmSync(path9.join(root, name), { recursive: true, force: true });
8331
8548
  removed.push(name);
8332
8549
  }
8333
8550
  const manifest = {
@@ -8335,8 +8552,8 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
8335
8552
  source,
8336
8553
  skills: Object.fromEntries(skills.map((skill) => [skill.name, skill.updatedAt]))
8337
8554
  };
8338
- writeJsonAtomic(path8.join(root, SKILLS_MANIFEST), manifest, 420);
8339
- return { root, written, removed };
8555
+ writeJsonAtomic(path9.join(root, SKILLS_MANIFEST), manifest, 420);
8556
+ return { root, written, added, updated, removed };
8340
8557
  }
8341
8558
  function skillsStatus(root, skills) {
8342
8559
  const manifest = readSkillsManifest(root);
@@ -8345,7 +8562,7 @@ function skillsStatus(root, skills) {
8345
8562
  const behind = [];
8346
8563
  const missing = [];
8347
8564
  for (const skill of skills) {
8348
- if (!(skill.name in held) || !fs6.existsSync(path8.join(root, skill.name, "SKILL.md"))) missing.push(skill.name);
8565
+ if (!(skill.name in held) || !fs6.existsSync(path9.join(root, skill.name, "SKILL.md"))) missing.push(skill.name);
8349
8566
  else if (held[skill.name] !== skill.updatedAt) behind.push(skill.name);
8350
8567
  else current.push(skill.name);
8351
8568
  }
@@ -8368,16 +8585,16 @@ var skillsUsage = `diverge skills <list|install|status> [--harness claude,codex,
8368
8585
 
8369
8586
  list the skills the platform serves you: your organization's skills whose tools you can run
8370
8587
  install write them, and the CLI's own "diverge" skill, into each harness's user skill folder
8371
- (~/.claude/skills, ~/.codex/skills, $DSH_HOME/skills); retires the ones the organization retired
8588
+ (~/.claude/skills, ~/.codex/skills, $DSH_HOME/skills); retires the ones the organization retired;
8589
+ reports what changed \u2014 the full list is \`skills list\`
8372
8590
  status what each folder holds against what the platform serves now
8373
8591
 
8374
8592
  A skill is a procedure over the tools it names; you get the ones whose every
8375
8593
  tool you may run. Re-run install after the organization changes its skills.`;
8376
8594
  async function installableSkillSet(apiUrl, env) {
8377
- const source = { tokenFile: tokenFile(env), baseUrl: apiUrl, clientId: clientId(env) };
8378
- const token = await getUserToken(source);
8595
+ const token = await getUserToken(tokenSource(env, apiUrl));
8379
8596
  const organizationId = tokenClaims(token.access_token).organizationId;
8380
- if (!organizationId) throw new CliError("The login is not bound to an organization; run: diverge org use <id|name>");
8597
+ if (!organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
8381
8598
  const served = await fetchInstallableSkills(apiUrl, token.access_token);
8382
8599
  return { skills: [DIVERGE_SKILL, ...served.skills], organizationId };
8383
8600
  }
@@ -8390,12 +8607,16 @@ function harnessesOf(flag) {
8390
8607
  function installForHarness(io, harness, skills, apiUrl, into) {
8391
8608
  const root = into ?? skillsRoot(harness, io.env);
8392
8609
  const result = installSkills(root, skills, apiUrl);
8393
- const retired = result.removed.length ? `; retired ${result.removed.join(", ")}` : "";
8394
- io.stdout(`${harness}: ${result.written.length} skills in ${root} (${result.written.join(", ")})${retired}`);
8610
+ const changes = [
8611
+ ...result.added.length ? [`${result.added.length} new: ${few(result.added)}`] : [],
8612
+ ...result.updated.length ? [`${result.updated.length} updated: ${few(result.updated)}`] : [],
8613
+ ...result.removed.length ? [`${result.removed.length} retired: ${few(result.removed)}`] : []
8614
+ ];
8615
+ io.stdout(`${harness}: ${result.written.length} skills in ${root}${changes.length ? ` (${changes.join("; ")})` : " (unchanged)"}`);
8395
8616
  }
8396
8617
  async function runSkills(args, io) {
8397
8618
  const [verb, ...rest] = args;
8398
- const { values } = parseArgs({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
8619
+ const { values } = parseArgs2({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
8399
8620
  const apiUrl = resolveApiUrl(void 0, io.env);
8400
8621
  switch (verb) {
8401
8622
  case "list": {
@@ -8416,9 +8637,9 @@ async function runSkills(args, io) {
8416
8637
  for (const harness of harnessesOf(values.harness)) {
8417
8638
  const status2 = skillsStatus(values.into ?? skillsRoot(harness, io.env), skills);
8418
8639
  const parts = [`${status2.current.length} current`];
8419
- if (status2.behind.length) parts.push(`${status2.behind.length} behind (${status2.behind.join(", ")})`);
8420
- if (status2.missing.length) parts.push(`${status2.missing.length} missing (${status2.missing.join(", ")})`);
8421
- if (status2.retired.length) parts.push(`${status2.retired.length} retired (${status2.retired.join(", ")})`);
8640
+ if (status2.behind.length) parts.push(`${status2.behind.length} behind (${few(status2.behind)})`);
8641
+ if (status2.missing.length) parts.push(`${status2.missing.length} missing${status2.current.length || status2.behind.length ? ` (${few(status2.missing)})` : ""}`);
8642
+ if (status2.retired.length) parts.push(`${status2.retired.length} retired (${few(status2.retired)})`);
8422
8643
  behind ||= status2.behind.length + status2.missing.length + status2.retired.length > 0;
8423
8644
  io.stdout(`${harness}: ${parts.join(", ")} in ${status2.root}${status2.installedAt ? ` (installed ${status2.installedAt})` : " (never installed)"}`);
8424
8645
  }
@@ -8434,9 +8655,9 @@ ${skillsUsage}`);
8434
8655
 
8435
8656
  // src/agents/rotate.ts
8436
8657
  import os7 from "node:os";
8437
- import path9 from "node:path";
8658
+ import path10 from "node:path";
8438
8659
  function defaultTokenFilePath() {
8439
- return path9.join(os7.homedir(), ".divergent", "token");
8660
+ return path10.join(os7.homedir(), ".divergent", "token");
8440
8661
  }
8441
8662
  var MIN_ROTATE_INTERVAL_SECONDS = 60;
8442
8663
  async function rotateTokenFile(loop) {
@@ -8452,14 +8673,14 @@ async function rotateTokenFile(loop) {
8452
8673
 
8453
8674
  // src/agents/self-command.ts
8454
8675
  import fs7 from "node:fs";
8455
- import path10 from "node:path";
8676
+ import path11 from "node:path";
8456
8677
  function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
8457
8678
  const script = argv[1];
8458
8679
  if (!script) return { command: execPath, args: [] };
8459
8680
  if (platform !== "win32") {
8460
8681
  try {
8461
8682
  fs7.accessSync(script, fs7.constants.X_OK);
8462
- return { command: path10.resolve(script), args: [] };
8683
+ return { command: path11.resolve(script), args: [] };
8463
8684
  } catch {
8464
8685
  }
8465
8686
  }
@@ -8508,7 +8729,7 @@ function syncDivergentPreset(io, binary, home) {
8508
8729
  return;
8509
8730
  }
8510
8731
  const dir = writeDivergentPreset(home, shipped);
8511
- const settingsPath = path11.join(home, "settings.yaml");
8732
+ const settingsPath = path12.join(home, "settings.yaml");
8512
8733
  const settings = withDivergentPresetDefault(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null);
8513
8734
  if (settings !== null) writeTextAtomic(settingsPath, settings);
8514
8735
  io.stderr(`Agent preset "${DIVERGENT_PRESET_ID}" (${dir}): web sessions search through the platform${settings !== null ? "; set as the default preset in settings.yaml" : ""}.`);
@@ -8528,18 +8749,22 @@ async function overlayAndDefault(apiUrl, token, surface, io, requested) {
8528
8749
  const defaultModel = dshDefaultModel(models, requested);
8529
8750
  return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models, defaultModel }), defaultModel, models };
8530
8751
  }
8752
+ function organizationArgs(organizationId) {
8753
+ const trimmed = organizationId?.trim();
8754
+ return trimmed ? ["--organization-id", trimmed] : [];
8755
+ }
8531
8756
  async function setup(args, io) {
8532
8757
  const [agent, ...rest] = args;
8533
8758
  const apiUrl = resolveApiUrl(void 0, io.env);
8534
8759
  switch (agent) {
8535
8760
  case "claude": {
8536
- const { values } = parseArgs2({ args: rest, options: { scope: { type: "string", default: "user" }, "project-id": { type: "string" }, "organization-id": { type: "string" }, "model-discovery": { type: "boolean", default: false } }, strict: true });
8761
+ 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 });
8537
8762
  if (values.scope !== "user" && values.scope !== "project") throw new CliError("--scope must be user or project");
8538
8763
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8539
8764
  const file = claudeSettingsPath(values.scope);
8540
8765
  const { settings, warnings } = mergeClaudeSettings(readJson(file), {
8541
8766
  baseUrl: anthropicBaseUrl(apiUrl),
8542
- tokenHelper: shellCommand(resolveSelfCommand(), "token"),
8767
+ tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
8543
8768
  projectId: values["project-id"],
8544
8769
  modelDiscovery: values["model-discovery"]
8545
8770
  });
@@ -8550,7 +8775,7 @@ async function setup(args, io) {
8550
8775
  return ExitCode.Ok;
8551
8776
  }
8552
8777
  case "opencode": {
8553
- const { values } = parseArgs2({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
8778
+ const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
8554
8779
  const surface = surfaceOf(values.surface, "anthropic");
8555
8780
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8556
8781
  const baseUrl = surfaceBaseUrl(apiUrl, surface);
@@ -8564,12 +8789,12 @@ async function setup(args, io) {
8564
8789
  writeJsonAtomic(file, mergeOpencodeConfig(readJson(file), { baseUrl, surface, models }));
8565
8790
  io.stdout(`OpenCode provider divergent-${surface} \u2192 ${baseUrl}. Wrote ${file}.`);
8566
8791
  io.stdout("Keep DIVERGENT_TOKEN fresh for the session (user tokens expire; the login refreshes):");
8567
- io.stdout(' export DIVERGENT_TOKEN="$(diverge token)"');
8792
+ io.stdout(` export DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})"`);
8568
8793
  return ExitCode.Ok;
8569
8794
  }
8570
8795
  case "codex": {
8571
- const { values } = parseArgs2({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
8572
- const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"] });
8796
+ const { values } = parseArgs3({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
8797
+ const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"], organizationId: values["organization-id"] });
8573
8798
  if (values.print) {
8574
8799
  io.stdout(block.trimEnd());
8575
8800
  return ExitCode.Ok;
@@ -8584,7 +8809,7 @@ async function setup(args, io) {
8584
8809
  return ExitCode.Ok;
8585
8810
  }
8586
8811
  case "dsh": {
8587
- const { values } = parseArgs2({ args: rest, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" }, into: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
8812
+ 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 });
8588
8813
  const surface = surfaceOf(values.surface, "openai");
8589
8814
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8590
8815
  if (!values.print) await skillsFor(io, apiUrl, "dsh");
@@ -8593,13 +8818,13 @@ async function setup(args, io) {
8593
8818
  io.stdout(overlay.trimEnd());
8594
8819
  return ExitCode.Ok;
8595
8820
  }
8596
- const file = values.into ?? path11.join(dshHome(io.env), `divergent-${surface}.yml`);
8821
+ const file = values.into ?? path12.join(dshHome(io.env), `divergent-${surface}.yml`);
8597
8822
  writeTextAtomic(file, overlay);
8598
8823
  io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
8599
8824
  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.");
8600
8825
  syncDivergentPreset(io, findOnPath("dsh", io.env), dshHome(io.env));
8601
8826
  io.stdout("Boot a profile with the overlay and the token in its environment (user tokens expire; the login refreshes):");
8602
- io.stdout(` DIVERGENT_TOKEN="$(diverge token)" dsh --profile headless --patch ${file} "<task>"`);
8827
+ io.stdout(` DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})" dsh --profile headless --patch ${file} "<task>"`);
8603
8828
  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`);
8604
8829
  return ExitCode.Ok;
8605
8830
  }
@@ -8617,7 +8842,7 @@ ${agentsUsage}`);
8617
8842
  const terminator = rest.indexOf("--");
8618
8843
  const flags = terminator === -1 ? rest : rest.slice(0, terminator);
8619
8844
  const passthrough = terminator === -1 ? [] : rest.slice(terminator + 1);
8620
- const { values } = parseArgs2({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
8845
+ const { values } = parseArgs3({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
8621
8846
  const surface = surfaceOf(values.surface, "openai");
8622
8847
  const apiUrl = resolveApiUrl(void 0, io.env);
8623
8848
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
@@ -8626,7 +8851,7 @@ ${agentsUsage}`);
8626
8851
  await skillsFor(io, apiUrl, "dsh");
8627
8852
  const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
8628
8853
  const home = dshHome(io.env);
8629
- const settingsPath = path11.join(home, "settings.yaml");
8854
+ const settingsPath = path12.join(home, "settings.yaml");
8630
8855
  let pinned;
8631
8856
  try {
8632
8857
  pinned = dshSettingsDefaultModel(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null, surface);
@@ -8635,22 +8860,22 @@ ${agentsUsage}`);
8635
8860
  }
8636
8861
  const served = servedIds(overlay);
8637
8862
  if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
8638
- const overlayPath = path11.join(home, `divergent-${surface}.yml`);
8863
+ const overlayPath = path12.join(home, `divergent-${surface}.yml`);
8639
8864
  writeTextAtomic(overlayPath, overlay);
8640
8865
  syncDivergentPreset(io, binary, home);
8641
8866
  io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
8642
8867
  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>"');
8643
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
8868
+ const source = tokenSource(io.env, apiUrl);
8644
8869
  return runDsh({
8645
8870
  binary,
8646
8871
  overlayPath,
8647
- credentialsPath: path11.join(home, ".credentials.yaml"),
8872
+ credentialsPath: path12.join(home, ".credentials.yaml"),
8648
8873
  args: passthrough,
8649
8874
  env: io.env,
8650
8875
  initialToken: token.access_token,
8651
8876
  freshToken: async () => {
8652
8877
  try {
8653
- return (await getUserToken(source, 120)).access_token;
8878
+ return (await getUserToken(source, { freshForSeconds: 120, organizationId: values["organization-id"] })).access_token;
8654
8879
  } catch (error) {
8655
8880
  io.stderr(`warning: could not refresh the platform token: ${error instanceof Error ? error.message : String(error)}`);
8656
8881
  return null;
@@ -8659,7 +8884,7 @@ ${agentsUsage}`);
8659
8884
  });
8660
8885
  }
8661
8886
  async function status(args, io) {
8662
- const { values } = parseArgs2({ args, options: { "organization-id": { type: "string" } }, strict: true });
8887
+ const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
8663
8888
  const apiUrl = resolveApiUrl(void 0, io.env);
8664
8889
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8665
8890
  const claims = tokenClaims(token.access_token);
@@ -8673,19 +8898,19 @@ async function status(args, io) {
8673
8898
  return ExitCode.Ok;
8674
8899
  }
8675
8900
  async function rotate(args, io) {
8676
- const { values } = parseArgs2({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
8901
+ const { values } = parseArgs3({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
8677
8902
  const interval = Number(values.interval);
8678
8903
  if (!Number.isFinite(interval) || interval <= 0) throw new CliError("--interval must be a positive number of seconds");
8679
8904
  const apiUrl = resolveApiUrl(void 0, io.env);
8680
8905
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8681
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
8906
+ const source = tokenSource(io.env, apiUrl);
8682
8907
  const tokenFilePath = values.into ?? defaultTokenFilePath();
8683
8908
  io.stderr(`Keeping ${tokenFilePath} fresh as you (organization ${tokenClaims(token.access_token).organizationId})`);
8684
8909
  return rotateTokenFile({
8685
8910
  tokenFile: tokenFilePath,
8686
8911
  intervalSeconds: interval,
8687
8912
  once: values.once,
8688
- freshToken: async () => (await getUserToken(source, Math.max(interval, 60) + 60)).access_token,
8913
+ freshToken: async () => (await getUserToken(source, { freshForSeconds: Math.max(interval, 60) + 60, organizationId: values["organization-id"] })).access_token,
8689
8914
  report: (line) => io.stderr(line),
8690
8915
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
8691
8916
  });
@@ -8718,184 +8943,30 @@ ${agentsUsage}`);
8718
8943
  }
8719
8944
  }
8720
8945
 
8721
- // src/commands/login.ts
8722
- import { parseArgs as parseArgs3 } from "node:util";
8723
-
8724
- // src/auth/organizations.ts
8725
- function chooseOrganization(input) {
8726
- const memberships = input.memberships;
8727
- const flag = input.flag?.trim();
8728
- if (flag) {
8729
- const match = memberships.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
8730
- if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
8731
- if (input.platformActor) return { kind: "chosen", organizationId: flag, reason: "flag" };
8732
- throw new CliError(`Not one of your organizations: ${flag}. Yours: ${memberships.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
8733
- }
8734
- const remembered = input.remembered?.trim();
8735
- if (remembered) {
8736
- const match = memberships.find((organization) => organization.id === remembered);
8737
- if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
8738
- if (input.platformActor) return { kind: "chosen", organizationId: remembered, reason: "remembered" };
8739
- }
8740
- if (memberships.length === 1) return { kind: "chosen", organizationId: memberships[0].id, name: memberships[0].name, reason: "only" };
8741
- if (memberships.length === 0) return { kind: "none" };
8742
- return { kind: "ask", options: memberships };
8743
- }
8744
- function organizationMenu(options) {
8745
- return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
8746
- }
8747
- function pickFromMenu(answer, options) {
8748
- const trimmed = answer.trim();
8749
- const index = Number(trimmed);
8750
- if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
8751
- const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
8752
- if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
8753
- return match;
8754
- }
8755
-
8756
- // src/auth/userinfo.ts
8757
- async function fetchUserInfo(baseUrl, accessToken) {
8758
- return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
8759
- }
8760
- async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
8761
- try {
8762
- const info = await fetchUserInfo(baseUrl, accessToken);
8763
- return options.withSub ? `${info.email} (${info.sub})` : info.email;
8764
- } catch (error) {
8765
- return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
8766
- }
8767
- }
8768
-
8769
- // src/prompt.ts
8770
- import readline from "node:readline";
8771
- function ask(question) {
8772
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
8773
- return new Promise((resolve) => rl.question(question, (answer) => {
8774
- rl.close();
8775
- resolve(answer);
8776
- }));
8777
- }
8778
-
8779
- // src/commands/login.ts
8780
- var DEFAULT_SCOPES = "mcp:read mcp:execute inference:execute";
8781
- var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
8782
-
8783
- Sign in through the browser, then choose where to act. The browser only
8784
- authenticates the person; the organization is chosen here afterwards \u2014 the
8785
- one named by --organization-id (id or name), else the last login's, else the
8786
- only membership, else a menu \u2014 and the login is bound to it on the platform.
8787
- Switch later with: diverge org use <id|name>.
8788
- --as <email> names the account: the platform signs that account in instead of
8789
- reusing whatever session the browser holds, and issues nothing to any other.
8790
- Without --as, the account of the last login is used; switching is explicit.
8791
- Without --scopes, the scopes of the last login are asked for again; a first
8792
- login asks for the CLI's own (mcp:read mcp:execute inference:execute).`;
8793
- function resolveScopes(flag, config) {
8794
- const explicit = flag?.trim();
8795
- if (explicit) return explicit.split(/\s+/).join(" ");
8796
- const remembered = config.scopes?.trim();
8797
- return remembered ? remembered.split(/\s+/).join(" ") : DEFAULT_SCOPES;
8798
- }
8799
- function resolveLoginHint(flag, config) {
8800
- const explicit = flag?.trim().toLowerCase();
8801
- if (explicit) return explicit;
8802
- const remembered = config.account?.trim().toLowerCase();
8803
- return remembered || void 0;
8804
- }
8805
- async function runLogin(args, io) {
8806
- const { values } = parseArgs3({
8807
- args,
8808
- options: {
8809
- "organization-id": { type: "string" },
8810
- scopes: { type: "string" },
8811
- as: { type: "string" },
8812
- "no-browser": { type: "boolean", default: false },
8813
- "api-url": { type: "string" }
8814
- },
8815
- strict: true,
8816
- allowPositionals: false
8817
- });
8818
- const config = readConfig(io.env);
8819
- const apiUrl = resolveApiUrl(values["api-url"], io.env, config);
8820
- if (values["api-url"]) writeConfig({ ...config, apiUrl }, io.env);
8821
- const loginHint = resolveLoginHint(values.as, config);
8822
- if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
8823
- const scopes = resolveScopes(values.scopes, config);
8824
- io.stdout(`Asking for scopes: ${scopes}${values.scopes ? "" : config.scopes ? " (the last login; pass --scopes to change)" : ""}.`);
8825
- const token = await login({
8826
- baseUrl: apiUrl,
8827
- clientId: clientId(io.env),
8828
- scopes,
8829
- openBrowser: !values["no-browser"],
8830
- tokenFile: tokenFile(io.env),
8831
- loginHint,
8832
- print: io.stdout
8833
- });
8834
- const claims = tokenClaims(token.access_token);
8835
- let info;
8836
- try {
8837
- info = await fetchUserInfo(apiUrl, token.access_token);
8838
- } catch (error) {
8839
- throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
8840
- }
8841
- io.stdout(`Signed in as: ${info.email}`);
8842
- const bound = await bindChosenOrganization(io, apiUrl, values["organization-id"], config.organization, info);
8843
- const remaining = Math.max(0, Math.round(bound.token.expires_at - Date.now() / 1e3));
8844
- io.stdout(`Logged in to ${apiUrl}; token cached (${remaining}s to expiry, auto-refreshes).`);
8845
- writeConfig({ ...readConfig(io.env), account: info.email, scopes, organization: bound.organizationId }, io.env);
8846
- const granted = new Set(tokenClaims(bound.token.access_token).scopes ?? []);
8847
- const missing = scopes.split(" ").filter((scope) => scope && !granted.has(scope));
8848
- if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
8849
- return 0;
8850
- }
8851
- async function bindChosenOrganization(io, apiUrl, flag, remembered, info) {
8852
- const memberships = info.organizations ?? [];
8853
- let choice = chooseOrganization({ flag, remembered, memberships, platformActor: info.platformActor });
8854
- if (choice.kind === "none") {
8855
- throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
8856
- }
8857
- if (choice.kind === "ask") {
8858
- if (!io.interactive) {
8859
- throw new CliError(`You belong to several organizations; pass --organization-id <id|name>:
8860
- ${organizationMenu(choice.options).join("\n")}`);
8861
- }
8862
- io.stdout("Choose the organization to act in:");
8863
- for (const line of organizationMenu(choice.options)) io.stdout(line);
8864
- const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
8865
- choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
8866
- }
8867
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
8868
- const token = await bindOrganization(source, choice.organizationId);
8869
- const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
8870
- const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
8871
- io.stdout(`Organization: ${label}${why}`);
8872
- return { organizationId: choice.organizationId, token };
8873
- }
8874
-
8875
8946
  // src/commands/org.ts
8876
8947
  import { parseArgs as parseArgs4 } from "node:util";
8877
8948
  var orgUsage = `diverge org [use <id|name>]
8878
8949
 
8879
- Without a verb: the organizations you may act in, the bound one marked.
8880
- use <id|name>: bind the login to that organization on the platform, without a
8881
- browser; every later command acts there. A platform actor may name an
8882
- organization they are not a member of.`;
8950
+ Without a verb: the organizations you may act in, the default marked.
8951
+ use <id|name>: make it the default \u2014 where every command that names no
8952
+ organization acts, in every shell on this machine that shares the login. It
8953
+ is a local preference: the platform is asked only for a token there, which
8954
+ proves you may act in it. For one command, pass --organization-id <id> (or
8955
+ set DIVERGENT_ORG_ID) instead. A platform actor may act in, and so sees, every
8956
+ organization.`;
8883
8957
  async function runOrg(args, io) {
8884
8958
  const { positionals } = parseArgs4({ args, options: {}, strict: true, allowPositionals: true });
8885
- const apiUrl = resolveApiUrl(void 0, io.env);
8886
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
8887
- const token = await getUserToken(source);
8888
- const info = await fetchUserInfo(apiUrl, token.access_token);
8889
- const memberships = info.organizations ?? [];
8890
- const current = tokenClaims(token.access_token).organizationId ?? void 0;
8959
+ const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
8960
+ const info = await fetchUserInfo(source.baseUrl, (await getUserToken(source)).access_token);
8961
+ const organizations = info.organizations ?? [];
8962
+ const current = requireLogin(source).organization;
8891
8963
  const verb = positionals[0];
8892
8964
  if (!verb) {
8893
- if (memberships.length === 0 && !current) {
8965
+ if (organizations.length === 0) {
8894
8966
  io.stdout("You are not a member of any organization.");
8895
8967
  return 0;
8896
8968
  }
8897
- for (const organization of memberships) io.stdout(`${organization.id === current ? "*" : " "} ${organization.name} (${organization.id})`);
8898
- if (current && !memberships.some((organization) => organization.id === current)) io.stdout(`* ${current} (not a membership; acting as the platform)`);
8969
+ for (const organization of organizations) io.stdout(`${organization.id === current ? "*" : " "} ${organization.name} (${organization.id})`);
8899
8970
  return 0;
8900
8971
  }
8901
8972
  if (verb !== "use") throw new CliError(`Unknown verb: ${verb}
@@ -8903,35 +8974,37 @@ async function runOrg(args, io) {
8903
8974
  ${orgUsage}`);
8904
8975
  const target = positionals[1];
8905
8976
  if (!target) throw new CliError("org use needs an organization id or name");
8906
- const choice = chooseOrganization({ flag: target, memberships, platformActor: info.platformActor });
8977
+ const choice = chooseOrganization({ flag: target, organizations });
8907
8978
  if (choice.kind !== "chosen") throw new CliError("org use needs an organization id or name");
8908
- const bound = await bindOrganization(source, choice.organizationId);
8909
- writeConfig({ ...readConfig(io.env), organization: choice.organizationId }, io.env);
8910
- io.stdout(`Organization: ${choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId}; scopes ${(tokenClaims(bound.access_token).scopes ?? []).join(" ") || "none"}`);
8979
+ const token = await useOrganization(source, choice.organizationId);
8980
+ io.stdout(`Default organization: ${choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId}; scopes ${(tokenClaims(token.access_token).scopes ?? []).join(" ") || "none"}`);
8911
8981
  return 0;
8912
8982
  }
8913
8983
 
8914
8984
  // src/commands/logout.ts
8915
- var logoutUsage = "diverge logout\n\nRevoke the login's session on the platform and forget it locally.";
8985
+ var logoutUsage = "diverge logout\n\nRevoke this deployment's login on the platform and forget it locally, with every token issued from it. Logins to other deployments are kept.";
8916
8986
  async function runLogout(_args, io) {
8917
- const file = tokenFile(io.env);
8918
- const token = loadUserToken(file);
8987
+ const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
8988
+ const token = loadLogin(source);
8919
8989
  if (!token) {
8920
- io.stdout("Not logged in.");
8990
+ io.stdout(`Not logged in to ${source.baseUrl}.`);
8921
8991
  return 0;
8922
8992
  }
8923
8993
  if (token.refresh_token) {
8924
8994
  try {
8925
- await revokeRefreshToken(token.base_url, clientId(io.env), token.refresh_token);
8995
+ await revokeRefreshToken(source.baseUrl, source.clientId, token.refresh_token);
8926
8996
  } catch (error) {
8927
- io.stderr(`warning: could not revoke the session on ${token.base_url}: ${error instanceof Error ? error.message : String(error)}`);
8997
+ io.stderr(`warning: could not revoke the session on ${source.baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
8928
8998
  }
8929
8999
  }
8930
- deleteUserToken(file);
8931
- io.stdout(`Logged out of ${token.base_url}${token.base_url === resolveApiUrl(void 0, io.env) ? "" : " (a different deployment than the configured one)"}.`);
9000
+ forgetLogin(source);
9001
+ io.stdout(`Logged out of ${source.baseUrl}.`);
8932
9002
  return 0;
8933
9003
  }
8934
9004
 
9005
+ // src/commands/status.ts
9006
+ import path13 from "node:path";
9007
+
8935
9008
  // src/tools/settings.ts
8936
9009
  var BRIDGE_SCOPES = "mcp:read mcp:execute";
8937
9010
  var LOGIN_HINT = "diverge login";
@@ -8939,21 +9012,19 @@ function loadToolsSettings(options, env = process.env) {
8939
9012
  const apiUrl = resolveApiUrl(void 0, env);
8940
9013
  const projectId = options.projectId?.trim() || env.DIVERGENT_PROJECT_ID?.trim() || void 0;
8941
9014
  let organizationId = options.organizationId?.trim() || env.DIVERGENT_ORG_ID?.trim() || "";
8942
- const cached = loadUserToken(tokenFile(env));
8943
- if (cached && cached.base_url === apiUrl) {
8944
- organizationId ||= tokenClaims(cached.access_token).organizationId?.trim() || "";
8945
- if (!organizationId) throw new CliError(`The login carries no organization. Pass --organization-id or sign in to one: ${LOGIN_HINT} --organization-id <id>`);
9015
+ const cached = loadLogin(tokenSource(env, apiUrl));
9016
+ if (cached) {
9017
+ organizationId ||= cached.organization ?? "";
9018
+ if (!organizationId) throw new CliError(`The login has no default organization. Pass --organization-id or choose one: diverge org use <id|name>`);
8946
9019
  return { apiUrl, organizationId, projectId, mode: "operator", clientId: clientId(env) };
8947
9020
  }
8948
- if (!organizationId) throw new CliError("No organization configured. Pass --organization-id or set DIVERGENT_ORG_ID.");
8949
9021
  const serviceClientId = env.DIVERGENT_CLIENT_ID?.trim() ?? "";
8950
9022
  const serviceClientSecret = env.DIVERGENT_CLIENT_SECRET?.trim() ?? "";
8951
9023
  const delegatedUserId = env.DIVERGENT_USER_ID?.trim() ?? "";
8952
9024
  if (!serviceClientId || !serviceClientSecret) {
8953
- throw new CliError(`Not signed in for ${apiUrl} and no service credentials configured.
8954
- Sign in as yourself: ${LOGIN_HINT}
8955
- Or set DIVERGENT_CLIENT_ID and DIVERGENT_CLIENT_SECRET.`);
9025
+ throw noLogin(apiUrl, "Or, unattended, set DIVERGENT_CLIENT_ID, DIVERGENT_CLIENT_SECRET, DIVERGENT_USER_ID and DIVERGENT_ORG_ID.");
8956
9026
  }
9027
+ if (!organizationId) throw new CliError("Service credentials act in the organization named: pass --organization-id or set DIVERGENT_ORG_ID.");
8957
9028
  if (!delegatedUserId) {
8958
9029
  throw new CliError(`DIVERGENT_USER_ID is required for service-credential execution: tools run under that human's RBAC, and without it the service account executes app-only, which governed tools deliberately restrict.
8959
9030
  Signing in as yourself avoids this entirely: ${LOGIN_HINT}`);
@@ -8971,10 +9042,9 @@ function actingAsLine(settings, env = process.env) {
8971
9042
  actingAs ??= (async () => {
8972
9043
  let accessToken;
8973
9044
  try {
8974
- accessToken = (await getUserToken({ tokenFile: tokenFile(env), baseUrl: settings.apiUrl, clientId: settings.clientId })).access_token;
9045
+ accessToken = (await getUserToken(tokenSource(env, settings.apiUrl))).access_token;
8975
9046
  } catch (error) {
8976
- const sub = tokenClaims(loadUserToken(tokenFile(env))?.access_token ?? "").sub;
8977
- return `Acting as: ${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
9047
+ return `Acting as: unknown user (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
8978
9048
  }
8979
9049
  return `Acting as: ${await describeAccount(settings.apiUrl, accessToken, tokenClaims(accessToken).sub)}`;
8980
9050
  })();
@@ -8994,54 +9064,73 @@ function describeToolExecution(env = process.env) {
8994
9064
  var statusUsage = "diverge status\n\nShow the deployment, the cached login, whose account it is, what it carries, and who a tool call would run as. One request: the account behind the login.";
8995
9065
  async function runStatus(_args, io) {
8996
9066
  const apiUrl = resolveApiUrl(void 0, io.env);
9067
+ const source = tokenSource(io.env, apiUrl);
8997
9068
  io.stdout(`diverge ${VERSION}`);
8998
9069
  io.stdout(`Deployment: ${apiUrl}`);
8999
- io.stdout(`State: ${configDir(io.env)}`);
9000
- const token = loadUserToken(tokenFile(io.env));
9001
- if (!token) {
9002
- io.stdout("Not logged in. Run: diverge login");
9070
+ io.stdout(`State: ${path13.dirname(source.loginFile)}`);
9071
+ const record = loadLogin(source);
9072
+ if (!record) {
9073
+ io.stdout(`Not logged in. Run: diverge login${apiUrl === DEFAULT_API_URL ? "" : ` --api-url ${apiUrl}`}`);
9003
9074
  io.stdout(describeToolExecution(io.env));
9004
9075
  return 1;
9005
9076
  }
9006
- const claims = tokenClaims(token.access_token);
9007
- const remaining = Math.round(token.expires_at - Date.now() / 1e3);
9008
- const mismatch = token.base_url !== apiUrl ? ` (for ${token.base_url}, not the configured deployment)` : "";
9009
- io.stdout(`Login: ${isFresh(token, 0) ? `valid for ${remaining}s` : "expired (auto-refreshes)"}; refresh token ${token.refresh_token ? "present" : "absent"}${mismatch}`);
9010
- const account = await signedInAs(token, io);
9077
+ io.stdout(`Login: ${describeSignIn(record)}`);
9078
+ const account = await signedInAs(source);
9011
9079
  io.stdout(`Signed in as: ${account.line}`);
9012
- const organization = account.organizations?.find((candidate) => candidate.id === claims.organizationId);
9013
- io.stdout(`Organization: ${claims.organizationId ? `${organization ? `${organization.name} (${claims.organizationId})` : claims.organizationId}` : "none (run: diverge org use <id|name>)"}`);
9014
- io.stdout(`Scopes: ${(claims.scopes ?? []).join(" ") || "none"}`);
9080
+ const organization = account.organizations?.find((candidate) => candidate.id === record.organization);
9081
+ io.stdout(`Organization: ${record.organization ? `${organization ? `${organization.name} (${record.organization})` : record.organization} (the default; commands may name another)` : "none (run: diverge org use <id|name>)"}`);
9082
+ io.stdout(`Scopes: ${account.scopes ?? "unknown"}`);
9015
9083
  io.stdout(describeToolExecution(io.env));
9016
9084
  return 0;
9017
9085
  }
9018
- async function signedInAs(cached, io) {
9019
- const sub = tokenClaims(cached.access_token).sub;
9086
+ async function signedInAs(source) {
9020
9087
  let accessToken;
9021
9088
  try {
9022
- accessToken = (await getUserToken({ tokenFile: tokenFile(io.env), baseUrl: cached.base_url, clientId: clientId(io.env) })).access_token;
9089
+ accessToken = (await getUserToken(source)).access_token;
9023
9090
  } catch (error) {
9024
- return { line: `${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})` };
9091
+ return { line: `unknown (${error instanceof Error ? error.message : String(error)})` };
9025
9092
  }
9093
+ const claims = tokenClaims(accessToken);
9094
+ const scopes = (claims.scopes ?? []).join(" ") || "none";
9026
9095
  try {
9027
- const info = await fetchUserInfo(cached.base_url, accessToken);
9028
- return { line: `${info.email} (${info.sub})`, organizations: info.organizations };
9096
+ const info = await fetchUserInfo(source.baseUrl, accessToken);
9097
+ return { line: `${info.email} (${info.sub})`, organizations: info.organizations, scopes };
9029
9098
  } catch (error) {
9030
- return { line: `${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})` };
9099
+ return { line: `${claims.sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`, scopes };
9031
9100
  }
9032
9101
  }
9102
+ function describeSignIn(record, nowMs = Date.now()) {
9103
+ const { idle, cap } = loginExpiry(record);
9104
+ if (idle === null) return "refresh token present";
9105
+ const at = (seconds) => new Date(seconds * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
9106
+ const left = (seconds) => {
9107
+ const remaining = seconds - nowMs / 1e3;
9108
+ const hours = Math.floor(remaining / 3600);
9109
+ return hours >= 24 ? `${Math.floor(hours / 24)}d ${hours % 24}h` : `${hours}h ${Math.floor(remaining % 3600 / 60)}m`;
9110
+ };
9111
+ if (idle <= nowMs / 1e3) return `sign-in expired at ${at(idle)} (run: diverge login)`;
9112
+ const idleLine = `renews while used; idle until ${at(idle)} (${left(idle)} left)`;
9113
+ if (cap === null || cap <= idle) return cap === null ? idleLine : `ends ${at(idle)} (${left(idle)} left; 30-day limit, run: diverge login then)`;
9114
+ return `${idleLine}; ends at the latest ${at(cap)} (${left(cap)} left)`;
9115
+ }
9033
9116
 
9034
9117
  // src/commands/token.ts
9035
- var tokenUsage = "diverge token\n\nPrint a fresh access token and nothing else: the credential helper for Claude Code and Codex.";
9036
- async function runToken(_args, io) {
9037
- const apiUrl = resolveApiUrl(void 0, io.env);
9038
- const token = await getUserToken({ tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) });
9118
+ import { parseArgs as parseArgs5 } from "node:util";
9119
+ var tokenUsage = `diverge token [--organization-id <id>]
9120
+
9121
+ Print a fresh access token and nothing else: the credential helper for Claude
9122
+ Code, Codex and forge. The token is for the organization named by
9123
+ --organization-id or DIVERGENT_ORG_ID, else the login's default.`;
9124
+ async function runToken(args, io) {
9125
+ const { values } = parseArgs5({ args, options: { "organization-id": { type: "string" } }, strict: true, allowPositionals: false });
9126
+ const organizationId = values["organization-id"]?.trim() || io.env.DIVERGENT_ORG_ID?.trim() || void 0;
9127
+ const token = await getUserToken(tokenSource(io.env, resolveApiUrl(void 0, io.env)), { organizationId });
9039
9128
  io.stdout(token.access_token);
9040
9129
  return 0;
9041
9130
  }
9042
9131
 
9043
9132
  // src/commands/tools.ts
9044
- import { parseArgs as parseArgs5 } from "node:util";
9133
+ import { parseArgs as parseArgs6 } from "node:util";
9045
9134
 
9046
9135
  // src/mcp/bridge.ts
9047
9136
  import readline2 from "node:readline";
@@ -9065,26 +9154,26 @@ var ToolsClient = class {
9065
9154
  serviceToken = null;
9066
9155
  async token() {
9067
9156
  if (this.settings.mode === "operator") {
9068
- try {
9069
- return (await getUserToken({ tokenFile: tokenFile(this.env), baseUrl: this.settings.apiUrl, clientId: this.settings.clientId })).access_token;
9070
- } catch (error) {
9071
- throw new CliError(`${error instanceof Error ? error.message : String(error)}
9072
- Sign in with: ${LOGIN_HINT}`);
9073
- }
9157
+ return (await getUserToken(tokenSource(this.env, this.settings.apiUrl), { organizationId: this.settings.organizationId })).access_token;
9074
9158
  }
9075
9159
  if (this.serviceToken && Date.now() < this.serviceToken.expiresAtMs) return this.serviceToken.value;
9076
- const response = await this.fetchImpl(tokenEndpoint(this.settings.apiUrl), {
9077
- method: "POST",
9078
- headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
9079
- body: JSON.stringify({
9080
- grant_type: "client_credentials",
9081
- client_id: this.settings.serviceClientId,
9082
- client_secret: this.settings.serviceClientSecret,
9083
- scopes: BRIDGE_SCOPES,
9084
- organization_id: this.settings.organizationId
9085
- }),
9086
- signal: AbortSignal.timeout(3e4)
9087
- });
9160
+ let response;
9161
+ try {
9162
+ response = await this.fetchImpl(tokenEndpoint(this.settings.apiUrl), {
9163
+ method: "POST",
9164
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
9165
+ body: JSON.stringify({
9166
+ grant_type: "client_credentials",
9167
+ client_id: this.settings.serviceClientId,
9168
+ client_secret: this.settings.serviceClientSecret,
9169
+ scopes: BRIDGE_SCOPES,
9170
+ organization_id: this.settings.organizationId
9171
+ }),
9172
+ signal: AbortSignal.timeout(3e4)
9173
+ });
9174
+ } catch (error) {
9175
+ throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
9176
+ }
9088
9177
  if (!response.ok) throw new CliError(`Token acquisition failed (${response.status}): ${(await response.text()).slice(0, 400)}`);
9089
9178
  const body = await response.json();
9090
9179
  if (!body.access_token) throw new CliError("Token response contained no access_token");
@@ -9097,10 +9186,10 @@ var ToolsClient = class {
9097
9186
  if (this.settings.mode === "service") headers["x-divergent-user-id"] = this.settings.delegatedUserId;
9098
9187
  return headers;
9099
9188
  }
9100
- scoped(path13) {
9189
+ scoped(path15) {
9101
9190
  const query = new URLSearchParams({ organizationId: this.settings.organizationId });
9102
9191
  if (this.settings.projectId) query.set("projectId", this.settings.projectId);
9103
- return `${this.settings.apiUrl}${path13}?${query.toString()}`;
9192
+ return `${this.settings.apiUrl}${path15}?${query.toString()}`;
9104
9193
  }
9105
9194
  async send(method, url, body) {
9106
9195
  return this.request(method, url, body, void 0);
@@ -9112,9 +9201,10 @@ var ToolsClient = class {
9112
9201
  */
9113
9202
  async request(method, url, body, refused) {
9114
9203
  for (let attempt = 1; attempt <= 2; attempt += 1) {
9204
+ const headers = await this.headers();
9115
9205
  let response;
9116
9206
  try {
9117
- response = await this.fetchImpl(url, { method, headers: await this.headers(), body: body === void 0 ? void 0 : JSON.stringify(body), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
9207
+ response = await this.fetchImpl(url, { method, headers, body: body === void 0 ? void 0 : JSON.stringify(body), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
9118
9208
  } catch (error) {
9119
9209
  throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
9120
9210
  }
@@ -9146,8 +9236,32 @@ var ToolsClient = class {
9146
9236
  const body = await this.send("GET", this.scoped("/api/plugins/functions/tools"));
9147
9237
  return body.tools ?? [];
9148
9238
  }
9149
- callTool(name, args) {
9150
- return this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), { toolName: name, arguments: args ?? {} });
9239
+ /**
9240
+ * One tool, looked up at its source when the catalog does not list it yet —
9241
+ * a tool deployed after its server's catalog was read. A 404 is an answer.
9242
+ */
9243
+ describeTool(name) {
9244
+ return this.request("GET", this.scoped(`/api/plugins/functions/tools/${encodeURIComponent(name)}`), void 0, (status2, text) => {
9245
+ if (status2 !== 404) return void 0;
9246
+ try {
9247
+ const parsed = JSON.parse(text);
9248
+ if (typeof parsed.error === "string" && parsed.error) return { notFound: parsed.error };
9249
+ } catch {
9250
+ }
9251
+ return { notFound: `No tool named '${name}' is available.` };
9252
+ });
9253
+ }
9254
+ /**
9255
+ * One call. The platform answers a completed call with its invocation
9256
+ * envelope, `{status: 'succeeded', invocationId?, result}`; the tool's own
9257
+ * payload is `result` inside it. Every caller wants the payload, so it is
9258
+ * unwrapped here, once — a pending approval passes through as it is.
9259
+ */
9260
+ async callTool(name, args) {
9261
+ const body = await this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), { toolName: name, arguments: args ?? {} });
9262
+ const outcome = body.result;
9263
+ if (outcome?.status !== "succeeded") return body;
9264
+ return { ...body, result: outcome.result };
9151
9265
  }
9152
9266
  /** The read half of raise-and-read (ADR 0047); anyone but the raiser sees a 404. */
9153
9267
  getInvocation(invocationId) {
@@ -9369,7 +9483,7 @@ async function waitForInvocation(id, options, deps) {
9369
9483
 
9370
9484
  // src/tools/call-args.ts
9371
9485
  import fs9 from "node:fs";
9372
- function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8")) {
9486
+ async function resolveCallArgs(options, stdin = readStdin) {
9373
9487
  if (options.argsJson !== void 0 && options.argsJson !== "{}" && options.argsFile !== void 0) {
9374
9488
  throw new CliError("give the arguments once: --args-json or --args-file");
9375
9489
  }
@@ -9384,7 +9498,7 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
9384
9498
  }
9385
9499
  } else if (options.argsJson === "-") {
9386
9500
  source = "--args-json - (stdin)";
9387
- text = readStdin();
9501
+ text = await stdin();
9388
9502
  } else {
9389
9503
  source = "--args-json";
9390
9504
  text = options.argsJson ?? "{}";
@@ -9398,6 +9512,11 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
9398
9512
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(`${source} must be a JSON object`);
9399
9513
  return parsed;
9400
9514
  }
9515
+ async function readStdin() {
9516
+ const chunks = [];
9517
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
9518
+ return Buffer.concat(chunks).toString("utf8");
9519
+ }
9401
9520
 
9402
9521
  // src/tools/batch.ts
9403
9522
  function parseBatch(text) {
@@ -9432,21 +9551,29 @@ async function runOne(entry, deps) {
9432
9551
  if (body.success === false) return { tool: entry.tool, status: "failed", error: body.error || "Tool execution failed" };
9433
9552
  if (!isPending(body.result)) return { tool: entry.tool, status: "executed", result: body.result ?? null };
9434
9553
  const pending = { tool: entry.tool, status: "pending", invocationId: body.result.invocationId, decideAt: deps.decideAt(body.result) };
9554
+ deps.onPending?.(body.result);
9435
9555
  if (!deps.waitFor) return pending;
9436
9556
  const settled = await deps.waitFor(body.result);
9437
- switch (settled?.status) {
9557
+ return (settled && outcomeOf(entry.tool, pending.invocationId, settled)) ?? pending;
9558
+ }
9559
+ function outcomeOf(tool, invocationId, invocation) {
9560
+ switch (invocation.status) {
9438
9561
  case "succeeded":
9439
- return { tool: entry.tool, status: "executed", result: settled.result ?? null, invocationId: pending.invocationId };
9562
+ return { tool, status: "executed", result: invocation.result ?? null, invocationId };
9440
9563
  case "rejected":
9441
- return { tool: entry.tool, status: "rejected", invocationId: pending.invocationId, error: settled.decisionReason ?? void 0 };
9564
+ return { tool, status: "rejected", invocationId, error: invocation.decisionReason ?? void 0 };
9442
9565
  case "expired":
9443
- return { tool: entry.tool, status: "expired", invocationId: pending.invocationId };
9566
+ return { tool, status: "expired", invocationId };
9444
9567
  case "failed":
9445
- return { tool: entry.tool, status: "failed", invocationId: pending.invocationId, error: settled.errorMessage || "execution failed" };
9568
+ return { tool, status: "failed", invocationId, error: invocation.errorMessage || "execution failed" };
9446
9569
  default:
9447
- return pending;
9570
+ return null;
9448
9571
  }
9449
9572
  }
9573
+ function readBackOutcomeOf(tool, invocationId, invocation) {
9574
+ if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) return { tool, status: "pending", invocationId };
9575
+ return outcomeOf(tool, invocationId, invocation);
9576
+ }
9450
9577
  function worstExitCode(outcomes) {
9451
9578
  const has = (status2) => outcomes.some((outcome) => outcome.status === status2);
9452
9579
  if (has("failed")) return ExitCode.Failure;
@@ -9465,6 +9592,10 @@ function matchesCapability(selector, capability) {
9465
9592
  if (selector === capability) return true;
9466
9593
  return selector.endsWith("*") && capability.startsWith(selector.slice(0, -1));
9467
9594
  }
9595
+ function matchesFilter(filter, tool) {
9596
+ const needle = filter.toLowerCase();
9597
+ return tool.name.toLowerCase().includes(needle) || (tool.description ?? "").toLowerCase().includes(needle);
9598
+ }
9468
9599
  function formatToolList(tools) {
9469
9600
  if (tools.length === 0) return ["No tools available."];
9470
9601
  const nameWidth = Math.max(...tools.map((tool) => tool.name.length));
@@ -9523,38 +9654,29 @@ function formatPending(result, apiUrl, organizationId) {
9523
9654
  lines.push(`Read the outcome later: diverge tools invocation ${result.invocationId} --organization-id ${organizationId}`);
9524
9655
  return lines;
9525
9656
  }
9526
- function settledOutcome(invocation) {
9527
- switch (invocation.status) {
9528
- case "succeeded":
9529
- return { stdout: JSON.stringify(invocation.result ?? null, null, 2), exitCode: ExitCode.Ok };
9530
- case "rejected":
9531
- return { stderr: `Rejected by the operator${invocation.decisionReason ? `: ${invocation.decisionReason}` : "."}`, exitCode: ExitCode.Rejected };
9532
- case "expired":
9533
- return { stderr: "Expired before anyone decided. Raise a fresh call; the old id is final.", exitCode: ExitCode.Expired };
9534
- case "failed":
9535
- throw new CliError(`Approved, but execution failed: ${invocation.errorMessage || "unknown error"}`);
9536
- default:
9537
- throw new CliError(`Unexpected invocation status: ${invocation.status}`);
9538
- }
9539
- }
9540
9657
 
9541
9658
  // src/commands/tools.ts
9542
9659
  var toolsUsage = `diverge tools <list|capabilities|describe|call|run|invocation|bridge> [--organization-id <id>] [--project-id <id>] ...
9543
9660
 
9544
- list [--filter <substring>] [--capability <selector>] [--json]
9661
+ list [--filter <text>] [--capability <selector>] [--json]
9662
+ --filter matches tool names and descriptions, ignoring case
9545
9663
  capabilities [--json]
9546
9664
  describe <name> [--json]
9547
- call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>]
9665
+ call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>] [--json]
9548
9666
  arguments inline, from stdin (-), or from a file: PowerShell strips quotes inside inline JSON
9549
- run <file> [--wait] [--timeout <seconds>]
9667
+ --json prints exactly one JSON object on stdout, errors included:
9668
+ {tool, status: executed|pending|rejected|expired|failed, result?, error?, invocationId?, decideAt?}
9669
+ run <file|-> [--wait] [--timeout <seconds>]
9550
9670
  several calls from one file (a JSON array of {tool, args}, or one per line), in order,
9551
9671
  one JSON line of outcome each; exit code is the worst: 1 failed, 4 rejected, 5 expired, 3 pending
9552
9672
  invocation <id> [--wait] [--timeout <seconds>] [--json]
9673
+ --json prints the invocation record; the exit code is call's for the same status
9553
9674
  bridge [--tools <name,name>] (stdio MCP server; the allowlist keeps a harness to the tools it was given)
9554
9675
 
9555
9676
  Discover and execute platform tools as yourself. A write that needs a human is
9556
9677
  persisted as a pending invocation, decided in Compass, and read back here by
9557
- id. Exit codes: 0 executed, 3 pending, 4 rejected, 5 expired.`;
9678
+ id. Exit codes, the same for call, run and invocation, --json or not:
9679
+ 0 executed, 1 failed, 3 pending, 4 rejected, 5 expired.`;
9558
9680
  var SCOPE_OPTIONS = { "organization-id": { type: "string" }, "project-id": { type: "string" } };
9559
9681
  async function client(values, io) {
9560
9682
  const settings = loadToolsSettings({ organizationId: values["organization-id"], projectId: values["project-id"] }, io.env);
@@ -9562,14 +9684,72 @@ async function client(values, io) {
9562
9684
  io.stderr(await actingAsLine(settings, io.env));
9563
9685
  return { settings, client: new ToolsClient(settings, io.env) };
9564
9686
  }
9565
- function emit(io, outcome) {
9566
- if (outcome.stdout !== void 0) io.stdout(outcome.stdout);
9567
- if (outcome.stderr !== void 0) io.stderr(outcome.stderr);
9568
- return outcome.exitCode;
9569
- }
9570
9687
  var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9688
+ var CALL_OPTIONS = { ...SCOPE_OPTIONS, "args-json": { type: "string" }, "args-file": { type: "string" }, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" }, json: { type: "boolean", default: false } };
9689
+ function timeoutOf(seconds) {
9690
+ const timeoutMs = Number(seconds) * 1e3;
9691
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
9692
+ return timeoutMs;
9693
+ }
9694
+ function waiter(tools, timeoutMs) {
9695
+ return async (pending) => {
9696
+ const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
9697
+ return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
9698
+ };
9699
+ }
9700
+ async function call(rest, io) {
9701
+ const json = rest.includes("--json");
9702
+ let tool = "";
9703
+ let waited = "";
9704
+ let outcome;
9705
+ try {
9706
+ const { values, positionals } = parseArgs6({ args: rest, options: CALL_OPTIONS, strict: true, allowPositionals: true });
9707
+ tool = positionals[0] ?? "";
9708
+ if (!tool) throw new CliError("call needs a tool name");
9709
+ const args = await resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
9710
+ const timeoutMs = timeoutOf(values.timeout);
9711
+ if (values.wait) waited = values.timeout;
9712
+ const { settings, client: tools } = await client(values, io);
9713
+ outcome = await runOne({ tool, args }, {
9714
+ callTool: (name, callArgs) => tools.callTool(name, callArgs),
9715
+ decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
9716
+ ...json ? {} : { onPending: (pending) => formatPending(pending, settings.apiUrl, settings.organizationId).forEach((line) => io.stdout(line)) },
9717
+ ...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
9718
+ });
9719
+ } catch (error) {
9720
+ if (!json) throw error;
9721
+ outcome = { tool, status: "failed", error: error instanceof Error ? error.message : String(error) };
9722
+ }
9723
+ if (json) io.stdout(JSON.stringify(outcome, null, 2));
9724
+ else printOutcome(outcome, waited, io);
9725
+ return worstExitCode([outcome]);
9726
+ }
9727
+ function printOutcome(outcome, waited, io) {
9728
+ switch (outcome.status) {
9729
+ case "executed":
9730
+ io.stdout(JSON.stringify(outcome.result ?? null, null, 2));
9731
+ return;
9732
+ case "pending":
9733
+ if (waited) io.stderr(`Still waiting on the operator after ${waited}s; the invocation remains open.`);
9734
+ return;
9735
+ case "rejected":
9736
+ io.stderr(`Rejected by the operator${outcome.error ? `: ${outcome.error}` : "."}`);
9737
+ return;
9738
+ case "expired":
9739
+ io.stderr("Expired before anyone decided. Raise a fresh call; the old id is final.");
9740
+ return;
9741
+ case "failed":
9742
+ throw new CliError(outcome.invocationId ? `Approved, but execution failed: ${outcome.error}` : outcome.error || "Tool execution failed");
9743
+ }
9744
+ }
9571
9745
  function toolsVerbUsage(verb) {
9572
- const lines = toolsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `));
9746
+ const lines = [];
9747
+ let inVerb = false;
9748
+ for (const line of toolsUsage.split("\n")) {
9749
+ if (/^ {2}\S/.test(line)) inVerb = line.trimStart().startsWith(`${verb} `);
9750
+ else if (!/^ {3,}\S/.test(line)) inVerb = false;
9751
+ if (inVerb) lines.push(line);
9752
+ }
9573
9753
  return lines.length ? `diverge tools ${verb} ...
9574
9754
 
9575
9755
  ${lines.join("\n")}
@@ -9584,17 +9764,17 @@ async function runTools(args, io) {
9584
9764
  }
9585
9765
  switch (verb) {
9586
9766
  case "list": {
9587
- const { values } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
9767
+ const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
9588
9768
  const { client: tools } = await client(values, io);
9589
9769
  let list = await tools.listTools();
9590
- if (values.filter) list = list.filter((tool) => tool.name.includes(values.filter));
9770
+ if (values.filter) list = list.filter((tool) => matchesFilter(values.filter, tool));
9591
9771
  if (values.capability) list = list.filter((tool) => matchesCapability(values.capability, tool.capability));
9592
9772
  if (values.json) io.stdout(JSON.stringify(list, null, 2));
9593
9773
  else for (const line of formatToolList(list)) io.stdout(line);
9594
9774
  return ExitCode.Ok;
9595
9775
  }
9596
9776
  case "capabilities": {
9597
- const { values } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
9777
+ const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
9598
9778
  const { client: tools } = await client(values, io);
9599
9779
  const list = await tools.listTools();
9600
9780
  if (values.json) io.stdout(JSON.stringify(capabilityClusters(list), null, 2));
@@ -9602,89 +9782,63 @@ async function runTools(args, io) {
9602
9782
  return ExitCode.Ok;
9603
9783
  }
9604
9784
  case "describe": {
9605
- const { values, positionals } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
9785
+ const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
9606
9786
  const name = positionals[0];
9607
9787
  if (!name) throw new CliError("describe needs a tool name");
9608
9788
  const { settings, client: tools } = await client(values, io);
9609
- const list = await tools.listTools();
9610
- const tool = list.find((entry) => entry.name === name);
9611
- if (!tool) {
9612
- const close = list.filter((entry) => entry.name.includes(name)).map((entry) => entry.name).slice(0, 5);
9613
- throw new CliError(`No tool named '${name}' is available.${close.length ? ` Did you mean: ${close.join(", ")}?` : ""}`);
9789
+ const described = await tools.describeTool(name);
9790
+ if ("notFound" in described) {
9791
+ const close = (await tools.listTools()).filter((entry) => entry.name.includes(name)).map((entry) => entry.name).slice(0, 5);
9792
+ throw new CliError(`${described.notFound}${close.length ? `
9793
+ Did you mean: ${close.join(", ")}?` : ""}`);
9614
9794
  }
9795
+ const { tool } = described;
9615
9796
  if (values.json) io.stdout(JSON.stringify(tool, null, 2));
9616
9797
  else for (const line of formatDescribe(tool, settings.organizationId)) io.stdout(line);
9617
9798
  return ExitCode.Ok;
9618
9799
  }
9619
- case "call": {
9620
- const { values, positionals } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, "args-json": { type: "string" }, "args-file": { type: "string" }, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
9621
- const name = positionals[0];
9622
- if (!name) throw new CliError("call needs a tool name");
9623
- const parsedArgs = resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
9624
- const timeoutMs = Number(values.timeout) * 1e3;
9625
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
9626
- const { settings, client: tools } = await client(values, io);
9627
- const body = await tools.callTool(name, parsedArgs);
9628
- if (body.success === false) throw new CliError(body.error || "Tool execution failed");
9629
- if (!isPending(body.result)) {
9630
- io.stdout(JSON.stringify(body.result ?? null, null, 2));
9631
- return ExitCode.Ok;
9632
- }
9633
- for (const line of formatPending(body.result, settings.apiUrl, settings.organizationId)) io.stdout(line);
9634
- if (!values.wait) return ExitCode.Pending;
9635
- const invocation = await waitForInvocation(body.result.invocationId ?? "", { pollAfterMs: body.result.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
9636
- if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
9637
- io.stderr(`Still ${invocation.status} after ${values.timeout}s; the invocation remains open.`);
9638
- return ExitCode.Pending;
9639
- }
9640
- return emit(io, settledOutcome(invocation));
9641
- }
9800
+ case "call":
9801
+ return call(rest, io);
9642
9802
  case "run": {
9643
- const { values, positionals } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
9803
+ const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
9644
9804
  const file = positionals[0];
9645
- if (!file) throw new CliError("run needs a file of calls");
9805
+ if (!file) throw new CliError("run needs a file of calls, or - for stdin");
9646
9806
  let entries;
9647
9807
  try {
9648
- entries = parseBatch(fs10.readFileSync(file, "utf8"));
9808
+ entries = parseBatch(file === "-" ? await readStdin() : fs10.readFileSync(file, "utf8"));
9649
9809
  } catch (error) {
9650
9810
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
9651
9811
  }
9652
- const timeoutMs = Number(values.timeout) * 1e3;
9653
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
9812
+ const timeoutMs = timeoutOf(values.timeout);
9654
9813
  const { settings, client: tools } = await client(values, io);
9655
9814
  const { exitCode } = await runBatch(entries, {
9656
9815
  callTool: (tool, args2) => tools.callTool(tool, args2),
9657
9816
  decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
9658
9817
  onOutcome: (outcome) => io.stdout(JSON.stringify(outcome)),
9659
- ...values.wait ? {
9660
- waitFor: async (pending) => {
9661
- const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
9662
- return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
9663
- }
9664
- } : {}
9818
+ ...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
9665
9819
  });
9666
9820
  return exitCode;
9667
9821
  }
9668
9822
  case "invocation": {
9669
- const { values, positionals } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" }, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
9823
+ 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 });
9670
9824
  const id = positionals[0];
9671
9825
  if (!id) throw new CliError("invocation needs an id");
9672
9826
  const { client: tools } = await client(values, io);
9673
9827
  let invocation;
9674
9828
  if (values.wait) invocation = await waitForInvocation(id, { timeoutMs: Number(values.timeout) * 1e3 }, { getInvocation: (target) => tools.getInvocation(target), sleep: realSleep, now: Date.now });
9675
9829
  else invocation = (await tools.getInvocation(id)).invocation ?? {};
9830
+ const outcome = readBackOutcomeOf(invocation.toolName ?? "", id, invocation);
9676
9831
  if (values.json) {
9677
9832
  io.stdout(JSON.stringify(invocation, null, 2));
9678
- return ExitCode.Ok;
9679
- }
9680
- if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
9681
- io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
9682
- return ExitCode.Pending;
9833
+ return outcome ? worstExitCode([outcome]) : ExitCode.Failure;
9683
9834
  }
9684
- return emit(io, settledOutcome(invocation));
9835
+ if (!outcome) throw new CliError(`Unexpected invocation status: ${invocation.status}`);
9836
+ if (outcome.status === "pending") io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
9837
+ else printOutcome(outcome, "", io);
9838
+ return worstExitCode([outcome]);
9685
9839
  }
9686
9840
  case "bridge": {
9687
- const { values } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
9841
+ const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
9688
9842
  const { client: tools } = await client(values, io);
9689
9843
  const allow = (values.tools ?? "").split(",").map((name) => name.trim()).filter(Boolean);
9690
9844
  return serveBridge(new BridgeServer(tools, { tools: allow }), process.stdin, process.stdout);
@@ -9698,7 +9852,7 @@ ${toolsUsage}`);
9698
9852
 
9699
9853
  // src/commands/plugins.ts
9700
9854
  import fs11 from "node:fs";
9701
- import { parseArgs as parseArgs6 } from "node:util";
9855
+ import { parseArgs as parseArgs7 } from "node:util";
9702
9856
  var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
9703
9857
 
9704
9858
  validate <manifest.json> (dry run: the platform's full validation, nothing stored)
@@ -9742,7 +9896,8 @@ function formatPlugin(plugin) {
9742
9896
  return [
9743
9897
  `${plugin.pluginId}@${plugin.version} ${plugin.name} (${parent}; runtime ${plugin.runtimeType}; digest ${plugin.digest.slice(0, 12)})`,
9744
9898
  ` tools: ${plugin.toolNames.length ? plugin.toolNames.join(", ") : "none"}`,
9745
- ...plugin.resourceNames.length ? [` resources: ${plugin.resourceNames.join(", ")}`] : []
9899
+ ...plugin.resourceNames.length ? [` resources: ${plugin.resourceNames.join(", ")}`] : [],
9900
+ ...plugin.refusals?.length ? [" NOT SERVED: the platform refuses this version; publish a fixed one:", ...plugin.refusals.map((reason) => ` - ${reason}`)] : []
9746
9901
  ];
9747
9902
  }
9748
9903
  function pluginsVerbUsage(verb) {
@@ -9761,7 +9916,7 @@ async function runPlugins(args, io) {
9761
9916
  }
9762
9917
  switch (verb) {
9763
9918
  case "validate": {
9764
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
9919
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
9765
9920
  const manifest = readManifest(positionals[0]);
9766
9921
  const tools = await client2(values, io);
9767
9922
  const outcome = await tools.publishPlugin(manifest, { dryRun: true });
@@ -9778,7 +9933,7 @@ async function runPlugins(args, io) {
9778
9933
  return ExitCode.Ok;
9779
9934
  }
9780
9935
  case "publish": {
9781
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
9936
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
9782
9937
  const manifest = readManifest(positionals[0]);
9783
9938
  const tools = await client2(values, io);
9784
9939
  const outcome = await tools.publishPlugin(manifest, { dryRun: false });
@@ -9796,7 +9951,7 @@ async function runPlugins(args, io) {
9796
9951
  return ExitCode.Ok;
9797
9952
  }
9798
9953
  case "list": {
9799
- const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
9954
+ const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
9800
9955
  const tools = await client2(values, io);
9801
9956
  const plugins = await tools.listPlugins();
9802
9957
  if (values.json) io.stdout(JSON.stringify(plugins, null, 2));
@@ -9805,7 +9960,7 @@ async function runPlugins(args, io) {
9805
9960
  return ExitCode.Ok;
9806
9961
  }
9807
9962
  case "retire": {
9808
- const { values, positionals } = parseArgs6({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
9963
+ const { values, positionals } = parseArgs7({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
9809
9964
  const pluginId = positionals[0];
9810
9965
  if (!pluginId) throw new CliError(`A plugin id is required.
9811
9966
 
@@ -9828,11 +9983,11 @@ import { spawn as spawn4 } from "node:child_process";
9828
9983
  // src/update-notice.ts
9829
9984
  import { spawn as spawn3 } from "node:child_process";
9830
9985
  import fs12 from "node:fs";
9831
- import path12 from "node:path";
9986
+ import path14 from "node:path";
9832
9987
  var UPDATE_CHECK_FILE = "update-check.json";
9833
9988
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9834
9989
  function updateCheckFile(env) {
9835
- return path12.join(configDir(env), UPDATE_CHECK_FILE);
9990
+ return path14.join(configDir(env), UPDATE_CHECK_FILE);
9836
9991
  }
9837
9992
  function readUpdateCheck(env) {
9838
9993
  try {
@@ -9952,7 +10107,7 @@ function usage() {
9952
10107
  ...lines,
9953
10108
  "",
9954
10109
  "Run `diverge <command> --help` for its options.",
9955
- "Environment: DIVERGENT_API_URL (deployment), DIVERGE_HOME (state directory, default ~/.diverge), DIVERGENT_ORG_ID and DIVERGENT_PROJECT_ID (tool scope)."
10110
+ "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)."
9956
10111
  ].join("\n");
9957
10112
  }
9958
10113
  async function run2(argv, io) {