@granular-software/sdk 0.1.0 → 0.2.1
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/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 +8325 -0
- package/dist/index.d.mts +149 -4
- package/dist/index.d.ts +149 -4
- package/dist/index.js +441 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +441 -31
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D46q5WTh.d.mts → types-DiMEb3SE.d.mts} +41 -1
- package/dist/{types-D46q5WTh.d.ts → types-DiMEb3SE.d.ts} +41 -1
- package/package.json +18 -8
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
1
|
import * as Automerge from '@automerge/automerge';
|
|
3
2
|
|
|
4
3
|
// src/ws-client.ts
|
|
4
|
+
var GlobalWebSocket = void 0;
|
|
5
|
+
if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
6
|
+
GlobalWebSocket = globalThis.WebSocket;
|
|
7
|
+
}
|
|
8
|
+
var READY_STATE_OPEN = 1;
|
|
5
9
|
var WSClient = class {
|
|
6
10
|
ws = null;
|
|
7
11
|
url;
|
|
@@ -16,7 +20,9 @@ var WSClient = class {
|
|
|
16
20
|
syncState = Automerge.initSyncState();
|
|
17
21
|
reconnectTimer = null;
|
|
18
22
|
isExplicitlyDisconnected = false;
|
|
23
|
+
options;
|
|
19
24
|
constructor(options) {
|
|
25
|
+
this.options = options;
|
|
20
26
|
this.url = options.url;
|
|
21
27
|
this.sessionId = options.sessionId;
|
|
22
28
|
this.token = options.token;
|
|
@@ -27,34 +33,68 @@ var WSClient = class {
|
|
|
27
33
|
*/
|
|
28
34
|
async connect() {
|
|
29
35
|
this.isExplicitlyDisconnected = false;
|
|
36
|
+
const WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
|
|
37
|
+
if (!WebSocketClass) {
|
|
38
|
+
throw new Error('No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.');
|
|
39
|
+
}
|
|
30
40
|
return new Promise((resolve, reject) => {
|
|
31
41
|
try {
|
|
32
42
|
const wsUrl = new URL(this.url);
|
|
33
43
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
34
44
|
wsUrl.searchParams.set("token", this.token);
|
|
35
|
-
this.ws = new
|
|
36
|
-
this.ws
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
this.ws = new WebSocketClass(wsUrl.toString());
|
|
46
|
+
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
47
|
+
const socket = this.ws;
|
|
48
|
+
if (typeof socket.on === "function") {
|
|
49
|
+
socket.on("open", () => {
|
|
50
|
+
this.emit("open", {});
|
|
51
|
+
resolve();
|
|
52
|
+
});
|
|
53
|
+
socket.on("message", (data) => {
|
|
54
|
+
try {
|
|
55
|
+
const message = JSON.parse(data.toString());
|
|
56
|
+
this.handleMessage(message);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("[Granular] Failed to parse message:", error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
socket.on("error", (error) => {
|
|
62
|
+
this.emit("error", error);
|
|
63
|
+
if (socket.readyState !== READY_STATE_OPEN) {
|
|
64
|
+
reject(error);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
socket.on("close", () => {
|
|
68
|
+
this.emit("close", {});
|
|
69
|
+
this.handleDisconnect();
|
|
70
|
+
});
|
|
71
|
+
} else {
|
|
72
|
+
this.ws.onopen = () => {
|
|
73
|
+
this.emit("open", {});
|
|
74
|
+
resolve();
|
|
75
|
+
};
|
|
76
|
+
this.ws.onmessage = (event) => {
|
|
77
|
+
try {
|
|
78
|
+
const data = event.data;
|
|
79
|
+
const message = JSON.parse(data.toString());
|
|
80
|
+
this.handleMessage(message);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error("[Granular] Failed to parse message:", error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
this.ws.onerror = (event) => {
|
|
86
|
+
const error = new Error("WebSocket error");
|
|
87
|
+
error.event = event;
|
|
88
|
+
this.emit("error", error);
|
|
89
|
+
if (this.ws?.readyState !== READY_STATE_OPEN) {
|
|
90
|
+
reject(error);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
this.ws.onclose = () => {
|
|
94
|
+
this.emit("close", {});
|
|
95
|
+
this.handleDisconnect();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
58
98
|
} catch (error) {
|
|
59
99
|
reject(error);
|
|
60
100
|
}
|
|
@@ -74,8 +114,8 @@ var WSClient = class {
|
|
|
74
114
|
console.log("[Granular DEBUG] Received message:", JSON.stringify(message).slice(0, 500));
|
|
75
115
|
if ("type" in message && message.type === "sync") {
|
|
76
116
|
const syncMessage = message;
|
|
117
|
+
let bytes;
|
|
77
118
|
try {
|
|
78
|
-
let bytes;
|
|
79
119
|
const payload = syncMessage.message || syncMessage.data;
|
|
80
120
|
if (typeof payload === "string") {
|
|
81
121
|
const binaryString = atob(payload);
|
|
@@ -91,6 +131,7 @@ var WSClient = class {
|
|
|
91
131
|
} else {
|
|
92
132
|
return;
|
|
93
133
|
}
|
|
134
|
+
console.log("[Granular DEBUG] Applying sync bytes:", bytes.length);
|
|
94
135
|
const [newDoc, newSyncState] = Automerge.receiveSyncMessage(
|
|
95
136
|
this.doc,
|
|
96
137
|
this.syncState,
|
|
@@ -98,8 +139,49 @@ var WSClient = class {
|
|
|
98
139
|
);
|
|
99
140
|
this.doc = newDoc;
|
|
100
141
|
this.syncState = newSyncState;
|
|
142
|
+
const docAny = this.doc;
|
|
143
|
+
if (docAny.catalog) {
|
|
144
|
+
console.log("[Granular DEBUG] Doc catalog sync applied. Keys in catalog:", Object.keys(docAny.catalog || {}));
|
|
145
|
+
console.log("[Granular DEBUG] RawToolCatalogs:", Object.keys(docAny.catalog.rawToolCatalogs || {}));
|
|
146
|
+
} else {
|
|
147
|
+
console.log("[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:", Object.keys(docAny));
|
|
148
|
+
}
|
|
149
|
+
this.emit("sync", this.doc);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
try {
|
|
152
|
+
console.log("[Granular DEBUG] receiveSyncMessage failed, trying applyChanges...");
|
|
153
|
+
const [newDoc] = Automerge.applyChanges(this.doc, [bytes]);
|
|
154
|
+
this.doc = newDoc;
|
|
155
|
+
this.emit("sync", this.doc);
|
|
156
|
+
console.log("[Granular DEBUG] applyChanges succeeded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
|
|
157
|
+
} catch (applyError) {
|
|
158
|
+
console.warn("[Granular] Failed to apply sync message (both sync & applyChanges)", e, applyError);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if ("type" in message && message.type === "snapshot") {
|
|
164
|
+
const snapshotMessage = message;
|
|
165
|
+
try {
|
|
166
|
+
const bytes = new Uint8Array(snapshotMessage.data);
|
|
167
|
+
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
168
|
+
this.doc = Automerge.load(bytes);
|
|
169
|
+
this.emit("sync", this.doc);
|
|
170
|
+
console.log("[Granular DEBUG] Snapshot loaded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.warn("[Granular] Failed to load snapshot message", e);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if ("type" in message && message.type === "change") {
|
|
177
|
+
const changeMessage = message;
|
|
178
|
+
try {
|
|
179
|
+
const bytes = new Uint8Array(changeMessage.data);
|
|
180
|
+
const [newDoc] = Automerge.applyChanges(this.doc, [bytes]);
|
|
181
|
+
this.doc = newDoc;
|
|
101
182
|
this.emit("sync", this.doc);
|
|
102
183
|
} catch (e) {
|
|
184
|
+
console.warn("[Granular] Failed to apply change message", e);
|
|
103
185
|
}
|
|
104
186
|
return;
|
|
105
187
|
}
|
|
@@ -137,7 +219,7 @@ var WSClient = class {
|
|
|
137
219
|
* @throws {Error} If connection is closed or timeout occurs
|
|
138
220
|
*/
|
|
139
221
|
async call(method, params) {
|
|
140
|
-
if (!this.ws || this.ws.readyState !==
|
|
222
|
+
if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) {
|
|
141
223
|
throw new Error("WebSocket not connected");
|
|
142
224
|
}
|
|
143
225
|
const id = `rpc-${this.nextRpcId++}`;
|
|
@@ -266,6 +348,10 @@ var Session = class {
|
|
|
266
348
|
/** Tracks which tools are instance methods (className set, not static) */
|
|
267
349
|
instanceTools = /* @__PURE__ */ new Set();
|
|
268
350
|
currentDomainRevision = null;
|
|
351
|
+
/** Local effect registry: name → full ToolWithHandler */
|
|
352
|
+
effects = /* @__PURE__ */ new Map();
|
|
353
|
+
/** Last known tools for diffing */
|
|
354
|
+
lastKnownTools = /* @__PURE__ */ new Map();
|
|
269
355
|
constructor(client, clientId) {
|
|
270
356
|
this.client = client;
|
|
271
357
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
@@ -381,6 +467,85 @@ var Session = class {
|
|
|
381
467
|
rejected: result.rejected
|
|
382
468
|
};
|
|
383
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Publish a single effect (tool) to the sandbox.
|
|
472
|
+
*
|
|
473
|
+
* Adds the effect to the local registry and re-publishes the
|
|
474
|
+
* full tool catalog to the server. If an effect with the same
|
|
475
|
+
* name already exists, it is replaced.
|
|
476
|
+
*
|
|
477
|
+
* @param effect - The effect (tool) to publish
|
|
478
|
+
* @returns PublishToolsResult with domainRevision
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```typescript
|
|
482
|
+
* await env.publishEffect({
|
|
483
|
+
* name: 'get_bio',
|
|
484
|
+
* description: 'Get biography of an author',
|
|
485
|
+
* className: 'author',
|
|
486
|
+
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
487
|
+
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
488
|
+
* });
|
|
489
|
+
* ```
|
|
490
|
+
*/
|
|
491
|
+
async publishEffect(effect) {
|
|
492
|
+
this.effects.set(effect.name, effect);
|
|
493
|
+
return this._syncEffects();
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Publish multiple effects (tools) at once.
|
|
497
|
+
*
|
|
498
|
+
* Adds all effects to the local registry and re-publishes the
|
|
499
|
+
* full tool catalog in a single RPC call.
|
|
500
|
+
*
|
|
501
|
+
* @param effects - Array of effects to publish
|
|
502
|
+
* @returns PublishToolsResult with domainRevision
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```typescript
|
|
506
|
+
* await env.publishEffects([
|
|
507
|
+
* { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
|
|
508
|
+
* { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
|
|
509
|
+
* ]);
|
|
510
|
+
* ```
|
|
511
|
+
*/
|
|
512
|
+
async publishEffects(effects) {
|
|
513
|
+
for (const effect of effects) {
|
|
514
|
+
this.effects.set(effect.name, effect);
|
|
515
|
+
}
|
|
516
|
+
return this._syncEffects();
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Remove an effect by name and re-publish the remaining catalog.
|
|
520
|
+
*
|
|
521
|
+
* @param name - The name of the effect to remove
|
|
522
|
+
* @returns PublishToolsResult with domainRevision
|
|
523
|
+
*/
|
|
524
|
+
async unpublishEffect(name) {
|
|
525
|
+
this.effects.delete(name);
|
|
526
|
+
this.toolHandlers.delete(name);
|
|
527
|
+
this.instanceTools.delete(name);
|
|
528
|
+
return this._syncEffects();
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Remove all effects and publish an empty catalog.
|
|
532
|
+
*
|
|
533
|
+
* @returns PublishToolsResult with domainRevision
|
|
534
|
+
*/
|
|
535
|
+
async unpublishAllEffects() {
|
|
536
|
+
this.effects.clear();
|
|
537
|
+
this.toolHandlers.clear();
|
|
538
|
+
this.instanceTools.clear();
|
|
539
|
+
return this._syncEffects();
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Internal: re-publish the full effect catalog to the server.
|
|
543
|
+
* Called after any mutation to the effects Map.
|
|
544
|
+
*/
|
|
545
|
+
async _syncEffects() {
|
|
546
|
+
const allEffects = Array.from(this.effects.values());
|
|
547
|
+
return this.publishTools(allEffects);
|
|
548
|
+
}
|
|
384
549
|
/**
|
|
385
550
|
* Submit a job to execute code in the sandbox.
|
|
386
551
|
*
|
|
@@ -398,9 +563,9 @@ var Session = class {
|
|
|
398
563
|
* execute locally and return the result to the sandbox.
|
|
399
564
|
*/
|
|
400
565
|
async submitJob(code, domainRevision) {
|
|
401
|
-
const revision = domainRevision || this.currentDomainRevision;
|
|
566
|
+
const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
|
|
402
567
|
if (!revision) {
|
|
403
|
-
throw new Error("No domain revision available. Call publishTools()
|
|
568
|
+
throw new Error("No domain revision available. Call publishTools() or ensure schema is activated.");
|
|
404
569
|
}
|
|
405
570
|
const result = await this.client.call("job.submit", {
|
|
406
571
|
domainRevision: revision,
|
|
@@ -431,6 +596,72 @@ var Session = class {
|
|
|
431
596
|
async answerPrompt(promptId, answer) {
|
|
432
597
|
await this.client.call("prompt.answer", { promptId, value: answer });
|
|
433
598
|
}
|
|
599
|
+
/**
|
|
600
|
+
* Get the current list of available tools.
|
|
601
|
+
* Consolidates tools from all connected clients.
|
|
602
|
+
*/
|
|
603
|
+
getTools() {
|
|
604
|
+
const doc = this.client.doc;
|
|
605
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
606
|
+
const domainPkg = doc.domain?.packages?.domain;
|
|
607
|
+
if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
|
|
608
|
+
for (const tool of domainPkg.tools) {
|
|
609
|
+
if (!tool?.name) continue;
|
|
610
|
+
toolMap.set(tool.name, {
|
|
611
|
+
name: tool.name,
|
|
612
|
+
description: tool.description,
|
|
613
|
+
inputSchema: tool.inputSchema,
|
|
614
|
+
outputSchema: tool.outputSchema,
|
|
615
|
+
className: tool.className || void 0,
|
|
616
|
+
static: tool.static || false,
|
|
617
|
+
ready: false,
|
|
618
|
+
publishedAt: void 0
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const catalogs = doc.catalog?.rawToolCatalogs || {};
|
|
623
|
+
for (const [clientId, catalog] of Object.entries(catalogs)) {
|
|
624
|
+
const cat = catalog;
|
|
625
|
+
if (!cat.tools) continue;
|
|
626
|
+
for (const tool of cat.tools) {
|
|
627
|
+
if (!tool?.name) continue;
|
|
628
|
+
const existing = toolMap.get(tool.name);
|
|
629
|
+
if (existing?.publishedAt && cat.publishedAt && existing.publishedAt > cat.publishedAt) continue;
|
|
630
|
+
const isLocal = clientId === this.clientId;
|
|
631
|
+
const ready = isLocal ? this.effects.has(tool.name) || this.toolHandlers.has(tool.name) || this.instanceTools.has(tool.name) : true;
|
|
632
|
+
toolMap.set(tool.name, {
|
|
633
|
+
name: tool.name,
|
|
634
|
+
description: tool.description,
|
|
635
|
+
inputSchema: tool.inputSchema,
|
|
636
|
+
outputSchema: tool.outputSchema,
|
|
637
|
+
className: tool.className || existing?.className,
|
|
638
|
+
static: tool.static || existing?.static || false,
|
|
639
|
+
clientId,
|
|
640
|
+
ready,
|
|
641
|
+
publishedAt: cat.publishedAt
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return Array.from(toolMap.values());
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Subscribe to tool changes (added, removed, updated).
|
|
649
|
+
* @param callback - Function called with change events
|
|
650
|
+
* @returns Unsubscribe function
|
|
651
|
+
*/
|
|
652
|
+
onToolsChanged(callback) {
|
|
653
|
+
const handler = (data) => callback(data);
|
|
654
|
+
if (!this.eventListeners.has("tools:changed")) {
|
|
655
|
+
this.eventListeners.set("tools:changed", []);
|
|
656
|
+
}
|
|
657
|
+
this.eventListeners.get("tools:changed").push(handler);
|
|
658
|
+
return () => {
|
|
659
|
+
const listeners = this.eventListeners.get("tools:changed");
|
|
660
|
+
if (listeners) {
|
|
661
|
+
this.eventListeners.set("tools:changed", listeners.filter((h) => h !== handler));
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
434
665
|
/**
|
|
435
666
|
* Get the current domain state and available tools
|
|
436
667
|
*/
|
|
@@ -639,7 +870,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
639
870
|
});
|
|
640
871
|
}
|
|
641
872
|
setupEventHandlers() {
|
|
642
|
-
this.client.on("sync", (doc) =>
|
|
873
|
+
this.client.on("sync", (doc) => {
|
|
874
|
+
this.emit("sync", doc);
|
|
875
|
+
this.checkForToolChanges();
|
|
876
|
+
});
|
|
643
877
|
this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
|
|
644
878
|
this.client.on("disconnect", () => this.emit("disconnect", {}));
|
|
645
879
|
this.client.on("job.status", (data) => {
|
|
@@ -658,6 +892,33 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
658
892
|
handlers.forEach((h) => h(data));
|
|
659
893
|
}
|
|
660
894
|
}
|
|
895
|
+
/**
|
|
896
|
+
* Check for changes in the tool catalog and emit 'tools:changed' if needed
|
|
897
|
+
*/
|
|
898
|
+
checkForToolChanges() {
|
|
899
|
+
const currentTools = this.getTools();
|
|
900
|
+
const currentMap = new Map(currentTools.map((t) => [t.name, t]));
|
|
901
|
+
const added = [];
|
|
902
|
+
const removed = [];
|
|
903
|
+
for (const [name, tool] of currentMap) {
|
|
904
|
+
if (!this.lastKnownTools.has(name)) {
|
|
905
|
+
added.push(name);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
for (const name of this.lastKnownTools.keys()) {
|
|
909
|
+
if (!currentMap.has(name)) {
|
|
910
|
+
removed.push(name);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
if (added.length > 0 || removed.length > 0) {
|
|
914
|
+
this.lastKnownTools = currentMap;
|
|
915
|
+
this.emit("tools:changed", {
|
|
916
|
+
tools: currentTools,
|
|
917
|
+
added,
|
|
918
|
+
removed
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
661
922
|
};
|
|
662
923
|
var JobImplementation = class {
|
|
663
924
|
id;
|
|
@@ -731,6 +992,18 @@ var JobImplementation = class {
|
|
|
731
992
|
this._rejectResult(jobData.error || new Error("Job failed"));
|
|
732
993
|
}
|
|
733
994
|
});
|
|
995
|
+
this.client.on("tool.call.start", (data) => {
|
|
996
|
+
const d = data;
|
|
997
|
+
if (d.jobId === id) {
|
|
998
|
+
this.emit("toolCallStart", { callId: d.callId, toolName: d.toolName, input: d.input, timestamp: d.timestamp });
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
this.client.on("tool.call.end", (data) => {
|
|
1002
|
+
const d = data;
|
|
1003
|
+
if (d.jobId === id) {
|
|
1004
|
+
this.emit("toolCallEnd", { callId: d.callId, toolName: d.toolName, result: d.result, error: d.error, durationMs: d.durationMs, timestamp: d.timestamp });
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
734
1007
|
}
|
|
735
1008
|
get result() {
|
|
736
1009
|
return this._resultPromise;
|
|
@@ -1445,11 +1718,57 @@ var Environment = class _Environment extends Session {
|
|
|
1445
1718
|
async publishTools(tools, revision = "1.0.0") {
|
|
1446
1719
|
return super.publishTools(tools, revision);
|
|
1447
1720
|
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Publish a single effect (tool) incrementally.
|
|
1723
|
+
*
|
|
1724
|
+
* Adds the effect to the local registry and re-publishes the
|
|
1725
|
+
* full tool catalog to the server. If an effect with the same
|
|
1726
|
+
* name already exists, it is replaced.
|
|
1727
|
+
*
|
|
1728
|
+
* @example
|
|
1729
|
+
* ```typescript
|
|
1730
|
+
* await env.publishEffect({
|
|
1731
|
+
* name: 'get_bio',
|
|
1732
|
+
* description: 'Get biography',
|
|
1733
|
+
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
1734
|
+
* handler: async (params) => ({ bio: 'Hello' }),
|
|
1735
|
+
* });
|
|
1736
|
+
* ```
|
|
1737
|
+
*/
|
|
1738
|
+
async publishEffect(effect) {
|
|
1739
|
+
return super.publishEffect(effect);
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Publish multiple effects (tools) at once.
|
|
1743
|
+
*
|
|
1744
|
+
* Adds all effects to the local registry and re-publishes the
|
|
1745
|
+
* full tool catalog in a single RPC call.
|
|
1746
|
+
*/
|
|
1747
|
+
async publishEffects(effects) {
|
|
1748
|
+
return super.publishEffects(effects);
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Remove an effect by name and re-publish the remaining catalog.
|
|
1752
|
+
*/
|
|
1753
|
+
async unpublishEffect(name) {
|
|
1754
|
+
return super.unpublishEffect(name);
|
|
1755
|
+
}
|
|
1756
|
+
/**
|
|
1757
|
+
* Remove all effects and publish an empty catalog.
|
|
1758
|
+
*/
|
|
1759
|
+
async unpublishAllEffects() {
|
|
1760
|
+
return super.unpublishAllEffects();
|
|
1761
|
+
}
|
|
1448
1762
|
};
|
|
1449
1763
|
var Granular = class {
|
|
1450
1764
|
apiKey;
|
|
1451
1765
|
apiUrl;
|
|
1452
1766
|
httpUrl;
|
|
1767
|
+
WebSocketCtor;
|
|
1768
|
+
/** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
|
|
1769
|
+
sandboxEffects = /* @__PURE__ */ new Map();
|
|
1770
|
+
/** Active environments tracker: sandboxId → Environment[] */
|
|
1771
|
+
activeEnvironments = /* @__PURE__ */ new Map();
|
|
1453
1772
|
/**
|
|
1454
1773
|
* Create a new Granular client
|
|
1455
1774
|
* @param options - Client configuration
|
|
@@ -1457,6 +1776,7 @@ var Granular = class {
|
|
|
1457
1776
|
constructor(options) {
|
|
1458
1777
|
this.apiKey = options.apiKey;
|
|
1459
1778
|
this.apiUrl = options.apiUrl || "wss://api.granular.dev/v2/ws";
|
|
1779
|
+
this.WebSocketCtor = options.WebSocketCtor;
|
|
1460
1780
|
this.httpUrl = this.apiUrl.replace("wss://", "https://").replace("/ws", "");
|
|
1461
1781
|
}
|
|
1462
1782
|
/**
|
|
@@ -1527,7 +1847,7 @@ var Granular = class {
|
|
|
1527
1847
|
* ```
|
|
1528
1848
|
*/
|
|
1529
1849
|
async connect(options) {
|
|
1530
|
-
const clientId = `client_${Date.now()}`;
|
|
1850
|
+
const clientId = options.clientId || `client_${Date.now()}`;
|
|
1531
1851
|
const sandbox = await this.findOrCreateSandbox(options.sandbox);
|
|
1532
1852
|
for (const profileName of options.user.permissions) {
|
|
1533
1853
|
const profileId = await this.ensurePermissionProfile(sandbox.sandboxId, profileName);
|
|
@@ -1540,14 +1860,104 @@ var Granular = class {
|
|
|
1540
1860
|
const client = new WSClient({
|
|
1541
1861
|
url: this.apiUrl,
|
|
1542
1862
|
sessionId: envData.environmentId,
|
|
1543
|
-
token: this.apiKey
|
|
1863
|
+
token: this.apiKey,
|
|
1864
|
+
WebSocketCtor: this.WebSocketCtor
|
|
1544
1865
|
});
|
|
1545
1866
|
await client.connect();
|
|
1546
1867
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
1547
1868
|
const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
|
|
1869
|
+
if (!this.activeEnvironments.has(sandbox.sandboxId)) {
|
|
1870
|
+
this.activeEnvironments.set(sandbox.sandboxId, []);
|
|
1871
|
+
}
|
|
1872
|
+
this.activeEnvironments.get(sandbox.sandboxId).push(environment);
|
|
1873
|
+
environment.on("disconnect", () => {
|
|
1874
|
+
const list = this.activeEnvironments.get(sandbox.sandboxId);
|
|
1875
|
+
if (list) {
|
|
1876
|
+
this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
|
|
1877
|
+
}
|
|
1878
|
+
});
|
|
1548
1879
|
await environment.hello();
|
|
1880
|
+
const effects = this.sandboxEffects.get(sandbox.sandboxId);
|
|
1881
|
+
if (effects && effects.size > 0) {
|
|
1882
|
+
const effectsList = Array.from(effects.values());
|
|
1883
|
+
console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
|
|
1884
|
+
await environment.publishEffects(effectsList);
|
|
1885
|
+
}
|
|
1549
1886
|
return environment;
|
|
1550
1887
|
}
|
|
1888
|
+
// ── Sandbox-Level Effects ──
|
|
1889
|
+
/**
|
|
1890
|
+
* Register an effect (tool) for a specific sandbox.
|
|
1891
|
+
*
|
|
1892
|
+
* The effect will be automatically published to:
|
|
1893
|
+
* 1. Any currently active environments for this sandbox
|
|
1894
|
+
* 2. Any new environments created/connected for this sandbox
|
|
1895
|
+
*
|
|
1896
|
+
* @param sandboxNameOrId - The name or ID of the sandbox
|
|
1897
|
+
* @param effect - The tool definition and handler
|
|
1898
|
+
*/
|
|
1899
|
+
async registerEffect(sandboxNameOrId, effect) {
|
|
1900
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1901
|
+
const sandboxId = sandbox.sandboxId;
|
|
1902
|
+
if (!this.sandboxEffects.has(sandboxId)) {
|
|
1903
|
+
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
1904
|
+
}
|
|
1905
|
+
this.sandboxEffects.get(sandboxId).set(effect.name, effect);
|
|
1906
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1907
|
+
for (const env of activeEnvs) {
|
|
1908
|
+
await env.publishEffect(effect);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
/**
|
|
1912
|
+
* Register multiple effects (tools) for a specific sandbox.
|
|
1913
|
+
*
|
|
1914
|
+
* batch version of `registerEffect`.
|
|
1915
|
+
*/
|
|
1916
|
+
async registerEffects(sandboxNameOrId, effects) {
|
|
1917
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1918
|
+
const sandboxId = sandbox.sandboxId;
|
|
1919
|
+
if (!this.sandboxEffects.has(sandboxId)) {
|
|
1920
|
+
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
1921
|
+
}
|
|
1922
|
+
const map = this.sandboxEffects.get(sandboxId);
|
|
1923
|
+
for (const effect of effects) {
|
|
1924
|
+
map.set(effect.name, effect);
|
|
1925
|
+
}
|
|
1926
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1927
|
+
for (const env of activeEnvs) {
|
|
1928
|
+
await env.publishEffects(effects);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* Unregister an effect from a sandbox.
|
|
1933
|
+
*
|
|
1934
|
+
* Removes it from the local registry and unpublishes it from
|
|
1935
|
+
* all active environments.
|
|
1936
|
+
*/
|
|
1937
|
+
async unregisterEffect(sandboxNameOrId, name) {
|
|
1938
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1939
|
+
const sandboxId = sandbox.sandboxId;
|
|
1940
|
+
const map = this.sandboxEffects.get(sandboxId);
|
|
1941
|
+
if (map) {
|
|
1942
|
+
map.delete(name);
|
|
1943
|
+
}
|
|
1944
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1945
|
+
for (const env of activeEnvs) {
|
|
1946
|
+
await env.unpublishEffect(name);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Unregister all effects for a sandbox.
|
|
1951
|
+
*/
|
|
1952
|
+
async unregisterAllEffects(sandboxNameOrId) {
|
|
1953
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1954
|
+
const sandboxId = sandbox.sandboxId;
|
|
1955
|
+
this.sandboxEffects.delete(sandboxId);
|
|
1956
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1957
|
+
for (const env of activeEnvs) {
|
|
1958
|
+
await env.unpublishAllEffects();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1551
1961
|
/**
|
|
1552
1962
|
* Find a sandbox by name or create it if it doesn't exist
|
|
1553
1963
|
*/
|