@divergeai/cli 0.1.14 → 0.1.23

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 +45 -19
  2. package/dist/diverge.mjs +872 -639
  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.14" : "dev";
7426
+ var VERSION = true ? "0.1.23" : "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,302 @@ 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/usage.ts
7853
+ function usageEntries(usage2) {
7854
+ const entries = [];
7855
+ let current = null;
7856
+ for (const line of usage2.split("\n")) {
7857
+ if (/^ {2}\S/.test(line)) {
7858
+ const label = line.trim().split(/ {2,}/)[0];
7859
+ const verbs = [];
7860
+ for (const word of label.split(" ")) {
7861
+ if (!/^[a-z][a-z-]*$/.test(word)) break;
7862
+ verbs.push(word);
7863
+ }
7864
+ current = { verbs, summary: line.trim().slice(label.length).trim(), lines: [line] };
7865
+ entries.push(current);
7866
+ } else if (current && /^ {3,}\S/.test(line)) {
7867
+ current.lines.push(line);
7868
+ } else {
7869
+ current = null;
7870
+ }
7871
+ }
7872
+ return entries;
7873
+ }
7874
+ function startsWith(words, prefix) {
7875
+ return prefix.every((word, index) => words[index] === word);
7876
+ }
7877
+ function verbPath(usage2, args) {
7878
+ let best = [];
7879
+ for (const entry of usageEntries(usage2)) {
7880
+ let shared = 0;
7881
+ while (shared < entry.verbs.length && args[shared] === entry.verbs[shared]) shared++;
7882
+ if (shared > best.length) best = entry.verbs.slice(0, shared);
7883
+ }
7884
+ return best;
7885
+ }
7886
+ function groupOptions(usage2) {
7887
+ return usage2.split("\n", 1)[0].match(/\[--[^\]]+\]/g) ?? [];
7888
+ }
7889
+ function verbUsage(command, usage2, path15) {
7890
+ const lines = usageEntries(usage2).filter((entry) => startsWith(entry.verbs, path15)).flatMap((entry) => entry.lines);
7891
+ if (path15.length === 0 || lines.length === 0) return usage2;
7892
+ const shared = groupOptions(usage2);
7893
+ return [`diverge ${command} ${path15.join(" ")} ...`, "", ...lines, ...shared.length ? ["", `Every ${command} command also takes ${shared.join(" ")}.`] : []].join("\n");
7894
+ }
7895
+ var COMMON_WORDS = /* @__PURE__ */ new Set(["the", "and", "you", "for", "its", "with", "not", "one", "are", "all", "any", "from", "that", "this", "own", "per", "into", "out", "else", "each", "what", "when", "your"]);
7896
+ function distance(a, b) {
7897
+ const d = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_2, j) => i === 0 ? j : j === 0 ? i : 0));
7898
+ for (let i = 1; i <= a.length; i++) {
7899
+ for (let j = 1; j <= b.length; j++) {
7900
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
7901
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
7902
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
7903
+ }
7904
+ }
7905
+ return d[a.length][b.length];
7906
+ }
7907
+ function namesIt(word, guess) {
7908
+ if (word.length < 3 || COMMON_WORDS.has(word)) return false;
7909
+ return word === guess || guess.length >= 3 && word.startsWith(guess) || guess.startsWith(word) || guess.endsWith(word);
7910
+ }
7911
+ function prose(line) {
7912
+ return line.toLowerCase().replace(/\[[^\]]*\]|--?[a-z][a-z-]*/g, " ");
7913
+ }
7914
+ function closest(guess, candidates, rest = [], limit = 3) {
7915
+ const target = guess.toLowerCase().replace(/^-+/, "");
7916
+ const threshold = Math.max(1, Math.floor(target.length / 3));
7917
+ const matches = [];
7918
+ for (const candidate of candidates) {
7919
+ const first = candidate.summary ?? "";
7920
+ const name = candidate.name.toLowerCase();
7921
+ if (name && rest.includes(candidate.name)) {
7922
+ matches.push({ candidate, score: 0, hint: first });
7923
+ continue;
7924
+ }
7925
+ const edits = name ? distance(target, name) : Infinity;
7926
+ if (edits <= threshold || name && target.length >= 2 && name.startsWith(target)) {
7927
+ matches.push({ candidate, score: Math.min(edits, threshold), hint: first });
7928
+ continue;
7929
+ }
7930
+ const line = candidate.lines.find((text) => (prose(text).match(/[a-z]+/g) ?? []).some((word) => namesIt(word, target)));
7931
+ if (line) matches.push({ candidate, score: threshold + 1, hint: line });
7932
+ }
7933
+ return matches.sort((a, b) => a.score - b.score).slice(0, limit);
7934
+ }
7935
+ function didYouMean(matches) {
7936
+ if (matches.length === 0) return "";
7937
+ const shown = (match) => {
7938
+ const hint = match.hint.trim();
7939
+ return hint && hint !== match.candidate.invocation ? `${match.candidate.invocation} (${hint})` : match.candidate.invocation;
7940
+ };
7941
+ if (matches.length === 1) return ` Did you mean: ${shown(matches[0])}?`;
7942
+ return ` Did you mean one of:
7943
+ ${matches.map((match) => ` ${shown(match)}`).join("\n")}`;
7944
+ }
7945
+ function verbCandidates(command, usage2, path15) {
7946
+ const byName = /* @__PURE__ */ new Map();
7947
+ for (const entry of usageEntries(usage2)) {
7948
+ if (!startsWith(entry.verbs, path15)) continue;
7949
+ const name = entry.verbs[path15.length] ?? "";
7950
+ const invocation = ["diverge", command, ...path15, ...name ? [name] : []].join(" ");
7951
+ const lines = [entry.summary, ...entry.lines.slice(1).map((line) => line.trim())].filter(Boolean);
7952
+ const known = byName.get(name);
7953
+ if (known) known.lines.push(...lines);
7954
+ else byName.set(name, { invocation, name, summary: entry.summary, lines });
7955
+ }
7956
+ return [...byName.values()];
7957
+ }
7958
+ function unknownVerb(command, usage2, path15, verb, rest = []) {
7959
+ const candidates = verbCandidates(command, usage2, path15);
7960
+ const where = [command, ...path15].join(" ");
7961
+ if (!verb) return new CliError(`${where} needs one of: ${candidates.map((candidate) => candidate.name).filter(Boolean).join(", ")}
7962
+
7963
+ ${usage2}`);
7964
+ return new CliError(`Unknown ${where} verb: ${verb}.${didYouMean(closest(verb, candidates, rest))}
7965
+
7966
+ ${usage2}`);
7967
+ }
7968
+ function optionCandidates(usage2, path15) {
7969
+ const entries = usageEntries(usage2);
7970
+ const [header = "", ...body] = usage2.split("\n");
7971
+ const described = entries.length === 0 || path15.length === 0 ? body : entries.filter((entry) => startsWith(entry.verbs, path15)).flatMap((entry) => entry.lines);
7972
+ const byName = /* @__PURE__ */ new Map();
7973
+ const add = (line, describes) => {
7974
+ for (const [flag] of line.matchAll(/--[a-z][a-z-]*/g)) {
7975
+ const candidate = byName.get(flag) ?? { invocation: flag, name: flag.slice(2), lines: [] };
7976
+ byName.set(flag, candidate);
7977
+ if (describes && !candidate.lines.includes(line.trim())) candidate.lines.push(line.trim());
7978
+ }
7979
+ };
7980
+ add(header, false);
7981
+ for (const line of described) add(line, true);
7982
+ return [...byName.values()];
7983
+ }
7984
+ function unknownOption(error, usage2, args) {
7985
+ if (!(error instanceof Error) || error.name !== "TypeError") return null;
7986
+ const option = /^Unknown option '([^']+)'/.exec(error.message)?.[1];
7987
+ if (!option) return null;
7988
+ const path15 = verbPath(usage2, args);
7989
+ return `Unknown option: ${option}.${didYouMean(closest(option, optionCandidates(usage2, path15)))}`;
7990
+ }
7991
+
7992
+ // src/commands/login.ts
7993
+ import { parseArgs } from "node:util";
7994
+
7995
+ // src/auth/organizations.ts
7996
+ function chooseOrganization(input) {
7997
+ const organizations = input.organizations;
7998
+ const flag = input.flag?.trim();
7999
+ if (flag) {
8000
+ const match = organizations.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
8001
+ if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
8002
+ throw new CliError(`Not one of your organizations: ${flag}. Yours: ${organizations.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
8003
+ }
8004
+ const remembered = input.remembered?.trim();
8005
+ if (remembered) {
8006
+ const match = organizations.find((organization) => organization.id === remembered);
8007
+ if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
8008
+ }
8009
+ if (organizations.length === 1) return { kind: "chosen", organizationId: organizations[0].id, name: organizations[0].name, reason: "only" };
8010
+ if (organizations.length === 0) return { kind: "none" };
8011
+ return { kind: "ask", options: organizations };
8012
+ }
8013
+ function organizationMenu(options) {
8014
+ return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
7767
8015
  }
7768
- function tokenFile(env = process.env) {
7769
- return path2.join(configDir(env), "token.json");
8016
+ function pickFromMenu(answer, options) {
8017
+ const trimmed = answer.trim();
8018
+ const index = Number(trimmed);
8019
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
8020
+ const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
8021
+ if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
8022
+ return match;
7770
8023
  }
7771
- function configFile(env = process.env) {
7772
- return path2.join(configDir(env), "config.json");
8024
+
8025
+ // src/auth/userinfo.ts
8026
+ async function fetchUserInfo(baseUrl, accessToken) {
8027
+ return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
7773
8028
  }
7774
- function readConfig(env = process.env) {
8029
+ async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
7775
8030
  try {
7776
- const parsed = JSON.parse(fs3.readFileSync(configFile(env), "utf8"));
7777
- return parsed && typeof parsed === "object" ? parsed : {};
7778
- } catch {
7779
- return {};
8031
+ const info = await fetchUserInfo(baseUrl, accessToken);
8032
+ return options.withSub ? `${info.email} (${info.sub})` : info.email;
8033
+ } catch (error) {
8034
+ return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
7780
8035
  }
7781
8036
  }
7782
- function writeConfig(config, env = process.env) {
7783
- writeJsonAtomic(configFile(env), config);
8037
+
8038
+ // src/prompt.ts
8039
+ import readline from "node:readline";
8040
+ function ask(question) {
8041
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
8042
+ return new Promise((resolve) => rl.question(question, (answer) => {
8043
+ rl.close();
8044
+ resolve(answer);
8045
+ }));
7784
8046
  }
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(/\/+$/, "");
8047
+
8048
+ // src/commands/login.ts
8049
+ var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
8050
+
8051
+ Sign in through the browser, then choose where to act. The browser only
8052
+ authenticates the person; the organization is chosen here afterwards \u2014 the
8053
+ one named by --organization-id (id or name), else the last login's, else the
8054
+ only membership, else a menu. The login itself is yours, not an
8055
+ organization's; the default is a local preference.
8056
+ That is the default: commands act there unless they name another with
8057
+ --organization-id or DIVERGENT_ORG_ID. Change it with: diverge org use <id|name>.
8058
+ --as <email> names the account: the platform signs that account in instead of
8059
+ reusing whatever session the browser holds, and issues nothing to any other.
8060
+ Without --as, the account of the last login is used; switching is explicit.
8061
+ Without --scopes, the login asks for the scopes the platform registers for
8062
+ this CLI \u2014 everything it and forge use \u2014 and holds what your roles grant; a
8063
+ scope the platform adds later reaches this login on its next refresh.
8064
+ --scopes asks for less, deliberately, and the login stays that narrow.
8065
+ --api-url signs in to another deployment (a test stack, say). Each deployment
8066
+ keeps its own login, so this never replaces another one; commands reach it
8067
+ when DIVERGENT_API_URL names it.`;
8068
+ function resolveScopes(flag) {
8069
+ const explicit = flag?.trim();
8070
+ return explicit ? explicit.split(/\s+/).join(" ") : void 0;
7788
8071
  }
7789
- function clientId(env = process.env) {
7790
- return env.DIVERGENT_DIVERGE_CLIENT_ID?.trim() || DEFAULT_CLIENT_ID;
8072
+ function resolveLoginHint(flag, previous) {
8073
+ const explicit = flag?.trim().toLowerCase();
8074
+ if (explicit) return explicit;
8075
+ const remembered = previous.account?.trim().toLowerCase();
8076
+ return remembered || void 0;
8077
+ }
8078
+ async function runLogin(args, io) {
8079
+ const { values } = parseArgs({
8080
+ args,
8081
+ options: {
8082
+ "organization-id": { type: "string" },
8083
+ scopes: { type: "string" },
8084
+ as: { type: "string" },
8085
+ "no-browser": { type: "boolean", default: false },
8086
+ "api-url": { type: "string" }
8087
+ },
8088
+ strict: true,
8089
+ allowPositionals: false
8090
+ });
8091
+ const apiUrl = resolveApiUrl(values["api-url"], io.env);
8092
+ const source = tokenSource(io.env, apiUrl);
8093
+ const previous = loadLogin(source);
8094
+ const loginHint = resolveLoginHint(values.as, { account: previous?.account });
8095
+ if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
8096
+ const scopes = resolveScopes(values.scopes);
8097
+ io.stdout(scopes ? `Asking for scopes: ${scopes}.` : "Asking for the scopes the platform registers for this CLI.");
8098
+ const signIn = await login({
8099
+ baseUrl: apiUrl,
8100
+ clientId: source.clientId,
8101
+ scopes,
8102
+ openBrowser: !values["no-browser"],
8103
+ loginHint,
8104
+ print: io.stdout
8105
+ });
8106
+ let info;
8107
+ try {
8108
+ info = await fetchUserInfo(apiUrl, signIn.token.access_token);
8109
+ } catch (error) {
8110
+ throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
8111
+ }
8112
+ io.stdout(`Signed in as: ${info.email}`);
8113
+ const organizationId = await chooseDefaultOrganization(io, values["organization-id"], previous?.organization, info);
8114
+ const issued = await issueToken(source, { base_url: source.baseUrl, refresh_token: signIn.refreshToken, account: info.email }, organizationId);
8115
+ saveLogin(source, { ...issued.record, organization: organizationId });
8116
+ saveOrganizationToken(source, organizationId, issued.token);
8117
+ io.stdout(`Logged in to ${apiUrl}.`);
8118
+ if (apiUrl !== resolveApiUrl(void 0, io.env)) io.stdout(`Commands reach this deployment with DIVERGENT_API_URL=${apiUrl}.`);
8119
+ const granted = new Set(tokenClaims(issued.token.access_token).scopes ?? []);
8120
+ io.stdout(`Scopes: ${[...granted].join(" ") || "none"}`);
8121
+ const missing = (scopes ?? "").split(" ").filter((scope) => scope && !granted.has(scope));
8122
+ if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
8123
+ return 0;
8124
+ }
8125
+ async function chooseDefaultOrganization(io, flag, remembered, info) {
8126
+ let choice = chooseOrganization({ flag, remembered, organizations: info.organizations ?? [] });
8127
+ if (choice.kind === "none") {
8128
+ throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
8129
+ }
8130
+ if (choice.kind === "ask") {
8131
+ if (!io.interactive) {
8132
+ throw new CliError(`You may act in several organizations; pass --organization-id <id|name>:
8133
+ ${organizationMenu(choice.options).join("\n")}`);
8134
+ }
8135
+ io.stdout("Choose the organization to act in:");
8136
+ for (const line of organizationMenu(choice.options)) io.stdout(line);
8137
+ const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
8138
+ choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
8139
+ }
8140
+ const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
8141
+ const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
8142
+ io.stdout(`Organization: ${label}${why}`);
8143
+ return choice.organizationId;
7791
8144
  }
7792
8145
 
7793
8146
  // src/gateway/urls.ts
7794
8147
  var SURFACES = ["anthropic", "openai"];
7795
- var GATEWAY_SCOPES = "mcp:read mcp:execute inference:execute";
7796
8148
  var INFERENCE_SCOPE = "inference:execute";
7797
8149
  function anthropicBaseUrl(apiUrl) {
7798
8150
  return `${apiUrl.replace(/\/+$/, "")}/api/plugins/inference/anthropic`;
@@ -7803,12 +8155,6 @@ function openaiBaseUrl(apiUrl) {
7803
8155
  function surfaceBaseUrl(apiUrl, surface) {
7804
8156
  return surface === "openai" ? openaiBaseUrl(apiUrl) : anthropicBaseUrl(apiUrl);
7805
8157
  }
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
8158
 
7813
8159
  // src/gateway/models.ts
7814
8160
  async function listModels(apiUrl, accessToken, surface) {
@@ -7825,35 +8171,27 @@ async function listModels(apiUrl, accessToken, surface) {
7825
8171
  throw error;
7826
8172
  }
7827
8173
  }
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;
8174
+ async function ensureInferenceLogin(io, apiUrl, organizationId) {
8175
+ const source = tokenSource(io.env, apiUrl);
8176
+ if (!loadLogin(source)) {
8177
+ if (!io.interactive) throw new CliError(`Not signed in to ${apiUrl}. Run: diverge login${organizationId ? ` --organization-id ${organizationId}` : ""}`);
8178
+ await runLogin(organizationId ? ["--organization-id", organizationId] : [], io);
7835
8179
  }
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}` : ""}`);
8180
+ const token = await getUserToken(source, { organizationId });
8181
+ const claims = tokenClaims(token.access_token);
8182
+ if (!claims.organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
8183
+ if (!(claims.scopes ?? []).includes(INFERENCE_SCOPE)) {
8184
+ 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
8185
  }
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
- });
8186
+ return token;
7849
8187
  }
7850
8188
 
7851
8189
  // src/agents/claude.ts
7852
8190
  import os2 from "node:os";
7853
- import path3 from "node:path";
8191
+ import path4 from "node:path";
7854
8192
  var CLAUDE_MODEL_DISCOVERY_FLAG = "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY";
7855
8193
  function claudeSettingsPath(scope, cwd = process.cwd()) {
7856
- return scope === "user" ? path3.join(os2.homedir(), ".claude", "settings.json") : path3.join(cwd, ".claude", "settings.local.json");
8194
+ return scope === "user" ? path4.join(os2.homedir(), ".claude", "settings.json") : path4.join(cwd, ".claude", "settings.local.json");
7857
8195
  }
7858
8196
  function mergeClaudeSettings(existing, options) {
7859
8197
  const settings = structuredClone(existing ?? {});
@@ -7874,11 +8212,11 @@ function mergeClaudeSettings(existing, options) {
7874
8212
 
7875
8213
  // src/agents/codex.ts
7876
8214
  import os3 from "node:os";
7877
- import path4 from "node:path";
8215
+ import path5 from "node:path";
7878
8216
  var CODEX_BLOCK_START = "# >>> divergent >>>";
7879
8217
  var CODEX_BLOCK_END = "# <<< divergent <<<";
7880
8218
  function codexConfigPath() {
7881
- return path4.join(os3.homedir(), ".codex", "config.toml");
8219
+ return path5.join(os3.homedir(), ".codex", "config.toml");
7882
8220
  }
7883
8221
  var tomlString = (value) => JSON.stringify(value);
7884
8222
  function codexProviderToml(options) {
@@ -7894,7 +8232,7 @@ function codexProviderToml(options) {
7894
8232
  "",
7895
8233
  "[model_providers.divergent.auth]",
7896
8234
  `command = ${tomlString(options.self.command)}`,
7897
- `args = [${[...options.self.args, "token"].map(tomlString).join(", ")}]`,
8235
+ `args = [${[...options.self.args, "token", ...options.organizationId ? ["--organization-id", options.organizationId] : []].map(tomlString).join(", ")}]`,
7898
8236
  "refresh_interval_ms = 600000",
7899
8237
  CODEX_BLOCK_END,
7900
8238
  ""
@@ -7919,10 +8257,10 @@ var import_yaml = __toESM(require_dist(), 1);
7919
8257
  import fs4 from "node:fs";
7920
8258
  import { createRequire } from "node:module";
7921
8259
  import os4 from "node:os";
7922
- import path5 from "node:path";
8260
+ import path6 from "node:path";
7923
8261
  var DSH_SHIPPED_DEFAULT_MODEL = "deepseek-flash";
7924
8262
  function dshHome(env = process.env) {
7925
- return env.DSH_HOME?.trim() || path5.join(os4.homedir(), ".dsh");
8263
+ return env.DSH_HOME?.trim() || path6.join(os4.homedir(), ".dsh");
7926
8264
  }
7927
8265
  function dshDefaultModel(models, requested) {
7928
8266
  if (requested) return requested;
@@ -8011,30 +8349,30 @@ var DIVERGENT_PRESET_ID = "divergent";
8011
8349
  var DSH_USER_PRESETS_DIR = ".agent-presets";
8012
8350
  function dshPackageDir(binary) {
8013
8351
  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;
8352
+ const dir2 = path6.join(path6.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8353
+ return fs4.existsSync(path6.join(dir2, "package.json")) ? dir2 : null;
8016
8354
  }
8017
8355
  let dir;
8018
8356
  try {
8019
- dir = path5.dirname(fs4.realpathSync(binary));
8357
+ dir = path6.dirname(fs4.realpathSync(binary));
8020
8358
  } catch {
8021
8359
  return null;
8022
8360
  }
8023
8361
  for (; ; ) {
8024
8362
  try {
8025
- if (JSON.parse(fs4.readFileSync(path5.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8363
+ if (JSON.parse(fs4.readFileSync(path6.join(dir, "package.json"), "utf8")).name === "@deepseek-ai/dsh") return dir;
8026
8364
  } catch {
8027
8365
  }
8028
- const parent = path5.dirname(dir);
8366
+ const parent = path6.dirname(dir);
8029
8367
  if (parent === dir) return null;
8030
8368
  dir = parent;
8031
8369
  }
8032
8370
  }
8033
8371
  function shippedPresetDir(packageDir, id = "standard") {
8034
8372
  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;
8373
+ const require2 = createRequire(path6.join(packageDir, "package.json"));
8374
+ const dir = path6.join(path6.dirname(require2.resolve("@deepseek-ai/dsh-agent-presets/package.json")), "presets", id);
8375
+ return fs4.existsSync(path6.join(dir, "agent.cordis.yml")) ? dir : null;
8038
8376
  } catch {
8039
8377
  return null;
8040
8378
  }
@@ -8071,9 +8409,9 @@ var DIVERGENT_PRESET_METADATA = [
8071
8409
  ""
8072
8410
  ].join("\n");
8073
8411
  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);
8412
+ const dir = path6.join(home, DSH_USER_PRESETS_DIR, DIVERGENT_PRESET_ID);
8413
+ writeTextAtomic(path6.join(dir, "agent.cordis.yml"), divergentPreset(fs4.readFileSync(path6.join(shippedDir, "agent.cordis.yml"), "utf8")), 420);
8414
+ writeTextAtomic(path6.join(dir, "preset.yml"), DIVERGENT_PRESET_METADATA, 420);
8077
8415
  return dir;
8078
8416
  }
8079
8417
  function withDivergentPresetDefault(settingsText) {
@@ -8087,9 +8425,9 @@ function withDivergentPresetDefault(settingsText) {
8087
8425
 
8088
8426
  // src/agents/opencode.ts
8089
8427
  import os5 from "node:os";
8090
- import path6 from "node:path";
8428
+ import path7 from "node:path";
8091
8429
  function opencodeConfigPath() {
8092
- return path6.join(os5.homedir(), ".config", "opencode", "opencode.json");
8430
+ return path7.join(os5.homedir(), ".config", "opencode", "opencode.json");
8093
8431
  }
8094
8432
  function mergeOpencodeConfig(existing, options) {
8095
8433
  const tokenEnv = options.tokenEnv ?? "DIVERGENT_TOKEN";
@@ -8106,14 +8444,14 @@ function mergeOpencodeConfig(existing, options) {
8106
8444
  // src/agents/run-dsh.ts
8107
8445
  import { spawn as spawn2 } from "node:child_process";
8108
8446
  import fs5 from "node:fs";
8109
- import path7 from "node:path";
8447
+ import path8 from "node:path";
8110
8448
  function findOnPath(name, env = process.env, platform = process.platform) {
8111
- const dirs = (env.PATH ?? "").split(path7.delimiter).filter(Boolean);
8449
+ const dirs = (env.PATH ?? "").split(path8.delimiter).filter(Boolean);
8112
8450
  const extensions = (env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
8113
8451
  const candidates = platform === "win32" ? extensions.map((extension) => `${name}${extension.toLowerCase()}`) : [name];
8114
8452
  for (const dir of dirs) {
8115
8453
  for (const candidate of candidates) {
8116
- const full = path7.join(dir, candidate);
8454
+ const full = path8.join(dir, candidate);
8117
8455
  try {
8118
8456
  fs5.accessSync(full, fs5.constants.X_OK);
8119
8457
  if (fs5.statSync(full).isFile()) return full;
@@ -8139,16 +8477,16 @@ function dshArgs(overlayPath, args) {
8139
8477
  }
8140
8478
  function dshLaunch(binary, args, platform = process.platform, execPath = process.execPath) {
8141
8479
  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");
8480
+ const packageDir = path8.join(path8.dirname(binary), "node_modules", "@deepseek-ai", "dsh");
8143
8481
  let bin;
8144
8482
  try {
8145
- bin = JSON.parse(fs5.readFileSync(path7.join(packageDir, "package.json"), "utf8")).bin;
8483
+ bin = JSON.parse(fs5.readFileSync(path8.join(packageDir, "package.json"), "utf8")).bin;
8146
8484
  } catch {
8147
8485
  throw new CliError(`${binary} is not an npm shim for @deepseek-ai/dsh (no ${packageDir}); reinstall: npm install -g @deepseek-ai/dsh`);
8148
8486
  }
8149
8487
  const script = typeof bin === "string" ? bin : typeof bin === "object" && bin !== null ? bin.dsh : void 0;
8150
8488
  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] };
8489
+ return { command: execPath, args: [path8.join(packageDir, script), ...args] };
8152
8490
  }
8153
8491
  async function keepTokenFresh(loop) {
8154
8492
  let written = null;
@@ -8215,7 +8553,7 @@ function runDsh(options) {
8215
8553
  }
8216
8554
 
8217
8555
  // src/commands/skills.ts
8218
- import { parseArgs } from "node:util";
8556
+ import { parseArgs as parseArgs2 } from "node:util";
8219
8557
 
8220
8558
  // src/skills/bundled.ts
8221
8559
  var DIVERGE_SKILL = {
@@ -8235,9 +8573,9 @@ their permissions and audit trail. Nothing runs locally; the CLI is the
8235
8573
  transport. Keep the catalog out of context: pull the one schema you need.
8236
8574
 
8237
8575
  \`\`\`bash
8238
- diverge tools list --filter <word> # find candidates by name or capability
8576
+ diverge tools list --filter <word> # find candidates by name or description
8239
8577
  diverge tools describe <tool> # the one schema, on demand
8240
- diverge tools call <tool> --args-file args.json # arguments from a file (see below)
8578
+ diverge tools call <tool> --args-file args.json --json # arguments from a file, one JSON outcome out
8241
8579
  diverge tools call search_online --args-json '{"query": "..."}' # web search is a platform tool too
8242
8580
  \`\`\`
8243
8581
 
@@ -8253,20 +8591,28 @@ Write the object to a file and pass \`--args-file\`, or pipe it with
8253
8591
  \`--args-json -\`; both are exact on every shell. A worked call:
8254
8592
 
8255
8593
  \`\`\`bash
8256
- diverge tools describe divix-mcp_implementation_search_source
8594
+ diverge tools describe divix-data-sync_implementation_search_source
8257
8595
  cat > /tmp/args.json <<'JSON'
8258
8596
  {"query": "cr9b5_serial", "limit": 20}
8259
8597
  JSON
8260
- diverge tools call divix-mcp_implementation_search_source --args-file /tmp/args.json
8598
+ diverge tools call divix-data-sync_implementation_search_source --args-file /tmp/args.json
8261
8599
  \`\`\`
8262
8600
 
8601
+ ## Read the outcome from stdout with --json
8602
+
8603
+ \`call --json\` prints exactly one JSON object on stdout whatever happens,
8604
+ errors included: \`{"tool", "status", "result"?, "error"?, "invocationId"?,
8605
+ "decideAt"?}\`, where status is executed, pending, rejected, expired or failed.
8606
+ Parse stdout alone; stderr only carries the scope and identity lines. Without
8607
+ \`--json\`, errors are text on stderr.
8608
+
8263
8609
  ## Several calls at once
8264
8610
 
8265
8611
  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
8612
+ once instead of one round trip each: \`diverge tools run calls.json\` (or \`run -\` for stdin), a JSON
8267
8613
  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.
8614
+ order; each prints one JSON line of outcome, the same shape as \`call --json\`;
8615
+ the exit code is the worst of them.
8270
8616
 
8271
8617
  ## Exit codes are the contract
8272
8618
 
@@ -8284,8 +8630,11 @@ cadence). Never decide your own invocation from any surface you can drive.
8284
8630
 
8285
8631
  ## Session
8286
8632
 
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.
8633
+ - \`diverge status\` says who is signed in and at which organization; a call
8634
+ that names none runs there. To act in another organization, pass
8635
+ \`--organization-id <id>\` (or set \`DIVERGENT_ORG_ID\`) on the call; that
8636
+ never changes the default. \`diverge org use <id|name>\` changes it for
8637
+ every session on the machine, so leave it to the person.
8289
8638
  - A missing login or scope prints the command to run; \`diverge login\` opens
8290
8639
  the browser.
8291
8640
  - \`--json\` on \`list\`, \`describe\` and \`invocation\` gives raw records.
@@ -8295,22 +8644,22 @@ cadence). Never decide your own invocation from any surface you can drive.
8295
8644
  // src/skills/install.ts
8296
8645
  import fs6 from "node:fs";
8297
8646
  import os6 from "node:os";
8298
- import path8 from "node:path";
8647
+ import path9 from "node:path";
8299
8648
  var HARNESSES = ["claude", "codex", "dsh"];
8300
8649
  var SKILLS_MANIFEST = ".diverge-skills.json";
8301
8650
  function skillsRoot(harness, env = process.env) {
8302
8651
  switch (harness) {
8303
8652
  case "claude":
8304
- return path8.join(env.CLAUDE_CONFIG_DIR?.trim() || path8.join(os6.homedir(), ".claude"), "skills");
8653
+ return path9.join(env.CLAUDE_CONFIG_DIR?.trim() || path9.join(os6.homedir(), ".claude"), "skills");
8305
8654
  case "codex":
8306
- return path8.join(env.CODEX_HOME?.trim() || path8.join(os6.homedir(), ".codex"), "skills");
8655
+ return path9.join(env.CODEX_HOME?.trim() || path9.join(os6.homedir(), ".codex"), "skills");
8307
8656
  case "dsh":
8308
- return path8.join(dshHome(env), "skills");
8657
+ return path9.join(dshHome(env), "skills");
8309
8658
  }
8310
8659
  }
8311
8660
  function readSkillsManifest(root) {
8312
8661
  try {
8313
- const parsed = JSON.parse(fs6.readFileSync(path8.join(root, SKILLS_MANIFEST), "utf8"));
8662
+ const parsed = JSON.parse(fs6.readFileSync(path9.join(root, SKILLS_MANIFEST), "utf8"));
8314
8663
  if (parsed && typeof parsed === "object" && typeof parsed.skills === "object") return parsed;
8315
8664
  } catch {
8316
8665
  }
@@ -8327,7 +8676,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
8327
8676
  const updated = [];
8328
8677
  for (const skill of skills) {
8329
8678
  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`);
8330
- writeTextAtomic(path8.join(root, skill.name, "SKILL.md"), skill.content, 420);
8679
+ writeTextAtomic(path9.join(root, skill.name, "SKILL.md"), skill.content, 420);
8331
8680
  written.push(skill.name);
8332
8681
  if (!(skill.name in held)) added.push(skill.name);
8333
8682
  else if (held[skill.name] !== skill.updatedAt) updated.push(skill.name);
@@ -8335,7 +8684,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
8335
8684
  const removed = [];
8336
8685
  for (const name of Object.keys(previous?.skills ?? {})) {
8337
8686
  if (skills.some((skill) => skill.name === name)) continue;
8338
- fs6.rmSync(path8.join(root, name), { recursive: true, force: true });
8687
+ fs6.rmSync(path9.join(root, name), { recursive: true, force: true });
8339
8688
  removed.push(name);
8340
8689
  }
8341
8690
  const manifest = {
@@ -8343,7 +8692,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
8343
8692
  source,
8344
8693
  skills: Object.fromEntries(skills.map((skill) => [skill.name, skill.updatedAt]))
8345
8694
  };
8346
- writeJsonAtomic(path8.join(root, SKILLS_MANIFEST), manifest, 420);
8695
+ writeJsonAtomic(path9.join(root, SKILLS_MANIFEST), manifest, 420);
8347
8696
  return { root, written, added, updated, removed };
8348
8697
  }
8349
8698
  function skillsStatus(root, skills) {
@@ -8353,7 +8702,7 @@ function skillsStatus(root, skills) {
8353
8702
  const behind = [];
8354
8703
  const missing = [];
8355
8704
  for (const skill of skills) {
8356
- if (!(skill.name in held) || !fs6.existsSync(path8.join(root, skill.name, "SKILL.md"))) missing.push(skill.name);
8705
+ if (!(skill.name in held) || !fs6.existsSync(path9.join(root, skill.name, "SKILL.md"))) missing.push(skill.name);
8357
8706
  else if (held[skill.name] !== skill.updatedAt) behind.push(skill.name);
8358
8707
  else current.push(skill.name);
8359
8708
  }
@@ -8383,10 +8732,9 @@ var skillsUsage = `diverge skills <list|install|status> [--harness claude,codex,
8383
8732
  A skill is a procedure over the tools it names; you get the ones whose every
8384
8733
  tool you may run. Re-run install after the organization changes its skills.`;
8385
8734
  async function installableSkillSet(apiUrl, env) {
8386
- const source = { tokenFile: tokenFile(env), baseUrl: apiUrl, clientId: clientId(env) };
8387
- const token = await getUserToken(source);
8735
+ const token = await getUserToken(tokenSource(env, apiUrl));
8388
8736
  const organizationId = tokenClaims(token.access_token).organizationId;
8389
- if (!organizationId) throw new CliError("The login is not bound to an organization; run: diverge org use <id|name>");
8737
+ if (!organizationId) throw new CliError("The login has no default organization; run: diverge org use <id|name>");
8390
8738
  const served = await fetchInstallableSkills(apiUrl, token.access_token);
8391
8739
  return { skills: [DIVERGE_SKILL, ...served.skills], organizationId };
8392
8740
  }
@@ -8408,7 +8756,7 @@ function installForHarness(io, harness, skills, apiUrl, into) {
8408
8756
  }
8409
8757
  async function runSkills(args, io) {
8410
8758
  const [verb, ...rest] = args;
8411
- const { values } = parseArgs({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
8759
+ const { values } = parseArgs2({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
8412
8760
  const apiUrl = resolveApiUrl(void 0, io.env);
8413
8761
  switch (verb) {
8414
8762
  case "list": {
@@ -8439,17 +8787,15 @@ async function runSkills(args, io) {
8439
8787
  return ExitCode.Ok;
8440
8788
  }
8441
8789
  default:
8442
- throw new CliError(`skills needs one of: list, install, status
8443
-
8444
- ${skillsUsage}`);
8790
+ throw unknownVerb("skills", skillsUsage, [], verb, rest);
8445
8791
  }
8446
8792
  }
8447
8793
 
8448
8794
  // src/agents/rotate.ts
8449
8795
  import os7 from "node:os";
8450
- import path9 from "node:path";
8796
+ import path10 from "node:path";
8451
8797
  function defaultTokenFilePath() {
8452
- return path9.join(os7.homedir(), ".divergent", "token");
8798
+ return path10.join(os7.homedir(), ".divergent", "token");
8453
8799
  }
8454
8800
  var MIN_ROTATE_INTERVAL_SECONDS = 60;
8455
8801
  async function rotateTokenFile(loop) {
@@ -8465,14 +8811,14 @@ async function rotateTokenFile(loop) {
8465
8811
 
8466
8812
  // src/agents/self-command.ts
8467
8813
  import fs7 from "node:fs";
8468
- import path10 from "node:path";
8814
+ import path11 from "node:path";
8469
8815
  function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
8470
8816
  const script = argv[1];
8471
8817
  if (!script) return { command: execPath, args: [] };
8472
8818
  if (platform !== "win32") {
8473
8819
  try {
8474
8820
  fs7.accessSync(script, fs7.constants.X_OK);
8475
- return { command: path10.resolve(script), args: [] };
8821
+ return { command: path11.resolve(script), args: [] };
8476
8822
  } catch {
8477
8823
  }
8478
8824
  }
@@ -8521,7 +8867,7 @@ function syncDivergentPreset(io, binary, home) {
8521
8867
  return;
8522
8868
  }
8523
8869
  const dir = writeDivergentPreset(home, shipped);
8524
- const settingsPath = path11.join(home, "settings.yaml");
8870
+ const settingsPath = path12.join(home, "settings.yaml");
8525
8871
  const settings = withDivergentPresetDefault(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null);
8526
8872
  if (settings !== null) writeTextAtomic(settingsPath, settings);
8527
8873
  io.stderr(`Agent preset "${DIVERGENT_PRESET_ID}" (${dir}): web sessions search through the platform${settings !== null ? "; set as the default preset in settings.yaml" : ""}.`);
@@ -8541,18 +8887,22 @@ async function overlayAndDefault(apiUrl, token, surface, io, requested) {
8541
8887
  const defaultModel = dshDefaultModel(models, requested);
8542
8888
  return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models, defaultModel }), defaultModel, models };
8543
8889
  }
8890
+ function organizationArgs(organizationId) {
8891
+ const trimmed = organizationId?.trim();
8892
+ return trimmed ? ["--organization-id", trimmed] : [];
8893
+ }
8544
8894
  async function setup(args, io) {
8545
8895
  const [agent, ...rest] = args;
8546
8896
  const apiUrl = resolveApiUrl(void 0, io.env);
8547
8897
  switch (agent) {
8548
8898
  case "claude": {
8549
- 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 });
8899
+ 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 });
8550
8900
  if (values.scope !== "user" && values.scope !== "project") throw new CliError("--scope must be user or project");
8551
8901
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8552
8902
  const file = claudeSettingsPath(values.scope);
8553
8903
  const { settings, warnings } = mergeClaudeSettings(readJson(file), {
8554
8904
  baseUrl: anthropicBaseUrl(apiUrl),
8555
- tokenHelper: shellCommand(resolveSelfCommand(), "token"),
8905
+ tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
8556
8906
  projectId: values["project-id"],
8557
8907
  modelDiscovery: values["model-discovery"]
8558
8908
  });
@@ -8563,7 +8913,7 @@ async function setup(args, io) {
8563
8913
  return ExitCode.Ok;
8564
8914
  }
8565
8915
  case "opencode": {
8566
- const { values } = parseArgs2({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
8916
+ const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
8567
8917
  const surface = surfaceOf(values.surface, "anthropic");
8568
8918
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8569
8919
  const baseUrl = surfaceBaseUrl(apiUrl, surface);
@@ -8577,12 +8927,12 @@ async function setup(args, io) {
8577
8927
  writeJsonAtomic(file, mergeOpencodeConfig(readJson(file), { baseUrl, surface, models }));
8578
8928
  io.stdout(`OpenCode provider divergent-${surface} \u2192 ${baseUrl}. Wrote ${file}.`);
8579
8929
  io.stdout("Keep DIVERGENT_TOKEN fresh for the session (user tokens expire; the login refreshes):");
8580
- io.stdout(' export DIVERGENT_TOKEN="$(diverge token)"');
8930
+ io.stdout(` export DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})"`);
8581
8931
  return ExitCode.Ok;
8582
8932
  }
8583
8933
  case "codex": {
8584
- const { values } = parseArgs2({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
8585
- const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"] });
8934
+ const { values } = parseArgs3({ args: rest, options: { "project-id": { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" }, print: { type: "boolean", default: false } }, strict: true });
8935
+ const block = codexProviderToml({ openaiBaseUrl: openaiBaseUrl(apiUrl), self: resolveSelfCommand(), projectId: values["project-id"], organizationId: values["organization-id"] });
8586
8936
  if (values.print) {
8587
8937
  io.stdout(block.trimEnd());
8588
8938
  return ExitCode.Ok;
@@ -8597,7 +8947,7 @@ async function setup(args, io) {
8597
8947
  return ExitCode.Ok;
8598
8948
  }
8599
8949
  case "dsh": {
8600
- 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 });
8950
+ 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 });
8601
8951
  const surface = surfaceOf(values.surface, "openai");
8602
8952
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8603
8953
  if (!values.print) await skillsFor(io, apiUrl, "dsh");
@@ -8606,31 +8956,27 @@ async function setup(args, io) {
8606
8956
  io.stdout(overlay.trimEnd());
8607
8957
  return ExitCode.Ok;
8608
8958
  }
8609
- const file = values.into ?? path11.join(dshHome(io.env), `divergent-${surface}.yml`);
8959
+ const file = values.into ?? path12.join(dshHome(io.env), `divergent-${surface}.yml`);
8610
8960
  writeTextAtomic(file, overlay);
8611
8961
  io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
8612
8962
  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.");
8613
8963
  syncDivergentPreset(io, findOnPath("dsh", io.env), dshHome(io.env));
8614
8964
  io.stdout("Boot a profile with the overlay and the token in its environment (user tokens expire; the login refreshes):");
8615
- io.stdout(` DIVERGENT_TOKEN="$(diverge token)" dsh --profile headless --patch ${file} "<task>"`);
8965
+ io.stdout(` DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})" dsh --profile headless --patch ${file} "<task>"`);
8616
8966
  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`);
8617
8967
  return ExitCode.Ok;
8618
8968
  }
8619
8969
  default:
8620
- throw new CliError(`agents setup needs one of: claude, opencode, codex, dsh
8621
-
8622
- ${agentsUsage}`);
8970
+ throw unknownVerb("agents", agentsUsage, ["setup"], agent, rest);
8623
8971
  }
8624
8972
  }
8625
8973
  async function run(args, io) {
8626
8974
  const [agent, ...rest] = args;
8627
- if (agent !== "dsh") throw new CliError(`agents run supports: dsh
8628
-
8629
- ${agentsUsage}`);
8975
+ if (agent !== "dsh") throw unknownVerb("agents", agentsUsage, ["run"], agent, rest);
8630
8976
  const terminator = rest.indexOf("--");
8631
8977
  const flags = terminator === -1 ? rest : rest.slice(0, terminator);
8632
8978
  const passthrough = terminator === -1 ? [] : rest.slice(terminator + 1);
8633
- const { values } = parseArgs2({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
8979
+ const { values } = parseArgs3({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
8634
8980
  const surface = surfaceOf(values.surface, "openai");
8635
8981
  const apiUrl = resolveApiUrl(void 0, io.env);
8636
8982
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
@@ -8639,7 +8985,7 @@ ${agentsUsage}`);
8639
8985
  await skillsFor(io, apiUrl, "dsh");
8640
8986
  const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
8641
8987
  const home = dshHome(io.env);
8642
- const settingsPath = path11.join(home, "settings.yaml");
8988
+ const settingsPath = path12.join(home, "settings.yaml");
8643
8989
  let pinned;
8644
8990
  try {
8645
8991
  pinned = dshSettingsDefaultModel(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null, surface);
@@ -8648,22 +8994,22 @@ ${agentsUsage}`);
8648
8994
  }
8649
8995
  const served = servedIds(overlay);
8650
8996
  if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
8651
- const overlayPath = path11.join(home, `divergent-${surface}.yml`);
8997
+ const overlayPath = path12.join(home, `divergent-${surface}.yml`);
8652
8998
  writeTextAtomic(overlayPath, overlay);
8653
8999
  syncDivergentPreset(io, binary, home);
8654
9000
  io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
8655
9001
  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>"');
8656
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
9002
+ const source = tokenSource(io.env, apiUrl);
8657
9003
  return runDsh({
8658
9004
  binary,
8659
9005
  overlayPath,
8660
- credentialsPath: path11.join(home, ".credentials.yaml"),
9006
+ credentialsPath: path12.join(home, ".credentials.yaml"),
8661
9007
  args: passthrough,
8662
9008
  env: io.env,
8663
9009
  initialToken: token.access_token,
8664
9010
  freshToken: async () => {
8665
9011
  try {
8666
- return (await getUserToken(source, 120)).access_token;
9012
+ return (await getUserToken(source, { freshForSeconds: 120, organizationId: values["organization-id"] })).access_token;
8667
9013
  } catch (error) {
8668
9014
  io.stderr(`warning: could not refresh the platform token: ${error instanceof Error ? error.message : String(error)}`);
8669
9015
  return null;
@@ -8672,7 +9018,7 @@ ${agentsUsage}`);
8672
9018
  });
8673
9019
  }
8674
9020
  async function status(args, io) {
8675
- const { values } = parseArgs2({ args, options: { "organization-id": { type: "string" } }, strict: true });
9021
+ const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
8676
9022
  const apiUrl = resolveApiUrl(void 0, io.env);
8677
9023
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8678
9024
  const claims = tokenClaims(token.access_token);
@@ -8686,35 +9032,25 @@ async function status(args, io) {
8686
9032
  return ExitCode.Ok;
8687
9033
  }
8688
9034
  async function rotate(args, io) {
8689
- const { values } = parseArgs2({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
9035
+ const { values } = parseArgs3({ args, options: { into: { type: "string" }, interval: { type: "string", default: "600" }, once: { type: "boolean", default: false }, "organization-id": { type: "string" } }, strict: true });
8690
9036
  const interval = Number(values.interval);
8691
9037
  if (!Number.isFinite(interval) || interval <= 0) throw new CliError("--interval must be a positive number of seconds");
8692
9038
  const apiUrl = resolveApiUrl(void 0, io.env);
8693
9039
  const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
8694
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
9040
+ const source = tokenSource(io.env, apiUrl);
8695
9041
  const tokenFilePath = values.into ?? defaultTokenFilePath();
8696
9042
  io.stderr(`Keeping ${tokenFilePath} fresh as you (organization ${tokenClaims(token.access_token).organizationId})`);
8697
9043
  return rotateTokenFile({
8698
9044
  tokenFile: tokenFilePath,
8699
9045
  intervalSeconds: interval,
8700
9046
  once: values.once,
8701
- freshToken: async () => (await getUserToken(source, Math.max(interval, 60) + 60)).access_token,
9047
+ freshToken: async () => (await getUserToken(source, { freshForSeconds: Math.max(interval, 60) + 60, organizationId: values["organization-id"] })).access_token,
8702
9048
  report: (line) => io.stderr(line),
8703
9049
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
8704
9050
  });
8705
9051
  }
8706
- function agentsVerbUsage(verb) {
8707
- const lines = agentsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `) || line.trimStart().startsWith(`${verb} `));
8708
- return lines.length ? `diverge agents ${verb} ...
8709
-
8710
- ${lines.join("\n")}` : agentsUsage;
8711
- }
8712
9052
  async function runAgents(args, io) {
8713
9053
  const [verb, ...rest] = args;
8714
- if (verb && (rest.includes("--help") || rest.includes("-h")) && !rest.includes("--")) {
8715
- io.stdout(agentsVerbUsage(verb));
8716
- return ExitCode.Ok;
8717
- }
8718
9054
  switch (verb) {
8719
9055
  case "setup":
8720
9056
  return setup(rest, io);
@@ -8725,226 +9061,71 @@ async function runAgents(args, io) {
8725
9061
  case "status":
8726
9062
  return status(rest, io);
8727
9063
  default:
8728
- throw new CliError(`agents needs one of: setup, run, rotate, status
8729
-
8730
- ${agentsUsage}`);
8731
- }
8732
- }
8733
-
8734
- // src/commands/login.ts
8735
- import { parseArgs as parseArgs3 } from "node:util";
8736
-
8737
- // src/auth/organizations.ts
8738
- function chooseOrganization(input) {
8739
- const memberships = input.memberships;
8740
- const flag = input.flag?.trim();
8741
- if (flag) {
8742
- const match = memberships.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
8743
- if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
8744
- if (input.platformActor) return { kind: "chosen", organizationId: flag, reason: "flag" };
8745
- throw new CliError(`Not one of your organizations: ${flag}. Yours: ${memberships.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
8746
- }
8747
- const remembered = input.remembered?.trim();
8748
- if (remembered) {
8749
- const match = memberships.find((organization) => organization.id === remembered);
8750
- if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
8751
- if (input.platformActor) return { kind: "chosen", organizationId: remembered, reason: "remembered" };
8752
- }
8753
- if (memberships.length === 1) return { kind: "chosen", organizationId: memberships[0].id, name: memberships[0].name, reason: "only" };
8754
- if (memberships.length === 0) return { kind: "none" };
8755
- return { kind: "ask", options: memberships };
8756
- }
8757
- function organizationMenu(options) {
8758
- return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
8759
- }
8760
- function pickFromMenu(answer, options) {
8761
- const trimmed = answer.trim();
8762
- const index = Number(trimmed);
8763
- if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
8764
- const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
8765
- if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
8766
- return match;
8767
- }
8768
-
8769
- // src/auth/userinfo.ts
8770
- async function fetchUserInfo(baseUrl, accessToken) {
8771
- return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
8772
- }
8773
- async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
8774
- try {
8775
- const info = await fetchUserInfo(baseUrl, accessToken);
8776
- return options.withSub ? `${info.email} (${info.sub})` : info.email;
8777
- } catch (error) {
8778
- return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
8779
- }
8780
- }
8781
-
8782
- // src/prompt.ts
8783
- import readline from "node:readline";
8784
- function ask(question) {
8785
- const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
8786
- return new Promise((resolve) => rl.question(question, (answer) => {
8787
- rl.close();
8788
- resolve(answer);
8789
- }));
8790
- }
8791
-
8792
- // src/commands/login.ts
8793
- var DEFAULT_SCOPES = "mcp:read mcp:execute inference:execute";
8794
- var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
8795
-
8796
- Sign in through the browser, then choose where to act. The browser only
8797
- authenticates the person; the organization is chosen here afterwards \u2014 the
8798
- one named by --organization-id (id or name), else the last login's, else the
8799
- only membership, else a menu \u2014 and the login is bound to it on the platform.
8800
- Switch later with: diverge org use <id|name>.
8801
- --as <email> names the account: the platform signs that account in instead of
8802
- reusing whatever session the browser holds, and issues nothing to any other.
8803
- Without --as, the account of the last login is used; switching is explicit.
8804
- Without --scopes, the scopes of the last login are asked for again; a first
8805
- login asks for the CLI's own (mcp:read mcp:execute inference:execute).`;
8806
- function resolveScopes(flag, config) {
8807
- const explicit = flag?.trim();
8808
- if (explicit) return explicit.split(/\s+/).join(" ");
8809
- const remembered = config.scopes?.trim();
8810
- return remembered ? remembered.split(/\s+/).join(" ") : DEFAULT_SCOPES;
8811
- }
8812
- function resolveLoginHint(flag, config) {
8813
- const explicit = flag?.trim().toLowerCase();
8814
- if (explicit) return explicit;
8815
- const remembered = config.account?.trim().toLowerCase();
8816
- return remembered || void 0;
8817
- }
8818
- async function runLogin(args, io) {
8819
- const { values } = parseArgs3({
8820
- args,
8821
- options: {
8822
- "organization-id": { type: "string" },
8823
- scopes: { type: "string" },
8824
- as: { type: "string" },
8825
- "no-browser": { type: "boolean", default: false },
8826
- "api-url": { type: "string" }
8827
- },
8828
- strict: true,
8829
- allowPositionals: false
8830
- });
8831
- const config = readConfig(io.env);
8832
- const apiUrl = resolveApiUrl(values["api-url"], io.env, config);
8833
- if (values["api-url"]) writeConfig({ ...config, apiUrl }, io.env);
8834
- const loginHint = resolveLoginHint(values.as, config);
8835
- if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
8836
- const scopes = resolveScopes(values.scopes, config);
8837
- io.stdout(`Asking for scopes: ${scopes}${values.scopes ? "" : config.scopes ? " (the last login; pass --scopes to change)" : ""}.`);
8838
- const token = await login({
8839
- baseUrl: apiUrl,
8840
- clientId: clientId(io.env),
8841
- scopes,
8842
- openBrowser: !values["no-browser"],
8843
- tokenFile: tokenFile(io.env),
8844
- loginHint,
8845
- print: io.stdout
8846
- });
8847
- const claims = tokenClaims(token.access_token);
8848
- let info;
8849
- try {
8850
- info = await fetchUserInfo(apiUrl, token.access_token);
8851
- } catch (error) {
8852
- throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
9064
+ throw unknownVerb("agents", agentsUsage, [], verb, rest);
8853
9065
  }
8854
- io.stdout(`Signed in as: ${info.email}`);
8855
- const bound = await bindChosenOrganization(io, apiUrl, values["organization-id"], config.organization, info);
8856
- const remaining = Math.max(0, Math.round(bound.token.expires_at - Date.now() / 1e3));
8857
- io.stdout(`Logged in to ${apiUrl}; token cached (${remaining}s to expiry, auto-refreshes).`);
8858
- writeConfig({ ...readConfig(io.env), account: info.email, scopes, organization: bound.organizationId }, io.env);
8859
- const granted = new Set(tokenClaims(bound.token.access_token).scopes ?? []);
8860
- const missing = scopes.split(" ").filter((scope) => scope && !granted.has(scope));
8861
- if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
8862
- return 0;
8863
- }
8864
- async function bindChosenOrganization(io, apiUrl, flag, remembered, info) {
8865
- const memberships = info.organizations ?? [];
8866
- let choice = chooseOrganization({ flag, remembered, memberships, platformActor: info.platformActor });
8867
- if (choice.kind === "none") {
8868
- throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
8869
- }
8870
- if (choice.kind === "ask") {
8871
- if (!io.interactive) {
8872
- throw new CliError(`You belong to several organizations; pass --organization-id <id|name>:
8873
- ${organizationMenu(choice.options).join("\n")}`);
8874
- }
8875
- io.stdout("Choose the organization to act in:");
8876
- for (const line of organizationMenu(choice.options)) io.stdout(line);
8877
- const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
8878
- choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
8879
- }
8880
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
8881
- const token = await bindOrganization(source, choice.organizationId);
8882
- const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
8883
- const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
8884
- io.stdout(`Organization: ${label}${why}`);
8885
- return { organizationId: choice.organizationId, token };
8886
9066
  }
8887
9067
 
8888
9068
  // src/commands/org.ts
8889
9069
  import { parseArgs as parseArgs4 } from "node:util";
8890
9070
  var orgUsage = `diverge org [use <id|name>]
8891
9071
 
8892
- Without a verb: the organizations you may act in, the bound one marked.
8893
- use <id|name>: bind the login to that organization on the platform, without a
8894
- browser; every later command acts there. A platform actor may name an
8895
- organization they are not a member of.`;
9072
+ (no verb) list the organizations you may act in, the default marked
9073
+ use <id|name> make it the default: where every command that names no organization acts,
9074
+ in every shell on this machine that shares the login
9075
+
9076
+ The default is a local preference: the platform is asked only for a token
9077
+ there, which proves you may act in it. For one command, pass
9078
+ --organization-id <id> (or set DIVERGENT_ORG_ID) instead. A platform actor may
9079
+ act in, and so sees, every organization.`;
8896
9080
  async function runOrg(args, io) {
8897
9081
  const { positionals } = parseArgs4({ args, options: {}, strict: true, allowPositionals: true });
8898
- const apiUrl = resolveApiUrl(void 0, io.env);
8899
- const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
8900
- const token = await getUserToken(source);
8901
- const info = await fetchUserInfo(apiUrl, token.access_token);
8902
- const memberships = info.organizations ?? [];
8903
- const current = tokenClaims(token.access_token).organizationId ?? void 0;
8904
9082
  const verb = positionals[0];
9083
+ if (verb && verb !== "use") throw unknownVerb("org", orgUsage, [], verb, positionals.slice(1));
9084
+ const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
9085
+ const info = await fetchUserInfo(source.baseUrl, (await getUserToken(source)).access_token);
9086
+ const organizations = info.organizations ?? [];
9087
+ const current = requireLogin(source).organization;
8905
9088
  if (!verb) {
8906
- if (memberships.length === 0 && !current) {
9089
+ if (organizations.length === 0) {
8907
9090
  io.stdout("You are not a member of any organization.");
8908
9091
  return 0;
8909
9092
  }
8910
- for (const organization of memberships) io.stdout(`${organization.id === current ? "*" : " "} ${organization.name} (${organization.id})`);
8911
- if (current && !memberships.some((organization) => organization.id === current)) io.stdout(`* ${current} (not a membership; acting as the platform)`);
9093
+ for (const organization of organizations) io.stdout(`${organization.id === current ? "*" : " "} ${organization.name} (${organization.id})`);
8912
9094
  return 0;
8913
9095
  }
8914
- if (verb !== "use") throw new CliError(`Unknown verb: ${verb}
8915
-
8916
- ${orgUsage}`);
8917
9096
  const target = positionals[1];
8918
9097
  if (!target) throw new CliError("org use needs an organization id or name");
8919
- const choice = chooseOrganization({ flag: target, memberships, platformActor: info.platformActor });
9098
+ const choice = chooseOrganization({ flag: target, organizations });
8920
9099
  if (choice.kind !== "chosen") throw new CliError("org use needs an organization id or name");
8921
- const bound = await bindOrganization(source, choice.organizationId);
8922
- writeConfig({ ...readConfig(io.env), organization: choice.organizationId }, io.env);
8923
- io.stdout(`Organization: ${choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId}; scopes ${(tokenClaims(bound.access_token).scopes ?? []).join(" ") || "none"}`);
9100
+ const token = await useOrganization(source, choice.organizationId);
9101
+ io.stdout(`Default organization: ${choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId}; scopes ${(tokenClaims(token.access_token).scopes ?? []).join(" ") || "none"}`);
8924
9102
  return 0;
8925
9103
  }
8926
9104
 
8927
9105
  // src/commands/logout.ts
8928
- var logoutUsage = "diverge logout\n\nRevoke the login's session on the platform and forget it locally.";
9106
+ 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.";
8929
9107
  async function runLogout(_args, io) {
8930
- const file = tokenFile(io.env);
8931
- const token = loadUserToken(file);
9108
+ const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
9109
+ const token = loadLogin(source);
8932
9110
  if (!token) {
8933
- io.stdout("Not logged in.");
9111
+ io.stdout(`Not logged in to ${source.baseUrl}.`);
8934
9112
  return 0;
8935
9113
  }
8936
9114
  if (token.refresh_token) {
8937
9115
  try {
8938
- await revokeRefreshToken(token.base_url, clientId(io.env), token.refresh_token);
9116
+ await revokeRefreshToken(source.baseUrl, source.clientId, token.refresh_token);
8939
9117
  } catch (error) {
8940
- io.stderr(`warning: could not revoke the session on ${token.base_url}: ${error instanceof Error ? error.message : String(error)}`);
9118
+ io.stderr(`warning: could not revoke the session on ${source.baseUrl}: ${error instanceof Error ? error.message : String(error)}`);
8941
9119
  }
8942
9120
  }
8943
- deleteUserToken(file);
8944
- io.stdout(`Logged out of ${token.base_url}${token.base_url === resolveApiUrl(void 0, io.env) ? "" : " (a different deployment than the configured one)"}.`);
9121
+ forgetLogin(source);
9122
+ io.stdout(`Logged out of ${source.baseUrl}.`);
8945
9123
  return 0;
8946
9124
  }
8947
9125
 
9126
+ // src/commands/status.ts
9127
+ import path13 from "node:path";
9128
+
8948
9129
  // src/tools/settings.ts
8949
9130
  var BRIDGE_SCOPES = "mcp:read mcp:execute";
8950
9131
  var LOGIN_HINT = "diverge login";
@@ -8952,21 +9133,19 @@ function loadToolsSettings(options, env = process.env) {
8952
9133
  const apiUrl = resolveApiUrl(void 0, env);
8953
9134
  const projectId = options.projectId?.trim() || env.DIVERGENT_PROJECT_ID?.trim() || void 0;
8954
9135
  let organizationId = options.organizationId?.trim() || env.DIVERGENT_ORG_ID?.trim() || "";
8955
- const cached = loadUserToken(tokenFile(env));
8956
- if (cached && cached.base_url === apiUrl) {
8957
- organizationId ||= tokenClaims(cached.access_token).organizationId?.trim() || "";
8958
- if (!organizationId) throw new CliError(`The login carries no organization. Pass --organization-id or sign in to one: ${LOGIN_HINT} --organization-id <id>`);
9136
+ const cached = loadLogin(tokenSource(env, apiUrl));
9137
+ if (cached) {
9138
+ organizationId ||= cached.organization ?? "";
9139
+ if (!organizationId) throw new CliError(`The login has no default organization. Pass --organization-id or choose one: diverge org use <id|name>`);
8959
9140
  return { apiUrl, organizationId, projectId, mode: "operator", clientId: clientId(env) };
8960
9141
  }
8961
- if (!organizationId) throw new CliError("No organization configured. Pass --organization-id or set DIVERGENT_ORG_ID.");
8962
9142
  const serviceClientId = env.DIVERGENT_CLIENT_ID?.trim() ?? "";
8963
9143
  const serviceClientSecret = env.DIVERGENT_CLIENT_SECRET?.trim() ?? "";
8964
9144
  const delegatedUserId = env.DIVERGENT_USER_ID?.trim() ?? "";
8965
9145
  if (!serviceClientId || !serviceClientSecret) {
8966
- throw new CliError(`Not signed in for ${apiUrl} and no service credentials configured.
8967
- Sign in as yourself: ${LOGIN_HINT}
8968
- Or set DIVERGENT_CLIENT_ID and DIVERGENT_CLIENT_SECRET.`);
9146
+ throw noLogin(apiUrl, "Or, unattended, set DIVERGENT_CLIENT_ID, DIVERGENT_CLIENT_SECRET, DIVERGENT_USER_ID and DIVERGENT_ORG_ID.");
8969
9147
  }
9148
+ if (!organizationId) throw new CliError("Service credentials act in the organization named: pass --organization-id or set DIVERGENT_ORG_ID.");
8970
9149
  if (!delegatedUserId) {
8971
9150
  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.
8972
9151
  Signing in as yourself avoids this entirely: ${LOGIN_HINT}`);
@@ -8984,10 +9163,9 @@ function actingAsLine(settings, env = process.env) {
8984
9163
  actingAs ??= (async () => {
8985
9164
  let accessToken;
8986
9165
  try {
8987
- accessToken = (await getUserToken({ tokenFile: tokenFile(env), baseUrl: settings.apiUrl, clientId: settings.clientId })).access_token;
9166
+ accessToken = (await getUserToken(tokenSource(env, settings.apiUrl))).access_token;
8988
9167
  } catch (error) {
8989
- const sub = tokenClaims(loadUserToken(tokenFile(env))?.access_token ?? "").sub;
8990
- return `Acting as: ${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
9168
+ return `Acting as: unknown user (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
8991
9169
  }
8992
9170
  return `Acting as: ${await describeAccount(settings.apiUrl, accessToken, tokenClaims(accessToken).sub)}`;
8993
9171
  })();
@@ -9007,54 +9185,73 @@ function describeToolExecution(env = process.env) {
9007
9185
  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.";
9008
9186
  async function runStatus(_args, io) {
9009
9187
  const apiUrl = resolveApiUrl(void 0, io.env);
9188
+ const source = tokenSource(io.env, apiUrl);
9010
9189
  io.stdout(`diverge ${VERSION}`);
9011
9190
  io.stdout(`Deployment: ${apiUrl}`);
9012
- io.stdout(`State: ${configDir(io.env)}`);
9013
- const token = loadUserToken(tokenFile(io.env));
9014
- if (!token) {
9015
- io.stdout("Not logged in. Run: diverge login");
9191
+ io.stdout(`State: ${path13.dirname(source.loginFile)}`);
9192
+ const record = loadLogin(source);
9193
+ if (!record) {
9194
+ io.stdout(`Not logged in. Run: diverge login${apiUrl === DEFAULT_API_URL ? "" : ` --api-url ${apiUrl}`}`);
9016
9195
  io.stdout(describeToolExecution(io.env));
9017
9196
  return 1;
9018
9197
  }
9019
- const claims = tokenClaims(token.access_token);
9020
- const remaining = Math.round(token.expires_at - Date.now() / 1e3);
9021
- const mismatch = token.base_url !== apiUrl ? ` (for ${token.base_url}, not the configured deployment)` : "";
9022
- io.stdout(`Login: ${isFresh(token, 0) ? `valid for ${remaining}s` : "expired (auto-refreshes)"}; refresh token ${token.refresh_token ? "present" : "absent"}${mismatch}`);
9023
- const account = await signedInAs(token, io);
9198
+ io.stdout(`Login: ${describeSignIn(record)}`);
9199
+ const account = await signedInAs(source);
9024
9200
  io.stdout(`Signed in as: ${account.line}`);
9025
- const organization = account.organizations?.find((candidate) => candidate.id === claims.organizationId);
9026
- io.stdout(`Organization: ${claims.organizationId ? `${organization ? `${organization.name} (${claims.organizationId})` : claims.organizationId}` : "none (run: diverge org use <id|name>)"}`);
9027
- io.stdout(`Scopes: ${(claims.scopes ?? []).join(" ") || "none"}`);
9201
+ const organization = account.organizations?.find((candidate) => candidate.id === record.organization);
9202
+ 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>)"}`);
9203
+ io.stdout(`Scopes: ${account.scopes ?? "unknown"}`);
9028
9204
  io.stdout(describeToolExecution(io.env));
9029
9205
  return 0;
9030
9206
  }
9031
- async function signedInAs(cached, io) {
9032
- const sub = tokenClaims(cached.access_token).sub;
9207
+ async function signedInAs(source) {
9033
9208
  let accessToken;
9034
9209
  try {
9035
- accessToken = (await getUserToken({ tokenFile: tokenFile(io.env), baseUrl: cached.base_url, clientId: clientId(io.env) })).access_token;
9210
+ accessToken = (await getUserToken(source)).access_token;
9036
9211
  } catch (error) {
9037
- return { line: `${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})` };
9212
+ return { line: `unknown (${error instanceof Error ? error.message : String(error)})` };
9038
9213
  }
9214
+ const claims = tokenClaims(accessToken);
9215
+ const scopes = (claims.scopes ?? []).join(" ") || "none";
9039
9216
  try {
9040
- const info = await fetchUserInfo(cached.base_url, accessToken);
9041
- return { line: `${info.email} (${info.sub})`, organizations: info.organizations };
9217
+ const info = await fetchUserInfo(source.baseUrl, accessToken);
9218
+ return { line: `${info.email} (${info.sub})`, organizations: info.organizations, scopes };
9042
9219
  } catch (error) {
9043
- return { line: `${sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})` };
9220
+ return { line: `${claims.sub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`, scopes };
9044
9221
  }
9045
9222
  }
9223
+ function describeSignIn(record, nowMs = Date.now()) {
9224
+ const { idle, cap } = loginExpiry(record);
9225
+ if (idle === null) return "refresh token present";
9226
+ const at = (seconds) => new Date(seconds * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
9227
+ const left = (seconds) => {
9228
+ const remaining = seconds - nowMs / 1e3;
9229
+ const hours = Math.floor(remaining / 3600);
9230
+ return hours >= 24 ? `${Math.floor(hours / 24)}d ${hours % 24}h` : `${hours}h ${Math.floor(remaining % 3600 / 60)}m`;
9231
+ };
9232
+ if (idle <= nowMs / 1e3) return `sign-in expired at ${at(idle)} (run: diverge login)`;
9233
+ const idleLine = `renews while used; idle until ${at(idle)} (${left(idle)} left)`;
9234
+ if (cap === null || cap <= idle) return cap === null ? idleLine : `ends ${at(idle)} (${left(idle)} left; 30-day limit, run: diverge login then)`;
9235
+ return `${idleLine}; ends at the latest ${at(cap)} (${left(cap)} left)`;
9236
+ }
9046
9237
 
9047
9238
  // src/commands/token.ts
9048
- var tokenUsage = "diverge token\n\nPrint a fresh access token and nothing else: the credential helper for Claude Code and Codex.";
9049
- async function runToken(_args, io) {
9050
- const apiUrl = resolveApiUrl(void 0, io.env);
9051
- const token = await getUserToken({ tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) });
9239
+ import { parseArgs as parseArgs5 } from "node:util";
9240
+ var tokenUsage = `diverge token [--organization-id <id>]
9241
+
9242
+ Print a fresh access token and nothing else: the credential helper for Claude
9243
+ Code, Codex and forge. The token is for the organization named by
9244
+ --organization-id or DIVERGENT_ORG_ID, else the login's default.`;
9245
+ async function runToken(args, io) {
9246
+ const { values } = parseArgs5({ args, options: { "organization-id": { type: "string" } }, strict: true, allowPositionals: false });
9247
+ const organizationId = values["organization-id"]?.trim() || io.env.DIVERGENT_ORG_ID?.trim() || void 0;
9248
+ const token = await getUserToken(tokenSource(io.env, resolveApiUrl(void 0, io.env)), { organizationId });
9052
9249
  io.stdout(token.access_token);
9053
9250
  return 0;
9054
9251
  }
9055
9252
 
9056
9253
  // src/commands/tools.ts
9057
- import { parseArgs as parseArgs5 } from "node:util";
9254
+ import { parseArgs as parseArgs6 } from "node:util";
9058
9255
 
9059
9256
  // src/mcp/bridge.ts
9060
9257
  import readline2 from "node:readline";
@@ -9078,26 +9275,26 @@ var ToolsClient = class {
9078
9275
  serviceToken = null;
9079
9276
  async token() {
9080
9277
  if (this.settings.mode === "operator") {
9081
- try {
9082
- return (await getUserToken({ tokenFile: tokenFile(this.env), baseUrl: this.settings.apiUrl, clientId: this.settings.clientId })).access_token;
9083
- } catch (error) {
9084
- throw new CliError(`${error instanceof Error ? error.message : String(error)}
9085
- Sign in with: ${LOGIN_HINT}`);
9086
- }
9278
+ return (await getUserToken(tokenSource(this.env, this.settings.apiUrl), { organizationId: this.settings.organizationId })).access_token;
9087
9279
  }
9088
9280
  if (this.serviceToken && Date.now() < this.serviceToken.expiresAtMs) return this.serviceToken.value;
9089
- const response = await this.fetchImpl(tokenEndpoint(this.settings.apiUrl), {
9090
- method: "POST",
9091
- headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
9092
- body: JSON.stringify({
9093
- grant_type: "client_credentials",
9094
- client_id: this.settings.serviceClientId,
9095
- client_secret: this.settings.serviceClientSecret,
9096
- scopes: BRIDGE_SCOPES,
9097
- organization_id: this.settings.organizationId
9098
- }),
9099
- signal: AbortSignal.timeout(3e4)
9100
- });
9281
+ let response;
9282
+ try {
9283
+ response = await this.fetchImpl(tokenEndpoint(this.settings.apiUrl), {
9284
+ method: "POST",
9285
+ headers: { "Content-Type": "application/json", "User-Agent": USER_AGENT },
9286
+ body: JSON.stringify({
9287
+ grant_type: "client_credentials",
9288
+ client_id: this.settings.serviceClientId,
9289
+ client_secret: this.settings.serviceClientSecret,
9290
+ scopes: BRIDGE_SCOPES,
9291
+ organization_id: this.settings.organizationId
9292
+ }),
9293
+ signal: AbortSignal.timeout(3e4)
9294
+ });
9295
+ } catch (error) {
9296
+ throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
9297
+ }
9101
9298
  if (!response.ok) throw new CliError(`Token acquisition failed (${response.status}): ${(await response.text()).slice(0, 400)}`);
9102
9299
  const body = await response.json();
9103
9300
  if (!body.access_token) throw new CliError("Token response contained no access_token");
@@ -9110,10 +9307,10 @@ var ToolsClient = class {
9110
9307
  if (this.settings.mode === "service") headers["x-divergent-user-id"] = this.settings.delegatedUserId;
9111
9308
  return headers;
9112
9309
  }
9113
- scoped(path13) {
9310
+ scoped(path15) {
9114
9311
  const query = new URLSearchParams({ organizationId: this.settings.organizationId });
9115
9312
  if (this.settings.projectId) query.set("projectId", this.settings.projectId);
9116
- return `${this.settings.apiUrl}${path13}?${query.toString()}`;
9313
+ return `${this.settings.apiUrl}${path15}?${query.toString()}`;
9117
9314
  }
9118
9315
  async send(method, url, body) {
9119
9316
  return this.request(method, url, body, void 0);
@@ -9125,9 +9322,10 @@ var ToolsClient = class {
9125
9322
  */
9126
9323
  async request(method, url, body, refused) {
9127
9324
  for (let attempt = 1; attempt <= 2; attempt += 1) {
9325
+ const headers = await this.headers();
9128
9326
  let response;
9129
9327
  try {
9130
- 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) });
9328
+ response = await this.fetchImpl(url, { method, headers, body: body === void 0 ? void 0 : JSON.stringify(body), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
9131
9329
  } catch (error) {
9132
9330
  throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
9133
9331
  }
@@ -9159,8 +9357,32 @@ var ToolsClient = class {
9159
9357
  const body = await this.send("GET", this.scoped("/api/plugins/functions/tools"));
9160
9358
  return body.tools ?? [];
9161
9359
  }
9162
- callTool(name, args) {
9163
- return this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), { toolName: name, arguments: args ?? {} });
9360
+ /**
9361
+ * One tool, looked up at its source when the catalog does not list it yet —
9362
+ * a tool deployed after its server's catalog was read. A 404 is an answer.
9363
+ */
9364
+ describeTool(name) {
9365
+ return this.request("GET", this.scoped(`/api/plugins/functions/tools/${encodeURIComponent(name)}`), void 0, (status2, text) => {
9366
+ if (status2 !== 404) return void 0;
9367
+ try {
9368
+ const parsed = JSON.parse(text);
9369
+ if (typeof parsed.error === "string" && parsed.error) return { notFound: parsed.error };
9370
+ } catch {
9371
+ }
9372
+ return { notFound: `No tool named '${name}' is available.` };
9373
+ });
9374
+ }
9375
+ /**
9376
+ * One call. The platform answers a completed call with its invocation
9377
+ * envelope, `{status: 'succeeded', invocationId?, result}`; the tool's own
9378
+ * payload is `result` inside it. Every caller wants the payload, so it is
9379
+ * unwrapped here, once — a pending approval passes through as it is.
9380
+ */
9381
+ async callTool(name, args) {
9382
+ const body = await this.send("POST", this.scoped("/api/plugins/functions/tools/execute"), { toolName: name, arguments: args ?? {} });
9383
+ const outcome = body.result;
9384
+ if (outcome?.status !== "succeeded") return body;
9385
+ return { ...body, result: outcome.result };
9164
9386
  }
9165
9387
  /** The read half of raise-and-read (ADR 0047); anyone but the raiser sees a 404. */
9166
9388
  getInvocation(invocationId) {
@@ -9382,7 +9604,7 @@ async function waitForInvocation(id, options, deps) {
9382
9604
 
9383
9605
  // src/tools/call-args.ts
9384
9606
  import fs9 from "node:fs";
9385
- function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8")) {
9607
+ async function resolveCallArgs(options, stdin = readStdin) {
9386
9608
  if (options.argsJson !== void 0 && options.argsJson !== "{}" && options.argsFile !== void 0) {
9387
9609
  throw new CliError("give the arguments once: --args-json or --args-file");
9388
9610
  }
@@ -9397,7 +9619,7 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
9397
9619
  }
9398
9620
  } else if (options.argsJson === "-") {
9399
9621
  source = "--args-json - (stdin)";
9400
- text = readStdin();
9622
+ text = await stdin();
9401
9623
  } else {
9402
9624
  source = "--args-json";
9403
9625
  text = options.argsJson ?? "{}";
@@ -9411,6 +9633,11 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
9411
9633
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(`${source} must be a JSON object`);
9412
9634
  return parsed;
9413
9635
  }
9636
+ async function readStdin() {
9637
+ const chunks = [];
9638
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
9639
+ return Buffer.concat(chunks).toString("utf8");
9640
+ }
9414
9641
 
9415
9642
  // src/tools/batch.ts
9416
9643
  function parseBatch(text) {
@@ -9445,21 +9672,29 @@ async function runOne(entry, deps) {
9445
9672
  if (body.success === false) return { tool: entry.tool, status: "failed", error: body.error || "Tool execution failed" };
9446
9673
  if (!isPending(body.result)) return { tool: entry.tool, status: "executed", result: body.result ?? null };
9447
9674
  const pending = { tool: entry.tool, status: "pending", invocationId: body.result.invocationId, decideAt: deps.decideAt(body.result) };
9675
+ deps.onPending?.(body.result);
9448
9676
  if (!deps.waitFor) return pending;
9449
9677
  const settled = await deps.waitFor(body.result);
9450
- switch (settled?.status) {
9678
+ return (settled && outcomeOf(entry.tool, pending.invocationId, settled)) ?? pending;
9679
+ }
9680
+ function outcomeOf(tool, invocationId, invocation) {
9681
+ switch (invocation.status) {
9451
9682
  case "succeeded":
9452
- return { tool: entry.tool, status: "executed", result: settled.result ?? null, invocationId: pending.invocationId };
9683
+ return { tool, status: "executed", result: invocation.result ?? null, invocationId };
9453
9684
  case "rejected":
9454
- return { tool: entry.tool, status: "rejected", invocationId: pending.invocationId, error: settled.decisionReason ?? void 0 };
9685
+ return { tool, status: "rejected", invocationId, error: invocation.decisionReason ?? void 0 };
9455
9686
  case "expired":
9456
- return { tool: entry.tool, status: "expired", invocationId: pending.invocationId };
9687
+ return { tool, status: "expired", invocationId };
9457
9688
  case "failed":
9458
- return { tool: entry.tool, status: "failed", invocationId: pending.invocationId, error: settled.errorMessage || "execution failed" };
9689
+ return { tool, status: "failed", invocationId, error: invocation.errorMessage || "execution failed" };
9459
9690
  default:
9460
- return pending;
9691
+ return null;
9461
9692
  }
9462
9693
  }
9694
+ function readBackOutcomeOf(tool, invocationId, invocation) {
9695
+ if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) return { tool, status: "pending", invocationId };
9696
+ return outcomeOf(tool, invocationId, invocation);
9697
+ }
9463
9698
  function worstExitCode(outcomes) {
9464
9699
  const has = (status2) => outcomes.some((outcome) => outcome.status === status2);
9465
9700
  if (has("failed")) return ExitCode.Failure;
@@ -9478,6 +9713,10 @@ function matchesCapability(selector, capability) {
9478
9713
  if (selector === capability) return true;
9479
9714
  return selector.endsWith("*") && capability.startsWith(selector.slice(0, -1));
9480
9715
  }
9716
+ function matchesFilter(filter, tool) {
9717
+ const needle = filter.toLowerCase();
9718
+ return tool.name.toLowerCase().includes(needle) || (tool.description ?? "").toLowerCase().includes(needle);
9719
+ }
9481
9720
  function formatToolList(tools) {
9482
9721
  if (tools.length === 0) return ["No tools available."];
9483
9722
  const nameWidth = Math.max(...tools.map((tool) => tool.name.length));
@@ -9536,38 +9775,29 @@ function formatPending(result, apiUrl, organizationId) {
9536
9775
  lines.push(`Read the outcome later: diverge tools invocation ${result.invocationId} --organization-id ${organizationId}`);
9537
9776
  return lines;
9538
9777
  }
9539
- function settledOutcome(invocation) {
9540
- switch (invocation.status) {
9541
- case "succeeded":
9542
- return { stdout: JSON.stringify(invocation.result ?? null, null, 2), exitCode: ExitCode.Ok };
9543
- case "rejected":
9544
- return { stderr: `Rejected by the operator${invocation.decisionReason ? `: ${invocation.decisionReason}` : "."}`, exitCode: ExitCode.Rejected };
9545
- case "expired":
9546
- return { stderr: "Expired before anyone decided. Raise a fresh call; the old id is final.", exitCode: ExitCode.Expired };
9547
- case "failed":
9548
- throw new CliError(`Approved, but execution failed: ${invocation.errorMessage || "unknown error"}`);
9549
- default:
9550
- throw new CliError(`Unexpected invocation status: ${invocation.status}`);
9551
- }
9552
- }
9553
9778
 
9554
9779
  // src/commands/tools.ts
9555
9780
  var toolsUsage = `diverge tools <list|capabilities|describe|call|run|invocation|bridge> [--organization-id <id>] [--project-id <id>] ...
9556
9781
 
9557
- list [--filter <substring>] [--capability <selector>] [--json]
9782
+ list [--filter <text>] [--capability <selector>] [--json]
9783
+ --filter searches tool names and descriptions, ignoring case
9558
9784
  capabilities [--json]
9559
9785
  describe <name> [--json]
9560
- call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>]
9786
+ call <name> [--args-json '{...}' | --args-json - | --args-file <path>] [--wait] [--timeout <seconds>] [--json]
9561
9787
  arguments inline, from stdin (-), or from a file: PowerShell strips quotes inside inline JSON
9562
- run <file> [--wait] [--timeout <seconds>]
9788
+ --json prints exactly one JSON object on stdout, errors included:
9789
+ {tool, status: executed|pending|rejected|expired|failed, result?, error?, invocationId?, decideAt?}
9790
+ run <file|-> [--wait] [--timeout <seconds>]
9563
9791
  several calls from one file (a JSON array of {tool, args}, or one per line), in order,
9564
9792
  one JSON line of outcome each; exit code is the worst: 1 failed, 4 rejected, 5 expired, 3 pending
9565
9793
  invocation <id> [--wait] [--timeout <seconds>] [--json]
9794
+ --json prints the invocation record; the exit code is call's for the same status
9566
9795
  bridge [--tools <name,name>] (stdio MCP server; the allowlist keeps a harness to the tools it was given)
9567
9796
 
9568
9797
  Discover and execute platform tools as yourself. A write that needs a human is
9569
9798
  persisted as a pending invocation, decided in Compass, and read back here by
9570
- id. Exit codes: 0 executed, 3 pending, 4 rejected, 5 expired.`;
9799
+ id. Exit codes, the same for call, run and invocation, --json or not:
9800
+ 0 executed, 1 failed, 3 pending, 4 rejected, 5 expired.`;
9571
9801
  var SCOPE_OPTIONS = { "organization-id": { type: "string" }, "project-id": { type: "string" } };
9572
9802
  async function client(values, io) {
9573
9803
  const settings = loadToolsSettings({ organizationId: values["organization-id"], projectId: values["project-id"] }, io.env);
@@ -9575,39 +9805,79 @@ async function client(values, io) {
9575
9805
  io.stderr(await actingAsLine(settings, io.env));
9576
9806
  return { settings, client: new ToolsClient(settings, io.env) };
9577
9807
  }
9578
- function emit(io, outcome) {
9579
- if (outcome.stdout !== void 0) io.stdout(outcome.stdout);
9580
- if (outcome.stderr !== void 0) io.stderr(outcome.stderr);
9581
- return outcome.exitCode;
9582
- }
9583
9808
  var realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
9584
- function toolsVerbUsage(verb) {
9585
- const lines = toolsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `));
9586
- return lines.length ? `diverge tools ${verb} ...
9587
-
9588
- ${lines.join("\n")}
9589
-
9590
- Every tools command takes --organization-id <id> (or DIVERGENT_ORG_ID, else the signed-in login's organization) and --project-id <id> (or DIVERGENT_PROJECT_ID).` : toolsUsage;
9809
+ 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 } };
9810
+ function timeoutOf(seconds) {
9811
+ const timeoutMs = Number(seconds) * 1e3;
9812
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
9813
+ return timeoutMs;
9814
+ }
9815
+ function waiter(tools, timeoutMs) {
9816
+ return async (pending) => {
9817
+ const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
9818
+ return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
9819
+ };
9820
+ }
9821
+ async function call(rest, io) {
9822
+ const json = rest.includes("--json");
9823
+ let tool = "";
9824
+ let waited = "";
9825
+ let outcome;
9826
+ try {
9827
+ const { values, positionals } = parseArgs6({ args: rest, options: CALL_OPTIONS, strict: true, allowPositionals: true });
9828
+ tool = positionals[0] ?? "";
9829
+ if (!tool) throw new CliError("call needs a tool name");
9830
+ const args = await resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
9831
+ const timeoutMs = timeoutOf(values.timeout);
9832
+ if (values.wait) waited = values.timeout;
9833
+ const { settings, client: tools } = await client(values, io);
9834
+ outcome = await runOne({ tool, args }, {
9835
+ callTool: (name, callArgs) => tools.callTool(name, callArgs),
9836
+ decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
9837
+ ...json ? {} : { onPending: (pending) => formatPending(pending, settings.apiUrl, settings.organizationId).forEach((line) => io.stdout(line)) },
9838
+ ...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
9839
+ });
9840
+ } catch (error) {
9841
+ if (!json) throw error;
9842
+ outcome = { tool, status: "failed", error: unknownOption(error, toolsUsage, ["call"]) ?? (error instanceof Error ? error.message : String(error)) };
9843
+ }
9844
+ if (json) io.stdout(JSON.stringify(outcome, null, 2));
9845
+ else printOutcome(outcome, waited, io);
9846
+ return worstExitCode([outcome]);
9847
+ }
9848
+ function printOutcome(outcome, waited, io) {
9849
+ switch (outcome.status) {
9850
+ case "executed":
9851
+ io.stdout(JSON.stringify(outcome.result ?? null, null, 2));
9852
+ return;
9853
+ case "pending":
9854
+ if (waited) io.stderr(`Still waiting on the operator after ${waited}s; the invocation remains open.`);
9855
+ return;
9856
+ case "rejected":
9857
+ io.stderr(`Rejected by the operator${outcome.error ? `: ${outcome.error}` : "."}`);
9858
+ return;
9859
+ case "expired":
9860
+ io.stderr("Expired before anyone decided. Raise a fresh call; the old id is final.");
9861
+ return;
9862
+ case "failed":
9863
+ throw new CliError(outcome.invocationId ? `Approved, but execution failed: ${outcome.error}` : outcome.error || "Tool execution failed");
9864
+ }
9591
9865
  }
9592
9866
  async function runTools(args, io) {
9593
9867
  const [verb, ...rest] = args;
9594
- if (verb && (rest.includes("--help") || rest.includes("-h"))) {
9595
- io.stdout(toolsVerbUsage(verb));
9596
- return ExitCode.Ok;
9597
- }
9598
9868
  switch (verb) {
9599
9869
  case "list": {
9600
- const { values } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
9870
+ const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
9601
9871
  const { client: tools } = await client(values, io);
9602
9872
  let list = await tools.listTools();
9603
- if (values.filter) list = list.filter((tool) => tool.name.includes(values.filter));
9873
+ if (values.filter) list = list.filter((tool) => matchesFilter(values.filter, tool));
9604
9874
  if (values.capability) list = list.filter((tool) => matchesCapability(values.capability, tool.capability));
9605
9875
  if (values.json) io.stdout(JSON.stringify(list, null, 2));
9606
9876
  else for (const line of formatToolList(list)) io.stdout(line);
9607
9877
  return ExitCode.Ok;
9608
9878
  }
9609
9879
  case "capabilities": {
9610
- const { values } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
9880
+ const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
9611
9881
  const { client: tools } = await client(values, io);
9612
9882
  const list = await tools.listTools();
9613
9883
  if (values.json) io.stdout(JSON.stringify(capabilityClusters(list), null, 2));
@@ -9615,103 +9885,75 @@ async function runTools(args, io) {
9615
9885
  return ExitCode.Ok;
9616
9886
  }
9617
9887
  case "describe": {
9618
- const { values, positionals } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
9888
+ const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
9619
9889
  const name = positionals[0];
9620
9890
  if (!name) throw new CliError("describe needs a tool name");
9621
9891
  const { settings, client: tools } = await client(values, io);
9622
- const list = await tools.listTools();
9623
- const tool = list.find((entry) => entry.name === name);
9624
- if (!tool) {
9625
- const close = list.filter((entry) => entry.name.includes(name)).map((entry) => entry.name).slice(0, 5);
9626
- throw new CliError(`No tool named '${name}' is available.${close.length ? ` Did you mean: ${close.join(", ")}?` : ""}`);
9892
+ const described = await tools.describeTool(name);
9893
+ if ("notFound" in described) {
9894
+ const close = (await tools.listTools()).filter((entry) => entry.name.includes(name)).map((entry) => entry.name).slice(0, 5);
9895
+ throw new CliError(`${described.notFound}${close.length ? `
9896
+ Did you mean: ${close.join(", ")}?` : ""}`);
9627
9897
  }
9898
+ const { tool } = described;
9628
9899
  if (values.json) io.stdout(JSON.stringify(tool, null, 2));
9629
9900
  else for (const line of formatDescribe(tool, settings.organizationId)) io.stdout(line);
9630
9901
  return ExitCode.Ok;
9631
9902
  }
9632
- case "call": {
9633
- 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 });
9634
- const name = positionals[0];
9635
- if (!name) throw new CliError("call needs a tool name");
9636
- const parsedArgs = resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
9637
- const timeoutMs = Number(values.timeout) * 1e3;
9638
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
9639
- const { settings, client: tools } = await client(values, io);
9640
- const body = await tools.callTool(name, parsedArgs);
9641
- if (body.success === false) throw new CliError(body.error || "Tool execution failed");
9642
- if (!isPending(body.result)) {
9643
- io.stdout(JSON.stringify(body.result ?? null, null, 2));
9644
- return ExitCode.Ok;
9645
- }
9646
- for (const line of formatPending(body.result, settings.apiUrl, settings.organizationId)) io.stdout(line);
9647
- if (!values.wait) return ExitCode.Pending;
9648
- const invocation = await waitForInvocation(body.result.invocationId ?? "", { pollAfterMs: body.result.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
9649
- if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
9650
- io.stderr(`Still ${invocation.status} after ${values.timeout}s; the invocation remains open.`);
9651
- return ExitCode.Pending;
9652
- }
9653
- return emit(io, settledOutcome(invocation));
9654
- }
9903
+ case "call":
9904
+ return call(rest, io);
9655
9905
  case "run": {
9656
- const { values, positionals } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
9906
+ const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, wait: { type: "boolean", default: false }, timeout: { type: "string", default: "600" } }, strict: true, allowPositionals: true });
9657
9907
  const file = positionals[0];
9658
- if (!file) throw new CliError("run needs a file of calls");
9908
+ if (!file) throw new CliError("run needs a file of calls, or - for stdin");
9659
9909
  let entries;
9660
9910
  try {
9661
- entries = parseBatch(fs10.readFileSync(file, "utf8"));
9911
+ entries = parseBatch(file === "-" ? await readStdin() : fs10.readFileSync(file, "utf8"));
9662
9912
  } catch (error) {
9663
9913
  throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
9664
9914
  }
9665
- const timeoutMs = Number(values.timeout) * 1e3;
9666
- if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
9915
+ const timeoutMs = timeoutOf(values.timeout);
9667
9916
  const { settings, client: tools } = await client(values, io);
9668
9917
  const { exitCode } = await runBatch(entries, {
9669
9918
  callTool: (tool, args2) => tools.callTool(tool, args2),
9670
9919
  decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
9671
9920
  onOutcome: (outcome) => io.stdout(JSON.stringify(outcome)),
9672
- ...values.wait ? {
9673
- waitFor: async (pending) => {
9674
- const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
9675
- return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
9676
- }
9677
- } : {}
9921
+ ...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
9678
9922
  });
9679
9923
  return exitCode;
9680
9924
  }
9681
9925
  case "invocation": {
9682
- 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 });
9926
+ 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 });
9683
9927
  const id = positionals[0];
9684
9928
  if (!id) throw new CliError("invocation needs an id");
9685
9929
  const { client: tools } = await client(values, io);
9686
9930
  let invocation;
9687
9931
  if (values.wait) invocation = await waitForInvocation(id, { timeoutMs: Number(values.timeout) * 1e3 }, { getInvocation: (target) => tools.getInvocation(target), sleep: realSleep, now: Date.now });
9688
9932
  else invocation = (await tools.getInvocation(id)).invocation ?? {};
9933
+ const outcome = readBackOutcomeOf(invocation.toolName ?? "", id, invocation);
9689
9934
  if (values.json) {
9690
9935
  io.stdout(JSON.stringify(invocation, null, 2));
9691
- return ExitCode.Ok;
9692
- }
9693
- if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
9694
- io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
9695
- return ExitCode.Pending;
9936
+ return outcome ? worstExitCode([outcome]) : ExitCode.Failure;
9696
9937
  }
9697
- return emit(io, settledOutcome(invocation));
9938
+ if (!outcome) throw new CliError(`Unexpected invocation status: ${invocation.status}`);
9939
+ if (outcome.status === "pending") io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
9940
+ else printOutcome(outcome, "", io);
9941
+ return worstExitCode([outcome]);
9698
9942
  }
9699
9943
  case "bridge": {
9700
- const { values } = parseArgs5({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
9944
+ const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
9701
9945
  const { client: tools } = await client(values, io);
9702
9946
  const allow = (values.tools ?? "").split(",").map((name) => name.trim()).filter(Boolean);
9703
9947
  return serveBridge(new BridgeServer(tools, { tools: allow }), process.stdin, process.stdout);
9704
9948
  }
9705
9949
  default:
9706
- throw new CliError(`tools needs one of: list, capabilities, describe, call, run, invocation, bridge
9707
-
9708
- ${toolsUsage}`);
9950
+ throw unknownVerb("tools", toolsUsage, [], verb, rest);
9709
9951
  }
9710
9952
  }
9711
9953
 
9712
9954
  // src/commands/plugins.ts
9713
9955
  import fs11 from "node:fs";
9714
- import { parseArgs as parseArgs6 } from "node:util";
9956
+ import { parseArgs as parseArgs7 } from "node:util";
9715
9957
  var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
9716
9958
 
9717
9959
  validate <manifest.json> (dry run: the platform's full validation, nothing stored)
@@ -9755,26 +9997,15 @@ function formatPlugin(plugin) {
9755
9997
  return [
9756
9998
  `${plugin.pluginId}@${plugin.version} ${plugin.name} (${parent}; runtime ${plugin.runtimeType}; digest ${plugin.digest.slice(0, 12)})`,
9757
9999
  ` tools: ${plugin.toolNames.length ? plugin.toolNames.join(", ") : "none"}`,
9758
- ...plugin.resourceNames.length ? [` resources: ${plugin.resourceNames.join(", ")}`] : []
10000
+ ...plugin.resourceNames.length ? [` resources: ${plugin.resourceNames.join(", ")}`] : [],
10001
+ ...plugin.refusals?.length ? [" NOT SERVED: the platform refuses this version; publish a fixed one:", ...plugin.refusals.map((reason) => ` - ${reason}`)] : []
9759
10002
  ];
9760
10003
  }
9761
- function pluginsVerbUsage(verb) {
9762
- const lines = pluginsUsage.split("\n").filter((line) => line.trimStart().startsWith(`${verb} `));
9763
- return lines.length ? `diverge plugins ${verb} ...
9764
-
9765
- ${lines.join("\n")}
9766
-
9767
- Every plugins command takes --organization-id <id> (or DIVERGENT_ORG_ID, else the signed-in login's organization).` : pluginsUsage;
9768
- }
9769
10004
  async function runPlugins(args, io) {
9770
10005
  const [verb, ...rest] = args;
9771
- if (verb && (rest.includes("--help") || rest.includes("-h"))) {
9772
- io.stdout(pluginsVerbUsage(verb));
9773
- return ExitCode.Ok;
9774
- }
9775
10006
  switch (verb) {
9776
10007
  case "validate": {
9777
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
10008
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
9778
10009
  const manifest = readManifest(positionals[0]);
9779
10010
  const tools = await client2(values, io);
9780
10011
  const outcome = await tools.publishPlugin(manifest, { dryRun: true });
@@ -9791,7 +10022,7 @@ async function runPlugins(args, io) {
9791
10022
  return ExitCode.Ok;
9792
10023
  }
9793
10024
  case "publish": {
9794
- const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
10025
+ const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
9795
10026
  const manifest = readManifest(positionals[0]);
9796
10027
  const tools = await client2(values, io);
9797
10028
  const outcome = await tools.publishPlugin(manifest, { dryRun: false });
@@ -9809,7 +10040,7 @@ async function runPlugins(args, io) {
9809
10040
  return ExitCode.Ok;
9810
10041
  }
9811
10042
  case "list": {
9812
- const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
10043
+ const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
9813
10044
  const tools = await client2(values, io);
9814
10045
  const plugins = await tools.listPlugins();
9815
10046
  if (values.json) io.stdout(JSON.stringify(plugins, null, 2));
@@ -9818,7 +10049,7 @@ async function runPlugins(args, io) {
9818
10049
  return ExitCode.Ok;
9819
10050
  }
9820
10051
  case "retire": {
9821
- const { values, positionals } = parseArgs6({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
10052
+ const { values, positionals } = parseArgs7({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
9822
10053
  const pluginId = positionals[0];
9823
10054
  if (!pluginId) throw new CliError(`A plugin id is required.
9824
10055
 
@@ -9829,9 +10060,7 @@ ${pluginsUsage}`);
9829
10060
  return ExitCode.Ok;
9830
10061
  }
9831
10062
  default:
9832
- throw new CliError(`plugins needs one of: validate, publish, list, retire
9833
-
9834
- ${pluginsUsage}`);
10063
+ throw unknownVerb("plugins", pluginsUsage, [], verb, rest);
9835
10064
  }
9836
10065
  }
9837
10066
 
@@ -9841,11 +10070,11 @@ import { spawn as spawn4 } from "node:child_process";
9841
10070
  // src/update-notice.ts
9842
10071
  import { spawn as spawn3 } from "node:child_process";
9843
10072
  import fs12 from "node:fs";
9844
- import path12 from "node:path";
10073
+ import path14 from "node:path";
9845
10074
  var UPDATE_CHECK_FILE = "update-check.json";
9846
10075
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9847
10076
  function updateCheckFile(env) {
9848
- return path12.join(configDir(env), UPDATE_CHECK_FILE);
10077
+ return path14.join(configDir(env), UPDATE_CHECK_FILE);
9849
10078
  }
9850
10079
  function readUpdateCheck(env) {
9851
10080
  try {
@@ -9942,9 +10171,9 @@ function installLatest(version) {
9942
10171
 
9943
10172
  // src/cli.ts
9944
10173
  var COMMANDS = [
9945
- { name: "login", summary: "Sign in through the browser", usage: loginUsage, run: runLogin },
10174
+ { name: "login", summary: "Sign in (authenticate) through the browser", usage: loginUsage, run: runLogin },
9946
10175
  { name: "logout", summary: "Revoke the login and forget it", usage: logoutUsage, run: runLogout },
9947
- { name: "status", summary: "Show the deployment and the cached login", usage: statusUsage, run: runStatus },
10176
+ { name: "status", summary: "Show who you are signed in as: the deployment, the account and what the login carries", usage: statusUsage, run: runStatus },
9948
10177
  { name: "org", summary: "Show or switch the organization the login acts in", usage: orgUsage, run: runOrg },
9949
10178
  { name: "token", summary: "Print a fresh access token", usage: tokenUsage, run: runToken },
9950
10179
  { name: "agents", summary: "Point a coding agent at the model gateway (setup, run, status)", usage: agentsUsage, run: runAgents },
@@ -9965,7 +10194,7 @@ function usage() {
9965
10194
  ...lines,
9966
10195
  "",
9967
10196
  "Run `diverge <command> --help` for its options.",
9968
- "Environment: DIVERGENT_API_URL (deployment), DIVERGE_HOME (state directory, default ~/.diverge), DIVERGENT_ORG_ID and DIVERGENT_PROJECT_ID (tool scope)."
10197
+ "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)."
9969
10198
  ].join("\n");
9970
10199
  }
9971
10200
  async function run2(argv, io) {
@@ -9980,15 +10209,19 @@ async function run2(argv, io) {
9980
10209
  }
9981
10210
  const command = COMMANDS.find((candidate) => candidate.name === name);
9982
10211
  if (!command) {
9983
- io.stderr(`Unknown command: ${name}
10212
+ const candidates = COMMANDS.map((known) => ({ invocation: `diverge ${known.name}`, name: known.name, summary: known.summary, lines: [known.summary] }));
10213
+ io.stderr(`Unknown command: ${name}.${didYouMean(closest(name, candidates, rest))}
9984
10214
 
9985
10215
  ${usage()}`);
9986
10216
  return ExitCode.Failure;
9987
10217
  }
9988
10218
  const own = rest.includes("--") ? rest.slice(0, rest.indexOf("--")) : rest;
9989
- if ((own.includes("--help") || own.includes("-h")) && (own.length === 1 || own[0].startsWith("-"))) {
9990
- io.stdout(command.usage);
9991
- return ExitCode.Ok;
10219
+ if (own.includes("--help") || own.includes("-h")) {
10220
+ const path15 = verbPath(command.usage, own);
10221
+ if (path15.length > 0 || own[0].startsWith("-")) {
10222
+ io.stdout(verbUsage(command.name, command.usage, path15));
10223
+ return ExitCode.Ok;
10224
+ }
9992
10225
  }
9993
10226
  const code2 = await runCommand(command, rest, io);
9994
10227
  updateNotice(argv, io);
@@ -10003,9 +10236,9 @@ async function runCommand(command, rest, io) {
10003
10236
  return error.exitCode;
10004
10237
  }
10005
10238
  if (error instanceof Error && error.name === "TypeError" && /Unknown option|Option .* argument|Unexpected argument/.test(error.message)) {
10006
- io.stderr(`Error: ${error.message}
10239
+ io.stderr(`Error: ${unknownOption(error, command.usage, rest) ?? error.message}
10007
10240
 
10008
- ${command.usage}`);
10241
+ ${verbUsage(command.name, command.usage, verbPath(command.usage, rest))}`);
10009
10242
  return ExitCode.Failure;
10010
10243
  }
10011
10244
  io.stderr(`Error: ${error instanceof Error ? error.message : String(error)}`);