@castari/cli 0.1.11 β 0.2.1
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 +79 -90
- package/bin/cast.js +2 -0
- package/dist/commands/agents.d.ts +6 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +174 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/apikey.d.ts +6 -0
- package/dist/commands/apikey.d.ts.map +1 -0
- package/dist/commands/apikey.js +57 -0
- package/dist/commands/apikey.js.map +1 -0
- package/dist/commands/deploy.d.ts +3 -3
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +24 -64
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/invoke.d.ts +3 -0
- package/dist/commands/invoke.d.ts.map +1 -0
- package/dist/commands/invoke.js +50 -0
- package/dist/commands/invoke.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/secrets.d.ts +6 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +90 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/stop.d.ts +3 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +25 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/usage.d.ts +3 -0
- package/dist/commands/usage.d.ts.map +1 -0
- package/dist/commands/usage.js +76 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +27 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -34
- package/dist/index.js.map +1 -0
- package/dist/utils/errors.d.ts +6 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +42 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/output.d.ts +45 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +75 -0
- package/dist/utils/output.js.map +1 -0
- package/package.json +56 -35
- package/LICENSE +0 -21
- package/dist/commands/client-id.d.ts +0 -1
- package/dist/commands/client-id.js +0 -25
- package/dist/commands/dev.d.ts +0 -1
- package/dist/commands/dev.js +0 -17
- package/dist/commands/generate-secrets.d.ts +0 -1
- package/dist/commands/generate-secrets.js +0 -60
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.js +0 -899
- package/dist/commands/start.d.ts +0 -5
- package/dist/commands/start.js +0 -100
- package/dist/utils/client-auth.d.ts +0 -11
- package/dist/utils/client-auth.js +0 -21
- package/dist/utils/client-id.d.ts +0 -10
- package/dist/utils/client-id.js +0 -36
package/dist/commands/init.js
DELETED
|
@@ -1,899 +0,0 @@
|
|
|
1
|
-
import inquirer from 'inquirer';
|
|
2
|
-
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
export async function init(options = {}) {
|
|
6
|
-
if (options.demo) {
|
|
7
|
-
return initDemo();
|
|
8
|
-
}
|
|
9
|
-
console.log(chalk.blue('π€ Initializing new Castari agent project...'));
|
|
10
|
-
const answers = await inquirer.prompt([
|
|
11
|
-
{
|
|
12
|
-
type: 'input',
|
|
13
|
-
name: 'name',
|
|
14
|
-
message: 'Agent name (used for snapshot):',
|
|
15
|
-
default: 'my-castari-agent',
|
|
16
|
-
},
|
|
17
|
-
]);
|
|
18
|
-
const packageJson = {
|
|
19
|
-
name: answers.name,
|
|
20
|
-
version: '0.1.0',
|
|
21
|
-
type: 'module',
|
|
22
|
-
scripts: {
|
|
23
|
-
dev: 'castari dev',
|
|
24
|
-
deploy: 'castari deploy',
|
|
25
|
-
start: 'castari start',
|
|
26
|
-
},
|
|
27
|
-
dependencies: {
|
|
28
|
-
'@castari/sdk': '^0.1.5',
|
|
29
|
-
'@anthropic-ai/claude-agent-sdk': '^0.1.50',
|
|
30
|
-
},
|
|
31
|
-
castari: {
|
|
32
|
-
volume: 'default-data',
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
const tsConfig = {
|
|
36
|
-
compilerOptions: {
|
|
37
|
-
target: 'ESNext',
|
|
38
|
-
module: 'ESNext',
|
|
39
|
-
moduleResolution: 'bundler',
|
|
40
|
-
strict: true,
|
|
41
|
-
skipLibCheck: true,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
const agentTs = `import { serve, tool } from '@castari/sdk'
|
|
45
|
-
|
|
46
|
-
// Define your agent tools and logic here
|
|
47
|
-
const myTool = tool({
|
|
48
|
-
name: 'hello',
|
|
49
|
-
description: 'Say hello',
|
|
50
|
-
inputSchema: {
|
|
51
|
-
type: 'object',
|
|
52
|
-
properties: {
|
|
53
|
-
name: { type: 'string' },
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
handler: async ({ name }) => {
|
|
57
|
-
return \`Hello, \${name}!\`
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Start the agent server
|
|
62
|
-
serve({
|
|
63
|
-
tools: [myTool],
|
|
64
|
-
systemPrompt: 'You are a helpful Castari agent.',
|
|
65
|
-
})
|
|
66
|
-
`;
|
|
67
|
-
const envExample = `ANTHROPIC_API_KEY=sk-ant-...\nCASTARI_CLIENT_ID=your-client-id\nCASTARI_API_KEY=castari_your-api-key\n`;
|
|
68
|
-
await writeFile('package.json', JSON.stringify(packageJson, null, 2));
|
|
69
|
-
await writeFile('tsconfig.json', JSON.stringify(tsConfig, null, 2));
|
|
70
|
-
await writeFile('.env.example', envExample);
|
|
71
|
-
await mkdir('src', { recursive: true });
|
|
72
|
-
await writeFile(join('src', 'agent.ts'), agentTs);
|
|
73
|
-
console.log(chalk.green('β
Project initialized!'));
|
|
74
|
-
console.log(chalk.white('Run `bun install` to install dependencies.'));
|
|
75
|
-
}
|
|
76
|
-
async function initDemo() {
|
|
77
|
-
console.log(chalk.blue('π¨ Initializing Castari demo (web + agent)...'));
|
|
78
|
-
const demoRoot = 'castari_demo';
|
|
79
|
-
const agentDir = join(demoRoot, 'agent');
|
|
80
|
-
const webDir = join(demoRoot, 'web');
|
|
81
|
-
const writeJson = async (path, obj) => writeFile(path, JSON.stringify(obj, null, 2));
|
|
82
|
-
// Agent scaffold
|
|
83
|
-
await mkdir(join(agentDir, 'src'), { recursive: true });
|
|
84
|
-
await writeJson(join(agentDir, 'package.json'), {
|
|
85
|
-
name: 'castari-demo-agent',
|
|
86
|
-
version: '0.1.0',
|
|
87
|
-
private: true,
|
|
88
|
-
type: 'module',
|
|
89
|
-
scripts: {
|
|
90
|
-
dev: 'bun run src/agent.ts',
|
|
91
|
-
deploy: 'castari deploy',
|
|
92
|
-
start: 'bun run src/agent.ts',
|
|
93
|
-
},
|
|
94
|
-
dependencies: {
|
|
95
|
-
'@castari/sdk': '^0.1.5',
|
|
96
|
-
'@anthropic-ai/claude-agent-sdk': '^0.1.50',
|
|
97
|
-
},
|
|
98
|
-
castari: {
|
|
99
|
-
volume: 'castari-demo-workspace',
|
|
100
|
-
},
|
|
101
|
-
});
|
|
102
|
-
await writeJson(join(agentDir, 'tsconfig.json'), {
|
|
103
|
-
compilerOptions: {
|
|
104
|
-
target: 'ESNext',
|
|
105
|
-
module: 'ESNext',
|
|
106
|
-
moduleResolution: 'bundler',
|
|
107
|
-
strict: true,
|
|
108
|
-
skipLibCheck: true,
|
|
109
|
-
esModuleInterop: true,
|
|
110
|
-
},
|
|
111
|
-
include: ['src'],
|
|
112
|
-
});
|
|
113
|
-
await writeFile(join(agentDir, '.env.example'), 'CASTARI_CLIENT_ID=your-client-id\nCASTARI_API_KEY=castari_your-api-key\n');
|
|
114
|
-
await writeFile(join(agentDir, 'src', 'agent.ts'), `import { serve } from '@castari/sdk'
|
|
115
|
-
|
|
116
|
-
serve({
|
|
117
|
-
systemPrompt: [
|
|
118
|
-
'You are Castari Demo Agent.',
|
|
119
|
-
'Keep responses concise and friendly.',
|
|
120
|
-
'Feel free to explain how you can help with code or product questions.',
|
|
121
|
-
].join(' '),
|
|
122
|
-
includePartialMessages: true,
|
|
123
|
-
})
|
|
124
|
-
`);
|
|
125
|
-
// Web scaffold
|
|
126
|
-
await mkdir(join(webDir, 'app', 'api', 'chat'), { recursive: true });
|
|
127
|
-
await mkdir(join(webDir, 'lib'), { recursive: true });
|
|
128
|
-
await mkdir(join(webDir, 'public'), { recursive: true });
|
|
129
|
-
await writeJson(join(webDir, 'package.json'), {
|
|
130
|
-
name: 'castari-demo-web',
|
|
131
|
-
version: '0.1.0',
|
|
132
|
-
private: true,
|
|
133
|
-
scripts: {
|
|
134
|
-
dev: 'next dev',
|
|
135
|
-
build: 'next build',
|
|
136
|
-
start: 'next start',
|
|
137
|
-
},
|
|
138
|
-
dependencies: {
|
|
139
|
-
'@castari/sdk': '^0.1.5',
|
|
140
|
-
next: '14.2.3',
|
|
141
|
-
react: '18.3.1',
|
|
142
|
-
'react-dom': '18.3.1',
|
|
143
|
-
},
|
|
144
|
-
devDependencies: {
|
|
145
|
-
'@types/node': '^20.11.0',
|
|
146
|
-
'@types/react': '^18.2.0',
|
|
147
|
-
'@types/react-dom': '^18.2.0',
|
|
148
|
-
typescript: '^5.6.3',
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
await writeJson(join(webDir, 'tsconfig.json'), {
|
|
152
|
-
compilerOptions: {
|
|
153
|
-
target: 'ES2020',
|
|
154
|
-
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
155
|
-
allowJs: true,
|
|
156
|
-
skipLibCheck: true,
|
|
157
|
-
strict: true,
|
|
158
|
-
noEmit: true,
|
|
159
|
-
esModuleInterop: true,
|
|
160
|
-
module: 'esnext',
|
|
161
|
-
moduleResolution: 'bundler',
|
|
162
|
-
resolveJsonModule: true,
|
|
163
|
-
isolatedModules: true,
|
|
164
|
-
jsx: 'preserve',
|
|
165
|
-
incremental: true,
|
|
166
|
-
types: ['node'],
|
|
167
|
-
baseUrl: '.',
|
|
168
|
-
paths: {
|
|
169
|
-
'@/*': ['./*'],
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
173
|
-
exclude: ['node_modules'],
|
|
174
|
-
});
|
|
175
|
-
await writeFile(join(webDir, 'next-env.d.ts'), `/// <reference types="next" />
|
|
176
|
-
/// <reference types="next/types/global" />
|
|
177
|
-
/// <reference types="next/image-types/global" />
|
|
178
|
-
|
|
179
|
-
// NOTE: This file should not be edited
|
|
180
|
-
`);
|
|
181
|
-
await writeFile(join(webDir, 'next.config.mjs'), `/** @type {import('next').NextConfig} */
|
|
182
|
-
const nextConfig = {
|
|
183
|
-
experimental: {
|
|
184
|
-
serverActions: false
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export default nextConfig
|
|
189
|
-
`);
|
|
190
|
-
await writeFile(join(webDir, '.env.example'), 'ANTHROPIC_API_KEY=sk-ant-...\nCASTARI_CLIENT_ID=your-client-id\nCASTARI_API_KEY=castari_your-api-key\n# CASTARI_DEBUG=false\n');
|
|
191
|
-
await writeFile(join(webDir, 'app', 'globals.css'), `:root {
|
|
192
|
-
color-scheme: dark;
|
|
193
|
-
--bg: #131418;
|
|
194
|
-
--panel: #1c1d24;
|
|
195
|
-
--border: #2a2b36;
|
|
196
|
-
--muted: #888888;
|
|
197
|
-
--text: #ffffff;
|
|
198
|
-
--accent: #ffffff;
|
|
199
|
-
font-family: var(--font-inter), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
* {
|
|
203
|
-
box-sizing: border-box;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
body {
|
|
207
|
-
margin: 0;
|
|
208
|
-
background-color: var(--bg);
|
|
209
|
-
color: var(--text);
|
|
210
|
-
min-height: 100vh;
|
|
211
|
-
font-weight: 300;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
a {
|
|
215
|
-
color: inherit;
|
|
216
|
-
text-decoration: none;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
.page-shell {
|
|
220
|
-
position: relative;
|
|
221
|
-
min-height: 100vh;
|
|
222
|
-
overflow: hidden;
|
|
223
|
-
display: flex;
|
|
224
|
-
justify-content: center;
|
|
225
|
-
padding: 36px 20px 72px;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
.content {
|
|
229
|
-
position: relative;
|
|
230
|
-
width: min(1180px, 100%);
|
|
231
|
-
display: flex;
|
|
232
|
-
flex-direction: column;
|
|
233
|
-
gap: 28px;
|
|
234
|
-
z-index: 1;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
.topbar {
|
|
238
|
-
display: flex;
|
|
239
|
-
align-items: center;
|
|
240
|
-
justify-content: space-between;
|
|
241
|
-
gap: 12px;
|
|
242
|
-
flex-wrap: wrap;
|
|
243
|
-
padding-bottom: 12px;
|
|
244
|
-
border-bottom: 1px solid var(--border);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.brand {
|
|
248
|
-
display: flex;
|
|
249
|
-
align-items: center;
|
|
250
|
-
gap: 14px;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
.brand-logo {
|
|
254
|
-
width: 32px;
|
|
255
|
-
height: auto;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
.brand-name {
|
|
259
|
-
font-family: var(--font-orbitron), 'Orbitron', sans-serif;
|
|
260
|
-
letter-spacing: 0.06em;
|
|
261
|
-
text-transform: uppercase;
|
|
262
|
-
font-size: 18px;
|
|
263
|
-
color: var(--text);
|
|
264
|
-
font-weight: 600;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
.top-pills {
|
|
268
|
-
display: flex;
|
|
269
|
-
gap: 8px;
|
|
270
|
-
flex-wrap: wrap;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
.hero {
|
|
274
|
-
display: flex;
|
|
275
|
-
flex-direction: column;
|
|
276
|
-
gap: 12px;
|
|
277
|
-
padding: 40px 0 20px;
|
|
278
|
-
align-items: center;
|
|
279
|
-
text-align: center;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
.hero-title {
|
|
283
|
-
font-family: var(--font-orbitron), 'Orbitron', sans-serif;
|
|
284
|
-
font-size: clamp(30px, 4vw, 44px);
|
|
285
|
-
margin: 0;
|
|
286
|
-
letter-spacing: 0.02em;
|
|
287
|
-
font-weight: 600;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
.hero-copy {
|
|
291
|
-
color: var(--muted);
|
|
292
|
-
font-size: 18px;
|
|
293
|
-
line-height: 1.6;
|
|
294
|
-
max-width: 600px;
|
|
295
|
-
margin: 0;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
.card {
|
|
299
|
-
position: relative;
|
|
300
|
-
background: var(--panel);
|
|
301
|
-
border: 1px solid var(--border);
|
|
302
|
-
border-radius: 20px;
|
|
303
|
-
padding: 22px;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
.panel {
|
|
307
|
-
overflow: hidden;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
.panel-header {
|
|
311
|
-
display: flex;
|
|
312
|
-
align-items: center;
|
|
313
|
-
justify-content: space-between;
|
|
314
|
-
gap: 14px;
|
|
315
|
-
flex-wrap: wrap;
|
|
316
|
-
margin-bottom: 20px;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
.panel-title {
|
|
320
|
-
font-size: 18px;
|
|
321
|
-
font-weight: 500;
|
|
322
|
-
margin: 0;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
.panel-copy {
|
|
326
|
-
color: var(--muted);
|
|
327
|
-
font-size: 14px;
|
|
328
|
-
margin: 4px 0 0;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
.pill-row {
|
|
332
|
-
display: flex;
|
|
333
|
-
gap: 8px;
|
|
334
|
-
flex-wrap: wrap;
|
|
335
|
-
margin-bottom: 12px;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
.messages {
|
|
339
|
-
display: flex;
|
|
340
|
-
flex-direction: column;
|
|
341
|
-
gap: 12px;
|
|
342
|
-
background: #16171d;
|
|
343
|
-
border: 1px solid var(--border);
|
|
344
|
-
border-radius: 16px;
|
|
345
|
-
padding: 14px;
|
|
346
|
-
min-height: 300px;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
.bubble {
|
|
350
|
-
padding: 14px 16px;
|
|
351
|
-
border-radius: 14px;
|
|
352
|
-
line-height: 1.5;
|
|
353
|
-
font-size: 15px;
|
|
354
|
-
max-width: 85%;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
.bubble.assistant {
|
|
358
|
-
background: #1c1d24;
|
|
359
|
-
color: var(--text);
|
|
360
|
-
align-self: flex-start;
|
|
361
|
-
border: 1px solid var(--border);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.bubble.user {
|
|
365
|
-
background: #ffffff;
|
|
366
|
-
color: #000000;
|
|
367
|
-
align-self: flex-end;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.form {
|
|
371
|
-
display: flex;
|
|
372
|
-
gap: 12px;
|
|
373
|
-
align-items: center;
|
|
374
|
-
margin-top: 16px;
|
|
375
|
-
background: #16171d;
|
|
376
|
-
border: 1px solid var(--border);
|
|
377
|
-
border-radius: 14px;
|
|
378
|
-
padding: 10px;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
.input {
|
|
382
|
-
flex: 1;
|
|
383
|
-
padding: 12px 14px;
|
|
384
|
-
border-radius: 12px;
|
|
385
|
-
border: 1px solid transparent;
|
|
386
|
-
font-size: 15px;
|
|
387
|
-
outline: none;
|
|
388
|
-
background: transparent;
|
|
389
|
-
color: var(--text);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
.input::placeholder {
|
|
393
|
-
color: #666666;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
.button {
|
|
397
|
-
background: #ffffff;
|
|
398
|
-
color: #000000;
|
|
399
|
-
border: none;
|
|
400
|
-
border-radius: 12px;
|
|
401
|
-
padding: 12px 24px;
|
|
402
|
-
font-weight: 600;
|
|
403
|
-
cursor: pointer;
|
|
404
|
-
transition: opacity 0.2s;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
.button:disabled {
|
|
408
|
-
opacity: 0.5;
|
|
409
|
-
cursor: not-allowed;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
.button:hover:not(:disabled) {
|
|
413
|
-
opacity: 0.9;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.status {
|
|
417
|
-
font-size: 13px;
|
|
418
|
-
color: var(--muted);
|
|
419
|
-
margin-top: 10px;
|
|
420
|
-
text-align: center;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
.pill {
|
|
424
|
-
display: inline-flex;
|
|
425
|
-
align-items: center;
|
|
426
|
-
gap: 8px;
|
|
427
|
-
padding: 6px 12px;
|
|
428
|
-
border-radius: 999px;
|
|
429
|
-
border: 1px solid var(--border);
|
|
430
|
-
background: transparent;
|
|
431
|
-
color: var(--muted);
|
|
432
|
-
font-size: 12px;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
.pill-accent {
|
|
436
|
-
color: var(--text);
|
|
437
|
-
border-color: var(--text);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
.pill-ghost {
|
|
441
|
-
border-color: var(--border);
|
|
442
|
-
color: var(--muted);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
@media (max-width: 720px) {
|
|
446
|
-
.page-shell {
|
|
447
|
-
padding: 28px 16px 56px;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
.form {
|
|
451
|
-
flex-direction: column;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
.button {
|
|
455
|
-
width: 100%;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
`);
|
|
459
|
-
await writeFile(join(webDir, 'app', 'layout.tsx'), `import './globals.css'
|
|
460
|
-
import { ReactNode } from 'react'
|
|
461
|
-
import { Inter, Orbitron } from 'next/font/google'
|
|
462
|
-
|
|
463
|
-
const inter = Inter({
|
|
464
|
-
subsets: ['latin'],
|
|
465
|
-
weight: ['300', '500', '700'],
|
|
466
|
-
variable: '--font-inter',
|
|
467
|
-
})
|
|
468
|
-
|
|
469
|
-
const orbitron = Orbitron({
|
|
470
|
-
subsets: ['latin'],
|
|
471
|
-
weight: ['600'],
|
|
472
|
-
variable: '--font-orbitron',
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
476
|
-
return (
|
|
477
|
-
<html lang="en">
|
|
478
|
-
<head>
|
|
479
|
-
<title>Castari Demo Chat</title>
|
|
480
|
-
</head>
|
|
481
|
-
<body className={\`\${inter.variable} \${orbitron.variable}\`}>{children}</body>
|
|
482
|
-
</html>
|
|
483
|
-
)
|
|
484
|
-
}
|
|
485
|
-
`);
|
|
486
|
-
await writeFile(join(webDir, 'app', 'page.tsx'), `"use client"
|
|
487
|
-
|
|
488
|
-
import { FormEvent, useState } from 'react'
|
|
489
|
-
import Image from 'next/image'
|
|
490
|
-
|
|
491
|
-
type ChatMessage = {
|
|
492
|
-
role: 'assistant' | 'user'
|
|
493
|
-
content: string
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export default function Home() {
|
|
497
|
-
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
498
|
-
{
|
|
499
|
-
role: 'assistant',
|
|
500
|
-
content:
|
|
501
|
-
'Hi! I am the Castari demo agent. Ask me anything about your code, architecture, or how this demo works.',
|
|
502
|
-
},
|
|
503
|
-
])
|
|
504
|
-
const [input, setInput] = useState('')
|
|
505
|
-
const [status, setStatus] = useState<string | null>(null)
|
|
506
|
-
const [sending, setSending] = useState(false)
|
|
507
|
-
|
|
508
|
-
const sendMessage = async (event: FormEvent) => {
|
|
509
|
-
event.preventDefault()
|
|
510
|
-
if (!input.trim() || sending) return
|
|
511
|
-
|
|
512
|
-
const userMessage: ChatMessage = { role: 'user', content: input.trim() }
|
|
513
|
-
setMessages(prev => [...prev, userMessage])
|
|
514
|
-
setInput('')
|
|
515
|
-
setSending(true)
|
|
516
|
-
setStatus('Connecting to your sandbox...')
|
|
517
|
-
|
|
518
|
-
// Add assistant placeholder immediately so we can stream into it
|
|
519
|
-
let assistantIndex = -1
|
|
520
|
-
setMessages(prev => {
|
|
521
|
-
assistantIndex = prev.length
|
|
522
|
-
return [...prev, { role: 'assistant', content: '' }]
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
const response = await fetch('/api/chat', {
|
|
527
|
-
method: 'POST',
|
|
528
|
-
headers: { 'Content-Type': 'application/json' },
|
|
529
|
-
body: JSON.stringify({ message: userMessage.content }),
|
|
530
|
-
})
|
|
531
|
-
|
|
532
|
-
if (!response.ok || !response.body) {
|
|
533
|
-
const errorText = await response.text()
|
|
534
|
-
throw new Error(errorText || 'Request failed')
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const reader = response.body.getReader()
|
|
538
|
-
const decoder = new TextDecoder()
|
|
539
|
-
|
|
540
|
-
while (true) {
|
|
541
|
-
const { value, done } = await reader.read()
|
|
542
|
-
if (done) break
|
|
543
|
-
const chunk = decoder.decode(value, { stream: true })
|
|
544
|
-
if (!chunk) continue
|
|
545
|
-
const idx = assistantIndex
|
|
546
|
-
setMessages(prev =>
|
|
547
|
-
prev.map((m, i) => (i === idx ? { ...m, content: m.content + chunk } : m)),
|
|
548
|
-
)
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
setStatus(null)
|
|
552
|
-
} catch (err) {
|
|
553
|
-
const message =
|
|
554
|
-
err instanceof Error ? err.message : 'Something went wrong talking to the agent.'
|
|
555
|
-
setMessages(prev =>
|
|
556
|
-
prev.map((m, i) =>
|
|
557
|
-
i === assistantIndex
|
|
558
|
-
? { ...m, content: \`I hit an error: \${message}\` }
|
|
559
|
-
: m,
|
|
560
|
-
),
|
|
561
|
-
)
|
|
562
|
-
setStatus('Temporary hiccup. Try again in a moment.')
|
|
563
|
-
} finally {
|
|
564
|
-
setSending(false)
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return (
|
|
569
|
-
<main className="page-shell">
|
|
570
|
-
<div className="content">
|
|
571
|
-
<header className="topbar">
|
|
572
|
-
<div className="brand">
|
|
573
|
-
<Image
|
|
574
|
-
src="/logo.svg"
|
|
575
|
-
alt="Castari Logo"
|
|
576
|
-
width={32}
|
|
577
|
-
height={32}
|
|
578
|
-
className="brand-logo"
|
|
579
|
-
/>
|
|
580
|
-
<div>
|
|
581
|
-
<div className="brand-name">Castari</div>
|
|
582
|
-
</div>
|
|
583
|
-
</div>
|
|
584
|
-
|
|
585
|
-
<div className="top-pills">
|
|
586
|
-
<span className="pill pill-ghost">Demo sandbox</span>
|
|
587
|
-
</div>
|
|
588
|
-
</header>
|
|
589
|
-
|
|
590
|
-
<section className="hero">
|
|
591
|
-
<h1 className="hero-title">Production-Ready Agents</h1>
|
|
592
|
-
<p className="hero-copy">
|
|
593
|
-
Secure, sandboxed AI agents for your infrastructure.
|
|
594
|
-
</p>
|
|
595
|
-
</section>
|
|
596
|
-
|
|
597
|
-
<section className="card panel">
|
|
598
|
-
<div className="panel-header">
|
|
599
|
-
<div>
|
|
600
|
-
<div className="panel-title">Chat with Castari</div>
|
|
601
|
-
</div>
|
|
602
|
-
<span className="pill pill-accent">Live</span>
|
|
603
|
-
</div>
|
|
604
|
-
|
|
605
|
-
<div className="pill-row">
|
|
606
|
-
<span className="pill">Snapshot: castari-demo-agent</span>
|
|
607
|
-
<span className="pill">Volume: castari-demo-workspace</span>
|
|
608
|
-
</div>
|
|
609
|
-
|
|
610
|
-
<div className="messages">
|
|
611
|
-
{messages.map((m, idx) => (
|
|
612
|
-
<div key={idx} className={\`bubble \${m.role}\`}>
|
|
613
|
-
{m.content}
|
|
614
|
-
</div>
|
|
615
|
-
))}
|
|
616
|
-
</div>
|
|
617
|
-
|
|
618
|
-
<form className="form" onSubmit={sendMessage}>
|
|
619
|
-
<input
|
|
620
|
-
className="input"
|
|
621
|
-
placeholder="Ask the Castari agent anything..."
|
|
622
|
-
value={input}
|
|
623
|
-
onChange={e => setInput(e.target.value)}
|
|
624
|
-
/>
|
|
625
|
-
<button className="button" type="submit" disabled={sending}>
|
|
626
|
-
{sending ? 'Talkingβ¦' : 'Send'}
|
|
627
|
-
</button>
|
|
628
|
-
</form>
|
|
629
|
-
{status && <div className="status">{status}</div>}
|
|
630
|
-
</section>
|
|
631
|
-
</div>
|
|
632
|
-
</main>
|
|
633
|
-
)
|
|
634
|
-
}
|
|
635
|
-
`);
|
|
636
|
-
await writeFile(join(webDir, 'lib', 'castari-client.ts'), `import { CastariClient, type WSOutputMessage } from '@castari/sdk/client'
|
|
637
|
-
|
|
638
|
-
const SNAPSHOT = 'castari-demo-agent'
|
|
639
|
-
const LABELS = { app: 'castari-demo', env: 'local' }
|
|
640
|
-
const VOLUME = 'castari-demo-workspace'
|
|
641
|
-
|
|
642
|
-
let clientPromise: Promise<CastariClient> | null = null
|
|
643
|
-
let currentClient: CastariClient | null = null
|
|
644
|
-
|
|
645
|
-
async function createClient() {
|
|
646
|
-
const anthropicApiKey = process.env.ANTHROPIC_API_KEY
|
|
647
|
-
if (!anthropicApiKey) {
|
|
648
|
-
throw new Error('ANTHROPIC_API_KEY is required to contact the Castari agent')
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const client = new CastariClient({
|
|
652
|
-
snapshot: SNAPSHOT,
|
|
653
|
-
// volume: VOLUME, // opt-in; omit to run ephemeral unless provided
|
|
654
|
-
labels: LABELS,
|
|
655
|
-
clientId: process.env.CASTARI_CLIENT_ID,
|
|
656
|
-
platformApiKey: process.env.CASTARI_API_KEY,
|
|
657
|
-
anthropicApiKey,
|
|
658
|
-
debug: process.env.CASTARI_DEBUG === 'true',
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
// Clear singleton when connection closes so we reconnect on next message
|
|
662
|
-
client.onClose((code, reason) => {
|
|
663
|
-
console.log(\`[Castari] Connection closed (code=\${code}), will reconnect on next message\`)
|
|
664
|
-
clientPromise = null
|
|
665
|
-
currentClient = null
|
|
666
|
-
})
|
|
667
|
-
|
|
668
|
-
await client.start()
|
|
669
|
-
currentClient = client
|
|
670
|
-
return client
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
async function getClient() {
|
|
674
|
-
// If we have a client but it's disconnected, clear and recreate
|
|
675
|
-
if (currentClient && !currentClient.isConnected()) {
|
|
676
|
-
console.log('[Castari] Client disconnected, reconnecting...')
|
|
677
|
-
clientPromise = null
|
|
678
|
-
currentClient = null
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (!clientPromise) {
|
|
682
|
-
clientPromise = createClient()
|
|
683
|
-
}
|
|
684
|
-
return clientPromise
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function extractText(content: unknown): string {
|
|
688
|
-
if (typeof content === 'string') return content
|
|
689
|
-
if (Array.isArray(content)) {
|
|
690
|
-
return content
|
|
691
|
-
.map(block => {
|
|
692
|
-
if (typeof block === 'string') return block
|
|
693
|
-
if (block && typeof block === 'object' && 'text' in block) {
|
|
694
|
-
return (block as { text?: string }).text ?? ''
|
|
695
|
-
}
|
|
696
|
-
return ''
|
|
697
|
-
})
|
|
698
|
-
.join('\\n')
|
|
699
|
-
}
|
|
700
|
-
if (content && typeof content === 'object' && 'text' in (content as any)) {
|
|
701
|
-
return (content as any).text ?? ''
|
|
702
|
-
}
|
|
703
|
-
return ''
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function extractStreamDelta(event: any): string {
|
|
707
|
-
if (!event || typeof event !== 'object') return ''
|
|
708
|
-
if (event.type === 'content_block_delta' && event.delta?.text) {
|
|
709
|
-
return String(event.delta.text)
|
|
710
|
-
}
|
|
711
|
-
if (event.type === 'content_block_start' && event.content_block?.text) {
|
|
712
|
-
return String(event.content_block.text)
|
|
713
|
-
}
|
|
714
|
-
if (event.type === 'message_delta' && event.delta?.text) {
|
|
715
|
-
return String(event.delta.text)
|
|
716
|
-
}
|
|
717
|
-
return ''
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
export async function streamMessageToAgent(
|
|
721
|
-
message: string,
|
|
722
|
-
handlers: {
|
|
723
|
-
onChunk?: (text: string) => void
|
|
724
|
-
},
|
|
725
|
-
): Promise<void> {
|
|
726
|
-
const client = await getClient()
|
|
727
|
-
|
|
728
|
-
return new Promise((resolve, reject) => {
|
|
729
|
-
const timeout = setTimeout(() => {
|
|
730
|
-
cleanup()
|
|
731
|
-
reject(new Error('Timed out waiting for agent response'))
|
|
732
|
-
}, 60000)
|
|
733
|
-
|
|
734
|
-
let sawStreamChunk = false
|
|
735
|
-
|
|
736
|
-
const unsubscribe = client.onMessage((msg: WSOutputMessage) => {
|
|
737
|
-
if (msg.type !== 'sdk_message') return
|
|
738
|
-
const data: any = msg.data
|
|
739
|
-
|
|
740
|
-
if (data.type === 'stream_event') {
|
|
741
|
-
const deltaText = extractStreamDelta(data.event)
|
|
742
|
-
if (deltaText) {
|
|
743
|
-
sawStreamChunk = true
|
|
744
|
-
handlers.onChunk?.(deltaText)
|
|
745
|
-
}
|
|
746
|
-
} else if (data.type === 'assistant_message') {
|
|
747
|
-
finish()
|
|
748
|
-
} else if (data.type === 'result' && data.subtype === 'success') {
|
|
749
|
-
if (!sawStreamChunk) {
|
|
750
|
-
const text =
|
|
751
|
-
typeof data.result === 'string'
|
|
752
|
-
? data.result
|
|
753
|
-
: extractText(data.result?.output || data.result?.message || '')
|
|
754
|
-
if (text) handlers.onChunk?.(text)
|
|
755
|
-
}
|
|
756
|
-
finish()
|
|
757
|
-
} else if (data.type === 'error') {
|
|
758
|
-
cleanup()
|
|
759
|
-
reject(new Error(data.error || 'Agent returned an error'))
|
|
760
|
-
}
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
const cleanup = () => {
|
|
764
|
-
clearTimeout(timeout)
|
|
765
|
-
unsubscribe()
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const finish = () => {
|
|
769
|
-
cleanup()
|
|
770
|
-
resolve()
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
try {
|
|
774
|
-
client.send({
|
|
775
|
-
type: 'user_message',
|
|
776
|
-
data: {
|
|
777
|
-
type: 'user',
|
|
778
|
-
message: { role: 'user', content: message },
|
|
779
|
-
} as any,
|
|
780
|
-
})
|
|
781
|
-
} catch (err) {
|
|
782
|
-
cleanup()
|
|
783
|
-
reject(err)
|
|
784
|
-
}
|
|
785
|
-
})
|
|
786
|
-
}
|
|
787
|
-
`);
|
|
788
|
-
await writeFile(join(webDir, 'app', 'api', 'chat', 'route.ts'), `export const runtime = 'nodejs'
|
|
789
|
-
|
|
790
|
-
import { streamMessageToAgent } from '@/lib/castari-client'
|
|
791
|
-
|
|
792
|
-
export async function POST(request: Request) {
|
|
793
|
-
const body = (await request.json()) as { message?: string }
|
|
794
|
-
const message = body.message?.trim()
|
|
795
|
-
|
|
796
|
-
if (!message) {
|
|
797
|
-
return new Response(JSON.stringify({ error: 'message is required' }), {
|
|
798
|
-
status: 400,
|
|
799
|
-
headers: { 'Content-Type': 'application/json' },
|
|
800
|
-
})
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const encoder = new TextEncoder()
|
|
804
|
-
|
|
805
|
-
const stream = new ReadableStream({
|
|
806
|
-
async start(controller) {
|
|
807
|
-
try {
|
|
808
|
-
await streamMessageToAgent(message, {
|
|
809
|
-
onChunk: text => controller.enqueue(encoder.encode(text)),
|
|
810
|
-
})
|
|
811
|
-
controller.close()
|
|
812
|
-
} catch (err) {
|
|
813
|
-
controller.error(err)
|
|
814
|
-
}
|
|
815
|
-
},
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
return new Response(stream, {
|
|
819
|
-
headers: {
|
|
820
|
-
'Content-Type': 'text/plain; charset=utf-8',
|
|
821
|
-
'Transfer-Encoding': 'chunked',
|
|
822
|
-
},
|
|
823
|
-
})
|
|
824
|
-
}
|
|
825
|
-
`);
|
|
826
|
-
// Logo SVG
|
|
827
|
-
await writeFile(join(webDir, 'public', 'logo.svg'), `<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
828
|
-
<path d="M0 32L31.619 0L63.2381 32L31.619 64L0 32Z" fill="#A44A0C"/>
|
|
829
|
-
<path d="M31.527 31.8743H6.87042L24.3595 24.3417L31.527 7.24737V31.8743Z" fill="white"/>
|
|
830
|
-
<path d="M31.527 31.8743H6.87042L24.3595 24.3417L31.527 31.8743Z" fill="url(#paint0_linear_173_2089)"/>
|
|
831
|
-
<path d="M31.5268 31.872L31.5192 6.91829L24.0817 24.6204L31.5268 31.872Z" fill="url(#paint1_linear_173_2089)"/>
|
|
832
|
-
<path d="M31.7116 32.128L31.7192 57.0817L38.9973 39.3796L31.7116 32.128Z" fill="url(#paint2_linear_173_2089)"/>
|
|
833
|
-
<path d="M31.7117 32.1258H56.3683L38.8792 39.6584L31.7117 56.7527V32.1258Z" fill="white"/>
|
|
834
|
-
<path d="M31.7117 32.1258H56.3683L38.8792 39.6584L31.7117 32.1258Z" fill="url(#paint3_linear_173_2089)"/>
|
|
835
|
-
<defs>
|
|
836
|
-
<linearGradient id="paint0_linear_173_2089" x1="53.6475" y1="32.6825" x2="36.7362" y2="37.9872" gradientUnits="userSpaceOnUse">
|
|
837
|
-
<stop stop-color="#F8F8F8"/>
|
|
838
|
-
<stop offset="1" stop-color="#EACAB4"/>
|
|
839
|
-
</linearGradient>
|
|
840
|
-
<linearGradient id="paint1_linear_173_2089" x1="6.97479" y1="59.2675" x2="54.1923" y2="54.7965" gradientUnits="userSpaceOnUse">
|
|
841
|
-
<stop stop-color="white"/>
|
|
842
|
-
<stop offset="1" stop-color="#EDEDED"/>
|
|
843
|
-
</linearGradient>
|
|
844
|
-
<linearGradient id="paint2_linear_173_2089" x1="6.97479" y1="59.2675" x2="54.1923" y2="54.7965" gradientUnits="userSpaceOnUse">
|
|
845
|
-
<stop stop-color="white"/>
|
|
846
|
-
<stop offset="1" stop-color="#EDEDED"/>
|
|
847
|
-
</linearGradient>
|
|
848
|
-
<linearGradient id="paint3_linear_173_2089" x1="53.6475" y1="32.6825" x2="36.7362" y2="37.9872" gradientUnits="userSpaceOnUse">
|
|
849
|
-
<stop stop-color="#F8F8F8"/>
|
|
850
|
-
<stop offset="1" stop-color="#EACAB4"/>
|
|
851
|
-
</linearGradient>
|
|
852
|
-
</defs>
|
|
853
|
-
</svg>
|
|
854
|
-
`);
|
|
855
|
-
// Demo README
|
|
856
|
-
await writeFile(join(demoRoot, 'README.md'), `# Castari Demo (Web + Agent)
|
|
857
|
-
|
|
858
|
-
This demo pairs a minimal Castari agent with a simple Next.js chat UI.
|
|
859
|
-
|
|
860
|
-
Structure:
|
|
861
|
-
- \`agent/\` β Castari agent you deploy to the platform.
|
|
862
|
-
- \`web/\` β Next.js app that chats with the agent via \`CastariClient\`.
|
|
863
|
-
|
|
864
|
-
## Prerequisites
|
|
865
|
-
- Bun (for the Castari CLI and scripts)
|
|
866
|
-
- Node 18+ (for the Next.js app)
|
|
867
|
-
- \`ANTHROPIC_API_KEY\`
|
|
868
|
-
- Castari credentials (\`CASTARI_CLIENT_ID\` and \`CASTARI_API_KEY\`)
|
|
869
|
-
|
|
870
|
-
## Getting Castari Credentials
|
|
871
|
-
Generate your clientId and apiKey by running:
|
|
872
|
-
\`\`\`bash
|
|
873
|
-
castari generate-secrets
|
|
874
|
-
\`\`\`
|
|
875
|
-
This will output both values so you can copy them to your \`.env\` files.
|
|
876
|
-
|
|
877
|
-
## 1) Prepare and deploy the agent
|
|
878
|
-
\`\`\`bash
|
|
879
|
-
cd castari_demo/agent
|
|
880
|
-
cp .env.example .env # add CASTARI_CLIENT_ID, CASTARI_API_KEY
|
|
881
|
-
bun install # pulls @castari/sdk from npm
|
|
882
|
-
castari deploy # builds snapshot castari-demo-agent (CLI must be installed)
|
|
883
|
-
\`\`\`
|
|
884
|
-
|
|
885
|
-
## 2) Run the web app
|
|
886
|
-
\`\`\`bash
|
|
887
|
-
cd ../web
|
|
888
|
-
cp .env.example .env # add ANTHROPIC_API_KEY, CASTARI_CLIENT_ID, CASTARI_API_KEY
|
|
889
|
-
npm install # or bun install
|
|
890
|
-
npm run dev # opens http://localhost:3000
|
|
891
|
-
\`\`\`
|
|
892
|
-
|
|
893
|
-
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).
|
|
894
|
-
`);
|
|
895
|
-
console.log(chalk.green('β
Demo scaffold created at ./castari_demo'));
|
|
896
|
-
console.log(chalk.white('Next steps:'));
|
|
897
|
-
console.log(chalk.white(' 1) cd castari_demo/agent && bun install && castari deploy'));
|
|
898
|
-
console.log(chalk.white(' 2) cd ../web && npm install && npm run dev'));
|
|
899
|
-
}
|