@granular-software/sdk 0.3.2 → 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
@@ -4,10 +4,12 @@
4
4
  var fs = require('fs');
5
5
  var path = require('path');
6
6
  var readline = require('readline');
7
+ var http = require('http');
8
+ var child_process = require('child_process');
9
+ var crypto = require('crypto');
7
10
  var process6 = require('process');
8
11
  var os = require('os');
9
12
  var tty = require('tty');
10
- var child_process = require('child_process');
11
13
 
12
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
15
 
@@ -32,6 +34,7 @@ function _interopNamespace(e) {
32
34
  var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
33
35
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
34
36
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
37
+ var http__namespace = /*#__PURE__*/_interopNamespace(http);
35
38
  var process6__default = /*#__PURE__*/_interopDefault(process6);
36
39
  var os__default = /*#__PURE__*/_interopDefault(os);
37
40
  var tty__default = /*#__PURE__*/_interopDefault(tty);
@@ -5455,6 +5458,12 @@ function loadApiUrl() {
5455
5458
  if (process.env.GRANULAR_API_URL) return process.env.GRANULAR_API_URL;
5456
5459
  return "https://cf-api-gateway.arthur6084.workers.dev/granular";
5457
5460
  }
5461
+ function loadAuthUrl() {
5462
+ if (process.env.GRANULAR_AUTH_URL) {
5463
+ return process.env.GRANULAR_AUTH_URL;
5464
+ }
5465
+ return "https://app.granular.software";
5466
+ }
5458
5467
  function saveApiKey(apiKey) {
5459
5468
  const envLocalPath = getEnvLocalPath();
5460
5469
  let content = "";
@@ -5722,6 +5731,347 @@ var ApiClient = class {
5722
5731
  });
5723
5732
  }
5724
5733
  };
5734
+ var CALLBACK_HOST = "127.0.0.1";
5735
+ var CALLBACK_PATH = "/callback";
5736
+ var DEFAULT_TIMEOUT_MS = 3 * 60 * 1e3;
5737
+ function normalizeAuthBaseUrl(input) {
5738
+ const url = new URL(input);
5739
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
5740
+ throw new Error("Auth URL must start with http:// or https://");
5741
+ }
5742
+ return url;
5743
+ }
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";
5838
+ return `<!doctype html>
5839
+ <html lang="en">
5840
+ <head>
5841
+ <meta charset="utf-8" />
5842
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5843
+ <title>${safeTitle}</title>
5844
+ <style>
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
+ }
5927
+ </style>
5928
+ </head>
5929
+ <body>
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>
5941
+ </body>
5942
+ </html>`;
5943
+ }
5944
+ function describeAuthError(code) {
5945
+ switch (code) {
5946
+ case "unauthorized":
5947
+ return "You must sign in before authorizing the CLI.";
5948
+ case "no_organization":
5949
+ return "Your account is not associated with an organization.";
5950
+ case "key_creation_failed":
5951
+ return "Failed to create an API key for CLI login.";
5952
+ case "missing_key_value":
5953
+ return "The auth server did not return an API key value.";
5954
+ case "auth_failed":
5955
+ return "Authentication failed on the server.";
5956
+ default:
5957
+ return code;
5958
+ }
5959
+ }
5960
+ function openExternalUrl(url) {
5961
+ const platform = process.platform;
5962
+ return new Promise((resolve) => {
5963
+ let child;
5964
+ if (platform === "darwin") {
5965
+ child = child_process.spawn("open", [url], { stdio: "ignore" });
5966
+ } else if (platform === "win32") {
5967
+ child = child_process.spawn("cmd", ["/c", "start", "", url], {
5968
+ stdio: "ignore",
5969
+ windowsHide: true
5970
+ });
5971
+ } else {
5972
+ child = child_process.spawn("xdg-open", [url], { stdio: "ignore" });
5973
+ }
5974
+ child.once("error", () => resolve(false));
5975
+ child.once("spawn", () => resolve(true));
5976
+ });
5977
+ }
5978
+ function closeServer(server) {
5979
+ return new Promise((resolve) => {
5980
+ server.close(() => resolve());
5981
+ });
5982
+ }
5983
+ async function loginWithBrowser(options) {
5984
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5985
+ const authBaseUrl = normalizeAuthBaseUrl(options.authBaseUrl);
5986
+ const state = crypto.randomBytes(24).toString("hex");
5987
+ const server = http__namespace.createServer();
5988
+ await new Promise((resolve, reject) => {
5989
+ server.once("error", reject);
5990
+ server.listen(0, CALLBACK_HOST, () => resolve());
5991
+ });
5992
+ const address = server.address();
5993
+ if (!address || typeof address === "string") {
5994
+ await closeServer(server);
5995
+ throw new Error("Could not start local callback server.");
5996
+ }
5997
+ const callbackUrl = `http://${CALLBACK_HOST}:${address.port}${CALLBACK_PATH}`;
5998
+ const authUrl = new URL("/api/cli/auth/start", authBaseUrl);
5999
+ authUrl.searchParams.set("callback", callbackUrl);
6000
+ authUrl.searchParams.set("state", state);
6001
+ const browserOpened = await openExternalUrl(authUrl.toString());
6002
+ options.onAuthUrl?.(authUrl.toString(), browserOpened);
6003
+ let settled = false;
6004
+ try {
6005
+ const apiKey = await new Promise((resolve, reject) => {
6006
+ const timeout = setTimeout(() => {
6007
+ if (settled) return;
6008
+ settled = true;
6009
+ reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for browser authentication.`));
6010
+ }, timeoutMs);
6011
+ server.on("request", (req, res) => {
6012
+ const url = new URL(req.url || "/", `http://${CALLBACK_HOST}`);
6013
+ if (url.pathname !== CALLBACK_PATH) {
6014
+ res.statusCode = 404;
6015
+ res.end("Not found");
6016
+ return;
6017
+ }
6018
+ const returnedState = url.searchParams.get("state");
6019
+ const apiKey2 = url.searchParams.get("api_key");
6020
+ const error2 = url.searchParams.get("error");
6021
+ if (!returnedState || returnedState !== state) {
6022
+ res.statusCode = 400;
6023
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
6024
+ res.end(renderHtmlPage("Granular login failed", "State mismatch. Please close this window and retry.", "error"));
6025
+ if (!settled) {
6026
+ settled = true;
6027
+ clearTimeout(timeout);
6028
+ reject(new Error("Browser auth state mismatch."));
6029
+ }
6030
+ return;
6031
+ }
6032
+ if (error2) {
6033
+ res.statusCode = 400;
6034
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
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
+ );
6042
+ if (!settled) {
6043
+ settled = true;
6044
+ clearTimeout(timeout);
6045
+ reject(new Error(describeAuthError(error2)));
6046
+ }
6047
+ return;
6048
+ }
6049
+ if (!apiKey2) {
6050
+ res.statusCode = 400;
6051
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
6052
+ res.end(renderHtmlPage("Granular login failed", "No API key was returned. Please retry.", "error"));
6053
+ if (!settled) {
6054
+ settled = true;
6055
+ clearTimeout(timeout);
6056
+ reject(new Error("No API key returned from browser auth."));
6057
+ }
6058
+ return;
6059
+ }
6060
+ res.statusCode = 200;
6061
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
6062
+ res.end(renderHtmlPage("Granular login complete", "Authentication succeeded. You can close this window.", "success"));
6063
+ if (!settled) {
6064
+ settled = true;
6065
+ clearTimeout(timeout);
6066
+ resolve(apiKey2);
6067
+ }
6068
+ });
6069
+ });
6070
+ return apiKey;
6071
+ } finally {
6072
+ await closeServer(server);
6073
+ }
6074
+ }
5725
6075
 
5726
6076
  // ../../node_modules/.bun/chalk@5.4.1/node_modules/chalk/source/vendor/ansi-styles/index.js
5727
6077
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -7311,12 +7661,15 @@ function prompt(question, defaultValue) {
7311
7661
  });
7312
7662
  });
7313
7663
  }
7314
- function promptSecret(question) {
7664
+ function waitForEnter(message) {
7665
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
7666
+ return Promise.resolve();
7667
+ }
7315
7668
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7316
7669
  return new Promise((resolve) => {
7317
- rl.question(` ${question}: `, (answer) => {
7670
+ rl.question(` ${message}`, () => {
7318
7671
  rl.close();
7319
- resolve(answer.trim());
7672
+ resolve();
7320
7673
  });
7321
7674
  });
7322
7675
  }
@@ -7339,13 +7692,27 @@ async function initCommand(projectName, options) {
7339
7692
  }
7340
7693
  let apiKey = loadApiKey();
7341
7694
  if (!apiKey) {
7342
- info("No API key found. Let's set one up.");
7343
- console.log();
7344
- dim("Get your API key at https://granular.dev/dashboard/api-keys");
7695
+ info("No API key found. Starting browser login...");
7696
+ await waitForEnter("Press Enter to open the login page in your browser...");
7345
7697
  console.log();
7346
- apiKey = await promptSecret("Enter your API key");
7347
- if (!apiKey) {
7348
- error("API key is required.");
7698
+ const waiting = spinner("Opening browser and waiting for authentication...");
7699
+ try {
7700
+ apiKey = await loginWithBrowser({
7701
+ authBaseUrl: loadAuthUrl(),
7702
+ onAuthUrl: (authUrl, opened) => {
7703
+ if (!opened) {
7704
+ waiting.stop();
7705
+ warn("Could not open your browser automatically.");
7706
+ dim(`Open this URL to continue:
7707
+ ${authUrl}`);
7708
+ waiting.start();
7709
+ }
7710
+ }
7711
+ });
7712
+ waiting.succeed(" Browser authentication completed.");
7713
+ } catch (err) {
7714
+ waiting.fail(` Browser authentication failed: ${err.message}`);
7715
+ dim("Run `granular login --manual` to paste an API key directly.");
7349
7716
  process.exit(1);
7350
7717
  }
7351
7718
  } else {
@@ -7439,7 +7806,7 @@ async function initCommand(projectName, options) {
7439
7806
  hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
7440
7807
  console.log();
7441
7808
  }
7442
- function promptSecret2(question) {
7809
+ function promptSecret(question) {
7443
7810
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7444
7811
  return new Promise((resolve) => {
7445
7812
  rl.question(` ${question}: `, (answer) => {
@@ -7448,26 +7815,73 @@ function promptSecret2(question) {
7448
7815
  });
7449
7816
  });
7450
7817
  }
7451
- async function loginCommand() {
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
+ }
7830
+ function maskApiKey(apiKey) {
7831
+ if (apiKey.length < 10) return `${apiKey}...`;
7832
+ return `${apiKey.substring(0, 10)}...`;
7833
+ }
7834
+ async function loginCommand(options = {}) {
7452
7835
  printHeader();
7453
7836
  const existing = loadApiKey();
7454
7837
  if (existing) {
7455
7838
  info("An API key is already configured.");
7456
- dim(`Current: ${existing.substring(0, 10)}...`);
7839
+ dim(`Current: ${maskApiKey(existing)}`);
7457
7840
  console.log();
7458
7841
  }
7459
- dim("Get your API key at https://granular.dev/dashboard/api-keys");
7460
- console.log();
7461
- const apiKey = await promptSecret2("Enter your API key");
7842
+ const apiUrl = loadApiUrl();
7843
+ let apiKey = options.apiKey?.trim();
7844
+ if (!apiKey) {
7845
+ if (options.manual) {
7846
+ dim("Get your API key at https://app.granular.software/w/default/api-keys");
7847
+ console.log();
7848
+ apiKey = await promptSecret("Enter your API key");
7849
+ } else {
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();
7853
+ const waiting = spinner("Opening browser and waiting for authentication...");
7854
+ try {
7855
+ apiKey = await loginWithBrowser({
7856
+ authBaseUrl: loadAuthUrl(),
7857
+ timeoutMs,
7858
+ onAuthUrl: (authUrl, opened) => {
7859
+ if (!opened) {
7860
+ waiting.stop();
7861
+ warn("Could not open your browser automatically.");
7862
+ dim(`Open this URL to continue:
7863
+ ${authUrl}`);
7864
+ waiting.start();
7865
+ }
7866
+ }
7867
+ });
7868
+ waiting.succeed(" Browser authentication completed.");
7869
+ } catch (err) {
7870
+ waiting.fail(` Browser authentication failed: ${err.message}`);
7871
+ dim("Retry with `granular login --manual` to paste an API key directly.");
7872
+ process.exit(1);
7873
+ }
7874
+ }
7875
+ }
7462
7876
  if (!apiKey) {
7463
7877
  error("API key is required.");
7464
7878
  process.exit(1);
7465
7879
  }
7466
- if (!apiKey.startsWith("sk_")) {
7467
- warn('API key should start with "sk_". Continuing anyway...');
7880
+ if (!apiKey.startsWith("sk_") && !apiKey.startsWith("gn_sk_")) {
7881
+ warn("API key does not look like a recognized Granular/WorkOS key. Continuing anyway...");
7468
7882
  }
7469
7883
  const spinner2 = spinner("Validating...");
7470
- const api = new ApiClient(apiKey, loadApiUrl());
7884
+ const api = new ApiClient(apiKey, apiUrl);
7471
7885
  const valid = await api.validateKey();
7472
7886
  if (!valid) {
7473
7887
  spinner2.fail(" Invalid API key.");
@@ -7475,7 +7889,7 @@ async function loginCommand() {
7475
7889
  }
7476
7890
  saveApiKey(apiKey);
7477
7891
  spinner2.succeed(" Authenticated successfully.");
7478
- dim("API key saved to .env.local");
7892
+ dim("API key saved to .env.local (used by CLI and SDK examples)");
7479
7893
  console.log();
7480
7894
  }
7481
7895
 
@@ -8217,9 +8631,16 @@ program2.command("init [project-name]").description("Initialize a new Granular p
8217
8631
  process.exit(1);
8218
8632
  }
8219
8633
  });
8220
- program2.command("login").description("Authenticate with your Granular API key").action(async () => {
8634
+ program2.command("login").description("Authenticate with Granular (browser by default)").option("--manual", "Prompt for API key instead of browser login").option("--api-key <key>", "Use a provided API key directly").option("--timeout <seconds>", "Browser auth timeout in seconds", (value) => {
8635
+ const n = Number.parseInt(value, 10);
8636
+ return Number.isFinite(n) ? n : 180;
8637
+ }).action(async (opts) => {
8221
8638
  try {
8222
- await loginCommand();
8639
+ await loginCommand({
8640
+ manual: opts.manual,
8641
+ apiKey: opts.apiKey,
8642
+ timeout: opts.timeout
8643
+ });
8223
8644
  } catch (err) {
8224
8645
  error(err.message);
8225
8646
  process.exit(1);
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[] */