@castari/sdk 0.0.2 → 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 +12 -39
- package/dist/client.js +43 -188
- 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,37 @@
|
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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). */
|
|
15
|
+
/** Optional labels to apply to the sandbox (and filter by for reuse) */
|
|
16
|
+
labels?: Record<string, string>;
|
|
17
|
+
/** Optional volume name to mount at /home/daytona/agent-workspace */
|
|
46
18
|
volume?: string;
|
|
47
|
-
/**
|
|
48
|
-
|
|
19
|
+
/** Castari Platform API URL. Defaults to https://api.castari.com (or localhost in dev) */
|
|
20
|
+
platformUrl?: string;
|
|
49
21
|
}
|
|
50
22
|
export declare class CastariClient {
|
|
51
23
|
private ws?;
|
|
52
24
|
private options;
|
|
53
25
|
private messageHandlers;
|
|
54
|
-
private
|
|
55
|
-
private daytona?;
|
|
26
|
+
private sandboxId?;
|
|
56
27
|
constructor(options?: ClientOptions);
|
|
57
28
|
start(): Promise<void>;
|
|
58
29
|
private setupLocalConnection;
|
|
59
|
-
private
|
|
30
|
+
private setupPlatformConnection;
|
|
60
31
|
private handleMessage;
|
|
61
32
|
onMessage(handler: (message: WSOutputMessage) => void): () => void;
|
|
62
33
|
send(message: WSInputMessage): void;
|
|
63
|
-
stop(
|
|
34
|
+
stop(options?: {
|
|
35
|
+
delete?: boolean;
|
|
36
|
+
}): Promise<void>;
|
|
64
37
|
}
|
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,186 +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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
117
|
+
console.log(`🚀 Requesting sandbox from ${platformUrl}...`);
|
|
118
|
+
}
|
|
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,
|
|
124
|
+
labels: this.options.labels,
|
|
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}`);
|
|
165
131
|
}
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
image: this.options.image,
|
|
169
|
-
resources: this.options.resources,
|
|
170
|
-
autoStopInterval: this.options.autoStopInterval,
|
|
171
|
-
autoArchiveInterval: this.options.autoArchiveInterval,
|
|
172
|
-
autoDeleteInterval: this.options.autoDeleteInterval,
|
|
173
|
-
ephemeral: this.options.ephemeral,
|
|
174
|
-
public: true,
|
|
175
|
-
volumes: volumeMounts,
|
|
176
|
-
};
|
|
177
|
-
this.sandbox = await this.daytona.create(createParams);
|
|
132
|
+
const { id, url, token } = await response.json();
|
|
133
|
+
this.sandboxId = id;
|
|
178
134
|
if (this.options.debug) {
|
|
179
|
-
console.log(`✅ Sandbox
|
|
180
|
-
}
|
|
181
|
-
// Wait for sandbox to reach STARTED state before interacting with it
|
|
182
|
-
const sandboxAny = this.sandbox;
|
|
183
|
-
if (typeof sandboxAny.waitUntilStarted === 'function') {
|
|
184
|
-
if (this.options.debug) {
|
|
185
|
-
console.log('⏳ Waiting for sandbox to reach STARTED state...');
|
|
186
|
-
}
|
|
187
|
-
await sandboxAny.waitUntilStarted(60);
|
|
188
|
-
}
|
|
189
|
-
// Start the Castari server process inside the sandbox.
|
|
190
|
-
// We keep the container's default ENTRYPOINT as a no-op ('sleep infinity') for snapshot validation,
|
|
191
|
-
// and explicitly start the server here via Daytona's process API, invoking the server entrypoint directly.
|
|
192
|
-
const serverCommand = `sh -lc "cd /home/daytona/app && CASTARI_WORKSPACE=${DEFAULT_WORKSPACE_MOUNT} bun start"`;
|
|
193
|
-
try {
|
|
194
|
-
const sessionId = `castari-server-${this.sandbox.id}`;
|
|
195
|
-
const processApi = this.sandbox.process;
|
|
196
|
-
if (processApi &&
|
|
197
|
-
typeof processApi.createSession === 'function' &&
|
|
198
|
-
typeof processApi.executeSessionCommand === 'function') {
|
|
199
|
-
if (this.options.debug) {
|
|
200
|
-
console.log(`▶️ Creating process session ${sessionId} and starting server...`);
|
|
201
|
-
}
|
|
202
|
-
await processApi.createSession(sessionId);
|
|
203
|
-
await processApi.executeSessionCommand(sessionId, {
|
|
204
|
-
command: serverCommand,
|
|
205
|
-
async: true,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
else if (processApi && typeof processApi.executeCommand === 'function') {
|
|
209
|
-
if (this.options.debug) {
|
|
210
|
-
console.log('▶️ Starting server via executeCommand...');
|
|
211
|
-
}
|
|
212
|
-
await processApi.executeCommand({
|
|
213
|
-
command: serverCommand,
|
|
214
|
-
async: true,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
console.warn('Daytona SDK process API is not available; unable to start server automatically inside sandbox.');
|
|
219
|
-
}
|
|
220
|
-
// Give the server a brief moment to start listening on port 3000 before configuring it.
|
|
221
|
-
if (this.options.debug) {
|
|
222
|
-
console.log('⏳ Waiting briefly for Castari server to start inside sandbox...');
|
|
223
|
-
}
|
|
224
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
225
|
-
}
|
|
226
|
-
catch (err) {
|
|
227
|
-
console.error('Failed to start Castari server inside sandbox:', err);
|
|
228
|
-
}
|
|
229
|
-
// If user supplies a direct connection URL, use it to avoid preview discovery
|
|
230
|
-
let baseUrl = this.options.daytonaConnectionUrl || process.env.DAYTONA_CONNECTION_URL || null;
|
|
231
|
-
let previewToken;
|
|
232
|
-
if (!baseUrl) {
|
|
233
|
-
// Try getPreviewUrl (newer SDKs), then getPreviewLink (older SDKs)
|
|
234
|
-
let preview = null;
|
|
235
|
-
if (typeof this.sandbox.getPreviewUrl === 'function') {
|
|
236
|
-
try {
|
|
237
|
-
preview = await this.sandbox.getPreviewUrl(3000);
|
|
238
|
-
}
|
|
239
|
-
catch (err) {
|
|
240
|
-
if (this.options.debug) {
|
|
241
|
-
console.warn('⚠️ getPreviewUrl failed; will fall back to getPreviewLink or manual domain.', err);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (!preview?.url && typeof this.sandbox.getPreviewLink === 'function') {
|
|
246
|
-
try {
|
|
247
|
-
preview = await this.sandbox.getPreviewLink(3000);
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
if (this.options.debug) {
|
|
251
|
-
console.warn('⚠️ getPreviewLink failed; will attempt fallback domain if provided.', err);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
baseUrl = preview?.url || null;
|
|
256
|
-
previewToken = preview?.token;
|
|
257
|
-
if (!baseUrl) {
|
|
258
|
-
// Fallback: construct from provided preview domain if available
|
|
259
|
-
const previewDomain = this.options.daytonaPreviewDomain || process.env.DAYTONA_PREVIEW_DOMAIN;
|
|
260
|
-
if (previewDomain) {
|
|
261
|
-
baseUrl = `https://${3000}-${this.sandbox.id}.${previewDomain}`;
|
|
262
|
-
if (this.options.debug) {
|
|
263
|
-
console.warn(`⚠️ Preview URL not provided by SDK; using fallback domain ${previewDomain}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
throw new Error('Failed to get preview URL from Daytona sandbox. Provide daytonaConnectionUrl or DAYTONA_PREVIEW_DOMAIN.');
|
|
268
|
-
}
|
|
269
|
-
}
|
|
135
|
+
console.log(`✅ Sandbox started: ${id} at ${url}`);
|
|
270
136
|
}
|
|
271
|
-
baseUrl =
|
|
137
|
+
const baseUrl = url.replace(/\/$/, '');
|
|
272
138
|
const configUrl = `${baseUrl.replace('ws://', 'http://').replace('wss://', 'https://')}/config`;
|
|
273
139
|
const wsUrlBase = `${baseUrl.replace('https://', 'wss://').replace('http://', 'ws://')}/ws`;
|
|
274
140
|
return {
|
|
275
141
|
configUrl,
|
|
276
142
|
wsUrl: wsUrlBase,
|
|
277
|
-
previewToken,
|
|
143
|
+
previewToken: token,
|
|
278
144
|
cleanup: async () => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (this.options.debug)
|
|
282
|
-
console.log('🧹 Sandbox deleted');
|
|
283
|
-
}
|
|
284
|
-
catch (err) {
|
|
285
|
-
const msg = err?.response?.data?.message || err?.message || String(err);
|
|
286
|
-
if (msg.includes('state change in progress')) {
|
|
287
|
-
if (this.options.debug) {
|
|
288
|
-
console.warn('⚠️ Sandbox deletion skipped (state change in progress)');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
console.error('Failed to delete sandbox:', err);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
},
|
|
145
|
+
await this.stop({ delete: true });
|
|
146
|
+
}
|
|
296
147
|
};
|
|
297
148
|
}
|
|
298
149
|
handleMessage(message) {
|
|
@@ -313,27 +164,31 @@ export class CastariClient {
|
|
|
313
164
|
}
|
|
314
165
|
this.ws.send(JSON.stringify(message));
|
|
315
166
|
}
|
|
316
|
-
async stop() {
|
|
167
|
+
async stop(options = { delete: true }) {
|
|
317
168
|
if (this.ws) {
|
|
318
169
|
this.ws.close();
|
|
319
170
|
}
|
|
320
|
-
if (this.
|
|
171
|
+
if (this.sandboxId) {
|
|
172
|
+
const platformUrl = this.options.platformUrl || process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
|
|
321
173
|
try {
|
|
322
|
-
await
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
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()}`);
|
|
332
184
|
}
|
|
333
|
-
else {
|
|
334
|
-
console.
|
|
185
|
+
else if (this.options.debug) {
|
|
186
|
+
console.log(`🛑 Sandbox ${options.delete ? 'deleted' : 'stopped'}`);
|
|
335
187
|
}
|
|
336
188
|
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.error('Failed to call stop endpoint:', err);
|
|
191
|
+
}
|
|
337
192
|
}
|
|
338
193
|
}
|
|
339
194
|
}
|
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"
|