@castari/cli 0.0.2 β 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 +20 -0
- package/dist/commands/deploy.js +35 -37
- package/dist/commands/init.d.ts +5 -1
- package/dist/commands/init.js +622 -3
- package/dist/commands/start.js +3 -6
- package/dist/index.js +4 -3
- package/package.json +9 -6
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @castari/cli
|
|
2
|
+
|
|
3
|
+
The Command Line Interface for Castari.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @castari/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
See the [CLI Reference](../../docs/cli-reference.md) for detailed documentation.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
castari init # Create a new agent
|
|
17
|
+
castari dev # Run locally
|
|
18
|
+
castari deploy # Deploy to Platform
|
|
19
|
+
castari start # Start sandbox
|
|
20
|
+
```
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { Daytona, Image } from '@daytonaio/sdk';
|
|
2
1
|
import { readFile } from 'fs/promises';
|
|
3
2
|
import chalk from 'chalk';
|
|
3
|
+
import AdmZip from 'adm-zip';
|
|
4
4
|
export async function deploy(options) {
|
|
5
|
-
const apiKey = process.env.DAYTONA_API_KEY;
|
|
6
|
-
if (!apiKey) {
|
|
7
|
-
console.error(chalk.red('Error: DAYTONA_API_KEY is required.'));
|
|
8
|
-
process.exit(1);
|
|
9
|
-
}
|
|
10
5
|
// Read package.json to get default snapshot name
|
|
11
6
|
let snapshotName = options.snapshot;
|
|
12
7
|
if (!snapshotName) {
|
|
@@ -23,42 +18,45 @@ export async function deploy(options) {
|
|
|
23
18
|
console.error(chalk.red('Error: Snapshot name is required (via --snapshot or package.json name).'));
|
|
24
19
|
process.exit(1);
|
|
25
20
|
}
|
|
26
|
-
console.log(chalk.blue(
|
|
27
|
-
const
|
|
28
|
-
apiKey,
|
|
29
|
-
apiUrl: process.env.DAYTONA_API_URL,
|
|
30
|
-
target: process.env.DAYTONA_TARGET,
|
|
31
|
-
});
|
|
21
|
+
console.log(chalk.blue(`π¦ Packaging source code...`));
|
|
22
|
+
const zip = new AdmZip();
|
|
32
23
|
const projectRoot = process.cwd();
|
|
33
|
-
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.
|
|
24
|
+
// Add local files, respecting a simple ignore list for now
|
|
25
|
+
// Ideally we'd parse .gitignore, but for MVP let's just ignore common heavy folders
|
|
26
|
+
const ignoreList = ['node_modules', '.git', 'dist', '.daytona', '.env'];
|
|
27
|
+
zip.addLocalFolder(projectRoot, undefined, (filename) => {
|
|
28
|
+
// Simple filter: return false to exclude
|
|
29
|
+
// filename is the relative path in the zip? No, it seems to be the absolute path or filename.
|
|
30
|
+
// AdmZip filter is a bit tricky. Let's assume it passes the name.
|
|
31
|
+
// Actually addLocalFolder filter takes a RegExp or function.
|
|
32
|
+
// If function: (filename: string) => boolean.
|
|
33
|
+
// Note: filename passed to filter is usually the relative path.
|
|
34
|
+
// Let's try to be safe and just add everything except node_modules at the root.
|
|
35
|
+
// Actually, addLocalFolder adds everything.
|
|
36
|
+
// We might want to use addLocalFile loop or similar if we want strict control.
|
|
37
|
+
// But let's try the filter.
|
|
38
|
+
return !ignoreList.some(ignore => filename.includes(ignore));
|
|
39
|
+
});
|
|
40
|
+
const zipBuffer = zip.toBuffer();
|
|
41
|
+
console.log(chalk.blue(`π Uploading to Castari Platform...`));
|
|
42
|
+
const platformUrl = process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
|
|
43
|
+
const formData = new FormData();
|
|
44
|
+
formData.append('file', new Blob([zipBuffer]), 'source.zip');
|
|
45
|
+
formData.append('snapshot', snapshotName);
|
|
40
46
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(chalk.yellow(`ποΈ Deleted existing snapshot "${snapshotName}"`));
|
|
45
|
-
}
|
|
46
|
-
catch (e) {
|
|
47
|
-
// Ignore delete error (e.g. not found)
|
|
48
|
-
}
|
|
49
|
-
await daytona.snapshot.create({
|
|
50
|
-
name: snapshotName,
|
|
51
|
-
image,
|
|
52
|
-
}, {
|
|
53
|
-
onLogs: log => {
|
|
54
|
-
if (log)
|
|
55
|
-
process.stdout.write(log);
|
|
56
|
-
},
|
|
47
|
+
const response = await fetch(`${platformUrl}/deploy`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: formData,
|
|
57
50
|
});
|
|
58
|
-
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const errorText = await response.text();
|
|
53
|
+
throw new Error(`Platform error (${response.status}): ${errorText}`);
|
|
54
|
+
}
|
|
55
|
+
const result = await response.json();
|
|
56
|
+
console.log(chalk.green(`β
Snapshot "${result.snapshot}" created successfully!`));
|
|
59
57
|
}
|
|
60
58
|
catch (err) {
|
|
61
|
-
console.error(chalk.red('
|
|
59
|
+
console.error(chalk.red('Deploy failed:'), err.message || err);
|
|
62
60
|
process.exit(1);
|
|
63
61
|
}
|
|
64
62
|
}
|
package/dist/commands/init.d.ts
CHANGED
package/dist/commands/init.js
CHANGED
|
@@ -2,7 +2,10 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import { writeFile, mkdir } from 'fs/promises';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
export async function init() {
|
|
5
|
+
export async function init(options = {}) {
|
|
6
|
+
if (options.demo) {
|
|
7
|
+
return initDemo();
|
|
8
|
+
}
|
|
6
9
|
console.log(chalk.blue('π€ Initializing new Castari agent project...'));
|
|
7
10
|
const answers = await inquirer.prompt([
|
|
8
11
|
{
|
|
@@ -22,7 +25,7 @@ export async function init() {
|
|
|
22
25
|
start: 'castari start',
|
|
23
26
|
},
|
|
24
27
|
dependencies: {
|
|
25
|
-
'@castari/sdk': '
|
|
28
|
+
'@castari/sdk': '^0.0.4',
|
|
26
29
|
'@anthropic-ai/claude-agent-sdk': '^0.1.44',
|
|
27
30
|
},
|
|
28
31
|
castari: {
|
|
@@ -62,7 +65,7 @@ serve({
|
|
|
62
65
|
systemPrompt: 'You are a helpful Castari agent.',
|
|
63
66
|
})
|
|
64
67
|
`;
|
|
65
|
-
const envExample = `ANTHROPIC_API_KEY=sk-ant-...\
|
|
68
|
+
const envExample = `ANTHROPIC_API_KEY=sk-ant-...\n# CASTARI_PLATFORM_URL=https://api.castari.com\n`;
|
|
66
69
|
await writeFile('package.json', JSON.stringify(packageJson, null, 2));
|
|
67
70
|
await writeFile('tsconfig.json', JSON.stringify(tsConfig, null, 2));
|
|
68
71
|
await writeFile('.env.example', envExample);
|
|
@@ -71,3 +74,619 @@ serve({
|
|
|
71
74
|
console.log(chalk.green('β
Project initialized!'));
|
|
72
75
|
console.log(chalk.white('Run `bun install` to install dependencies.'));
|
|
73
76
|
}
|
|
77
|
+
async function initDemo() {
|
|
78
|
+
console.log(chalk.blue('π¨ Initializing Castari demo (web + agent)...'));
|
|
79
|
+
const demoRoot = 'castari_demo';
|
|
80
|
+
const agentDir = join(demoRoot, 'agent');
|
|
81
|
+
const webDir = join(demoRoot, 'web');
|
|
82
|
+
const writeJson = async (path, obj) => writeFile(path, JSON.stringify(obj, null, 2));
|
|
83
|
+
// Agent scaffold
|
|
84
|
+
await mkdir(join(agentDir, 'src'), { recursive: true });
|
|
85
|
+
await writeJson(join(agentDir, 'package.json'), {
|
|
86
|
+
name: 'castari-demo-agent',
|
|
87
|
+
version: '0.1.0',
|
|
88
|
+
private: true,
|
|
89
|
+
type: 'module',
|
|
90
|
+
scripts: {
|
|
91
|
+
dev: 'bun run src/agent.ts',
|
|
92
|
+
deploy: 'castari deploy',
|
|
93
|
+
start: 'bun run src/agent.ts',
|
|
94
|
+
},
|
|
95
|
+
dependencies: {
|
|
96
|
+
'@castari/sdk': '^0.0.4',
|
|
97
|
+
'@anthropic-ai/claude-agent-sdk': '^0.1.50',
|
|
98
|
+
},
|
|
99
|
+
castari: {
|
|
100
|
+
volume: 'castari-demo-workspace',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
await writeJson(join(agentDir, 'tsconfig.json'), {
|
|
104
|
+
compilerOptions: {
|
|
105
|
+
target: 'ESNext',
|
|
106
|
+
module: 'ESNext',
|
|
107
|
+
moduleResolution: 'bundler',
|
|
108
|
+
strict: true,
|
|
109
|
+
skipLibCheck: true,
|
|
110
|
+
esModuleInterop: true,
|
|
111
|
+
},
|
|
112
|
+
include: ['src'],
|
|
113
|
+
});
|
|
114
|
+
await writeFile(join(agentDir, '.env.example'), 'ANTHROPIC_API_KEY=sk-ant-...\n# CASTARI_PLATFORM_URL=http://localhost:3000\n');
|
|
115
|
+
await writeFile(join(agentDir, 'src', 'agent.ts'), `import { serve } from '@castari/sdk'
|
|
116
|
+
|
|
117
|
+
serve({
|
|
118
|
+
systemPrompt: [
|
|
119
|
+
'You are Castari Demo Agent.',
|
|
120
|
+
'Keep responses concise and friendly.',
|
|
121
|
+
'Feel free to explain how you can help with code or product questions.',
|
|
122
|
+
].join(' '),
|
|
123
|
+
includePartialMessages: true,
|
|
124
|
+
})
|
|
125
|
+
`);
|
|
126
|
+
// Web scaffold
|
|
127
|
+
await mkdir(join(webDir, 'app', 'api', 'chat'), { recursive: true });
|
|
128
|
+
await writeJson(join(webDir, 'package.json'), {
|
|
129
|
+
name: 'castari-demo-web',
|
|
130
|
+
version: '0.1.0',
|
|
131
|
+
private: true,
|
|
132
|
+
scripts: {
|
|
133
|
+
dev: 'next dev',
|
|
134
|
+
build: 'next build',
|
|
135
|
+
start: 'next start',
|
|
136
|
+
},
|
|
137
|
+
dependencies: {
|
|
138
|
+
'@castari/sdk': '^0.0.4',
|
|
139
|
+
next: '14.2.3',
|
|
140
|
+
react: '18.3.1',
|
|
141
|
+
'react-dom': '18.3.1',
|
|
142
|
+
},
|
|
143
|
+
devDependencies: {
|
|
144
|
+
'@types/node': '^20.11.0',
|
|
145
|
+
'@types/react': '^18.2.0',
|
|
146
|
+
'@types/react-dom': '^18.2.0',
|
|
147
|
+
typescript: '^5.6.3',
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
await writeJson(join(webDir, 'tsconfig.json'), {
|
|
151
|
+
compilerOptions: {
|
|
152
|
+
target: 'ES2020',
|
|
153
|
+
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
154
|
+
allowJs: true,
|
|
155
|
+
skipLibCheck: true,
|
|
156
|
+
strict: true,
|
|
157
|
+
noEmit: true,
|
|
158
|
+
esModuleInterop: true,
|
|
159
|
+
module: 'esnext',
|
|
160
|
+
moduleResolution: 'bundler',
|
|
161
|
+
resolveJsonModule: true,
|
|
162
|
+
isolatedModules: true,
|
|
163
|
+
jsx: 'preserve',
|
|
164
|
+
incremental: true,
|
|
165
|
+
types: ['node'],
|
|
166
|
+
baseUrl: '.',
|
|
167
|
+
paths: {
|
|
168
|
+
'@/*': ['./*'],
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
172
|
+
exclude: ['node_modules'],
|
|
173
|
+
});
|
|
174
|
+
await writeFile(join(webDir, 'next-env.d.ts'), `/// <reference types="next" />
|
|
175
|
+
/// <reference types="next/types/global" />
|
|
176
|
+
/// <reference types="next/image-types/global" />
|
|
177
|
+
|
|
178
|
+
// NOTE: This file should not be edited
|
|
179
|
+
`);
|
|
180
|
+
await writeFile(join(webDir, 'next.config.mjs'), `/** @type {import('next').NextConfig} */
|
|
181
|
+
const nextConfig = {
|
|
182
|
+
experimental: {
|
|
183
|
+
serverActions: false
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default nextConfig
|
|
188
|
+
`);
|
|
189
|
+
await writeFile(join(webDir, '.env.example'), 'ANTHROPIC_API_KEY=sk-ant-...\n# CASTARI_PLATFORM_URL=http://localhost:3000\n# CASTARI_DEBUG=false\n');
|
|
190
|
+
await writeFile(join(webDir, 'app', 'globals.css'), `:root {
|
|
191
|
+
color-scheme: light;
|
|
192
|
+
background: #f5f5f5;
|
|
193
|
+
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
* {
|
|
197
|
+
box-sizing: border-box;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
body {
|
|
201
|
+
margin: 0;
|
|
202
|
+
background: radial-gradient(circle at 20% 20%, #f7f0ff 0, #f5f5f5 35%), radial-gradient(circle at 80% 0%, #e0f4ff 0, #f5f5f5 30%);
|
|
203
|
+
color: #0f172a;
|
|
204
|
+
min-height: 100vh;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
a {
|
|
208
|
+
color: inherit;
|
|
209
|
+
text-decoration: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.chat-container {
|
|
213
|
+
max-width: 760px;
|
|
214
|
+
margin: 0 auto;
|
|
215
|
+
padding: 32px 16px 64px;
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 16px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.card {
|
|
222
|
+
background: rgba(255, 255, 255, 0.85);
|
|
223
|
+
backdrop-filter: blur(6px);
|
|
224
|
+
border: 1px solid rgba(15, 23, 42, 0.06);
|
|
225
|
+
border-radius: 16px;
|
|
226
|
+
box-shadow: 0 20px 60px rgba(15, 23, 42, 0.12);
|
|
227
|
+
padding: 24px;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.heading {
|
|
231
|
+
display: flex;
|
|
232
|
+
align-items: center;
|
|
233
|
+
gap: 12px;
|
|
234
|
+
font-size: 22px;
|
|
235
|
+
font-weight: 700;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.badge {
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
font-weight: 600;
|
|
241
|
+
color: #0ea5e9;
|
|
242
|
+
background: rgba(14, 165, 233, 0.1);
|
|
243
|
+
padding: 6px 10px;
|
|
244
|
+
border-radius: 999px;
|
|
245
|
+
border: 1px solid rgba(14, 165, 233, 0.2);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.messages {
|
|
249
|
+
display: flex;
|
|
250
|
+
flex-direction: column;
|
|
251
|
+
gap: 12px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.bubble {
|
|
255
|
+
padding: 14px 16px;
|
|
256
|
+
border-radius: 14px;
|
|
257
|
+
max-width: 90%;
|
|
258
|
+
line-height: 1.5;
|
|
259
|
+
font-size: 15px;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.bubble.assistant {
|
|
263
|
+
background: #0f172a;
|
|
264
|
+
color: white;
|
|
265
|
+
align-self: flex-start;
|
|
266
|
+
border-bottom-left-radius: 4px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.bubble.user {
|
|
270
|
+
background: white;
|
|
271
|
+
color: #0f172a;
|
|
272
|
+
border: 1px solid rgba(15, 23, 42, 0.06);
|
|
273
|
+
align-self: flex-end;
|
|
274
|
+
border-bottom-right-radius: 4px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.form {
|
|
278
|
+
display: flex;
|
|
279
|
+
gap: 12px;
|
|
280
|
+
margin-top: 8px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.input {
|
|
284
|
+
flex: 1;
|
|
285
|
+
padding: 14px 16px;
|
|
286
|
+
border-radius: 12px;
|
|
287
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
288
|
+
font-size: 15px;
|
|
289
|
+
outline: none;
|
|
290
|
+
transition: border-color 0.15s ease;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.input:focus {
|
|
294
|
+
border-color: #0ea5e9;
|
|
295
|
+
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.12);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.button {
|
|
299
|
+
background: linear-gradient(135deg, #0ea5e9, #6366f1);
|
|
300
|
+
color: white;
|
|
301
|
+
border: none;
|
|
302
|
+
border-radius: 12px;
|
|
303
|
+
padding: 12px 18px;
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
cursor: pointer;
|
|
306
|
+
box-shadow: 0 12px 25px rgba(99, 102, 241, 0.35);
|
|
307
|
+
min-width: 110px;
|
|
308
|
+
transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.button:disabled {
|
|
312
|
+
opacity: 0.6;
|
|
313
|
+
cursor: not-allowed;
|
|
314
|
+
box-shadow: none;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.button:not(:disabled):hover {
|
|
318
|
+
transform: translateY(-1px);
|
|
319
|
+
box-shadow: 0 16px 30px rgba(99, 102, 241, 0.45);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.status {
|
|
323
|
+
font-size: 13px;
|
|
324
|
+
color: #475569;
|
|
325
|
+
margin-top: 6px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.pill-row {
|
|
329
|
+
display: flex;
|
|
330
|
+
gap: 8px;
|
|
331
|
+
flex-wrap: wrap;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.pill {
|
|
335
|
+
padding: 6px 10px;
|
|
336
|
+
border-radius: 999px;
|
|
337
|
+
background: rgba(15, 23, 42, 0.06);
|
|
338
|
+
font-size: 12px;
|
|
339
|
+
color: #0f172a;
|
|
340
|
+
border: 1px solid rgba(15, 23, 42, 0.08);
|
|
341
|
+
}
|
|
342
|
+
`);
|
|
343
|
+
await writeFile(join(webDir, 'app', 'layout.tsx'), `import './globals.css'
|
|
344
|
+
import { ReactNode } from 'react'
|
|
345
|
+
|
|
346
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
347
|
+
return (
|
|
348
|
+
<html lang="en">
|
|
349
|
+
<head>
|
|
350
|
+
<title>Castari Demo Chat</title>
|
|
351
|
+
</head>
|
|
352
|
+
<body>{children}</body>
|
|
353
|
+
</html>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
`);
|
|
357
|
+
await writeFile(join(webDir, 'app', 'page.tsx'), `"use client"
|
|
358
|
+
|
|
359
|
+
import { FormEvent, useState } from 'react'
|
|
360
|
+
|
|
361
|
+
type ChatMessage = {
|
|
362
|
+
role: 'assistant' | 'user'
|
|
363
|
+
content: string
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export default function Home() {
|
|
367
|
+
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
368
|
+
{
|
|
369
|
+
role: 'assistant',
|
|
370
|
+
content:
|
|
371
|
+
'Hi! I am the Castari demo agent. Ask me anything about your code, architecture, or how this demo works.',
|
|
372
|
+
},
|
|
373
|
+
])
|
|
374
|
+
const [input, setInput] = useState('')
|
|
375
|
+
const [status, setStatus] = useState<string | null>(null)
|
|
376
|
+
const [sending, setSending] = useState(false)
|
|
377
|
+
|
|
378
|
+
const sendMessage = async (event: FormEvent) => {
|
|
379
|
+
event.preventDefault()
|
|
380
|
+
if (!input.trim() || sending) return
|
|
381
|
+
|
|
382
|
+
const userMessage: ChatMessage = { role: 'user', content: input.trim() }
|
|
383
|
+
setMessages(prev => [...prev, userMessage])
|
|
384
|
+
setInput('')
|
|
385
|
+
setSending(true)
|
|
386
|
+
setStatus('Connecting to your sandbox...')
|
|
387
|
+
|
|
388
|
+
// Add assistant placeholder immediately so we can stream into it
|
|
389
|
+
let assistantIndex = -1
|
|
390
|
+
setMessages(prev => {
|
|
391
|
+
assistantIndex = prev.length
|
|
392
|
+
return [...prev, { role: 'assistant', content: '' }]
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const response = await fetch('/api/chat', {
|
|
397
|
+
method: 'POST',
|
|
398
|
+
headers: { 'Content-Type': 'application/json' },
|
|
399
|
+
body: JSON.stringify({ message: userMessage.content }),
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
if (!response.ok || !response.body) {
|
|
403
|
+
const errorText = await response.text()
|
|
404
|
+
throw new Error(errorText || 'Request failed')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const reader = response.body.getReader()
|
|
408
|
+
const decoder = new TextDecoder()
|
|
409
|
+
|
|
410
|
+
while (true) {
|
|
411
|
+
const { value, done } = await reader.read()
|
|
412
|
+
if (done) break
|
|
413
|
+
const chunk = decoder.decode(value, { stream: true })
|
|
414
|
+
if (!chunk) continue
|
|
415
|
+
const idx = assistantIndex
|
|
416
|
+
setMessages(prev =>
|
|
417
|
+
prev.map((m, i) => (i === idx ? { ...m, content: m.content + chunk } : m)),
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
setStatus(null)
|
|
422
|
+
} catch (err) {
|
|
423
|
+
const message =
|
|
424
|
+
err instanceof Error ? err.message : 'Something went wrong talking to the agent.'
|
|
425
|
+
setMessages(prev =>
|
|
426
|
+
prev.map((m, i) =>
|
|
427
|
+
i === assistantIndex
|
|
428
|
+
? { ...m, content: \`I hit an error: \${message}\` }
|
|
429
|
+
: m,
|
|
430
|
+
),
|
|
431
|
+
)
|
|
432
|
+
setStatus('Temporary hiccup. Try again in a moment.')
|
|
433
|
+
} finally {
|
|
434
|
+
setSending(false)
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<main className="chat-container">
|
|
440
|
+
<div className="card">
|
|
441
|
+
<div className="heading">
|
|
442
|
+
<span>Castari Demo Chat</span>
|
|
443
|
+
<span className="badge">Single sandbox</span>
|
|
444
|
+
</div>
|
|
445
|
+
<p style={{ margin: '8px 0 16px', color: '#475569' }}>
|
|
446
|
+
This UI connects to a Castari agent running in a Daytona sandbox. Messages reuse the same
|
|
447
|
+
sandbox (via labels) so the conversation stays warm while the sandbox is alive.
|
|
448
|
+
</p>
|
|
449
|
+
|
|
450
|
+
<div className="pill-row" style={{ marginBottom: 12 }}>
|
|
451
|
+
<span className="pill">Snapshot: castari-demo-agent</span>
|
|
452
|
+
<span className="pill">Volume: castari-demo-workspace</span>
|
|
453
|
+
<span className="pill">Labels: app=castari-demo, env=local</span>
|
|
454
|
+
</div>
|
|
455
|
+
|
|
456
|
+
<div className="messages">
|
|
457
|
+
{messages.map((m, idx) => (
|
|
458
|
+
<div key={idx} className={\`bubble \${m.role}\`}>
|
|
459
|
+
{m.content}
|
|
460
|
+
</div>
|
|
461
|
+
))}
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<form className="form" onSubmit={sendMessage}>
|
|
465
|
+
<input
|
|
466
|
+
className="input"
|
|
467
|
+
placeholder="Ask the Castari agent anything..."
|
|
468
|
+
value={input}
|
|
469
|
+
onChange={e => setInput(e.target.value)}
|
|
470
|
+
/>
|
|
471
|
+
<button className="button" type="submit" disabled={sending}>
|
|
472
|
+
{sending ? 'Talkingβ¦' : 'Send'}
|
|
473
|
+
</button>
|
|
474
|
+
</form>
|
|
475
|
+
{status && <div className="status">{status}</div>}
|
|
476
|
+
</div>
|
|
477
|
+
</main>
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
`);
|
|
481
|
+
await writeFile(join(webDir, 'lib', 'castari-client.ts'), `import { CastariClient, type WSOutputMessage } from '@castari/sdk/client'
|
|
482
|
+
|
|
483
|
+
const SNAPSHOT = 'castari-demo-agent'
|
|
484
|
+
const LABELS = { app: 'castari-demo', env: 'local' }
|
|
485
|
+
const VOLUME = 'castari-demo-workspace'
|
|
486
|
+
|
|
487
|
+
let clientPromise: Promise<CastariClient> | null = null
|
|
488
|
+
|
|
489
|
+
async function createClient() {
|
|
490
|
+
const anthropicApiKey = process.env.ANTHROPIC_API_KEY
|
|
491
|
+
if (!anthropicApiKey) {
|
|
492
|
+
throw new Error('ANTHROPIC_API_KEY is required to contact the Castari agent')
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const client = new CastariClient({
|
|
496
|
+
snapshot: SNAPSHOT,
|
|
497
|
+
volume: VOLUME,
|
|
498
|
+
labels: LABELS,
|
|
499
|
+
platformUrl: process.env.CASTARI_PLATFORM_URL,
|
|
500
|
+
anthropicApiKey,
|
|
501
|
+
debug: process.env.CASTARI_DEBUG === 'true',
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
await client.start()
|
|
505
|
+
return client
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function getClient() {
|
|
509
|
+
if (!clientPromise) {
|
|
510
|
+
clientPromise = createClient()
|
|
511
|
+
}
|
|
512
|
+
return clientPromise
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function extractText(content: unknown): string {
|
|
516
|
+
if (typeof content === 'string') return content
|
|
517
|
+
if (Array.isArray(content)) {
|
|
518
|
+
return content
|
|
519
|
+
.map(block => {
|
|
520
|
+
if (typeof block === 'string') return block
|
|
521
|
+
if (block && typeof block === 'object' && 'text' in block) {
|
|
522
|
+
return (block as { text?: string }).text ?? ''
|
|
523
|
+
}
|
|
524
|
+
return ''
|
|
525
|
+
})
|
|
526
|
+
.join('\\n')
|
|
527
|
+
}
|
|
528
|
+
if (content && typeof content === 'object' && 'text' in (content as any)) {
|
|
529
|
+
return (content as any).text ?? ''
|
|
530
|
+
}
|
|
531
|
+
return ''
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function extractStreamDelta(event: any): string {
|
|
535
|
+
if (!event || typeof event !== 'object') return ''
|
|
536
|
+
if (event.type === 'content_block_delta' && event.delta?.text) {
|
|
537
|
+
return String(event.delta.text)
|
|
538
|
+
}
|
|
539
|
+
if (event.type === 'content_block_start' && event.content_block?.text) {
|
|
540
|
+
return String(event.content_block.text)
|
|
541
|
+
}
|
|
542
|
+
if (event.type === 'message_delta' && event.delta?.text) {
|
|
543
|
+
return String(event.delta.text)
|
|
544
|
+
}
|
|
545
|
+
return ''
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export async function streamMessageToAgent(
|
|
549
|
+
message: string,
|
|
550
|
+
handlers: {
|
|
551
|
+
onChunk?: (text: string) => void
|
|
552
|
+
},
|
|
553
|
+
): Promise<void> {
|
|
554
|
+
const client = await getClient()
|
|
555
|
+
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
const timeout = setTimeout(() => {
|
|
558
|
+
cleanup()
|
|
559
|
+
reject(new Error('Timed out waiting for agent response'))
|
|
560
|
+
}, 60000)
|
|
561
|
+
|
|
562
|
+
let sawStreamChunk = false
|
|
563
|
+
|
|
564
|
+
const unsubscribe = client.onMessage((msg: WSOutputMessage) => {
|
|
565
|
+
if (msg.type !== 'sdk_message') return
|
|
566
|
+
const data: any = msg.data
|
|
567
|
+
|
|
568
|
+
if (data.type === 'stream_event') {
|
|
569
|
+
const deltaText = extractStreamDelta(data.event)
|
|
570
|
+
if (deltaText) {
|
|
571
|
+
sawStreamChunk = true
|
|
572
|
+
handlers.onChunk?.(deltaText)
|
|
573
|
+
}
|
|
574
|
+
} else if (data.type === 'assistant_message') {
|
|
575
|
+
finish()
|
|
576
|
+
} else if (data.type === 'result' && data.subtype === 'success') {
|
|
577
|
+
if (!sawStreamChunk) {
|
|
578
|
+
const text =
|
|
579
|
+
typeof data.result === 'string'
|
|
580
|
+
? data.result
|
|
581
|
+
: extractText(data.result?.output || data.result?.message || '')
|
|
582
|
+
if (text) handlers.onChunk?.(text)
|
|
583
|
+
}
|
|
584
|
+
finish()
|
|
585
|
+
} else if (data.type === 'error') {
|
|
586
|
+
cleanup()
|
|
587
|
+
reject(new Error(data.error || 'Agent returned an error'))
|
|
588
|
+
}
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
const cleanup = () => {
|
|
592
|
+
clearTimeout(timeout)
|
|
593
|
+
unsubscribe()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const finish = () => {
|
|
597
|
+
cleanup()
|
|
598
|
+
resolve()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
client.send({
|
|
603
|
+
type: 'user_message',
|
|
604
|
+
data: {
|
|
605
|
+
type: 'user',
|
|
606
|
+
message: { role: 'user', content: message },
|
|
607
|
+
} as any,
|
|
608
|
+
})
|
|
609
|
+
} catch (err) {
|
|
610
|
+
cleanup()
|
|
611
|
+
reject(err)
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
`);
|
|
616
|
+
await writeFile(join(webDir, 'app', 'api', 'chat', 'route.ts'), `export const runtime = 'nodejs'
|
|
617
|
+
|
|
618
|
+
import { streamMessageToAgent } from '@/lib/castari-client'
|
|
619
|
+
|
|
620
|
+
export async function POST(request: Request) {
|
|
621
|
+
const body = (await request.json()) as { message?: string }
|
|
622
|
+
const message = body.message?.trim()
|
|
623
|
+
|
|
624
|
+
if (!message) {
|
|
625
|
+
return new Response(JSON.stringify({ error: 'message is required' }), {
|
|
626
|
+
status: 400,
|
|
627
|
+
headers: { 'Content-Type': 'application/json' },
|
|
628
|
+
})
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const encoder = new TextEncoder()
|
|
632
|
+
|
|
633
|
+
const stream = new ReadableStream({
|
|
634
|
+
async start(controller) {
|
|
635
|
+
try {
|
|
636
|
+
await streamMessageToAgent(message, {
|
|
637
|
+
onChunk: text => controller.enqueue(encoder.encode(text)),
|
|
638
|
+
})
|
|
639
|
+
controller.close()
|
|
640
|
+
} catch (err) {
|
|
641
|
+
controller.error(err)
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
return new Response(stream, {
|
|
647
|
+
headers: {
|
|
648
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
649
|
+
'Transfer-Encoding': 'chunked',
|
|
650
|
+
},
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
`);
|
|
654
|
+
// Demo README
|
|
655
|
+
await writeFile(join(demoRoot, 'README.md'), `# Castari Demo (Web + Agent)
|
|
656
|
+
|
|
657
|
+
This demo pairs a minimal Castari agent with a simple Next.js chat UI.
|
|
658
|
+
|
|
659
|
+
Structure:
|
|
660
|
+
- \`agent/\` β Castari agent you deploy to the platform.
|
|
661
|
+
- \`web/\` β Next.js app that chats with the agent via \`CastariClient\`.
|
|
662
|
+
|
|
663
|
+
## Prerequisites
|
|
664
|
+
- Bun (for the Castari CLI and scripts)
|
|
665
|
+
- Node 18+ (for the Next.js app)
|
|
666
|
+
- \`ANTHROPIC_API_KEY\`
|
|
667
|
+
- Castari Platform running locally or reachable via \`CASTARI_PLATFORM_URL\`
|
|
668
|
+
|
|
669
|
+
## 1) Prepare and deploy the agent
|
|
670
|
+
\`\`\`bash
|
|
671
|
+
cd castari_demo/agent
|
|
672
|
+
cp .env.example .env # add ANTHROPIC_API_KEY (and CASTARI_PLATFORM_URL if self-hosted)
|
|
673
|
+
bun install # pulls @castari/sdk from npm
|
|
674
|
+
castari deploy # builds snapshot castari-demo-agent (CLI must be installed)
|
|
675
|
+
# Optional: bun run src/agent.ts # run locally without the CLI
|
|
676
|
+
\`\`\`
|
|
677
|
+
|
|
678
|
+
## 2) Run the web app
|
|
679
|
+
\`\`\`bash
|
|
680
|
+
cd ../web
|
|
681
|
+
cp .env.example .env # add ANTHROPIC_API_KEY and CASTARI_PLATFORM_URL if needed
|
|
682
|
+
npm install # or bun install
|
|
683
|
+
npm run dev # opens http://localhost:3000
|
|
684
|
+
\`\`\`
|
|
685
|
+
|
|
686
|
+
The web app uses a single sandbox labeled \`{ app: 'castari-demo', env: 'local' }\`. That keeps the same sandbox alive across requests so chats stay in memory as long as you donβt delete it (\`stop({ delete: false })\` behavior).
|
|
687
|
+
`);
|
|
688
|
+
console.log(chalk.green('β
Demo scaffold created at ./castari_demo'));
|
|
689
|
+
console.log(chalk.white('Next steps:'));
|
|
690
|
+
console.log(chalk.white(' 1) cd castari_demo/agent && bun install && castari deploy'));
|
|
691
|
+
console.log(chalk.white(' 2) cd ../web && npm install && npm run dev'));
|
|
692
|
+
}
|
package/dist/commands/start.js
CHANGED
|
@@ -5,11 +5,6 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
export async function start(options) {
|
|
6
6
|
// Load .env from current directory
|
|
7
7
|
dotenv.config();
|
|
8
|
-
const apiKey = process.env.DAYTONA_API_KEY;
|
|
9
|
-
if (!apiKey) {
|
|
10
|
-
console.error(chalk.red('Error: DAYTONA_API_KEY is required.'));
|
|
11
|
-
process.exit(1);
|
|
12
|
-
}
|
|
13
8
|
// Read package.json for defaults
|
|
14
9
|
let snapshotName = options.snapshot;
|
|
15
10
|
let volumeName = options.volume;
|
|
@@ -31,11 +26,13 @@ export async function start(options) {
|
|
|
31
26
|
if (volumeName) {
|
|
32
27
|
console.log(chalk.blue(`π¦ Using volume: ${volumeName}`));
|
|
33
28
|
}
|
|
29
|
+
const platformUrl = process.env.CASTARI_PLATFORM_URL;
|
|
34
30
|
const client = new CastariClient({
|
|
35
31
|
snapshot: snapshotName,
|
|
36
32
|
volume: volumeName,
|
|
37
|
-
|
|
33
|
+
platformUrl,
|
|
38
34
|
debug: true, // Enable debug logs for CLI
|
|
35
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY
|
|
39
36
|
});
|
|
40
37
|
try {
|
|
41
38
|
await client.start();
|
package/dist/index.js
CHANGED
|
@@ -7,13 +7,14 @@ import { dev } from './commands/dev';
|
|
|
7
7
|
const cli = cac('castari');
|
|
8
8
|
cli
|
|
9
9
|
.command('init', 'Initialize a new Castari agent project')
|
|
10
|
+
.option('--demo', 'Generate a Castari demo (web + agent) scaffold')
|
|
10
11
|
.action(init);
|
|
11
12
|
cli
|
|
12
|
-
.command('deploy', '
|
|
13
|
+
.command('deploy', 'Deploy the agent to the Castari Platform')
|
|
13
14
|
.option('--snapshot <name>', 'Snapshot name (overrides package.json)')
|
|
14
15
|
.action(deploy);
|
|
15
16
|
cli
|
|
16
|
-
.command('start', 'Start a
|
|
17
|
+
.command('start', 'Start a sandbox for the agent')
|
|
17
18
|
.option('--snapshot <name>', 'Snapshot name')
|
|
18
19
|
.option('--volume <name>', 'Volume name for persistence')
|
|
19
20
|
.option('--id <id>', 'Custom sandbox ID')
|
|
@@ -22,5 +23,5 @@ cli
|
|
|
22
23
|
.command('dev', 'Run the agent locally for development')
|
|
23
24
|
.action(dev);
|
|
24
25
|
cli.help();
|
|
25
|
-
cli.version('0.0.
|
|
26
|
+
cli.version('0.0.4');
|
|
26
27
|
cli.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@castari/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"castari": "./dist/index.js"
|
|
@@ -17,17 +17,20 @@
|
|
|
17
17
|
"dev": "bun run src/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@anthropic-ai/claude-agent-sdk": "^0.1.44",
|
|
21
|
+
"@castari/sdk": "^0.0.4",
|
|
22
|
+
"adm-zip": "^0.5.16",
|
|
20
23
|
"cac": "^6.7.14",
|
|
21
|
-
"inquirer": "^9.2.12",
|
|
22
24
|
"chalk": "^5.3.0",
|
|
23
25
|
"dotenv": "^16.3.1",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"@anthropic-ai/claude-agent-sdk": "^0.1.44"
|
|
26
|
+
"form-data": "^4.0.5",
|
|
27
|
+
"inquirer": "^9.2.12"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
30
|
+
"@types/adm-zip": "^0.5.7",
|
|
31
|
+
"@types/form-data": "^2.5.2",
|
|
29
32
|
"@types/inquirer": "^9.0.7",
|
|
30
33
|
"@types/node": "^20.10.0",
|
|
31
34
|
"typescript": "^5.6.3"
|
|
32
35
|
}
|
|
33
|
-
}
|
|
36
|
+
}
|