@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 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, Daytona mode is used. */
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
- daytonaApiKey?: string;
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). */
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
- /** @deprecated Use `volume` instead */
48
- workspaceVolumeName?: string;
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 sandbox?;
55
- private daytona?;
26
+ private sandboxId?;
56
27
  constructor(options?: ClientOptions);
57
28
  start(): Promise<void>;
58
29
  private setupLocalConnection;
59
- private setupDaytonaConnection;
30
+ private setupPlatformConnection;
60
31
  private handleMessage;
61
32
  onMessage(handler: (message: WSOutputMessage) => void): () => void;
62
33
  send(message: WSInputMessage): void;
63
- stop(): Promise<void>;
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
- sandbox;
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.setupDaytonaConnection();
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 setupDaytonaConnection() {
117
- const { Daytona } = await import('@daytonaio/sdk');
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('🚀 Creating Daytona sandbox...');
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}...`);
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 createParams = {
167
- snapshot: this.options.snapshot,
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 created: ${this.sandbox.id}`);
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 = baseUrl.replace(/\/$/, '');
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
- try {
280
- await this.sandbox?.delete();
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.sandbox) {
171
+ if (this.sandboxId) {
172
+ const platformUrl = this.options.platformUrl || process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
321
173
  try {
322
- await this.sandbox.delete();
323
- if (this.options.debug)
324
- console.log('🧹 Sandbox deleted');
325
- }
326
- catch (err) {
327
- const msg = err?.response?.data?.message || err?.message || String(err);
328
- if (msg.includes('state change in progress')) {
329
- if (this.options.debug) {
330
- console.warn('⚠️ Sandbox deletion skipped (state change in progress)');
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.error('Failed to delete sandbox:', err);
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.2",
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"