@granular-software/sdk 0.4.5 → 0.4.7

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
@@ -8,7 +8,7 @@ The official TypeScript SDK for Granular.
8
8
 
9
9
  - 🏗️ **Domain Ontology** — Define classes, properties, and relationships as a typed graph
10
10
  - 🔧 **Class-Based Tools** — Attach instance methods, static methods, and global tools with typed I/O
11
- - 🤖 **Domain Synthesis** — Auto-generated TypeScript classes with `find()`, relationship accessors, and typed methods
11
+ - 🤖 **Domain Synthesis** — Auto-generated TypeScript classes with `get({ path })`, `list()`, relationship accessors, and typed methods
12
12
  - 🔒 **Secure Execution** — Run AI-generated code in isolated sandboxes
13
13
  - 👥 **User Permissions** — Control what each user can access
14
14
  - ⚡ **Real-time** — WebSocket-based streaming and events
@@ -55,16 +55,16 @@ import { Granular, type ManifestContent } from '@granular-software/sdk';
55
55
 
56
56
  const granular = new Granular({ apiKey: process.env.GRANULAR_API_KEY });
57
57
 
58
- // 1. Record user with permissions
59
- const user = await granular.recordUser({
58
+ // 1. Connect to sandbox for one of your app users
59
+ const env = await granular.connect({
60
+ sandbox: 'my-sandbox',
60
61
  userId: 'user_123',
61
62
  permissions: ['agent'],
63
+ name: 'Jane Doe', // optional
64
+ email: 'jane@example.com', // optional
62
65
  });
63
66
 
64
- // 2. Connect to sandbox
65
- const env = await granular.connect({ sandbox: 'my-sandbox', user });
66
-
67
- // 3. Define your domain ontology
67
+ // 2. Define your domain ontology
68
68
  const manifest: ManifestContent = {
69
69
  schemaVersion: 2,
70
70
  name: 'my-app',
@@ -105,7 +105,7 @@ const manifest: ManifestContent = {
105
105
 
106
106
  await env.applyManifest(manifest);
107
107
 
108
- // 4. Record object instances
108
+ // 3. Record object instances
109
109
  await env.recordObject({
110
110
  className: 'customer',
111
111
  id: 'cust_42',
@@ -113,7 +113,7 @@ await env.recordObject({
113
113
  fields: { name: 'Acme Corp', email: 'billing@acme.com', tier: 'enterprise' },
114
114
  });
115
115
 
116
- // 5. Register live effect handlers for effects already declared in the build manifest
116
+ // 4. Register live effect handlers for effects already declared in the build manifest
117
117
  await granular.registerEffects(env.sandboxId, [
118
118
  {
119
119
  name: 'get_billing_summary',
@@ -147,8 +147,10 @@ await granular.registerEffects(env.sandboxId, [
147
147
  const job = await env.submitJob(`
148
148
  import { Customer } from './sandbox-tools';
149
149
 
150
- // Find returns a typed Customer instance with loaded field values
151
- const acme = await Customer.find({ id: 'cust_42' });
150
+ // list() discovers instances; get({ path }) hydrates a specific graph object
151
+ const customers = await Customer.list();
152
+ const acme = customers.find((customer) => customer.name === 'Acme Corp');
153
+ if (!acme) throw new Error('Customer not found');
152
154
  console.log(acme.name); // "Acme Corp"
153
155
  console.log(acme.email); // "billing@acme.com"
154
156
 
@@ -170,11 +172,11 @@ Effects must be declared ahead of time in the sandbox build manifest with `withE
170
172
  ## Core Flow
171
173
 
172
174
  ```
173
- declare effects in build manifest → connect() → recordObject() → registerEffects() → submitJob()
175
+ declare effects in build manifest → connect({ userId }) → recordObject() → registerEffects() → submitJob()
174
176
  ```
175
177
 
176
- 1. **`recordUser()`** — Register a user and their permission profiles
177
- 2. **`connect()`** — Connect to a sandbox, returning an `Environment`
178
+ 1. **`connect()`** — Connect to a sandbox for a given `userId`, returning an `Environment`
179
+ 2. **`recordUser()`** — Optional explicit user upsert when you want the returned `granularId`
178
180
  3. **`applyManifest()`** — Define your domain ontology (classes, properties, relationships)
179
181
  4. **`recordObject()`** — Create/update instances of your classes with fields and relationships
180
182
  5. **`granular.registerEffects()`** — Register sandbox-scoped live handlers for effects declared in the build manifest
@@ -230,9 +232,15 @@ await env.applyManifest(manifest);
230
232
 
231
233
  ## Recording Object Instances
232
234
 
233
- After defining the ontology, populate it with data:
235
+ After defining the ontology, connect as a user and populate it with data:
234
236
 
235
237
  ```typescript
238
+ const env = await granular.connect({
239
+ sandbox: 'library-app',
240
+ userId: 'user_123',
241
+ permissions: ['agent'],
242
+ });
243
+
236
244
  const tolkien = await env.recordObject({
237
245
  className: 'author',
238
246
  id: 'tolkien', // Real-world ID (unique per class)
@@ -340,8 +348,11 @@ export declare class Author {
340
348
 
341
349
  constructor(id: string, fields?: Record<string, any>);
342
350
 
343
- /** Find an Author by ID */
344
- static find(query: { id: string }): Promise<Author | null>;
351
+ /** Get a cached Author by graph path, hydrating from the graph when needed */
352
+ static get(query: { path: string; refresh?: boolean }): Promise<Author | null>;
353
+
354
+ /** List known Author instances */
355
+ static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Author[]>;
345
356
 
346
357
  /** Get biography of an author */
347
358
  get_bio(input?: { detailed?: boolean }): Promise<{ bio: string; source?: string }>;
@@ -359,7 +370,9 @@ export declare class Book {
359
370
  readonly isbn: string;
360
371
  readonly published_year: number;
361
372
 
362
- static find(query: { id: string }): Promise<Book | null>;
373
+ static get(query: { path: string; refresh?: boolean }): Promise<Book | null>;
374
+
375
+ static list(query?: { limit?: number; saveAs?: string; refresh?: boolean }): Promise<Book[]>;
363
376
 
364
377
  /** Navigate to author (many_to_one) */
365
378
  get_author(): Promise<Author | null>;
@@ -374,7 +387,9 @@ The LLM or user writes code against these typed classes:
374
387
  ```typescript
375
388
  import { Author, Book, global_search } from './sandbox-tools';
376
389
 
377
- const tolkien = await Author.find({ id: 'tolkien' });
390
+ const authors = await Author.list();
391
+ const tolkien = authors.find((author) => author.name === 'J.R.R. Tolkien');
392
+ if (!tolkien) throw new Error('Author not found');
378
393
  console.log(tolkien.name); // "J.R.R. Tolkien"
379
394
 
380
395
  const bio = await tolkien.get_bio({ detailed: true });
package/dist/cli/index.js CHANGED
@@ -7771,7 +7771,31 @@ async function main() {
7771
7771
  await granular.registerEffects(SANDBOX_ID, ${effectsArray});
7772
7772
 
7773
7773
  log('Effects registered. Process kept alive for simulator. Press Ctrl+C to exit.');
7774
- await new Promise(() => {});
7774
+
7775
+ const keepAliveTimer = setInterval(() => {
7776
+ // Keep an active event-loop handle so Bun/Node does not exit immediately.
7777
+ }, 60_000);
7778
+
7779
+ await new Promise<void>((resolve) => {
7780
+ let shuttingDown = false;
7781
+
7782
+ const shutdown = async (signal: string) => {
7783
+ if (shuttingDown) return;
7784
+ shuttingDown = true;
7785
+ clearInterval(keepAliveTimer);
7786
+ log(\`Received \${signal}. Shutting down effects host...\`);
7787
+ try {
7788
+ await granular.disconnectEffects(SANDBOX_ID);
7789
+ } catch (error) {
7790
+ console.warn('[Effects] Failed to disconnect live effects cleanly:', error);
7791
+ }
7792
+ resolve();
7793
+ process.exit(0);
7794
+ };
7795
+
7796
+ process.once('SIGINT', () => { void shutdown('SIGINT'); });
7797
+ process.once('SIGTERM', () => { void shutdown('SIGTERM'); });
7798
+ });
7775
7799
  }
7776
7800
 
7777
7801
  main().catch((err) => {
@@ -8598,17 +8622,12 @@ const granular = new Granular({`);
8598
8622
  lines.push(` ...(process.env.GRANULAR_TOKEN ? { token: process.env.GRANULAR_TOKEN } : { apiKey: process.env.GRANULAR_API_KEY! }),`);
8599
8623
  lines.push(`});`);
8600
8624
  lines.push(`
8601
- // 1. Record the user (creates or updates identity)`);
8602
- lines.push(`const user = await granular.recordUser({`);
8603
- lines.push(` userId: 'user_123', // Your internal ID`);
8604
- lines.push(` email: 'user@example.com',`);
8605
- lines.push(` permissions: ['default'], // Permission profile`);
8606
- lines.push(`});`);
8607
- lines.push(`
8608
- // 2. Connect to the sandbox`);
8625
+ // 1. Connect to the sandbox for one of your app users`);
8609
8626
  lines.push(`const env = await granular.connect({`);
8610
8627
  lines.push(` sandbox: '${sandboxId}',`);
8611
- lines.push(` user,`);
8628
+ lines.push(` userId: 'user_123', // Your app's user ID`);
8629
+ lines.push(` email: 'user@example.com', // optional`);
8630
+ lines.push(` permissions: ['default'], // Permission profile`);
8612
8631
  lines.push(`});`);
8613
8632
  lines.push(`
8614
8633
  console.log('Connected to:', env.environmentId);`);
@@ -8619,6 +8638,12 @@ console.log('Connected to:', env.environmentId);`);
8619
8638
  if (classes.length > 0) {
8620
8639
  lines.push(`
8621
8640
  \`\`\`typescript`);
8641
+ lines.push(`const env = await granular.connect({`);
8642
+ lines.push(` sandbox: '${sandboxId}',`);
8643
+ lines.push(` userId: 'user_123',`);
8644
+ lines.push(` permissions: ['default'],`);
8645
+ lines.push(`});`);
8646
+ lines.push(``);
8622
8647
  for (const cls of classes) {
8623
8648
  const exampleFields = {};
8624
8649
  for (const [k, v] of Object.entries(cls.fields)) {
@@ -8714,12 +8739,16 @@ console.log('Connected to:', env.environmentId);`);
8714
8739
  if (classes.length > 0) {
8715
8740
  const cls = classes[0];
8716
8741
  const ClassName = cls.name.charAt(0).toUpperCase() + cls.name.slice(1);
8717
- lines.push(` // 1. Find object`);
8718
- lines.push(` const item = await ${ClassName}.find({ id: 'unique_${cls.name}_id' });`);
8719
- lines.push(` console.log('Found:', item.id);`);
8742
+ lines.push(` // 1. List objects`);
8743
+ lines.push(` const items = await ${ClassName}.list({ limit: 10, saveAs: '${cls.name}_items' });`);
8744
+ lines.push(` const item = items[0] ?? null;`);
8745
+ lines.push(` console.log('Loaded:', item?.id);`);
8720
8746
  lines.push(``);
8721
8747
  lines.push(` // 2. Call instance method`);
8722
- lines.push(` const summary = await item.summarize({ maxLength: 100 });`);
8748
+ lines.push(` if (item) {`);
8749
+ lines.push(` const summary = await item.summarize({ maxLength: 100 });`);
8750
+ lines.push(` console.log(summary);`);
8751
+ lines.push(` }`);
8723
8752
  lines.push(``);
8724
8753
  lines.push(` // 3. Call static method`);
8725
8754
  lines.push(` const results = await ${ClassName}.search({ query: 'test' });`);
package/dist/index.d.mts CHANGED
@@ -40,9 +40,13 @@ type GranularAuth = string;
40
40
  * A user/subject object returned from recordUser()
41
41
  */
42
42
  interface User {
43
- /** Internal subject ID */
43
+ /** Internal Granular user identifier */
44
+ granularId: string;
45
+ /** External user identifier from your app */
46
+ userId: string;
47
+ /** @deprecated Use `granularId` instead */
44
48
  subjectId: string;
45
- /** External identity ID (e.g. Auth0 ID) */
49
+ /** @deprecated Use `userId` instead */
46
50
  identityId: string;
47
51
  /** User's display name */
48
52
  name?: string;
@@ -68,6 +72,10 @@ interface RecordUserOptions {
68
72
  * Subject as returned from the API
69
73
  */
70
74
  interface Subject {
75
+ /** Internal Granular user identifier */
76
+ granularId: string;
77
+ /** External user identifier from your app */
78
+ userId: string;
71
79
  subjectId: string;
72
80
  tenantId: string;
73
81
  identityId: string;
@@ -83,8 +91,24 @@ interface Subject {
83
91
  interface ConnectOptions {
84
92
  /** The sandbox name or ID to connect to */
85
93
  sandbox: string;
86
- /** The user to connect as (from recordUser()) */
87
- user: User;
94
+ /**
95
+ * External user identifier from your app. This is the primary input for
96
+ * connecting to a sandbox and the only required user field in the common case.
97
+ */
98
+ userId?: string;
99
+ /**
100
+ * Internal Granular user identifier. Optional fallback when you only know
101
+ * the Granular-side ID for an existing subject.
102
+ */
103
+ granularId?: string;
104
+ /** Optional display name used when upserting the user */
105
+ name?: string;
106
+ /** Optional email used when upserting the user */
107
+ email?: string;
108
+ /** Permission profile IDs or names to ensure before connecting */
109
+ permissions?: string[];
110
+ /** Backwards-compatible user object returned from recordUser() */
111
+ user?: User;
88
112
  /** Optional stable client ID. Defaults to `client_${Date.now()}`. Use a fixed
89
113
  * value for long-lived effect hosts so tool catalogs don't accumulate. */
90
114
  clientId?: string;
@@ -274,6 +298,8 @@ interface EffectHandlerContext {
274
298
  principalId?: string;
275
299
  permissionProfileId?: string;
276
300
  user: {
301
+ granularId?: string;
302
+ userId?: string;
277
303
  subjectId: string;
278
304
  identityId?: string;
279
305
  principalId?: string;
@@ -904,7 +930,8 @@ declare class Session {
904
930
  * ```typescript
905
931
  * import { Author, Book, global_search } from './sandbox-tools';
906
932
  *
907
- * const tolkien = await Author.find({ id: 'tolkien' });
933
+ * const authors = await Author.list({ limit: 10, saveAs: 'recent_authors' });
934
+ * const tolkien = await Author.get({ path: 'author_tolkien' });
908
935
  * const bio = await tolkien.get_bio({ detailed: true });
909
936
  * const books = await tolkien.get_books();
910
937
  * ```
@@ -1016,6 +1043,8 @@ declare class Environment extends Session {
1016
1043
  get sandboxId(): string;
1017
1044
  /** The subject ID */
1018
1045
  get subjectId(): string;
1046
+ /** Internal Granular user identifier for this environment */
1047
+ get granularId(): string;
1019
1048
  /** The permission profile ID */
1020
1049
  get permissionProfileId(): string;
1021
1050
  /** The GraphQL API endpoint URL */
@@ -1329,7 +1358,7 @@ declare class Granular {
1329
1358
  * Records/upserts a user and prepares them for sandbox connections
1330
1359
  *
1331
1360
  * @param options - User options
1332
- * @returns The user object to pass to connect()
1361
+ * @returns The recorded user with both `userId` and `granularId`
1333
1362
  *
1334
1363
  * @example
1335
1364
  * ```typescript
@@ -1341,6 +1370,7 @@ declare class Granular {
1341
1370
  * ```
1342
1371
  */
1343
1372
  recordUser(options: RecordUserOptions): Promise<User>;
1373
+ private resolveConnectUser;
1344
1374
  /**
1345
1375
  * Connect to a sandbox and establish a real-time environment session.
1346
1376
  *
@@ -1353,14 +1383,10 @@ declare class Granular {
1353
1383
  *
1354
1384
  * @example
1355
1385
  * ```typescript
1356
- * const user = await granular.recordUser({
1357
- * userId: 'user_123',
1358
- * permissions: ['agent'],
1359
- * });
1360
- *
1361
1386
  * const environment = await granular.connect({
1362
1387
  * sandbox: 'my-sandbox',
1363
- * user,
1388
+ * userId: 'user_123',
1389
+ * permissions: ['agent'],
1364
1390
  * });
1365
1391
  *
1366
1392
  * await granular.registerEffect('my-sandbox', {
@@ -1410,6 +1436,13 @@ declare class Granular {
1410
1436
  * sandbox-scoped live catalog.
1411
1437
  */
1412
1438
  unregisterEffect(sandboxNameOrId: string, name: string): Promise<void>;
1439
+ /**
1440
+ * Disconnect one sandbox-scoped effect host, or all of them when no sandbox is provided.
1441
+ *
1442
+ * This is primarily useful for long-lived helper processes such as generated
1443
+ * `granular-effects.ts` scripts that need to shut down cleanly on SIGINT/SIGTERM.
1444
+ */
1445
+ disconnectEffects(sandboxNameOrId?: string): Promise<void>;
1413
1446
  /**
1414
1447
  * Unregister all effects for a sandbox.
1415
1448
  */
package/dist/index.d.ts CHANGED
@@ -40,9 +40,13 @@ type GranularAuth = string;
40
40
  * A user/subject object returned from recordUser()
41
41
  */
42
42
  interface User {
43
- /** Internal subject ID */
43
+ /** Internal Granular user identifier */
44
+ granularId: string;
45
+ /** External user identifier from your app */
46
+ userId: string;
47
+ /** @deprecated Use `granularId` instead */
44
48
  subjectId: string;
45
- /** External identity ID (e.g. Auth0 ID) */
49
+ /** @deprecated Use `userId` instead */
46
50
  identityId: string;
47
51
  /** User's display name */
48
52
  name?: string;
@@ -68,6 +72,10 @@ interface RecordUserOptions {
68
72
  * Subject as returned from the API
69
73
  */
70
74
  interface Subject {
75
+ /** Internal Granular user identifier */
76
+ granularId: string;
77
+ /** External user identifier from your app */
78
+ userId: string;
71
79
  subjectId: string;
72
80
  tenantId: string;
73
81
  identityId: string;
@@ -83,8 +91,24 @@ interface Subject {
83
91
  interface ConnectOptions {
84
92
  /** The sandbox name or ID to connect to */
85
93
  sandbox: string;
86
- /** The user to connect as (from recordUser()) */
87
- user: User;
94
+ /**
95
+ * External user identifier from your app. This is the primary input for
96
+ * connecting to a sandbox and the only required user field in the common case.
97
+ */
98
+ userId?: string;
99
+ /**
100
+ * Internal Granular user identifier. Optional fallback when you only know
101
+ * the Granular-side ID for an existing subject.
102
+ */
103
+ granularId?: string;
104
+ /** Optional display name used when upserting the user */
105
+ name?: string;
106
+ /** Optional email used when upserting the user */
107
+ email?: string;
108
+ /** Permission profile IDs or names to ensure before connecting */
109
+ permissions?: string[];
110
+ /** Backwards-compatible user object returned from recordUser() */
111
+ user?: User;
88
112
  /** Optional stable client ID. Defaults to `client_${Date.now()}`. Use a fixed
89
113
  * value for long-lived effect hosts so tool catalogs don't accumulate. */
90
114
  clientId?: string;
@@ -274,6 +298,8 @@ interface EffectHandlerContext {
274
298
  principalId?: string;
275
299
  permissionProfileId?: string;
276
300
  user: {
301
+ granularId?: string;
302
+ userId?: string;
277
303
  subjectId: string;
278
304
  identityId?: string;
279
305
  principalId?: string;
@@ -904,7 +930,8 @@ declare class Session {
904
930
  * ```typescript
905
931
  * import { Author, Book, global_search } from './sandbox-tools';
906
932
  *
907
- * const tolkien = await Author.find({ id: 'tolkien' });
933
+ * const authors = await Author.list({ limit: 10, saveAs: 'recent_authors' });
934
+ * const tolkien = await Author.get({ path: 'author_tolkien' });
908
935
  * const bio = await tolkien.get_bio({ detailed: true });
909
936
  * const books = await tolkien.get_books();
910
937
  * ```
@@ -1016,6 +1043,8 @@ declare class Environment extends Session {
1016
1043
  get sandboxId(): string;
1017
1044
  /** The subject ID */
1018
1045
  get subjectId(): string;
1046
+ /** Internal Granular user identifier for this environment */
1047
+ get granularId(): string;
1019
1048
  /** The permission profile ID */
1020
1049
  get permissionProfileId(): string;
1021
1050
  /** The GraphQL API endpoint URL */
@@ -1329,7 +1358,7 @@ declare class Granular {
1329
1358
  * Records/upserts a user and prepares them for sandbox connections
1330
1359
  *
1331
1360
  * @param options - User options
1332
- * @returns The user object to pass to connect()
1361
+ * @returns The recorded user with both `userId` and `granularId`
1333
1362
  *
1334
1363
  * @example
1335
1364
  * ```typescript
@@ -1341,6 +1370,7 @@ declare class Granular {
1341
1370
  * ```
1342
1371
  */
1343
1372
  recordUser(options: RecordUserOptions): Promise<User>;
1373
+ private resolveConnectUser;
1344
1374
  /**
1345
1375
  * Connect to a sandbox and establish a real-time environment session.
1346
1376
  *
@@ -1353,14 +1383,10 @@ declare class Granular {
1353
1383
  *
1354
1384
  * @example
1355
1385
  * ```typescript
1356
- * const user = await granular.recordUser({
1357
- * userId: 'user_123',
1358
- * permissions: ['agent'],
1359
- * });
1360
- *
1361
1386
  * const environment = await granular.connect({
1362
1387
  * sandbox: 'my-sandbox',
1363
- * user,
1388
+ * userId: 'user_123',
1389
+ * permissions: ['agent'],
1364
1390
  * });
1365
1391
  *
1366
1392
  * await granular.registerEffect('my-sandbox', {
@@ -1410,6 +1436,13 @@ declare class Granular {
1410
1436
  * sandbox-scoped live catalog.
1411
1437
  */
1412
1438
  unregisterEffect(sandboxNameOrId: string, name: string): Promise<void>;
1439
+ /**
1440
+ * Disconnect one sandbox-scoped effect host, or all of them when no sandbox is provided.
1441
+ *
1442
+ * This is primarily useful for long-lived helper processes such as generated
1443
+ * `granular-effects.ts` scripts that need to shut down cleanly on SIGINT/SIGTERM.
1444
+ */
1445
+ disconnectEffects(sandboxNameOrId?: string): Promise<void>;
1413
1446
  /**
1414
1447
  * Unregister all effects for a sandbox.
1415
1448
  */