@granular-software/sdk 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1801 @@
1
+ 'use strict';
2
+
3
+ var WebSocket = require('ws');
4
+ var Automerge = require('@automerge/automerge');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
27
+ var Automerge__namespace = /*#__PURE__*/_interopNamespace(Automerge);
28
+
29
+ // src/ws-client.ts
30
+ var WSClient = class {
31
+ ws = null;
32
+ url;
33
+ sessionId;
34
+ token;
35
+ messageQueue = [];
36
+ syncHandlers = [];
37
+ rpcHandlers = /* @__PURE__ */ new Map();
38
+ eventHandlers = /* @__PURE__ */ new Map();
39
+ nextRpcId = 1;
40
+ doc = Automerge__namespace.init();
41
+ syncState = Automerge__namespace.initSyncState();
42
+ reconnectTimer = null;
43
+ isExplicitlyDisconnected = false;
44
+ constructor(options) {
45
+ this.url = options.url;
46
+ this.sessionId = options.sessionId;
47
+ this.token = options.token;
48
+ }
49
+ /**
50
+ * Connect to the WebSocket server
51
+ * @returns {Promise<void>} Resolves when connection is open
52
+ */
53
+ async connect() {
54
+ this.isExplicitlyDisconnected = false;
55
+ return new Promise((resolve, reject) => {
56
+ try {
57
+ const wsUrl = new URL(this.url);
58
+ wsUrl.searchParams.set("sessionId", this.sessionId);
59
+ wsUrl.searchParams.set("token", this.token);
60
+ this.ws = new WebSocket__default.default(wsUrl.toString());
61
+ this.ws.on("open", () => {
62
+ this.emit("open", {});
63
+ resolve();
64
+ });
65
+ this.ws.on("message", (data) => {
66
+ try {
67
+ const message = JSON.parse(data.toString());
68
+ this.handleMessage(message);
69
+ } catch (error) {
70
+ console.error("[Granular] Failed to parse message:", error);
71
+ }
72
+ });
73
+ this.ws.on("error", (error) => {
74
+ this.emit("error", error);
75
+ if (this.ws?.readyState !== WebSocket__default.default.OPEN) {
76
+ reject(error);
77
+ }
78
+ });
79
+ this.ws.on("close", () => {
80
+ this.emit("close", {});
81
+ this.handleDisconnect();
82
+ });
83
+ } catch (error) {
84
+ reject(error);
85
+ }
86
+ });
87
+ }
88
+ handleDisconnect() {
89
+ this.ws = null;
90
+ if (!this.isExplicitlyDisconnected) {
91
+ this.reconnectTimer = setTimeout(() => {
92
+ console.log("[Granular] Attempting reconnect...");
93
+ this.connect().catch((e) => console.error("[Granular] Reconnect failed:", e));
94
+ }, 3e3);
95
+ }
96
+ }
97
+ handleMessage(message) {
98
+ if (typeof message !== "object" || message === null) return;
99
+ console.log("[Granular DEBUG] Received message:", JSON.stringify(message).slice(0, 500));
100
+ if ("type" in message && message.type === "sync") {
101
+ const syncMessage = message;
102
+ try {
103
+ let bytes;
104
+ const payload = syncMessage.message || syncMessage.data;
105
+ if (typeof payload === "string") {
106
+ const binaryString = atob(payload);
107
+ const len = binaryString.length;
108
+ bytes = new Uint8Array(len);
109
+ for (let i = 0; i < len; i++) {
110
+ bytes[i] = binaryString.charCodeAt(i);
111
+ }
112
+ } else if (Array.isArray(payload)) {
113
+ bytes = new Uint8Array(payload);
114
+ } else if (payload instanceof Uint8Array) {
115
+ bytes = payload;
116
+ } else {
117
+ return;
118
+ }
119
+ const [newDoc, newSyncState] = Automerge__namespace.receiveSyncMessage(
120
+ this.doc,
121
+ this.syncState,
122
+ bytes
123
+ );
124
+ this.doc = newDoc;
125
+ this.syncState = newSyncState;
126
+ this.emit("sync", this.doc);
127
+ } catch (e) {
128
+ }
129
+ return;
130
+ }
131
+ if ("type" in message && (message.type === "rpc_result" || message.type === "rpc_error")) {
132
+ const response = message;
133
+ const pending = this.messageQueue.find((q) => q.id === response.id);
134
+ if (pending) {
135
+ if (response.type === "rpc_error") {
136
+ pending.reject(
137
+ new Error(`RPC error: ${response.error?.message || "Unknown error"}`)
138
+ );
139
+ } else {
140
+ pending.resolve(response.result);
141
+ }
142
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== response.id);
143
+ }
144
+ return;
145
+ }
146
+ if ("type" in message && message.type === "rpc" && "method" in message && "id" in message) {
147
+ const request = message;
148
+ this.handleIncomingRpc(request).catch((error) => {
149
+ console.error("[Granular] Error handling incoming RPC:", error);
150
+ });
151
+ return;
152
+ }
153
+ if ("type" in message && typeof message.type === "string") {
154
+ this.emit(message.type, message);
155
+ }
156
+ }
157
+ /**
158
+ * Make an RPC call to the server
159
+ * @param {string} method - RPC method name
160
+ * @param {unknown} params - Request parameters
161
+ * @returns {Promise<unknown>} Response result
162
+ * @throws {Error} If connection is closed or timeout occurs
163
+ */
164
+ async call(method, params) {
165
+ if (!this.ws || this.ws.readyState !== WebSocket__default.default.OPEN) {
166
+ throw new Error("WebSocket not connected");
167
+ }
168
+ const id = `rpc-${this.nextRpcId++}`;
169
+ const request = {
170
+ type: "rpc",
171
+ method,
172
+ params,
173
+ id
174
+ };
175
+ return new Promise((resolve, reject) => {
176
+ this.messageQueue.push({ resolve, reject, id });
177
+ this.ws.send(JSON.stringify(request));
178
+ setTimeout(() => {
179
+ const pending = this.messageQueue.find((q) => q.id === id);
180
+ if (pending) {
181
+ this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
182
+ reject(new Error(`RPC timeout: ${method}`));
183
+ }
184
+ }, 3e4);
185
+ });
186
+ }
187
+ async handleIncomingRpc(request) {
188
+ const handler = this.rpcHandlers.get(request.method);
189
+ if (!handler) {
190
+ const errorResponse = {
191
+ type: "rpc_error",
192
+ id: request.id,
193
+ error: {
194
+ code: -32601,
195
+ message: `Method not found: ${request.method}`
196
+ }
197
+ };
198
+ this.ws?.send(JSON.stringify(errorResponse));
199
+ return;
200
+ }
201
+ try {
202
+ const result = await handler(request.params);
203
+ if (request.method !== "tool.invoke") {
204
+ const successResponse = {
205
+ type: "rpc_result",
206
+ id: request.id,
207
+ result
208
+ };
209
+ this.ws?.send(JSON.stringify(successResponse));
210
+ }
211
+ } catch (error) {
212
+ const errorResponse = {
213
+ type: "rpc_error",
214
+ id: request.id,
215
+ error: {
216
+ code: -32e3,
217
+ message: error instanceof Error ? error.message : "Unknown error"
218
+ }
219
+ };
220
+ this.ws?.send(JSON.stringify(errorResponse));
221
+ }
222
+ }
223
+ /**
224
+ * Subscribe to client events
225
+ * @param {string} event - Event name
226
+ * @param {Function} handler - Event handler
227
+ */
228
+ on(event, handler) {
229
+ if (!this.eventHandlers.has(event)) {
230
+ this.eventHandlers.set(event, []);
231
+ }
232
+ this.eventHandlers.get(event).push(handler);
233
+ }
234
+ /**
235
+ * Register an RPC handler for incoming server requests
236
+ * @param {string} method - RPC method name
237
+ * @param {Function} handler - Handler function
238
+ */
239
+ registerRpcHandler(method, handler) {
240
+ this.rpcHandlers.set(method, handler);
241
+ }
242
+ /**
243
+ * Unsubscribe from client events
244
+ * @param {string} event - Event name
245
+ * @param {Function} handler - Handler to remove
246
+ */
247
+ off(event, handler) {
248
+ const handlers = this.eventHandlers.get(event);
249
+ if (handlers) {
250
+ this.eventHandlers.set(
251
+ event,
252
+ handlers.filter((h) => h !== handler)
253
+ );
254
+ }
255
+ }
256
+ /**
257
+ * Emit an event locally
258
+ * @param {string} event - Event name
259
+ * @param params - Event data
260
+ */
261
+ emit(event, params) {
262
+ const handlers = this.eventHandlers.get(event);
263
+ if (handlers) {
264
+ handlers.forEach((handler) => handler(params));
265
+ }
266
+ }
267
+ /**
268
+ * Disconnect the WebSocket and clear state
269
+ */
270
+ disconnect() {
271
+ this.isExplicitlyDisconnected = true;
272
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
273
+ if (this.ws) {
274
+ this.ws.close();
275
+ this.ws = null;
276
+ }
277
+ this.messageQueue.forEach((q) => q.reject(new Error("Client explicitly disconnected")));
278
+ this.messageQueue = [];
279
+ this.rpcHandlers.clear();
280
+ this.emit("disconnect", {});
281
+ }
282
+ };
283
+
284
+ // src/session.ts
285
+ var Session = class {
286
+ client;
287
+ clientId;
288
+ jobsMap = /* @__PURE__ */ new Map();
289
+ eventListeners = /* @__PURE__ */ new Map();
290
+ toolHandlers = /* @__PURE__ */ new Map();
291
+ /** Tracks which tools are instance methods (className set, not static) */
292
+ instanceTools = /* @__PURE__ */ new Set();
293
+ currentDomainRevision = null;
294
+ constructor(client, clientId) {
295
+ this.client = client;
296
+ this.clientId = clientId || `client_${Date.now()}`;
297
+ this.setupEventHandlers();
298
+ this.setupToolInvokeHandler();
299
+ }
300
+ // --- Public API ---
301
+ get document() {
302
+ return this.client.doc;
303
+ }
304
+ get domainRevision() {
305
+ return this.currentDomainRevision;
306
+ }
307
+ /**
308
+ * Make a raw RPC call to the session's Durable Object.
309
+ *
310
+ * Use this when you need to call an RPC method that doesn't have a
311
+ * dedicated wrapper method on the Session/Environment class.
312
+ *
313
+ * @param method - RPC method name (e.g. 'domain.fetchPackagePart')
314
+ * @param params - Request parameters
315
+ * @returns The raw RPC response
316
+ *
317
+ * @example
318
+ * ```typescript
319
+ * const result = await env.rpc('domain.fetchPackagePart', {
320
+ * moduleSpecifier: '@sandbox/domain',
321
+ * part: 'types',
322
+ * });
323
+ * ```
324
+ */
325
+ async rpc(method, params = {}) {
326
+ return this.client.call(method, params);
327
+ }
328
+ /**
329
+ * Send client hello to establish the session
330
+ */
331
+ async hello() {
332
+ const result = await this.client.call("client.hello", {
333
+ clientId: this.clientId,
334
+ protocolVersion: "2.0"
335
+ });
336
+ return result;
337
+ }
338
+ /**
339
+ * Publish tools to the sandbox and register handlers for reverse-RPC.
340
+ *
341
+ * Tools can be:
342
+ * - **Instance methods**: set `className` (handler receives `(objectId, params)`)
343
+ * - **Static methods**: set `className` + `static: true` (handler receives `(params)`)
344
+ * - **Global tools**: omit `className` (handler receives `(params)`)
345
+ *
346
+ * Both `inputSchema` and `outputSchema` accept JSON Schema objects. The
347
+ * `outputSchema` drives the return type in the auto-generated TypeScript
348
+ * class declarations that sandbox code imports from `./sandbox-tools`.
349
+ *
350
+ * This method:
351
+ * 1. Extracts tool schemas (including `outputSchema`) from the provided tools
352
+ * 2. Publishes them via `client.publishRawToolCatalog` RPC
353
+ * 3. Registers handlers locally for `tool.invoke` RPC calls
354
+ * 4. Returns the `domainRevision` needed for job submission
355
+ *
356
+ * @param tools - Array of tools with handlers
357
+ * @param revision - Optional revision string (default: "1.0.0")
358
+ * @returns PublishToolsResult with domainRevision
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * await env.publishTools([
363
+ * {
364
+ * name: 'get_bio',
365
+ * description: 'Get biography of an author',
366
+ * className: 'author',
367
+ * inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
368
+ * outputSchema: { type: 'object', properties: { bio: { type: 'string' } }, required: ['bio'] },
369
+ * handler: async (id, params) => ({ bio: `Bio of ${id}` }),
370
+ * },
371
+ * ]);
372
+ * ```
373
+ */
374
+ async publishTools(tools, revision = "1.0.0") {
375
+ const schemas = tools.map((tool) => ({
376
+ name: tool.name,
377
+ description: tool.description,
378
+ inputSchema: tool.inputSchema,
379
+ outputSchema: tool.outputSchema,
380
+ stability: tool.stability || "stable",
381
+ provenance: tool.provenance || { source: "mcp" },
382
+ tags: tool.tags,
383
+ className: tool.className,
384
+ static: tool.static
385
+ }));
386
+ const result = await this.client.call("client.publishRawToolCatalog", {
387
+ clientId: this.clientId,
388
+ revision,
389
+ tools: schemas
390
+ });
391
+ if (!result.accepted || !result.domainRevision) {
392
+ throw new Error(`Failed to publish tools: ${JSON.stringify(result.rejected)}`);
393
+ }
394
+ for (const tool of tools) {
395
+ this.toolHandlers.set(tool.name, tool.handler);
396
+ if (tool.className && !tool.static) {
397
+ this.instanceTools.add(tool.name);
398
+ } else {
399
+ this.instanceTools.delete(tool.name);
400
+ }
401
+ }
402
+ this.currentDomainRevision = result.domainRevision;
403
+ return {
404
+ accepted: result.accepted,
405
+ domainRevision: result.domainRevision,
406
+ rejected: result.rejected
407
+ };
408
+ }
409
+ /**
410
+ * Submit a job to execute code in the sandbox.
411
+ *
412
+ * The code can import typed classes from `./sandbox-tools`:
413
+ * ```typescript
414
+ * import { Author, Book, global_search } from './sandbox-tools';
415
+ *
416
+ * const tolkien = await Author.find({ id: 'tolkien' });
417
+ * const bio = await tolkien.get_bio({ detailed: true });
418
+ * const books = await tolkien.get_books();
419
+ * ```
420
+ *
421
+ * Tool calls (instance methods, static methods, global functions) trigger
422
+ * `tool.invoke` RPC back to this client, where the registered handlers
423
+ * execute locally and return the result to the sandbox.
424
+ */
425
+ async submitJob(code, domainRevision) {
426
+ const revision = domainRevision || this.currentDomainRevision;
427
+ if (!revision) {
428
+ throw new Error("No domain revision available. Call publishTools() first.");
429
+ }
430
+ const result = await this.client.call("job.submit", {
431
+ domainRevision: revision,
432
+ code
433
+ });
434
+ if (!result.jobId) {
435
+ throw new Error("Failed to submit job: no jobId returned");
436
+ }
437
+ const job = new JobImplementation(result.jobId, this.client);
438
+ this.jobsMap.set(result.jobId, job);
439
+ return job;
440
+ }
441
+ /**
442
+ * Register a handler for a specific tool.
443
+ * @param isInstance - If true, handler will receive (id, params) for instance method dispatch.
444
+ */
445
+ registerToolHandler(name, handler, isInstance = false) {
446
+ this.toolHandlers.set(name, handler);
447
+ if (isInstance) {
448
+ this.instanceTools.add(name);
449
+ } else {
450
+ this.instanceTools.delete(name);
451
+ }
452
+ }
453
+ /**
454
+ * Respond to a prompt request from the sandbox
455
+ */
456
+ async answerPrompt(promptId, answer) {
457
+ await this.client.call("prompt.answer", { promptId, value: answer });
458
+ }
459
+ /**
460
+ * Get the current domain state and available tools
461
+ */
462
+ async getDomain() {
463
+ return this.client.call("domain.getSummary", {});
464
+ }
465
+ /**
466
+ * Get auto-generated TypeScript class declarations for the current domain.
467
+ *
468
+ * Returns typed declarations including:
469
+ * - Classes with readonly properties (from manifest)
470
+ * - `static find(query: { id: string })` for each class
471
+ * - Instance methods with typed input/output (from `publishTools` with `className`)
472
+ * - Static methods (from `publishTools` with `className` + `static: true`)
473
+ * - Relationship accessors (`get_books()`, `get_author()`, etc.)
474
+ * - Global tool functions (from `publishTools` without `className`)
475
+ *
476
+ * Pass this documentation to LLMs so they can write correct typed code
477
+ * that imports from `./sandbox-tools`.
478
+ *
479
+ * Falls back to generating markdown docs from the domain summary if
480
+ * the synthesis server is unavailable.
481
+ */
482
+ async getDomainDocumentation() {
483
+ try {
484
+ const typesResult = await this.client.call("domain.fetchPackagePart", {
485
+ moduleSpecifier: "@sandbox/domain",
486
+ part: "types"
487
+ });
488
+ if (typesResult.content) {
489
+ return typesResult.content;
490
+ }
491
+ } catch {
492
+ }
493
+ const summary = await this.getDomain();
494
+ return this.generateFallbackDocs(summary);
495
+ }
496
+ /**
497
+ * Generate markdown documentation from the domain summary.
498
+ * Class-aware: groups tools by class with property/relationship info.
499
+ */
500
+ generateFallbackDocs(summary) {
501
+ const classes = summary.classes;
502
+ const globalTools = summary.globalTools;
503
+ const tools = summary.tools || [];
504
+ if (classes && Object.keys(classes).length > 0) {
505
+ let docs2 = "# Domain Documentation\n\n";
506
+ docs2 += "Import classes and tools from `./sandbox-tools`:\n\n";
507
+ const classNames = Object.keys(classes).map(
508
+ (c) => c.charAt(0).toUpperCase() + c.slice(1)
509
+ );
510
+ const globalNames = (globalTools || []).map((t) => t.name);
511
+ const allImports = [...classNames, ...globalNames].join(", ");
512
+ docs2 += `\`\`\`typescript
513
+ import { ${allImports} } from "./sandbox-tools";
514
+ \`\`\`
515
+
516
+ `;
517
+ for (const [className, cls] of Object.entries(classes)) {
518
+ const TsName = className.charAt(0).toUpperCase() + className.slice(1);
519
+ docs2 += `## ${TsName}
520
+
521
+ `;
522
+ if (cls.description) docs2 += `${cls.description}
523
+
524
+ `;
525
+ if (cls.properties?.length > 0) {
526
+ docs2 += "**Properties:**\n";
527
+ for (const p of cls.properties) {
528
+ docs2 += `- \`${p.name}\`${p.description ? ": " + p.description : ""}
529
+ `;
530
+ }
531
+ docs2 += "\n";
532
+ }
533
+ if (cls.relationships?.length > 0) {
534
+ docs2 += "**Relationships:**\n";
535
+ for (const r of cls.relationships) {
536
+ const fModel = r.foreignModel?.charAt(0).toUpperCase() + r.foreignModel?.slice(1);
537
+ docs2 += `- \`${r.localSubmodel}\` \u2192 ${fModel} (${r.kind})
538
+ `;
539
+ }
540
+ docs2 += "\n";
541
+ }
542
+ if (cls.methods?.length > 0) {
543
+ docs2 += "**Methods:**\n";
544
+ for (const m of cls.methods) {
545
+ docs2 += `- \`${TsName}.${m.name}(input)\``;
546
+ if (m.description) docs2 += `: ${m.description}`;
547
+ docs2 += "\n";
548
+ if (m.inputSchema?.properties) {
549
+ docs2 += " ```json\n " + JSON.stringify(m.inputSchema, null, 2).replace(/\n/g, "\n ") + "\n ```\n";
550
+ }
551
+ }
552
+ docs2 += "\n";
553
+ }
554
+ }
555
+ if (globalTools && globalTools.length > 0) {
556
+ docs2 += "## Global Tools\n\n";
557
+ for (const tool of globalTools) {
558
+ docs2 += `### ${tool.name}
559
+
560
+ `;
561
+ if (tool.description) docs2 += `${tool.description}
562
+
563
+ `;
564
+ if (tool.inputSchema) {
565
+ docs2 += "**Input Schema:**\n```json\n";
566
+ docs2 += JSON.stringify(tool.inputSchema, null, 2);
567
+ docs2 += "\n```\n\n";
568
+ }
569
+ }
570
+ }
571
+ return docs2;
572
+ }
573
+ if (!tools || tools.length === 0) {
574
+ return "No tools available in this domain.";
575
+ }
576
+ let docs = "# Available Tools\n\n";
577
+ docs += "Import tools from `./sandbox-tools` and call them with await:\n\n";
578
+ docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
579
+ docs += "// Example:\n";
580
+ docs += `const result = await tools.${tools[0]?.name || "example"}(input);
581
+ `;
582
+ docs += "```\n\n";
583
+ for (const tool of tools) {
584
+ docs += `## ${tool.name}
585
+
586
+ `;
587
+ if (tool.description) {
588
+ docs += `${tool.description}
589
+
590
+ `;
591
+ }
592
+ if (tool.inputSchema) {
593
+ docs += "**Input Schema:**\n```json\n";
594
+ docs += JSON.stringify(tool.inputSchema, null, 2);
595
+ docs += "\n```\n\n";
596
+ }
597
+ if (tool.outputSchema) {
598
+ docs += "**Output Schema:**\n```json\n";
599
+ docs += JSON.stringify(tool.outputSchema, null, 2);
600
+ docs += "\n```\n\n";
601
+ }
602
+ }
603
+ return docs;
604
+ }
605
+ /**
606
+ * Close the session and disconnect from the sandbox
607
+ */
608
+ async disconnect() {
609
+ this.client.disconnect();
610
+ }
611
+ // --- Event Handling ---
612
+ /**
613
+ * Subscribe to session events
614
+ */
615
+ on(event, handler) {
616
+ if (!this.eventListeners.has(event)) {
617
+ this.eventListeners.set(event, []);
618
+ }
619
+ this.eventListeners.get(event).push(handler);
620
+ }
621
+ /**
622
+ * Unsubscribe from session events
623
+ */
624
+ off(event, handler) {
625
+ const handlers = this.eventListeners.get(event);
626
+ if (handlers) {
627
+ this.eventListeners.set(event, handlers.filter((h) => h !== handler));
628
+ }
629
+ }
630
+ // --- Internal ---
631
+ setupToolInvokeHandler() {
632
+ this.client.registerRpcHandler("tool.invoke", async (params) => {
633
+ const { callId, toolName, input } = params;
634
+ this.emit("tool:invoke", { callId, toolName, input });
635
+ const handler = this.toolHandlers.get(toolName);
636
+ if (!handler) {
637
+ await this.client.call("tool.result", {
638
+ callId,
639
+ error: { code: "TOOL_NOT_FOUND", message: `Tool handler not found: ${toolName}` }
640
+ });
641
+ return;
642
+ }
643
+ try {
644
+ let result;
645
+ if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
646
+ const { _objectId, ...restParams } = input;
647
+ result = await handler(_objectId, restParams);
648
+ } else {
649
+ result = await handler(input);
650
+ }
651
+ this.emit("tool:result", { callId, result });
652
+ await this.client.call("tool.result", {
653
+ callId,
654
+ result
655
+ });
656
+ } catch (error) {
657
+ const errorMessage = error instanceof Error ? error.message : String(error);
658
+ this.emit("tool:result", { callId, error: errorMessage });
659
+ await this.client.call("tool.result", {
660
+ callId,
661
+ error: { code: "TOOL_EXECUTION_FAILED", message: errorMessage }
662
+ });
663
+ }
664
+ });
665
+ }
666
+ setupEventHandlers() {
667
+ this.client.on("sync", (doc) => this.emit("sync", doc));
668
+ this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
669
+ this.client.on("disconnect", () => this.emit("disconnect", {}));
670
+ this.client.on("job.status", (data) => {
671
+ this.emit("job:status", data);
672
+ });
673
+ this.client.on("exec.completed", (data) => {
674
+ this.emit("exec:completed", data);
675
+ });
676
+ this.client.on("exec.progress", (data) => {
677
+ this.emit("exec:progress", data);
678
+ });
679
+ }
680
+ emit(event, data) {
681
+ const handlers = this.eventListeners.get(event);
682
+ if (handlers) {
683
+ handlers.forEach((h) => h(data));
684
+ }
685
+ }
686
+ };
687
+ var JobImplementation = class {
688
+ id;
689
+ client;
690
+ status = "queued";
691
+ _resultPromise;
692
+ _resolveResult;
693
+ _rejectResult;
694
+ eventListeners = /* @__PURE__ */ new Map();
695
+ constructor(id, client) {
696
+ this.id = id;
697
+ this.client = client;
698
+ this._resultPromise = new Promise((resolve, reject) => {
699
+ this._resolveResult = resolve;
700
+ this._rejectResult = reject;
701
+ });
702
+ this.client.on("exec.completed", (data) => {
703
+ const execData = data;
704
+ if (execData.execId === id || execData.jobId === id) {
705
+ this.status = "succeeded";
706
+ this.emit("status", this.status);
707
+ if (execData.error) {
708
+ this._rejectResult(execData.error);
709
+ } else {
710
+ this._resolveResult(execData.result);
711
+ }
712
+ }
713
+ });
714
+ this.client.on("exec.progress", (data) => {
715
+ const progressData = data;
716
+ if (progressData.execId === id || progressData.jobId === id) {
717
+ if (progressData.stdout) {
718
+ this.emit("stdout", progressData.stdout);
719
+ }
720
+ if (progressData.stderr) {
721
+ this.emit("stderr", progressData.stderr);
722
+ }
723
+ }
724
+ });
725
+ this.client.on(`job.${id}.status`, (status) => {
726
+ this.status = status;
727
+ this.emit("status", status);
728
+ });
729
+ this.client.on(`job.${id}.stdout`, (line) => {
730
+ this.emit("stdout", line);
731
+ });
732
+ this.client.on(`job.${id}.stderr`, (line) => {
733
+ this.emit("stderr", line);
734
+ });
735
+ this.client.on(`job.${id}.result`, (result) => {
736
+ this.status = "succeeded";
737
+ this._resolveResult(result);
738
+ });
739
+ this.client.on(`job.${id}.error`, (error) => {
740
+ this.status = "failed";
741
+ this._rejectResult(error);
742
+ });
743
+ this.client.on("job.completed", (data) => {
744
+ const jobData = data;
745
+ if (jobData.jobId === id) {
746
+ this.status = "succeeded";
747
+ this.emit("status", this.status);
748
+ this._resolveResult(jobData.result);
749
+ }
750
+ });
751
+ this.client.on("job.failed", (data) => {
752
+ const jobData = data;
753
+ if (jobData.jobId === id) {
754
+ this.status = "failed";
755
+ this.emit("status", this.status);
756
+ this._rejectResult(jobData.error || new Error("Job failed"));
757
+ }
758
+ });
759
+ }
760
+ get result() {
761
+ return this._resultPromise;
762
+ }
763
+ on(event, handler) {
764
+ if (!this.eventListeners.has(event)) {
765
+ this.eventListeners.set(event, []);
766
+ }
767
+ this.eventListeners.get(event).push(handler);
768
+ }
769
+ emit(event, data) {
770
+ const handlers = this.eventListeners.get(event);
771
+ if (handlers) {
772
+ handlers.forEach((h) => h(data));
773
+ }
774
+ }
775
+ };
776
+
777
+ // src/client.ts
778
+ var STANDARD_MODULES_OPERATIONS = [
779
+ { create: "entity", has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } } },
780
+ { create: "class", extends: "entity", has: {} },
781
+ { create: "user", extends: "entity", has: { email: { value: void 0 }, firstName: { value: void 0 }, lastName: { value: void 0 } } },
782
+ { create: "company", extends: "entity", has: { name: { value: void 0 }, website: { value: void 0 } } },
783
+ { create: "string", has: { value: { value: void 0 } } },
784
+ { create: "number", has: { value: { value: 0 } } },
785
+ { create: "boolean", has: { value: { value: false } } },
786
+ { create: "tool_parameter", has: { name: { value: void 0 }, type: { value: "string" }, description: { value: void 0 }, required: { value: false } } }
787
+ ];
788
+ var BUILTIN_MODULES = {
789
+ "standard_modules": STANDARD_MODULES_OPERATIONS
790
+ };
791
+ var Environment = class _Environment extends Session {
792
+ envData;
793
+ _apiKey;
794
+ _apiEndpoint;
795
+ constructor(client, envData, clientId, apiKey, apiEndpoint) {
796
+ super(client, clientId);
797
+ this.envData = envData;
798
+ this._apiKey = apiKey;
799
+ this._apiEndpoint = apiEndpoint;
800
+ }
801
+ /** The environment ID */
802
+ get environmentId() {
803
+ return this.envData.environmentId;
804
+ }
805
+ /** The sandbox ID */
806
+ get sandboxId() {
807
+ return this.envData.sandboxId;
808
+ }
809
+ /** The subject ID */
810
+ get subjectId() {
811
+ return this.envData.subjectId;
812
+ }
813
+ /** The permission profile ID */
814
+ get permissionProfileId() {
815
+ return this.envData.permissionProfileId;
816
+ }
817
+ /** The GraphQL API endpoint URL */
818
+ get apiEndpoint() {
819
+ return this._apiEndpoint;
820
+ }
821
+ // ==================== ID ↔ GRAPH PATH MAPPING ====================
822
+ /**
823
+ * Convert a class name + real-world ID into a unique graph path.
824
+ *
825
+ * Two objects of *different* classes may share the same real-world ID,
826
+ * so the graph path must incorporate the class to guarantee uniqueness.
827
+ *
828
+ * Format: `{className}_{id}` — deterministic, human-readable.
829
+ *
830
+ * **Convention**: class names should be simple identifiers without
831
+ * underscores (e.g. `author`, `book`). This ensures the prefix is
832
+ * unambiguously parseable by `extractIdFromGraphPath`.
833
+ */
834
+ static toGraphPath(className, id) {
835
+ return `${className}_${id}`;
836
+ }
837
+ /**
838
+ * Extract the real-world ID from a graph path, given the class name.
839
+ *
840
+ * Strips the `{className}_` prefix. Returns the raw path if the
841
+ * expected prefix is not found.
842
+ */
843
+ static extractIdFromGraphPath(graphPath, className) {
844
+ const prefix = `${className}_`;
845
+ return graphPath.startsWith(prefix) ? graphPath.substring(prefix.length) : graphPath;
846
+ }
847
+ /**
848
+ * Execute a GraphQL query against the environment's graph.
849
+ *
850
+ * The query uses the Granular graph query language (based on Cypher/GraphQL).
851
+ * Authentication is handled automatically using the SDK's API key.
852
+ *
853
+ * @param query - The GraphQL query string
854
+ * @param variables - Optional variables for the query
855
+ * @returns The query result data
856
+ *
857
+ * @example
858
+ * ```typescript
859
+ * // Read the workspace
860
+ * const result = await env.graphql(
861
+ * `query { model(path: "workspace") { path label submodels { path label } } }`
862
+ * );
863
+ * console.log(result.data);
864
+ *
865
+ * // Create a model
866
+ * const created = await env.graphql(
867
+ * `mutation { at(path: "workspace") { create_submodel(subpath: "my_node", label: "My Node", prototype: "Model") { model { path label } } } }`
868
+ * );
869
+ * ```
870
+ */
871
+ async graphql(query, variables) {
872
+ const response = await fetch(this._apiEndpoint, {
873
+ method: "POST",
874
+ headers: {
875
+ "Content-Type": "application/json",
876
+ "Authorization": `Bearer ${this._apiKey}`
877
+ },
878
+ body: JSON.stringify({
879
+ environmentId: this.environmentId,
880
+ query,
881
+ variables
882
+ })
883
+ });
884
+ if (!response.ok) {
885
+ const errorText = await response.text();
886
+ throw new Error(`GraphQL API Error (${response.status}): ${errorText}`);
887
+ }
888
+ return response.json();
889
+ }
890
+ // ==================== RELATIONSHIP METHODS ====================
891
+ /**
892
+ * Define a relationship between two model types.
893
+ *
894
+ * Creates both submodels (if they don't exist) and links them with
895
+ * a RelationshipDef node that encodes cardinality.
896
+ *
897
+ * @example
898
+ * ```typescript
899
+ * // Author has many Books, Book has one Author
900
+ * const rel = await env.defineRelationship({
901
+ * model: 'author',
902
+ * localSubmodel: 'books',
903
+ * localIsMany: true,
904
+ * foreignModel: 'book',
905
+ * foreignSubmodel: 'author',
906
+ * foreignIsMany: false,
907
+ * });
908
+ * console.log(rel.relationship_kind); // "one_to_many"
909
+ * ```
910
+ */
911
+ async defineRelationship(options) {
912
+ const result = await this.graphql(
913
+ `mutation DefineRelationship(
914
+ $localSubmodel: String!,
915
+ $foreignModel: String!,
916
+ $foreignSubmodel: String!,
917
+ $localIsMany: Boolean!,
918
+ $foreignIsMany: Boolean!,
919
+ $name: String
920
+ ) {
921
+ at(path: "${options.model}") {
922
+ define_relationship(
923
+ local_submodel: $localSubmodel
924
+ foreign_model: $foreignModel
925
+ foreign_submodel: $foreignSubmodel
926
+ local_is_many: $localIsMany
927
+ foreign_is_many: $foreignIsMany
928
+ name: $name
929
+ ) {
930
+ name
931
+ local_submodel { path label }
932
+ local_is_many
933
+ foreign_submodel { path label }
934
+ foreign_is_many
935
+ foreign_model { path label }
936
+ relationship_kind
937
+ }
938
+ }
939
+ }`,
940
+ {
941
+ localSubmodel: options.localSubmodel,
942
+ foreignModel: options.foreignModel,
943
+ foreignSubmodel: options.foreignSubmodel,
944
+ localIsMany: options.localIsMany,
945
+ foreignIsMany: options.foreignIsMany,
946
+ name: options.name
947
+ }
948
+ );
949
+ if (result.errors?.length) {
950
+ throw new Error(`defineRelationship failed: ${result.errors[0].message}`);
951
+ }
952
+ return result.data.at.define_relationship;
953
+ }
954
+ /**
955
+ * Get all relationships for a model type.
956
+ *
957
+ * @param modelPath - The model type path (e.g., "author")
958
+ * @returns Array of relationships from this model's perspective
959
+ *
960
+ * @example
961
+ * ```typescript
962
+ * const rels = await env.getRelationships('author');
963
+ * for (const rel of rels) {
964
+ * console.log(`${rel.local_submodel.path} -> ${rel.foreign_model.path} (${rel.relationship_kind})`);
965
+ * }
966
+ * ```
967
+ */
968
+ async getRelationships(modelPath) {
969
+ const result = await this.graphql(
970
+ `query GetRelationships($path: String) {
971
+ model(path: $path) {
972
+ relationships {
973
+ name
974
+ local_submodel { path label }
975
+ local_is_many
976
+ foreign_submodel { path label }
977
+ foreign_is_many
978
+ foreign_model { path label }
979
+ relationship_kind
980
+ }
981
+ }
982
+ }`,
983
+ { path: modelPath }
984
+ );
985
+ if (result.errors?.length) {
986
+ throw new Error(`getRelationships failed: ${result.errors[0].message}`);
987
+ }
988
+ return result.data?.model?.relationships || [];
989
+ }
990
+ /**
991
+ * Attach a target model to a relationship submodel.
992
+ *
993
+ * Handles cardinality automatically:
994
+ * - "One" side: sets/replaces the reference
995
+ * - "Many" side: adds the target to the collection
996
+ *
997
+ * If the target model doesn't exist, it's created as an instance of the foreign type.
998
+ * Bidirectional sync is automatic.
999
+ *
1000
+ * @param modelPath - The model instance path (e.g., "tolkien")
1001
+ * @param submodelPath - The relationship submodel (e.g., "books")
1002
+ * @param targetPath - The target model to attach (e.g., "lord_of_the_rings")
1003
+ *
1004
+ * @example
1005
+ * ```typescript
1006
+ * // Attach a book to an author (many side)
1007
+ * await env.attach('tolkien', 'books', 'lord_of_the_rings');
1008
+ * // This also automatically sets lord_of_the_rings:author -> tolkien
1009
+ * ```
1010
+ */
1011
+ async attach(modelPath, submodelPath, targetPath) {
1012
+ const result = await this.graphql(
1013
+ `mutation Attach($target: String!) {
1014
+ at(path: "${modelPath}") {
1015
+ at(submodel: "${submodelPath}") {
1016
+ attach(target: $target) {
1017
+ model { path }
1018
+ }
1019
+ }
1020
+ }
1021
+ }`,
1022
+ { target: targetPath }
1023
+ );
1024
+ if (result.errors?.length) {
1025
+ throw new Error(`attach failed: ${result.errors[0].message}`);
1026
+ }
1027
+ }
1028
+ /**
1029
+ * Detach a target model from a relationship submodel.
1030
+ *
1031
+ * Handles bidirectional cleanup automatically.
1032
+ *
1033
+ * @param modelPath - The model instance path
1034
+ * @param submodelPath - The relationship submodel
1035
+ * @param targetPath - The target to detach (optional for "one" side; omit on "many" side to detach all)
1036
+ *
1037
+ * @example
1038
+ * ```typescript
1039
+ * // Detach a specific book
1040
+ * await env.detach('tolkien', 'books', 'lord_of_the_rings');
1041
+ *
1042
+ * // Detach all books
1043
+ * await env.detach('tolkien', 'books');
1044
+ * ```
1045
+ */
1046
+ async detach(modelPath, submodelPath, targetPath) {
1047
+ const targetArg = targetPath ? `target: "${targetPath}"` : "";
1048
+ const result = await this.graphql(
1049
+ `mutation Detach {
1050
+ at(path: "${modelPath}") {
1051
+ at(submodel: "${submodelPath}") {
1052
+ detach(${targetArg}) {
1053
+ model { path }
1054
+ }
1055
+ }
1056
+ }
1057
+ }`
1058
+ );
1059
+ if (result.errors?.length) {
1060
+ throw new Error(`detach failed: ${result.errors[0].message}`);
1061
+ }
1062
+ }
1063
+ /**
1064
+ * List all related models through a relationship submodel.
1065
+ *
1066
+ * @param modelPath - The model instance path
1067
+ * @param submodelPath - The relationship submodel
1068
+ * @returns Array of related model references
1069
+ *
1070
+ * @example
1071
+ * ```typescript
1072
+ * const books = await env.listRelated('tolkien', 'books');
1073
+ * console.log(books); // [{ path: "lord_of_the_rings", label: "Lord of the Rings" }, ...]
1074
+ * ```
1075
+ */
1076
+ async listRelated(modelPath, submodelPath) {
1077
+ const result = await this.graphql(
1078
+ `mutation ListRelated {
1079
+ at(path: "${modelPath}") {
1080
+ at(submodel: "${submodelPath}") {
1081
+ list_related { path label }
1082
+ }
1083
+ }
1084
+ }`
1085
+ );
1086
+ if (result.errors?.length) {
1087
+ throw new Error(`listRelated failed: ${result.errors[0].message}`);
1088
+ }
1089
+ return result.data?.at?.at?.list_related || [];
1090
+ }
1091
+ /**
1092
+ * Apply a manifest to the current environment's graph.
1093
+ *
1094
+ * Translates each manifest operation into GraphQL mutations and executes them
1095
+ * in order. This is the core mechanism for creating classes, fields, and
1096
+ * relationships from a declarative manifest.
1097
+ *
1098
+ * @param manifest - The manifest content to apply
1099
+ * @returns Summary of applied operations
1100
+ *
1101
+ * @example
1102
+ * ```typescript
1103
+ * await environment.applyManifest({
1104
+ * schemaVersion: 2,
1105
+ * name: 'my-app',
1106
+ * volumes: [{
1107
+ * name: 'schema',
1108
+ * scope: 'sandbox',
1109
+ * operations: [
1110
+ * { create: 'author', extends: 'class', has: { name: { type: 'string' } } },
1111
+ * { create: 'book', extends: 'class', has: { title: { type: 'string' } } },
1112
+ * { defineRelationship: {
1113
+ * left: 'author', right: 'book',
1114
+ * leftSubmodel: 'books', rightSubmodel: 'author',
1115
+ * leftIsMany: true, rightIsMany: false,
1116
+ * }},
1117
+ * ],
1118
+ * }],
1119
+ * });
1120
+ * ```
1121
+ */
1122
+ async applyManifest(manifest) {
1123
+ const errors = [];
1124
+ let applied = 0;
1125
+ for (const volume of manifest.volumes) {
1126
+ const aliasMap = {};
1127
+ if (volume.imports) {
1128
+ for (const imp of volume.imports) {
1129
+ aliasMap[imp.alias] = imp.name;
1130
+ const builtinOps = BUILTIN_MODULES[imp.name];
1131
+ if (builtinOps) {
1132
+ for (const op of builtinOps) {
1133
+ try {
1134
+ await this._applyOperation(op, {});
1135
+ applied++;
1136
+ } catch (err) {
1137
+ if (!err.message?.includes("already exists")) {
1138
+ errors.push(`Import ${imp.name} operation failed: ${err.message}`);
1139
+ }
1140
+ }
1141
+ }
1142
+ } else {
1143
+ errors.push(`Unknown module: "${imp.name}" (only built-in modules are supported)`);
1144
+ }
1145
+ }
1146
+ }
1147
+ for (const op of volume.operations) {
1148
+ try {
1149
+ await this._applyOperation(op, aliasMap);
1150
+ applied++;
1151
+ } catch (err) {
1152
+ errors.push(`Operation failed: ${err.message}`);
1153
+ }
1154
+ }
1155
+ }
1156
+ return { applied, errors };
1157
+ }
1158
+ /**
1159
+ * Resolve an alias reference like "@std/class" → "class"
1160
+ * Strips the alias prefix, returning the bare model path.
1161
+ */
1162
+ _resolveAlias(ref, aliasMap) {
1163
+ if (!ref.startsWith("@")) return ref;
1164
+ const slashIdx = ref.indexOf("/");
1165
+ if (slashIdx === -1) return ref;
1166
+ const alias = ref.substring(0, slashIdx);
1167
+ const symbolName = ref.substring(slashIdx + 1);
1168
+ if (aliasMap[alias]) {
1169
+ return symbolName;
1170
+ }
1171
+ return ref;
1172
+ }
1173
+ /**
1174
+ * Apply a single manifest operation via GraphQL
1175
+ */
1176
+ async _applyOperation(op, aliasMap) {
1177
+ if (op.defineRelationship) {
1178
+ const rel = op.defineRelationship;
1179
+ await this.defineRelationship({
1180
+ model: this._resolveAlias(rel.left, aliasMap),
1181
+ localSubmodel: rel.leftSubmodel,
1182
+ localIsMany: rel.leftIsMany,
1183
+ foreignModel: this._resolveAlias(rel.right, aliasMap),
1184
+ foreignSubmodel: rel.rightSubmodel,
1185
+ foreignIsMany: rel.rightIsMany,
1186
+ name: rel.name
1187
+ });
1188
+ return;
1189
+ }
1190
+ if (op.create) {
1191
+ const label = op.create.charAt(0).toUpperCase() + op.create.slice(1);
1192
+ const extendsRef = op.extends ? this._resolveAlias(op.extends, aliasMap) : void 0;
1193
+ const instanceOfRef = op.instanceOf ? this._resolveAlias(op.instanceOf, aliasMap) : void 0;
1194
+ if (extendsRef) {
1195
+ await this.graphql(
1196
+ `mutation { create_model(path: "${op.create}", label: "${label}") { model { path } } }`
1197
+ );
1198
+ await this.graphql(
1199
+ `mutation { at(path: "${op.create}") { add_superclass(superclass: "${extendsRef}") { model { path } } } }`
1200
+ );
1201
+ } else if (instanceOfRef) {
1202
+ await this.graphql(
1203
+ `mutation { at(path: "${instanceOfRef}") { instantiate(path: "${op.create}", label: "${label}") { model { path } } } }`
1204
+ );
1205
+ } else {
1206
+ await this.graphql(
1207
+ `mutation { create_model(path: "${op.create}", label: "${label}") { model { path } } }`
1208
+ );
1209
+ }
1210
+ if (op.has) {
1211
+ await this._applyFields(op.create, op.has);
1212
+ }
1213
+ return;
1214
+ }
1215
+ if (op.on) {
1216
+ const target = this._resolveAlias(op.on, aliasMap);
1217
+ if (op.has) {
1218
+ await this._applyFields(target, op.has);
1219
+ }
1220
+ return;
1221
+ }
1222
+ }
1223
+ /**
1224
+ * Apply field definitions (has) to a model via GraphQL
1225
+ */
1226
+ async _applyFields(modelPath, fields) {
1227
+ for (const [fieldName, spec] of Object.entries(fields)) {
1228
+ const fieldLabel = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
1229
+ await this.graphql(
1230
+ `mutation {
1231
+ at(path: "${modelPath}") {
1232
+ create_submodel(subpath: "${fieldName}", label: "${fieldLabel}") {
1233
+ model { path }
1234
+ }
1235
+ }
1236
+ }`
1237
+ );
1238
+ if (spec.type) {
1239
+ await this.graphql(
1240
+ `mutation {
1241
+ at(path: "${modelPath}") {
1242
+ at(submodel: "${fieldName}") {
1243
+ add_prototype(prototype: "${spec.type}") { done }
1244
+ }
1245
+ }
1246
+ }`
1247
+ );
1248
+ }
1249
+ if (spec.description) {
1250
+ await this.graphql(
1251
+ `mutation {
1252
+ at(path: "${modelPath}") {
1253
+ at(submodel: "${fieldName}") {
1254
+ set_description(description: "${spec.description}") { done }
1255
+ }
1256
+ }
1257
+ }`
1258
+ );
1259
+ }
1260
+ if (spec.value !== void 0 && spec.value !== null) {
1261
+ if (typeof spec.value === "string") {
1262
+ await this.graphql(
1263
+ `mutation {
1264
+ at(path: "${modelPath}") {
1265
+ at(submodel: "${fieldName}") {
1266
+ set_string_value(value: "${spec.value}") { done }
1267
+ }
1268
+ }
1269
+ }`
1270
+ );
1271
+ } else if (typeof spec.value === "number") {
1272
+ await this.graphql(
1273
+ `mutation {
1274
+ at(path: "${modelPath}") {
1275
+ at(submodel: "${fieldName}") {
1276
+ set_number_value(value: ${spec.value}) { done }
1277
+ }
1278
+ }
1279
+ }`
1280
+ );
1281
+ } else if (typeof spec.value === "boolean") {
1282
+ await this.graphql(
1283
+ `mutation {
1284
+ at(path: "${modelPath}") {
1285
+ at(submodel: "${fieldName}") {
1286
+ set_boolean_value(value: ${spec.value}) { done }
1287
+ }
1288
+ }
1289
+ }`
1290
+ );
1291
+ }
1292
+ }
1293
+ if (spec.ref) {
1294
+ await this.graphql(
1295
+ `mutation {
1296
+ at(path: "${modelPath}") {
1297
+ at(submodel: "${fieldName}") {
1298
+ set_reference(reference: "${spec.ref}") { done }
1299
+ }
1300
+ }
1301
+ }`
1302
+ );
1303
+ }
1304
+ if (spec.has) {
1305
+ await this._applyFields(`${modelPath}:${fieldName}`, spec.has);
1306
+ }
1307
+ }
1308
+ }
1309
+ // ==================== RECORD OBJECT ====================
1310
+ /**
1311
+ * Create or update an instance of a class in the graph.
1312
+ *
1313
+ * Uses `instantiate` under the hood, which has find-or-create semantics:
1314
+ * if an instance with the given `id` already exists for the class it is
1315
+ * returned; otherwise a new instance is created. Fields are then set
1316
+ * (overwriting previous values) and relationships are attached.
1317
+ *
1318
+ * The graph path is derived as `{className}_{id}` to ensure uniqueness
1319
+ * across classes (two objects of different classes may share the same
1320
+ * real-world ID). Relationship targets are also resolved automatically
1321
+ * using the foreign class from the relationship definition.
1322
+ *
1323
+ * @param options - The object specification
1324
+ * @returns The graph path, real-world ID, and creation status
1325
+ *
1326
+ * @example
1327
+ * ```typescript
1328
+ * // Create an author with fields
1329
+ * const result = await env.recordObject({
1330
+ * className: 'author',
1331
+ * id: 'tolkien',
1332
+ * label: 'J.R.R. Tolkien',
1333
+ * fields: { name: 'J.R.R. Tolkien', birth_year: 1892 },
1334
+ * relationships: { books: ['lotr', 'silmarillion'] },
1335
+ * });
1336
+ * // result.path → 'author_tolkien' (internal graph path)
1337
+ * // result.id → 'tolkien' (real-world ID)
1338
+ * // result.created → true
1339
+ * ```
1340
+ */
1341
+ async recordObject(options) {
1342
+ const { className, id, label, fields, relationships } = options;
1343
+ const graphPath = _Environment.toGraphPath(className, id);
1344
+ const existsResult = await this.graphql(
1345
+ `query { model(path: "${graphPath}") { path } }`
1346
+ );
1347
+ const alreadyExists = !!existsResult.data?.model;
1348
+ const instResult = await this.graphql(
1349
+ `mutation {
1350
+ at(path: "${className}") {
1351
+ instantiate(path: "${graphPath}"${label ? `, label: "${label}"` : ""}) {
1352
+ model { path }
1353
+ }
1354
+ }
1355
+ }`
1356
+ );
1357
+ if (instResult.errors?.length) {
1358
+ throw new Error(`recordObject instantiate failed: ${instResult.errors[0].message}`);
1359
+ }
1360
+ const instancePath = instResult.data?.at?.instantiate?.model?.path ?? graphPath;
1361
+ await this.graphql(
1362
+ `mutation {
1363
+ at(path: "${instancePath}") {
1364
+ create_submodel(subpath: "_realId", label: "_realId") {
1365
+ model { path }
1366
+ }
1367
+ }
1368
+ }`
1369
+ );
1370
+ await this.graphql(
1371
+ `mutation {
1372
+ at(path: "${instancePath}") {
1373
+ at(submodel: "_realId") {
1374
+ set_string_value(value: "${id.replace(/"/g, '\\"')}") { done }
1375
+ }
1376
+ }
1377
+ }`
1378
+ );
1379
+ if (fields) {
1380
+ for (const [fieldName, value] of Object.entries(fields)) {
1381
+ if (value === null) continue;
1382
+ await this.graphql(
1383
+ `mutation {
1384
+ at(path: "${instancePath}") {
1385
+ create_submodel(subpath: "${fieldName}", label: "${fieldName}") {
1386
+ model { path }
1387
+ }
1388
+ }
1389
+ }`
1390
+ );
1391
+ if (typeof value === "string") {
1392
+ await this.graphql(
1393
+ `mutation {
1394
+ at(path: "${instancePath}") {
1395
+ at(submodel: "${fieldName}") {
1396
+ set_string_value(value: "${value.replace(/"/g, '\\"')}") { done }
1397
+ }
1398
+ }
1399
+ }`
1400
+ );
1401
+ } else if (typeof value === "number") {
1402
+ await this.graphql(
1403
+ `mutation {
1404
+ at(path: "${instancePath}") {
1405
+ at(submodel: "${fieldName}") {
1406
+ set_number_value(value: ${value}) { done }
1407
+ }
1408
+ }
1409
+ }`
1410
+ );
1411
+ } else if (typeof value === "boolean") {
1412
+ await this.graphql(
1413
+ `mutation {
1414
+ at(path: "${instancePath}") {
1415
+ at(submodel: "${fieldName}") {
1416
+ set_boolean_value(value: ${value}) { done }
1417
+ }
1418
+ }
1419
+ }`
1420
+ );
1421
+ }
1422
+ }
1423
+ }
1424
+ if (relationships) {
1425
+ const rels = await this.getRelationships(className);
1426
+ const relMap = {};
1427
+ for (const rel of rels) {
1428
+ const submodelLeaf = rel.local_submodel.path.includes(":") ? rel.local_submodel.path.split(":").pop() : rel.local_submodel.path;
1429
+ relMap[submodelLeaf] = rel.foreign_model.path;
1430
+ }
1431
+ for (const [submodelName, targets] of Object.entries(relationships)) {
1432
+ const foreignClass = relMap[submodelName];
1433
+ const targetList = Array.isArray(targets) ? targets : [targets];
1434
+ for (const target of targetList) {
1435
+ const targetGraphPath = foreignClass ? _Environment.toGraphPath(foreignClass, target) : target;
1436
+ await this.attach(instancePath, submodelName, targetGraphPath);
1437
+ }
1438
+ }
1439
+ }
1440
+ return { path: instancePath, id, created: !alreadyExists };
1441
+ }
1442
+ // ==================== PUBLISH TOOLS ====================
1443
+ /**
1444
+ * Convenience method: publish tools and get back wrapped result.
1445
+ * This is the main entry point for setting up tools.
1446
+ *
1447
+ * @example
1448
+ * ```typescript
1449
+ * const tools = [
1450
+ * {
1451
+ * name: 'get_weather',
1452
+ * description: 'Get weather for a city',
1453
+ * inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
1454
+ * handler: async ({ city }) => ({ temp: 22 }),
1455
+ * },
1456
+ * ];
1457
+ *
1458
+ * const { domainRevision } = await environment.publishTools(tools);
1459
+ *
1460
+ * // Now submit jobs that use those tools
1461
+ * const job = await environment.submitJob(`
1462
+ * import { Author } from './sandbox-tools';
1463
+ * const weather = await Author.get_weather({ city: 'Paris' });
1464
+ * return weather;
1465
+ * `);
1466
+ *
1467
+ * const result = await job.result;
1468
+ * ```
1469
+ */
1470
+ async publishTools(tools, revision = "1.0.0") {
1471
+ return super.publishTools(tools, revision);
1472
+ }
1473
+ };
1474
+ var Granular = class {
1475
+ apiKey;
1476
+ apiUrl;
1477
+ httpUrl;
1478
+ /**
1479
+ * Create a new Granular client
1480
+ * @param options - Client configuration
1481
+ */
1482
+ constructor(options) {
1483
+ this.apiKey = options.apiKey;
1484
+ this.apiUrl = options.apiUrl || "wss://api.granular.dev/v2/ws";
1485
+ this.httpUrl = this.apiUrl.replace("wss://", "https://").replace("/ws", "");
1486
+ }
1487
+ /**
1488
+ * Records/upserts a user and prepares them for sandbox connections
1489
+ *
1490
+ * @param options - User options
1491
+ * @returns The user object to pass to connect()
1492
+ *
1493
+ * @example
1494
+ * ```typescript
1495
+ * const user = await granular.recordUser({
1496
+ * userId: 'user_123',
1497
+ * name: 'John Doe',
1498
+ * permissions: ['agent'],
1499
+ * });
1500
+ * ```
1501
+ */
1502
+ async recordUser(options) {
1503
+ const subject = await this.request("/control/subjects", {
1504
+ method: "POST",
1505
+ body: JSON.stringify({
1506
+ identityId: options.userId,
1507
+ name: options.name,
1508
+ email: options.email
1509
+ })
1510
+ });
1511
+ return {
1512
+ subjectId: subject.subjectId,
1513
+ identityId: options.userId,
1514
+ name: options.name,
1515
+ email: options.email,
1516
+ permissions: options.permissions || []
1517
+ };
1518
+ }
1519
+ /**
1520
+ * Connect to a sandbox and establish a real-time environment session.
1521
+ *
1522
+ * After connecting, use `environment.publishTools()` to register tools,
1523
+ * then `environment.submitJob()` to execute code that uses those tools.
1524
+ *
1525
+ * @param options - Connection options
1526
+ * @returns An active environment session
1527
+ *
1528
+ * @example
1529
+ * ```typescript
1530
+ * const user = await granular.recordUser({
1531
+ * userId: 'user_123',
1532
+ * permissions: ['agent'],
1533
+ * });
1534
+ *
1535
+ * const environment = await granular.connect({
1536
+ * sandbox: 'my-sandbox',
1537
+ * user,
1538
+ * });
1539
+ *
1540
+ * // Publish tools
1541
+ * await environment.publishTools([
1542
+ * { name: 'greet', description: 'Say hello', inputSchema: {}, handler: async () => 'Hello!' },
1543
+ * ]);
1544
+ *
1545
+ * // Submit job
1546
+ * const job = await environment.submitJob(`
1547
+ * import { tools } from './sandbox-tools';
1548
+ * return await tools.greet({});
1549
+ * `);
1550
+ *
1551
+ * console.log(await job.result); // 'Hello!'
1552
+ * ```
1553
+ */
1554
+ async connect(options) {
1555
+ const clientId = `client_${Date.now()}`;
1556
+ const sandbox = await this.findOrCreateSandbox(options.sandbox);
1557
+ for (const profileName of options.user.permissions) {
1558
+ const profileId = await this.ensurePermissionProfile(sandbox.sandboxId, profileName);
1559
+ await this.ensureAssignment(options.user.subjectId, sandbox.sandboxId, profileId);
1560
+ }
1561
+ const envData = await this.environments.create(sandbox.sandboxId, {
1562
+ subjectId: options.user.subjectId,
1563
+ permissionProfileId: null
1564
+ });
1565
+ const client = new WSClient({
1566
+ url: this.apiUrl,
1567
+ sessionId: envData.environmentId,
1568
+ token: this.apiKey
1569
+ });
1570
+ await client.connect();
1571
+ const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
1572
+ const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
1573
+ await environment.hello();
1574
+ return environment;
1575
+ }
1576
+ /**
1577
+ * Find a sandbox by name or create it if it doesn't exist
1578
+ */
1579
+ async findOrCreateSandbox(nameOrId) {
1580
+ try {
1581
+ const sandbox = await this.sandboxes.get(nameOrId);
1582
+ return sandbox;
1583
+ } catch {
1584
+ const sandboxes = await this.sandboxes.list();
1585
+ const existing = sandboxes.items.find((s) => s.name === nameOrId);
1586
+ if (existing) {
1587
+ return existing;
1588
+ }
1589
+ const created = await this.sandboxes.create({ name: nameOrId });
1590
+ return created;
1591
+ }
1592
+ }
1593
+ /**
1594
+ * Ensure a permission profile exists for a sandbox, creating it if needed.
1595
+ * If profileName matches an existing profile name, returns its ID.
1596
+ * Otherwise, creates a new profile with default allow-all rules.
1597
+ */
1598
+ async ensurePermissionProfile(sandboxId, profileName) {
1599
+ try {
1600
+ const profiles = await this.permissionProfiles.list(sandboxId);
1601
+ const existing = profiles.find((p) => p.name === profileName);
1602
+ if (existing) {
1603
+ return existing.permissionProfileId;
1604
+ }
1605
+ } catch {
1606
+ }
1607
+ const created = await this.permissionProfiles.create(sandboxId, {
1608
+ name: profileName,
1609
+ rules: {
1610
+ tools: { allow: ["*"] },
1611
+ resources: { allow: ["*"] }
1612
+ }
1613
+ });
1614
+ return created.permissionProfileId;
1615
+ }
1616
+ /**
1617
+ * Ensure an assignment exists for a subject in a sandbox with a permission profile
1618
+ */
1619
+ async ensureAssignment(subjectId, sandboxId, permissionProfileId) {
1620
+ try {
1621
+ const assignments = await this.request(
1622
+ `/control/subjects/${subjectId}/assignments`
1623
+ );
1624
+ const existing = assignments.items.find(
1625
+ (a) => a.sandboxId === sandboxId
1626
+ );
1627
+ if (existing) {
1628
+ if (existing.permissionProfileId === permissionProfileId) {
1629
+ return;
1630
+ }
1631
+ await this.request(`/control/assignments/${existing.assignmentId}`, {
1632
+ method: "DELETE"
1633
+ });
1634
+ }
1635
+ } catch {
1636
+ }
1637
+ await this.request(`/control/subjects/${subjectId}/assignments`, {
1638
+ method: "POST",
1639
+ body: JSON.stringify({
1640
+ sandboxId,
1641
+ subjectId,
1642
+ permissionProfileId
1643
+ })
1644
+ });
1645
+ }
1646
+ /**
1647
+ * Sandbox management API
1648
+ */
1649
+ get sandboxes() {
1650
+ return {
1651
+ list: async () => {
1652
+ return this.request("/control/sandboxes");
1653
+ },
1654
+ get: async (id) => {
1655
+ return this.request(`/control/sandboxes/${id}`);
1656
+ },
1657
+ create: async (data) => {
1658
+ return this.request("/control/sandboxes", {
1659
+ method: "POST",
1660
+ body: JSON.stringify(data)
1661
+ });
1662
+ },
1663
+ update: async (id, data) => {
1664
+ return this.request(`/control/sandboxes/${id}`, {
1665
+ method: "PATCH",
1666
+ body: JSON.stringify(data)
1667
+ });
1668
+ },
1669
+ delete: async (id) => {
1670
+ return this.request(`/control/sandboxes/${id}`, {
1671
+ method: "DELETE"
1672
+ });
1673
+ }
1674
+ };
1675
+ }
1676
+ /**
1677
+ * Permission Profile management for sandboxes
1678
+ */
1679
+ get permissionProfiles() {
1680
+ return {
1681
+ list: async (sandboxId) => {
1682
+ const result = await this.request(
1683
+ `/control/sandboxes/${sandboxId}/permission-profiles`
1684
+ );
1685
+ return result.items;
1686
+ },
1687
+ get: async (sandboxId, profileId) => {
1688
+ return this.request(
1689
+ `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`
1690
+ );
1691
+ },
1692
+ create: async (sandboxId, data) => {
1693
+ return this.request(
1694
+ `/control/sandboxes/${sandboxId}/permission-profiles`,
1695
+ {
1696
+ method: "POST",
1697
+ body: JSON.stringify(data)
1698
+ }
1699
+ );
1700
+ },
1701
+ delete: async (sandboxId, profileId) => {
1702
+ return this.request(
1703
+ `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`,
1704
+ {
1705
+ method: "DELETE"
1706
+ }
1707
+ );
1708
+ }
1709
+ };
1710
+ }
1711
+ /**
1712
+ * Environment management
1713
+ */
1714
+ get environments() {
1715
+ return {
1716
+ list: async (sandboxId) => {
1717
+ const result = await this.request(
1718
+ `/control/sandboxes/${sandboxId}/environments`
1719
+ );
1720
+ return result.items;
1721
+ },
1722
+ get: async (environmentId) => {
1723
+ return this.request(`/control/environments/${environmentId}`);
1724
+ },
1725
+ create: async (sandboxId, data) => {
1726
+ return this.request(`/control/sandboxes/${sandboxId}/environments`, {
1727
+ method: "POST",
1728
+ body: JSON.stringify(data)
1729
+ });
1730
+ },
1731
+ delete: async (environmentId) => {
1732
+ return this.request(`/control/environments/${environmentId}`, {
1733
+ method: "DELETE"
1734
+ });
1735
+ }
1736
+ };
1737
+ }
1738
+ /**
1739
+ * Subject management
1740
+ */
1741
+ get subjects() {
1742
+ return {
1743
+ get: async (subjectId) => {
1744
+ return this.request(`/control/subjects/${subjectId}`);
1745
+ },
1746
+ listAssignments: async (subjectId) => {
1747
+ return this.request(`/control/subjects/${subjectId}/assignments`);
1748
+ }
1749
+ };
1750
+ }
1751
+ /**
1752
+ * @deprecated Use recordUser() instead
1753
+ */
1754
+ get users() {
1755
+ return {
1756
+ create: async (data) => {
1757
+ return this.request("/control/subjects", {
1758
+ method: "POST",
1759
+ body: JSON.stringify({
1760
+ identityId: data.id,
1761
+ name: data.name,
1762
+ email: data.email
1763
+ })
1764
+ });
1765
+ },
1766
+ get: async (id) => {
1767
+ return this.request(`/control/subjects/${id}`);
1768
+ }
1769
+ };
1770
+ }
1771
+ /**
1772
+ * Make an authenticated API request
1773
+ */
1774
+ async request(path, options = {}) {
1775
+ const url = `${this.httpUrl}${path}`;
1776
+ console.log(`[SDK] Requesting: ${url}`);
1777
+ const response = await fetch(url, {
1778
+ ...options,
1779
+ headers: {
1780
+ "Authorization": `Bearer ${this.apiKey}`,
1781
+ "Content-Type": "application/json",
1782
+ ...options.headers
1783
+ }
1784
+ });
1785
+ if (!response.ok) {
1786
+ const errorText = await response.text();
1787
+ throw new Error(`Granular API Error (${response.status}): ${errorText}`);
1788
+ }
1789
+ if (response.status === 204) {
1790
+ return { deleted: true };
1791
+ }
1792
+ return response.json();
1793
+ }
1794
+ };
1795
+
1796
+ exports.Environment = Environment;
1797
+ exports.Granular = Granular;
1798
+ exports.Session = Session;
1799
+ exports.WSClient = WSClient;
1800
+ //# sourceMappingURL=index.js.map
1801
+ //# sourceMappingURL=index.js.map