@granular-software/sdk 0.3.3 → 0.4.1
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 +20 -0
- package/dist/adapters/anthropic.d.mts +1 -1
- package/dist/adapters/anthropic.d.ts +1 -1
- package/dist/adapters/langchain.d.mts +1 -1
- package/dist/adapters/langchain.d.ts +1 -1
- package/dist/adapters/mastra.d.mts +1 -1
- package/dist/adapters/mastra.d.ts +1 -1
- package/dist/adapters/openai.d.mts +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/cli/index.js +306 -18
- package/dist/index.d.mts +24 -2
- package/dist/index.d.ts +24 -2
- package/dist/index.js +325 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +325 -21
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CnX4jXYQ.d.mts → types-BOPsFZYi.d.mts} +34 -1
- package/dist/{types-CnX4jXYQ.d.ts → types-BOPsFZYi.d.ts} +34 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3939,6 +3939,9 @@ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
|
3939
3939
|
GlobalWebSocket = globalThis.WebSocket;
|
|
3940
3940
|
}
|
|
3941
3941
|
var READY_STATE_OPEN = 1;
|
|
3942
|
+
var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
3943
|
+
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
3944
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3942
3945
|
var WSClient = class {
|
|
3943
3946
|
ws = null;
|
|
3944
3947
|
url;
|
|
@@ -3952,6 +3955,7 @@ var WSClient = class {
|
|
|
3952
3955
|
doc = Automerge__namespace.init();
|
|
3953
3956
|
syncState = Automerge__namespace.initSyncState();
|
|
3954
3957
|
reconnectTimer = null;
|
|
3958
|
+
tokenRefreshTimer = null;
|
|
3955
3959
|
isExplicitlyDisconnected = false;
|
|
3956
3960
|
options;
|
|
3957
3961
|
constructor(options) {
|
|
@@ -3960,12 +3964,113 @@ var WSClient = class {
|
|
|
3960
3964
|
this.sessionId = options.sessionId;
|
|
3961
3965
|
this.token = options.token;
|
|
3962
3966
|
}
|
|
3967
|
+
clearTokenRefreshTimer() {
|
|
3968
|
+
if (this.tokenRefreshTimer) {
|
|
3969
|
+
clearTimeout(this.tokenRefreshTimer);
|
|
3970
|
+
this.tokenRefreshTimer = null;
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
decodeBase64Url(payload) {
|
|
3974
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3975
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
3976
|
+
if (typeof atob === "function") {
|
|
3977
|
+
return atob(padded);
|
|
3978
|
+
}
|
|
3979
|
+
const maybeBuffer = globalThis.Buffer;
|
|
3980
|
+
if (maybeBuffer) {
|
|
3981
|
+
return maybeBuffer.from(padded, "base64").toString("utf8");
|
|
3982
|
+
}
|
|
3983
|
+
throw new Error("No base64 decoder available");
|
|
3984
|
+
}
|
|
3985
|
+
getTokenExpiryMs(token) {
|
|
3986
|
+
const parts = token.split(".");
|
|
3987
|
+
if (parts.length < 2) {
|
|
3988
|
+
return null;
|
|
3989
|
+
}
|
|
3990
|
+
try {
|
|
3991
|
+
const payloadRaw = this.decodeBase64Url(parts[1]);
|
|
3992
|
+
const payload = JSON.parse(payloadRaw);
|
|
3993
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
return payload.exp * 1e3;
|
|
3997
|
+
} catch {
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
scheduleTokenRefresh() {
|
|
4002
|
+
this.clearTokenRefreshTimer();
|
|
4003
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4007
|
+
if (!expiresAt) {
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const refreshInMs = Math.max(1e3, expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS);
|
|
4011
|
+
const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
|
|
4012
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4013
|
+
void this.refreshTokenInBackground();
|
|
4014
|
+
}, delay);
|
|
4015
|
+
}
|
|
4016
|
+
async refreshTokenInBackground() {
|
|
4017
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
try {
|
|
4021
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4022
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4023
|
+
throw new Error("Token provider returned no token");
|
|
4024
|
+
}
|
|
4025
|
+
this.token = refreshedToken;
|
|
4026
|
+
this.scheduleTokenRefresh();
|
|
4027
|
+
} catch (error) {
|
|
4028
|
+
if (this.isExplicitlyDisconnected) {
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
console.warn("[Granular] Token refresh failed, retrying soon:", error);
|
|
4032
|
+
this.clearTokenRefreshTimer();
|
|
4033
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4034
|
+
void this.refreshTokenInBackground();
|
|
4035
|
+
}, TOKEN_REFRESH_RETRY_MS);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
async resolveTokenForConnect() {
|
|
4039
|
+
if (!this.options.tokenProvider) {
|
|
4040
|
+
return this.token;
|
|
4041
|
+
}
|
|
4042
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4043
|
+
const shouldRefresh = expiresAt !== null && expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
|
|
4044
|
+
if (!shouldRefresh) {
|
|
4045
|
+
return this.token;
|
|
4046
|
+
}
|
|
4047
|
+
try {
|
|
4048
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4049
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4050
|
+
throw new Error("Token provider returned no token");
|
|
4051
|
+
}
|
|
4052
|
+
this.token = refreshedToken;
|
|
4053
|
+
return refreshedToken;
|
|
4054
|
+
} catch (error) {
|
|
4055
|
+
if (expiresAt > Date.now()) {
|
|
4056
|
+
console.warn("[Granular] Token refresh failed, using current token:", error);
|
|
4057
|
+
return this.token;
|
|
4058
|
+
}
|
|
4059
|
+
throw error;
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
3963
4062
|
/**
|
|
3964
4063
|
* Connect to the WebSocket server
|
|
3965
4064
|
* @returns {Promise<void>} Resolves when connection is open
|
|
3966
4065
|
*/
|
|
3967
4066
|
async connect() {
|
|
4067
|
+
const token = await this.resolveTokenForConnect();
|
|
3968
4068
|
this.isExplicitlyDisconnected = false;
|
|
4069
|
+
this.scheduleTokenRefresh();
|
|
4070
|
+
if (this.reconnectTimer) {
|
|
4071
|
+
clearTimeout(this.reconnectTimer);
|
|
4072
|
+
this.reconnectTimer = null;
|
|
4073
|
+
}
|
|
3969
4074
|
let WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
|
|
3970
4075
|
if (!WebSocketClass) {
|
|
3971
4076
|
try {
|
|
@@ -3981,12 +4086,16 @@ var WSClient = class {
|
|
|
3981
4086
|
try {
|
|
3982
4087
|
const wsUrl = new URL(this.url);
|
|
3983
4088
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
3984
|
-
wsUrl.searchParams.set("token",
|
|
4089
|
+
wsUrl.searchParams.set("token", token);
|
|
3985
4090
|
this.ws = new WebSocketClass(wsUrl.toString());
|
|
3986
4091
|
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
3987
4092
|
const socket = this.ws;
|
|
3988
4093
|
if (typeof socket.on === "function") {
|
|
3989
4094
|
socket.on("open", () => {
|
|
4095
|
+
if (this.reconnectTimer) {
|
|
4096
|
+
clearTimeout(this.reconnectTimer);
|
|
4097
|
+
this.reconnectTimer = null;
|
|
4098
|
+
}
|
|
3990
4099
|
this.emit("open", {});
|
|
3991
4100
|
resolve();
|
|
3992
4101
|
});
|
|
@@ -4004,12 +4113,20 @@ var WSClient = class {
|
|
|
4004
4113
|
reject(error);
|
|
4005
4114
|
}
|
|
4006
4115
|
});
|
|
4007
|
-
socket.on("close", () => {
|
|
4008
|
-
this.
|
|
4009
|
-
|
|
4116
|
+
socket.on("close", (code, reason) => {
|
|
4117
|
+
this.handleDisconnect({
|
|
4118
|
+
code,
|
|
4119
|
+
reason: this.normalizeReason(reason),
|
|
4120
|
+
// ws does not provide wasClean on Node-style close callback
|
|
4121
|
+
wasClean: code === 1e3
|
|
4122
|
+
});
|
|
4010
4123
|
});
|
|
4011
4124
|
} else {
|
|
4012
4125
|
this.ws.onopen = () => {
|
|
4126
|
+
if (this.reconnectTimer) {
|
|
4127
|
+
clearTimeout(this.reconnectTimer);
|
|
4128
|
+
this.reconnectTimer = null;
|
|
4129
|
+
}
|
|
4013
4130
|
this.emit("open", {});
|
|
4014
4131
|
resolve();
|
|
4015
4132
|
};
|
|
@@ -4030,9 +4147,12 @@ var WSClient = class {
|
|
|
4030
4147
|
reject(error);
|
|
4031
4148
|
}
|
|
4032
4149
|
};
|
|
4033
|
-
this.ws.onclose = () => {
|
|
4034
|
-
this.
|
|
4035
|
-
|
|
4150
|
+
this.ws.onclose = (event) => {
|
|
4151
|
+
this.handleDisconnect({
|
|
4152
|
+
code: event.code,
|
|
4153
|
+
reason: event.reason,
|
|
4154
|
+
wasClean: event.wasClean
|
|
4155
|
+
});
|
|
4036
4156
|
};
|
|
4037
4157
|
}
|
|
4038
4158
|
} catch (error) {
|
|
@@ -4040,13 +4160,80 @@ var WSClient = class {
|
|
|
4040
4160
|
}
|
|
4041
4161
|
});
|
|
4042
4162
|
}
|
|
4043
|
-
|
|
4163
|
+
normalizeReason(reason) {
|
|
4164
|
+
if (reason === null || reason === void 0) return void 0;
|
|
4165
|
+
if (typeof reason === "string") return reason;
|
|
4166
|
+
if (typeof reason === "object" && reason && "toString" in reason) {
|
|
4167
|
+
try {
|
|
4168
|
+
const text = String(reason.toString());
|
|
4169
|
+
return text || void 0;
|
|
4170
|
+
} catch {
|
|
4171
|
+
return void 0;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
return void 0;
|
|
4175
|
+
}
|
|
4176
|
+
rejectPending(error) {
|
|
4177
|
+
this.messageQueue.forEach((pending) => pending.reject(error));
|
|
4178
|
+
this.messageQueue = [];
|
|
4179
|
+
}
|
|
4180
|
+
buildDisconnectError(info) {
|
|
4181
|
+
const details = [
|
|
4182
|
+
info.code !== void 0 ? `code=${info.code}` : void 0,
|
|
4183
|
+
info.reason ? `reason=${info.reason}` : void 0
|
|
4184
|
+
].filter(Boolean).join(", ");
|
|
4185
|
+
const suffix = details ? ` (${details})` : "";
|
|
4186
|
+
return new Error(`WebSocket disconnected${suffix}`);
|
|
4187
|
+
}
|
|
4188
|
+
handleDisconnect(close = {}) {
|
|
4189
|
+
const reconnectDelayMs = 3e3;
|
|
4190
|
+
const unexpected = !this.isExplicitlyDisconnected;
|
|
4191
|
+
const info = {
|
|
4192
|
+
code: close.code,
|
|
4193
|
+
reason: close.reason,
|
|
4194
|
+
wasClean: close.wasClean,
|
|
4195
|
+
unexpected,
|
|
4196
|
+
timestamp: Date.now(),
|
|
4197
|
+
reconnectScheduled: false
|
|
4198
|
+
};
|
|
4044
4199
|
this.ws = null;
|
|
4045
|
-
|
|
4200
|
+
this.emit("close", info);
|
|
4201
|
+
if (this.reconnectTimer) {
|
|
4202
|
+
clearTimeout(this.reconnectTimer);
|
|
4203
|
+
this.reconnectTimer = null;
|
|
4204
|
+
}
|
|
4205
|
+
if (unexpected) {
|
|
4206
|
+
const disconnectError = this.buildDisconnectError(info);
|
|
4207
|
+
this.rejectPending(disconnectError);
|
|
4208
|
+
this.emit("disconnect", info);
|
|
4209
|
+
info.reconnectScheduled = true;
|
|
4210
|
+
info.reconnectDelayMs = reconnectDelayMs;
|
|
4211
|
+
if (this.options.onUnexpectedClose) {
|
|
4212
|
+
try {
|
|
4213
|
+
this.options.onUnexpectedClose(info);
|
|
4214
|
+
} catch (callbackError) {
|
|
4215
|
+
console.error("[Granular] onUnexpectedClose callback failed:", callbackError);
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4046
4218
|
this.reconnectTimer = setTimeout(() => {
|
|
4047
4219
|
console.log("[Granular] Attempting reconnect...");
|
|
4048
|
-
this.connect().catch((
|
|
4049
|
-
|
|
4220
|
+
this.connect().catch((error) => {
|
|
4221
|
+
console.error("[Granular] Reconnect failed:", error);
|
|
4222
|
+
const reconnectInfo = {
|
|
4223
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4224
|
+
sessionId: this.sessionId,
|
|
4225
|
+
timestamp: Date.now()
|
|
4226
|
+
};
|
|
4227
|
+
this.emit("reconnect_error", reconnectInfo);
|
|
4228
|
+
if (this.options.onReconnectError) {
|
|
4229
|
+
try {
|
|
4230
|
+
this.options.onReconnectError(reconnectInfo);
|
|
4231
|
+
} catch (callbackError) {
|
|
4232
|
+
console.error("[Granular] onReconnectError callback failed:", callbackError);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
});
|
|
4236
|
+
}, reconnectDelayMs);
|
|
4050
4237
|
}
|
|
4051
4238
|
}
|
|
4052
4239
|
handleMessage(message) {
|
|
@@ -4104,10 +4291,10 @@ var WSClient = class {
|
|
|
4104
4291
|
const snapshotMessage = message;
|
|
4105
4292
|
try {
|
|
4106
4293
|
const bytes = new Uint8Array(snapshotMessage.data);
|
|
4107
|
-
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
4294
|
+
console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
|
|
4108
4295
|
this.doc = Automerge__namespace.load(bytes);
|
|
4109
4296
|
this.emit("sync", this.doc);
|
|
4110
|
-
console.log("[Granular DEBUG]
|
|
4297
|
+
console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge__namespace.toJS(this.doc)));
|
|
4111
4298
|
} catch (e) {
|
|
4112
4299
|
console.warn("[Granular] Failed to load snapshot message", e);
|
|
4113
4300
|
}
|
|
@@ -4266,13 +4453,16 @@ var WSClient = class {
|
|
|
4266
4453
|
*/
|
|
4267
4454
|
disconnect() {
|
|
4268
4455
|
this.isExplicitlyDisconnected = true;
|
|
4269
|
-
if (this.reconnectTimer)
|
|
4456
|
+
if (this.reconnectTimer) {
|
|
4457
|
+
clearTimeout(this.reconnectTimer);
|
|
4458
|
+
this.reconnectTimer = null;
|
|
4459
|
+
}
|
|
4460
|
+
this.clearTokenRefreshTimer();
|
|
4270
4461
|
if (this.ws) {
|
|
4271
|
-
this.ws.close();
|
|
4462
|
+
this.ws.close(1e3, "Client disconnect");
|
|
4272
4463
|
this.ws = null;
|
|
4273
4464
|
}
|
|
4274
|
-
this.
|
|
4275
|
-
this.messageQueue = [];
|
|
4465
|
+
this.rejectPending(new Error("Client explicitly disconnected"));
|
|
4276
4466
|
this.rpcHandlers.clear();
|
|
4277
4467
|
this.emit("disconnect", {});
|
|
4278
4468
|
}
|
|
@@ -4754,6 +4944,13 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4754
4944
|
* Close the session and disconnect from the sandbox
|
|
4755
4945
|
*/
|
|
4756
4946
|
async disconnect() {
|
|
4947
|
+
try {
|
|
4948
|
+
await this.client.call("client.goodbye", {
|
|
4949
|
+
clientId: this.clientId,
|
|
4950
|
+
timestamp: Date.now()
|
|
4951
|
+
});
|
|
4952
|
+
} catch (error) {
|
|
4953
|
+
}
|
|
4757
4954
|
this.client.disconnect();
|
|
4758
4955
|
}
|
|
4759
4956
|
// --- Event Handling ---
|
|
@@ -4817,7 +5014,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4817
5014
|
this.checkForToolChanges();
|
|
4818
5015
|
});
|
|
4819
5016
|
this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
|
|
4820
|
-
this.client.on("disconnect", () => this.emit("disconnect", {}));
|
|
5017
|
+
this.client.on("disconnect", (payload) => this.emit("disconnect", payload || {}));
|
|
4821
5018
|
this.client.on("job.status", (data) => {
|
|
4822
5019
|
this.emit("job:status", data);
|
|
4823
5020
|
});
|
|
@@ -4964,6 +5161,50 @@ var JobImplementation = class {
|
|
|
4964
5161
|
}
|
|
4965
5162
|
};
|
|
4966
5163
|
|
|
5164
|
+
// src/endpoints.ts
|
|
5165
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5166
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5167
|
+
function readEnv(name) {
|
|
5168
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5169
|
+
return process.env[name];
|
|
5170
|
+
}
|
|
5171
|
+
function normalizeMode(value) {
|
|
5172
|
+
if (!value) return void 0;
|
|
5173
|
+
const normalized = value.trim().toLowerCase();
|
|
5174
|
+
if (normalized === "local") return "local";
|
|
5175
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5176
|
+
if (normalized === "auto") return "auto";
|
|
5177
|
+
return void 0;
|
|
5178
|
+
}
|
|
5179
|
+
function isTruthy(value) {
|
|
5180
|
+
if (!value) return false;
|
|
5181
|
+
const normalized = value.trim().toLowerCase();
|
|
5182
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5183
|
+
}
|
|
5184
|
+
function resolveEndpointMode(explicitMode) {
|
|
5185
|
+
const explicit = normalizeMode(explicitMode);
|
|
5186
|
+
if (explicit === "local" || explicit === "production") {
|
|
5187
|
+
return explicit;
|
|
5188
|
+
}
|
|
5189
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5190
|
+
if (envMode === "local" || envMode === "production") {
|
|
5191
|
+
return envMode;
|
|
5192
|
+
}
|
|
5193
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5194
|
+
return "local";
|
|
5195
|
+
}
|
|
5196
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5197
|
+
return "production";
|
|
5198
|
+
}
|
|
5199
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5200
|
+
}
|
|
5201
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5202
|
+
if (explicitApiUrl) {
|
|
5203
|
+
return explicitApiUrl;
|
|
5204
|
+
}
|
|
5205
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5206
|
+
}
|
|
5207
|
+
|
|
4967
5208
|
// src/client.ts
|
|
4968
5209
|
var STANDARD_MODULES_OPERATIONS = [
|
|
4969
5210
|
{ create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
|
|
@@ -5008,6 +5249,60 @@ var Environment = class _Environment extends Session {
|
|
|
5008
5249
|
get apiEndpoint() {
|
|
5009
5250
|
return this._apiEndpoint;
|
|
5010
5251
|
}
|
|
5252
|
+
getRuntimeBaseUrl() {
|
|
5253
|
+
try {
|
|
5254
|
+
const endpoint = new URL(this._apiEndpoint);
|
|
5255
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
5256
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
5257
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
5258
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
5259
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
5260
|
+
}
|
|
5261
|
+
endpoint.search = "";
|
|
5262
|
+
endpoint.hash = "";
|
|
5263
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
5264
|
+
} catch {
|
|
5265
|
+
return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
5266
|
+
}
|
|
5267
|
+
}
|
|
5268
|
+
/**
|
|
5269
|
+
* Close the session and disconnect from the sandbox.
|
|
5270
|
+
*
|
|
5271
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
5272
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
5273
|
+
* acknowledgement was observed.
|
|
5274
|
+
*/
|
|
5275
|
+
async disconnect() {
|
|
5276
|
+
let wsNotifiedRuntime = false;
|
|
5277
|
+
try {
|
|
5278
|
+
const goodbye = await this.rpc("client.goodbye", {
|
|
5279
|
+
timestamp: Date.now()
|
|
5280
|
+
});
|
|
5281
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
5282
|
+
} catch {
|
|
5283
|
+
wsNotifiedRuntime = false;
|
|
5284
|
+
}
|
|
5285
|
+
if (!wsNotifiedRuntime) {
|
|
5286
|
+
try {
|
|
5287
|
+
const runtimeBase = this.getRuntimeBaseUrl();
|
|
5288
|
+
await fetch(
|
|
5289
|
+
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
5290
|
+
{
|
|
5291
|
+
method: "POST",
|
|
5292
|
+
headers: {
|
|
5293
|
+
"Content-Type": "application/json",
|
|
5294
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
5295
|
+
},
|
|
5296
|
+
body: JSON.stringify({
|
|
5297
|
+
reason: "sdk_disconnect_http_fallback"
|
|
5298
|
+
})
|
|
5299
|
+
}
|
|
5300
|
+
);
|
|
5301
|
+
} catch {
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
this.client.disconnect();
|
|
5305
|
+
}
|
|
5011
5306
|
// ==================== GRAPH CONTAINER READINESS ====================
|
|
5012
5307
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
5013
5308
|
graphContainerStatus = null;
|
|
@@ -5743,7 +6038,10 @@ var Granular = class {
|
|
|
5743
6038
|
apiKey;
|
|
5744
6039
|
apiUrl;
|
|
5745
6040
|
httpUrl;
|
|
6041
|
+
tokenProvider;
|
|
5746
6042
|
WebSocketCtor;
|
|
6043
|
+
onUnexpectedClose;
|
|
6044
|
+
onReconnectError;
|
|
5747
6045
|
/** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
|
|
5748
6046
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
5749
6047
|
/** Active environments tracker: sandboxId → Environment[] */
|
|
@@ -5758,9 +6056,12 @@ var Granular = class {
|
|
|
5758
6056
|
throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
|
|
5759
6057
|
}
|
|
5760
6058
|
this.apiKey = auth;
|
|
5761
|
-
this.apiUrl = options.apiUrl
|
|
6059
|
+
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
6060
|
+
this.tokenProvider = options.tokenProvider;
|
|
5762
6061
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
5763
|
-
this.
|
|
6062
|
+
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
6063
|
+
this.onReconnectError = options.onReconnectError;
|
|
6064
|
+
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
5764
6065
|
}
|
|
5765
6066
|
/**
|
|
5766
6067
|
* Records/upserts a user and prepares them for sandbox connections
|
|
@@ -5844,7 +6145,10 @@ var Granular = class {
|
|
|
5844
6145
|
url: this.apiUrl,
|
|
5845
6146
|
sessionId: envData.environmentId,
|
|
5846
6147
|
token: this.apiKey,
|
|
5847
|
-
|
|
6148
|
+
tokenProvider: this.tokenProvider,
|
|
6149
|
+
WebSocketCtor: this.WebSocketCtor,
|
|
6150
|
+
onUnexpectedClose: this.onUnexpectedClose,
|
|
6151
|
+
onReconnectError: this.onReconnectError
|
|
5848
6152
|
});
|
|
5849
6153
|
await client.connect();
|
|
5850
6154
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|