@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.mjs
CHANGED
|
@@ -3917,6 +3917,9 @@ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
|
3917
3917
|
GlobalWebSocket = globalThis.WebSocket;
|
|
3918
3918
|
}
|
|
3919
3919
|
var READY_STATE_OPEN = 1;
|
|
3920
|
+
var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
|
|
3921
|
+
var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
|
|
3922
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
3920
3923
|
var WSClient = class {
|
|
3921
3924
|
ws = null;
|
|
3922
3925
|
url;
|
|
@@ -3930,6 +3933,7 @@ var WSClient = class {
|
|
|
3930
3933
|
doc = Automerge.init();
|
|
3931
3934
|
syncState = Automerge.initSyncState();
|
|
3932
3935
|
reconnectTimer = null;
|
|
3936
|
+
tokenRefreshTimer = null;
|
|
3933
3937
|
isExplicitlyDisconnected = false;
|
|
3934
3938
|
options;
|
|
3935
3939
|
constructor(options) {
|
|
@@ -3938,12 +3942,113 @@ var WSClient = class {
|
|
|
3938
3942
|
this.sessionId = options.sessionId;
|
|
3939
3943
|
this.token = options.token;
|
|
3940
3944
|
}
|
|
3945
|
+
clearTokenRefreshTimer() {
|
|
3946
|
+
if (this.tokenRefreshTimer) {
|
|
3947
|
+
clearTimeout(this.tokenRefreshTimer);
|
|
3948
|
+
this.tokenRefreshTimer = null;
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
decodeBase64Url(payload) {
|
|
3952
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
3953
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
|
|
3954
|
+
if (typeof atob === "function") {
|
|
3955
|
+
return atob(padded);
|
|
3956
|
+
}
|
|
3957
|
+
const maybeBuffer = globalThis.Buffer;
|
|
3958
|
+
if (maybeBuffer) {
|
|
3959
|
+
return maybeBuffer.from(padded, "base64").toString("utf8");
|
|
3960
|
+
}
|
|
3961
|
+
throw new Error("No base64 decoder available");
|
|
3962
|
+
}
|
|
3963
|
+
getTokenExpiryMs(token) {
|
|
3964
|
+
const parts = token.split(".");
|
|
3965
|
+
if (parts.length < 2) {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
try {
|
|
3969
|
+
const payloadRaw = this.decodeBase64Url(parts[1]);
|
|
3970
|
+
const payload = JSON.parse(payloadRaw);
|
|
3971
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
return payload.exp * 1e3;
|
|
3975
|
+
} catch {
|
|
3976
|
+
return null;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
scheduleTokenRefresh() {
|
|
3980
|
+
this.clearTokenRefreshTimer();
|
|
3981
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
3982
|
+
return;
|
|
3983
|
+
}
|
|
3984
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
3985
|
+
if (!expiresAt) {
|
|
3986
|
+
return;
|
|
3987
|
+
}
|
|
3988
|
+
const refreshInMs = Math.max(1e3, expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS);
|
|
3989
|
+
const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
|
|
3990
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
3991
|
+
void this.refreshTokenInBackground();
|
|
3992
|
+
}, delay);
|
|
3993
|
+
}
|
|
3994
|
+
async refreshTokenInBackground() {
|
|
3995
|
+
if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
|
|
3996
|
+
return;
|
|
3997
|
+
}
|
|
3998
|
+
try {
|
|
3999
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4000
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4001
|
+
throw new Error("Token provider returned no token");
|
|
4002
|
+
}
|
|
4003
|
+
this.token = refreshedToken;
|
|
4004
|
+
this.scheduleTokenRefresh();
|
|
4005
|
+
} catch (error) {
|
|
4006
|
+
if (this.isExplicitlyDisconnected) {
|
|
4007
|
+
return;
|
|
4008
|
+
}
|
|
4009
|
+
console.warn("[Granular] Token refresh failed, retrying soon:", error);
|
|
4010
|
+
this.clearTokenRefreshTimer();
|
|
4011
|
+
this.tokenRefreshTimer = setTimeout(() => {
|
|
4012
|
+
void this.refreshTokenInBackground();
|
|
4013
|
+
}, TOKEN_REFRESH_RETRY_MS);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
async resolveTokenForConnect() {
|
|
4017
|
+
if (!this.options.tokenProvider) {
|
|
4018
|
+
return this.token;
|
|
4019
|
+
}
|
|
4020
|
+
const expiresAt = this.getTokenExpiryMs(this.token);
|
|
4021
|
+
const shouldRefresh = expiresAt !== null && expiresAt - Date.now() <= TOKEN_REFRESH_LEEWAY_MS;
|
|
4022
|
+
if (!shouldRefresh) {
|
|
4023
|
+
return this.token;
|
|
4024
|
+
}
|
|
4025
|
+
try {
|
|
4026
|
+
const refreshedToken = await this.options.tokenProvider();
|
|
4027
|
+
if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
|
|
4028
|
+
throw new Error("Token provider returned no token");
|
|
4029
|
+
}
|
|
4030
|
+
this.token = refreshedToken;
|
|
4031
|
+
return refreshedToken;
|
|
4032
|
+
} catch (error) {
|
|
4033
|
+
if (expiresAt > Date.now()) {
|
|
4034
|
+
console.warn("[Granular] Token refresh failed, using current token:", error);
|
|
4035
|
+
return this.token;
|
|
4036
|
+
}
|
|
4037
|
+
throw error;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
3941
4040
|
/**
|
|
3942
4041
|
* Connect to the WebSocket server
|
|
3943
4042
|
* @returns {Promise<void>} Resolves when connection is open
|
|
3944
4043
|
*/
|
|
3945
4044
|
async connect() {
|
|
4045
|
+
const token = await this.resolveTokenForConnect();
|
|
3946
4046
|
this.isExplicitlyDisconnected = false;
|
|
4047
|
+
this.scheduleTokenRefresh();
|
|
4048
|
+
if (this.reconnectTimer) {
|
|
4049
|
+
clearTimeout(this.reconnectTimer);
|
|
4050
|
+
this.reconnectTimer = null;
|
|
4051
|
+
}
|
|
3947
4052
|
let WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
|
|
3948
4053
|
if (!WebSocketClass) {
|
|
3949
4054
|
try {
|
|
@@ -3959,12 +4064,16 @@ var WSClient = class {
|
|
|
3959
4064
|
try {
|
|
3960
4065
|
const wsUrl = new URL(this.url);
|
|
3961
4066
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
3962
|
-
wsUrl.searchParams.set("token",
|
|
4067
|
+
wsUrl.searchParams.set("token", token);
|
|
3963
4068
|
this.ws = new WebSocketClass(wsUrl.toString());
|
|
3964
4069
|
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
3965
4070
|
const socket = this.ws;
|
|
3966
4071
|
if (typeof socket.on === "function") {
|
|
3967
4072
|
socket.on("open", () => {
|
|
4073
|
+
if (this.reconnectTimer) {
|
|
4074
|
+
clearTimeout(this.reconnectTimer);
|
|
4075
|
+
this.reconnectTimer = null;
|
|
4076
|
+
}
|
|
3968
4077
|
this.emit("open", {});
|
|
3969
4078
|
resolve();
|
|
3970
4079
|
});
|
|
@@ -3982,12 +4091,20 @@ var WSClient = class {
|
|
|
3982
4091
|
reject(error);
|
|
3983
4092
|
}
|
|
3984
4093
|
});
|
|
3985
|
-
socket.on("close", () => {
|
|
3986
|
-
this.
|
|
3987
|
-
|
|
4094
|
+
socket.on("close", (code, reason) => {
|
|
4095
|
+
this.handleDisconnect({
|
|
4096
|
+
code,
|
|
4097
|
+
reason: this.normalizeReason(reason),
|
|
4098
|
+
// ws does not provide wasClean on Node-style close callback
|
|
4099
|
+
wasClean: code === 1e3
|
|
4100
|
+
});
|
|
3988
4101
|
});
|
|
3989
4102
|
} else {
|
|
3990
4103
|
this.ws.onopen = () => {
|
|
4104
|
+
if (this.reconnectTimer) {
|
|
4105
|
+
clearTimeout(this.reconnectTimer);
|
|
4106
|
+
this.reconnectTimer = null;
|
|
4107
|
+
}
|
|
3991
4108
|
this.emit("open", {});
|
|
3992
4109
|
resolve();
|
|
3993
4110
|
};
|
|
@@ -4008,9 +4125,12 @@ var WSClient = class {
|
|
|
4008
4125
|
reject(error);
|
|
4009
4126
|
}
|
|
4010
4127
|
};
|
|
4011
|
-
this.ws.onclose = () => {
|
|
4012
|
-
this.
|
|
4013
|
-
|
|
4128
|
+
this.ws.onclose = (event) => {
|
|
4129
|
+
this.handleDisconnect({
|
|
4130
|
+
code: event.code,
|
|
4131
|
+
reason: event.reason,
|
|
4132
|
+
wasClean: event.wasClean
|
|
4133
|
+
});
|
|
4014
4134
|
};
|
|
4015
4135
|
}
|
|
4016
4136
|
} catch (error) {
|
|
@@ -4018,13 +4138,80 @@ var WSClient = class {
|
|
|
4018
4138
|
}
|
|
4019
4139
|
});
|
|
4020
4140
|
}
|
|
4021
|
-
|
|
4141
|
+
normalizeReason(reason) {
|
|
4142
|
+
if (reason === null || reason === void 0) return void 0;
|
|
4143
|
+
if (typeof reason === "string") return reason;
|
|
4144
|
+
if (typeof reason === "object" && reason && "toString" in reason) {
|
|
4145
|
+
try {
|
|
4146
|
+
const text = String(reason.toString());
|
|
4147
|
+
return text || void 0;
|
|
4148
|
+
} catch {
|
|
4149
|
+
return void 0;
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
return void 0;
|
|
4153
|
+
}
|
|
4154
|
+
rejectPending(error) {
|
|
4155
|
+
this.messageQueue.forEach((pending) => pending.reject(error));
|
|
4156
|
+
this.messageQueue = [];
|
|
4157
|
+
}
|
|
4158
|
+
buildDisconnectError(info) {
|
|
4159
|
+
const details = [
|
|
4160
|
+
info.code !== void 0 ? `code=${info.code}` : void 0,
|
|
4161
|
+
info.reason ? `reason=${info.reason}` : void 0
|
|
4162
|
+
].filter(Boolean).join(", ");
|
|
4163
|
+
const suffix = details ? ` (${details})` : "";
|
|
4164
|
+
return new Error(`WebSocket disconnected${suffix}`);
|
|
4165
|
+
}
|
|
4166
|
+
handleDisconnect(close = {}) {
|
|
4167
|
+
const reconnectDelayMs = 3e3;
|
|
4168
|
+
const unexpected = !this.isExplicitlyDisconnected;
|
|
4169
|
+
const info = {
|
|
4170
|
+
code: close.code,
|
|
4171
|
+
reason: close.reason,
|
|
4172
|
+
wasClean: close.wasClean,
|
|
4173
|
+
unexpected,
|
|
4174
|
+
timestamp: Date.now(),
|
|
4175
|
+
reconnectScheduled: false
|
|
4176
|
+
};
|
|
4022
4177
|
this.ws = null;
|
|
4023
|
-
|
|
4178
|
+
this.emit("close", info);
|
|
4179
|
+
if (this.reconnectTimer) {
|
|
4180
|
+
clearTimeout(this.reconnectTimer);
|
|
4181
|
+
this.reconnectTimer = null;
|
|
4182
|
+
}
|
|
4183
|
+
if (unexpected) {
|
|
4184
|
+
const disconnectError = this.buildDisconnectError(info);
|
|
4185
|
+
this.rejectPending(disconnectError);
|
|
4186
|
+
this.emit("disconnect", info);
|
|
4187
|
+
info.reconnectScheduled = true;
|
|
4188
|
+
info.reconnectDelayMs = reconnectDelayMs;
|
|
4189
|
+
if (this.options.onUnexpectedClose) {
|
|
4190
|
+
try {
|
|
4191
|
+
this.options.onUnexpectedClose(info);
|
|
4192
|
+
} catch (callbackError) {
|
|
4193
|
+
console.error("[Granular] onUnexpectedClose callback failed:", callbackError);
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4024
4196
|
this.reconnectTimer = setTimeout(() => {
|
|
4025
4197
|
console.log("[Granular] Attempting reconnect...");
|
|
4026
|
-
this.connect().catch((
|
|
4027
|
-
|
|
4198
|
+
this.connect().catch((error) => {
|
|
4199
|
+
console.error("[Granular] Reconnect failed:", error);
|
|
4200
|
+
const reconnectInfo = {
|
|
4201
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4202
|
+
sessionId: this.sessionId,
|
|
4203
|
+
timestamp: Date.now()
|
|
4204
|
+
};
|
|
4205
|
+
this.emit("reconnect_error", reconnectInfo);
|
|
4206
|
+
if (this.options.onReconnectError) {
|
|
4207
|
+
try {
|
|
4208
|
+
this.options.onReconnectError(reconnectInfo);
|
|
4209
|
+
} catch (callbackError) {
|
|
4210
|
+
console.error("[Granular] onReconnectError callback failed:", callbackError);
|
|
4211
|
+
}
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
}, reconnectDelayMs);
|
|
4028
4215
|
}
|
|
4029
4216
|
}
|
|
4030
4217
|
handleMessage(message) {
|
|
@@ -4082,10 +4269,10 @@ var WSClient = class {
|
|
|
4082
4269
|
const snapshotMessage = message;
|
|
4083
4270
|
try {
|
|
4084
4271
|
const bytes = new Uint8Array(snapshotMessage.data);
|
|
4085
|
-
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
4272
|
+
console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
|
|
4086
4273
|
this.doc = Automerge.load(bytes);
|
|
4087
4274
|
this.emit("sync", this.doc);
|
|
4088
|
-
console.log("[Granular DEBUG]
|
|
4275
|
+
console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
|
|
4089
4276
|
} catch (e) {
|
|
4090
4277
|
console.warn("[Granular] Failed to load snapshot message", e);
|
|
4091
4278
|
}
|
|
@@ -4244,13 +4431,16 @@ var WSClient = class {
|
|
|
4244
4431
|
*/
|
|
4245
4432
|
disconnect() {
|
|
4246
4433
|
this.isExplicitlyDisconnected = true;
|
|
4247
|
-
if (this.reconnectTimer)
|
|
4434
|
+
if (this.reconnectTimer) {
|
|
4435
|
+
clearTimeout(this.reconnectTimer);
|
|
4436
|
+
this.reconnectTimer = null;
|
|
4437
|
+
}
|
|
4438
|
+
this.clearTokenRefreshTimer();
|
|
4248
4439
|
if (this.ws) {
|
|
4249
|
-
this.ws.close();
|
|
4440
|
+
this.ws.close(1e3, "Client disconnect");
|
|
4250
4441
|
this.ws = null;
|
|
4251
4442
|
}
|
|
4252
|
-
this.
|
|
4253
|
-
this.messageQueue = [];
|
|
4443
|
+
this.rejectPending(new Error("Client explicitly disconnected"));
|
|
4254
4444
|
this.rpcHandlers.clear();
|
|
4255
4445
|
this.emit("disconnect", {});
|
|
4256
4446
|
}
|
|
@@ -4732,6 +4922,13 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4732
4922
|
* Close the session and disconnect from the sandbox
|
|
4733
4923
|
*/
|
|
4734
4924
|
async disconnect() {
|
|
4925
|
+
try {
|
|
4926
|
+
await this.client.call("client.goodbye", {
|
|
4927
|
+
clientId: this.clientId,
|
|
4928
|
+
timestamp: Date.now()
|
|
4929
|
+
});
|
|
4930
|
+
} catch (error) {
|
|
4931
|
+
}
|
|
4735
4932
|
this.client.disconnect();
|
|
4736
4933
|
}
|
|
4737
4934
|
// --- Event Handling ---
|
|
@@ -4795,7 +4992,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4795
4992
|
this.checkForToolChanges();
|
|
4796
4993
|
});
|
|
4797
4994
|
this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
|
|
4798
|
-
this.client.on("disconnect", () => this.emit("disconnect", {}));
|
|
4995
|
+
this.client.on("disconnect", (payload) => this.emit("disconnect", payload || {}));
|
|
4799
4996
|
this.client.on("job.status", (data) => {
|
|
4800
4997
|
this.emit("job:status", data);
|
|
4801
4998
|
});
|
|
@@ -4942,6 +5139,50 @@ var JobImplementation = class {
|
|
|
4942
5139
|
}
|
|
4943
5140
|
};
|
|
4944
5141
|
|
|
5142
|
+
// src/endpoints.ts
|
|
5143
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5144
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5145
|
+
function readEnv(name) {
|
|
5146
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5147
|
+
return process.env[name];
|
|
5148
|
+
}
|
|
5149
|
+
function normalizeMode(value) {
|
|
5150
|
+
if (!value) return void 0;
|
|
5151
|
+
const normalized = value.trim().toLowerCase();
|
|
5152
|
+
if (normalized === "local") return "local";
|
|
5153
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5154
|
+
if (normalized === "auto") return "auto";
|
|
5155
|
+
return void 0;
|
|
5156
|
+
}
|
|
5157
|
+
function isTruthy(value) {
|
|
5158
|
+
if (!value) return false;
|
|
5159
|
+
const normalized = value.trim().toLowerCase();
|
|
5160
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5161
|
+
}
|
|
5162
|
+
function resolveEndpointMode(explicitMode) {
|
|
5163
|
+
const explicit = normalizeMode(explicitMode);
|
|
5164
|
+
if (explicit === "local" || explicit === "production") {
|
|
5165
|
+
return explicit;
|
|
5166
|
+
}
|
|
5167
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5168
|
+
if (envMode === "local" || envMode === "production") {
|
|
5169
|
+
return envMode;
|
|
5170
|
+
}
|
|
5171
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5172
|
+
return "local";
|
|
5173
|
+
}
|
|
5174
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5175
|
+
return "production";
|
|
5176
|
+
}
|
|
5177
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5178
|
+
}
|
|
5179
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5180
|
+
if (explicitApiUrl) {
|
|
5181
|
+
return explicitApiUrl;
|
|
5182
|
+
}
|
|
5183
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5184
|
+
}
|
|
5185
|
+
|
|
4945
5186
|
// src/client.ts
|
|
4946
5187
|
var STANDARD_MODULES_OPERATIONS = [
|
|
4947
5188
|
{ create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
|
|
@@ -4986,6 +5227,60 @@ var Environment = class _Environment extends Session {
|
|
|
4986
5227
|
get apiEndpoint() {
|
|
4987
5228
|
return this._apiEndpoint;
|
|
4988
5229
|
}
|
|
5230
|
+
getRuntimeBaseUrl() {
|
|
5231
|
+
try {
|
|
5232
|
+
const endpoint = new URL(this._apiEndpoint);
|
|
5233
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
5234
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
5235
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
5236
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
5237
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
5238
|
+
}
|
|
5239
|
+
endpoint.search = "";
|
|
5240
|
+
endpoint.hash = "";
|
|
5241
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
5242
|
+
} catch {
|
|
5243
|
+
return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
5244
|
+
}
|
|
5245
|
+
}
|
|
5246
|
+
/**
|
|
5247
|
+
* Close the session and disconnect from the sandbox.
|
|
5248
|
+
*
|
|
5249
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
5250
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
5251
|
+
* acknowledgement was observed.
|
|
5252
|
+
*/
|
|
5253
|
+
async disconnect() {
|
|
5254
|
+
let wsNotifiedRuntime = false;
|
|
5255
|
+
try {
|
|
5256
|
+
const goodbye = await this.rpc("client.goodbye", {
|
|
5257
|
+
timestamp: Date.now()
|
|
5258
|
+
});
|
|
5259
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
5260
|
+
} catch {
|
|
5261
|
+
wsNotifiedRuntime = false;
|
|
5262
|
+
}
|
|
5263
|
+
if (!wsNotifiedRuntime) {
|
|
5264
|
+
try {
|
|
5265
|
+
const runtimeBase = this.getRuntimeBaseUrl();
|
|
5266
|
+
await fetch(
|
|
5267
|
+
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
5268
|
+
{
|
|
5269
|
+
method: "POST",
|
|
5270
|
+
headers: {
|
|
5271
|
+
"Content-Type": "application/json",
|
|
5272
|
+
"Authorization": `Bearer ${this._apiKey}`
|
|
5273
|
+
},
|
|
5274
|
+
body: JSON.stringify({
|
|
5275
|
+
reason: "sdk_disconnect_http_fallback"
|
|
5276
|
+
})
|
|
5277
|
+
}
|
|
5278
|
+
);
|
|
5279
|
+
} catch {
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
this.client.disconnect();
|
|
5283
|
+
}
|
|
4989
5284
|
// ==================== GRAPH CONTAINER READINESS ====================
|
|
4990
5285
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
4991
5286
|
graphContainerStatus = null;
|
|
@@ -5721,7 +6016,10 @@ var Granular = class {
|
|
|
5721
6016
|
apiKey;
|
|
5722
6017
|
apiUrl;
|
|
5723
6018
|
httpUrl;
|
|
6019
|
+
tokenProvider;
|
|
5724
6020
|
WebSocketCtor;
|
|
6021
|
+
onUnexpectedClose;
|
|
6022
|
+
onReconnectError;
|
|
5725
6023
|
/** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
|
|
5726
6024
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
5727
6025
|
/** Active environments tracker: sandboxId → Environment[] */
|
|
@@ -5736,9 +6034,12 @@ var Granular = class {
|
|
|
5736
6034
|
throw new Error("Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options.");
|
|
5737
6035
|
}
|
|
5738
6036
|
this.apiKey = auth;
|
|
5739
|
-
this.apiUrl = options.apiUrl
|
|
6037
|
+
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
6038
|
+
this.tokenProvider = options.tokenProvider;
|
|
5740
6039
|
this.WebSocketCtor = options.WebSocketCtor;
|
|
5741
|
-
this.
|
|
6040
|
+
this.onUnexpectedClose = options.onUnexpectedClose;
|
|
6041
|
+
this.onReconnectError = options.onReconnectError;
|
|
6042
|
+
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
5742
6043
|
}
|
|
5743
6044
|
/**
|
|
5744
6045
|
* Records/upserts a user and prepares them for sandbox connections
|
|
@@ -5822,7 +6123,10 @@ var Granular = class {
|
|
|
5822
6123
|
url: this.apiUrl,
|
|
5823
6124
|
sessionId: envData.environmentId,
|
|
5824
6125
|
token: this.apiKey,
|
|
5825
|
-
|
|
6126
|
+
tokenProvider: this.tokenProvider,
|
|
6127
|
+
WebSocketCtor: this.WebSocketCtor,
|
|
6128
|
+
onUnexpectedClose: this.onUnexpectedClose,
|
|
6129
|
+
onReconnectError: this.onReconnectError
|
|
5826
6130
|
});
|
|
5827
6131
|
await client.connect();
|
|
5828
6132
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|