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