@granular-software/sdk 0.3.3 → 0.3.4

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.
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.mjs';
1
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.mjs';
2
2
 
3
3
  /**
4
4
  * Anthropic tool_use block from message response
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.js';
1
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.js';
2
2
 
3
3
  /**
4
4
  * Anthropic tool_use block from message response
@@ -1,5 +1,5 @@
1
1
  import { StructuredTool } from '@langchain/core/tools';
2
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.mjs';
2
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.mjs';
3
3
 
4
4
  /**
5
5
  * Helper to convert LangChain tools to Granular tools.
@@ -1,5 +1,5 @@
1
1
  import { StructuredTool } from '@langchain/core/tools';
2
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.js';
2
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.js';
3
3
 
4
4
  /**
5
5
  * Helper to convert LangChain tools to Granular tools.
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.mjs';
2
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.mjs';
3
3
 
4
4
  /**
5
5
  * Mastra tool definition interface
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.js';
2
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.js';
3
3
 
4
4
  /**
5
5
  * Mastra tool definition interface
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.mjs';
1
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.mjs';
2
2
 
3
3
  /**
4
4
  * OpenAI tool call from chat completion response
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-CnX4jXYQ.js';
1
+ import { T as ToolWithHandler } from '../types-D5B8WlF4.js';
2
2
 
3
3
  /**
4
4
  * OpenAI tool call from chat completion response
package/dist/cli/index.js CHANGED
@@ -5741,23 +5741,203 @@ function normalizeAuthBaseUrl(input) {
5741
5741
  }
5742
5742
  return url;
5743
5743
  }
5744
- function renderHtmlPage(title, message) {
5744
+ function escapeHtml(input) {
5745
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5746
+ }
5747
+ function generateSquirclePath(size = 128, radius = 56, exp = 4, steps = 72) {
5748
+ const center = size / 2;
5749
+ const points = [];
5750
+ for (let i = 0; i <= steps; i++) {
5751
+ const angle = i / steps * 2 * Math.PI;
5752
+ const cosA = Math.cos(angle);
5753
+ const sinA = Math.sin(angle);
5754
+ const r = 1 / Math.pow(
5755
+ Math.pow(Math.abs(cosA), exp) + Math.pow(Math.abs(sinA), exp),
5756
+ 1 / exp
5757
+ );
5758
+ const x = center + r * cosA * radius;
5759
+ const y = center + r * sinA * radius;
5760
+ points.push(`${x.toFixed(3)} ${y.toFixed(3)}`);
5761
+ }
5762
+ return `M ${points.join(" L ")} Z`;
5763
+ }
5764
+ function generateHilbertPath(order = 4, size = 128, radius = 51) {
5765
+ const n = 1 << order;
5766
+ const center = size / 2;
5767
+ const squircleExp = 4;
5768
+ const points = [];
5769
+ function squareToSquircle(u, v) {
5770
+ if (Math.abs(u) < 1e-3 && Math.abs(v) < 1e-3) return [u, v];
5771
+ const angle = Math.atan2(v, u);
5772
+ const cosA = Math.cos(angle);
5773
+ const sinA = Math.sin(angle);
5774
+ const squircleR = 1 / Math.pow(
5775
+ Math.pow(Math.abs(cosA), squircleExp) + Math.pow(Math.abs(sinA), squircleExp),
5776
+ 1 / squircleExp
5777
+ );
5778
+ const squareDist = Math.max(Math.abs(u), Math.abs(v));
5779
+ const newDist = squareDist * squircleR;
5780
+ return [newDist * cosA, newDist * sinA];
5781
+ }
5782
+ function d2xy(d) {
5783
+ let x = 0;
5784
+ let y = 0;
5785
+ let t = d;
5786
+ for (let s = 1; s < n; s *= 2) {
5787
+ const rx = 1 & Math.floor(t) / 2;
5788
+ const ry = 1 & (Math.floor(t) ^ rx);
5789
+ if (ry === 0) {
5790
+ if (rx === 1) {
5791
+ x = s - 1 - x;
5792
+ y = s - 1 - y;
5793
+ }
5794
+ [x, y] = [y, x];
5795
+ }
5796
+ x += s * rx;
5797
+ y += s * ry;
5798
+ t = Math.floor(t) / 4;
5799
+ }
5800
+ return [x, y];
5801
+ }
5802
+ for (let i = 0; i < n * n; i++) {
5803
+ const [x, y] = d2xy(i);
5804
+ const normX = x / (n - 1) * 2 - 1;
5805
+ const normY = y / (n - 1) * 2 - 1;
5806
+ const [sqX, sqY] = squareToSquircle(normX, normY);
5807
+ points.push([center + sqX * radius, center + sqY * radius]);
5808
+ }
5809
+ return points.map(([x, y], i) => `${i === 0 ? "M" : "L"} ${x.toFixed(3)} ${y.toFixed(3)}`).join(" ");
5810
+ }
5811
+ function renderLogoSvg() {
5812
+ const squirclePath = generateSquirclePath();
5813
+ const hilbertPath = generateHilbertPath();
5814
+ return `<svg class="logo" viewBox="0 0 128 128" role="img" aria-label="Granular logo" xmlns="http://www.w3.org/2000/svg">
5815
+ <defs>
5816
+ <linearGradient id="granular-logo-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
5817
+ <stop offset="0%" stop-color="#ff8a54" />
5818
+ <stop offset="55%" stop-color="#ff6b35" />
5819
+ <stop offset="100%" stop-color="#e34b1f" />
5820
+ </linearGradient>
5821
+ <filter id="granular-logo-glow" x="-30%" y="-30%" width="160%" height="160%">
5822
+ <feGaussianBlur stdDeviation="1.6" result="blur" />
5823
+ <feMerge>
5824
+ <feMergeNode in="blur" />
5825
+ <feMergeNode in="SourceGraphic" />
5826
+ </feMerge>
5827
+ </filter>
5828
+ </defs>
5829
+ <path d="${squirclePath}" fill="rgba(255,255,255,0.05)" stroke="rgba(255,255,255,0.25)" stroke-width="1.2" />
5830
+ <path d="${hilbertPath}" fill="none" stroke="url(#granular-logo-gradient)" stroke-width="2.8" stroke-linecap="round" stroke-linejoin="round" filter="url(#granular-logo-glow)" />
5831
+ </svg>`;
5832
+ }
5833
+ function renderHtmlPage(title, message, variant) {
5834
+ const safeTitle = escapeHtml(title);
5835
+ const safeMessage = escapeHtml(message);
5836
+ const accent = variant === "success" ? "#ff6b35" : "#ff5c6b";
5837
+ const badge = variant === "success" ? "Success" : "Auth Error";
5745
5838
  return `<!doctype html>
5746
5839
  <html lang="en">
5747
5840
  <head>
5748
5841
  <meta charset="utf-8" />
5749
5842
  <meta name="viewport" content="width=device-width, initial-scale=1" />
5750
- <title>${title}</title>
5843
+ <title>${safeTitle}</title>
5751
5844
  <style>
5752
- :root { color-scheme: light dark; }
5753
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; line-height: 1.5; }
5754
- h1 { margin-bottom: 0.5rem; }
5755
- p { margin-top: 0; color: #666; }
5845
+ :root { color-scheme: dark; }
5846
+ * { box-sizing: border-box; }
5847
+ body {
5848
+ margin: 0;
5849
+ min-height: 100vh;
5850
+ display: grid;
5851
+ place-items: center;
5852
+ padding: 28px;
5853
+ font-family: "Avenir Next", "SF Pro Display", "Segoe UI", sans-serif;
5854
+ color: #f8f8f8;
5855
+ background:
5856
+ radial-gradient(1000px 480px at 12% -10%, rgba(255, 110, 58, 0.26), transparent 60%),
5857
+ radial-gradient(760px 480px at 100% 120%, rgba(99, 102, 241, 0.22), transparent 65%),
5858
+ linear-gradient(140deg, #0f0f12 0%, #131319 45%, #11131a 100%);
5859
+ }
5860
+ .panel {
5861
+ width: min(520px, 100%);
5862
+ border-radius: 24px;
5863
+ border: 1px solid rgba(255, 255, 255, 0.16);
5864
+ background: linear-gradient(160deg, rgba(255,255,255,0.08), rgba(255,255,255,0.03));
5865
+ backdrop-filter: blur(8px);
5866
+ box-shadow:
5867
+ 0 18px 60px rgba(0, 0, 0, 0.45),
5868
+ inset 0 1px 0 rgba(255, 255, 255, 0.18);
5869
+ padding: 26px 26px 24px;
5870
+ }
5871
+ .head {
5872
+ display: flex;
5873
+ align-items: center;
5874
+ gap: 14px;
5875
+ margin-bottom: 14px;
5876
+ }
5877
+ .logo-wrap {
5878
+ width: 62px;
5879
+ height: 62px;
5880
+ border-radius: 18px;
5881
+ border: 1px solid rgba(255,255,255,0.2);
5882
+ background: radial-gradient(circle at 25% 20%, rgba(255,255,255,0.17), rgba(255,255,255,0.02));
5883
+ display: grid;
5884
+ place-items: center;
5885
+ flex: 0 0 auto;
5886
+ }
5887
+ .logo {
5888
+ width: 46px;
5889
+ height: 46px;
5890
+ display: block;
5891
+ }
5892
+ .badge {
5893
+ display: inline-flex;
5894
+ align-items: center;
5895
+ border-radius: 999px;
5896
+ padding: 5px 11px;
5897
+ border: 1px solid ${accent}66;
5898
+ background: ${accent}26;
5899
+ color: ${accent};
5900
+ font-size: 12px;
5901
+ font-weight: 600;
5902
+ letter-spacing: 0.04em;
5903
+ text-transform: uppercase;
5904
+ margin-bottom: 8px;
5905
+ }
5906
+ h1 {
5907
+ margin: 0;
5908
+ font-size: 25px;
5909
+ line-height: 1.2;
5910
+ letter-spacing: 0.01em;
5911
+ color: #ffffff;
5912
+ }
5913
+ p {
5914
+ margin: 0;
5915
+ color: #d7d8df;
5916
+ font-size: 15px;
5917
+ line-height: 1.6;
5918
+ }
5919
+ .footer {
5920
+ margin-top: 18px;
5921
+ padding-top: 14px;
5922
+ border-top: 1px solid rgba(255, 255, 255, 0.11);
5923
+ font-size: 12px;
5924
+ color: #9ea3b2;
5925
+ letter-spacing: 0.02em;
5926
+ }
5756
5927
  </style>
5757
5928
  </head>
5758
5929
  <body>
5759
- <h1>${title}</h1>
5760
- <p>${message}</p>
5930
+ <main class="panel">
5931
+ <div class="head">
5932
+ <div class="logo-wrap">${renderLogoSvg()}</div>
5933
+ <div>
5934
+ <div class="badge">${badge}</div>
5935
+ <h1>${safeTitle}</h1>
5936
+ </div>
5937
+ </div>
5938
+ <p>${safeMessage}</p>
5939
+ <div class="footer">Granular CLI authentication flow completed.</div>
5940
+ </main>
5761
5941
  </body>
5762
5942
  </html>`;
5763
5943
  }
@@ -5841,7 +6021,7 @@ async function loginWithBrowser(options) {
5841
6021
  if (!returnedState || returnedState !== state) {
5842
6022
  res.statusCode = 400;
5843
6023
  res.setHeader("Content-Type", "text/html; charset=utf-8");
5844
- res.end(renderHtmlPage("Granular login failed", "State mismatch. Please close this window and retry."));
6024
+ res.end(renderHtmlPage("Granular login failed", "State mismatch. Please close this window and retry.", "error"));
5845
6025
  if (!settled) {
5846
6026
  settled = true;
5847
6027
  clearTimeout(timeout);
@@ -5852,7 +6032,13 @@ async function loginWithBrowser(options) {
5852
6032
  if (error2) {
5853
6033
  res.statusCode = 400;
5854
6034
  res.setHeader("Content-Type", "text/html; charset=utf-8");
5855
- res.end(renderHtmlPage("Granular login failed", "Authentication was canceled or failed. You can close this window."));
6035
+ res.end(
6036
+ renderHtmlPage(
6037
+ "Granular login failed",
6038
+ `${describeAuthError(error2)} You can close this window and return to the CLI.`,
6039
+ "error"
6040
+ )
6041
+ );
5856
6042
  if (!settled) {
5857
6043
  settled = true;
5858
6044
  clearTimeout(timeout);
@@ -5863,7 +6049,7 @@ async function loginWithBrowser(options) {
5863
6049
  if (!apiKey2) {
5864
6050
  res.statusCode = 400;
5865
6051
  res.setHeader("Content-Type", "text/html; charset=utf-8");
5866
- res.end(renderHtmlPage("Granular login failed", "No API key was returned. Please retry."));
6052
+ res.end(renderHtmlPage("Granular login failed", "No API key was returned. Please retry.", "error"));
5867
6053
  if (!settled) {
5868
6054
  settled = true;
5869
6055
  clearTimeout(timeout);
@@ -5873,7 +6059,7 @@ async function loginWithBrowser(options) {
5873
6059
  }
5874
6060
  res.statusCode = 200;
5875
6061
  res.setHeader("Content-Type", "text/html; charset=utf-8");
5876
- res.end(renderHtmlPage("Granular login complete", "Authentication succeeded. You can close this window."));
6062
+ res.end(renderHtmlPage("Granular login complete", "Authentication succeeded. You can close this window.", "success"));
5877
6063
  if (!settled) {
5878
6064
  settled = true;
5879
6065
  clearTimeout(timeout);
@@ -7475,6 +7661,18 @@ function prompt(question, defaultValue) {
7475
7661
  });
7476
7662
  });
7477
7663
  }
7664
+ function waitForEnter(message) {
7665
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
7666
+ return Promise.resolve();
7667
+ }
7668
+ const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7669
+ return new Promise((resolve) => {
7670
+ rl.question(` ${message}`, () => {
7671
+ rl.close();
7672
+ resolve();
7673
+ });
7674
+ });
7675
+ }
7478
7676
  function confirm(question, defaultYes = true) {
7479
7677
  const hint2 = defaultYes ? "Y/n" : "y/N";
7480
7678
  return prompt(`${question} [${hint2}]`).then((answer) => {
@@ -7495,6 +7693,8 @@ async function initCommand(projectName, options) {
7495
7693
  let apiKey = loadApiKey();
7496
7694
  if (!apiKey) {
7497
7695
  info("No API key found. Starting browser login...");
7696
+ await waitForEnter("Press Enter to open the login page in your browser...");
7697
+ console.log();
7498
7698
  const waiting = spinner("Opening browser and waiting for authentication...");
7499
7699
  try {
7500
7700
  apiKey = await loginWithBrowser({
@@ -7615,6 +7815,18 @@ function promptSecret(question) {
7615
7815
  });
7616
7816
  });
7617
7817
  }
7818
+ function waitForEnter2(message) {
7819
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
7820
+ return Promise.resolve();
7821
+ }
7822
+ const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7823
+ return new Promise((resolve) => {
7824
+ rl.question(` ${message}`, () => {
7825
+ rl.close();
7826
+ resolve();
7827
+ });
7828
+ });
7829
+ }
7618
7830
  function maskApiKey(apiKey) {
7619
7831
  if (apiKey.length < 10) return `${apiKey}...`;
7620
7832
  return `${apiKey.substring(0, 10)}...`;
@@ -7636,6 +7848,8 @@ async function loginCommand(options = {}) {
7636
7848
  apiKey = await promptSecret("Enter your API key");
7637
7849
  } else {
7638
7850
  const timeoutMs = options.timeout && options.timeout > 0 ? options.timeout * 1e3 : void 0;
7851
+ await waitForEnter2("Press Enter to open the login page in your browser...");
7852
+ console.log();
7639
7853
  const waiting = spinner("Opening browser and waiting for authentication...");
7640
7854
  try {
7641
7855
  apiKey = await loginWithBrowser({
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as Automerge from '@automerge/automerge';
2
- import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-CnX4jXYQ.mjs';
3
- export { a2 as APIError, u as Assignment, z as Build, F as BuildListResponse, B as BuildPolicy, y as BuildStatus, v as EnvironmentListResponse, r as GranularAuth, H as JobStatus, K as JobSubmitResult, w as Manifest, a0 as ManifestImport, x as ManifestListResponse, $ as ManifestOperation, Z as ManifestPropertySpec, _ as ManifestRelationshipDef, a1 as ManifestVolume, t as PermissionProfileListResponse, s as PermissionRules, L as Prompt, N as RPCRequest, V as RPCRequestFromServer, O as RPCResponse, Q as SyncMessage, X as ToolInvokeParams, Y as ToolResultParams, q as ToolSchema } from './types-CnX4jXYQ.mjs';
2
+ import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-D5B8WlF4.mjs';
3
+ export { a4 as APIError, u as Assignment, z as Build, F as BuildListResponse, B as BuildPolicy, y as BuildStatus, v as EnvironmentListResponse, r as GranularAuth, H as JobStatus, K as JobSubmitResult, w as Manifest, a2 as ManifestImport, x as ManifestListResponse, a1 as ManifestOperation, $ as ManifestPropertySpec, a0 as ManifestRelationshipDef, a3 as ManifestVolume, t as PermissionProfileListResponse, s as PermissionRules, L as Prompt, Q as RPCRequest, Y as RPCRequestFromServer, V as RPCResponse, X as SyncMessage, Z as ToolInvokeParams, _ as ToolResultParams, q as ToolSchema, N as WSDisconnectInfo, O as WSReconnectErrorInfo } from './types-D5B8WlF4.mjs';
4
4
  import { Doc } from '@automerge/automerge/slim';
5
5
 
6
6
  declare class WSClient {
@@ -24,6 +24,9 @@ declare class WSClient {
24
24
  * @returns {Promise<void>} Resolves when connection is open
25
25
  */
26
26
  connect(): Promise<void>;
27
+ private normalizeReason;
28
+ private rejectPending;
29
+ private buildDisconnectError;
27
30
  private handleDisconnect;
28
31
  private handleMessage;
29
32
  /**
@@ -638,6 +641,8 @@ declare class Granular {
638
641
  private apiUrl;
639
642
  private httpUrl;
640
643
  private WebSocketCtor?;
644
+ private onUnexpectedClose?;
645
+ private onReconnectError?;
641
646
  /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
642
647
  private sandboxEffects;
643
648
  /** Active environments tracker: sandboxId → Environment[] */
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as Automerge from '@automerge/automerge';
2
- import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-CnX4jXYQ.js';
3
- export { a2 as APIError, u as Assignment, z as Build, F as BuildListResponse, B as BuildPolicy, y as BuildStatus, v as EnvironmentListResponse, r as GranularAuth, H as JobStatus, K as JobSubmitResult, w as Manifest, a0 as ManifestImport, x as ManifestListResponse, $ as ManifestOperation, Z as ManifestPropertySpec, _ as ManifestRelationshipDef, a1 as ManifestVolume, t as PermissionProfileListResponse, s as PermissionRules, L as Prompt, N as RPCRequest, V as RPCRequestFromServer, O as RPCResponse, Q as SyncMessage, X as ToolInvokeParams, Y as ToolResultParams, q as ToolSchema } from './types-CnX4jXYQ.js';
2
+ import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, b as ToolInfo, c as ToolsChangedEvent, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, d as GraphQLResult, e as DefineRelationshipOptions, f as RelationshipInfo, M as ModelRef, g as ManifestContent, h as RecordObjectOptions, i as RecordObjectResult, S as SandboxListResponse, j as Sandbox, k as CreateSandboxData, l as DeleteResponse, m as PermissionProfile, n as CreatePermissionProfileData, o as CreateEnvironmentData, p as Subject, A as AssignmentListResponse } from './types-D5B8WlF4.js';
3
+ export { a4 as APIError, u as Assignment, z as Build, F as BuildListResponse, B as BuildPolicy, y as BuildStatus, v as EnvironmentListResponse, r as GranularAuth, H as JobStatus, K as JobSubmitResult, w as Manifest, a2 as ManifestImport, x as ManifestListResponse, a1 as ManifestOperation, $ as ManifestPropertySpec, a0 as ManifestRelationshipDef, a3 as ManifestVolume, t as PermissionProfileListResponse, s as PermissionRules, L as Prompt, Q as RPCRequest, Y as RPCRequestFromServer, V as RPCResponse, X as SyncMessage, Z as ToolInvokeParams, _ as ToolResultParams, q as ToolSchema, N as WSDisconnectInfo, O as WSReconnectErrorInfo } from './types-D5B8WlF4.js';
4
4
  import { Doc } from '@automerge/automerge/slim';
5
5
 
6
6
  declare class WSClient {
@@ -24,6 +24,9 @@ declare class WSClient {
24
24
  * @returns {Promise<void>} Resolves when connection is open
25
25
  */
26
26
  connect(): Promise<void>;
27
+ private normalizeReason;
28
+ private rejectPending;
29
+ private buildDisconnectError;
27
30
  private handleDisconnect;
28
31
  private handleMessage;
29
32
  /**
@@ -638,6 +641,8 @@ declare class Granular {
638
641
  private apiUrl;
639
642
  private httpUrl;
640
643
  private WebSocketCtor?;
644
+ private onUnexpectedClose?;
645
+ private onReconnectError?;
641
646
  /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
642
647
  private sandboxEffects;
643
648
  /** Active environments tracker: sandboxId → Environment[] */
package/dist/index.js CHANGED
@@ -3966,6 +3966,10 @@ var WSClient = class {
3966
3966
  */
3967
3967
  async connect() {
3968
3968
  this.isExplicitlyDisconnected = false;
3969
+ if (this.reconnectTimer) {
3970
+ clearTimeout(this.reconnectTimer);
3971
+ this.reconnectTimer = null;
3972
+ }
3969
3973
  let WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
3970
3974
  if (!WebSocketClass) {
3971
3975
  try {
@@ -3987,6 +3991,10 @@ var WSClient = class {
3987
3991
  const socket = this.ws;
3988
3992
  if (typeof socket.on === "function") {
3989
3993
  socket.on("open", () => {
3994
+ if (this.reconnectTimer) {
3995
+ clearTimeout(this.reconnectTimer);
3996
+ this.reconnectTimer = null;
3997
+ }
3990
3998
  this.emit("open", {});
3991
3999
  resolve();
3992
4000
  });
@@ -4004,12 +4012,20 @@ var WSClient = class {
4004
4012
  reject(error);
4005
4013
  }
4006
4014
  });
4007
- socket.on("close", () => {
4008
- this.emit("close", {});
4009
- this.handleDisconnect();
4015
+ socket.on("close", (code, reason) => {
4016
+ this.handleDisconnect({
4017
+ code,
4018
+ reason: this.normalizeReason(reason),
4019
+ // ws does not provide wasClean on Node-style close callback
4020
+ wasClean: code === 1e3
4021
+ });
4010
4022
  });
4011
4023
  } else {
4012
4024
  this.ws.onopen = () => {
4025
+ if (this.reconnectTimer) {
4026
+ clearTimeout(this.reconnectTimer);
4027
+ this.reconnectTimer = null;
4028
+ }
4013
4029
  this.emit("open", {});
4014
4030
  resolve();
4015
4031
  };
@@ -4030,9 +4046,12 @@ var WSClient = class {
4030
4046
  reject(error);
4031
4047
  }
4032
4048
  };
4033
- this.ws.onclose = () => {
4034
- this.emit("close", {});
4035
- this.handleDisconnect();
4049
+ this.ws.onclose = (event) => {
4050
+ this.handleDisconnect({
4051
+ code: event.code,
4052
+ reason: event.reason,
4053
+ wasClean: event.wasClean
4054
+ });
4036
4055
  };
4037
4056
  }
4038
4057
  } catch (error) {
@@ -4040,13 +4059,80 @@ var WSClient = class {
4040
4059
  }
4041
4060
  });
4042
4061
  }
4043
- handleDisconnect() {
4062
+ normalizeReason(reason) {
4063
+ if (reason === null || reason === void 0) return void 0;
4064
+ if (typeof reason === "string") return reason;
4065
+ if (typeof reason === "object" && reason && "toString" in reason) {
4066
+ try {
4067
+ const text = String(reason.toString());
4068
+ return text || void 0;
4069
+ } catch {
4070
+ return void 0;
4071
+ }
4072
+ }
4073
+ return void 0;
4074
+ }
4075
+ rejectPending(error) {
4076
+ this.messageQueue.forEach((pending) => pending.reject(error));
4077
+ this.messageQueue = [];
4078
+ }
4079
+ buildDisconnectError(info) {
4080
+ const details = [
4081
+ info.code !== void 0 ? `code=${info.code}` : void 0,
4082
+ info.reason ? `reason=${info.reason}` : void 0
4083
+ ].filter(Boolean).join(", ");
4084
+ const suffix = details ? ` (${details})` : "";
4085
+ return new Error(`WebSocket disconnected${suffix}`);
4086
+ }
4087
+ handleDisconnect(close = {}) {
4088
+ const reconnectDelayMs = 3e3;
4089
+ const unexpected = !this.isExplicitlyDisconnected;
4090
+ const info = {
4091
+ code: close.code,
4092
+ reason: close.reason,
4093
+ wasClean: close.wasClean,
4094
+ unexpected,
4095
+ timestamp: Date.now(),
4096
+ reconnectScheduled: false
4097
+ };
4044
4098
  this.ws = null;
4045
- if (!this.isExplicitlyDisconnected) {
4099
+ this.emit("close", info);
4100
+ if (this.reconnectTimer) {
4101
+ clearTimeout(this.reconnectTimer);
4102
+ this.reconnectTimer = null;
4103
+ }
4104
+ if (unexpected) {
4105
+ const disconnectError = this.buildDisconnectError(info);
4106
+ this.rejectPending(disconnectError);
4107
+ this.emit("disconnect", info);
4108
+ info.reconnectScheduled = true;
4109
+ info.reconnectDelayMs = reconnectDelayMs;
4110
+ if (this.options.onUnexpectedClose) {
4111
+ try {
4112
+ this.options.onUnexpectedClose(info);
4113
+ } catch (callbackError) {
4114
+ console.error("[Granular] onUnexpectedClose callback failed:", callbackError);
4115
+ }
4116
+ }
4046
4117
  this.reconnectTimer = setTimeout(() => {
4047
4118
  console.log("[Granular] Attempting reconnect...");
4048
- this.connect().catch((e) => console.error("[Granular] Reconnect failed:", e));
4049
- }, 3e3);
4119
+ this.connect().catch((error) => {
4120
+ console.error("[Granular] Reconnect failed:", error);
4121
+ const reconnectInfo = {
4122
+ error: error instanceof Error ? error.message : String(error),
4123
+ sessionId: this.sessionId,
4124
+ timestamp: Date.now()
4125
+ };
4126
+ this.emit("reconnect_error", reconnectInfo);
4127
+ if (this.options.onReconnectError) {
4128
+ try {
4129
+ this.options.onReconnectError(reconnectInfo);
4130
+ } catch (callbackError) {
4131
+ console.error("[Granular] onReconnectError callback failed:", callbackError);
4132
+ }
4133
+ }
4134
+ });
4135
+ }, reconnectDelayMs);
4050
4136
  }
4051
4137
  }
4052
4138
  handleMessage(message) {
@@ -4266,13 +4352,15 @@ var WSClient = class {
4266
4352
  */
4267
4353
  disconnect() {
4268
4354
  this.isExplicitlyDisconnected = true;
4269
- if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
4355
+ if (this.reconnectTimer) {
4356
+ clearTimeout(this.reconnectTimer);
4357
+ this.reconnectTimer = null;
4358
+ }
4270
4359
  if (this.ws) {
4271
- this.ws.close();
4360
+ this.ws.close(1e3, "Client disconnect");
4272
4361
  this.ws = null;
4273
4362
  }
4274
- this.messageQueue.forEach((q) => q.reject(new Error("Client explicitly disconnected")));
4275
- this.messageQueue = [];
4363
+ this.rejectPending(new Error("Client explicitly disconnected"));
4276
4364
  this.rpcHandlers.clear();
4277
4365
  this.emit("disconnect", {});
4278
4366
  }
@@ -4817,7 +4905,7 @@ import { ${allImports} } from "./sandbox-tools";
4817
4905
  this.checkForToolChanges();
4818
4906
  });
4819
4907
  this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
4820
- this.client.on("disconnect", () => this.emit("disconnect", {}));
4908
+ this.client.on("disconnect", (payload) => this.emit("disconnect", payload || {}));
4821
4909
  this.client.on("job.status", (data) => {
4822
4910
  this.emit("job:status", data);
4823
4911
  });
@@ -5744,6 +5832,8 @@ var Granular = class {
5744
5832
  apiUrl;
5745
5833
  httpUrl;
5746
5834
  WebSocketCtor;
5835
+ onUnexpectedClose;
5836
+ onReconnectError;
5747
5837
  /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
5748
5838
  sandboxEffects = /* @__PURE__ */ new Map();
5749
5839
  /** Active environments tracker: sandboxId → Environment[] */
@@ -5760,6 +5850,8 @@ var Granular = class {
5760
5850
  this.apiKey = auth;
5761
5851
  this.apiUrl = options.apiUrl || "wss://api.granular.dev/v2/ws";
5762
5852
  this.WebSocketCtor = options.WebSocketCtor;
5853
+ this.onUnexpectedClose = options.onUnexpectedClose;
5854
+ this.onReconnectError = options.onReconnectError;
5763
5855
  this.httpUrl = this.apiUrl.replace("wss://", "https://").replace("/ws", "");
5764
5856
  }
5765
5857
  /**
@@ -5844,7 +5936,9 @@ var Granular = class {
5844
5936
  url: this.apiUrl,
5845
5937
  sessionId: envData.environmentId,
5846
5938
  token: this.apiKey,
5847
- WebSocketCtor: this.WebSocketCtor
5939
+ WebSocketCtor: this.WebSocketCtor,
5940
+ onUnexpectedClose: this.onUnexpectedClose,
5941
+ onReconnectError: this.onReconnectError
5848
5942
  });
5849
5943
  await client.connect();
5850
5944
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;