@castari/sdk 0.0.3 โ 0.0.4
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 +39 -0
- package/dist/client.d.ts +8 -39
- package/dist/client.js +39 -217
- package/package.json +2 -3
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# @castari/sdk
|
|
2
|
+
|
|
3
|
+
The SDK for building Castari agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @castari/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
See the [SDK Reference](../../docs/sdk-reference.md) for detailed documentation.
|
|
14
|
+
|
|
15
|
+
### Defining an Agent
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { serve, tool } from '@castari/sdk'
|
|
19
|
+
|
|
20
|
+
const myTool = tool({ ... })
|
|
21
|
+
|
|
22
|
+
serve({
|
|
23
|
+
tools: [myTool],
|
|
24
|
+
systemPrompt: 'You are a helpful assistant.'
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Connecting to an Agent
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { CastariClient } from '@castari/sdk/client'
|
|
32
|
+
|
|
33
|
+
const client = new CastariClient({
|
|
34
|
+
snapshot: 'my-agent',
|
|
35
|
+
// platformUrl: '...' // Optional
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await client.start()
|
|
39
|
+
```
|
package/dist/client.d.ts
CHANGED
|
@@ -1,64 +1,33 @@
|
|
|
1
1
|
import type { QueryConfig, WSInputMessage, WSOutputMessage } from './types';
|
|
2
2
|
export * from './types';
|
|
3
|
-
type DaytonaCreateOptions = {
|
|
4
|
-
snapshot?: string;
|
|
5
|
-
image?: unknown;
|
|
6
|
-
resources?: {
|
|
7
|
-
cpu?: number;
|
|
8
|
-
memory?: number;
|
|
9
|
-
disk?: number;
|
|
10
|
-
};
|
|
11
|
-
autoStopInterval?: number;
|
|
12
|
-
autoArchiveInterval?: number;
|
|
13
|
-
autoDeleteInterval?: number;
|
|
14
|
-
ephemeral?: boolean;
|
|
15
|
-
public?: boolean;
|
|
16
|
-
volumes?: {
|
|
17
|
-
volumeId: string;
|
|
18
|
-
mountPath: string;
|
|
19
|
-
}[];
|
|
20
|
-
};
|
|
21
3
|
/**
|
|
22
4
|
* Configuration options for the Castari Client.
|
|
23
5
|
*/
|
|
24
6
|
export interface ClientOptions extends Partial<QueryConfig> {
|
|
25
|
-
/** Local/custom connection URL (e.g., 'http://localhost:3000'). If omitted,
|
|
7
|
+
/** Local/custom connection URL (e.g., 'http://localhost:3000'). If omitted, Platform mode is used. */
|
|
26
8
|
connectionUrl?: string;
|
|
27
9
|
/** Anthropic API key (required unless present in process.env.ANTHROPIC_API_KEY) */
|
|
28
10
|
anthropicApiKey?: string;
|
|
29
11
|
/** Enable debug logging */
|
|
30
12
|
debug?: boolean;
|
|
31
|
-
|
|
32
|
-
daytonaApiUrl?: string;
|
|
33
|
-
daytonaTarget?: string;
|
|
34
|
-
/** Override to skip preview discovery; should be base URL to server (e.g., https://3000-<sandboxId>.<domain>) */
|
|
35
|
-
daytonaConnectionUrl?: string;
|
|
13
|
+
/** Snapshot name to deploy/start */
|
|
36
14
|
snapshot?: string;
|
|
37
|
-
image?: unknown;
|
|
38
|
-
/** Optional override for preview domain if provider metadata is missing. Example: "preview.daytona.io" */
|
|
39
|
-
daytonaPreviewDomain?: string;
|
|
40
|
-
resources?: DaytonaCreateOptions['resources'];
|
|
41
|
-
autoStopInterval?: number;
|
|
42
|
-
autoArchiveInterval?: number;
|
|
43
|
-
autoDeleteInterval?: number;
|
|
44
|
-
ephemeral?: boolean;
|
|
45
|
-
/** Optional Daytona volume name to mount at /home/daytona/agent-workspace. Defaults to undefined (no volume). */
|
|
46
|
-
volume?: string;
|
|
47
15
|
/** Optional labels to apply to the sandbox (and filter by for reuse) */
|
|
48
16
|
labels?: Record<string, string>;
|
|
49
|
-
/**
|
|
50
|
-
|
|
17
|
+
/** Optional volume name to mount at /home/daytona/agent-workspace */
|
|
18
|
+
volume?: string;
|
|
19
|
+
/** Castari Platform API URL. Defaults to https://api.castari.com (or localhost in dev) */
|
|
20
|
+
platformUrl?: string;
|
|
51
21
|
}
|
|
52
22
|
export declare class CastariClient {
|
|
53
23
|
private ws?;
|
|
54
24
|
private options;
|
|
55
25
|
private messageHandlers;
|
|
56
|
-
private
|
|
57
|
-
private daytona?;
|
|
26
|
+
private sandboxId?;
|
|
58
27
|
constructor(options?: ClientOptions);
|
|
59
28
|
start(): Promise<void>;
|
|
60
29
|
private setupLocalConnection;
|
|
61
|
-
private
|
|
30
|
+
private setupPlatformConnection;
|
|
62
31
|
private handleMessage;
|
|
63
32
|
onMessage(handler: (message: WSOutputMessage) => void): () => void;
|
|
64
33
|
send(message: WSInputMessage): void;
|
package/dist/client.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
const DEFAULT_LOCAL_URL = 'http://localhost:3000';
|
|
3
|
-
const DEFAULT_WORKSPACE_MOUNT = '/home/daytona/agent-workspace';
|
|
4
3
|
export class CastariClient {
|
|
5
4
|
ws;
|
|
6
5
|
options;
|
|
7
6
|
messageHandlers = [];
|
|
8
|
-
|
|
9
|
-
daytona;
|
|
7
|
+
sandboxId;
|
|
10
8
|
constructor(options = {}) {
|
|
11
9
|
this.options = {
|
|
12
10
|
...options,
|
|
@@ -19,7 +17,7 @@ export class CastariClient {
|
|
|
19
17
|
}
|
|
20
18
|
const connection = this.options.connectionUrl
|
|
21
19
|
? await this.setupLocalConnection()
|
|
22
|
-
: await this.
|
|
20
|
+
: await this.setupPlatformConnection();
|
|
23
21
|
if (this.options.debug) {
|
|
24
22
|
console.log(`๐ก Configuring server at ${connection.configUrl}...`);
|
|
25
23
|
}
|
|
@@ -113,212 +111,39 @@ export class CastariClient {
|
|
|
113
111
|
wsUrl: `${baseUrl.replace('http://', 'ws://').replace('https://', 'wss://')}/ws`,
|
|
114
112
|
};
|
|
115
113
|
}
|
|
116
|
-
async
|
|
117
|
-
const
|
|
118
|
-
const apiKey = this.options.daytonaApiKey || process.env.DAYTONA_API_KEY;
|
|
119
|
-
if (!apiKey) {
|
|
120
|
-
throw new Error('DAYTONA_API_KEY is required for Daytona mode');
|
|
121
|
-
}
|
|
122
|
-
this.daytona = new Daytona({
|
|
123
|
-
apiKey,
|
|
124
|
-
apiUrl: this.options.daytonaApiUrl || process.env.DAYTONA_API_URL,
|
|
125
|
-
target: this.options.daytonaTarget || process.env.DAYTONA_TARGET,
|
|
126
|
-
});
|
|
127
|
-
const volumeName = this.options.volume || this.options.workspaceVolumeName;
|
|
128
|
-
let volumeMounts = [];
|
|
129
|
-
if (volumeName) {
|
|
130
|
-
const volumeService = this.daytona.volume;
|
|
131
|
-
if (volumeService && typeof volumeService.get === 'function') {
|
|
132
|
-
// Ensure volume exists
|
|
133
|
-
let volume;
|
|
134
|
-
try {
|
|
135
|
-
volume = await volumeService.get(volumeName, true); // true = create if missing
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
// If get fails, try create explicitly if the SDK requires it, but usually get(name, true) handles it.
|
|
139
|
-
// Assuming get(name, true) works as per previous implementation.
|
|
140
|
-
console.error('Failed to get/create volume:', err);
|
|
141
|
-
throw err;
|
|
142
|
-
}
|
|
143
|
-
volumeMounts = [
|
|
144
|
-
{
|
|
145
|
-
volumeId: volume.id,
|
|
146
|
-
mountPath: DEFAULT_WORKSPACE_MOUNT,
|
|
147
|
-
},
|
|
148
|
-
];
|
|
149
|
-
if (this.options.debug) {
|
|
150
|
-
console.log(`๐๏ธ Using Daytona volume ${volume.id} (${volumeName}) at ${DEFAULT_WORKSPACE_MOUNT}`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
console.warn('Daytona SDK does not expose volume.get; skipping volume mount. Update @daytonaio/sdk or disable volume.');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
114
|
+
async setupPlatformConnection() {
|
|
115
|
+
const platformUrl = this.options.platformUrl || process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
|
|
157
116
|
if (this.options.debug) {
|
|
158
|
-
console.log(
|
|
159
|
-
if (volumeName) {
|
|
160
|
-
console.log(`๐ฆ Mounting volume ${volumeName} at ${DEFAULT_WORKSPACE_MOUNT}`);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
console.log('๐ฆ No volume configured; using ephemeral container filesystem.');
|
|
164
|
-
}
|
|
117
|
+
console.log(`๐ Requesting sandbox from ${platformUrl}...`);
|
|
165
118
|
}
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
autoArchiveInterval: this.options.autoArchiveInterval,
|
|
172
|
-
autoDeleteInterval: this.options.autoDeleteInterval,
|
|
173
|
-
ephemeral: this.options.ephemeral,
|
|
174
|
-
public: true,
|
|
175
|
-
volumes: volumeMounts,
|
|
176
|
-
};
|
|
177
|
-
// Check for existing sandbox if labels are provided
|
|
178
|
-
if (this.options.labels) {
|
|
179
|
-
const existing = await this.daytona.list(this.options.labels);
|
|
180
|
-
if (existing.items.length > 0) {
|
|
181
|
-
// Use the first matching sandbox
|
|
182
|
-
this.sandbox = existing.items[0];
|
|
183
|
-
if (this.options.debug) {
|
|
184
|
-
console.log(`โป๏ธ Found existing sandbox: ${this.sandbox.id} (${this.sandbox.state})`);
|
|
185
|
-
}
|
|
186
|
-
// Ensure it's started
|
|
187
|
-
if (this.sandbox.state !== 'started') {
|
|
188
|
-
if (this.options.debug) {
|
|
189
|
-
console.log('โถ๏ธ Starting existing sandbox...');
|
|
190
|
-
}
|
|
191
|
-
await this.sandbox.start();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
// Create new if not found
|
|
196
|
-
if (!this.sandbox) {
|
|
197
|
-
// Add labels to create params
|
|
198
|
-
const paramsWithLabels = {
|
|
199
|
-
...createParams,
|
|
119
|
+
const response = await fetch(`${platformUrl}/sandbox/start`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
snapshot: this.options.snapshot,
|
|
200
124
|
labels: this.options.labels,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
// Wait for sandbox to reach STARTED state before interacting with it
|
|
208
|
-
const sandboxAny = this.sandbox;
|
|
209
|
-
if (typeof sandboxAny.waitUntilStarted === 'function') {
|
|
210
|
-
if (this.options.debug) {
|
|
211
|
-
console.log('โณ Waiting for sandbox to reach STARTED state...');
|
|
212
|
-
}
|
|
213
|
-
await sandboxAny.waitUntilStarted(60);
|
|
214
|
-
}
|
|
215
|
-
// Start the Castari server process inside the sandbox.
|
|
216
|
-
// We keep the container's default ENTRYPOINT as a no-op ('sleep infinity') for snapshot validation,
|
|
217
|
-
// and explicitly start the server here via Daytona's process API, invoking the server entrypoint directly.
|
|
218
|
-
const serverCommand = `sh -lc "cd /home/daytona/app && CASTARI_WORKSPACE=${DEFAULT_WORKSPACE_MOUNT} bun start"`;
|
|
219
|
-
try {
|
|
220
|
-
const sessionId = `castari-server-${this.sandbox.id}`;
|
|
221
|
-
const processApi = this.sandbox.process;
|
|
222
|
-
if (processApi &&
|
|
223
|
-
typeof processApi.createSession === 'function' &&
|
|
224
|
-
typeof processApi.executeSessionCommand === 'function') {
|
|
225
|
-
if (this.options.debug) {
|
|
226
|
-
console.log(`โถ๏ธ Creating process session ${sessionId} and starting server...`);
|
|
227
|
-
}
|
|
228
|
-
await processApi.createSession(sessionId);
|
|
229
|
-
await processApi.executeSessionCommand(sessionId, {
|
|
230
|
-
command: serverCommand,
|
|
231
|
-
async: true,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
else if (processApi && typeof processApi.executeCommand === 'function') {
|
|
235
|
-
if (this.options.debug) {
|
|
236
|
-
console.log('โถ๏ธ Starting server via executeCommand...');
|
|
237
|
-
}
|
|
238
|
-
await processApi.executeCommand({
|
|
239
|
-
command: serverCommand,
|
|
240
|
-
async: true,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
244
|
-
console.warn('Daytona SDK process API is not available; unable to start server automatically inside sandbox.');
|
|
245
|
-
}
|
|
246
|
-
// Give the server a brief moment to start listening on port 3000 before configuring it.
|
|
247
|
-
if (this.options.debug) {
|
|
248
|
-
console.log('โณ Waiting briefly for Castari server to start inside sandbox...');
|
|
249
|
-
}
|
|
250
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
console.error('Failed to start Castari server inside sandbox:', err);
|
|
125
|
+
volume: this.options.volume
|
|
126
|
+
})
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const errorText = await response.text();
|
|
130
|
+
throw new Error(`Failed to start sandbox: ${errorText}`);
|
|
254
131
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Try getPreviewUrl (newer SDKs), then getPreviewLink (older SDKs)
|
|
260
|
-
let preview = null;
|
|
261
|
-
if (typeof this.sandbox.getPreviewUrl === 'function') {
|
|
262
|
-
try {
|
|
263
|
-
preview = await this.sandbox.getPreviewUrl(3000);
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
if (this.options.debug) {
|
|
267
|
-
console.warn('โ ๏ธ getPreviewUrl failed; will fall back to getPreviewLink or manual domain.', err);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (!preview?.url && typeof this.sandbox.getPreviewLink === 'function') {
|
|
272
|
-
try {
|
|
273
|
-
preview = await this.sandbox.getPreviewLink(3000);
|
|
274
|
-
}
|
|
275
|
-
catch (err) {
|
|
276
|
-
if (this.options.debug) {
|
|
277
|
-
console.warn('โ ๏ธ getPreviewLink failed; will attempt fallback domain if provided.', err);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
baseUrl = preview?.url || null;
|
|
282
|
-
previewToken = preview?.token;
|
|
283
|
-
if (!baseUrl) {
|
|
284
|
-
// Fallback: construct from provided preview domain if available
|
|
285
|
-
const previewDomain = this.options.daytonaPreviewDomain || process.env.DAYTONA_PREVIEW_DOMAIN;
|
|
286
|
-
if (previewDomain) {
|
|
287
|
-
baseUrl = `https://${3000}-${this.sandbox.id}.${previewDomain}`;
|
|
288
|
-
if (this.options.debug) {
|
|
289
|
-
console.warn(`โ ๏ธ Preview URL not provided by SDK; using fallback domain ${previewDomain}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
throw new Error('Failed to get preview URL from Daytona sandbox. Provide daytonaConnectionUrl or DAYTONA_PREVIEW_DOMAIN.');
|
|
294
|
-
}
|
|
295
|
-
}
|
|
132
|
+
const { id, url, token } = await response.json();
|
|
133
|
+
this.sandboxId = id;
|
|
134
|
+
if (this.options.debug) {
|
|
135
|
+
console.log(`โ
Sandbox started: ${id} at ${url}`);
|
|
296
136
|
}
|
|
297
|
-
baseUrl =
|
|
137
|
+
const baseUrl = url.replace(/\/$/, '');
|
|
298
138
|
const configUrl = `${baseUrl.replace('ws://', 'http://').replace('wss://', 'https://')}/config`;
|
|
299
139
|
const wsUrlBase = `${baseUrl.replace('https://', 'wss://').replace('http://', 'ws://')}/ws`;
|
|
300
140
|
return {
|
|
301
141
|
configUrl,
|
|
302
142
|
wsUrl: wsUrlBase,
|
|
303
|
-
previewToken,
|
|
143
|
+
previewToken: token,
|
|
304
144
|
cleanup: async () => {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (this.options.debug)
|
|
308
|
-
console.log('๐งน Sandbox deleted');
|
|
309
|
-
}
|
|
310
|
-
catch (err) {
|
|
311
|
-
const msg = err?.response?.data?.message || err?.message || String(err);
|
|
312
|
-
if (msg.includes('state change in progress')) {
|
|
313
|
-
if (this.options.debug) {
|
|
314
|
-
console.warn('โ ๏ธ Sandbox deletion skipped (state change in progress)');
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
console.error('Failed to delete sandbox:', err);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
},
|
|
145
|
+
await this.stop({ delete: true });
|
|
146
|
+
}
|
|
322
147
|
};
|
|
323
148
|
}
|
|
324
149
|
handleMessage(message) {
|
|
@@ -343,29 +168,26 @@ export class CastariClient {
|
|
|
343
168
|
if (this.ws) {
|
|
344
169
|
this.ws.close();
|
|
345
170
|
}
|
|
346
|
-
if (this.
|
|
171
|
+
if (this.sandboxId) {
|
|
172
|
+
const platformUrl = this.options.platformUrl || process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
|
|
347
173
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
174
|
+
const response = await fetch(`${platformUrl}/sandbox/stop`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
sandboxId: this.sandboxId,
|
|
179
|
+
delete: options.delete
|
|
180
|
+
})
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
console.error(`Failed to stop sandbox: ${await response.text()}`);
|
|
352
184
|
}
|
|
353
|
-
else {
|
|
354
|
-
|
|
355
|
-
if (this.options.debug)
|
|
356
|
-
console.log('๐ Sandbox stopped (preserved)');
|
|
185
|
+
else if (this.options.debug) {
|
|
186
|
+
console.log(`๐ Sandbox ${options.delete ? 'deleted' : 'stopped'}`);
|
|
357
187
|
}
|
|
358
188
|
}
|
|
359
189
|
catch (err) {
|
|
360
|
-
|
|
361
|
-
if (msg.includes('state change in progress')) {
|
|
362
|
-
if (this.options.debug) {
|
|
363
|
-
console.warn(`โ ๏ธ Sandbox ${options.delete ? 'deletion' : 'stop'} skipped (state change in progress)`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
else {
|
|
367
|
-
console.error(`Failed to ${options.delete ? 'delete' : 'stop'} sandbox:`, err);
|
|
368
|
-
}
|
|
190
|
+
console.error('Failed to call stop endpoint:', err);
|
|
369
191
|
}
|
|
370
192
|
}
|
|
371
193
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@castari/sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -29,8 +29,7 @@
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/claude-agent-sdk": "^0.1.44"
|
|
33
|
-
"@daytonaio/sdk": "^0.115.2"
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.44"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"@types/bun": "latest"
|