@granular-software/sdk 0.2.0 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-D46q5WTh.mjs';
1
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.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-D46q5WTh.js';
1
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.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-D46q5WTh.mjs';
2
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.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-D46q5WTh.js';
2
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.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-D46q5WTh.mjs';
2
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.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-D46q5WTh.js';
2
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.js';
3
3
 
4
4
  /**
5
5
  * Mastra tool definition interface
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-D46q5WTh.mjs';
1
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.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-D46q5WTh.js';
1
+ import { T as ToolWithHandler } from '../types-DiMEb3SE.js';
2
2
 
3
3
  /**
4
4
  * OpenAI tool call from chat completion response
package/dist/cli/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ var fs = require('fs');
4
5
  var path = require('path');
5
6
  var readline = require('readline');
6
- var fs = require('fs');
7
7
  var process6 = require('process');
8
8
  var os = require('os');
9
9
  var tty = require('tty');
10
+ var child_process = require('child_process');
10
11
 
11
12
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
13
 
@@ -28,9 +29,9 @@ function _interopNamespace(e) {
28
29
  return Object.freeze(n);
29
30
  }
30
31
 
32
+ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
31
33
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
32
34
  var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
33
- var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
34
35
  var process6__default = /*#__PURE__*/_interopDefault(process6);
35
36
  var os__default = /*#__PURE__*/_interopDefault(os);
36
37
  var tty__default = /*#__PURE__*/_interopDefault(tty);
@@ -1185,7 +1186,7 @@ var require_command = __commonJS({
1185
1186
  var EventEmitter = __require("events").EventEmitter;
1186
1187
  var childProcess = __require("child_process");
1187
1188
  var path4 = __require("path");
1188
- var fs4 = __require("fs");
1189
+ var fs5 = __require("fs");
1189
1190
  var process10 = __require("process");
1190
1191
  var { Argument: Argument2, humanReadableArgName } = require_argument();
1191
1192
  var { CommanderError: CommanderError2 } = require_error();
@@ -2165,7 +2166,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2165
2166
  * @param {string} subcommandName
2166
2167
  */
2167
2168
  _checkForMissingExecutable(executableFile, executableDir, subcommandName) {
2168
- if (fs4.existsSync(executableFile)) return;
2169
+ if (fs5.existsSync(executableFile)) return;
2169
2170
  const executableDirMessage = executableDir ? `searched for local subcommand relative to directory '${executableDir}'` : "no directory for search for local subcommand, use .executableDir() to supply a custom directory";
2170
2171
  const executableMissing = `'${executableFile}' does not exist
2171
2172
  - if '${subcommandName}' is not meant to be an executable command, remove description parameter from '.command()' and use '.description()' instead
@@ -2184,10 +2185,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
2184
2185
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
2185
2186
  function findFile(baseDir, baseName) {
2186
2187
  const localBin = path4.resolve(baseDir, baseName);
2187
- if (fs4.existsSync(localBin)) return localBin;
2188
+ if (fs5.existsSync(localBin)) return localBin;
2188
2189
  if (sourceExt.includes(path4.extname(baseName))) return void 0;
2189
2190
  const foundExt = sourceExt.find(
2190
- (ext) => fs4.existsSync(`${localBin}${ext}`)
2191
+ (ext) => fs5.existsSync(`${localBin}${ext}`)
2191
2192
  );
2192
2193
  if (foundExt) return `${localBin}${foundExt}`;
2193
2194
  return void 0;
@@ -2199,7 +2200,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2199
2200
  if (this._scriptPath) {
2200
2201
  let resolvedScriptPath;
2201
2202
  try {
2202
- resolvedScriptPath = fs4.realpathSync(this._scriptPath);
2203
+ resolvedScriptPath = fs5.realpathSync(this._scriptPath);
2203
2204
  } catch {
2204
2205
  resolvedScriptPath = this._scriptPath;
2205
2206
  }
@@ -3430,7 +3431,7 @@ var require_package = __commonJS({
3430
3431
  // ../../node_modules/.bun/dotenv@16.6.1/node_modules/dotenv/lib/main.js
3431
3432
  var require_main = __commonJS({
3432
3433
  "../../node_modules/.bun/dotenv@16.6.1/node_modules/dotenv/lib/main.js"(exports, module) {
3433
- var fs4 = __require("fs");
3434
+ var fs5 = __require("fs");
3434
3435
  var path4 = __require("path");
3435
3436
  var os2 = __require("os");
3436
3437
  var crypto = __require("crypto");
@@ -3539,7 +3540,7 @@ var require_main = __commonJS({
3539
3540
  if (options && options.path && options.path.length > 0) {
3540
3541
  if (Array.isArray(options.path)) {
3541
3542
  for (const filepath of options.path) {
3542
- if (fs4.existsSync(filepath)) {
3543
+ if (fs5.existsSync(filepath)) {
3543
3544
  possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
3544
3545
  }
3545
3546
  }
@@ -3549,7 +3550,7 @@ var require_main = __commonJS({
3549
3550
  } else {
3550
3551
  possibleVaultPath = path4.resolve(process.cwd(), ".env.vault");
3551
3552
  }
3552
- if (fs4.existsSync(possibleVaultPath)) {
3553
+ if (fs5.existsSync(possibleVaultPath)) {
3553
3554
  return possibleVaultPath;
3554
3555
  }
3555
3556
  return null;
@@ -3598,7 +3599,7 @@ var require_main = __commonJS({
3598
3599
  const parsedAll = {};
3599
3600
  for (const path5 of optionPaths) {
3600
3601
  try {
3601
- const parsed = DotenvModule.parse(fs4.readFileSync(path5, { encoding }));
3602
+ const parsed = DotenvModule.parse(fs5.readFileSync(path5, { encoding }));
3602
3603
  DotenvModule.populate(parsedAll, parsed, options);
3603
3604
  } catch (e) {
3604
3605
  if (debug) {
@@ -5535,6 +5536,18 @@ function createDefaultManifest(name) {
5535
5536
  }
5536
5537
  };
5537
5538
  }
5539
+ var STD_CLASS_NAMES = /* @__PURE__ */ new Set(["entity", "class", "user", "company", "string", "number", "boolean", "tool_parameter"]);
5540
+ function getClassNamesFromManifest(manifest) {
5541
+ const names = /* @__PURE__ */ new Set();
5542
+ for (const vol of manifest.volumes ?? []) {
5543
+ for (const op of vol.operations ?? []) {
5544
+ if (op.create && !STD_CLASS_NAMES.has(op.create)) {
5545
+ names.add(op.create);
5546
+ }
5547
+ }
5548
+ }
5549
+ return Array.from(names);
5550
+ }
5538
5551
  function resolveConfig(options) {
5539
5552
  const apiKey = loadApiKey();
5540
5553
  const apiUrl = loadApiUrl();
@@ -7119,6 +7132,183 @@ function buildStatus(status) {
7119
7132
  }
7120
7133
 
7121
7134
  // src/cli/commands/init.ts
7135
+ var EFFECTS_SCRIPT_NAME = "granular-effects.ts";
7136
+ function toWsUrl(apiUrl) {
7137
+ return apiUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
7138
+ }
7139
+ function generateEffectsScript(sandboxId, apiUrl, classNames) {
7140
+ const wsUrl = toWsUrl(apiUrl);
7141
+ classNames.length > 0;
7142
+ const mockDataEntries = classNames.map(
7143
+ (c) => ` ${c}: [
7144
+ { id: '${c}_001', name: 'Sample ${c}' },
7145
+ { id: '${c}_002', name: 'Another ${c}' },
7146
+ ]`
7147
+ );
7148
+ const mockDataBlock = mockDataEntries.length > 0 ? `const MOCK_DATA: Record<string, any[]> = {
7149
+ ${mockDataEntries.join(",\n")},
7150
+ };` : "const MOCK_DATA: Record<string, any[]> = {};";
7151
+ const instanceEffects = classNames.map((className) => {
7152
+ className.charAt(0).toUpperCase() + className.slice(1);
7153
+ return ` {
7154
+ name: 'get_info',
7155
+ description: \`Get details of a ${className} by ID\`,
7156
+ className: '${className}',
7157
+ inputSchema: {
7158
+ type: 'object',
7159
+ properties: { include_related: { type: 'boolean', description: 'Include related items' } },
7160
+ },
7161
+ handler: async (id: string, params: any) => {
7162
+ logEffect('get_info', '${className}', id, params);
7163
+ const item = (MOCK_DATA['${className}'] || []).find((x: any) => x.id === id);
7164
+ if (!item) return { error: \`${className} "\${id}" not found\` };
7165
+ return { ...item, ...(params?.include_related ? { related: [] } : {}) };
7166
+ },
7167
+ }`;
7168
+ });
7169
+ const staticEffects = classNames.map((className) => {
7170
+ const searchName = "search_" + className + "s";
7171
+ return ` {
7172
+ name: '${searchName}',
7173
+ description: \`Search ${className}s by keyword\`,
7174
+ className: '${className}',
7175
+ static: true,
7176
+ inputSchema: {
7177
+ type: 'object',
7178
+ properties: {
7179
+ query: { type: 'string', description: 'Search keyword' },
7180
+ limit: { type: 'number', description: 'Max results' },
7181
+ },
7182
+ required: ['query'],
7183
+ },
7184
+ handler: async (params: any) => {
7185
+ logEffect('${searchName}', null, null, params);
7186
+ const q = ((params?.query) || '').toLowerCase();
7187
+ const limit = params?.limit ?? 10;
7188
+ const list = (MOCK_DATA['${className}'] || []).filter((x: any) =>
7189
+ String(x.name || x.id || '').toLowerCase().includes(q)
7190
+ ).slice(0, limit);
7191
+ return { query: params?.query, results: list, total: list.length };
7192
+ },
7193
+ }`;
7194
+ });
7195
+ const allEffects = [...instanceEffects, ...staticEffects];
7196
+ if (allEffects.length > 0) {
7197
+ allEffects.push(` {
7198
+ name: 'full_text_search',
7199
+ description: 'Search across all types',
7200
+ inputSchema: {
7201
+ type: 'object',
7202
+ properties: {
7203
+ query: { type: 'string', description: 'Search query' },
7204
+ types: { type: 'array', items: { type: 'string' }, description: 'Filter by type' },
7205
+ },
7206
+ required: ['query'],
7207
+ },
7208
+ handler: async (params: any) => {
7209
+ logEffect('full_text_search', null, null, params);
7210
+ const q = ((params?.query) || '').toLowerCase();
7211
+ const types: string[] = params?.types || [${classNames.map((c) => `'${c}'`).join(", ")}];
7212
+ const matches: any[] = [];
7213
+ for (const type of types) {
7214
+ const items = MOCK_DATA[type] || [];
7215
+ for (const item of items) {
7216
+ const text = Object.values(item).join(' ').toLowerCase();
7217
+ if (!q || text.includes(q)) {
7218
+ matches.push({ type, id: item.id, label: item.name || item.id, snippet: text.slice(0, 60) });
7219
+ }
7220
+ }
7221
+ }
7222
+ return { query: params?.query, types, matches, total: matches.length };
7223
+ },
7224
+ }`);
7225
+ }
7226
+ const effectsArray = allEffects.length > 0 ? `[
7227
+ ${allEffects.join(",\n")}
7228
+ ]` : "[]";
7229
+ return `/**
7230
+ * Granular effects script for sandbox ${sandboxId}
7231
+ * Run with: npx tsx ${EFFECTS_SCRIPT_NAME}
7232
+ * Requires: GRANULAR_API_KEY in env or .env.local, and optional "ws" package for Node.
7233
+ *
7234
+ * Keeps the process alive so the simulator can invoke these effect handlers.
7235
+ */
7236
+
7237
+ import { Granular } from '@granular-software/sdk';
7238
+
7239
+ const SANDBOX_ID = '${sandboxId}';
7240
+ const API_URL = '${wsUrl}';
7241
+
7242
+ function log(msg: string) {
7243
+ console.log(\`[\${new Date().toISOString()}] [Effects] \${msg}\`);
7244
+ }
7245
+
7246
+ function logEffect(name: string, className: string | null, id: string | null, params: any) {
7247
+ const target = className ? (id ? \`\${className}(\${id})\` : \`\${className}\`) : 'global';
7248
+ console.log(\` [EFFECT] \${name}(\${target}) \${JSON.stringify(params || {})}\`);
7249
+ }
7250
+
7251
+ async function main() {
7252
+ const apiKey = process.env.GRANULAR_API_KEY;
7253
+ if (!apiKey) {
7254
+ console.error('[Effects] GRANULAR_API_KEY is required (set in .env.local or environment).');
7255
+ process.exit(1);
7256
+ }
7257
+
7258
+ let WebSocketCtor: any;
7259
+ try {
7260
+ WebSocketCtor = (await import('ws')).default;
7261
+ } catch {
7262
+ log('Optional: install "ws" for Node.js WebSocket support. Using global WebSocket.');
7263
+ }
7264
+
7265
+ log('Initializing client...');
7266
+ const granular = new Granular({
7267
+ apiKey,
7268
+ apiUrl: API_URL,
7269
+ ...(WebSocketCtor && { WebSocketCtor }),
7270
+ });
7271
+
7272
+ const userId = 'effects_user';
7273
+ log(\`Recording user \${userId}\`);
7274
+ const user = await granular.recordUser({
7275
+ userId,
7276
+ name: 'Effects Host',
7277
+ permissions: ['default'],
7278
+ });
7279
+
7280
+ log(\`Connecting to sandbox \${SANDBOX_ID}\`);
7281
+ const env = await granular.connect({ sandbox: SANDBOX_ID, user, clientId: 'effects-host' });
7282
+
7283
+ log('Waiting for graph container to be ready...');
7284
+ for (let i = 0; i < 6; i++) {
7285
+ await new Promise((r) => setTimeout(r, 5000));
7286
+ try {
7287
+ const probe = await env.graphql(\`query { model(path: "class") { path } }\`);
7288
+ if ((probe as any)?.data?.model?.path) {
7289
+ log(\`Graph ready after \${(i + 1) * 5}s\`);
7290
+ break;
7291
+ }
7292
+ } catch {
7293
+ log(\`Not ready yet (\${(i + 1) * 5}s)...\`);
7294
+ }
7295
+ }
7296
+
7297
+ log('Publishing effects...');
7298
+ ${mockDataBlock}
7299
+
7300
+ await env.publishEffects(${effectsArray});
7301
+
7302
+ log('Effects published. Process kept alive for simulator. Press Ctrl+C to exit.');
7303
+ await new Promise(() => {});
7304
+ }
7305
+
7306
+ main().catch((err) => {
7307
+ console.error('[Effects] Failed:', err);
7308
+ process.exit(1);
7309
+ });
7310
+ `;
7311
+ }
7122
7312
  function prompt(question, defaultValue) {
7123
7313
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7124
7314
  const suffix = defaultValue ? ` ${brand.muted(`(${defaultValue})`)}` : "";
@@ -7228,6 +7418,11 @@ async function initCommand(projectName) {
7228
7418
  dim("You can retry with `granular build`");
7229
7419
  }
7230
7420
  }
7421
+ const classNames = getClassNamesFromManifest(project.manifest);
7422
+ const effectsPath = path__namespace.join(getProjectRoot(), EFFECTS_SCRIPT_NAME);
7423
+ const effectsContent = generateEffectsScript(sandbox.sandboxId, apiUrl, classNames);
7424
+ fs__namespace.writeFileSync(effectsPath, effectsContent, "utf-8");
7425
+ success(`Created ${brand.bold(EFFECTS_SCRIPT_NAME)} (effects script for simulator)`);
7231
7426
  console.log();
7232
7427
  divider();
7233
7428
  console.log();
@@ -7237,13 +7432,20 @@ async function initCommand(projectName) {
7237
7432
  "Sandbox": sandbox.sandboxId,
7238
7433
  "Manifest": "granular.json",
7239
7434
  "Config": ".granularrc",
7240
- "API Key": ".env.local"
7435
+ "API Key": ".env.local",
7436
+ "Effects script": EFFECTS_SCRIPT_NAME
7241
7437
  });
7242
7438
  nextSteps([
7243
7439
  { command: "granular add class <name>", description: "Add a class to your schema" },
7244
7440
  { command: "granular build", description: "Build your sandbox" },
7245
7441
  { command: "granular status", description: "Check build status" }
7246
7442
  ]);
7443
+ console.log();
7444
+ dim("Run the effects script (keeps process alive for simulator):");
7445
+ hint(`npx tsx ${EFFECTS_SCRIPT_NAME}`, "requires tsx and GRANULAR_API_KEY in .env.local");
7446
+ dim("Open the simulator in your browser:");
7447
+ hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
7448
+ console.log();
7247
7449
  }
7248
7450
  function promptSecret2(question) {
7249
7451
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
@@ -7994,6 +8196,22 @@ console.log('Connected to:', env.environmentId);`);
7994
8196
  spinner2.succeed(`Generated ${brand.bold("GRANULAR.md")}`);
7995
8197
  info(`Open GRANULAR.md to see your project documentation.`);
7996
8198
  }
8199
+ var SIMULATOR_BASE = "https://app.granular.software/simulator";
8200
+ function openUrl(url) {
8201
+ const platform = process.platform;
8202
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
8203
+ child_process.spawn(cmd, [url], { stdio: "ignore", shell: platform === "win32" });
8204
+ }
8205
+ async function simulateCommand(sandboxIdArg) {
8206
+ const sandboxId = sandboxIdArg ?? resolveConfig().sandboxId;
8207
+ if (!sandboxId) {
8208
+ error("No sandbox ID. Run from a project with `granular init` or pass a sandbox ID: granular simulate <sandbox-id>");
8209
+ process.exit(1);
8210
+ }
8211
+ const url = `${SIMULATOR_BASE}?sandboxId=${encodeURIComponent(sandboxId)}`;
8212
+ info(`Opening simulator: ${url}`);
8213
+ openUrl(url);
8214
+ }
7997
8215
 
7998
8216
  // src/cli/index.ts
7999
8217
  var VERSION = "0.2.0";
@@ -8096,4 +8314,12 @@ program2.command("document").description("Generate GRANULAR.md documentation fro
8096
8314
  process.exit(1);
8097
8315
  }
8098
8316
  });
8317
+ program2.command("simulate [sandbox-id]").description("Open the Granular simulator in the browser for the current (or given) sandbox").action(async (sandboxId) => {
8318
+ try {
8319
+ await simulateCommand(sandboxId);
8320
+ } catch (err) {
8321
+ error(err.message);
8322
+ process.exit(1);
8323
+ }
8324
+ });
8099
8325
  program2.parse(process.argv);
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as Automerge from '@automerge/automerge';
2
- import { Doc } from '@automerge/automerge';
3
- import { W as WSClientOptions, T as ToolWithHandler, P as PublishToolsResult, J as Job, a as ToolHandler, I as InstanceToolHandler, D as DomainState, G as GranularOptions, R as RecordUserOptions, U as User, C as ConnectOptions, E as EnvironmentData, b as GraphQLResult, c as DefineRelationshipOptions, d as RelationshipInfo, M as ModelRef, e as ManifestContent, f as RecordObjectOptions, g as RecordObjectResult, S as SandboxListResponse, h as Sandbox, i as CreateSandboxData, j as DeleteResponse, k as PermissionProfile, l as CreatePermissionProfileData, m as CreateEnvironmentData, n as Subject, A as AssignmentListResponse } from './types-D46q5WTh.mjs';
4
- export { $ as APIError, r as Assignment, w as Build, x as BuildListResponse, B as BuildPolicy, v as BuildStatus, s as EnvironmentListResponse, y as JobStatus, z as JobSubmitResult, t as Manifest, Z as ManifestImport, u as ManifestListResponse, Y as ManifestOperation, V as ManifestPropertySpec, X as ManifestRelationshipDef, _ as ManifestVolume, q as PermissionProfileListResponse, p as PermissionRules, F as Prompt, H as RPCRequest, N as RPCRequestFromServer, K as RPCResponse, L as SyncMessage, O as ToolInvokeParams, Q as ToolResultParams, o as ToolSchema } from './types-D46q5WTh.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-DiMEb3SE.mjs';
3
+ export { a1 as APIError, t as Assignment, y as Build, z as BuildListResponse, B as BuildPolicy, x as BuildStatus, u as EnvironmentListResponse, F as JobStatus, H as JobSubmitResult, v as Manifest, $ as ManifestImport, w as ManifestListResponse, _ as ManifestOperation, Y as ManifestPropertySpec, Z as ManifestRelationshipDef, a0 as ManifestVolume, s as PermissionProfileListResponse, r as PermissionRules, K as Prompt, L as RPCRequest, Q as RPCRequestFromServer, N as RPCResponse, O as SyncMessage, V as ToolInvokeParams, X as ToolResultParams, q as ToolSchema } from './types-DiMEb3SE.mjs';
4
+ import { Doc } from '@automerge/automerge/slim';
5
5
 
6
6
  declare class WSClient {
7
7
  private ws;
@@ -17,6 +17,7 @@ declare class WSClient {
17
17
  private syncState;
18
18
  private reconnectTimer;
19
19
  private isExplicitlyDisconnected;
20
+ private options;
20
21
  constructor(options: WSClientOptions);
21
22
  /**
22
23
  * Connect to the WebSocket server
@@ -73,6 +74,10 @@ declare class Session {
73
74
  /** Tracks which tools are instance methods (className set, not static) */
74
75
  private instanceTools;
75
76
  private currentDomainRevision;
77
+ /** Local effect registry: name → full ToolWithHandler */
78
+ private effects;
79
+ /** Last known tools for diffing */
80
+ private lastKnownTools;
76
81
  constructor(client: WSClient, clientId?: string);
77
82
  get document(): Doc<Record<string, unknown>>;
78
83
  get domainRevision(): string | null;
@@ -140,6 +145,64 @@ declare class Session {
140
145
  * ```
141
146
  */
142
147
  publishTools(tools: ToolWithHandler[], revision?: string): Promise<PublishToolsResult>;
148
+ /**
149
+ * Publish a single effect (tool) to the sandbox.
150
+ *
151
+ * Adds the effect to the local registry and re-publishes the
152
+ * full tool catalog to the server. If an effect with the same
153
+ * name already exists, it is replaced.
154
+ *
155
+ * @param effect - The effect (tool) to publish
156
+ * @returns PublishToolsResult with domainRevision
157
+ *
158
+ * @example
159
+ * ```typescript
160
+ * await env.publishEffect({
161
+ * name: 'get_bio',
162
+ * description: 'Get biography of an author',
163
+ * className: 'author',
164
+ * inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
165
+ * handler: async (id, params) => ({ bio: `Bio of ${id}` }),
166
+ * });
167
+ * ```
168
+ */
169
+ publishEffect(effect: ToolWithHandler): Promise<PublishToolsResult>;
170
+ /**
171
+ * Publish multiple effects (tools) at once.
172
+ *
173
+ * Adds all effects to the local registry and re-publishes the
174
+ * full tool catalog in a single RPC call.
175
+ *
176
+ * @param effects - Array of effects to publish
177
+ * @returns PublishToolsResult with domainRevision
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * await env.publishEffects([
182
+ * { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
183
+ * { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
184
+ * ]);
185
+ * ```
186
+ */
187
+ publishEffects(effects: ToolWithHandler[]): Promise<PublishToolsResult>;
188
+ /**
189
+ * Remove an effect by name and re-publish the remaining catalog.
190
+ *
191
+ * @param name - The name of the effect to remove
192
+ * @returns PublishToolsResult with domainRevision
193
+ */
194
+ unpublishEffect(name: string): Promise<PublishToolsResult>;
195
+ /**
196
+ * Remove all effects and publish an empty catalog.
197
+ *
198
+ * @returns PublishToolsResult with domainRevision
199
+ */
200
+ unpublishAllEffects(): Promise<PublishToolsResult>;
201
+ /**
202
+ * Internal: re-publish the full effect catalog to the server.
203
+ * Called after any mutation to the effects Map.
204
+ */
205
+ private _syncEffects;
143
206
  /**
144
207
  * Submit a job to execute code in the sandbox.
145
208
  *
@@ -166,6 +229,17 @@ declare class Session {
166
229
  * Respond to a prompt request from the sandbox
167
230
  */
168
231
  answerPrompt(promptId: string, answer: unknown): Promise<void>;
232
+ /**
233
+ * Get the current list of available tools.
234
+ * Consolidates tools from all connected clients.
235
+ */
236
+ getTools(): ToolInfo[];
237
+ /**
238
+ * Subscribe to tool changes (added, removed, updated).
239
+ * @param callback - Function called with change events
240
+ * @returns Unsubscribe function
241
+ */
242
+ onToolsChanged(callback: (event: ToolsChangedEvent) => void): () => void;
169
243
  /**
170
244
  * Get the current domain state and available tools
171
245
  */
@@ -208,6 +282,10 @@ declare class Session {
208
282
  private setupToolInvokeHandler;
209
283
  private setupEventHandlers;
210
284
  private emit;
285
+ /**
286
+ * Check for changes in the tool catalog and emit 'tools:changed' if needed
287
+ */
288
+ private checkForToolChanges;
211
289
  }
212
290
 
213
291
  /**
@@ -219,6 +297,7 @@ declare class Session {
219
297
  * 3. Publish tools via `publishTools()` (instance methods, static methods, global tools — with typed I/O)
220
298
  * 4. Submit jobs via `submitJob()` that import auto-generated typed classes from `./sandbox-tools`
221
299
  * 5. Execute GraphQL queries via `graphql()` (authenticated automatically)
300
+ * 6. List available tools via `getTools()` and listen for updates via `onToolsChanged()`
222
301
  *
223
302
  * Tool calls from the sandbox automatically invoke your handlers via reverse-RPC.
224
303
  *
@@ -485,11 +564,49 @@ declare class Environment extends Session {
485
564
  * ```
486
565
  */
487
566
  publishTools(tools: ToolWithHandler[], revision?: string): Promise<PublishToolsResult>;
567
+ /**
568
+ * Publish a single effect (tool) incrementally.
569
+ *
570
+ * Adds the effect to the local registry and re-publishes the
571
+ * full tool catalog to the server. If an effect with the same
572
+ * name already exists, it is replaced.
573
+ *
574
+ * @example
575
+ * ```typescript
576
+ * await env.publishEffect({
577
+ * name: 'get_bio',
578
+ * description: 'Get biography',
579
+ * inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
580
+ * handler: async (params) => ({ bio: 'Hello' }),
581
+ * });
582
+ * ```
583
+ */
584
+ publishEffect(effect: ToolWithHandler): Promise<PublishToolsResult>;
585
+ /**
586
+ * Publish multiple effects (tools) at once.
587
+ *
588
+ * Adds all effects to the local registry and re-publishes the
589
+ * full tool catalog in a single RPC call.
590
+ */
591
+ publishEffects(effects: ToolWithHandler[]): Promise<PublishToolsResult>;
592
+ /**
593
+ * Remove an effect by name and re-publish the remaining catalog.
594
+ */
595
+ unpublishEffect(name: string): Promise<PublishToolsResult>;
596
+ /**
597
+ * Remove all effects and publish an empty catalog.
598
+ */
599
+ unpublishAllEffects(): Promise<PublishToolsResult>;
488
600
  }
489
601
  declare class Granular {
490
602
  private apiKey;
491
603
  private apiUrl;
492
604
  private httpUrl;
605
+ private WebSocketCtor?;
606
+ /** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
607
+ private sandboxEffects;
608
+ /** Active environments tracker: sandboxId → Environment[] */
609
+ private activeEnvironments;
493
610
  /**
494
611
  * Create a new Granular client
495
612
  * @param options - Client configuration
@@ -547,6 +664,34 @@ declare class Granular {
547
664
  * ```
548
665
  */
549
666
  connect(options: ConnectOptions): Promise<Environment>;
667
+ /**
668
+ * Register an effect (tool) for a specific sandbox.
669
+ *
670
+ * The effect will be automatically published to:
671
+ * 1. Any currently active environments for this sandbox
672
+ * 2. Any new environments created/connected for this sandbox
673
+ *
674
+ * @param sandboxNameOrId - The name or ID of the sandbox
675
+ * @param effect - The tool definition and handler
676
+ */
677
+ registerEffect(sandboxNameOrId: string, effect: ToolWithHandler): Promise<void>;
678
+ /**
679
+ * Register multiple effects (tools) for a specific sandbox.
680
+ *
681
+ * batch version of `registerEffect`.
682
+ */
683
+ registerEffects(sandboxNameOrId: string, effects: ToolWithHandler[]): Promise<void>;
684
+ /**
685
+ * Unregister an effect from a sandbox.
686
+ *
687
+ * Removes it from the local registry and unpublishes it from
688
+ * all active environments.
689
+ */
690
+ unregisterEffect(sandboxNameOrId: string, name: string): Promise<void>;
691
+ /**
692
+ * Unregister all effects for a sandbox.
693
+ */
694
+ unregisterAllEffects(sandboxNameOrId: string): Promise<void>;
550
695
  /**
551
696
  * Find a sandbox by name or create it if it doesn't exist
552
697
  */
@@ -613,4 +758,4 @@ declare class Granular {
613
758
  private request;
614
759
  }
615
760
 
616
- export { AssignmentListResponse, ConnectOptions, CreateEnvironmentData, CreatePermissionProfileData, CreateSandboxData, DefineRelationshipOptions, DeleteResponse, DomainState, Environment, EnvironmentData, Granular, GranularOptions, GraphQLResult, InstanceToolHandler, Job, ManifestContent, ModelRef, PermissionProfile, PublishToolsResult, RecordObjectOptions, RecordObjectResult, RecordUserOptions, RelationshipInfo, Sandbox, SandboxListResponse, Session, Subject, ToolHandler, ToolWithHandler, User, WSClient, WSClientOptions };
761
+ export { AssignmentListResponse, ConnectOptions, CreateEnvironmentData, CreatePermissionProfileData, CreateSandboxData, DefineRelationshipOptions, DeleteResponse, DomainState, Environment, EnvironmentData, Granular, GranularOptions, GraphQLResult, InstanceToolHandler, Job, ManifestContent, ModelRef, PermissionProfile, PublishToolsResult, RecordObjectOptions, RecordObjectResult, RecordUserOptions, RelationshipInfo, Sandbox, SandboxListResponse, Session, Subject, ToolHandler, ToolInfo, ToolWithHandler, ToolsChangedEvent, User, WSClient, WSClientOptions };