@castari/sdk 0.0.3 → 0.0.5

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,35 @@
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). */
46
- volume?: string;
47
15
  /** Optional labels to apply to the sandbox (and filter by for reuse) */
48
16
  labels?: Record<string, string>;
49
- /** @deprecated Use `volume` instead */
50
- workspaceVolumeName?: string;
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;
21
+ /** Optional sessionId to resume */
22
+ resume?: string;
51
23
  }
52
24
  export declare class CastariClient {
53
25
  private ws?;
54
26
  private options;
55
27
  private messageHandlers;
56
- private sandbox?;
57
- private daytona?;
28
+ private sandboxId?;
58
29
  constructor(options?: ClientOptions);
59
30
  start(): Promise<void>;
60
31
  private setupLocalConnection;
61
- private setupDaytonaConnection;
32
+ private setupPlatformConnection;
62
33
  private handleMessage;
63
34
  onMessage(handler: (message: WSOutputMessage) => void): () => void;
64
35
  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
- 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
  }
@@ -29,7 +27,11 @@ export class CastariClient {
29
27
  allowedTools: this.options.allowedTools,
30
28
  systemPrompt: this.options.systemPrompt,
31
29
  model: this.options.model,
30
+ resume: this.options.resume,
32
31
  };
32
+ if (this.options.debug) {
33
+ console.log(`📋 Config payload:`, JSON.stringify(configPayload, null, 2));
34
+ }
33
35
  const configHeaders = {
34
36
  'Content-Type': 'application/json',
35
37
  };
@@ -113,212 +115,39 @@ export class CastariClient {
113
115
  wsUrl: `${baseUrl.replace('http://', 'ws://').replace('https://', 'wss://')}/ws`,
114
116
  };
115
117
  }
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
- }
118
+ async setupPlatformConnection() {
119
+ const platformUrl = this.options.platformUrl || process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
157
120
  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
- }
165
- }
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
- // 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
- }
121
+ console.log(`🚀 Requesting sandbox from ${platformUrl}...`);
194
122
  }
195
- // Create new if not found
196
- if (!this.sandbox) {
197
- // Add labels to create params
198
- const paramsWithLabels = {
199
- ...createParams,
123
+ const response = await fetch(`${platformUrl}/sandbox/start`, {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({
127
+ snapshot: this.options.snapshot,
200
128
  labels: this.options.labels,
201
- };
202
- this.sandbox = await this.daytona.create(paramsWithLabels);
203
- if (this.options.debug) {
204
- console.log(`✅ Sandbox created: ${this.sandbox.id}`);
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);
129
+ volume: this.options.volume
130
+ })
131
+ });
132
+ if (!response.ok) {
133
+ const errorText = await response.text();
134
+ throw new Error(`Failed to start sandbox: ${errorText}`);
254
135
  }
255
- // If user supplies a direct connection URL, use it to avoid preview discovery
256
- let baseUrl = this.options.daytonaConnectionUrl || process.env.DAYTONA_CONNECTION_URL || null;
257
- let previewToken;
258
- if (!baseUrl) {
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
- }
136
+ const { id, url, token } = await response.json();
137
+ this.sandboxId = id;
138
+ if (this.options.debug) {
139
+ console.log(`✅ Sandbox started: ${id} at ${url}`);
296
140
  }
297
- baseUrl = baseUrl.replace(/\/$/, '');
141
+ const baseUrl = url.replace(/\/$/, '');
298
142
  const configUrl = `${baseUrl.replace('ws://', 'http://').replace('wss://', 'https://')}/config`;
299
143
  const wsUrlBase = `${baseUrl.replace('https://', 'wss://').replace('http://', 'ws://')}/ws`;
300
144
  return {
301
145
  configUrl,
302
146
  wsUrl: wsUrlBase,
303
- previewToken,
147
+ previewToken: token,
304
148
  cleanup: async () => {
305
- try {
306
- await this.sandbox?.delete();
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
- },
149
+ await this.stop({ delete: true });
150
+ }
322
151
  };
323
152
  }
324
153
  handleMessage(message) {
@@ -343,29 +172,26 @@ export class CastariClient {
343
172
  if (this.ws) {
344
173
  this.ws.close();
345
174
  }
346
- if (this.sandbox) {
175
+ if (this.sandboxId) {
176
+ const platformUrl = this.options.platformUrl || process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
347
177
  try {
348
- if (options.delete) {
349
- await this.sandbox.delete();
350
- if (this.options.debug)
351
- console.log('🧹 Sandbox deleted');
178
+ const response = await fetch(`${platformUrl}/sandbox/stop`, {
179
+ method: 'POST',
180
+ headers: { 'Content-Type': 'application/json' },
181
+ body: JSON.stringify({
182
+ sandboxId: this.sandboxId,
183
+ delete: options.delete
184
+ })
185
+ });
186
+ if (!response.ok) {
187
+ console.error(`Failed to stop sandbox: ${await response.text()}`);
352
188
  }
353
- else {
354
- await this.sandbox.stop();
355
- if (this.options.debug)
356
- console.log('🛑 Sandbox stopped (preserved)');
189
+ else if (this.options.debug) {
190
+ console.log(`🛑 Sandbox ${options.delete ? 'deleted' : 'stopped'}`);
357
191
  }
358
192
  }
359
193
  catch (err) {
360
- const msg = err?.response?.data?.message || err?.message || String(err);
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
- }
194
+ console.error('Failed to call stop endpoint:', err);
369
195
  }
370
196
  }
371
197
  }
package/dist/server.js CHANGED
@@ -115,6 +115,12 @@ async function processMessages(initialOptions) {
115
115
  ...options,
116
116
  prompt: '[generator]', // avoid logging generator internals
117
117
  });
118
+ if (options.resume) {
119
+ console.info(`📋 Resuming session: ${options.resume}`);
120
+ }
121
+ else {
122
+ console.info('📋 Starting new session');
123
+ }
118
124
  activeStream = query({
119
125
  prompt: generateMessages(),
120
126
  options,
package/dist/types.d.ts CHANGED
@@ -55,4 +55,5 @@ export type QueryConfig = {
55
55
  };
56
56
  model?: string;
57
57
  anthropicApiKey?: string;
58
+ resume?: string;
58
59
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@castari/sdk",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
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"