@divergeai/cli 0.1.14 → 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 +720 -578
- 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,22 +8504,22 @@ 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
|
}
|
|
@@ -8327,7 +8536,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
|
|
|
8327
8536
|
const updated = [];
|
|
8328
8537
|
for (const skill of skills) {
|
|
8329
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`);
|
|
8330
|
-
writeTextAtomic(
|
|
8539
|
+
writeTextAtomic(path9.join(root, skill.name, "SKILL.md"), skill.content, 420);
|
|
8331
8540
|
written.push(skill.name);
|
|
8332
8541
|
if (!(skill.name in held)) added.push(skill.name);
|
|
8333
8542
|
else if (held[skill.name] !== skill.updatedAt) updated.push(skill.name);
|
|
@@ -8335,7 +8544,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
|
|
|
8335
8544
|
const removed = [];
|
|
8336
8545
|
for (const name of Object.keys(previous?.skills ?? {})) {
|
|
8337
8546
|
if (skills.some((skill) => skill.name === name)) continue;
|
|
8338
|
-
fs6.rmSync(
|
|
8547
|
+
fs6.rmSync(path9.join(root, name), { recursive: true, force: true });
|
|
8339
8548
|
removed.push(name);
|
|
8340
8549
|
}
|
|
8341
8550
|
const manifest = {
|
|
@@ -8343,7 +8552,7 @@ function installSkills(root, skills, source, now = () => /* @__PURE__ */ new Dat
|
|
|
8343
8552
|
source,
|
|
8344
8553
|
skills: Object.fromEntries(skills.map((skill) => [skill.name, skill.updatedAt]))
|
|
8345
8554
|
};
|
|
8346
|
-
writeJsonAtomic(
|
|
8555
|
+
writeJsonAtomic(path9.join(root, SKILLS_MANIFEST), manifest, 420);
|
|
8347
8556
|
return { root, written, added, updated, removed };
|
|
8348
8557
|
}
|
|
8349
8558
|
function skillsStatus(root, skills) {
|
|
@@ -8353,7 +8562,7 @@ function skillsStatus(root, skills) {
|
|
|
8353
8562
|
const behind = [];
|
|
8354
8563
|
const missing = [];
|
|
8355
8564
|
for (const skill of skills) {
|
|
8356
|
-
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);
|
|
8357
8566
|
else if (held[skill.name] !== skill.updatedAt) behind.push(skill.name);
|
|
8358
8567
|
else current.push(skill.name);
|
|
8359
8568
|
}
|
|
@@ -8383,10 +8592,9 @@ var skillsUsage = `diverge skills <list|install|status> [--harness claude,codex,
|
|
|
8383
8592
|
A skill is a procedure over the tools it names; you get the ones whose every
|
|
8384
8593
|
tool you may run. Re-run install after the organization changes its skills.`;
|
|
8385
8594
|
async function installableSkillSet(apiUrl, env) {
|
|
8386
|
-
const
|
|
8387
|
-
const token = await getUserToken(source);
|
|
8595
|
+
const token = await getUserToken(tokenSource(env, apiUrl));
|
|
8388
8596
|
const organizationId = tokenClaims(token.access_token).organizationId;
|
|
8389
|
-
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>");
|
|
8390
8598
|
const served = await fetchInstallableSkills(apiUrl, token.access_token);
|
|
8391
8599
|
return { skills: [DIVERGE_SKILL, ...served.skills], organizationId };
|
|
8392
8600
|
}
|
|
@@ -8408,7 +8616,7 @@ function installForHarness(io, harness, skills, apiUrl, into) {
|
|
|
8408
8616
|
}
|
|
8409
8617
|
async function runSkills(args, io) {
|
|
8410
8618
|
const [verb, ...rest] = args;
|
|
8411
|
-
const { values } =
|
|
8619
|
+
const { values } = parseArgs2({ args: rest, options: { harness: { type: "string" }, into: { type: "string" } }, strict: true });
|
|
8412
8620
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8413
8621
|
switch (verb) {
|
|
8414
8622
|
case "list": {
|
|
@@ -8447,9 +8655,9 @@ ${skillsUsage}`);
|
|
|
8447
8655
|
|
|
8448
8656
|
// src/agents/rotate.ts
|
|
8449
8657
|
import os7 from "node:os";
|
|
8450
|
-
import
|
|
8658
|
+
import path10 from "node:path";
|
|
8451
8659
|
function defaultTokenFilePath() {
|
|
8452
|
-
return
|
|
8660
|
+
return path10.join(os7.homedir(), ".divergent", "token");
|
|
8453
8661
|
}
|
|
8454
8662
|
var MIN_ROTATE_INTERVAL_SECONDS = 60;
|
|
8455
8663
|
async function rotateTokenFile(loop) {
|
|
@@ -8465,14 +8673,14 @@ async function rotateTokenFile(loop) {
|
|
|
8465
8673
|
|
|
8466
8674
|
// src/agents/self-command.ts
|
|
8467
8675
|
import fs7 from "node:fs";
|
|
8468
|
-
import
|
|
8676
|
+
import path11 from "node:path";
|
|
8469
8677
|
function resolveSelfCommand(argv = process.argv, platform = process.platform, execPath = process.execPath) {
|
|
8470
8678
|
const script = argv[1];
|
|
8471
8679
|
if (!script) return { command: execPath, args: [] };
|
|
8472
8680
|
if (platform !== "win32") {
|
|
8473
8681
|
try {
|
|
8474
8682
|
fs7.accessSync(script, fs7.constants.X_OK);
|
|
8475
|
-
return { command:
|
|
8683
|
+
return { command: path11.resolve(script), args: [] };
|
|
8476
8684
|
} catch {
|
|
8477
8685
|
}
|
|
8478
8686
|
}
|
|
@@ -8521,7 +8729,7 @@ function syncDivergentPreset(io, binary, home) {
|
|
|
8521
8729
|
return;
|
|
8522
8730
|
}
|
|
8523
8731
|
const dir = writeDivergentPreset(home, shipped);
|
|
8524
|
-
const settingsPath =
|
|
8732
|
+
const settingsPath = path12.join(home, "settings.yaml");
|
|
8525
8733
|
const settings = withDivergentPresetDefault(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null);
|
|
8526
8734
|
if (settings !== null) writeTextAtomic(settingsPath, settings);
|
|
8527
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" : ""}.`);
|
|
@@ -8541,18 +8749,22 @@ async function overlayAndDefault(apiUrl, token, surface, io, requested) {
|
|
|
8541
8749
|
const defaultModel = dshDefaultModel(models, requested);
|
|
8542
8750
|
return { overlay: dshPatchYaml({ baseUrl: surfaceBaseUrl(apiUrl, surface), surface, models, defaultModel }), defaultModel, models };
|
|
8543
8751
|
}
|
|
8752
|
+
function organizationArgs(organizationId) {
|
|
8753
|
+
const trimmed = organizationId?.trim();
|
|
8754
|
+
return trimmed ? ["--organization-id", trimmed] : [];
|
|
8755
|
+
}
|
|
8544
8756
|
async function setup(args, io) {
|
|
8545
8757
|
const [agent, ...rest] = args;
|
|
8546
8758
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8547
8759
|
switch (agent) {
|
|
8548
8760
|
case "claude": {
|
|
8549
|
-
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 });
|
|
8550
8762
|
if (values.scope !== "user" && values.scope !== "project") throw new CliError("--scope must be user or project");
|
|
8551
8763
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8552
8764
|
const file = claudeSettingsPath(values.scope);
|
|
8553
8765
|
const { settings, warnings } = mergeClaudeSettings(readJson(file), {
|
|
8554
8766
|
baseUrl: anthropicBaseUrl(apiUrl),
|
|
8555
|
-
tokenHelper: shellCommand(resolveSelfCommand(), "token"),
|
|
8767
|
+
tokenHelper: shellCommand(resolveSelfCommand(), "token", ...organizationArgs(values["organization-id"])),
|
|
8556
8768
|
projectId: values["project-id"],
|
|
8557
8769
|
modelDiscovery: values["model-discovery"]
|
|
8558
8770
|
});
|
|
@@ -8563,7 +8775,7 @@ async function setup(args, io) {
|
|
|
8563
8775
|
return ExitCode.Ok;
|
|
8564
8776
|
}
|
|
8565
8777
|
case "opencode": {
|
|
8566
|
-
const { values } =
|
|
8778
|
+
const { values } = parseArgs3({ args: rest, options: { surface: { type: "string" }, "organization-id": { type: "string" }, config: { type: "string" } }, strict: true });
|
|
8567
8779
|
const surface = surfaceOf(values.surface, "anthropic");
|
|
8568
8780
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8569
8781
|
const baseUrl = surfaceBaseUrl(apiUrl, surface);
|
|
@@ -8577,12 +8789,12 @@ async function setup(args, io) {
|
|
|
8577
8789
|
writeJsonAtomic(file, mergeOpencodeConfig(readJson(file), { baseUrl, surface, models }));
|
|
8578
8790
|
io.stdout(`OpenCode provider divergent-${surface} \u2192 ${baseUrl}. Wrote ${file}.`);
|
|
8579
8791
|
io.stdout("Keep DIVERGENT_TOKEN fresh for the session (user tokens expire; the login refreshes):");
|
|
8580
|
-
io.stdout(
|
|
8792
|
+
io.stdout(` export DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})"`);
|
|
8581
8793
|
return ExitCode.Ok;
|
|
8582
8794
|
}
|
|
8583
8795
|
case "codex": {
|
|
8584
|
-
const { values } =
|
|
8585
|
-
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"] });
|
|
8586
8798
|
if (values.print) {
|
|
8587
8799
|
io.stdout(block.trimEnd());
|
|
8588
8800
|
return ExitCode.Ok;
|
|
@@ -8597,7 +8809,7 @@ async function setup(args, io) {
|
|
|
8597
8809
|
return ExitCode.Ok;
|
|
8598
8810
|
}
|
|
8599
8811
|
case "dsh": {
|
|
8600
|
-
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 });
|
|
8601
8813
|
const surface = surfaceOf(values.surface, "openai");
|
|
8602
8814
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8603
8815
|
if (!values.print) await skillsFor(io, apiUrl, "dsh");
|
|
@@ -8606,13 +8818,13 @@ async function setup(args, io) {
|
|
|
8606
8818
|
io.stdout(overlay.trimEnd());
|
|
8607
8819
|
return ExitCode.Ok;
|
|
8608
8820
|
}
|
|
8609
|
-
const file = values.into ??
|
|
8821
|
+
const file = values.into ?? path12.join(dshHome(io.env), `divergent-${surface}.yml`);
|
|
8610
8822
|
writeTextAtomic(file, overlay);
|
|
8611
8823
|
io.stdout(`dsh route divergent-${surface} \u2192 ${surfaceBaseUrl(apiUrl, surface)} as you, organization ${tokenClaims(token.access_token).organizationId}, default model ${defaultModel}. Wrote ${file}.`);
|
|
8612
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.");
|
|
8613
8825
|
syncDivergentPreset(io, findOnPath("dsh", io.env), dshHome(io.env));
|
|
8614
8826
|
io.stdout("Boot a profile with the overlay and the token in its environment (user tokens expire; the login refreshes):");
|
|
8615
|
-
io.stdout(` DIVERGENT_TOKEN="$(diverge token)" dsh --profile headless --patch ${file} "<task>"`);
|
|
8827
|
+
io.stdout(` DIVERGENT_TOKEN="$(${["diverge", "token", ...organizationArgs(values["organization-id"])].join(" ")})" dsh --profile headless --patch ${file} "<task>"`);
|
|
8616
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`);
|
|
8617
8829
|
return ExitCode.Ok;
|
|
8618
8830
|
}
|
|
@@ -8630,7 +8842,7 @@ ${agentsUsage}`);
|
|
|
8630
8842
|
const terminator = rest.indexOf("--");
|
|
8631
8843
|
const flags = terminator === -1 ? rest : rest.slice(0, terminator);
|
|
8632
8844
|
const passthrough = terminator === -1 ? [] : rest.slice(terminator + 1);
|
|
8633
|
-
const { values } =
|
|
8845
|
+
const { values } = parseArgs3({ args: flags, options: { surface: { type: "string" }, model: { type: "string" }, "organization-id": { type: "string" } }, strict: true });
|
|
8634
8846
|
const surface = surfaceOf(values.surface, "openai");
|
|
8635
8847
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8636
8848
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
@@ -8639,7 +8851,7 @@ ${agentsUsage}`);
|
|
|
8639
8851
|
await skillsFor(io, apiUrl, "dsh");
|
|
8640
8852
|
const { overlay, defaultModel } = await overlayAndDefault(apiUrl, token, surface, io, values.model);
|
|
8641
8853
|
const home = dshHome(io.env);
|
|
8642
|
-
const settingsPath =
|
|
8854
|
+
const settingsPath = path12.join(home, "settings.yaml");
|
|
8643
8855
|
let pinned;
|
|
8644
8856
|
try {
|
|
8645
8857
|
pinned = dshSettingsDefaultModel(fs8.existsSync(settingsPath) ? fs8.readFileSync(settingsPath, "utf8") : null, surface);
|
|
@@ -8648,22 +8860,22 @@ ${agentsUsage}`);
|
|
|
8648
8860
|
}
|
|
8649
8861
|
const served = servedIds(overlay);
|
|
8650
8862
|
if (pinned && !served.includes(pinned)) throw new CliError(stalePinMessage(settingsPath, pinned, surface, served));
|
|
8651
|
-
const overlayPath =
|
|
8863
|
+
const overlayPath = path12.join(home, `divergent-${surface}.yml`);
|
|
8652
8864
|
writeTextAtomic(overlayPath, overlay);
|
|
8653
8865
|
syncDivergentPreset(io, binary, home);
|
|
8654
8866
|
io.stderr(`dsh on divergent-${surface} as you (organization ${tokenClaims(token.access_token).organizationId}), default model ${defaultModel}`);
|
|
8655
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>"');
|
|
8656
|
-
const source =
|
|
8868
|
+
const source = tokenSource(io.env, apiUrl);
|
|
8657
8869
|
return runDsh({
|
|
8658
8870
|
binary,
|
|
8659
8871
|
overlayPath,
|
|
8660
|
-
credentialsPath:
|
|
8872
|
+
credentialsPath: path12.join(home, ".credentials.yaml"),
|
|
8661
8873
|
args: passthrough,
|
|
8662
8874
|
env: io.env,
|
|
8663
8875
|
initialToken: token.access_token,
|
|
8664
8876
|
freshToken: async () => {
|
|
8665
8877
|
try {
|
|
8666
|
-
return (await getUserToken(source, 120)).access_token;
|
|
8878
|
+
return (await getUserToken(source, { freshForSeconds: 120, organizationId: values["organization-id"] })).access_token;
|
|
8667
8879
|
} catch (error) {
|
|
8668
8880
|
io.stderr(`warning: could not refresh the platform token: ${error instanceof Error ? error.message : String(error)}`);
|
|
8669
8881
|
return null;
|
|
@@ -8672,7 +8884,7 @@ ${agentsUsage}`);
|
|
|
8672
8884
|
});
|
|
8673
8885
|
}
|
|
8674
8886
|
async function status(args, io) {
|
|
8675
|
-
const { values } =
|
|
8887
|
+
const { values } = parseArgs3({ args, options: { "organization-id": { type: "string" } }, strict: true });
|
|
8676
8888
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8677
8889
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8678
8890
|
const claims = tokenClaims(token.access_token);
|
|
@@ -8686,19 +8898,19 @@ async function status(args, io) {
|
|
|
8686
8898
|
return ExitCode.Ok;
|
|
8687
8899
|
}
|
|
8688
8900
|
async function rotate(args, io) {
|
|
8689
|
-
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 });
|
|
8690
8902
|
const interval = Number(values.interval);
|
|
8691
8903
|
if (!Number.isFinite(interval) || interval <= 0) throw new CliError("--interval must be a positive number of seconds");
|
|
8692
8904
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
8693
8905
|
const token = await ensureInferenceLogin(io, apiUrl, values["organization-id"]);
|
|
8694
|
-
const source =
|
|
8906
|
+
const source = tokenSource(io.env, apiUrl);
|
|
8695
8907
|
const tokenFilePath = values.into ?? defaultTokenFilePath();
|
|
8696
8908
|
io.stderr(`Keeping ${tokenFilePath} fresh as you (organization ${tokenClaims(token.access_token).organizationId})`);
|
|
8697
8909
|
return rotateTokenFile({
|
|
8698
8910
|
tokenFile: tokenFilePath,
|
|
8699
8911
|
intervalSeconds: interval,
|
|
8700
8912
|
once: values.once,
|
|
8701
|
-
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,
|
|
8702
8914
|
report: (line) => io.stderr(line),
|
|
8703
8915
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
8704
8916
|
});
|
|
@@ -8731,184 +8943,30 @@ ${agentsUsage}`);
|
|
|
8731
8943
|
}
|
|
8732
8944
|
}
|
|
8733
8945
|
|
|
8734
|
-
// src/commands/login.ts
|
|
8735
|
-
import { parseArgs as parseArgs3 } from "node:util";
|
|
8736
|
-
|
|
8737
|
-
// src/auth/organizations.ts
|
|
8738
|
-
function chooseOrganization(input) {
|
|
8739
|
-
const memberships = input.memberships;
|
|
8740
|
-
const flag = input.flag?.trim();
|
|
8741
|
-
if (flag) {
|
|
8742
|
-
const match = memberships.find((organization) => organization.id === flag || organization.name.toLowerCase() === flag.toLowerCase());
|
|
8743
|
-
if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "flag" };
|
|
8744
|
-
if (input.platformActor) return { kind: "chosen", organizationId: flag, reason: "flag" };
|
|
8745
|
-
throw new CliError(`Not one of your organizations: ${flag}. Yours: ${memberships.map((organization) => `${organization.name} (${organization.id})`).join(", ") || "none"}`);
|
|
8746
|
-
}
|
|
8747
|
-
const remembered = input.remembered?.trim();
|
|
8748
|
-
if (remembered) {
|
|
8749
|
-
const match = memberships.find((organization) => organization.id === remembered);
|
|
8750
|
-
if (match) return { kind: "chosen", organizationId: match.id, name: match.name, reason: "remembered" };
|
|
8751
|
-
if (input.platformActor) return { kind: "chosen", organizationId: remembered, reason: "remembered" };
|
|
8752
|
-
}
|
|
8753
|
-
if (memberships.length === 1) return { kind: "chosen", organizationId: memberships[0].id, name: memberships[0].name, reason: "only" };
|
|
8754
|
-
if (memberships.length === 0) return { kind: "none" };
|
|
8755
|
-
return { kind: "ask", options: memberships };
|
|
8756
|
-
}
|
|
8757
|
-
function organizationMenu(options) {
|
|
8758
|
-
return options.map((organization, index) => ` ${index + 1}. ${organization.name} (${organization.id})`);
|
|
8759
|
-
}
|
|
8760
|
-
function pickFromMenu(answer, options) {
|
|
8761
|
-
const trimmed = answer.trim();
|
|
8762
|
-
const index = Number(trimmed);
|
|
8763
|
-
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1];
|
|
8764
|
-
const match = options.find((organization) => organization.id === trimmed || organization.name.toLowerCase() === trimmed.toLowerCase());
|
|
8765
|
-
if (!match) throw new CliError(`Not one of the listed organizations: ${trimmed}`);
|
|
8766
|
-
return match;
|
|
8767
|
-
}
|
|
8768
|
-
|
|
8769
|
-
// src/auth/userinfo.ts
|
|
8770
|
-
async function fetchUserInfo(baseUrl, accessToken) {
|
|
8771
|
-
return fetchJson(userinfoEndpoint(baseUrl), { headers: { Authorization: `Bearer ${accessToken}` }, timeoutMs: 15e3 });
|
|
8772
|
-
}
|
|
8773
|
-
async function describeAccount(baseUrl, accessToken, fallbackSub, options = {}) {
|
|
8774
|
-
try {
|
|
8775
|
-
const info = await fetchUserInfo(baseUrl, accessToken);
|
|
8776
|
-
return options.withSub ? `${info.email} (${info.sub})` : info.email;
|
|
8777
|
-
} catch (error) {
|
|
8778
|
-
return `${fallbackSub ?? "unknown user"} (email unavailable: ${error instanceof Error ? error.message : String(error)})`;
|
|
8779
|
-
}
|
|
8780
|
-
}
|
|
8781
|
-
|
|
8782
|
-
// src/prompt.ts
|
|
8783
|
-
import readline from "node:readline";
|
|
8784
|
-
function ask(question) {
|
|
8785
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
8786
|
-
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
8787
|
-
rl.close();
|
|
8788
|
-
resolve(answer);
|
|
8789
|
-
}));
|
|
8790
|
-
}
|
|
8791
|
-
|
|
8792
|
-
// src/commands/login.ts
|
|
8793
|
-
var DEFAULT_SCOPES = "mcp:read mcp:execute inference:execute";
|
|
8794
|
-
var loginUsage = `diverge login [--organization-id <id|name>] [--scopes "<scopes>"] [--as <email>] [--no-browser] [--api-url <url>]
|
|
8795
|
-
|
|
8796
|
-
Sign in through the browser, then choose where to act. The browser only
|
|
8797
|
-
authenticates the person; the organization is chosen here afterwards \u2014 the
|
|
8798
|
-
one named by --organization-id (id or name), else the last login's, else the
|
|
8799
|
-
only membership, else a menu \u2014 and the login is bound to it on the platform.
|
|
8800
|
-
Switch later with: diverge org use <id|name>.
|
|
8801
|
-
--as <email> names the account: the platform signs that account in instead of
|
|
8802
|
-
reusing whatever session the browser holds, and issues nothing to any other.
|
|
8803
|
-
Without --as, the account of the last login is used; switching is explicit.
|
|
8804
|
-
Without --scopes, the scopes of the last login are asked for again; a first
|
|
8805
|
-
login asks for the CLI's own (mcp:read mcp:execute inference:execute).`;
|
|
8806
|
-
function resolveScopes(flag, config) {
|
|
8807
|
-
const explicit = flag?.trim();
|
|
8808
|
-
if (explicit) return explicit.split(/\s+/).join(" ");
|
|
8809
|
-
const remembered = config.scopes?.trim();
|
|
8810
|
-
return remembered ? remembered.split(/\s+/).join(" ") : DEFAULT_SCOPES;
|
|
8811
|
-
}
|
|
8812
|
-
function resolveLoginHint(flag, config) {
|
|
8813
|
-
const explicit = flag?.trim().toLowerCase();
|
|
8814
|
-
if (explicit) return explicit;
|
|
8815
|
-
const remembered = config.account?.trim().toLowerCase();
|
|
8816
|
-
return remembered || void 0;
|
|
8817
|
-
}
|
|
8818
|
-
async function runLogin(args, io) {
|
|
8819
|
-
const { values } = parseArgs3({
|
|
8820
|
-
args,
|
|
8821
|
-
options: {
|
|
8822
|
-
"organization-id": { type: "string" },
|
|
8823
|
-
scopes: { type: "string" },
|
|
8824
|
-
as: { type: "string" },
|
|
8825
|
-
"no-browser": { type: "boolean", default: false },
|
|
8826
|
-
"api-url": { type: "string" }
|
|
8827
|
-
},
|
|
8828
|
-
strict: true,
|
|
8829
|
-
allowPositionals: false
|
|
8830
|
-
});
|
|
8831
|
-
const config = readConfig(io.env);
|
|
8832
|
-
const apiUrl = resolveApiUrl(values["api-url"], io.env, config);
|
|
8833
|
-
if (values["api-url"]) writeConfig({ ...config, apiUrl }, io.env);
|
|
8834
|
-
const loginHint = resolveLoginHint(values.as, config);
|
|
8835
|
-
if (loginHint) io.stdout(`Signing in as ${loginHint}${values.as ? "" : " (the last login; pass --as to switch)"}.`);
|
|
8836
|
-
const scopes = resolveScopes(values.scopes, config);
|
|
8837
|
-
io.stdout(`Asking for scopes: ${scopes}${values.scopes ? "" : config.scopes ? " (the last login; pass --scopes to change)" : ""}.`);
|
|
8838
|
-
const token = await login({
|
|
8839
|
-
baseUrl: apiUrl,
|
|
8840
|
-
clientId: clientId(io.env),
|
|
8841
|
-
scopes,
|
|
8842
|
-
openBrowser: !values["no-browser"],
|
|
8843
|
-
tokenFile: tokenFile(io.env),
|
|
8844
|
-
loginHint,
|
|
8845
|
-
print: io.stdout
|
|
8846
|
-
});
|
|
8847
|
-
const claims = tokenClaims(token.access_token);
|
|
8848
|
-
let info;
|
|
8849
|
-
try {
|
|
8850
|
-
info = await fetchUserInfo(apiUrl, token.access_token);
|
|
8851
|
-
} catch (error) {
|
|
8852
|
-
throw new CliError(`Signed in, but the platform could not say who or where (${error instanceof Error ? error.message : String(error)}); run: diverge login`);
|
|
8853
|
-
}
|
|
8854
|
-
io.stdout(`Signed in as: ${info.email}`);
|
|
8855
|
-
const bound = await bindChosenOrganization(io, apiUrl, values["organization-id"], config.organization, info);
|
|
8856
|
-
const remaining = Math.max(0, Math.round(bound.token.expires_at - Date.now() / 1e3));
|
|
8857
|
-
io.stdout(`Logged in to ${apiUrl}; token cached (${remaining}s to expiry, auto-refreshes).`);
|
|
8858
|
-
writeConfig({ ...readConfig(io.env), account: info.email, scopes, organization: bound.organizationId }, io.env);
|
|
8859
|
-
const granted = new Set(tokenClaims(bound.token.access_token).scopes ?? []);
|
|
8860
|
-
const missing = scopes.split(" ").filter((scope) => scope && !granted.has(scope));
|
|
8861
|
-
if (missing.length) io.stderr(`Not granted at this organization (your roles lack them): ${missing.join(" ")}`);
|
|
8862
|
-
return 0;
|
|
8863
|
-
}
|
|
8864
|
-
async function bindChosenOrganization(io, apiUrl, flag, remembered, info) {
|
|
8865
|
-
const memberships = info.organizations ?? [];
|
|
8866
|
-
let choice = chooseOrganization({ flag, remembered, memberships, platformActor: info.platformActor });
|
|
8867
|
-
if (choice.kind === "none") {
|
|
8868
|
-
throw new CliError("You are not a member of any organization; ask an administrator to add you, then sign in again.");
|
|
8869
|
-
}
|
|
8870
|
-
if (choice.kind === "ask") {
|
|
8871
|
-
if (!io.interactive) {
|
|
8872
|
-
throw new CliError(`You belong to several organizations; pass --organization-id <id|name>:
|
|
8873
|
-
${organizationMenu(choice.options).join("\n")}`);
|
|
8874
|
-
}
|
|
8875
|
-
io.stdout("Choose the organization to act in:");
|
|
8876
|
-
for (const line of organizationMenu(choice.options)) io.stdout(line);
|
|
8877
|
-
const picked = pickFromMenu(await ask("Organization (number, id or name): "), choice.options);
|
|
8878
|
-
choice = { kind: "chosen", organizationId: picked.id, name: picked.name, reason: "flag" };
|
|
8879
|
-
}
|
|
8880
|
-
const source = { tokenFile: tokenFile(io.env), baseUrl: apiUrl, clientId: clientId(io.env) };
|
|
8881
|
-
const token = await bindOrganization(source, choice.organizationId);
|
|
8882
|
-
const label = choice.name ? `${choice.name} (${choice.organizationId})` : choice.organizationId;
|
|
8883
|
-
const why = choice.reason === "remembered" ? " (the last login; pass --organization-id to switch)" : choice.reason === "only" ? " (your only organization)" : "";
|
|
8884
|
-
io.stdout(`Organization: ${label}${why}`);
|
|
8885
|
-
return { organizationId: choice.organizationId, token };
|
|
8886
|
-
}
|
|
8887
|
-
|
|
8888
8946
|
// src/commands/org.ts
|
|
8889
8947
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
8890
8948
|
var orgUsage = `diverge org [use <id|name>]
|
|
8891
8949
|
|
|
8892
|
-
Without a verb: the organizations you may act in, the
|
|
8893
|
-
use <id|name>:
|
|
8894
|
-
|
|
8895
|
-
|
|
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.`;
|
|
8896
8957
|
async function runOrg(args, io) {
|
|
8897
8958
|
const { positionals } = parseArgs4({ args, options: {}, strict: true, allowPositionals: true });
|
|
8898
|
-
const
|
|
8899
|
-
const
|
|
8900
|
-
const
|
|
8901
|
-
const
|
|
8902
|
-
const memberships = info.organizations ?? [];
|
|
8903
|
-
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;
|
|
8904
8963
|
const verb = positionals[0];
|
|
8905
8964
|
if (!verb) {
|
|
8906
|
-
if (
|
|
8965
|
+
if (organizations.length === 0) {
|
|
8907
8966
|
io.stdout("You are not a member of any organization.");
|
|
8908
8967
|
return 0;
|
|
8909
8968
|
}
|
|
8910
|
-
for (const organization of
|
|
8911
|
-
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})`);
|
|
8912
8970
|
return 0;
|
|
8913
8971
|
}
|
|
8914
8972
|
if (verb !== "use") throw new CliError(`Unknown verb: ${verb}
|
|
@@ -8916,35 +8974,37 @@ async function runOrg(args, io) {
|
|
|
8916
8974
|
${orgUsage}`);
|
|
8917
8975
|
const target = positionals[1];
|
|
8918
8976
|
if (!target) throw new CliError("org use needs an organization id or name");
|
|
8919
|
-
const choice = chooseOrganization({ flag: target,
|
|
8977
|
+
const choice = chooseOrganization({ flag: target, organizations });
|
|
8920
8978
|
if (choice.kind !== "chosen") throw new CliError("org use needs an organization id or name");
|
|
8921
|
-
const
|
|
8922
|
-
|
|
8923
|
-
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"}`);
|
|
8924
8981
|
return 0;
|
|
8925
8982
|
}
|
|
8926
8983
|
|
|
8927
8984
|
// src/commands/logout.ts
|
|
8928
|
-
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.";
|
|
8929
8986
|
async function runLogout(_args, io) {
|
|
8930
|
-
const
|
|
8931
|
-
const token =
|
|
8987
|
+
const source = tokenSource(io.env, resolveApiUrl(void 0, io.env));
|
|
8988
|
+
const token = loadLogin(source);
|
|
8932
8989
|
if (!token) {
|
|
8933
|
-
io.stdout(
|
|
8990
|
+
io.stdout(`Not logged in to ${source.baseUrl}.`);
|
|
8934
8991
|
return 0;
|
|
8935
8992
|
}
|
|
8936
8993
|
if (token.refresh_token) {
|
|
8937
8994
|
try {
|
|
8938
|
-
await revokeRefreshToken(
|
|
8995
|
+
await revokeRefreshToken(source.baseUrl, source.clientId, token.refresh_token);
|
|
8939
8996
|
} catch (error) {
|
|
8940
|
-
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)}`);
|
|
8941
8998
|
}
|
|
8942
8999
|
}
|
|
8943
|
-
|
|
8944
|
-
io.stdout(`Logged out of ${
|
|
9000
|
+
forgetLogin(source);
|
|
9001
|
+
io.stdout(`Logged out of ${source.baseUrl}.`);
|
|
8945
9002
|
return 0;
|
|
8946
9003
|
}
|
|
8947
9004
|
|
|
9005
|
+
// src/commands/status.ts
|
|
9006
|
+
import path13 from "node:path";
|
|
9007
|
+
|
|
8948
9008
|
// src/tools/settings.ts
|
|
8949
9009
|
var BRIDGE_SCOPES = "mcp:read mcp:execute";
|
|
8950
9010
|
var LOGIN_HINT = "diverge login";
|
|
@@ -8952,21 +9012,19 @@ function loadToolsSettings(options, env = process.env) {
|
|
|
8952
9012
|
const apiUrl = resolveApiUrl(void 0, env);
|
|
8953
9013
|
const projectId = options.projectId?.trim() || env.DIVERGENT_PROJECT_ID?.trim() || void 0;
|
|
8954
9014
|
let organizationId = options.organizationId?.trim() || env.DIVERGENT_ORG_ID?.trim() || "";
|
|
8955
|
-
const cached =
|
|
8956
|
-
if (cached
|
|
8957
|
-
organizationId ||=
|
|
8958
|
-
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>`);
|
|
8959
9019
|
return { apiUrl, organizationId, projectId, mode: "operator", clientId: clientId(env) };
|
|
8960
9020
|
}
|
|
8961
|
-
if (!organizationId) throw new CliError("No organization configured. Pass --organization-id or set DIVERGENT_ORG_ID.");
|
|
8962
9021
|
const serviceClientId = env.DIVERGENT_CLIENT_ID?.trim() ?? "";
|
|
8963
9022
|
const serviceClientSecret = env.DIVERGENT_CLIENT_SECRET?.trim() ?? "";
|
|
8964
9023
|
const delegatedUserId = env.DIVERGENT_USER_ID?.trim() ?? "";
|
|
8965
9024
|
if (!serviceClientId || !serviceClientSecret) {
|
|
8966
|
-
throw
|
|
8967
|
-
Sign in as yourself: ${LOGIN_HINT}
|
|
8968
|
-
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.");
|
|
8969
9026
|
}
|
|
9027
|
+
if (!organizationId) throw new CliError("Service credentials act in the organization named: pass --organization-id or set DIVERGENT_ORG_ID.");
|
|
8970
9028
|
if (!delegatedUserId) {
|
|
8971
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.
|
|
8972
9030
|
Signing in as yourself avoids this entirely: ${LOGIN_HINT}`);
|
|
@@ -8984,10 +9042,9 @@ function actingAsLine(settings, env = process.env) {
|
|
|
8984
9042
|
actingAs ??= (async () => {
|
|
8985
9043
|
let accessToken;
|
|
8986
9044
|
try {
|
|
8987
|
-
accessToken = (await getUserToken(
|
|
9045
|
+
accessToken = (await getUserToken(tokenSource(env, settings.apiUrl))).access_token;
|
|
8988
9046
|
} catch (error) {
|
|
8989
|
-
|
|
8990
|
-
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)})`;
|
|
8991
9048
|
}
|
|
8992
9049
|
return `Acting as: ${await describeAccount(settings.apiUrl, accessToken, tokenClaims(accessToken).sub)}`;
|
|
8993
9050
|
})();
|
|
@@ -9007,54 +9064,73 @@ function describeToolExecution(env = process.env) {
|
|
|
9007
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.";
|
|
9008
9065
|
async function runStatus(_args, io) {
|
|
9009
9066
|
const apiUrl = resolveApiUrl(void 0, io.env);
|
|
9067
|
+
const source = tokenSource(io.env, apiUrl);
|
|
9010
9068
|
io.stdout(`diverge ${VERSION}`);
|
|
9011
9069
|
io.stdout(`Deployment: ${apiUrl}`);
|
|
9012
|
-
io.stdout(`State: ${
|
|
9013
|
-
const
|
|
9014
|
-
if (!
|
|
9015
|
-
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}`}`);
|
|
9016
9074
|
io.stdout(describeToolExecution(io.env));
|
|
9017
9075
|
return 1;
|
|
9018
9076
|
}
|
|
9019
|
-
|
|
9020
|
-
const
|
|
9021
|
-
const mismatch = token.base_url !== apiUrl ? ` (for ${token.base_url}, not the configured deployment)` : "";
|
|
9022
|
-
io.stdout(`Login: ${isFresh(token, 0) ? `valid for ${remaining}s` : "expired (auto-refreshes)"}; refresh token ${token.refresh_token ? "present" : "absent"}${mismatch}`);
|
|
9023
|
-
const account = await signedInAs(token, io);
|
|
9077
|
+
io.stdout(`Login: ${describeSignIn(record)}`);
|
|
9078
|
+
const account = await signedInAs(source);
|
|
9024
9079
|
io.stdout(`Signed in as: ${account.line}`);
|
|
9025
|
-
const organization = account.organizations?.find((candidate) => candidate.id ===
|
|
9026
|
-
io.stdout(`Organization: ${
|
|
9027
|
-
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"}`);
|
|
9028
9083
|
io.stdout(describeToolExecution(io.env));
|
|
9029
9084
|
return 0;
|
|
9030
9085
|
}
|
|
9031
|
-
async function signedInAs(
|
|
9032
|
-
const sub = tokenClaims(cached.access_token).sub;
|
|
9086
|
+
async function signedInAs(source) {
|
|
9033
9087
|
let accessToken;
|
|
9034
9088
|
try {
|
|
9035
|
-
accessToken = (await getUserToken(
|
|
9089
|
+
accessToken = (await getUserToken(source)).access_token;
|
|
9036
9090
|
} catch (error) {
|
|
9037
|
-
return { line:
|
|
9091
|
+
return { line: `unknown (${error instanceof Error ? error.message : String(error)})` };
|
|
9038
9092
|
}
|
|
9093
|
+
const claims = tokenClaims(accessToken);
|
|
9094
|
+
const scopes = (claims.scopes ?? []).join(" ") || "none";
|
|
9039
9095
|
try {
|
|
9040
|
-
const info = await fetchUserInfo(
|
|
9041
|
-
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 };
|
|
9042
9098
|
} catch (error) {
|
|
9043
|
-
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 };
|
|
9044
9100
|
}
|
|
9045
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
|
+
}
|
|
9046
9116
|
|
|
9047
9117
|
// src/commands/token.ts
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
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 });
|
|
9052
9128
|
io.stdout(token.access_token);
|
|
9053
9129
|
return 0;
|
|
9054
9130
|
}
|
|
9055
9131
|
|
|
9056
9132
|
// src/commands/tools.ts
|
|
9057
|
-
import { parseArgs as
|
|
9133
|
+
import { parseArgs as parseArgs6 } from "node:util";
|
|
9058
9134
|
|
|
9059
9135
|
// src/mcp/bridge.ts
|
|
9060
9136
|
import readline2 from "node:readline";
|
|
@@ -9078,26 +9154,26 @@ var ToolsClient = class {
|
|
|
9078
9154
|
serviceToken = null;
|
|
9079
9155
|
async token() {
|
|
9080
9156
|
if (this.settings.mode === "operator") {
|
|
9081
|
-
|
|
9082
|
-
return (await getUserToken({ tokenFile: tokenFile(this.env), baseUrl: this.settings.apiUrl, clientId: this.settings.clientId })).access_token;
|
|
9083
|
-
} catch (error) {
|
|
9084
|
-
throw new CliError(`${error instanceof Error ? error.message : String(error)}
|
|
9085
|
-
Sign in with: ${LOGIN_HINT}`);
|
|
9086
|
-
}
|
|
9157
|
+
return (await getUserToken(tokenSource(this.env, this.settings.apiUrl), { organizationId: this.settings.organizationId })).access_token;
|
|
9087
9158
|
}
|
|
9088
9159
|
if (this.serviceToken && Date.now() < this.serviceToken.expiresAtMs) return this.serviceToken.value;
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
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
|
+
}
|
|
9101
9177
|
if (!response.ok) throw new CliError(`Token acquisition failed (${response.status}): ${(await response.text()).slice(0, 400)}`);
|
|
9102
9178
|
const body = await response.json();
|
|
9103
9179
|
if (!body.access_token) throw new CliError("Token response contained no access_token");
|
|
@@ -9110,10 +9186,10 @@ var ToolsClient = class {
|
|
|
9110
9186
|
if (this.settings.mode === "service") headers["x-divergent-user-id"] = this.settings.delegatedUserId;
|
|
9111
9187
|
return headers;
|
|
9112
9188
|
}
|
|
9113
|
-
scoped(
|
|
9189
|
+
scoped(path15) {
|
|
9114
9190
|
const query = new URLSearchParams({ organizationId: this.settings.organizationId });
|
|
9115
9191
|
if (this.settings.projectId) query.set("projectId", this.settings.projectId);
|
|
9116
|
-
return `${this.settings.apiUrl}${
|
|
9192
|
+
return `${this.settings.apiUrl}${path15}?${query.toString()}`;
|
|
9117
9193
|
}
|
|
9118
9194
|
async send(method, url, body) {
|
|
9119
9195
|
return this.request(method, url, body, void 0);
|
|
@@ -9125,9 +9201,10 @@ var ToolsClient = class {
|
|
|
9125
9201
|
*/
|
|
9126
9202
|
async request(method, url, body, refused) {
|
|
9127
9203
|
for (let attempt = 1; attempt <= 2; attempt += 1) {
|
|
9204
|
+
const headers = await this.headers();
|
|
9128
9205
|
let response;
|
|
9129
9206
|
try {
|
|
9130
|
-
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) });
|
|
9131
9208
|
} catch (error) {
|
|
9132
9209
|
throw new CliError(`Cannot reach ${this.settings.apiUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9133
9210
|
}
|
|
@@ -9159,8 +9236,32 @@ var ToolsClient = class {
|
|
|
9159
9236
|
const body = await this.send("GET", this.scoped("/api/plugins/functions/tools"));
|
|
9160
9237
|
return body.tools ?? [];
|
|
9161
9238
|
}
|
|
9162
|
-
|
|
9163
|
-
|
|
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 };
|
|
9164
9265
|
}
|
|
9165
9266
|
/** The read half of raise-and-read (ADR 0047); anyone but the raiser sees a 404. */
|
|
9166
9267
|
getInvocation(invocationId) {
|
|
@@ -9382,7 +9483,7 @@ async function waitForInvocation(id, options, deps) {
|
|
|
9382
9483
|
|
|
9383
9484
|
// src/tools/call-args.ts
|
|
9384
9485
|
import fs9 from "node:fs";
|
|
9385
|
-
function resolveCallArgs(options,
|
|
9486
|
+
async function resolveCallArgs(options, stdin = readStdin) {
|
|
9386
9487
|
if (options.argsJson !== void 0 && options.argsJson !== "{}" && options.argsFile !== void 0) {
|
|
9387
9488
|
throw new CliError("give the arguments once: --args-json or --args-file");
|
|
9388
9489
|
}
|
|
@@ -9397,7 +9498,7 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
|
|
|
9397
9498
|
}
|
|
9398
9499
|
} else if (options.argsJson === "-") {
|
|
9399
9500
|
source = "--args-json - (stdin)";
|
|
9400
|
-
text =
|
|
9501
|
+
text = await stdin();
|
|
9401
9502
|
} else {
|
|
9402
9503
|
source = "--args-json";
|
|
9403
9504
|
text = options.argsJson ?? "{}";
|
|
@@ -9411,6 +9512,11 @@ function resolveCallArgs(options, readStdin = () => fs9.readFileSync(0, "utf8"))
|
|
|
9411
9512
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new CliError(`${source} must be a JSON object`);
|
|
9412
9513
|
return parsed;
|
|
9413
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
|
+
}
|
|
9414
9520
|
|
|
9415
9521
|
// src/tools/batch.ts
|
|
9416
9522
|
function parseBatch(text) {
|
|
@@ -9445,21 +9551,29 @@ async function runOne(entry, deps) {
|
|
|
9445
9551
|
if (body.success === false) return { tool: entry.tool, status: "failed", error: body.error || "Tool execution failed" };
|
|
9446
9552
|
if (!isPending(body.result)) return { tool: entry.tool, status: "executed", result: body.result ?? null };
|
|
9447
9553
|
const pending = { tool: entry.tool, status: "pending", invocationId: body.result.invocationId, decideAt: deps.decideAt(body.result) };
|
|
9554
|
+
deps.onPending?.(body.result);
|
|
9448
9555
|
if (!deps.waitFor) return pending;
|
|
9449
9556
|
const settled = await deps.waitFor(body.result);
|
|
9450
|
-
|
|
9557
|
+
return (settled && outcomeOf(entry.tool, pending.invocationId, settled)) ?? pending;
|
|
9558
|
+
}
|
|
9559
|
+
function outcomeOf(tool, invocationId, invocation) {
|
|
9560
|
+
switch (invocation.status) {
|
|
9451
9561
|
case "succeeded":
|
|
9452
|
-
return { tool
|
|
9562
|
+
return { tool, status: "executed", result: invocation.result ?? null, invocationId };
|
|
9453
9563
|
case "rejected":
|
|
9454
|
-
return { tool
|
|
9564
|
+
return { tool, status: "rejected", invocationId, error: invocation.decisionReason ?? void 0 };
|
|
9455
9565
|
case "expired":
|
|
9456
|
-
return { tool
|
|
9566
|
+
return { tool, status: "expired", invocationId };
|
|
9457
9567
|
case "failed":
|
|
9458
|
-
return { tool
|
|
9568
|
+
return { tool, status: "failed", invocationId, error: invocation.errorMessage || "execution failed" };
|
|
9459
9569
|
default:
|
|
9460
|
-
return
|
|
9570
|
+
return null;
|
|
9461
9571
|
}
|
|
9462
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
|
+
}
|
|
9463
9577
|
function worstExitCode(outcomes) {
|
|
9464
9578
|
const has = (status2) => outcomes.some((outcome) => outcome.status === status2);
|
|
9465
9579
|
if (has("failed")) return ExitCode.Failure;
|
|
@@ -9478,6 +9592,10 @@ function matchesCapability(selector, capability) {
|
|
|
9478
9592
|
if (selector === capability) return true;
|
|
9479
9593
|
return selector.endsWith("*") && capability.startsWith(selector.slice(0, -1));
|
|
9480
9594
|
}
|
|
9595
|
+
function matchesFilter(filter, tool) {
|
|
9596
|
+
const needle = filter.toLowerCase();
|
|
9597
|
+
return tool.name.toLowerCase().includes(needle) || (tool.description ?? "").toLowerCase().includes(needle);
|
|
9598
|
+
}
|
|
9481
9599
|
function formatToolList(tools) {
|
|
9482
9600
|
if (tools.length === 0) return ["No tools available."];
|
|
9483
9601
|
const nameWidth = Math.max(...tools.map((tool) => tool.name.length));
|
|
@@ -9536,38 +9654,29 @@ function formatPending(result, apiUrl, organizationId) {
|
|
|
9536
9654
|
lines.push(`Read the outcome later: diverge tools invocation ${result.invocationId} --organization-id ${organizationId}`);
|
|
9537
9655
|
return lines;
|
|
9538
9656
|
}
|
|
9539
|
-
function settledOutcome(invocation) {
|
|
9540
|
-
switch (invocation.status) {
|
|
9541
|
-
case "succeeded":
|
|
9542
|
-
return { stdout: JSON.stringify(invocation.result ?? null, null, 2), exitCode: ExitCode.Ok };
|
|
9543
|
-
case "rejected":
|
|
9544
|
-
return { stderr: `Rejected by the operator${invocation.decisionReason ? `: ${invocation.decisionReason}` : "."}`, exitCode: ExitCode.Rejected };
|
|
9545
|
-
case "expired":
|
|
9546
|
-
return { stderr: "Expired before anyone decided. Raise a fresh call; the old id is final.", exitCode: ExitCode.Expired };
|
|
9547
|
-
case "failed":
|
|
9548
|
-
throw new CliError(`Approved, but execution failed: ${invocation.errorMessage || "unknown error"}`);
|
|
9549
|
-
default:
|
|
9550
|
-
throw new CliError(`Unexpected invocation status: ${invocation.status}`);
|
|
9551
|
-
}
|
|
9552
|
-
}
|
|
9553
9657
|
|
|
9554
9658
|
// src/commands/tools.ts
|
|
9555
9659
|
var toolsUsage = `diverge tools <list|capabilities|describe|call|run|invocation|bridge> [--organization-id <id>] [--project-id <id>] ...
|
|
9556
9660
|
|
|
9557
|
-
list [--filter <
|
|
9661
|
+
list [--filter <text>] [--capability <selector>] [--json]
|
|
9662
|
+
--filter matches tool names and descriptions, ignoring case
|
|
9558
9663
|
capabilities [--json]
|
|
9559
9664
|
describe <name> [--json]
|
|
9560
|
-
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]
|
|
9561
9666
|
arguments inline, from stdin (-), or from a file: PowerShell strips quotes inside inline JSON
|
|
9562
|
-
|
|
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>]
|
|
9563
9670
|
several calls from one file (a JSON array of {tool, args}, or one per line), in order,
|
|
9564
9671
|
one JSON line of outcome each; exit code is the worst: 1 failed, 4 rejected, 5 expired, 3 pending
|
|
9565
9672
|
invocation <id> [--wait] [--timeout <seconds>] [--json]
|
|
9673
|
+
--json prints the invocation record; the exit code is call's for the same status
|
|
9566
9674
|
bridge [--tools <name,name>] (stdio MCP server; the allowlist keeps a harness to the tools it was given)
|
|
9567
9675
|
|
|
9568
9676
|
Discover and execute platform tools as yourself. A write that needs a human is
|
|
9569
9677
|
persisted as a pending invocation, decided in Compass, and read back here by
|
|
9570
|
-
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.`;
|
|
9571
9680
|
var SCOPE_OPTIONS = { "organization-id": { type: "string" }, "project-id": { type: "string" } };
|
|
9572
9681
|
async function client(values, io) {
|
|
9573
9682
|
const settings = loadToolsSettings({ organizationId: values["organization-id"], projectId: values["project-id"] }, io.env);
|
|
@@ -9575,14 +9684,72 @@ async function client(values, io) {
|
|
|
9575
9684
|
io.stderr(await actingAsLine(settings, io.env));
|
|
9576
9685
|
return { settings, client: new ToolsClient(settings, io.env) };
|
|
9577
9686
|
}
|
|
9578
|
-
function emit(io, outcome) {
|
|
9579
|
-
if (outcome.stdout !== void 0) io.stdout(outcome.stdout);
|
|
9580
|
-
if (outcome.stderr !== void 0) io.stderr(outcome.stderr);
|
|
9581
|
-
return outcome.exitCode;
|
|
9582
|
-
}
|
|
9583
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
|
+
}
|
|
9584
9745
|
function toolsVerbUsage(verb) {
|
|
9585
|
-
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
|
+
}
|
|
9586
9753
|
return lines.length ? `diverge tools ${verb} ...
|
|
9587
9754
|
|
|
9588
9755
|
${lines.join("\n")}
|
|
@@ -9597,17 +9764,17 @@ async function runTools(args, io) {
|
|
|
9597
9764
|
}
|
|
9598
9765
|
switch (verb) {
|
|
9599
9766
|
case "list": {
|
|
9600
|
-
const { values } =
|
|
9767
|
+
const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, filter: { type: "string" }, capability: { type: "string" }, json: { type: "boolean", default: false } }, strict: true });
|
|
9601
9768
|
const { client: tools } = await client(values, io);
|
|
9602
9769
|
let list = await tools.listTools();
|
|
9603
|
-
if (values.filter) list = list.filter((tool) =>
|
|
9770
|
+
if (values.filter) list = list.filter((tool) => matchesFilter(values.filter, tool));
|
|
9604
9771
|
if (values.capability) list = list.filter((tool) => matchesCapability(values.capability, tool.capability));
|
|
9605
9772
|
if (values.json) io.stdout(JSON.stringify(list, null, 2));
|
|
9606
9773
|
else for (const line of formatToolList(list)) io.stdout(line);
|
|
9607
9774
|
return ExitCode.Ok;
|
|
9608
9775
|
}
|
|
9609
9776
|
case "capabilities": {
|
|
9610
|
-
const { values } =
|
|
9777
|
+
const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true });
|
|
9611
9778
|
const { client: tools } = await client(values, io);
|
|
9612
9779
|
const list = await tools.listTools();
|
|
9613
9780
|
if (values.json) io.stdout(JSON.stringify(capabilityClusters(list), null, 2));
|
|
@@ -9615,89 +9782,63 @@ async function runTools(args, io) {
|
|
|
9615
9782
|
return ExitCode.Ok;
|
|
9616
9783
|
}
|
|
9617
9784
|
case "describe": {
|
|
9618
|
-
const { values, positionals } =
|
|
9785
|
+
const { values, positionals } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, json: { type: "boolean", default: false } }, strict: true, allowPositionals: true });
|
|
9619
9786
|
const name = positionals[0];
|
|
9620
9787
|
if (!name) throw new CliError("describe needs a tool name");
|
|
9621
9788
|
const { settings, client: tools } = await client(values, io);
|
|
9622
|
-
const
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
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(", ")}?` : ""}`);
|
|
9627
9794
|
}
|
|
9795
|
+
const { tool } = described;
|
|
9628
9796
|
if (values.json) io.stdout(JSON.stringify(tool, null, 2));
|
|
9629
9797
|
else for (const line of formatDescribe(tool, settings.organizationId)) io.stdout(line);
|
|
9630
9798
|
return ExitCode.Ok;
|
|
9631
9799
|
}
|
|
9632
|
-
case "call":
|
|
9633
|
-
|
|
9634
|
-
const name = positionals[0];
|
|
9635
|
-
if (!name) throw new CliError("call needs a tool name");
|
|
9636
|
-
const parsedArgs = resolveCallArgs({ argsJson: values["args-json"], argsFile: values["args-file"] });
|
|
9637
|
-
const timeoutMs = Number(values.timeout) * 1e3;
|
|
9638
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
|
|
9639
|
-
const { settings, client: tools } = await client(values, io);
|
|
9640
|
-
const body = await tools.callTool(name, parsedArgs);
|
|
9641
|
-
if (body.success === false) throw new CliError(body.error || "Tool execution failed");
|
|
9642
|
-
if (!isPending(body.result)) {
|
|
9643
|
-
io.stdout(JSON.stringify(body.result ?? null, null, 2));
|
|
9644
|
-
return ExitCode.Ok;
|
|
9645
|
-
}
|
|
9646
|
-
for (const line of formatPending(body.result, settings.apiUrl, settings.organizationId)) io.stdout(line);
|
|
9647
|
-
if (!values.wait) return ExitCode.Pending;
|
|
9648
|
-
const invocation = await waitForInvocation(body.result.invocationId ?? "", { pollAfterMs: body.result.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
|
|
9649
|
-
if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
|
|
9650
|
-
io.stderr(`Still ${invocation.status} after ${values.timeout}s; the invocation remains open.`);
|
|
9651
|
-
return ExitCode.Pending;
|
|
9652
|
-
}
|
|
9653
|
-
return emit(io, settledOutcome(invocation));
|
|
9654
|
-
}
|
|
9800
|
+
case "call":
|
|
9801
|
+
return call(rest, io);
|
|
9655
9802
|
case "run": {
|
|
9656
|
-
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 });
|
|
9657
9804
|
const file = positionals[0];
|
|
9658
|
-
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");
|
|
9659
9806
|
let entries;
|
|
9660
9807
|
try {
|
|
9661
|
-
entries = parseBatch(fs10.readFileSync(file, "utf8"));
|
|
9808
|
+
entries = parseBatch(file === "-" ? await readStdin() : fs10.readFileSync(file, "utf8"));
|
|
9662
9809
|
} catch (error) {
|
|
9663
9810
|
throw new CliError(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
9664
9811
|
}
|
|
9665
|
-
const timeoutMs =
|
|
9666
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) throw new CliError("--timeout must be a number of seconds");
|
|
9812
|
+
const timeoutMs = timeoutOf(values.timeout);
|
|
9667
9813
|
const { settings, client: tools } = await client(values, io);
|
|
9668
9814
|
const { exitCode } = await runBatch(entries, {
|
|
9669
9815
|
callTool: (tool, args2) => tools.callTool(tool, args2),
|
|
9670
9816
|
decideAt: (pending) => `${settings.apiUrl}${pending.approvalUrl ?? ""}`,
|
|
9671
9817
|
onOutcome: (outcome) => io.stdout(JSON.stringify(outcome)),
|
|
9672
|
-
...values.wait ? {
|
|
9673
|
-
waitFor: async (pending) => {
|
|
9674
|
-
const invocation = await waitForInvocation(pending.invocationId ?? "", { pollAfterMs: pending.pollAfterMs, timeoutMs }, { getInvocation: (id) => tools.getInvocation(id), sleep: realSleep, now: Date.now });
|
|
9675
|
-
return PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "") ? null : invocation;
|
|
9676
|
-
}
|
|
9677
|
-
} : {}
|
|
9818
|
+
...values.wait ? { waitFor: waiter(tools, timeoutMs) } : {}
|
|
9678
9819
|
});
|
|
9679
9820
|
return exitCode;
|
|
9680
9821
|
}
|
|
9681
9822
|
case "invocation": {
|
|
9682
|
-
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 });
|
|
9683
9824
|
const id = positionals[0];
|
|
9684
9825
|
if (!id) throw new CliError("invocation needs an id");
|
|
9685
9826
|
const { client: tools } = await client(values, io);
|
|
9686
9827
|
let invocation;
|
|
9687
9828
|
if (values.wait) invocation = await waitForInvocation(id, { timeoutMs: Number(values.timeout) * 1e3 }, { getInvocation: (target) => tools.getInvocation(target), sleep: realSleep, now: Date.now });
|
|
9688
9829
|
else invocation = (await tools.getInvocation(id)).invocation ?? {};
|
|
9830
|
+
const outcome = readBackOutcomeOf(invocation.toolName ?? "", id, invocation);
|
|
9689
9831
|
if (values.json) {
|
|
9690
9832
|
io.stdout(JSON.stringify(invocation, null, 2));
|
|
9691
|
-
return ExitCode.
|
|
9692
|
-
}
|
|
9693
|
-
if (PENDING_INVOCATION_STATUSES.includes(invocation.status ?? "")) {
|
|
9694
|
-
io.stdout(`${invocation.status}: waiting on the operator (expires ${invocation.expiresAt}).`);
|
|
9695
|
-
return ExitCode.Pending;
|
|
9833
|
+
return outcome ? worstExitCode([outcome]) : ExitCode.Failure;
|
|
9696
9834
|
}
|
|
9697
|
-
|
|
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]);
|
|
9698
9839
|
}
|
|
9699
9840
|
case "bridge": {
|
|
9700
|
-
const { values } =
|
|
9841
|
+
const { values } = parseArgs6({ args: rest, options: { ...SCOPE_OPTIONS, tools: { type: "string" } }, strict: true });
|
|
9701
9842
|
const { client: tools } = await client(values, io);
|
|
9702
9843
|
const allow = (values.tools ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
9703
9844
|
return serveBridge(new BridgeServer(tools, { tools: allow }), process.stdin, process.stdout);
|
|
@@ -9711,7 +9852,7 @@ ${toolsUsage}`);
|
|
|
9711
9852
|
|
|
9712
9853
|
// src/commands/plugins.ts
|
|
9713
9854
|
import fs11 from "node:fs";
|
|
9714
|
-
import { parseArgs as
|
|
9855
|
+
import { parseArgs as parseArgs7 } from "node:util";
|
|
9715
9856
|
var pluginsUsage = `diverge plugins <validate|publish|list|retire> [--organization-id <id>] ...
|
|
9716
9857
|
|
|
9717
9858
|
validate <manifest.json> (dry run: the platform's full validation, nothing stored)
|
|
@@ -9755,7 +9896,8 @@ function formatPlugin(plugin) {
|
|
|
9755
9896
|
return [
|
|
9756
9897
|
`${plugin.pluginId}@${plugin.version} ${plugin.name} (${parent}; runtime ${plugin.runtimeType}; digest ${plugin.digest.slice(0, 12)})`,
|
|
9757
9898
|
` tools: ${plugin.toolNames.length ? plugin.toolNames.join(", ") : "none"}`,
|
|
9758
|
-
...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}`)] : []
|
|
9759
9901
|
];
|
|
9760
9902
|
}
|
|
9761
9903
|
function pluginsVerbUsage(verb) {
|
|
@@ -9774,7 +9916,7 @@ async function runPlugins(args, io) {
|
|
|
9774
9916
|
}
|
|
9775
9917
|
switch (verb) {
|
|
9776
9918
|
case "validate": {
|
|
9777
|
-
const { values, positionals } =
|
|
9919
|
+
const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
|
|
9778
9920
|
const manifest = readManifest(positionals[0]);
|
|
9779
9921
|
const tools = await client2(values, io);
|
|
9780
9922
|
const outcome = await tools.publishPlugin(manifest, { dryRun: true });
|
|
@@ -9791,7 +9933,7 @@ async function runPlugins(args, io) {
|
|
|
9791
9933
|
return ExitCode.Ok;
|
|
9792
9934
|
}
|
|
9793
9935
|
case "publish": {
|
|
9794
|
-
const { values, positionals } =
|
|
9936
|
+
const { values, positionals } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, allowPositionals: true, strict: true });
|
|
9795
9937
|
const manifest = readManifest(positionals[0]);
|
|
9796
9938
|
const tools = await client2(values, io);
|
|
9797
9939
|
const outcome = await tools.publishPlugin(manifest, { dryRun: false });
|
|
@@ -9809,7 +9951,7 @@ async function runPlugins(args, io) {
|
|
|
9809
9951
|
return ExitCode.Ok;
|
|
9810
9952
|
}
|
|
9811
9953
|
case "list": {
|
|
9812
|
-
const { values } =
|
|
9954
|
+
const { values } = parseArgs7({ args: rest, options: { ...SCOPE_OPTIONS2, json: { type: "boolean", default: false } }, strict: true });
|
|
9813
9955
|
const tools = await client2(values, io);
|
|
9814
9956
|
const plugins = await tools.listPlugins();
|
|
9815
9957
|
if (values.json) io.stdout(JSON.stringify(plugins, null, 2));
|
|
@@ -9818,7 +9960,7 @@ async function runPlugins(args, io) {
|
|
|
9818
9960
|
return ExitCode.Ok;
|
|
9819
9961
|
}
|
|
9820
9962
|
case "retire": {
|
|
9821
|
-
const { values, positionals } =
|
|
9963
|
+
const { values, positionals } = parseArgs7({ args: rest, options: SCOPE_OPTIONS2, allowPositionals: true, strict: true });
|
|
9822
9964
|
const pluginId = positionals[0];
|
|
9823
9965
|
if (!pluginId) throw new CliError(`A plugin id is required.
|
|
9824
9966
|
|
|
@@ -9841,11 +9983,11 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
9841
9983
|
// src/update-notice.ts
|
|
9842
9984
|
import { spawn as spawn3 } from "node:child_process";
|
|
9843
9985
|
import fs12 from "node:fs";
|
|
9844
|
-
import
|
|
9986
|
+
import path14 from "node:path";
|
|
9845
9987
|
var UPDATE_CHECK_FILE = "update-check.json";
|
|
9846
9988
|
var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
9847
9989
|
function updateCheckFile(env) {
|
|
9848
|
-
return
|
|
9990
|
+
return path14.join(configDir(env), UPDATE_CHECK_FILE);
|
|
9849
9991
|
}
|
|
9850
9992
|
function readUpdateCheck(env) {
|
|
9851
9993
|
try {
|
|
@@ -9965,7 +10107,7 @@ function usage() {
|
|
|
9965
10107
|
...lines,
|
|
9966
10108
|
"",
|
|
9967
10109
|
"Run `diverge <command> --help` for its options.",
|
|
9968
|
-
"Environment: DIVERGENT_API_URL (deployment), DIVERGE_HOME (state directory, default ~/.diverge), DIVERGENT_ORG_ID and DIVERGENT_PROJECT_ID (
|
|
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)."
|
|
9969
10111
|
].join("\n");
|
|
9970
10112
|
}
|
|
9971
10113
|
async function run2(argv, io) {
|