@castari/cli 0.0.6 → 0.0.8

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.
@@ -0,0 +1 @@
1
+ export declare function resolveClientId(): Promise<string>;
@@ -0,0 +1,25 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ import chalk from 'chalk';
6
+ const CONFIG_PATH = join(homedir(), '.castari', 'client.json');
7
+ export async function resolveClientId() {
8
+ const envId = process.env.CASTARI_CLIENT_ID;
9
+ if (envId)
10
+ return envId.trim();
11
+ if (existsSync(CONFIG_PATH)) {
12
+ try {
13
+ const raw = await readFile(CONFIG_PATH, 'utf-8');
14
+ const data = JSON.parse(raw);
15
+ if (data.clientId)
16
+ return data.clientId.trim();
17
+ }
18
+ catch {
19
+ // ignore and fall through
20
+ }
21
+ }
22
+ console.error(chalk.red('❌ CASTARI_CLIENT_ID is required.'));
23
+ console.error(chalk.white('Run `castari generate-client-id` to create one (stored in ~/.castari/client.json), or set CASTARI_CLIENT_ID in your environment.'));
24
+ throw new Error('Missing CASTARI_CLIENT_ID');
25
+ }
@@ -1,7 +1,9 @@
1
1
  import { readFile } from 'fs/promises';
2
2
  import chalk from 'chalk';
3
3
  import AdmZip from 'adm-zip';
4
+ import { getClientAuthOrExit } from '../utils/client-auth';
4
5
  export async function deploy(options) {
6
+ const { clientId, apiKey } = await getClientAuthOrExit();
5
7
  // Read package.json to get default snapshot name
6
8
  let snapshotName = options.snapshot;
7
9
  if (!snapshotName) {
@@ -43,17 +45,22 @@ export async function deploy(options) {
43
45
  const formData = new FormData();
44
46
  formData.append('file', new Blob([zipBuffer]), 'source.zip');
45
47
  formData.append('snapshot', snapshotName);
48
+ formData.append('clientId', clientId);
46
49
  try {
50
+ const headers = {};
51
+ if (apiKey)
52
+ headers['Authorization'] = `Bearer ${apiKey}`;
47
53
  const response = await fetch(`${platformUrl}/deploy`, {
48
54
  method: 'POST',
49
55
  body: formData,
56
+ headers,
50
57
  });
51
58
  if (!response.ok) {
52
59
  const errorText = await response.text();
53
60
  throw new Error(`Platform error (${response.status}): ${errorText}`);
54
61
  }
55
62
  const result = await response.json();
56
- console.log(chalk.green(`✅ Snapshot "${result.snapshot}" created successfully!`));
63
+ console.log(chalk.green(`✅ Snapshot "${result.snapshot}" created successfully for client ${clientId}`));
57
64
  }
58
65
  catch (err) {
59
66
  console.error(chalk.red('Deploy failed:'), err.message || err);
@@ -0,0 +1 @@
1
+ export declare function generateClientId(): Promise<void>;
@@ -0,0 +1,57 @@
1
+ import { randomUUID } from 'crypto';
2
+ import inquirer from 'inquirer';
3
+ import chalk from 'chalk';
4
+ import { saveClientAuth } from '../utils/client-auth';
5
+ export async function generateClientId() {
6
+ const platformUrl = process.env.CASTARI_PLATFORM_URL || 'http://localhost:3000';
7
+ const mintOnline = async () => {
8
+ try {
9
+ const res = await fetch(`${platformUrl}/client-ids`, { method: 'POST' });
10
+ if (!res.ok) {
11
+ const text = await res.text();
12
+ throw new Error(text || `status ${res.status}`);
13
+ }
14
+ const data = (await res.json());
15
+ if (!data.clientId)
16
+ throw new Error('No clientId returned');
17
+ await saveClientAuth({
18
+ clientId: data.clientId,
19
+ apiKey: data.apiKey,
20
+ source: 'online',
21
+ platformUrl,
22
+ });
23
+ console.log(chalk.green(`✅ Client ID minted: ${data.clientId}`));
24
+ if (data.apiKey) {
25
+ console.log(chalk.green(`✅ API Key issued`));
26
+ }
27
+ console.log(chalk.white(`Saved to ~/.castari/client.json`));
28
+ return data.clientId;
29
+ }
30
+ catch (err) {
31
+ console.error(chalk.yellow(`⚠️ Failed to mint via platform: ${err?.message || err}`));
32
+ return null;
33
+ }
34
+ };
35
+ const onlineId = await mintOnline();
36
+ if (onlineId)
37
+ return;
38
+ const { confirmOffline } = await inquirer.prompt([
39
+ {
40
+ type: 'confirm',
41
+ name: 'confirmOffline',
42
+ message: 'Generate an offline client ID instead?',
43
+ default: false,
44
+ },
45
+ ]);
46
+ if (!confirmOffline) {
47
+ console.error(chalk.red('Aborted. No client ID generated.'));
48
+ process.exit(1);
49
+ }
50
+ const offlineId = randomUUID();
51
+ await saveClientAuth({
52
+ clientId: offlineId,
53
+ source: 'offline',
54
+ });
55
+ console.log(chalk.green(`✅ Offline client ID generated: ${offlineId}`));
56
+ console.log(chalk.white(`Saved to ~/.castari/client.json`));
57
+ }
@@ -25,8 +25,8 @@ export async function init(options = {}) {
25
25
  start: 'castari start',
26
26
  },
27
27
  dependencies: {
28
- '@castari/sdk': '^0.0.4',
29
- '@anthropic-ai/claude-agent-sdk': '^0.1.44',
28
+ '@castari/sdk': '^0.0.6',
29
+ '@anthropic-ai/claude-agent-sdk': '^0.1.50',
30
30
  },
31
31
  castari: {
32
32
  volume: 'default-data',
@@ -41,8 +41,7 @@ export async function init(options = {}) {
41
41
  skipLibCheck: true,
42
42
  },
43
43
  };
44
- const agentTs = `import { serve } from '@castari/sdk/server'
45
- import { tool } from '@castari/sdk'
44
+ const agentTs = `import { serve, tool } from '@castari/sdk'
46
45
 
47
46
  // Define your agent tools and logic here
48
47
  const myTool = tool({
@@ -65,7 +64,7 @@ serve({
65
64
  systemPrompt: 'You are a helpful Castari agent.',
66
65
  })
67
66
  `;
68
- const envExample = `ANTHROPIC_API_KEY=sk-ant-...\n# CASTARI_PLATFORM_URL=https://api.castari.com\n`;
67
+ const envExample = `ANTHROPIC_API_KEY=sk-ant-...\nCASTARI_CLIENT_ID=your-client-id\nCASTARI_API_KEY=castari_your-api-key\n# CASTARI_PLATFORM_URL=https://api.castari.com\n`;
69
68
  await writeFile('package.json', JSON.stringify(packageJson, null, 2));
70
69
  await writeFile('tsconfig.json', JSON.stringify(tsConfig, null, 2));
71
70
  await writeFile('.env.example', envExample);
@@ -93,7 +92,7 @@ async function initDemo() {
93
92
  start: 'bun run src/agent.ts',
94
93
  },
95
94
  dependencies: {
96
- '@castari/sdk': '^0.0.4',
95
+ '@castari/sdk': '^0.0.6',
97
96
  '@anthropic-ai/claude-agent-sdk': '^0.1.50',
98
97
  },
99
98
  castari: {
@@ -111,7 +110,7 @@ async function initDemo() {
111
110
  },
112
111
  include: ['src'],
113
112
  });
114
- await writeFile(join(agentDir, '.env.example'), 'ANTHROPIC_API_KEY=sk-ant-...\n# CASTARI_PLATFORM_URL=http://localhost:3000\n');
113
+ await writeFile(join(agentDir, '.env.example'), 'ANTHROPIC_API_KEY=sk-ant-...\nCASTARI_CLIENT_ID=your-client-id\nCASTARI_API_KEY=castari_your-api-key\n# CASTARI_PLATFORM_URL=http://localhost:3000\n');
115
114
  await writeFile(join(agentDir, 'src', 'agent.ts'), `import { serve } from '@castari/sdk'
116
115
 
117
116
  serve({
@@ -126,6 +125,7 @@ serve({
126
125
  // Web scaffold
127
126
  await mkdir(join(webDir, 'app', 'api', 'chat'), { recursive: true });
128
127
  await mkdir(join(webDir, 'lib'), { recursive: true });
128
+ await mkdir(join(webDir, 'public'), { recursive: true });
129
129
  await writeJson(join(webDir, 'package.json'), {
130
130
  name: 'castari-demo-web',
131
131
  version: '0.1.0',
@@ -136,7 +136,7 @@ serve({
136
136
  start: 'next start',
137
137
  },
138
138
  dependencies: {
139
- '@castari/sdk': '^0.0.4',
139
+ '@castari/sdk': '^0.0.6',
140
140
  next: '14.2.3',
141
141
  react: '18.3.1',
142
142
  'react-dom': '18.3.1',
@@ -187,11 +187,16 @@ const nextConfig = {
187
187
 
188
188
  export default nextConfig
189
189
  `);
190
- 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, '.env.example'), 'ANTHROPIC_API_KEY=sk-ant-...\nCASTARI_CLIENT_ID=your-client-id\nCASTARI_API_KEY=castari_your-api-key\n# CASTARI_PLATFORM_URL=http://localhost:3000\n# CASTARI_DEBUG=false\n');
191
191
  await writeFile(join(webDir, 'app', 'globals.css'), `:root {
192
- color-scheme: light;
193
- background: #f5f5f5;
194
- font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
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;
195
200
  }
196
201
 
197
202
  * {
@@ -200,9 +205,10 @@ export default nextConfig
200
205
 
201
206
  body {
202
207
  margin: 0;
203
- background: radial-gradient(circle at 20% 20%, #f7f0ff 0, #f5f5f5 35%), radial-gradient(circle at 80% 0%, #e0f4ff 0, #f5f5f5 30%);
204
- color: #0f172a;
208
+ background-color: var(--bg);
209
+ color: var(--text);
205
210
  min-height: 100vh;
211
+ font-weight: 300;
206
212
  }
207
213
 
208
214
  a {
@@ -210,139 +216,261 @@ a {
210
216
  text-decoration: none;
211
217
  }
212
218
 
213
- .chat-container {
214
- max-width: 760px;
215
- margin: 0 auto;
216
- padding: 32px 16px 64px;
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%);
217
231
  display: flex;
218
232
  flex-direction: column;
219
- gap: 16px;
233
+ gap: 28px;
234
+ z-index: 1;
220
235
  }
221
236
 
222
- .card {
223
- background: rgba(255, 255, 255, 0.85);
224
- backdrop-filter: blur(6px);
225
- border: 1px solid rgba(15, 23, 42, 0.06);
226
- border-radius: 16px;
227
- box-shadow: 0 20px 60px rgba(15, 23, 42, 0.12);
228
- padding: 24px;
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);
229
245
  }
230
246
 
231
- .heading {
247
+ .brand {
232
248
  display: flex;
233
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;
234
276
  gap: 12px;
235
- font-size: 22px;
236
- font-weight: 700;
277
+ padding: 40px 0 20px;
278
+ align-items: center;
279
+ text-align: center;
237
280
  }
238
281
 
239
- .badge {
240
- font-size: 12px;
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;
241
287
  font-weight: 600;
242
- color: #0ea5e9;
243
- background: rgba(14, 165, 233, 0.1);
244
- padding: 6px 10px;
245
- border-radius: 999px;
246
- border: 1px solid rgba(14, 165, 233, 0.2);
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;
247
336
  }
248
337
 
249
338
  .messages {
250
339
  display: flex;
251
340
  flex-direction: column;
252
341
  gap: 12px;
342
+ background: #16171d;
343
+ border: 1px solid var(--border);
344
+ border-radius: 16px;
345
+ padding: 14px;
346
+ min-height: 300px;
253
347
  }
254
348
 
255
349
  .bubble {
256
350
  padding: 14px 16px;
257
351
  border-radius: 14px;
258
- max-width: 90%;
259
352
  line-height: 1.5;
260
353
  font-size: 15px;
354
+ max-width: 85%;
261
355
  }
262
356
 
263
357
  .bubble.assistant {
264
- background: #0f172a;
265
- color: white;
358
+ background: #1c1d24;
359
+ color: var(--text);
266
360
  align-self: flex-start;
267
- border-bottom-left-radius: 4px;
361
+ border: 1px solid var(--border);
268
362
  }
269
363
 
270
364
  .bubble.user {
271
- background: white;
272
- color: #0f172a;
273
- border: 1px solid rgba(15, 23, 42, 0.06);
365
+ background: #ffffff;
366
+ color: #000000;
274
367
  align-self: flex-end;
275
- border-bottom-right-radius: 4px;
276
368
  }
277
369
 
278
370
  .form {
279
371
  display: flex;
280
372
  gap: 12px;
281
- margin-top: 8px;
373
+ align-items: center;
374
+ margin-top: 16px;
375
+ background: #16171d;
376
+ border: 1px solid var(--border);
377
+ border-radius: 14px;
378
+ padding: 10px;
282
379
  }
283
380
 
284
381
  .input {
285
382
  flex: 1;
286
- padding: 14px 16px;
383
+ padding: 12px 14px;
287
384
  border-radius: 12px;
288
- border: 1px solid rgba(15, 23, 42, 0.08);
385
+ border: 1px solid transparent;
289
386
  font-size: 15px;
290
387
  outline: none;
291
- transition: border-color 0.15s ease;
388
+ background: transparent;
389
+ color: var(--text);
292
390
  }
293
391
 
294
- .input:focus {
295
- border-color: #0ea5e9;
296
- box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.12);
392
+ .input::placeholder {
393
+ color: #666666;
297
394
  }
298
395
 
299
396
  .button {
300
- background: linear-gradient(135deg, #0ea5e9, #6366f1);
301
- color: white;
397
+ background: #ffffff;
398
+ color: #000000;
302
399
  border: none;
303
400
  border-radius: 12px;
304
- padding: 12px 18px;
305
- font-weight: 700;
401
+ padding: 12px 24px;
402
+ font-weight: 600;
306
403
  cursor: pointer;
307
- box-shadow: 0 12px 25px rgba(99, 102, 241, 0.35);
308
- min-width: 110px;
309
- transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.12s ease;
404
+ transition: opacity 0.2s;
310
405
  }
311
406
 
312
407
  .button:disabled {
313
- opacity: 0.6;
408
+ opacity: 0.5;
314
409
  cursor: not-allowed;
315
- box-shadow: none;
316
410
  }
317
411
 
318
- .button:not(:disabled):hover {
319
- transform: translateY(-1px);
320
- box-shadow: 0 16px 30px rgba(99, 102, 241, 0.45);
412
+ .button:hover:not(:disabled) {
413
+ opacity: 0.9;
321
414
  }
322
415
 
323
416
  .status {
324
417
  font-size: 13px;
325
- color: #475569;
326
- margin-top: 6px;
327
- }
328
-
329
- .pill-row {
330
- display: flex;
331
- gap: 8px;
332
- flex-wrap: wrap;
418
+ color: var(--muted);
419
+ margin-top: 10px;
420
+ text-align: center;
333
421
  }
334
422
 
335
423
  .pill {
336
- padding: 6px 10px;
424
+ display: inline-flex;
425
+ align-items: center;
426
+ gap: 8px;
427
+ padding: 6px 12px;
337
428
  border-radius: 999px;
338
- background: rgba(15, 23, 42, 0.06);
429
+ border: 1px solid var(--border);
430
+ background: transparent;
431
+ color: var(--muted);
339
432
  font-size: 12px;
340
- color: #0f172a;
341
- border: 1px solid rgba(15, 23, 42, 0.08);
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
+ }
342
457
  }
343
458
  `);
344
459
  await writeFile(join(webDir, 'app', 'layout.tsx'), `import './globals.css'
345
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
+ })
346
474
 
347
475
  export default function RootLayout({ children }: { children: ReactNode }) {
348
476
  return (
@@ -350,7 +478,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
350
478
  <head>
351
479
  <title>Castari Demo Chat</title>
352
480
  </head>
353
- <body>{children}</body>
481
+ <body className={\`\${inter.variable} \${orbitron.variable}\`}>{children}</body>
354
482
  </html>
355
483
  )
356
484
  }
@@ -358,6 +486,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
358
486
  await writeFile(join(webDir, 'app', 'page.tsx'), `"use client"
359
487
 
360
488
  import { FormEvent, useState } from 'react'
489
+ import Image from 'next/image'
361
490
 
362
491
  type ChatMessage = {
363
492
  role: 'assistant' | 'user'
@@ -437,43 +566,71 @@ export default function Home() {
437
566
  }
438
567
 
439
568
  return (
440
- <main className="chat-container">
441
- <div className="card">
442
- <div className="heading">
443
- <span>Castari Demo Chat</span>
444
- <span className="badge">Single sandbox</span>
445
- </div>
446
- <p style={{ margin: '8px 0 16px', color: '#475569' }}>
447
- This UI connects to a Castari agent running in a Daytona sandbox. Messages reuse the same
448
- sandbox (via labels) so the conversation stays warm while the sandbox is alive.
449
- </p>
450
-
451
- <div className="pill-row" style={{ marginBottom: 12 }}>
452
- <span className="pill">Snapshot: castari-demo-agent</span>
453
- <span className="pill">Volume: castari-demo-workspace</span>
454
- <span className="pill">Labels: app=castari-demo, env=local</span>
455
- </div>
456
-
457
- <div className="messages">
458
- {messages.map((m, idx) => (
459
- <div key={idx} className={\`bubble \${m.role}\`}>
460
- {m.content}
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>
461
582
  </div>
462
- ))}
463
- </div>
464
-
465
- <form className="form" onSubmit={sendMessage}>
466
- <input
467
- className="input"
468
- placeholder="Ask the Castari agent anything..."
469
- value={input}
470
- onChange={e => setInput(e.target.value)}
471
- />
472
- <button className="button" type="submit" disabled={sending}>
473
- {sending ? 'Talking…' : 'Send'}
474
- </button>
475
- </form>
476
- {status && <div className="status">{status}</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
+ <p className="panel-copy">
602
+ This agent runs in a live Daytona sandbox.
603
+ </p>
604
+ </div>
605
+ <span className="pill pill-accent">Live</span>
606
+ </div>
607
+
608
+ <div className="pill-row">
609
+ <span className="pill">Snapshot: castari-demo-agent</span>
610
+ <span className="pill">Volume: castari-demo-workspace</span>
611
+ </div>
612
+
613
+ <div className="messages">
614
+ {messages.map((m, idx) => (
615
+ <div key={idx} className={\`bubble \${m.role}\`}>
616
+ {m.content}
617
+ </div>
618
+ ))}
619
+ </div>
620
+
621
+ <form className="form" onSubmit={sendMessage}>
622
+ <input
623
+ className="input"
624
+ placeholder="Ask the Castari agent anything..."
625
+ value={input}
626
+ onChange={e => setInput(e.target.value)}
627
+ />
628
+ <button className="button" type="submit" disabled={sending}>
629
+ {sending ? 'Talking…' : 'Send'}
630
+ </button>
631
+ </form>
632
+ {status && <div className="status">{status}</div>}
633
+ </section>
477
634
  </div>
478
635
  </main>
479
636
  )
@@ -495,9 +652,11 @@ async function createClient() {
495
652
 
496
653
  const client = new CastariClient({
497
654
  snapshot: SNAPSHOT,
498
- volume: VOLUME,
655
+ // volume: VOLUME, // opt-in; omit to run ephemeral unless provided
499
656
  labels: LABELS,
500
657
  platformUrl: process.env.CASTARI_PLATFORM_URL,
658
+ clientId: process.env.CASTARI_CLIENT_ID,
659
+ platformApiKey: process.env.CASTARI_API_KEY,
501
660
  anthropicApiKey,
502
661
  debug: process.env.CASTARI_DEBUG === 'true',
503
662
  })
@@ -651,6 +810,35 @@ export async function POST(request: Request) {
651
810
  },
652
811
  })
653
812
  }
813
+ `);
814
+ // Logo SVG
815
+ 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">
816
+ <path d="M0 32L31.619 0L63.2381 32L31.619 64L0 32Z" fill="#A44A0C"/>
817
+ <path d="M31.527 31.8743H6.87042L24.3595 24.3417L31.527 7.24737V31.8743Z" fill="white"/>
818
+ <path d="M31.527 31.8743H6.87042L24.3595 24.3417L31.527 31.8743Z" fill="url(#paint0_linear_173_2089)"/>
819
+ <path d="M31.5268 31.872L31.5192 6.91829L24.0817 24.6204L31.5268 31.872Z" fill="url(#paint1_linear_173_2089)"/>
820
+ <path d="M31.7116 32.128L31.7192 57.0817L38.9973 39.3796L31.7116 32.128Z" fill="url(#paint2_linear_173_2089)"/>
821
+ <path d="M31.7117 32.1258H56.3683L38.8792 39.6584L31.7117 56.7527V32.1258Z" fill="white"/>
822
+ <path d="M31.7117 32.1258H56.3683L38.8792 39.6584L31.7117 32.1258Z" fill="url(#paint3_linear_173_2089)"/>
823
+ <defs>
824
+ <linearGradient id="paint0_linear_173_2089" x1="53.6475" y1="32.6825" x2="36.7362" y2="37.9872" gradientUnits="userSpaceOnUse">
825
+ <stop stop-color="#F8F8F8"/>
826
+ <stop offset="1" stop-color="#EACAB4"/>
827
+ </linearGradient>
828
+ <linearGradient id="paint1_linear_173_2089" x1="6.97479" y1="59.2675" x2="54.1923" y2="54.7965" gradientUnits="userSpaceOnUse">
829
+ <stop stop-color="white"/>
830
+ <stop offset="1" stop-color="#EDEDED"/>
831
+ </linearGradient>
832
+ <linearGradient id="paint2_linear_173_2089" x1="6.97479" y1="59.2675" x2="54.1923" y2="54.7965" gradientUnits="userSpaceOnUse">
833
+ <stop stop-color="white"/>
834
+ <stop offset="1" stop-color="#EDEDED"/>
835
+ </linearGradient>
836
+ <linearGradient id="paint3_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
+ </defs>
841
+ </svg>
654
842
  `);
655
843
  // Demo README
656
844
  await writeFile(join(demoRoot, 'README.md'), `# Castari Demo (Web + Agent)
@@ -665,12 +853,20 @@ Structure:
665
853
  - Bun (for the Castari CLI and scripts)
666
854
  - Node 18+ (for the Next.js app)
667
855
  - \`ANTHROPIC_API_KEY\`
856
+ - Castari credentials (\`CASTARI_CLIENT_ID\` and \`CASTARI_API_KEY\`)
668
857
  - Castari Platform running locally or reachable via \`CASTARI_PLATFORM_URL\`
669
858
 
859
+ ## Getting Castari Credentials
860
+ Generate your clientId and apiKey by running:
861
+ \`\`\`bash
862
+ castari generate-client-id
863
+ \`\`\`
864
+ Or via the Castari Platform Console if available.
865
+
670
866
  ## 1) Prepare and deploy the agent
671
867
  \`\`\`bash
672
868
  cd castari_demo/agent
673
- cp .env.example .env # add ANTHROPIC_API_KEY (and CASTARI_PLATFORM_URL if self-hosted)
869
+ cp .env.example .env # add ANTHROPIC_API_KEY, CASTARI_CLIENT_ID, CASTARI_API_KEY
674
870
  bun install # pulls @castari/sdk from npm
675
871
  castari deploy # builds snapshot castari-demo-agent (CLI must be installed)
676
872
  # Optional: bun run src/agent.ts # run locally without the CLI
@@ -679,7 +875,7 @@ castari deploy # builds snapshot castari-demo-agent (CLI must be installe
679
875
  ## 2) Run the web app
680
876
  \`\`\`bash
681
877
  cd ../web
682
- cp .env.example .env # add ANTHROPIC_API_KEY and CASTARI_PLATFORM_URL if needed
878
+ cp .env.example .env # add ANTHROPIC_API_KEY, CASTARI_CLIENT_ID, CASTARI_API_KEY
683
879
  npm install # or bun install
684
880
  npm run dev # opens http://localhost:3000
685
881
  \`\`\`
@@ -2,6 +2,7 @@ import { CastariClient } from '@castari/sdk/client';
2
2
  import { readFile } from 'fs/promises';
3
3
  import chalk from 'chalk';
4
4
  import dotenv from 'dotenv';
5
+ import { getClientAuthOrExit } from '../utils/client-auth';
5
6
  export async function start(options) {
6
7
  // Load .env from current directory
7
8
  dotenv.config();
@@ -18,6 +19,7 @@ export async function start(options) {
18
19
  catch (e) {
19
20
  // Ignore if package.json missing
20
21
  }
22
+ const { clientId, apiKey } = await getClientAuthOrExit();
21
23
  if (!snapshotName) {
22
24
  console.error(chalk.red('Error: Snapshot name is required.'));
23
25
  process.exit(1);
@@ -26,14 +28,21 @@ export async function start(options) {
26
28
  if (volumeName) {
27
29
  console.log(chalk.blue(`📦 Using volume: ${volumeName}`));
28
30
  }
31
+ else {
32
+ console.log(chalk.blue(`📦 No volume specified (sandbox will be ephemeral)`));
33
+ }
29
34
  const platformUrl = process.env.CASTARI_PLATFORM_URL;
30
- const client = new CastariClient({
35
+ const clientOptions = {
31
36
  snapshot: snapshotName,
32
- volume: volumeName,
37
+ ...(volumeName ? { volume: volumeName } : {}),
33
38
  platformUrl,
34
39
  debug: true, // Enable debug logs for CLI
35
- anthropicApiKey: process.env.ANTHROPIC_API_KEY
36
- });
40
+ anthropicApiKey: process.env.ANTHROPIC_API_KEY,
41
+ clientId,
42
+ };
43
+ if (apiKey)
44
+ clientOptions.platformApiKey = apiKey;
45
+ const client = new CastariClient(clientOptions);
37
46
  try {
38
47
  await client.start();
39
48
  console.log(chalk.green('✅ Agent started!'));
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { init } from './commands/init';
4
4
  import { deploy } from './commands/deploy';
5
5
  import { start } from './commands/start';
6
6
  import { dev } from './commands/dev';
7
+ import { generateClientId } from './commands/generate-client-id';
7
8
  const cli = cac('castari');
8
9
  cli
9
10
  .command('init', 'Initialize a new Castari agent project')
@@ -22,6 +23,9 @@ cli
22
23
  cli
23
24
  .command('dev', 'Run the agent locally for development')
24
25
  .action(dev);
26
+ cli
27
+ .command('generate-client-id', 'Generate a new Castari Client ID and save it locally')
28
+ .action(generateClientId);
25
29
  cli.help();
26
- cli.version('0.0.4');
30
+ cli.version('0.0.7');
27
31
  cli.parse();
@@ -0,0 +1,11 @@
1
+ export type ClientAuth = {
2
+ clientId: string;
3
+ apiKey?: string;
4
+ };
5
+ export declare function getClientAuthOrExit(): Promise<ClientAuth>;
6
+ export declare function saveClientAuth(data: {
7
+ clientId: string;
8
+ apiKey?: string;
9
+ source?: 'online' | 'offline';
10
+ platformUrl?: string;
11
+ }): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { getClientIdOrExit, loadClientId, saveClientId } from './client-id';
2
+ export async function getClientAuthOrExit() {
3
+ const clientId = await getClientIdOrExit();
4
+ const stored = await loadClientId();
5
+ const apiKey = process.env.CASTARI_API_KEY ||
6
+ stored?.apiKey;
7
+ if (!apiKey) {
8
+ // We allow missing apiKey for now (platform may allow unauthenticated in dev)
9
+ return { clientId };
10
+ }
11
+ return { clientId, apiKey };
12
+ }
13
+ export async function saveClientAuth(data) {
14
+ await saveClientId({
15
+ clientId: data.clientId,
16
+ apiKey: data.apiKey,
17
+ source: data.source,
18
+ createdAt: new Date().toISOString(),
19
+ platformUrl: data.platformUrl,
20
+ });
21
+ }
@@ -0,0 +1,10 @@
1
+ export type StoredClientId = {
2
+ clientId: string;
3
+ source?: 'online' | 'offline';
4
+ createdAt?: string;
5
+ platformUrl?: string;
6
+ apiKey?: string;
7
+ };
8
+ export declare function loadClientId(): Promise<StoredClientId | null>;
9
+ export declare function saveClientId(data: StoredClientId): Promise<void>;
10
+ export declare function getClientIdOrExit(explicit?: string): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { homedir } from 'os';
2
+ import { join } from 'path';
3
+ import { mkdir, readFile, writeFile } from 'fs/promises';
4
+ import chalk from 'chalk';
5
+ const CONFIG_DIR = join(homedir(), '.castari');
6
+ const CONFIG_PATH = join(CONFIG_DIR, 'client.json');
7
+ export async function loadClientId() {
8
+ try {
9
+ const raw = await readFile(CONFIG_PATH, 'utf-8');
10
+ const parsed = JSON.parse(raw);
11
+ if (parsed.clientId && typeof parsed.clientId === 'string') {
12
+ return parsed;
13
+ }
14
+ return null;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export async function saveClientId(data) {
21
+ await mkdir(CONFIG_DIR, { recursive: true });
22
+ await writeFile(CONFIG_PATH, JSON.stringify(data, null, 2));
23
+ }
24
+ export async function getClientIdOrExit(explicit) {
25
+ const envId = process.env.CASTARI_CLIENT_ID;
26
+ if (explicit)
27
+ return explicit;
28
+ if (envId)
29
+ return envId;
30
+ const stored = await loadClientId();
31
+ if (stored?.clientId)
32
+ return stored.clientId;
33
+ console.error(chalk.red('CASTARI_CLIENT_ID is required.'));
34
+ console.error(chalk.white('Run `castari generate-client-id` to create one, or set CASTARI_CLIENT_ID in your environment.'));
35
+ process.exit(1);
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@castari/cli",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "castari": "./dist/index.js"
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@anthropic-ai/claude-agent-sdk": "^0.1.44",
21
- "@castari/sdk": "^0.0.4",
21
+ "@castari/sdk": "^0.0.6",
22
22
  "adm-zip": "^0.5.16",
23
23
  "cac": "^6.7.14",
24
24
  "chalk": "^5.3.0",
@@ -33,4 +33,4 @@
33
33
  "@types/node": "^20.10.0",
34
34
  "typescript": "^5.6.3"
35
35
  }
36
- }
36
+ }