@datagrout/conduit 0.1.0
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 +213 -0
- package/dist/chunk-RED5DKGI.mjs +122 -0
- package/dist/index.d.mts +1242 -0
- package/dist/index.d.ts +1242 -0
- package/dist/index.js +1736 -0
- package/dist/index.mjs +1597 -0
- package/dist/oauth-OMPWCI2X.mjs +10 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1736 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/oauth.ts
|
|
34
|
+
var oauth_exports = {};
|
|
35
|
+
__export(oauth_exports, {
|
|
36
|
+
OAuthTokenProvider: () => OAuthTokenProvider,
|
|
37
|
+
deriveTokenEndpoint: () => deriveTokenEndpoint
|
|
38
|
+
});
|
|
39
|
+
function deriveTokenEndpoint(mcpUrl) {
|
|
40
|
+
const mcpIdx = mcpUrl.indexOf("/mcp");
|
|
41
|
+
const base = mcpIdx !== -1 ? mcpUrl.slice(0, mcpIdx) : mcpUrl.replace(/\/$/, "");
|
|
42
|
+
return `${base}/oauth/token`;
|
|
43
|
+
}
|
|
44
|
+
var OAuthTokenProvider;
|
|
45
|
+
var init_oauth = __esm({
|
|
46
|
+
"src/oauth.ts"() {
|
|
47
|
+
"use strict";
|
|
48
|
+
OAuthTokenProvider = class {
|
|
49
|
+
clientId;
|
|
50
|
+
clientSecret;
|
|
51
|
+
tokenEndpoint;
|
|
52
|
+
scope;
|
|
53
|
+
cached = null;
|
|
54
|
+
fetchPromise = null;
|
|
55
|
+
constructor(opts) {
|
|
56
|
+
this.clientId = opts.clientId;
|
|
57
|
+
this.clientSecret = opts.clientSecret;
|
|
58
|
+
this.tokenEndpoint = opts.tokenEndpoint;
|
|
59
|
+
this.scope = opts.scope;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Return the current bearer token, fetching a fresh one if necessary.
|
|
63
|
+
* Concurrent callers share a single in-flight fetch.
|
|
64
|
+
*/
|
|
65
|
+
async getToken() {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (this.cached && this.cached.expiresAt - now > 6e4) {
|
|
68
|
+
return this.cached.accessToken;
|
|
69
|
+
}
|
|
70
|
+
if (!this.fetchPromise) {
|
|
71
|
+
this.fetchPromise = this.fetchToken().finally(() => {
|
|
72
|
+
this.fetchPromise = null;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const cached = await this.fetchPromise;
|
|
76
|
+
return cached.accessToken;
|
|
77
|
+
}
|
|
78
|
+
/** Invalidate the cached token (e.g. after receiving a 401). */
|
|
79
|
+
invalidate() {
|
|
80
|
+
this.cached = null;
|
|
81
|
+
}
|
|
82
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
83
|
+
async fetchToken() {
|
|
84
|
+
const body = new URLSearchParams({
|
|
85
|
+
grant_type: "client_credentials",
|
|
86
|
+
client_id: this.clientId,
|
|
87
|
+
client_secret: this.clientSecret
|
|
88
|
+
});
|
|
89
|
+
if (this.scope) {
|
|
90
|
+
body.set("scope", this.scope);
|
|
91
|
+
}
|
|
92
|
+
let response;
|
|
93
|
+
try {
|
|
94
|
+
response = await fetch(this.tokenEndpoint, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
97
|
+
body: body.toString()
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
throw new Error(`OAuth token request failed: ${err}`);
|
|
101
|
+
}
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const text = await response.text().catch(() => "");
|
|
104
|
+
throw new Error(`OAuth token endpoint returned ${response.status}: ${text}`);
|
|
105
|
+
}
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
const expiresIn = data.expires_in ?? 3600;
|
|
108
|
+
const cached = {
|
|
109
|
+
accessToken: data.access_token,
|
|
110
|
+
expiresAt: Date.now() + expiresIn * 1e3
|
|
111
|
+
};
|
|
112
|
+
this.cached = cached;
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// src/identity.ts
|
|
120
|
+
var identity_exports = {};
|
|
121
|
+
__export(identity_exports, {
|
|
122
|
+
ConduitIdentity: () => ConduitIdentity,
|
|
123
|
+
fetchWithIdentity: () => fetchWithIdentity
|
|
124
|
+
});
|
|
125
|
+
async function fetchWithIdentity(url, init, identity) {
|
|
126
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
127
|
+
console.warn(
|
|
128
|
+
"[conduit] mTLS identity is set but this environment does not support client certificates in fetch(). The request will proceed without mTLS."
|
|
129
|
+
);
|
|
130
|
+
return fetch(url, init);
|
|
131
|
+
}
|
|
132
|
+
const https = require("https");
|
|
133
|
+
const parsedUrl = new URL(url);
|
|
134
|
+
const options = {
|
|
135
|
+
hostname: parsedUrl.hostname,
|
|
136
|
+
port: parsedUrl.port || 443,
|
|
137
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
138
|
+
method: (init.method ?? "GET").toUpperCase(),
|
|
139
|
+
headers: flattenHeaders(init.headers),
|
|
140
|
+
cert: identity.certPem,
|
|
141
|
+
key: identity.keyPem,
|
|
142
|
+
...identity.caPem ? { ca: identity.caPem } : {}
|
|
143
|
+
};
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const req = https.request(options, (res) => {
|
|
146
|
+
const chunks = [];
|
|
147
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
148
|
+
res.on("end", () => {
|
|
149
|
+
const body = Buffer.concat(chunks);
|
|
150
|
+
const responseHeaders = new Headers();
|
|
151
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
152
|
+
if (value !== void 0) {
|
|
153
|
+
const v = Array.isArray(value) ? value.join(", ") : String(value);
|
|
154
|
+
responseHeaders.set(key, v);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
resolve(
|
|
158
|
+
new Response(body, {
|
|
159
|
+
status: res.statusCode ?? 200,
|
|
160
|
+
statusText: res.statusMessage ?? "",
|
|
161
|
+
headers: responseHeaders
|
|
162
|
+
})
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
res.on("error", reject);
|
|
166
|
+
});
|
|
167
|
+
req.on("error", reject);
|
|
168
|
+
if (init.body) {
|
|
169
|
+
req.write(init.body);
|
|
170
|
+
}
|
|
171
|
+
req.end();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function flattenHeaders(headers) {
|
|
175
|
+
if (!headers) return {};
|
|
176
|
+
if (headers instanceof Headers) {
|
|
177
|
+
const obj = {};
|
|
178
|
+
headers.forEach((v, k) => {
|
|
179
|
+
obj[k] = v;
|
|
180
|
+
});
|
|
181
|
+
return obj;
|
|
182
|
+
}
|
|
183
|
+
if (Array.isArray(headers)) {
|
|
184
|
+
return Object.fromEntries(headers);
|
|
185
|
+
}
|
|
186
|
+
return headers;
|
|
187
|
+
}
|
|
188
|
+
var ConduitIdentity;
|
|
189
|
+
var init_identity = __esm({
|
|
190
|
+
"src/identity.ts"() {
|
|
191
|
+
"use strict";
|
|
192
|
+
ConduitIdentity = class _ConduitIdentity {
|
|
193
|
+
_config;
|
|
194
|
+
constructor(config) {
|
|
195
|
+
this._config = config;
|
|
196
|
+
}
|
|
197
|
+
// ─── Constructors ───────────────────────────────────────────────────────────
|
|
198
|
+
/**
|
|
199
|
+
* Build an identity from PEM strings already in memory.
|
|
200
|
+
*
|
|
201
|
+
* @throws {Error} if the PEM strings do not look like a certificate or key.
|
|
202
|
+
*/
|
|
203
|
+
static fromPem(certPem, keyPem, caPem) {
|
|
204
|
+
if (!certPem.includes("-----BEGIN CERTIFICATE-----")) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
'certPem does not appear to contain a PEM certificate (missing "-----BEGIN CERTIFICATE-----")'
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!_ConduitIdentity._hasPemPrivateKey(keyPem)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
"keyPem does not appear to contain a PEM private key (expected PRIVATE KEY, RSA PRIVATE KEY, or EC PRIVATE KEY header)"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return new _ConduitIdentity({ certPem, keyPem, caPem });
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Build an identity by reading PEM files from disk (Node.js only).
|
|
218
|
+
*
|
|
219
|
+
* @throws {Error} in browser environments or if the files cannot be read.
|
|
220
|
+
*/
|
|
221
|
+
static fromPaths(certPath, keyPath, caPath) {
|
|
222
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
223
|
+
throw new Error("ConduitIdentity.fromPaths() is only available in Node.js environments");
|
|
224
|
+
}
|
|
225
|
+
const fs2 = require("fs");
|
|
226
|
+
const certPem = fs2.readFileSync(certPath, "utf8");
|
|
227
|
+
const keyPem = fs2.readFileSync(keyPath, "utf8");
|
|
228
|
+
const caPem = caPath ? fs2.readFileSync(caPath, "utf8") : void 0;
|
|
229
|
+
return _ConduitIdentity.fromPem(certPem, keyPem, caPem);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Build an identity from environment variables.
|
|
233
|
+
*
|
|
234
|
+
* Reads:
|
|
235
|
+
* - `CONDUIT_MTLS_CERT` — PEM string for the client certificate
|
|
236
|
+
* - `CONDUIT_MTLS_KEY` — PEM string for the private key
|
|
237
|
+
* - `CONDUIT_MTLS_CA` — PEM string for the CA (optional)
|
|
238
|
+
*
|
|
239
|
+
* @returns `null` if `CONDUIT_MTLS_CERT` is not set.
|
|
240
|
+
* @throws {Error} if `CONDUIT_MTLS_CERT` is set but `CONDUIT_MTLS_KEY` is missing.
|
|
241
|
+
*/
|
|
242
|
+
static fromEnv() {
|
|
243
|
+
const certPem = process.env.CONDUIT_MTLS_CERT;
|
|
244
|
+
if (!certPem) return null;
|
|
245
|
+
const keyPem = process.env.CONDUIT_MTLS_KEY;
|
|
246
|
+
if (!keyPem) {
|
|
247
|
+
throw new Error("CONDUIT_MTLS_CERT is set but CONDUIT_MTLS_KEY is missing");
|
|
248
|
+
}
|
|
249
|
+
const caPem = process.env.CONDUIT_MTLS_CA || void 0;
|
|
250
|
+
return _ConduitIdentity.fromPem(certPem, keyPem, caPem);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Try to locate an identity using the auto-discovery chain described in the
|
|
254
|
+
* module docs. Returns `null` if nothing is found.
|
|
255
|
+
*/
|
|
256
|
+
static tryDefault() {
|
|
257
|
+
return _ConduitIdentity.tryDiscover();
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Like {@link tryDefault} but checks `overrideDir` first.
|
|
261
|
+
*
|
|
262
|
+
* Discovery order:
|
|
263
|
+
* 1. `overrideDir` (if provided)
|
|
264
|
+
* 2. `CONDUIT_MTLS_CERT` + `CONDUIT_MTLS_KEY` env vars
|
|
265
|
+
* 3. `CONDUIT_IDENTITY_DIR` env var
|
|
266
|
+
* 4. `~/.conduit/`
|
|
267
|
+
* 5. `.conduit/` relative to cwd
|
|
268
|
+
*/
|
|
269
|
+
static tryDiscover(overrideDir) {
|
|
270
|
+
if (overrideDir) {
|
|
271
|
+
const id = _ConduitIdentity._tryLoadFromDir(overrideDir);
|
|
272
|
+
if (id) return id;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const id = _ConduitIdentity.fromEnv();
|
|
276
|
+
if (id) return id;
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
if (typeof process === "undefined" || !process.versions?.node) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const envDir = process.env.CONDUIT_IDENTITY_DIR;
|
|
283
|
+
if (envDir) {
|
|
284
|
+
const id = _ConduitIdentity._tryLoadFromDir(envDir);
|
|
285
|
+
if (id) return id;
|
|
286
|
+
}
|
|
287
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
288
|
+
if (home) {
|
|
289
|
+
const id = _ConduitIdentity._tryLoadFromDir(`${home}/.conduit`);
|
|
290
|
+
if (id) return id;
|
|
291
|
+
}
|
|
292
|
+
{
|
|
293
|
+
const id = _ConduitIdentity._tryLoadFromDir(".conduit");
|
|
294
|
+
if (id) return id;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
// ─── Builder-style setters ──────────────────────────────────────────────────
|
|
299
|
+
/** Attach a known certificate expiry for rotation checks. */
|
|
300
|
+
withExpiry(expiresAt) {
|
|
301
|
+
return new _ConduitIdentity({ ...this._config, expiresAt });
|
|
302
|
+
}
|
|
303
|
+
// ─── Introspection ──────────────────────────────────────────────────────────
|
|
304
|
+
/**
|
|
305
|
+
* Returns `true` if the certificate expires within `thresholdDays`.
|
|
306
|
+
*
|
|
307
|
+
* Always returns `false` when no expiry is set.
|
|
308
|
+
*/
|
|
309
|
+
needsRotation(thresholdDays) {
|
|
310
|
+
if (!this._config.expiresAt) return false;
|
|
311
|
+
const thresholdMs = thresholdDays * 864e5;
|
|
312
|
+
return Date.now() + thresholdMs > this._config.expiresAt.getTime();
|
|
313
|
+
}
|
|
314
|
+
get certPem() {
|
|
315
|
+
return this._config.certPem;
|
|
316
|
+
}
|
|
317
|
+
get keyPem() {
|
|
318
|
+
return this._config.keyPem;
|
|
319
|
+
}
|
|
320
|
+
get caPem() {
|
|
321
|
+
return this._config.caPem;
|
|
322
|
+
}
|
|
323
|
+
get expiresAt() {
|
|
324
|
+
return this._config.expiresAt;
|
|
325
|
+
}
|
|
326
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
327
|
+
static _hasPemPrivateKey(pem) {
|
|
328
|
+
return pem.includes("-----BEGIN PRIVATE KEY-----") || pem.includes("-----BEGIN RSA PRIVATE KEY-----") || pem.includes("-----BEGIN EC PRIVATE KEY-----") || pem.includes("-----BEGIN ENCRYPTED PRIVATE KEY-----");
|
|
329
|
+
}
|
|
330
|
+
static _tryLoadFromDir(dir) {
|
|
331
|
+
try {
|
|
332
|
+
const fs2 = require("fs");
|
|
333
|
+
const certPath = `${dir}/identity.pem`;
|
|
334
|
+
const keyPath = `${dir}/identity_key.pem`;
|
|
335
|
+
if (!fs2.existsSync(certPath) || !fs2.existsSync(keyPath)) return null;
|
|
336
|
+
const caPath = `${dir}/ca.pem`;
|
|
337
|
+
return _ConduitIdentity.fromPaths(certPath, keyPath, fs2.existsSync(caPath) ? caPath : void 0);
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// src/index.ts
|
|
347
|
+
var index_exports = {};
|
|
348
|
+
__export(index_exports, {
|
|
349
|
+
AuthError: () => AuthError,
|
|
350
|
+
Client: () => Client2,
|
|
351
|
+
ConduitError: () => ConduitError,
|
|
352
|
+
ConduitIdentity: () => ConduitIdentity,
|
|
353
|
+
DEFAULT_IDENTITY_DIR: () => DEFAULT_IDENTITY_DIR,
|
|
354
|
+
DG_CA_URL: () => DG_CA_URL,
|
|
355
|
+
DG_SUBSTRATE_ENDPOINT: () => DG_SUBSTRATE_ENDPOINT,
|
|
356
|
+
GuidedSession: () => GuidedSession,
|
|
357
|
+
InvalidConfigError: () => InvalidConfigError,
|
|
358
|
+
NetworkError: () => NetworkError,
|
|
359
|
+
NotInitializedError: () => NotInitializedError,
|
|
360
|
+
OAuthTokenProvider: () => OAuthTokenProvider,
|
|
361
|
+
RateLimitError: () => RateLimitError,
|
|
362
|
+
ServerError: () => ServerError,
|
|
363
|
+
deriveTokenEndpoint: () => deriveTokenEndpoint,
|
|
364
|
+
extractMeta: () => extractMeta,
|
|
365
|
+
fetchDgCaCert: () => fetchDgCaCert,
|
|
366
|
+
fetchWithIdentity: () => fetchWithIdentity,
|
|
367
|
+
generateKeypair: () => generateKeypair,
|
|
368
|
+
isDgUrl: () => isDgUrl,
|
|
369
|
+
refreshCaCert: () => refreshCaCert,
|
|
370
|
+
registerIdentity: () => registerIdentity,
|
|
371
|
+
rotateIdentity: () => rotateIdentity,
|
|
372
|
+
saveIdentity: () => saveIdentity,
|
|
373
|
+
version: () => version
|
|
374
|
+
});
|
|
375
|
+
module.exports = __toCommonJS(index_exports);
|
|
376
|
+
|
|
377
|
+
// src/client.ts
|
|
378
|
+
var path2 = __toESM(require("path"));
|
|
379
|
+
|
|
380
|
+
// src/transports/base.ts
|
|
381
|
+
var Transport = class {
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// src/transports/mcp.ts
|
|
385
|
+
init_oauth();
|
|
386
|
+
var import_client = require("@modelcontextprotocol/sdk/client/index.js");
|
|
387
|
+
var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
388
|
+
var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
389
|
+
var MCPTransport = class extends Transport {
|
|
390
|
+
url;
|
|
391
|
+
auth;
|
|
392
|
+
identity;
|
|
393
|
+
client;
|
|
394
|
+
clientTransport;
|
|
395
|
+
oauthProvider;
|
|
396
|
+
constructor(url, auth, identity) {
|
|
397
|
+
super();
|
|
398
|
+
this.url = url;
|
|
399
|
+
this.auth = auth;
|
|
400
|
+
this.identity = identity;
|
|
401
|
+
if (auth?.clientCredentials) {
|
|
402
|
+
const cc = auth.clientCredentials;
|
|
403
|
+
const tokenEndpoint = cc.tokenEndpoint ?? deriveTokenEndpoint(url);
|
|
404
|
+
this.oauthProvider = new OAuthTokenProvider({
|
|
405
|
+
clientId: cc.clientId,
|
|
406
|
+
clientSecret: cc.clientSecret,
|
|
407
|
+
tokenEndpoint,
|
|
408
|
+
scope: cc.scope
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async buildHeaders() {
|
|
413
|
+
const headers = {};
|
|
414
|
+
if (this.oauthProvider) {
|
|
415
|
+
const token = await this.oauthProvider.getToken();
|
|
416
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
417
|
+
} else if (this.auth?.bearer) {
|
|
418
|
+
headers["Authorization"] = `Bearer ${this.auth.bearer}`;
|
|
419
|
+
} else if (this.auth?.basic) {
|
|
420
|
+
const encoded = Buffer.from(
|
|
421
|
+
`${this.auth.basic.username}:${this.auth.basic.password}`
|
|
422
|
+
).toString("base64");
|
|
423
|
+
headers["Authorization"] = `Basic ${encoded}`;
|
|
424
|
+
} else if (this.auth?.custom) {
|
|
425
|
+
Object.assign(headers, this.auth.custom);
|
|
426
|
+
}
|
|
427
|
+
return headers;
|
|
428
|
+
}
|
|
429
|
+
async connect() {
|
|
430
|
+
if (this.url.startsWith("stdio://")) {
|
|
431
|
+
const command = this.url.replace("stdio://", "");
|
|
432
|
+
const parts = command.split(" ");
|
|
433
|
+
this.clientTransport = new import_stdio.StdioClientTransport({
|
|
434
|
+
command: parts[0],
|
|
435
|
+
args: parts.slice(1)
|
|
436
|
+
});
|
|
437
|
+
} else if (this.url.startsWith("http://") || this.url.startsWith("https://")) {
|
|
438
|
+
const headers = await this.buildHeaders();
|
|
439
|
+
const transportOpts = {};
|
|
440
|
+
if (Object.keys(headers).length > 0) {
|
|
441
|
+
transportOpts.requestInit = { headers };
|
|
442
|
+
}
|
|
443
|
+
if (this.identity) {
|
|
444
|
+
const id = this.identity;
|
|
445
|
+
transportOpts.fetcher = (url, init) => {
|
|
446
|
+
const { fetchWithIdentity: fetchWithIdentity2 } = (init_identity(), __toCommonJS(identity_exports));
|
|
447
|
+
return fetchWithIdentity2(
|
|
448
|
+
typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url,
|
|
449
|
+
init ?? {},
|
|
450
|
+
id
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
this.clientTransport = new import_sse.SSEClientTransport(
|
|
455
|
+
new URL(this.url),
|
|
456
|
+
transportOpts
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
throw new Error(`Unsupported MCP URL scheme: ${this.url}`);
|
|
460
|
+
}
|
|
461
|
+
this.client = new import_client.Client(
|
|
462
|
+
{ name: "datagrout-conduit", version: "0.1.0" },
|
|
463
|
+
{ capabilities: {} }
|
|
464
|
+
);
|
|
465
|
+
await this.client.connect(this.clientTransport);
|
|
466
|
+
}
|
|
467
|
+
async disconnect() {
|
|
468
|
+
if (this.client) {
|
|
469
|
+
await this.client.close();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async listTools(options) {
|
|
473
|
+
if (!this.client) {
|
|
474
|
+
throw new Error("Not connected. Call connect() first.");
|
|
475
|
+
}
|
|
476
|
+
const result = await this.client.listTools(options);
|
|
477
|
+
return result.tools.map((tool) => ({
|
|
478
|
+
name: tool.name,
|
|
479
|
+
description: tool.description,
|
|
480
|
+
inputSchema: tool.inputSchema,
|
|
481
|
+
annotations: tool.annotations
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
async callTool(name, args, options) {
|
|
485
|
+
if (!this.client) {
|
|
486
|
+
throw new Error("Not connected. Call connect() first.");
|
|
487
|
+
}
|
|
488
|
+
const result = await this.client.callTool({ name, arguments: args });
|
|
489
|
+
const content = result?.content;
|
|
490
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
491
|
+
const first = content[0];
|
|
492
|
+
if (first && typeof first.text === "string") {
|
|
493
|
+
try {
|
|
494
|
+
return JSON.parse(first.text);
|
|
495
|
+
} catch {
|
|
496
|
+
return { text: first.text };
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
async listResources(options) {
|
|
503
|
+
if (!this.client) {
|
|
504
|
+
throw new Error("Not connected. Call connect() first.");
|
|
505
|
+
}
|
|
506
|
+
const result = await this.client.listResources();
|
|
507
|
+
return result.resources.map((resource) => ({
|
|
508
|
+
uri: resource.uri,
|
|
509
|
+
name: resource.name,
|
|
510
|
+
description: resource.description,
|
|
511
|
+
mimeType: resource.mimeType
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
async readResource(uri, options) {
|
|
515
|
+
if (!this.client) {
|
|
516
|
+
throw new Error("Not connected. Call connect() first.");
|
|
517
|
+
}
|
|
518
|
+
const result = await this.client.readResource({ uri });
|
|
519
|
+
return result.contents;
|
|
520
|
+
}
|
|
521
|
+
async listPrompts(options) {
|
|
522
|
+
if (!this.client) {
|
|
523
|
+
throw new Error("Not connected. Call connect() first.");
|
|
524
|
+
}
|
|
525
|
+
const result = await this.client.listPrompts();
|
|
526
|
+
return result.prompts.map((prompt) => ({
|
|
527
|
+
name: prompt.name,
|
|
528
|
+
description: prompt.description,
|
|
529
|
+
arguments: prompt.arguments
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
async getPrompt(name, args, options) {
|
|
533
|
+
if (!this.client) {
|
|
534
|
+
throw new Error("Not connected. Call connect() first.");
|
|
535
|
+
}
|
|
536
|
+
const result = await this.client.getPrompt({ name, arguments: args || {} });
|
|
537
|
+
return result.messages;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// src/transports/jsonrpc.ts
|
|
542
|
+
init_identity();
|
|
543
|
+
init_oauth();
|
|
544
|
+
|
|
545
|
+
// src/errors.ts
|
|
546
|
+
var ConduitError = class extends Error {
|
|
547
|
+
constructor(message) {
|
|
548
|
+
super(message);
|
|
549
|
+
this.name = this.constructor.name;
|
|
550
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
var NotInitializedError = class extends ConduitError {
|
|
554
|
+
constructor() {
|
|
555
|
+
super("Client not initialized. Call connect() first.");
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
var RateLimitError = class extends ConduitError {
|
|
559
|
+
status;
|
|
560
|
+
retryAfter;
|
|
561
|
+
constructor(status, retryAfter) {
|
|
562
|
+
const limitStr = status.limit === "unlimited" ? "unlimited" : `${status.limit.perHour}/hour`;
|
|
563
|
+
super(`Rate limit exceeded (${status.used} / ${limitStr} calls this hour)`);
|
|
564
|
+
this.status = status;
|
|
565
|
+
this.retryAfter = retryAfter;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
var AuthError = class extends ConduitError {
|
|
569
|
+
constructor(message = "Authentication failed") {
|
|
570
|
+
super(message);
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
var NetworkError = class extends ConduitError {
|
|
574
|
+
constructor(message) {
|
|
575
|
+
super(message);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
var ServerError = class extends ConduitError {
|
|
579
|
+
code;
|
|
580
|
+
serverMessage;
|
|
581
|
+
constructor(code, serverMessage) {
|
|
582
|
+
super(`Server error ${code}: ${serverMessage}`);
|
|
583
|
+
this.code = code;
|
|
584
|
+
this.serverMessage = serverMessage;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
var InvalidConfigError = class extends ConduitError {
|
|
588
|
+
constructor(message) {
|
|
589
|
+
super(message);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// src/transports/jsonrpc.ts
|
|
594
|
+
function unwrapContent(result) {
|
|
595
|
+
if (result && Array.isArray(result.content) && result.content.length > 0) {
|
|
596
|
+
const first = result.content[0];
|
|
597
|
+
if (first && typeof first.text === "string") {
|
|
598
|
+
try {
|
|
599
|
+
return JSON.parse(first.text);
|
|
600
|
+
} catch {
|
|
601
|
+
return { text: first.text };
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
function parseRateLimitError(response) {
|
|
608
|
+
const used = parseInt(response.headers.get("X-RateLimit-Used") ?? "0", 10) || 0;
|
|
609
|
+
const limitStr = response.headers.get("X-RateLimit-Limit") ?? "50";
|
|
610
|
+
const limit = limitStr.toLowerCase() === "unlimited" ? "unlimited" : { perHour: parseInt(limitStr, 10) || 50 };
|
|
611
|
+
const isLimited = limit === "unlimited" ? false : used >= limit.perHour;
|
|
612
|
+
const remaining = limit === "unlimited" ? null : Math.max(0, limit.perHour - used);
|
|
613
|
+
return new RateLimitError({ used, limit, isLimited, remaining });
|
|
614
|
+
}
|
|
615
|
+
var JSONRPCTransport = class extends Transport {
|
|
616
|
+
url;
|
|
617
|
+
auth;
|
|
618
|
+
identity;
|
|
619
|
+
timeout;
|
|
620
|
+
requestId = 0;
|
|
621
|
+
/** Resolved token provider, present only when `auth.clientCredentials` is set. */
|
|
622
|
+
oauthProvider;
|
|
623
|
+
constructor(url, auth, timeout = 3e4, identity) {
|
|
624
|
+
super();
|
|
625
|
+
this.url = url;
|
|
626
|
+
this.auth = auth;
|
|
627
|
+
this.identity = identity;
|
|
628
|
+
this.timeout = timeout;
|
|
629
|
+
if (identity?.needsRotation(30)) {
|
|
630
|
+
console.warn("[conduit] mTLS certificate expires within 30 days \u2014 consider rotating");
|
|
631
|
+
}
|
|
632
|
+
if (auth?.clientCredentials) {
|
|
633
|
+
const cc = auth.clientCredentials;
|
|
634
|
+
const tokenEndpoint = cc.tokenEndpoint ?? (() => {
|
|
635
|
+
const { deriveTokenEndpoint: deriveTokenEndpoint2 } = (init_oauth(), __toCommonJS(oauth_exports));
|
|
636
|
+
return deriveTokenEndpoint2(url);
|
|
637
|
+
})();
|
|
638
|
+
this.oauthProvider = new OAuthTokenProvider({
|
|
639
|
+
clientId: cc.clientId,
|
|
640
|
+
clientSecret: cc.clientSecret,
|
|
641
|
+
tokenEndpoint,
|
|
642
|
+
scope: cc.scope
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async connect() {
|
|
647
|
+
}
|
|
648
|
+
async disconnect() {
|
|
649
|
+
}
|
|
650
|
+
async call(method, params) {
|
|
651
|
+
return this._callWithRetry(method, params, false);
|
|
652
|
+
}
|
|
653
|
+
async _callWithRetry(method, params, isRetry) {
|
|
654
|
+
const headers = {
|
|
655
|
+
"Content-Type": "application/json"
|
|
656
|
+
};
|
|
657
|
+
if (this.oauthProvider) {
|
|
658
|
+
const token = await this.oauthProvider.getToken();
|
|
659
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
660
|
+
} else if (this.auth?.bearer) {
|
|
661
|
+
headers["Authorization"] = `Bearer ${this.auth.bearer}`;
|
|
662
|
+
} else if (this.auth?.basic) {
|
|
663
|
+
const credentials = btoa(`${this.auth.basic.username}:${this.auth.basic.password}`);
|
|
664
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
665
|
+
} else if (this.auth?.custom) {
|
|
666
|
+
Object.assign(headers, this.auth.custom);
|
|
667
|
+
}
|
|
668
|
+
const request = {
|
|
669
|
+
jsonrpc: "2.0",
|
|
670
|
+
id: ++this.requestId,
|
|
671
|
+
method,
|
|
672
|
+
params: params || {}
|
|
673
|
+
};
|
|
674
|
+
const controller = new AbortController();
|
|
675
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
676
|
+
const fetchInit = {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers,
|
|
679
|
+
body: JSON.stringify(request),
|
|
680
|
+
signal: controller.signal
|
|
681
|
+
};
|
|
682
|
+
try {
|
|
683
|
+
const response = this.identity ? await fetchWithIdentity(this.url, fetchInit, this.identity) : await fetch(this.url, fetchInit);
|
|
684
|
+
if (response.status === 429) {
|
|
685
|
+
throw parseRateLimitError(response);
|
|
686
|
+
}
|
|
687
|
+
if (response.status === 401 && this.oauthProvider && !isRetry) {
|
|
688
|
+
this.oauthProvider.invalidate();
|
|
689
|
+
return this._callWithRetry(method, params, true);
|
|
690
|
+
}
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
693
|
+
}
|
|
694
|
+
const data = await response.json();
|
|
695
|
+
if (data.error) {
|
|
696
|
+
throw new Error(`JSONRPC Error: ${JSON.stringify(data.error)}`);
|
|
697
|
+
}
|
|
698
|
+
return data.result;
|
|
699
|
+
} finally {
|
|
700
|
+
clearTimeout(timeoutId);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
async listTools(options) {
|
|
704
|
+
const result = await this.call("tools/list", options);
|
|
705
|
+
return result?.tools || [];
|
|
706
|
+
}
|
|
707
|
+
async callTool(name, args, options) {
|
|
708
|
+
const params = { name, arguments: args, ...options };
|
|
709
|
+
const result = await this.call("tools/call", params);
|
|
710
|
+
return unwrapContent(result);
|
|
711
|
+
}
|
|
712
|
+
async listResources(options) {
|
|
713
|
+
const result = await this.call("resources/list", options);
|
|
714
|
+
return result?.resources || [];
|
|
715
|
+
}
|
|
716
|
+
async readResource(uri, options) {
|
|
717
|
+
const params = { uri, ...options };
|
|
718
|
+
return await this.call("resources/read", params);
|
|
719
|
+
}
|
|
720
|
+
async listPrompts(options) {
|
|
721
|
+
const result = await this.call("prompts/list", options);
|
|
722
|
+
return result?.prompts || [];
|
|
723
|
+
}
|
|
724
|
+
async getPrompt(name, args, options) {
|
|
725
|
+
const params = { name, arguments: args || {}, ...options };
|
|
726
|
+
return await this.call("prompts/get", params);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// src/client.ts
|
|
731
|
+
init_identity();
|
|
732
|
+
|
|
733
|
+
// src/registration.ts
|
|
734
|
+
var fs = __toESM(require("fs"));
|
|
735
|
+
var os = __toESM(require("os"));
|
|
736
|
+
var path = __toESM(require("path"));
|
|
737
|
+
var crypto = __toESM(require("crypto"));
|
|
738
|
+
var DG_CA_URL = "https://ca.datagrout.ai/ca.pem";
|
|
739
|
+
var DG_SUBSTRATE_ENDPOINT = "https://app.datagrout.ai/api/v1/substrate/identity";
|
|
740
|
+
var DEFAULT_IDENTITY_DIR = path.join(os.homedir(), ".conduit");
|
|
741
|
+
async function fetchDgCaCert(url = DG_CA_URL) {
|
|
742
|
+
const resp = await fetch(url, {
|
|
743
|
+
headers: { Accept: "application/x-pem-file, text/plain, */*" }
|
|
744
|
+
});
|
|
745
|
+
if (!resp.ok) {
|
|
746
|
+
throw new Error(`CA cert fetch failed (HTTP ${resp.status}) from ${url}`);
|
|
747
|
+
}
|
|
748
|
+
const pem = await resp.text();
|
|
749
|
+
if (!pem.includes("-----BEGIN CERTIFICATE-----")) {
|
|
750
|
+
throw new Error(`Response from ${url} does not look like a PEM certificate`);
|
|
751
|
+
}
|
|
752
|
+
return pem;
|
|
753
|
+
}
|
|
754
|
+
async function refreshCaCert(identityDir = DEFAULT_IDENTITY_DIR, url = DG_CA_URL) {
|
|
755
|
+
const pem = await fetchDgCaCert(url);
|
|
756
|
+
fs.mkdirSync(identityDir, { recursive: true });
|
|
757
|
+
const caPath = path.join(identityDir, "ca.pem");
|
|
758
|
+
fs.writeFileSync(caPath, pem, "utf8");
|
|
759
|
+
return caPath;
|
|
760
|
+
}
|
|
761
|
+
function generateKeypair() {
|
|
762
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", {
|
|
763
|
+
namedCurve: "P-256"
|
|
764
|
+
});
|
|
765
|
+
return {
|
|
766
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }),
|
|
767
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" })
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
async function registerIdentity(keypair, opts) {
|
|
771
|
+
const { privateKeyPem, publicKeyPem } = keypair;
|
|
772
|
+
const url = opts.endpoint.replace(/\/$/, "") + "/register";
|
|
773
|
+
const resp = await fetch(url, {
|
|
774
|
+
method: "POST",
|
|
775
|
+
headers: {
|
|
776
|
+
Authorization: `Bearer ${opts.authToken}`,
|
|
777
|
+
"Content-Type": "application/json"
|
|
778
|
+
},
|
|
779
|
+
body: JSON.stringify({ public_key_pem: publicKeyPem, name: opts.name })
|
|
780
|
+
});
|
|
781
|
+
if (!resp.ok) {
|
|
782
|
+
const body2 = await resp.text();
|
|
783
|
+
throw new Error(`Registration failed (HTTP ${resp.status}): ${body2}`);
|
|
784
|
+
}
|
|
785
|
+
const body = await resp.json();
|
|
786
|
+
let caPem = body.ca_cert_pem;
|
|
787
|
+
if (!caPem) {
|
|
788
|
+
try {
|
|
789
|
+
caPem = await fetchDgCaCert(opts.caUrl ?? DG_CA_URL);
|
|
790
|
+
} catch {
|
|
791
|
+
caPem = void 0;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
certPem: body.cert_pem,
|
|
796
|
+
keyPem: privateKeyPem,
|
|
797
|
+
caPem,
|
|
798
|
+
id: body.id,
|
|
799
|
+
name: body.name,
|
|
800
|
+
fingerprint: body.fingerprint,
|
|
801
|
+
registeredAt: body.registered_at ?? "",
|
|
802
|
+
validUntil: body.valid_until
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async function rotateIdentity(opts) {
|
|
806
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" });
|
|
807
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
808
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
809
|
+
const url = opts.endpoint.replace(/\/$/, "") + "/rotate";
|
|
810
|
+
const https = await import("https");
|
|
811
|
+
const urlModule = await import("url");
|
|
812
|
+
const parsed = new urlModule.URL(url);
|
|
813
|
+
const responseBody = await new Promise((resolve, reject) => {
|
|
814
|
+
const reqOptions = {
|
|
815
|
+
hostname: parsed.hostname,
|
|
816
|
+
port: parsed.port || 443,
|
|
817
|
+
path: parsed.pathname,
|
|
818
|
+
method: "POST",
|
|
819
|
+
headers: { "Content-Type": "application/json" },
|
|
820
|
+
cert: opts.currentCertPem,
|
|
821
|
+
key: opts.currentKeyPem
|
|
822
|
+
};
|
|
823
|
+
const body = JSON.stringify({ public_key_pem: publicKeyPem, name: opts.name });
|
|
824
|
+
const req = https.request(reqOptions, (res) => {
|
|
825
|
+
let data = "";
|
|
826
|
+
res.on("data", (chunk) => {
|
|
827
|
+
data += chunk;
|
|
828
|
+
});
|
|
829
|
+
res.on("end", () => {
|
|
830
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
831
|
+
resolve(data);
|
|
832
|
+
} else {
|
|
833
|
+
reject(new Error(`Rotation failed (HTTP ${res.statusCode}): ${data}`));
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
req.on("error", reject);
|
|
838
|
+
req.write(body);
|
|
839
|
+
req.end();
|
|
840
|
+
});
|
|
841
|
+
const respBody = JSON.parse(responseBody);
|
|
842
|
+
let caPem = respBody.ca_cert_pem;
|
|
843
|
+
if (!caPem) {
|
|
844
|
+
try {
|
|
845
|
+
caPem = await fetchDgCaCert(opts.caUrl ?? DG_CA_URL);
|
|
846
|
+
} catch {
|
|
847
|
+
caPem = void 0;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
certPem: respBody.cert_pem,
|
|
852
|
+
keyPem: privateKeyPem,
|
|
853
|
+
caPem,
|
|
854
|
+
id: respBody.id,
|
|
855
|
+
name: respBody.name,
|
|
856
|
+
fingerprint: respBody.fingerprint,
|
|
857
|
+
registeredAt: respBody.registered_at ?? "",
|
|
858
|
+
validUntil: respBody.valid_until
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function saveIdentity(identity, directory = DEFAULT_IDENTITY_DIR) {
|
|
862
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
863
|
+
const certPath = path.join(directory, "identity.pem");
|
|
864
|
+
const keyPath = path.join(directory, "identity_key.pem");
|
|
865
|
+
fs.writeFileSync(certPath, identity.certPem, "utf8");
|
|
866
|
+
fs.writeFileSync(keyPath, identity.keyPem, { encoding: "utf8", mode: 384 });
|
|
867
|
+
const result = { certPath, keyPath };
|
|
868
|
+
if (identity.caPem) {
|
|
869
|
+
const caPath = path.join(directory, "ca.pem");
|
|
870
|
+
fs.writeFileSync(caPath, identity.caPem, "utf8");
|
|
871
|
+
result.caPath = caPath;
|
|
872
|
+
}
|
|
873
|
+
return result;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/client.ts
|
|
877
|
+
var GuidedSession = class {
|
|
878
|
+
client;
|
|
879
|
+
state;
|
|
880
|
+
constructor(client, state) {
|
|
881
|
+
this.client = client;
|
|
882
|
+
this.state = state;
|
|
883
|
+
}
|
|
884
|
+
/** Unique session identifier used to resume the workflow. */
|
|
885
|
+
get sessionId() {
|
|
886
|
+
return this.state.sessionId;
|
|
887
|
+
}
|
|
888
|
+
/** Current workflow status (e.g. `"ready"`, `"completed"`). */
|
|
889
|
+
get status() {
|
|
890
|
+
return this.state.status;
|
|
891
|
+
}
|
|
892
|
+
/** Available options for the current step. */
|
|
893
|
+
get options() {
|
|
894
|
+
return this.state.options || [];
|
|
895
|
+
}
|
|
896
|
+
/** Final result, populated once `status === "completed"`. */
|
|
897
|
+
get result() {
|
|
898
|
+
return this.state.result;
|
|
899
|
+
}
|
|
900
|
+
/** Returns the raw `GuideState` snapshot for this step. */
|
|
901
|
+
getState() {
|
|
902
|
+
return this.state;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Advance the guided session by selecting an option.
|
|
906
|
+
*
|
|
907
|
+
* @param optionId - The `id` of the option to choose (from `session.options`).
|
|
908
|
+
* @returns A new `GuidedSession` reflecting the next workflow step.
|
|
909
|
+
*/
|
|
910
|
+
async choose(optionId) {
|
|
911
|
+
return await this.client.guide({
|
|
912
|
+
sessionId: this.sessionId,
|
|
913
|
+
choice: optionId
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Retrieve the completed workflow result.
|
|
918
|
+
*
|
|
919
|
+
* Throws if the session has not yet reached `status === "completed"`.
|
|
920
|
+
* Use `choose()` to advance through remaining steps first.
|
|
921
|
+
*/
|
|
922
|
+
async complete() {
|
|
923
|
+
if (this.status === "completed") {
|
|
924
|
+
return this.result;
|
|
925
|
+
}
|
|
926
|
+
throw new Error(
|
|
927
|
+
`Workflow not complete (status: ${this.status}). Call choose() with one of the available options.`
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
function isDgUrl(url) {
|
|
932
|
+
return url.includes("datagrout.ai") || url.includes("datagrout.dev") || !!process.env["CONDUIT_IS_DG"];
|
|
933
|
+
}
|
|
934
|
+
var Client2 = class _Client {
|
|
935
|
+
url;
|
|
936
|
+
auth;
|
|
937
|
+
useIntelligentInterface;
|
|
938
|
+
transport;
|
|
939
|
+
isDg;
|
|
940
|
+
dgWarned = false;
|
|
941
|
+
initialized = false;
|
|
942
|
+
maxRetries;
|
|
943
|
+
constructor(options) {
|
|
944
|
+
if (typeof options === "string") {
|
|
945
|
+
options = { url: options };
|
|
946
|
+
}
|
|
947
|
+
this.url = options.url;
|
|
948
|
+
this.auth = options.auth;
|
|
949
|
+
this.isDg = isDgUrl(this.url);
|
|
950
|
+
this.useIntelligentInterface = options.useIntelligentInterface ?? this.isDg;
|
|
951
|
+
this.maxRetries = options.maxRetries ?? 3;
|
|
952
|
+
let identity = options.identity ?? (options.identityAuto ? ConduitIdentity.tryDiscover(options.identityDir) ?? void 0 : void 0);
|
|
953
|
+
if (identity === void 0 && this.isDg && !options.disableMtls) {
|
|
954
|
+
identity = ConduitIdentity.tryDiscover(options.identityDir) ?? void 0;
|
|
955
|
+
}
|
|
956
|
+
const transportType = options.transport || "mcp";
|
|
957
|
+
if (transportType === "mcp") {
|
|
958
|
+
this.transport = new MCPTransport(this.url, this.auth, identity);
|
|
959
|
+
} else {
|
|
960
|
+
const rpcUrl = this.url.endsWith("/mcp") ? this.url.slice(0, -4) + "/rpc" : this.url;
|
|
961
|
+
this.transport = new JSONRPCTransport(rpcUrl, this.auth, options.timeout, identity);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// ===== Bootstrap / seamless mTLS =====
|
|
965
|
+
/**
|
|
966
|
+
* Create a `Client` with an mTLS identity bootstrapped automatically.
|
|
967
|
+
*
|
|
968
|
+
* Checks the auto-discovery chain first. If an existing identity is found
|
|
969
|
+
* and not within 7 days of expiry, it is reused. Otherwise a new keypair
|
|
970
|
+
* is generated, registered with DataGrout, and saved locally.
|
|
971
|
+
*
|
|
972
|
+
* After the first successful bootstrap the token is no longer needed —
|
|
973
|
+
* mTLS handles authentication on every subsequent run.
|
|
974
|
+
*
|
|
975
|
+
* @param options.url - DataGrout server URL.
|
|
976
|
+
* @param options.authToken - Bearer token for the initial registration call.
|
|
977
|
+
* @param options.name - Human-readable identity name (default: `"conduit-client"`).
|
|
978
|
+
* @param options.identityDir - Custom identity storage directory.
|
|
979
|
+
* @param options.substrateEndpoint - Override the DG Substrate endpoint.
|
|
980
|
+
*/
|
|
981
|
+
static async bootstrapIdentity(options) {
|
|
982
|
+
const dir = options.identityDir || DEFAULT_IDENTITY_DIR;
|
|
983
|
+
const name = options.name || "conduit-client";
|
|
984
|
+
const endpoint = options.substrateEndpoint || DG_SUBSTRATE_ENDPOINT;
|
|
985
|
+
const existing = ConduitIdentity.tryDiscover(dir);
|
|
986
|
+
if (existing && !existing.needsRotation(7)) {
|
|
987
|
+
const client2 = new _Client({ url: options.url, identity: existing });
|
|
988
|
+
await client2.connect();
|
|
989
|
+
return client2;
|
|
990
|
+
}
|
|
991
|
+
const keypair = generateKeypair();
|
|
992
|
+
const registered = await registerIdentity(keypair, {
|
|
993
|
+
endpoint,
|
|
994
|
+
authToken: options.authToken,
|
|
995
|
+
name
|
|
996
|
+
});
|
|
997
|
+
saveIdentity(registered, dir);
|
|
998
|
+
const identity = ConduitIdentity.fromPaths(
|
|
999
|
+
path2.join(dir, "identity.pem"),
|
|
1000
|
+
path2.join(dir, "identity_key.pem"),
|
|
1001
|
+
path2.join(dir, "ca.pem")
|
|
1002
|
+
);
|
|
1003
|
+
const client = new _Client({ url: options.url, identity });
|
|
1004
|
+
await client.connect();
|
|
1005
|
+
return client;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Like `bootstrapIdentity` but uses OAuth 2.1 `client_credentials` to obtain
|
|
1009
|
+
* the bearer token automatically — no pre-obtained token needed.
|
|
1010
|
+
*
|
|
1011
|
+
* @param options.url - DataGrout server URL.
|
|
1012
|
+
* @param options.clientId - OAuth client ID.
|
|
1013
|
+
* @param options.clientSecret - OAuth client secret.
|
|
1014
|
+
* @param options.name - Human-readable identity name (default: `"conduit-client"`).
|
|
1015
|
+
* @param options.identityDir - Custom identity storage directory.
|
|
1016
|
+
* @param options.substrateEndpoint - Override the DG Substrate endpoint.
|
|
1017
|
+
*/
|
|
1018
|
+
static async bootstrapIdentityOAuth(options) {
|
|
1019
|
+
const { OAuthTokenProvider: OAuthTokenProvider2, deriveTokenEndpoint: deriveTokenEndpoint2 } = await Promise.resolve().then(() => (init_oauth(), oauth_exports));
|
|
1020
|
+
const tokenEndpoint = deriveTokenEndpoint2(options.url);
|
|
1021
|
+
const provider = new OAuthTokenProvider2({
|
|
1022
|
+
clientId: options.clientId,
|
|
1023
|
+
clientSecret: options.clientSecret,
|
|
1024
|
+
tokenEndpoint
|
|
1025
|
+
});
|
|
1026
|
+
const token = await provider.getToken();
|
|
1027
|
+
return _Client.bootstrapIdentity({
|
|
1028
|
+
url: options.url,
|
|
1029
|
+
authToken: token,
|
|
1030
|
+
name: options.name,
|
|
1031
|
+
identityDir: options.identityDir,
|
|
1032
|
+
substrateEndpoint: options.substrateEndpoint
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
// ===== Lifecycle =====
|
|
1036
|
+
/**
|
|
1037
|
+
* Establish the underlying transport connection.
|
|
1038
|
+
*
|
|
1039
|
+
* Must be called before any other method. For the MCP transport this
|
|
1040
|
+
* performs the JSON-RPC `initialize` handshake; for JSONRPC it is a no-op
|
|
1041
|
+
* (connections are per-request).
|
|
1042
|
+
*/
|
|
1043
|
+
async connect() {
|
|
1044
|
+
await this.transport.connect();
|
|
1045
|
+
this.initialized = true;
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Close the underlying transport connection and mark the client as
|
|
1049
|
+
* uninitialized. After calling this, `connect()` must be called again
|
|
1050
|
+
* before issuing further requests.
|
|
1051
|
+
*/
|
|
1052
|
+
async disconnect() {
|
|
1053
|
+
await this.transport.disconnect();
|
|
1054
|
+
this.initialized = false;
|
|
1055
|
+
}
|
|
1056
|
+
ensureInitialized() {
|
|
1057
|
+
if (!this.initialized) {
|
|
1058
|
+
throw new NotInitializedError();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Wrap a transport call with automatic retry on "not initialized" errors
|
|
1063
|
+
* (JSON-RPC code -32002). Re-initializes the connection and retries up to
|
|
1064
|
+
* `maxRetries` times with 500ms backoff between attempts.
|
|
1065
|
+
*/
|
|
1066
|
+
async sendWithRetry(fn) {
|
|
1067
|
+
let retries = this.maxRetries;
|
|
1068
|
+
while (true) {
|
|
1069
|
+
try {
|
|
1070
|
+
return await fn();
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
const isNotInit = error?.code === -32002 || error?.message?.includes("not initialized");
|
|
1073
|
+
if (isNotInit && retries > 0) {
|
|
1074
|
+
retries--;
|
|
1075
|
+
await this.connect();
|
|
1076
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// ===== Standard MCP API (Drop-in Compatible) =====
|
|
1084
|
+
/**
|
|
1085
|
+
* List all tools exposed by the connected MCP server.
|
|
1086
|
+
*
|
|
1087
|
+
* Calls the MCP `tools/list` method with automatic cursor-based pagination.
|
|
1088
|
+
* When `useIntelligentInterface` is enabled (default for DataGrout URLs),
|
|
1089
|
+
* integration tools (names containing `@`) are filtered out, leaving only
|
|
1090
|
+
* the DataGrout semantic discovery interface.
|
|
1091
|
+
*
|
|
1092
|
+
* JSON-RPC method: `tools/list`
|
|
1093
|
+
*/
|
|
1094
|
+
async listTools(options) {
|
|
1095
|
+
this.ensureInitialized();
|
|
1096
|
+
return this.sendWithRetry(async () => {
|
|
1097
|
+
let allTools = [];
|
|
1098
|
+
let cursor;
|
|
1099
|
+
do {
|
|
1100
|
+
const response = await this.transport.listTools({ ...options, cursor });
|
|
1101
|
+
const tools = Array.isArray(response) ? response : response.tools || [];
|
|
1102
|
+
allTools.push(...tools);
|
|
1103
|
+
cursor = Array.isArray(response) ? void 0 : response.nextCursor;
|
|
1104
|
+
} while (cursor);
|
|
1105
|
+
if (this.useIntelligentInterface) {
|
|
1106
|
+
return allTools.filter((t) => !t.name.includes("@"));
|
|
1107
|
+
}
|
|
1108
|
+
return allTools;
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Invoke a named tool on the connected MCP server.
|
|
1113
|
+
*
|
|
1114
|
+
* JSON-RPC method: `tools/call`
|
|
1115
|
+
*
|
|
1116
|
+
* @param name - Fully-qualified tool name (e.g. `salesforce@v1/get_lead@v1`).
|
|
1117
|
+
* @param args - Tool input arguments.
|
|
1118
|
+
*/
|
|
1119
|
+
async callTool(name, args, _options) {
|
|
1120
|
+
this.ensureInitialized();
|
|
1121
|
+
return this.sendWithRetry(() => this.transport.callTool(name, args));
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* List resources exposed by the connected MCP server.
|
|
1125
|
+
*
|
|
1126
|
+
* JSON-RPC method: `resources/list`
|
|
1127
|
+
*/
|
|
1128
|
+
async listResources(options) {
|
|
1129
|
+
this.ensureInitialized();
|
|
1130
|
+
return this.sendWithRetry(() => this.transport.listResources(options));
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Read the content of a named resource.
|
|
1134
|
+
*
|
|
1135
|
+
* JSON-RPC method: `resources/read`
|
|
1136
|
+
*
|
|
1137
|
+
* @param uri - Resource URI as returned by `listResources()`.
|
|
1138
|
+
*/
|
|
1139
|
+
async readResource(uri, options) {
|
|
1140
|
+
this.ensureInitialized();
|
|
1141
|
+
return this.sendWithRetry(() => this.transport.readResource(uri, options));
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* List prompt templates exposed by the connected MCP server.
|
|
1145
|
+
*
|
|
1146
|
+
* JSON-RPC method: `prompts/list`
|
|
1147
|
+
*/
|
|
1148
|
+
async listPrompts(options) {
|
|
1149
|
+
this.ensureInitialized();
|
|
1150
|
+
return this.sendWithRetry(() => this.transport.listPrompts(options));
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Retrieve a prompt template, optionally instantiated with arguments.
|
|
1154
|
+
*
|
|
1155
|
+
* JSON-RPC method: `prompts/get`
|
|
1156
|
+
*
|
|
1157
|
+
* @param name - Prompt name as returned by `listPrompts()`.
|
|
1158
|
+
* @param args - Template argument values.
|
|
1159
|
+
*/
|
|
1160
|
+
async getPrompt(name, args, options) {
|
|
1161
|
+
this.ensureInitialized();
|
|
1162
|
+
return this.sendWithRetry(() => this.transport.getPrompt(name, args, options));
|
|
1163
|
+
}
|
|
1164
|
+
// ===== DG-awareness helpers =====
|
|
1165
|
+
warnIfNotDg(method) {
|
|
1166
|
+
if (!this.isDg && !this.dgWarned) {
|
|
1167
|
+
this.dgWarned = true;
|
|
1168
|
+
console.warn(
|
|
1169
|
+
`[conduit] \`${method}\` is a DataGrout-specific extension. The connected server may not support it. Standard MCP methods (listTools, callTool, \u2026) work on any server.`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
// ===== DataGrout Extensions =====
|
|
1174
|
+
/**
|
|
1175
|
+
* Semantically discover tools relevant to a goal or query.
|
|
1176
|
+
*
|
|
1177
|
+
* Uses DataGrout's vector-search index to find the best-matching tools
|
|
1178
|
+
* across all registered integrations. Returns ranked results with scores,
|
|
1179
|
+
* descriptions, and schemas.
|
|
1180
|
+
*
|
|
1181
|
+
* JSON-RPC method: `tools/call` → `data-grout/discovery.discover`
|
|
1182
|
+
*
|
|
1183
|
+
* @param options.query - Natural language search query.
|
|
1184
|
+
* @param options.goal - High-level goal description (alternative to `query`).
|
|
1185
|
+
* @param options.limit - Maximum results to return (default: 10).
|
|
1186
|
+
* @param options.minScore - Minimum relevance score (default: 0.0).
|
|
1187
|
+
* @param options.integrations - Filter by specific integration names.
|
|
1188
|
+
* @param options.servers - Filter by specific server IDs.
|
|
1189
|
+
*/
|
|
1190
|
+
async discover(options) {
|
|
1191
|
+
this.ensureInitialized();
|
|
1192
|
+
this.warnIfNotDg("discover");
|
|
1193
|
+
const params = {
|
|
1194
|
+
limit: options.limit ?? 10,
|
|
1195
|
+
min_score: options.minScore ?? 0
|
|
1196
|
+
};
|
|
1197
|
+
if (options.query) params.query = options.query;
|
|
1198
|
+
if (options.goal) params.goal = options.goal;
|
|
1199
|
+
if (options.integrations) params.integrations = options.integrations;
|
|
1200
|
+
if (options.servers) params.servers = options.servers;
|
|
1201
|
+
const result = await this.sendWithRetry(
|
|
1202
|
+
() => this.transport.callTool("data-grout/discovery.discover", params)
|
|
1203
|
+
);
|
|
1204
|
+
const tools = result.results || result.tools || [];
|
|
1205
|
+
return {
|
|
1206
|
+
queryUsed: result.goal_used || result.query_used || result.queryUsed || "",
|
|
1207
|
+
results: tools.map((r) => ({
|
|
1208
|
+
toolName: r.tool_name || r.toolName,
|
|
1209
|
+
integration: r.integration,
|
|
1210
|
+
serverId: r.server_id || r.serverId || r.server,
|
|
1211
|
+
score: r.score,
|
|
1212
|
+
distance: r.distance,
|
|
1213
|
+
description: r.description,
|
|
1214
|
+
sideEffects: r.side_effects || r.sideEffects,
|
|
1215
|
+
inputSchema: r.input_contract || r.input_schema || r.inputSchema,
|
|
1216
|
+
outputSchema: r.output_contract || r.output_schema || r.outputSchema
|
|
1217
|
+
})),
|
|
1218
|
+
total: result.total ?? tools.length,
|
|
1219
|
+
limit: result.limit ?? (options.limit ?? 10)
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Execute a single tool call routed through DataGrout's gateway.
|
|
1224
|
+
*
|
|
1225
|
+
* The gateway handles credential injection, usage tracking, and receipts.
|
|
1226
|
+
* Use `performBatch()` to execute multiple calls in one request.
|
|
1227
|
+
*
|
|
1228
|
+
* JSON-RPC method: `tools/call` → `data-grout/discovery.perform`
|
|
1229
|
+
*
|
|
1230
|
+
* @param options.tool - Fully-qualified tool name.
|
|
1231
|
+
* @param options.args - Tool input arguments.
|
|
1232
|
+
* @param options.demux - When `true`, use semantic demultiplexing.
|
|
1233
|
+
* @param options.demuxMode - Demux strictness (`"strict"` | `"fuzzy"`).
|
|
1234
|
+
*/
|
|
1235
|
+
async perform(options) {
|
|
1236
|
+
this.ensureInitialized();
|
|
1237
|
+
this.warnIfNotDg("perform");
|
|
1238
|
+
return await this.performWithTracking(
|
|
1239
|
+
options.tool,
|
|
1240
|
+
options.args,
|
|
1241
|
+
options.demux ? { demux: options.demux, demuxMode: options.demuxMode } : void 0
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Execute multiple tool calls in a single gateway request.
|
|
1246
|
+
*
|
|
1247
|
+
* JSON-RPC method: `tools/call` → `data-grout/discovery.perform`
|
|
1248
|
+
*
|
|
1249
|
+
* @param calls - Array of `{ tool, args }` call descriptors.
|
|
1250
|
+
*/
|
|
1251
|
+
async performBatch(calls) {
|
|
1252
|
+
this.ensureInitialized();
|
|
1253
|
+
this.warnIfNotDg("performBatch");
|
|
1254
|
+
return this.sendWithRetry(
|
|
1255
|
+
() => this.transport.callTool("data-grout/discovery.perform", calls)
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Start or advance a guided workflow session.
|
|
1260
|
+
*
|
|
1261
|
+
* The first call (with only `goal`) starts a new session and returns the
|
|
1262
|
+
* initial options. Subsequent calls provide `sessionId` and `choice` to
|
|
1263
|
+
* advance through steps. Returns a `GuidedSession` that exposes helpers
|
|
1264
|
+
* for navigating the workflow.
|
|
1265
|
+
*
|
|
1266
|
+
* JSON-RPC method: `tools/call` → `data-grout/discovery.guide`
|
|
1267
|
+
*
|
|
1268
|
+
* @param options.goal - High-level goal to accomplish (first call only).
|
|
1269
|
+
* @param options.policy - Optional policy constraints for tool selection.
|
|
1270
|
+
* @param options.sessionId - Resume an existing session (subsequent calls).
|
|
1271
|
+
* @param options.choice - Option ID selected at the current step.
|
|
1272
|
+
*/
|
|
1273
|
+
async guide(options) {
|
|
1274
|
+
this.ensureInitialized();
|
|
1275
|
+
this.warnIfNotDg("guide");
|
|
1276
|
+
const params = {};
|
|
1277
|
+
if (options.goal) params.goal = options.goal;
|
|
1278
|
+
if (options.policy) params.policy = options.policy;
|
|
1279
|
+
if (options.sessionId) params.session_id = options.sessionId;
|
|
1280
|
+
if (options.choice) params.choice = options.choice;
|
|
1281
|
+
const result = await this.sendWithRetry(
|
|
1282
|
+
() => this.transport.callTool("data-grout/discovery.guide", params)
|
|
1283
|
+
);
|
|
1284
|
+
const state = {
|
|
1285
|
+
sessionId: result.session_id || result.sessionId,
|
|
1286
|
+
step: result.step,
|
|
1287
|
+
message: result.message,
|
|
1288
|
+
status: result.status,
|
|
1289
|
+
options: result.options?.map((o) => ({
|
|
1290
|
+
id: o.id,
|
|
1291
|
+
label: o.label,
|
|
1292
|
+
cost: o.cost,
|
|
1293
|
+
viable: o.viable,
|
|
1294
|
+
metadata: o.metadata
|
|
1295
|
+
})),
|
|
1296
|
+
pathTaken: result.path_taken || result.pathTaken,
|
|
1297
|
+
totalCost: result.total_cost || result.totalCost,
|
|
1298
|
+
result: result.result,
|
|
1299
|
+
progress: result.progress
|
|
1300
|
+
};
|
|
1301
|
+
return new GuidedSession(this, state);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Execute a pre-planned sequence of tool calls (a "flow") through the
|
|
1305
|
+
* DataGrout gateway.
|
|
1306
|
+
*
|
|
1307
|
+
* JSON-RPC method: `tools/call` → `data-grout/flow.into`
|
|
1308
|
+
*
|
|
1309
|
+
* @param options.plan - Ordered list of tool call descriptors.
|
|
1310
|
+
* @param options.validateCtc - Validate each call against its CTC schema (default: `true`).
|
|
1311
|
+
* @param options.saveAsSkill - Persist the flow as a reusable skill (default: `false`).
|
|
1312
|
+
* @param options.inputData - Runtime input data for the flow.
|
|
1313
|
+
*/
|
|
1314
|
+
async flowInto(options) {
|
|
1315
|
+
this.ensureInitialized();
|
|
1316
|
+
this.warnIfNotDg("flowInto");
|
|
1317
|
+
const params = {
|
|
1318
|
+
plan: options.plan,
|
|
1319
|
+
validate_ctc: options.validateCtc ?? true,
|
|
1320
|
+
save_as_skill: options.saveAsSkill ?? false
|
|
1321
|
+
};
|
|
1322
|
+
if (options.inputData) {
|
|
1323
|
+
params.input_data = options.inputData;
|
|
1324
|
+
}
|
|
1325
|
+
return this.sendWithRetry(
|
|
1326
|
+
() => this.transport.callTool("data-grout/flow.into", params)
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Transform data from one annotated type to another using the DataGrout
|
|
1331
|
+
* Prism semantic mapping engine.
|
|
1332
|
+
*
|
|
1333
|
+
* JSON-RPC method: `tools/call` → `data-grout/prism.focus`
|
|
1334
|
+
*
|
|
1335
|
+
* @param options.data - Source payload to transform.
|
|
1336
|
+
* @param options.sourceType - Semantic type of the source data.
|
|
1337
|
+
* @param options.targetType - Desired semantic type for the output.
|
|
1338
|
+
* @param options.sourceAnnotations - Additional schema hints for the source.
|
|
1339
|
+
* @param options.targetAnnotations - Additional schema hints for the target.
|
|
1340
|
+
* @param options.context - Free-text context to guide the mapping.
|
|
1341
|
+
*/
|
|
1342
|
+
async prismFocus(options) {
|
|
1343
|
+
this.ensureInitialized();
|
|
1344
|
+
this.warnIfNotDg("prismFocus");
|
|
1345
|
+
const params = {
|
|
1346
|
+
data: options.data,
|
|
1347
|
+
source_type: options.sourceType,
|
|
1348
|
+
target_type: options.targetType,
|
|
1349
|
+
...options.sourceAnnotations && { source_annotations: options.sourceAnnotations },
|
|
1350
|
+
...options.targetAnnotations && { target_annotations: options.targetAnnotations },
|
|
1351
|
+
...options.context && { context: options.context }
|
|
1352
|
+
};
|
|
1353
|
+
return this.sendWithRetry(
|
|
1354
|
+
() => this.transport.callTool("data-grout/prism.focus", params)
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Generate an execution plan for a goal using DataGrout's planning engine.
|
|
1359
|
+
*
|
|
1360
|
+
* Returns an ordered list of tool calls that, when executed, accomplish the
|
|
1361
|
+
* stated goal. Throws `InvalidConfigError` if neither `goal` nor `query` is
|
|
1362
|
+
* provided.
|
|
1363
|
+
*
|
|
1364
|
+
* JSON-RPC method: `tools/call` → `data-grout/discovery.plan`
|
|
1365
|
+
*
|
|
1366
|
+
* @param options.goal - High-level goal description.
|
|
1367
|
+
* @param options.query - Search query to anchor the plan (alternative to `goal`).
|
|
1368
|
+
* @param options.server - Restrict planning to a specific server.
|
|
1369
|
+
* @param options.k - Maximum number of plan steps.
|
|
1370
|
+
* @param options.policy - Policy constraints for tool selection.
|
|
1371
|
+
* @param options.have - Pre-existing data/context available to the planner.
|
|
1372
|
+
* @param options.returnCallHandles - Include call handles in the response.
|
|
1373
|
+
* @param options.exposeVirtualSkills - Include virtual skills in the plan.
|
|
1374
|
+
* @param options.modelOverrides - Override LLM model settings.
|
|
1375
|
+
*/
|
|
1376
|
+
async plan(options) {
|
|
1377
|
+
this.ensureInitialized();
|
|
1378
|
+
this.warnIfNotDg("plan");
|
|
1379
|
+
if (!options.goal && !options.query) {
|
|
1380
|
+
throw new InvalidConfigError("plan() requires either goal or query");
|
|
1381
|
+
}
|
|
1382
|
+
const params = {};
|
|
1383
|
+
if (options.goal) params.goal = options.goal;
|
|
1384
|
+
if (options.query) params.query = options.query;
|
|
1385
|
+
if (options.server) params.server = options.server;
|
|
1386
|
+
if (options.k !== void 0) params.k = options.k;
|
|
1387
|
+
if (options.policy) params.policy = options.policy;
|
|
1388
|
+
if (options.have) params.have = options.have;
|
|
1389
|
+
if (options.returnCallHandles !== void 0) params.return_call_handles = options.returnCallHandles;
|
|
1390
|
+
if (options.exposeVirtualSkills !== void 0) params.expose_virtual_skills = options.exposeVirtualSkills;
|
|
1391
|
+
if (options.modelOverrides) params.model_overrides = options.modelOverrides;
|
|
1392
|
+
return this.sendWithRetry(
|
|
1393
|
+
() => this.transport.callTool("data-grout/discovery.plan", params)
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Analyse and summarise a payload using the DataGrout Prism refraction engine.
|
|
1398
|
+
*
|
|
1399
|
+
* JSON-RPC method: `tools/call` → `data-grout/prism.refract`
|
|
1400
|
+
*
|
|
1401
|
+
* @param options.goal - Description of what to extract or summarise.
|
|
1402
|
+
* @param options.payload - Input data to analyse (any JSON-serializable value).
|
|
1403
|
+
* @param options.verbose - Include detailed processing trace in the response.
|
|
1404
|
+
* @param options.chart - Also generate a chart representation.
|
|
1405
|
+
*/
|
|
1406
|
+
async refract(options) {
|
|
1407
|
+
this.ensureInitialized();
|
|
1408
|
+
this.warnIfNotDg("refract");
|
|
1409
|
+
const params = {
|
|
1410
|
+
goal: options.goal,
|
|
1411
|
+
payload: options.payload
|
|
1412
|
+
};
|
|
1413
|
+
if (options.verbose !== void 0) params.verbose = options.verbose;
|
|
1414
|
+
if (options.chart !== void 0) params.chart = options.chart;
|
|
1415
|
+
return this.sendWithRetry(
|
|
1416
|
+
() => this.transport.callTool("data-grout/prism.refract", params)
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Generate a chart from a data payload using the DataGrout Prism engine.
|
|
1421
|
+
*
|
|
1422
|
+
* JSON-RPC method: `tools/call` → `data-grout/prism.chart`
|
|
1423
|
+
*
|
|
1424
|
+
* @param options.goal - What the chart should visualise.
|
|
1425
|
+
* @param options.payload - Input data (any JSON-serializable value).
|
|
1426
|
+
* @param options.format - Output format (e.g. `"png"`, `"svg"`).
|
|
1427
|
+
* @param options.chartType - Chart type (e.g. `"bar"`, `"line"`, `"pie"`).
|
|
1428
|
+
* @param options.title - Chart title.
|
|
1429
|
+
* @param options.xLabel - X-axis label.
|
|
1430
|
+
* @param options.yLabel - Y-axis label.
|
|
1431
|
+
* @param options.width - Chart width in pixels.
|
|
1432
|
+
* @param options.height - Chart height in pixels.
|
|
1433
|
+
*/
|
|
1434
|
+
async chart(options) {
|
|
1435
|
+
this.ensureInitialized();
|
|
1436
|
+
this.warnIfNotDg("chart");
|
|
1437
|
+
const params = {
|
|
1438
|
+
goal: options.goal,
|
|
1439
|
+
payload: options.payload
|
|
1440
|
+
};
|
|
1441
|
+
if (options.format) params.format = options.format;
|
|
1442
|
+
if (options.chartType) params.chart_type = options.chartType;
|
|
1443
|
+
if (options.title) params.title = options.title;
|
|
1444
|
+
if (options.xLabel) params.x_label = options.xLabel;
|
|
1445
|
+
if (options.yLabel) params.y_label = options.yLabel;
|
|
1446
|
+
if (options.width !== void 0) params.width = options.width;
|
|
1447
|
+
if (options.height !== void 0) params.height = options.height;
|
|
1448
|
+
return this.sendWithRetry(
|
|
1449
|
+
() => this.transport.callTool("data-grout/prism.chart", params)
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Generate a document toward a natural-language goal.
|
|
1454
|
+
* Supported formats: markdown, html, pdf, json.
|
|
1455
|
+
*/
|
|
1456
|
+
async render(options) {
|
|
1457
|
+
this.ensureInitialized();
|
|
1458
|
+
this.warnIfNotDg("render");
|
|
1459
|
+
const { goal, format = "markdown", payload, sections, ...rest } = options;
|
|
1460
|
+
const params = { goal, format, ...rest };
|
|
1461
|
+
if (payload !== void 0) params.payload = payload;
|
|
1462
|
+
if (sections !== void 0) params.sections = sections;
|
|
1463
|
+
return this.sendWithRetry(() => this.transport.callTool("data-grout/prism.render", params));
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Convert content to another format (no LLM). Supports csv, xlsx, pdf, json, html, markdown, etc.
|
|
1467
|
+
*/
|
|
1468
|
+
async export(options) {
|
|
1469
|
+
this.ensureInitialized();
|
|
1470
|
+
this.warnIfNotDg("export");
|
|
1471
|
+
const { content, format, ...rest } = options;
|
|
1472
|
+
const params = { content, format, ...rest };
|
|
1473
|
+
return this.sendWithRetry(() => this.transport.callTool("data-grout/prism.export", params));
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Pause workflow for human approval. Use for destructive or policy-gated actions.
|
|
1477
|
+
*/
|
|
1478
|
+
async requestApproval(options) {
|
|
1479
|
+
this.ensureInitialized();
|
|
1480
|
+
this.warnIfNotDg("requestApproval");
|
|
1481
|
+
const { action, details, reason, context, ...rest } = options;
|
|
1482
|
+
const params = { action, ...rest };
|
|
1483
|
+
if (details !== void 0) params.details = details;
|
|
1484
|
+
if (reason !== void 0) params.reason = reason;
|
|
1485
|
+
if (context !== void 0) params.context = context;
|
|
1486
|
+
return this.sendWithRetry(() => this.transport.callTool("data-grout/flow.request-approval", params));
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Request user clarification for missing fields. Pauses until user provides values.
|
|
1490
|
+
*/
|
|
1491
|
+
async requestFeedback(options) {
|
|
1492
|
+
this.ensureInitialized();
|
|
1493
|
+
this.warnIfNotDg("requestFeedback");
|
|
1494
|
+
const { missing_fields, reason, ...rest } = options;
|
|
1495
|
+
const params = { missing_fields, reason, ...rest };
|
|
1496
|
+
return this.sendWithRetry(() => this.transport.callTool("data-grout/flow.request-feedback", params));
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* List recent tool executions for the current server.
|
|
1500
|
+
*/
|
|
1501
|
+
async executionHistory(options = {}) {
|
|
1502
|
+
this.ensureInitialized();
|
|
1503
|
+
this.warnIfNotDg("executionHistory");
|
|
1504
|
+
const params = { limit: options.limit ?? 50, offset: options.offset ?? 0, refractions_only: options.refractions_only ?? false, ...options };
|
|
1505
|
+
if (options.status !== void 0) params.status = options.status;
|
|
1506
|
+
return this.sendWithRetry(() => this.transport.callTool("data-grout/inspect.execution-history", params));
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Get details and transcript for a specific execution.
|
|
1510
|
+
*/
|
|
1511
|
+
async executionDetails(executionId) {
|
|
1512
|
+
this.ensureInitialized();
|
|
1513
|
+
this.warnIfNotDg("executionDetails");
|
|
1514
|
+
return this.sendWithRetry(() => this.transport.callTool("data-grout/inspect.execution-details", { execution_id: executionId }));
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Call any DataGrout first-party tool by its short name.
|
|
1518
|
+
*
|
|
1519
|
+
* Prepends `data-grout/` to the tool name automatically, so
|
|
1520
|
+
* `client.dg('prism.render', { ... })` calls `data-grout/prism.render`.
|
|
1521
|
+
*
|
|
1522
|
+
* JSON-RPC method: `tools/call` → `data-grout/<toolShortName>`
|
|
1523
|
+
*
|
|
1524
|
+
* @param toolShortName - Tool name without the `data-grout/` prefix.
|
|
1525
|
+
* @param params - Tool input arguments.
|
|
1526
|
+
*/
|
|
1527
|
+
async dg(toolShortName, params = {}) {
|
|
1528
|
+
this.ensureInitialized();
|
|
1529
|
+
const method = `data-grout/${toolShortName}`;
|
|
1530
|
+
return this.sendWithRetry(() => this.transport.callTool(method, params));
|
|
1531
|
+
}
|
|
1532
|
+
async remember(statementOrOptions, optionsArg) {
|
|
1533
|
+
this.ensureInitialized();
|
|
1534
|
+
let statement;
|
|
1535
|
+
let opts;
|
|
1536
|
+
if (typeof statementOrOptions === "string") {
|
|
1537
|
+
statement = statementOrOptions;
|
|
1538
|
+
opts = optionsArg;
|
|
1539
|
+
} else {
|
|
1540
|
+
opts = statementOrOptions;
|
|
1541
|
+
statement = opts.statement;
|
|
1542
|
+
}
|
|
1543
|
+
if (!statement && !opts?.facts?.length) {
|
|
1544
|
+
throw new InvalidConfigError("remember() requires either a statement or facts");
|
|
1545
|
+
}
|
|
1546
|
+
const params = {
|
|
1547
|
+
tag: opts?.tag ?? "default"
|
|
1548
|
+
};
|
|
1549
|
+
if (opts?.facts) {
|
|
1550
|
+
params.facts = opts.facts;
|
|
1551
|
+
} else {
|
|
1552
|
+
params.statement = statement;
|
|
1553
|
+
}
|
|
1554
|
+
return this.sendWithRetry(
|
|
1555
|
+
() => this.transport.callTool("data-grout/logic.remember", params)
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
async queryCell(questionOrOptions, optionsArg) {
|
|
1559
|
+
this.ensureInitialized();
|
|
1560
|
+
let question;
|
|
1561
|
+
let opts;
|
|
1562
|
+
if (typeof questionOrOptions === "string") {
|
|
1563
|
+
question = questionOrOptions;
|
|
1564
|
+
opts = optionsArg;
|
|
1565
|
+
} else {
|
|
1566
|
+
opts = questionOrOptions;
|
|
1567
|
+
question = opts.question;
|
|
1568
|
+
}
|
|
1569
|
+
if (!question && !opts?.patterns?.length) {
|
|
1570
|
+
throw new InvalidConfigError("queryCell() requires either a question or patterns");
|
|
1571
|
+
}
|
|
1572
|
+
const params = {
|
|
1573
|
+
limit: opts?.limit ?? 50
|
|
1574
|
+
};
|
|
1575
|
+
if (opts?.patterns) {
|
|
1576
|
+
params.patterns = opts.patterns;
|
|
1577
|
+
} else {
|
|
1578
|
+
params.question = question;
|
|
1579
|
+
}
|
|
1580
|
+
return this.sendWithRetry(
|
|
1581
|
+
() => this.transport.callTool("data-grout/logic.query", params)
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Retract facts from the agent's logic cell.
|
|
1586
|
+
*
|
|
1587
|
+
* Throws `InvalidConfigError` if neither `handles` nor `pattern` is provided.
|
|
1588
|
+
*
|
|
1589
|
+
* JSON-RPC method: `tools/call` → `data-grout/logic.forget`
|
|
1590
|
+
*
|
|
1591
|
+
* @param options.handles - Specific fact handles to retract.
|
|
1592
|
+
* @param options.pattern - Natural language pattern — retract all matching facts.
|
|
1593
|
+
*/
|
|
1594
|
+
async forget(options) {
|
|
1595
|
+
this.ensureInitialized();
|
|
1596
|
+
if (!options.handles?.length && !options.pattern) {
|
|
1597
|
+
throw new InvalidConfigError("forget() requires either handles or pattern");
|
|
1598
|
+
}
|
|
1599
|
+
const params = {};
|
|
1600
|
+
if (options.handles) params.handles = options.handles;
|
|
1601
|
+
if (options.pattern) params.pattern = options.pattern;
|
|
1602
|
+
return this.sendWithRetry(
|
|
1603
|
+
() => this.transport.callTool("data-grout/logic.forget", params)
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Reflect on the agent's logic cell — returns a full snapshot or a
|
|
1608
|
+
* per-entity view of all stored facts.
|
|
1609
|
+
*
|
|
1610
|
+
* JSON-RPC method: `tools/call` → `data-grout/logic.reflect`
|
|
1611
|
+
*
|
|
1612
|
+
* @param options.entity - Optional entity name to scope reflection.
|
|
1613
|
+
* @param options.summaryOnly - When `true`, return only counts (default: `false`).
|
|
1614
|
+
*/
|
|
1615
|
+
async reflect(options) {
|
|
1616
|
+
this.ensureInitialized();
|
|
1617
|
+
const params = {
|
|
1618
|
+
summary_only: options?.summaryOnly ?? false
|
|
1619
|
+
};
|
|
1620
|
+
if (options?.entity) params.entity = options.entity;
|
|
1621
|
+
return this.sendWithRetry(
|
|
1622
|
+
() => this.transport.callTool("data-grout/logic.reflect", params)
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Store a logical rule or policy in the agent's logic cell.
|
|
1627
|
+
*
|
|
1628
|
+
* Rules are permanent constraints evaluated during `queryCell()` calls.
|
|
1629
|
+
*
|
|
1630
|
+
* JSON-RPC method: `tools/call` → `data-grout/logic.constrain`
|
|
1631
|
+
*
|
|
1632
|
+
* @param rule - Natural language rule (e.g. `"VIP customers have ARR > $500K"`).
|
|
1633
|
+
* @param options.tag - Tag/namespace for this constraint (default: `"constraint"`).
|
|
1634
|
+
*/
|
|
1635
|
+
async constrain(rule, options) {
|
|
1636
|
+
this.ensureInitialized();
|
|
1637
|
+
const params = {
|
|
1638
|
+
rule,
|
|
1639
|
+
tag: options?.tag ?? "constraint"
|
|
1640
|
+
};
|
|
1641
|
+
return this.sendWithRetry(
|
|
1642
|
+
() => this.transport.callTool("data-grout/logic.constrain", params)
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Estimate the credit cost of a tool call without executing it.
|
|
1647
|
+
*
|
|
1648
|
+
* Passes `estimate_only: true` to the tool, which returns a cost breakdown
|
|
1649
|
+
* without performing any side effects or charging credits.
|
|
1650
|
+
*
|
|
1651
|
+
* @param tool - Fully-qualified tool name.
|
|
1652
|
+
* @param args - Tool input arguments.
|
|
1653
|
+
*/
|
|
1654
|
+
async estimateCost(tool, args) {
|
|
1655
|
+
this.ensureInitialized();
|
|
1656
|
+
const estimateArgs = { ...args, estimate_only: true };
|
|
1657
|
+
return this.sendWithRetry(
|
|
1658
|
+
() => this.transport.callTool(tool, estimateArgs)
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
// ===== Internal Helpers =====
|
|
1662
|
+
async performWithTracking(tool, args, options) {
|
|
1663
|
+
const params = { tool, args, ...options };
|
|
1664
|
+
return this.sendWithRetry(
|
|
1665
|
+
() => this.transport.callTool("data-grout/discovery.perform", params)
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
// src/index.ts
|
|
1671
|
+
init_identity();
|
|
1672
|
+
init_oauth();
|
|
1673
|
+
|
|
1674
|
+
// src/types.ts
|
|
1675
|
+
function extractMeta(result) {
|
|
1676
|
+
const raw = result?._meta?.datagrout ?? result?._datagrout ?? result?._meta;
|
|
1677
|
+
if (!raw?.receipt) return null;
|
|
1678
|
+
const r = raw.receipt;
|
|
1679
|
+
const receipt = {
|
|
1680
|
+
receiptId: r.receipt_id ?? "",
|
|
1681
|
+
transactionId: r.transaction_id,
|
|
1682
|
+
timestamp: r.timestamp ?? "",
|
|
1683
|
+
estimatedCredits: r.estimated_credits ?? 0,
|
|
1684
|
+
actualCredits: r.actual_credits ?? 0,
|
|
1685
|
+
netCredits: r.net_credits ?? 0,
|
|
1686
|
+
savings: r.savings ?? 0,
|
|
1687
|
+
savingsBonus: r.savings_bonus ?? 0,
|
|
1688
|
+
balanceBefore: r.balance_before,
|
|
1689
|
+
balanceAfter: r.balance_after,
|
|
1690
|
+
breakdown: r.breakdown ?? {},
|
|
1691
|
+
byok: {
|
|
1692
|
+
enabled: r.byok?.enabled ?? false,
|
|
1693
|
+
discountApplied: r.byok?.discount_applied ?? 0,
|
|
1694
|
+
discountRate: r.byok?.discount_rate ?? 0
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
const e = raw.credit_estimate;
|
|
1698
|
+
const creditEstimate = e ? {
|
|
1699
|
+
estimatedTotal: e.estimated_total ?? 0,
|
|
1700
|
+
actualTotal: e.actual_total ?? 0,
|
|
1701
|
+
netTotal: e.net_total ?? 0,
|
|
1702
|
+
breakdown: e.breakdown ?? {}
|
|
1703
|
+
} : void 0;
|
|
1704
|
+
return { receipt, creditEstimate };
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/index.ts
|
|
1708
|
+
var version = "0.1.0";
|
|
1709
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1710
|
+
0 && (module.exports = {
|
|
1711
|
+
AuthError,
|
|
1712
|
+
Client,
|
|
1713
|
+
ConduitError,
|
|
1714
|
+
ConduitIdentity,
|
|
1715
|
+
DEFAULT_IDENTITY_DIR,
|
|
1716
|
+
DG_CA_URL,
|
|
1717
|
+
DG_SUBSTRATE_ENDPOINT,
|
|
1718
|
+
GuidedSession,
|
|
1719
|
+
InvalidConfigError,
|
|
1720
|
+
NetworkError,
|
|
1721
|
+
NotInitializedError,
|
|
1722
|
+
OAuthTokenProvider,
|
|
1723
|
+
RateLimitError,
|
|
1724
|
+
ServerError,
|
|
1725
|
+
deriveTokenEndpoint,
|
|
1726
|
+
extractMeta,
|
|
1727
|
+
fetchDgCaCert,
|
|
1728
|
+
fetchWithIdentity,
|
|
1729
|
+
generateKeypair,
|
|
1730
|
+
isDgUrl,
|
|
1731
|
+
refreshCaCert,
|
|
1732
|
+
registerIdentity,
|
|
1733
|
+
rotateIdentity,
|
|
1734
|
+
saveIdentity,
|
|
1735
|
+
version
|
|
1736
|
+
});
|