@granular-software/sdk 0.3.4 → 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 +37 -14
- package/dist/adapters/anthropic.d.mts +1 -1
- package/dist/adapters/anthropic.d.ts +1 -1
- package/dist/adapters/langchain.d.mts +1 -1
- package/dist/adapters/langchain.d.ts +1 -1
- package/dist/adapters/mastra.d.mts +1 -1
- package/dist/adapters/mastra.d.ts +1 -1
- package/dist/adapters/openai.d.mts +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/cli/index.js +203 -45
- package/dist/index.d.mts +68 -162
- package/dist/index.d.ts +68 -162
- package/dist/index.js +519 -269
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +519 -269
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D5B8WlF4.d.mts → types-C0AVRsVR.d.mts} +92 -31
- package/dist/{types-D5B8WlF4.d.ts → types-C0AVRsVR.d.ts} +92 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,26 @@ bun add @granular-software/sdk
|
|
|
27
27
|
npm install @granular-software/sdk
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Endpoint Modes
|
|
31
|
+
|
|
32
|
+
By default, the SDK resolves endpoints like this:
|
|
33
|
+
|
|
34
|
+
- Local mode (`NODE_ENV=development`): `ws://localhost:8787/granular`
|
|
35
|
+
- Production mode (default): `wss://api.granular.dev/v2/ws`
|
|
36
|
+
|
|
37
|
+
Overrides:
|
|
38
|
+
|
|
39
|
+
- SDK option: `endpointMode: 'local' | 'production'`
|
|
40
|
+
- SDK option: `apiUrl: 'ws://... | wss://...'` (highest priority)
|
|
41
|
+
- Env: `GRANULAR_ENDPOINT_MODE=local|production`
|
|
42
|
+
- Env: `GRANULAR_API_URL=...` (highest priority)
|
|
43
|
+
|
|
44
|
+
CLI overrides:
|
|
45
|
+
|
|
46
|
+
- `granular --local <command>`
|
|
47
|
+
- `granular --prod <command>`
|
|
48
|
+
- `granular --env local|production <command>`
|
|
49
|
+
|
|
30
50
|
## Quick Start
|
|
31
51
|
|
|
32
52
|
```typescript
|
|
@@ -92,8 +112,8 @@ await env.recordObject({
|
|
|
92
112
|
fields: { name: 'Acme Corp', email: 'billing@acme.com', tier: 'enterprise' },
|
|
93
113
|
});
|
|
94
114
|
|
|
95
|
-
// 5.
|
|
96
|
-
await env.
|
|
115
|
+
// 5. Register live effect handlers for effects already declared in the build manifest
|
|
116
|
+
await granular.registerEffects(env.sandboxId, [
|
|
97
117
|
{
|
|
98
118
|
name: 'get_billing_summary',
|
|
99
119
|
description: 'Get billing summary for a customer',
|
|
@@ -114,8 +134,9 @@ await env.publishTools([
|
|
|
114
134
|
},
|
|
115
135
|
required: ['total', 'invoices'],
|
|
116
136
|
},
|
|
117
|
-
handler: async (customerId: string, params: any) => {
|
|
137
|
+
handler: async (customerId: string, params: any, ctx: any) => {
|
|
118
138
|
// customerId comes from `this.id` in sandbox code
|
|
139
|
+
console.log('Running for subject:', ctx.user.subjectId);
|
|
119
140
|
return { total: 4250.00, invoices: 3, period: params?.period || 'current' };
|
|
120
141
|
},
|
|
121
142
|
},
|
|
@@ -143,17 +164,19 @@ const job = await env.submitJob(`
|
|
|
143
164
|
const result = await job.result;
|
|
144
165
|
```
|
|
145
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
|
+
|
|
146
169
|
## Core Flow
|
|
147
170
|
|
|
148
171
|
```
|
|
149
|
-
|
|
172
|
+
declare effects in build manifest → connect() → recordObject() → registerEffects() → submitJob()
|
|
150
173
|
```
|
|
151
174
|
|
|
152
175
|
1. **`recordUser()`** — Register a user and their permission profiles
|
|
153
176
|
2. **`connect()`** — Connect to a sandbox, returning an `Environment`
|
|
154
177
|
3. **`applyManifest()`** — Define your domain ontology (classes, properties, relationships)
|
|
155
178
|
4. **`recordObject()`** — Create/update instances of your classes with fields and relationships
|
|
156
|
-
5. **`
|
|
179
|
+
5. **`granular.registerEffects()`** — Register sandbox-scoped live handlers for effects declared in the build manifest
|
|
157
180
|
6. **`submitJob()`** — Execute code in the sandbox that uses the auto-generated typed classes
|
|
158
181
|
|
|
159
182
|
## Defining the Domain Ontology
|
|
@@ -229,12 +252,12 @@ const lotr = await env.recordObject({
|
|
|
229
252
|
|
|
230
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.
|
|
231
254
|
|
|
232
|
-
##
|
|
255
|
+
## Effect Definitions
|
|
233
256
|
|
|
234
|
-
|
|
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:
|
|
235
258
|
|
|
236
259
|
```typescript
|
|
237
|
-
await env.
|
|
260
|
+
await granular.registerEffects(env.sandboxId, [
|
|
238
261
|
// Instance method: called as `tolkien.get_bio({ detailed: true })`
|
|
239
262
|
// Handler receives (objectId, params)
|
|
240
263
|
{
|
|
@@ -256,7 +279,7 @@ await env.publishTools([
|
|
|
256
279
|
},
|
|
257
280
|
required: ['bio'],
|
|
258
281
|
},
|
|
259
|
-
handler: async (id: string, params: any) => {
|
|
282
|
+
handler: async (id: string, params: any, ctx: any) => {
|
|
260
283
|
return { bio: `Biography of ${id}`, source: 'database' };
|
|
261
284
|
},
|
|
262
285
|
},
|
|
@@ -277,7 +300,7 @@ await env.publishTools([
|
|
|
277
300
|
type: 'object',
|
|
278
301
|
properties: { results: { type: 'array' } },
|
|
279
302
|
},
|
|
280
|
-
handler: async (params: any) => {
|
|
303
|
+
handler: async (params: any, ctx: any) => {
|
|
281
304
|
return { results: [`Found: ${params.query}`] };
|
|
282
305
|
},
|
|
283
306
|
},
|
|
@@ -296,7 +319,7 @@ await env.publishTools([
|
|
|
296
319
|
type: 'object',
|
|
297
320
|
properties: { results: { type: 'array' } },
|
|
298
321
|
},
|
|
299
|
-
handler: async (params: any) => {
|
|
322
|
+
handler: async (params: any, ctx: any) => {
|
|
300
323
|
return { results: [`Result for: ${params.query}`] };
|
|
301
324
|
},
|
|
302
325
|
},
|
|
@@ -450,8 +473,8 @@ Returns relationship definitions for a given class.
|
|
|
450
473
|
### `environment.listRelated(modelPath, submodelPath)`
|
|
451
474
|
Lists related instances through a relationship.
|
|
452
475
|
|
|
453
|
-
### `
|
|
454
|
-
|
|
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`).
|
|
455
478
|
|
|
456
479
|
### `environment.submitJob(code)`
|
|
457
480
|
Submits code to be executed in the sandbox. The code imports typed classes from `./sandbox-tools`.
|
|
@@ -463,7 +486,7 @@ Get auto-generated TypeScript class declarations. Pass this to LLMs to help them
|
|
|
463
486
|
Execute a GraphQL query against the environment's graph. Authenticated automatically.
|
|
464
487
|
|
|
465
488
|
### `environment.on(event, handler)`
|
|
466
|
-
Listen for events: `'
|
|
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.
|
|
467
490
|
|
|
468
491
|
### `Environment.toGraphPath(className, id)`
|
|
469
492
|
Convert a class name + real-world ID to a unique graph path (`{className}_{id}`).
|
package/dist/cli/index.js
CHANGED
|
@@ -5384,6 +5384,57 @@ var {
|
|
|
5384
5384
|
|
|
5385
5385
|
// src/cli/config.ts
|
|
5386
5386
|
var import_dotenv = __toESM(require_main());
|
|
5387
|
+
|
|
5388
|
+
// src/endpoints.ts
|
|
5389
|
+
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5390
|
+
var PRODUCTION_API_URL = "wss://api.granular.dev/v2/ws";
|
|
5391
|
+
var LOCAL_AUTH_URL = "http://localhost:3000";
|
|
5392
|
+
var PRODUCTION_AUTH_URL = "https://app.granular.software";
|
|
5393
|
+
function readEnv(name) {
|
|
5394
|
+
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5395
|
+
return process.env[name];
|
|
5396
|
+
}
|
|
5397
|
+
function normalizeMode(value) {
|
|
5398
|
+
if (!value) return void 0;
|
|
5399
|
+
const normalized = value.trim().toLowerCase();
|
|
5400
|
+
if (normalized === "local") return "local";
|
|
5401
|
+
if (normalized === "prod" || normalized === "production") return "production";
|
|
5402
|
+
if (normalized === "auto") return "auto";
|
|
5403
|
+
return void 0;
|
|
5404
|
+
}
|
|
5405
|
+
function isTruthy(value) {
|
|
5406
|
+
if (!value) return false;
|
|
5407
|
+
const normalized = value.trim().toLowerCase();
|
|
5408
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
5409
|
+
}
|
|
5410
|
+
function resolveEndpointMode(explicitMode) {
|
|
5411
|
+
const explicit = normalizeMode(explicitMode);
|
|
5412
|
+
if (explicit === "local" || explicit === "production") {
|
|
5413
|
+
return explicit;
|
|
5414
|
+
}
|
|
5415
|
+
const envMode = normalizeMode(readEnv("GRANULAR_ENDPOINT_MODE") || readEnv("GRANULAR_ENV"));
|
|
5416
|
+
if (envMode === "local" || envMode === "production") {
|
|
5417
|
+
return envMode;
|
|
5418
|
+
}
|
|
5419
|
+
if (isTruthy(readEnv("GRANULAR_USE_LOCAL_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_LOCAL"))) {
|
|
5420
|
+
return "local";
|
|
5421
|
+
}
|
|
5422
|
+
if (isTruthy(readEnv("GRANULAR_USE_PRODUCTION_ENDPOINTS")) || isTruthy(readEnv("GRANULAR_PROD"))) {
|
|
5423
|
+
return "production";
|
|
5424
|
+
}
|
|
5425
|
+
return readEnv("NODE_ENV") === "development" ? "local" : "production";
|
|
5426
|
+
}
|
|
5427
|
+
function resolveApiUrl(explicitApiUrl, mode) {
|
|
5428
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
|
|
5429
|
+
}
|
|
5430
|
+
function resolveAuthUrl(explicitAuthUrl, mode) {
|
|
5431
|
+
if (explicitAuthUrl) {
|
|
5432
|
+
return explicitAuthUrl;
|
|
5433
|
+
}
|
|
5434
|
+
return resolveEndpointMode(mode) === "local" ? LOCAL_AUTH_URL : PRODUCTION_AUTH_URL;
|
|
5435
|
+
}
|
|
5436
|
+
|
|
5437
|
+
// src/cli/config.ts
|
|
5387
5438
|
var MANIFEST_FILE = "granular.json";
|
|
5388
5439
|
var RC_FILE = ".granularrc";
|
|
5389
5440
|
var ENV_LOCAL_FILE = ".env.local";
|
|
@@ -5453,16 +5504,17 @@ function loadApiKey() {
|
|
|
5453
5504
|
return void 0;
|
|
5454
5505
|
}
|
|
5455
5506
|
function loadApiUrl() {
|
|
5507
|
+
if (process.env.GRANULAR_API_URL) return process.env.GRANULAR_API_URL;
|
|
5508
|
+
const modeOverride = process.env.GRANULAR_ENDPOINT_MODE;
|
|
5509
|
+
if (modeOverride === "local" || modeOverride === "production" || modeOverride === "prod") {
|
|
5510
|
+
return resolveApiUrl(void 0, modeOverride === "prod" ? "production" : modeOverride);
|
|
5511
|
+
}
|
|
5456
5512
|
const rc = readRcFile();
|
|
5457
5513
|
if (rc.apiUrl) return rc.apiUrl;
|
|
5458
|
-
|
|
5459
|
-
return "https://cf-api-gateway.arthur6084.workers.dev/granular";
|
|
5514
|
+
return resolveApiUrl();
|
|
5460
5515
|
}
|
|
5461
5516
|
function loadAuthUrl() {
|
|
5462
|
-
|
|
5463
|
-
return process.env.GRANULAR_AUTH_URL;
|
|
5464
|
-
}
|
|
5465
|
-
return "https://app.granular.software";
|
|
5517
|
+
return resolveAuthUrl(process.env.GRANULAR_AUTH_URL);
|
|
5466
5518
|
}
|
|
5467
5519
|
function saveApiKey(apiKey) {
|
|
5468
5520
|
const envLocalPath = getEnvLocalPath();
|
|
@@ -5500,7 +5552,105 @@ function ensureGitignore() {
|
|
|
5500
5552
|
fs__namespace.writeFileSync(gitignorePath, content + section, "utf-8");
|
|
5501
5553
|
}
|
|
5502
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
|
+
}
|
|
5503
5652
|
function createDefaultManifest(name) {
|
|
5653
|
+
const classNames = ["note", "tag"];
|
|
5504
5654
|
return {
|
|
5505
5655
|
manifest: {
|
|
5506
5656
|
schemaVersion: 2,
|
|
@@ -5539,7 +5689,8 @@ function createDefaultManifest(name) {
|
|
|
5539
5689
|
leftIsMany: true,
|
|
5540
5690
|
rightIsMany: true
|
|
5541
5691
|
}
|
|
5542
|
-
}
|
|
5692
|
+
},
|
|
5693
|
+
...createDefaultEffectOperations(classNames)
|
|
5543
5694
|
]
|
|
5544
5695
|
}]
|
|
5545
5696
|
}
|
|
@@ -5710,7 +5861,7 @@ var ApiClient = class {
|
|
|
5710
5861
|
return await this.createPermissionProfile(sandboxId, {
|
|
5711
5862
|
name: "default",
|
|
5712
5863
|
rules: {
|
|
5713
|
-
|
|
5864
|
+
effects: { allow: ["*"] },
|
|
5714
5865
|
resources: { allow: ["*"] }
|
|
5715
5866
|
}
|
|
5716
5867
|
});
|
|
@@ -7611,37 +7762,12 @@ async function main() {
|
|
|
7611
7762
|
apiUrl: process.env.GRANULAR_API_URL ?? API_URL,
|
|
7612
7763
|
});
|
|
7613
7764
|
|
|
7614
|
-
|
|
7615
|
-
log(\`Recording user \${userId}\`);
|
|
7616
|
-
const user = await granular.recordUser({
|
|
7617
|
-
userId,
|
|
7618
|
-
name: 'Effects Host',
|
|
7619
|
-
permissions: ['default'],
|
|
7620
|
-
});
|
|
7621
|
-
|
|
7622
|
-
log(\`Connecting to sandbox \${SANDBOX_ID}\`);
|
|
7623
|
-
const env = await granular.connect({ sandbox: SANDBOX_ID, user, clientId: 'effects-host' });
|
|
7624
|
-
|
|
7625
|
-
log('Waiting for graph container to be ready...');
|
|
7626
|
-
for (let i = 0; i < 6; i++) {
|
|
7627
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
7628
|
-
try {
|
|
7629
|
-
const probe = await env.graphql(\`query { model(path: "class") { path } }\`);
|
|
7630
|
-
if ((probe as any)?.data?.model?.path) {
|
|
7631
|
-
log(\`Graph ready after \${(i + 1) * 5}s\`);
|
|
7632
|
-
break;
|
|
7633
|
-
}
|
|
7634
|
-
} catch {
|
|
7635
|
-
log(\`Not ready yet (\${(i + 1) * 5}s)...\`);
|
|
7636
|
-
}
|
|
7637
|
-
}
|
|
7638
|
-
|
|
7639
|
-
log('Publishing effects...');
|
|
7765
|
+
log(\`Registering live effects for sandbox \${SANDBOX_ID}\`);
|
|
7640
7766
|
${mockDataBlock}
|
|
7641
7767
|
|
|
7642
|
-
await
|
|
7768
|
+
await granular.registerEffects(SANDBOX_ID, ${effectsArray});
|
|
7643
7769
|
|
|
7644
|
-
log('Effects
|
|
7770
|
+
log('Effects registered. Process kept alive for simulator. Press Ctrl+C to exit.');
|
|
7645
7771
|
await new Promise(() => {});
|
|
7646
7772
|
}
|
|
7647
7773
|
|
|
@@ -7806,6 +7932,7 @@ async function initCommand(projectName, options) {
|
|
|
7806
7932
|
hint("granular simulate", "opens app.granular.software/simulator for this sandbox");
|
|
7807
7933
|
console.log();
|
|
7808
7934
|
}
|
|
7935
|
+
var DEFAULT_LOCAL_API_KEY = "gn_sk_tenant_default_principal_local_e2e_00000000";
|
|
7809
7936
|
function promptSecret(question) {
|
|
7810
7937
|
const rl = readline__namespace.createInterface({ input: process.stdin, output: process.stdout });
|
|
7811
7938
|
return new Promise((resolve) => {
|
|
@@ -7831,6 +7958,9 @@ function maskApiKey(apiKey) {
|
|
|
7831
7958
|
if (apiKey.length < 10) return `${apiKey}...`;
|
|
7832
7959
|
return `${apiKey.substring(0, 10)}...`;
|
|
7833
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
|
+
}
|
|
7834
7964
|
async function loginCommand(options = {}) {
|
|
7835
7965
|
printHeader();
|
|
7836
7966
|
const existing = loadApiKey();
|
|
@@ -7842,7 +7972,10 @@ async function loginCommand(options = {}) {
|
|
|
7842
7972
|
const apiUrl = loadApiUrl();
|
|
7843
7973
|
let apiKey = options.apiKey?.trim();
|
|
7844
7974
|
if (!apiKey) {
|
|
7845
|
-
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) {
|
|
7846
7979
|
dim("Get your API key at https://app.granular.software/w/default/api-keys");
|
|
7847
7980
|
console.log();
|
|
7848
7981
|
apiKey = await promptSecret("Enter your API key");
|
|
@@ -8396,7 +8529,7 @@ ${manifest.description}`);
|
|
|
8396
8529
|
lines.push(`- [Integration Guide](#integration-guide)`);
|
|
8397
8530
|
lines.push(` - [1. Initialize & Connect](#1-initialize--connect)`);
|
|
8398
8531
|
lines.push(` - [2. Record Objects](#2-record-objects)`);
|
|
8399
|
-
lines.push(` - [3.
|
|
8532
|
+
lines.push(` - [3. Declare And Register Effects](#3-declare-and-register-effects)`);
|
|
8400
8533
|
lines.push(` - [4. Submit Jobs](#4-submit-jobs)`);
|
|
8401
8534
|
lines.push(`
|
|
8402
8535
|
## Getting Started`);
|
|
@@ -8512,11 +8645,11 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8512
8645
|
lines.push(`*(No classes defined in manifest)*`);
|
|
8513
8646
|
}
|
|
8514
8647
|
lines.push(`
|
|
8515
|
-
### 3.
|
|
8516
|
-
lines.push(`
|
|
8648
|
+
### 3. Declare And Register Effects`);
|
|
8649
|
+
lines.push(`Declare effects in your manifest with \`withEffect\`, then register live handlers at sandbox scope.`);
|
|
8517
8650
|
lines.push(`
|
|
8518
8651
|
\`\`\`typescript`);
|
|
8519
|
-
lines.push(`await
|
|
8652
|
+
lines.push(`await granular.registerEffects('your-sandbox-id', [`);
|
|
8520
8653
|
if (classes.length > 0) {
|
|
8521
8654
|
const cls = classes[0];
|
|
8522
8655
|
lines.push(` // Instance method on ${cls.name}`);
|
|
@@ -8526,8 +8659,9 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8526
8659
|
lines.push(` className: '${cls.name}',`);
|
|
8527
8660
|
lines.push(` inputSchema: { type: 'object', properties: { maxLength: { type: 'number' } } },`);
|
|
8528
8661
|
lines.push(` outputSchema: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] },`);
|
|
8529
|
-
lines.push(` handler: async (id, params) => {`);
|
|
8662
|
+
lines.push(` handler: async (id, params, ctx) => {`);
|
|
8530
8663
|
lines.push(` // 'id' is the real-world ID of the ${cls.name}`);
|
|
8664
|
+
lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
|
|
8531
8665
|
lines.push(` return { summary: \`Summary for \${id}\` };`);
|
|
8532
8666
|
lines.push(` },`);
|
|
8533
8667
|
lines.push(` },`);
|
|
@@ -8539,17 +8673,19 @@ console.log('Connected to:', env.environmentId);`);
|
|
|
8539
8673
|
lines.push(` static: true,`);
|
|
8540
8674
|
lines.push(` inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },`);
|
|
8541
8675
|
lines.push(` outputSchema: { type: 'object', properties: { ids: { type: 'array' } }, required: ['ids'] },`);
|
|
8542
|
-
lines.push(` handler: async (params) => {`);
|
|
8676
|
+
lines.push(` handler: async (params, ctx) => {`);
|
|
8677
|
+
lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
|
|
8543
8678
|
lines.push(` return { ids: [] };`);
|
|
8544
8679
|
lines.push(` },`);
|
|
8545
8680
|
lines.push(` },`);
|
|
8546
8681
|
}
|
|
8547
|
-
lines.push(` // Global
|
|
8682
|
+
lines.push(` // Global effect`);
|
|
8548
8683
|
lines.push(` {`);
|
|
8549
8684
|
lines.push(` name: 'notify',`);
|
|
8550
8685
|
lines.push(` description: 'Send notification',`);
|
|
8551
8686
|
lines.push(` inputSchema: { type: 'object', properties: { msg: { type: 'string' } }, required: ['msg'] },`);
|
|
8552
|
-
lines.push(` handler: async (params) => {`);
|
|
8687
|
+
lines.push(` handler: async (params, ctx) => {`);
|
|
8688
|
+
lines.push(` console.log('Invoked for user', ctx.user.subjectId);`);
|
|
8553
8689
|
lines.push(` console.log('Notification:', params.msg);`);
|
|
8554
8690
|
lines.push(` },`);
|
|
8555
8691
|
lines.push(` },`);
|
|
@@ -8623,6 +8759,28 @@ async function simulateCommand(sandboxIdArg) {
|
|
|
8623
8759
|
var VERSION = "0.2.0";
|
|
8624
8760
|
var program2 = new Command();
|
|
8625
8761
|
program2.name("granular").description("Build and deploy AI sandboxes from code").version(VERSION, "-v, --version");
|
|
8762
|
+
program2.option("--local", "Use local endpoints (localhost)").option("--prod", "Use production endpoints").option("--env <target>", "Endpoint target: local|production");
|
|
8763
|
+
program2.hook("preAction", () => {
|
|
8764
|
+
const opts = program2.opts();
|
|
8765
|
+
const normalizedEnv = opts.env?.trim().toLowerCase();
|
|
8766
|
+
if (opts.local && opts.prod) {
|
|
8767
|
+
error("Cannot use --local and --prod at the same time.");
|
|
8768
|
+
process.exit(1);
|
|
8769
|
+
}
|
|
8770
|
+
if (normalizedEnv && !["local", "production", "prod"].includes(normalizedEnv)) {
|
|
8771
|
+
error('Invalid --env value. Use "local" or "production".');
|
|
8772
|
+
process.exit(1);
|
|
8773
|
+
}
|
|
8774
|
+
let mode;
|
|
8775
|
+
if (opts.local || normalizedEnv === "local") {
|
|
8776
|
+
mode = "local";
|
|
8777
|
+
} else if (opts.prod || normalizedEnv === "production" || normalizedEnv === "prod") {
|
|
8778
|
+
mode = "production";
|
|
8779
|
+
}
|
|
8780
|
+
if (mode) {
|
|
8781
|
+
process.env.GRANULAR_ENDPOINT_MODE = mode;
|
|
8782
|
+
}
|
|
8783
|
+
});
|
|
8626
8784
|
program2.command("init [project-name]").description("Initialize a new Granular project").option("--skip-build", "Skip the initial build step").action(async (projectName, opts) => {
|
|
8627
8785
|
try {
|
|
8628
8786
|
await initCommand(projectName, { skipBuild: opts.skipBuild });
|