@granular-software/sdk 0.4.1 → 0.4.2

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
@@ -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
@@ -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
  }
@@ -5762,7 +5861,7 @@ var ApiClient = class {
5762
5861
  return await this.createPermissionProfile(sandboxId, {
5763
5862
  name: "default",
5764
5863
  rules: {
5765
- tools: { allow: ["*"] },
5864
+ effects: { allow: ["*"] },
5766
5865
  resources: { allow: ["*"] }
5767
5866
  }
5768
5867
  });
@@ -7663,37 +7762,12 @@ async function main() {
7663
7762
  apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
7664
7763
  });
7665
7764
 
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...');
7765
+ log(\`Registering live effects for sandbox \${SANDBOX_ID}\`);
7692
7766
  ${mockDataBlock}
7693
7767
 
7694
- await env.publishEffects(${effectsArray});
7768
+ await granular.registerEffects(SANDBOX_ID, ${effectsArray});
7695
7769
 
7696
- log('Effects published. Process kept alive for simulator. Press Ctrl+C to exit.');
7770
+ log('Effects registered. Process kept alive for simulator. Press Ctrl+C to exit.');
7697
7771
  await new Promise(() => {});
7698
7772
  }
7699
7773
 
@@ -7858,6 +7932,7 @@ async function initCommand(projectName, options) {
7858
7932
  hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
7859
7933
  console.log();
7860
7934
  }
7935
+ var DEFAULT_LOCAL_API_KEY = "gn_sk_tenant_default_principal_local_e2e_00000000";
7861
7936
  function promptSecret(question) {
7862
7937
  const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
7863
7938
  return new Promise((resolve) => {
@@ -7883,6 +7958,9 @@ function maskApiKey(apiKey) {
7883
7958
  if (apiKey.length < 10) return `${apiKey}...`;
7884
7959
  return `${apiKey.substring(0, 10)}...`;
7885
7960
  }
7961
+ function isLocalApiUrl(apiUrl) {
7962
+ 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:");
7963
+ }
7886
7964
  async function loginCommand(options = {}) {
7887
7965
  printHeader();
7888
7966
  const existing = loadApiKey();
@@ -7894,7 +7972,10 @@ async function loginCommand(options = {}) {
7894
7972
  const apiUrl = loadApiUrl();
7895
7973
  let apiKey = options.apiKey?.trim();
7896
7974
  if (!apiKey) {
7897
- if (options.manual) {
7975
+ if (!options.manual && isLocalApiUrl(apiUrl)) {
7976
+ apiKey = existing?.trim() || process.env.GRANULAR_LOCAL_API_KEY?.trim() || DEFAULT_LOCAL_API_KEY;
7977
+ info("Using local Granular API key for localhost development.");
7978
+ } else if (options.manual) {
7898
7979
  dim("Get your API key at https://app.granular.software/w/default/api-keys");
7899
7980
  console.log();
7900
7981
  apiKey = await promptSecret("Enter your API key");
@@ -8448,7 +8529,7 @@ ${manifest.description}`);
8448
8529
  lines.push(`- [Integration Guide](#integration-guide)`);
8449
8530
  lines.push(` - [1. Initialize & Connect](#1-initialize--connect)`);
8450
8531
  lines.push(` - [2. Record Objects](#2-record-objects)`);
8451
- lines.push(` - [3. Publish Tools](#3-publish-tools)`);
8532
+ lines.push(` - [3. Declare And Register Effects](#3-declare-and-register-effects)`);
8452
8533
  lines.push(` - [4. Submit Jobs](#4-submit-jobs)`);
8453
8534
  lines.push(`
8454
8535
  ## Getting Started`);
@@ -8564,11 +8645,11 @@ console.log('Connected to:', env.environmentId);`);
8564
8645
  lines.push(`*(No classes defined in manifest)*`);
8565
8646
  }
8566
8647
  lines.push(`
8567
- ### 3. Publish Tools`);
8568
- lines.push(`Define tools that your agent can use properly typed.`);
8648
+ ### 3. Declare And Register Effects`);
8649
+ lines.push(`Declare effects in your manifest with \`withEffect\`, then register live handlers at sandbox scope.`);
8569
8650
  lines.push(`
8570
8651
  \`\`\`typescript`);
8571
- lines.push(`await env.publishTools([`);
8652
+ lines.push(`await granular.registerEffects('your-sandbox-id', [`);
8572
8653
  if (classes.length > 0) {
8573
8654
  const cls = classes[0];
8574
8655
  lines.push(` // Instance method on ${cls.name}`);
@@ -8578,8 +8659,9 @@ console.log('Connected to:', env.environmentId);`);
8578
8659
  lines.push(` className: '${cls.name}',`);
8579
8660
  lines.push(` inputSchema: { type: 'object', properties: { maxLength: { type: 'number' } } },`);
8580
8661
  lines.push(` outputSchema: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] },`);
8581
- lines.push(` handler: async (id, params) => {`);
8662
+ lines.push(` handler: async (id, params, ctx) => {`);
8582
8663
  lines.push(` // 'id' is the real-world ID of the ${cls.name}`);
8664
+ lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
8583
8665
  lines.push(` return { summary: \`Summary for \${id}\` };`);
8584
8666
  lines.push(` },`);
8585
8667
  lines.push(` },`);
@@ -8591,17 +8673,19 @@ console.log('Connected to:', env.environmentId);`);
8591
8673
  lines.push(` static: true,`);
8592
8674
  lines.push(` inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },`);
8593
8675
  lines.push(` outputSchema: { type: 'object', properties: { ids: { type: 'array' } }, required: ['ids'] },`);
8594
- lines.push(` handler: async (params) => {`);
8676
+ lines.push(` handler: async (params, ctx) => {`);
8677
+ lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
8595
8678
  lines.push(` return { ids: [] };`);
8596
8679
  lines.push(` },`);
8597
8680
  lines.push(` },`);
8598
8681
  }
8599
- lines.push(` // Global tool`);
8682
+ lines.push(` // Global effect`);
8600
8683
  lines.push(` {`);
8601
8684
  lines.push(` name: 'notify',`);
8602
8685
  lines.push(` description: 'Send notification',`);
8603
8686
  lines.push(` inputSchema: { type: 'object', properties: { msg: { type: 'string' } }, required: ['msg'] },`);
8604
- lines.push(` handler: async (params) => {`);
8687
+ lines.push(` handler: async (params, ctx) => {`);
8688
+ lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
8605
8689
  lines.push(` console.log('Notification:', params.msg);`);
8606
8690
  lines.push(` },`);
8607
8691
  lines.push(` },`);