@granular-software/sdk 0.4.1 → 0.4.3

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 CHANGED
@@ -32,7 +32,7 @@ npm install @granular-software/sdk
32
32
  By default, the SDK resolves endpoints like this:
33
33
 
34
34
  - Local mode (`NODE_ENV=development`): `ws://localhost:8787/granular`
35
- - Production mode (default): `wss://api.granular.dev/v2/ws`
35
+ - Production mode (default): `wss://cf-api-gateway.arthur6084.workers.dev/granular`
36
36
 
37
37
  Overrides:
38
38
 
@@ -112,8 +112,8 @@ await env.recordObject({
112
112
  fields: { name: 'Acme Corp', email: 'billing@acme.com', tier: 'enterprise' },
113
113
  });
114
114
 
115
- // 5. Publish tools with typed input AND output schemas
116
- await env.publishTools([
115
+ // 5. Register live effect handlers for effects already declared in the build manifest
116
+ await granular.registerEffects(env.sandboxId, [
117
117
  {
118
118
  name: 'get_billing_summary',
119
119
  description: 'Get billing summary for a customer',
@@ -134,8 +134,9 @@ await env.publishTools([
134
134
  },
135
135
  required: ['total', 'invoices'],
136
136
  },
137
- handler: async (customerId: string, params: any) => {
137
+ handler: async (customerId: string, params: any, ctx: any) => {
138
138
  // customerId comes from `this.id` in sandbox code
139
+ console.log('Running for subject:', ctx.user.subjectId);
139
140
  return { total: 4250.00, invoices: 3, period: params?.period || 'current' };
140
141
  },
141
142
  },
@@ -163,17 +164,19 @@ const job = await env.submitJob(`
163
164
  const result = await job.result;
164
165
  ```
165
166
 
167
+ Effects must be declared ahead of time in the sandbox build manifest with `withEffect`. Live registration only makes already-declared effects available at runtime.
168
+
166
169
  ## Core Flow
167
170
 
168
171
  ```
169
- recordUser() connect()applyManifest() → recordObject() → publishTools() → submitJob()
172
+ declare effects in build manifest connect() → recordObject() → registerEffects() → submitJob()
170
173
  ```
171
174
 
172
175
  1. **`recordUser()`** — Register a user and their permission profiles
173
176
  2. **`connect()`** — Connect to a sandbox, returning an `Environment`
174
177
  3. **`applyManifest()`** — Define your domain ontology (classes, properties, relationships)
175
178
  4. **`recordObject()`** — Create/update instances of your classes with fields and relationships
176
- 5. **`publishTools()`** — Publish tool schemas (with `inputSchema` + `outputSchema`) and register local handlers
179
+ 5. **`granular.registerEffects()`** — Register sandbox-scoped live handlers for effects declared in the build manifest
177
180
  6. **`submitJob()`** — Execute code in the sandbox that uses the auto-generated typed classes
178
181
 
179
182
  ## Defining the Domain Ontology
@@ -249,12 +252,12 @@ const lotr = await env.recordObject({
249
252
 
250
253
  > **Cross-class ID uniqueness**: Two objects of different classes can share the same real-world ID (e.g., an `author` "tolkien" and a `publisher` "tolkien"). The SDK derives unique graph paths internally (`author_tolkien`, `publisher_tolkien`) so they never collide.
251
254
 
252
- ## Tool Definitions
255
+ ## Effect Definitions
253
256
 
254
- Tools can be **instance methods**, **static methods**, or **global functions**. Both `inputSchema` and `outputSchema` use JSON Schema:
257
+ Effects are declared in the manifest with `withEffect`, then their live handlers are registered at sandbox scope. Effects can be **instance methods**, **static methods**, or **global functions**. Both `inputSchema` and `outputSchema` use JSON Schema:
255
258
 
256
259
  ```typescript
257
- await env.publishTools([
260
+ await granular.registerEffects(env.sandboxId, [
258
261
  // Instance method: called as `tolkien.get_bio({ detailed: true })`
259
262
  // Handler receives (objectId, params)
260
263
  {
@@ -276,7 +279,7 @@ await env.publishTools([
276
279
  },
277
280
  required: ['bio'],
278
281
  },
279
- handler: async (id: string, params: any) => {
282
+ handler: async (id: string, params: any, ctx: any) => {
280
283
  return { bio: `Biography of ${id}`, source: 'database' };
281
284
  },
282
285
  },
@@ -297,7 +300,7 @@ await env.publishTools([
297
300
  type: 'object',
298
301
  properties: { results: { type: 'array' } },
299
302
  },
300
- handler: async (params: any) => {
303
+ handler: async (params: any, ctx: any) => {
301
304
  return { results: [`Found: ${params.query}`] };
302
305
  },
303
306
  },
@@ -316,7 +319,7 @@ await env.publishTools([
316
319
  type: 'object',
317
320
  properties: { results: { type: 'array' } },
318
321
  },
319
- handler: async (params: any) => {
322
+ handler: async (params: any, ctx: any) => {
320
323
  return { results: [`Result for: ${params.query}`] };
321
324
  },
322
325
  },
@@ -470,8 +473,8 @@ Returns relationship definitions for a given class.
470
473
  ### `environment.listRelated(modelPath, submodelPath)`
471
474
  Lists related instances through a relationship.
472
475
 
473
- ### `environment.publishTools(tools)`
474
- Publishes tool schemas (with `inputSchema` + `outputSchema`) and registers handlers locally. Tools can be instance methods (`className` set, `static` omitted), static methods (`static: true`), or global functions (no `className`).
476
+ ### `granular.registerEffects(sandboxId, effects)`
477
+ Registers sandbox-scoped live handlers for effects declared in the sandbox build manifest. Effects can be instance methods (`className` set, `static` omitted), static methods (`static: true`), or global functions (no `className`).
475
478
 
476
479
  ### `environment.submitJob(code)`
477
480
  Submits code to be executed in the sandbox. The code imports typed classes from `./sandbox-tools`.
@@ -483,7 +486,7 @@ Get auto-generated TypeScript class declarations. Pass this to LLMs to help them
483
486
  Execute a GraphQL query against the environment's graph. Authenticated automatically.
484
487
 
485
488
  ### `environment.on(event, handler)`
486
- Listen for events: `'tool:invoke'`, `'tool:result'`, `'job:status'`, `'stdout'`, etc.
489
+ Listen for events: `'effect:invoke'`, `'effect:result'`, `'job:status'`, `'stdout'`, etc. Legacy `'tool:*'` aliases still exist internally but are no longer the primary model.
487
490
 
488
491
  ### `Environment.toGraphPath(className, id)`
489
492
  Convert a class name + real-world ID to a unique graph path (`{className}_{id}`).
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-BOPsFZYi.mjs';
1
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.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-BOPsFZYi.js';
1
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.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-BOPsFZYi.mjs';
2
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.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-BOPsFZYi.js';
2
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.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-BOPsFZYi.mjs';
2
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.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-BOPsFZYi.js';
2
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.js';
3
3
 
4
4
  /**
5
5
  * Mastra tool definition interface
@@ -1,4 +1,4 @@
1
- import { T as ToolWithHandler } from '../types-BOPsFZYi.mjs';
1
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.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-BOPsFZYi.js';
1
+ import { T as ToolWithHandler } from '../types-C0AVRsVR.js';
2
2
 
3
3
  /**
4
4
  * OpenAI tool call from chat completion response
package/dist/cli/index.js CHANGED
@@ -5387,7 +5387,7 @@ var import_dotenv = __toESM(require_main());
5387
5387
 
5388
5388
  // src/endpoints.ts
5389
5389
  var LOCAL_API_URL = "ws://localhost:8787/granular";
5390
- var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
5390
+ var PRODUCTION_API_URL = "wss://cf-api-gateway.arthur6084.workers.dev/granular";
5391
5391
  var LOCAL_AUTH_URL = "http://localhost:3000";
5392
5392
  var PRODUCTION_AUTH_URL = "https://app.granular.software";
5393
5393
  function readEnv(name) {
@@ -5552,7 +5552,105 @@ function ensureGitignore() {
5552
5552
  fs__namespace.writeFileSync(gitignorePath, content + section, "utf-8");
5553
5553
  }
5554
5554
  }
5555
+ function createDefaultEffectOperations(classNames) {
5556
+ const operations = classNames.flatMap((className) => {
5557
+ const searchName = `search_${className}s`;
5558
+ return [
5559
+ {
5560
+ withEffect: {
5561
+ name: "get_info",
5562
+ description: `Get details of a ${className} by ID`,
5563
+ attachedClass: className,
5564
+ isStatic: false,
5565
+ inputSchema: {
5566
+ type: "object",
5567
+ properties: {
5568
+ include_related: { type: "boolean", description: "Include related items" }
5569
+ }
5570
+ },
5571
+ outputSchema: {
5572
+ type: "object",
5573
+ properties: {
5574
+ id: { type: "string" },
5575
+ name: { type: "string" },
5576
+ related: { type: "array", items: { type: "object" } }
5577
+ }
5578
+ }
5579
+ }
5580
+ },
5581
+ {
5582
+ withEffect: {
5583
+ name: searchName,
5584
+ description: `Search ${className}s by keyword`,
5585
+ attachedClass: className,
5586
+ isStatic: true,
5587
+ inputSchema: {
5588
+ type: "object",
5589
+ properties: {
5590
+ query: { type: "string", description: "Search keyword" },
5591
+ limit: { type: "number", description: "Max results" }
5592
+ },
5593
+ required: ["query"]
5594
+ },
5595
+ outputSchema: {
5596
+ type: "object",
5597
+ properties: {
5598
+ query: { type: "string" },
5599
+ results: {
5600
+ type: "array",
5601
+ items: {
5602
+ type: "object",
5603
+ properties: {
5604
+ id: { type: "string" },
5605
+ name: { type: "string" }
5606
+ }
5607
+ }
5608
+ },
5609
+ total: { type: "number" }
5610
+ }
5611
+ }
5612
+ }
5613
+ }
5614
+ ];
5615
+ });
5616
+ operations.push({
5617
+ withEffect: {
5618
+ name: "full_text_search",
5619
+ description: "Search across all types",
5620
+ inputSchema: {
5621
+ type: "object",
5622
+ properties: {
5623
+ query: { type: "string", description: "Search query" },
5624
+ types: { type: "array", items: { type: "string" }, description: "Filter by type" }
5625
+ },
5626
+ required: ["query"]
5627
+ },
5628
+ outputSchema: {
5629
+ type: "object",
5630
+ properties: {
5631
+ query: { type: "string" },
5632
+ types: { type: "array", items: { type: "string" } },
5633
+ matches: {
5634
+ type: "array",
5635
+ items: {
5636
+ type: "object",
5637
+ properties: {
5638
+ type: { type: "string" },
5639
+ id: { type: "string" },
5640
+ label: { type: "string" },
5641
+ snippet: { type: "string" }
5642
+ }
5643
+ }
5644
+ },
5645
+ total: { type: "number" }
5646
+ }
5647
+ }
5648
+ }
5649
+ });
5650
+ return operations;
5651
+ }
5555
5652
  function createDefaultManifest(name) {
5653
+ const classNames = ["note", "tag"];
5556
5654
  return {
5557
5655
  manifest: {
5558
5656
  schemaVersion: 2,
@@ -5591,7 +5689,8 @@ function createDefaultManifest(name) {
5591
5689
  leftIsMany: true,
5592
5690
  rightIsMany: true
5593
5691
  }
5594
- }
5692
+ },
5693
+ ...createDefaultEffectOperations(classNames)
5595
5694
  ]
5596
5695
  }]
5597
5696
  }
@@ -5676,6 +5775,9 @@ var ApiClient = class {
5676
5775
  return false;
5677
5776
  }
5678
5777
  }
5778
+ async validateKeyOrThrow() {
5779
+ await this.request("/control/sandboxes");
5780
+ }
5679
5781
  // ── Sandboxes ──
5680
5782
  async listSandboxes() {
5681
5783
  const result = await this.request("/control/sandboxes");
@@ -5762,7 +5864,7 @@ var ApiClient = class {
5762
5864
  return await this.createPermissionProfile(sandboxId, {
5763
5865
  name: "default",
5764
5866
  rules: {
5765
- tools: { allow: ["*"] },
5867
+ effects: { allow: ["*"] },
5766
5868
  resources: { allow: ["*"] }
5767
5869
  }
5768
5870
  });
@@ -7663,37 +7765,12 @@ async function main() {
7663
7765
  apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
7664
7766
  });
7665
7767
 
7666
- const userId = 'effects_user';
7667
- log(\`Recording user \${userId}\`);
7668
- const user = await granular.recordUser({
7669
- userId,
7670
- name: 'Effects Host',
7671
- permissions: ['default'],
7672
- });
7673
-
7674
- log(\`Connecting to sandbox \${SANDBOX_ID}\`);
7675
- const env = await granular.connect({ sandbox: SANDBOX_ID, user, clientId: 'effects-host' });
7676
-
7677
- log('Waiting for graph container to be ready...');
7678
- for (let i = 0; i < 6; i++) {
7679
- await new Promise((r) => setTimeout(r, 5000));
7680
- try {
7681
- const probe = await env.graphql(\`query { model(path: "class") { path } }\`);
7682
- if ((probe as any)?.data?.model?.path) {
7683
- log(\`Graph ready after \${(i + 1) * 5}s\`);
7684
- break;
7685
- }
7686
- } catch {
7687
- log(\`Not ready yet (\${(i + 1) * 5}s)...\`);
7688
- }
7689
- }
7690
-
7691
- log('Publishing effects...');
7768
+ log(\`Registering live effects for sandbox \${SANDBOX_ID}\`);
7692
7769
  ${mockDataBlock}
7693
7770
 
7694
- await env.publishEffects(${effectsArray});
7771
+ await granular.registerEffects(SANDBOX_ID, ${effectsArray});
7695
7772
 
7696
- log('Effects published. Process kept alive for simulator. Press Ctrl+C to exit.');
7773
+ log('Effects registered. Process kept alive for simulator. Press Ctrl+C to exit.');
7697
7774
  await new Promise(() => {});
7698
7775
  }
7699
7776
 
@@ -7773,9 +7850,11 @@ async function initCommand(projectName, options) {
7773
7850
  const apiUrl = loadApiUrl();
7774
7851
  const api = new ApiClient(apiKey, apiUrl);
7775
7852
  const validating = spinner("Validating API key...");
7776
- const isValid = await api.validateKey();
7777
- if (!isValid) {
7778
- validating.fail(" Invalid API key. Please check your key and try again.");
7853
+ try {
7854
+ await api.validateKeyOrThrow();
7855
+ } catch (err) {
7856
+ const detail = err?.message ? ` (${err.message})` : "";
7857
+ validating.fail(` API key validation failed${detail}.`);
7779
7858
  process.exit(1);
7780
7859
  }
7781
7860
  validating.succeed(" API key validated.");
@@ -7858,6 +7937,7 @@ async function initCommand(projectName, options) {
7858
7937
  hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
7859
7938
  console.log();
7860
7939
  }
7940
+ var DEFAULT_LOCAL_API_KEY = "gn_sk_tenant_default_principal_local_e2e_00000000";
7861
7941
  function promptSecret(question) {
7862
7942
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7863
7943
  return new Promise((resolve) => {
@@ -7883,6 +7963,9 @@ function maskApiKey(apiKey) {
7883
7963
  if (apiKey.length < 10) return `${apiKey}...`;
7884
7964
  return `${apiKey.substring(0, 10)}...`;
7885
7965
  }
7966
+ function isLocalApiUrl(apiUrl) {
7967
+ return apiUrl.startsWith("ws://localhost:") || apiUrl.startsWith("wss://localhost:") || apiUrl.startsWith("ws://127.0.0.1:") || apiUrl.startsWith("wss://127.0.0.1:") || apiUrl.startsWith("http://localhost:") || apiUrl.startsWith("https://localhost:") || apiUrl.startsWith("http://127.0.0.1:") || apiUrl.startsWith("https://127.0.0.1:");
7968
+ }
7886
7969
  async function loginCommand(options = {}) {
7887
7970
  printHeader();
7888
7971
  const existing = loadApiKey();
@@ -7894,7 +7977,10 @@ async function loginCommand(options = {}) {
7894
7977
  const apiUrl = loadApiUrl();
7895
7978
  let apiKey = options.apiKey?.trim();
7896
7979
  if (!apiKey) {
7897
- if (options.manual) {
7980
+ if (!options.manual && isLocalApiUrl(apiUrl)) {
7981
+ apiKey = existing?.trim() || process.env.GRANULAR_LOCAL_API_KEY?.trim() || DEFAULT_LOCAL_API_KEY;
7982
+ info("Using local Granular API key for localhost development.");
7983
+ } else if (options.manual) {
7898
7984
  dim("Get your API key at https://app.granular.software/w/default/api-keys");
7899
7985
  console.log();
7900
7986
  apiKey = await promptSecret("Enter your API key");
@@ -7934,9 +8020,11 @@ async function loginCommand(options = {}) {
7934
8020
  }
7935
8021
  const spinner2 = spinner("Validating...");
7936
8022
  const api = new ApiClient(apiKey, apiUrl);
7937
- const valid = await api.validateKey();
7938
- if (!valid) {
7939
- spinner2.fail(" Invalid API key.");
8023
+ try {
8024
+ await api.validateKeyOrThrow();
8025
+ } catch (err) {
8026
+ const detail = err?.message ? ` (${err.message})` : "";
8027
+ spinner2.fail(` API key validation failed${detail}.`);
7940
8028
  process.exit(1);
7941
8029
  }
7942
8030
  saveApiKey(apiKey);
@@ -8448,7 +8536,7 @@ ${manifest.description}`);
8448
8536
  lines.push(`- [Integration Guide](#integration-guide)`);
8449
8537
  lines.push(` - [1. Initialize & Connect](#1-initialize--connect)`);
8450
8538
  lines.push(` - [2. Record Objects](#2-record-objects)`);
8451
- lines.push(` - [3. Publish Tools](#3-publish-tools)`);
8539
+ lines.push(` - [3. Declare And Register Effects](#3-declare-and-register-effects)`);
8452
8540
  lines.push(` - [4. Submit Jobs](#4-submit-jobs)`);
8453
8541
  lines.push(`
8454
8542
  ## Getting Started`);
@@ -8564,11 +8652,11 @@ console.log('Connected to:', env.environmentId);`);
8564
8652
  lines.push(`*(No classes defined in manifest)*`);
8565
8653
  }
8566
8654
  lines.push(`
8567
- ### 3. Publish Tools`);
8568
- lines.push(`Define tools that your agent can use properly typed.`);
8655
+ ### 3. Declare And Register Effects`);
8656
+ lines.push(`Declare effects in your manifest with \`withEffect\`, then register live handlers at sandbox scope.`);
8569
8657
  lines.push(`
8570
8658
  \`\`\`typescript`);
8571
- lines.push(`await env.publishTools([`);
8659
+ lines.push(`await granular.registerEffects('your-sandbox-id', [`);
8572
8660
  if (classes.length > 0) {
8573
8661
  const cls = classes[0];
8574
8662
  lines.push(` // Instance method on ${cls.name}`);
@@ -8578,8 +8666,9 @@ console.log('Connected to:', env.environmentId);`);
8578
8666
  lines.push(` className: '${cls.name}',`);
8579
8667
  lines.push(` inputSchema: { type: 'object', properties: { maxLength: { type: 'number' } } },`);
8580
8668
  lines.push(` outputSchema: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] },`);
8581
- lines.push(` handler: async (id, params) => {`);
8669
+ lines.push(` handler: async (id, params, ctx) => {`);
8582
8670
  lines.push(` // 'id' is the real-world ID of the ${cls.name}`);
8671
+ lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
8583
8672
  lines.push(` return { summary: \`Summary for \${id}\` };`);
8584
8673
  lines.push(` },`);
8585
8674
  lines.push(` },`);
@@ -8591,17 +8680,19 @@ console.log('Connected to:', env.environmentId);`);
8591
8680
  lines.push(` static: true,`);
8592
8681
  lines.push(` inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },`);
8593
8682
  lines.push(` outputSchema: { type: 'object', properties: { ids: { type: 'array' } }, required: ['ids'] },`);
8594
- lines.push(` handler: async (params) => {`);
8683
+ lines.push(` handler: async (params, ctx) => {`);
8684
+ lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
8595
8685
  lines.push(` return { ids: [] };`);
8596
8686
  lines.push(` },`);
8597
8687
  lines.push(` },`);
8598
8688
  }
8599
- lines.push(` // Global tool`);
8689
+ lines.push(` // Global effect`);
8600
8690
  lines.push(` {`);
8601
8691
  lines.push(` name: 'notify',`);
8602
8692
  lines.push(` description: 'Send notification',`);
8603
8693
  lines.push(` inputSchema: { type: 'object', properties: { msg: { type: 'string' } }, required: ['msg'] },`);
8604
- lines.push(` handler: async (params) => {`);
8694
+ lines.push(` handler: async (params, ctx) => {`);
8695
+ lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
8605
8696
  lines.push(` console.log('Notification:', params.msg);`);
8606
8697
  lines.push(` },`);
8607
8698
  lines.push(` },`);