create-byoky-app 0.4.10

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,150 @@
1
+ import express from 'express';
2
+ import { createServer } from 'node:http';
3
+ import { WebSocketServer, WebSocket } from 'ws';
4
+ import { ByokyServer } from '@byoky/sdk/server';
5
+ import type { ByokyClient } from '@byoky/sdk/server';
6
+
7
+ const app = express();
8
+ app.use(express.json());
9
+
10
+ // CORS for local development
11
+ app.use((_req, res, next) => {
12
+ res.header('Access-Control-Allow-Origin', '*');
13
+ res.header('Access-Control-Allow-Headers', 'Content-Type');
14
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
15
+ next();
16
+ });
17
+
18
+ const server = createServer(app);
19
+ const wss = new WebSocketServer({ server, path: '/ws/relay' });
20
+ const byokyServer = new ByokyServer();
21
+
22
+ // Track connected clients
23
+ const clients = new Map<string, ByokyClient>();
24
+
25
+ wss.on('connection', async (ws: WebSocket) => {
26
+ console.log('[relay] New WebSocket connection');
27
+
28
+ try {
29
+ const client = await byokyServer.handleConnection(ws);
30
+ clients.set(client.sessionId, client);
31
+ console.log(`[relay] Client connected: ${client.sessionId}`);
32
+
33
+ const availableProviders = Object.entries(client.providers)
34
+ .filter(([, p]) => p.available)
35
+ .map(([id]) => id);
36
+ console.log(`[relay] Available providers: ${availableProviders.join(', ') || 'none'}`);
37
+
38
+ client.onClose(() => {
39
+ clients.delete(client.sessionId);
40
+ console.log(`[relay] Client disconnected: ${client.sessionId}`);
41
+ });
42
+ } catch (err) {
43
+ console.error('[relay] Connection failed:', err instanceof Error ? err.message : err);
44
+ }
45
+ });
46
+
47
+ // Health check
48
+ app.get('/api/health', (_req, res) => {
49
+ res.json({
50
+ ok: true,
51
+ clients: clients.size,
52
+ });
53
+ });
54
+
55
+ // Example: Make an LLM call through the relay
56
+ app.post('/api/generate', async (req, res) => {
57
+ const { sessionId, prompt } = req.body as { sessionId?: string; prompt?: string };
58
+
59
+ if (!sessionId || !prompt) {
60
+ res.status(400).json({ error: 'Missing sessionId or prompt' });
61
+ return;
62
+ }
63
+
64
+ const client = clients.get(sessionId);
65
+ if (!client || !client.connected) {
66
+ res.status(404).json({ error: 'Client not connected' });
67
+ return;
68
+ }
69
+
70
+ // Find the first available provider
71
+ const providerId = Object.entries(client.providers)
72
+ .find(([, p]) => p.available)?.[0];
73
+
74
+ if (!providerId) {
75
+ res.status(400).json({ error: 'No providers available' });
76
+ return;
77
+ }
78
+
79
+ try {
80
+ const proxyFetch = client.createFetch(providerId);
81
+
82
+ let url: string;
83
+ let headers: Record<string, string>;
84
+ let body: string;
85
+
86
+ if (providerId === 'anthropic') {
87
+ url = 'https://api.anthropic.com/v1/messages';
88
+ headers = {
89
+ 'Content-Type': 'application/json',
90
+ 'x-api-key': 'byoky',
91
+ 'anthropic-version': '2023-06-01',
92
+ };
93
+ body = JSON.stringify({
94
+ model: 'claude-sonnet-4-20250514',
95
+ max_tokens: 256,
96
+ messages: [{ role: 'user', content: prompt }],
97
+ });
98
+ } else if (providerId === 'openai') {
99
+ url = 'https://api.openai.com/v1/chat/completions';
100
+ headers = {
101
+ 'Content-Type': 'application/json',
102
+ 'Authorization': 'Bearer byoky',
103
+ };
104
+ body = JSON.stringify({
105
+ model: 'gpt-4o-mini',
106
+ max_tokens: 256,
107
+ messages: [{ role: 'user', content: prompt }],
108
+ });
109
+ } else {
110
+ url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent';
111
+ headers = { 'Content-Type': 'application/json' };
112
+ body = JSON.stringify({
113
+ contents: [{ parts: [{ text: prompt }] }],
114
+ generationConfig: { maxOutputTokens: 256 },
115
+ });
116
+ }
117
+
118
+ const response = await proxyFetch(url, { method: 'POST', headers, body });
119
+
120
+ if (!response.ok) {
121
+ const text = await response.text();
122
+ res.status(response.status).json({ error: text.slice(0, 500) });
123
+ return;
124
+ }
125
+
126
+ const data = await response.json();
127
+
128
+ let content: string;
129
+ if (providerId === 'anthropic') {
130
+ content = data.content?.[0]?.text ?? '';
131
+ } else if (providerId === 'openai') {
132
+ content = data.choices?.[0]?.message?.content ?? '';
133
+ } else {
134
+ content = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
135
+ }
136
+
137
+ res.json({ provider: providerId, content });
138
+ } catch (err) {
139
+ res.status(500).json({
140
+ error: err instanceof Error ? err.message : 'Internal server error',
141
+ });
142
+ }
143
+ });
144
+
145
+ const PORT = parseInt(process.env.PORT ?? '3001', 10);
146
+
147
+ server.listen(PORT, () => {
148
+ console.log(`[server] Listening on http://localhost:${PORT}`);
149
+ console.log(`[server] WebSocket relay at ws://localhost:${PORT}/ws/relay`);
150
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
14
+ },
15
+ "include": ["server", "client"]
16
+ }
@@ -0,0 +1,28 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ AI chat app powered by [Byoky](https://byoky.com) — use your own API keys without exposing them to websites.
4
+
5
+ ## Setup
6
+
7
+ 1. Install the [Byoky wallet extension](https://byoky.com) and add your Anthropic API key
8
+ 2. Install dependencies and start the dev server:
9
+
10
+ ```bash
11
+ npm install
12
+ npm run dev
13
+ ```
14
+
15
+ 3. Open [http://localhost:3000](http://localhost:3000)
16
+ 4. Click **Connect Wallet** and approve the connection
17
+ 5. Start chatting
18
+
19
+ ## How it works
20
+
21
+ - The app uses `@byoky/sdk` to connect to your Byoky wallet
22
+ - API calls are proxied through the wallet extension — your API key never leaves the extension
23
+ - Streaming responses are powered by the Anthropic SDK with a custom `fetch` from Byoky
24
+
25
+ ## Learn more
26
+
27
+ - [Byoky Documentation](https://byoky.com/dev)
28
+ - [Next.js Documentation](https://nextjs.org/docs)
@@ -0,0 +1,5 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {};
4
+
5
+ export default nextConfig;
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "@anthropic-ai/sdk": "^0.39.0",
12
+ "@byoky/sdk": "^0.4.9",
13
+ "next": "^15.1.0",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/react": "^19.0.0",
19
+ "@types/react-dom": "^19.0.0",
20
+ "typescript": "^5.7.0"
21
+ }
22
+ }
@@ -0,0 +1,29 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ :root {
10
+ --bg: #0a0a0a;
11
+ --bg-secondary: #141414;
12
+ --bg-tertiary: #1e1e1e;
13
+ --text: #e0e0e0;
14
+ --text-muted: #888;
15
+ --accent: #6366f1;
16
+ --accent-hover: #818cf8;
17
+ --border: #2a2a2a;
18
+ --user-bg: #1a1a2e;
19
+ --assistant-bg: #141414;
20
+ }
21
+
22
+ html,
23
+ body {
24
+ height: 100%;
25
+ background: var(--bg);
26
+ color: var(--text);
27
+ font-family: var(--font-inter), system-ui, -apple-system, sans-serif;
28
+ -webkit-font-smoothing: antialiased;
29
+ }
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from 'next';
2
+ import { Inter } from 'next/font/google';
3
+ import './globals.css';
4
+
5
+ const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
6
+
7
+ export const metadata: Metadata = {
8
+ title: '{{PROJECT_NAME}}',
9
+ description: 'AI chat powered by Byoky',
10
+ };
11
+
12
+ export default function RootLayout({
13
+ children,
14
+ }: {
15
+ children: React.ReactNode;
16
+ }) {
17
+ return (
18
+ <html lang="en" className={inter.variable}>
19
+ <body>{children}</body>
20
+ </html>
21
+ );
22
+ }
@@ -0,0 +1,346 @@
1
+ 'use client';
2
+
3
+ import { useState, useRef, useEffect, useCallback } from 'react';
4
+ import Anthropic from '@anthropic-ai/sdk';
5
+ import { Byoky, isExtensionInstalled, getStoreUrl } from '@byoky/sdk';
6
+ import type { ByokySession } from '@byoky/sdk';
7
+
8
+ interface Message {
9
+ role: 'user' | 'assistant';
10
+ content: string;
11
+ }
12
+
13
+ const byoky = new Byoky();
14
+
15
+ export default function Home() {
16
+ const [session, setSession] = useState<ByokySession | null>(null);
17
+ const [messages, setMessages] = useState<Message[]>([]);
18
+ const [input, setInput] = useState('');
19
+ const [isStreaming, setIsStreaming] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+ const [isConnecting, setIsConnecting] = useState(false);
22
+ const messagesEndRef = useRef<HTMLDivElement>(null);
23
+
24
+ const scrollToBottom = useCallback(() => {
25
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
26
+ }, []);
27
+
28
+ useEffect(() => {
29
+ scrollToBottom();
30
+ }, [messages, scrollToBottom]);
31
+
32
+ const handleConnect = async () => {
33
+ setError(null);
34
+ setIsConnecting(true);
35
+ try {
36
+ const s = await byoky.connect({
37
+ providers: [{ id: 'anthropic', required: true }],
38
+ modal: true,
39
+ });
40
+ setSession(s);
41
+ s.onDisconnect(() => {
42
+ setSession(null);
43
+ setError('Wallet disconnected');
44
+ });
45
+ } catch (err) {
46
+ if (!isExtensionInstalled()) {
47
+ const url = getStoreUrl();
48
+ setError(
49
+ url
50
+ ? 'Byoky wallet not found. Install the extension to continue.'
51
+ : 'Byoky wallet not found.'
52
+ );
53
+ } else if (err instanceof Error) {
54
+ setError(err.message);
55
+ }
56
+ } finally {
57
+ setIsConnecting(false);
58
+ }
59
+ };
60
+
61
+ const handleSend = async () => {
62
+ if (!session || !input.trim() || isStreaming) return;
63
+
64
+ const userMessage: Message = { role: 'user', content: input.trim() };
65
+ const updatedMessages = [...messages, userMessage];
66
+ setMessages(updatedMessages);
67
+ setInput('');
68
+ setIsStreaming(true);
69
+ setError(null);
70
+
71
+ try {
72
+ const proxyFetch = session.createFetch('anthropic');
73
+ const client = new Anthropic({
74
+ apiKey: 'byoky',
75
+ fetch: proxyFetch,
76
+ dangerouslyAllowBrowser: true,
77
+ });
78
+
79
+ const assistantMessage: Message = { role: 'assistant', content: '' };
80
+ setMessages([...updatedMessages, assistantMessage]);
81
+
82
+ const stream = client.messages.stream({
83
+ model: 'claude-sonnet-4-20250514',
84
+ max_tokens: 1024,
85
+ messages: updatedMessages.map((m) => ({
86
+ role: m.role,
87
+ content: m.content,
88
+ })),
89
+ });
90
+
91
+ for await (const event of stream) {
92
+ if (
93
+ event.type === 'content_block_delta' &&
94
+ event.delta.type === 'text_delta'
95
+ ) {
96
+ assistantMessage.content += event.delta.text;
97
+ setMessages((prev) => [
98
+ ...prev.slice(0, -1),
99
+ { ...assistantMessage },
100
+ ]);
101
+ }
102
+ }
103
+ } catch (err) {
104
+ setError(err instanceof Error ? err.message : 'Failed to send message');
105
+ } finally {
106
+ setIsStreaming(false);
107
+ }
108
+ };
109
+
110
+ const handleKeyDown = (e: React.KeyboardEvent) => {
111
+ if (e.key === 'Enter' && !e.shiftKey) {
112
+ e.preventDefault();
113
+ handleSend();
114
+ }
115
+ };
116
+
117
+ if (!session) {
118
+ return (
119
+ <div style={styles.connectContainer}>
120
+ <h1 style={styles.title}>{{PROJECT_NAME}}</h1>
121
+ <p style={styles.subtitle}>AI chat powered by your own API keys</p>
122
+ <button
123
+ onClick={handleConnect}
124
+ disabled={isConnecting}
125
+ style={{
126
+ ...styles.connectButton,
127
+ opacity: isConnecting ? 0.6 : 1,
128
+ }}
129
+ >
130
+ {isConnecting ? 'Connecting...' : 'Connect Wallet'}
131
+ </button>
132
+ {error && <p style={styles.error}>{error}</p>}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ return (
138
+ <div style={styles.chatContainer}>
139
+ <header style={styles.header}>
140
+ <h1 style={styles.headerTitle}>{{PROJECT_NAME}}</h1>
141
+ <div style={styles.headerRight}>
142
+ <span style={styles.status}>Connected</span>
143
+ <button
144
+ onClick={() => {
145
+ session.disconnect();
146
+ setSession(null);
147
+ setMessages([]);
148
+ }}
149
+ style={styles.disconnectButton}
150
+ >
151
+ Disconnect
152
+ </button>
153
+ </div>
154
+ </header>
155
+
156
+ <div style={styles.messagesContainer}>
157
+ {messages.length === 0 && (
158
+ <div style={styles.emptyState}>
159
+ <p style={styles.emptyText}>Send a message to start chatting</p>
160
+ </div>
161
+ )}
162
+ {messages.map((msg, i) => (
163
+ <div
164
+ key={i}
165
+ style={{
166
+ ...styles.message,
167
+ backgroundColor:
168
+ msg.role === 'user'
169
+ ? 'var(--user-bg)'
170
+ : 'var(--assistant-bg)',
171
+ }}
172
+ >
173
+ <div style={styles.messageRole}>
174
+ {msg.role === 'user' ? 'You' : 'Assistant'}
175
+ </div>
176
+ <div style={styles.messageContent}>{msg.content}</div>
177
+ </div>
178
+ ))}
179
+ <div ref={messagesEndRef} />
180
+ </div>
181
+
182
+ <div style={styles.inputContainer}>
183
+ {error && <p style={styles.inlineError}>{error}</p>}
184
+ <div style={styles.inputRow}>
185
+ <textarea
186
+ value={input}
187
+ onChange={(e) => setInput(e.target.value)}
188
+ onKeyDown={handleKeyDown}
189
+ placeholder="Type a message..."
190
+ rows={1}
191
+ style={styles.textarea}
192
+ />
193
+ <button
194
+ onClick={handleSend}
195
+ disabled={isStreaming || !input.trim()}
196
+ style={{
197
+ ...styles.sendButton,
198
+ opacity: isStreaming || !input.trim() ? 0.5 : 1,
199
+ }}
200
+ >
201
+ {isStreaming ? '...' : 'Send'}
202
+ </button>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ );
207
+ }
208
+
209
+ const styles: Record<string, React.CSSProperties> = {
210
+ connectContainer: {
211
+ display: 'flex',
212
+ flexDirection: 'column',
213
+ alignItems: 'center',
214
+ justifyContent: 'center',
215
+ height: '100vh',
216
+ gap: '16px',
217
+ },
218
+ title: {
219
+ fontSize: '2rem',
220
+ fontWeight: 700,
221
+ },
222
+ subtitle: {
223
+ color: 'var(--text-muted)',
224
+ fontSize: '1.1rem',
225
+ marginBottom: '8px',
226
+ },
227
+ connectButton: {
228
+ background: 'var(--accent)',
229
+ color: '#fff',
230
+ border: 'none',
231
+ borderRadius: '8px',
232
+ padding: '12px 32px',
233
+ fontSize: '1rem',
234
+ fontWeight: 600,
235
+ cursor: 'pointer',
236
+ },
237
+ error: {
238
+ color: '#f87171',
239
+ fontSize: '0.9rem',
240
+ maxWidth: '400px',
241
+ textAlign: 'center' as const,
242
+ },
243
+ chatContainer: {
244
+ display: 'flex',
245
+ flexDirection: 'column',
246
+ height: '100vh',
247
+ maxWidth: '800px',
248
+ margin: '0 auto',
249
+ },
250
+ header: {
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ justifyContent: 'space-between',
254
+ padding: '16px 24px',
255
+ borderBottom: '1px solid var(--border)',
256
+ },
257
+ headerTitle: {
258
+ fontSize: '1.1rem',
259
+ fontWeight: 600,
260
+ },
261
+ headerRight: {
262
+ display: 'flex',
263
+ alignItems: 'center',
264
+ gap: '12px',
265
+ },
266
+ status: {
267
+ color: '#4ade80',
268
+ fontSize: '0.85rem',
269
+ },
270
+ disconnectButton: {
271
+ background: 'transparent',
272
+ color: 'var(--text-muted)',
273
+ border: '1px solid var(--border)',
274
+ borderRadius: '6px',
275
+ padding: '6px 12px',
276
+ fontSize: '0.85rem',
277
+ cursor: 'pointer',
278
+ },
279
+ messagesContainer: {
280
+ flex: 1,
281
+ overflow: 'auto',
282
+ padding: '24px',
283
+ },
284
+ emptyState: {
285
+ display: 'flex',
286
+ alignItems: 'center',
287
+ justifyContent: 'center',
288
+ height: '100%',
289
+ },
290
+ emptyText: {
291
+ color: 'var(--text-muted)',
292
+ },
293
+ message: {
294
+ padding: '16px 20px',
295
+ borderRadius: '8px',
296
+ marginBottom: '12px',
297
+ },
298
+ messageRole: {
299
+ fontSize: '0.8rem',
300
+ fontWeight: 600,
301
+ color: 'var(--text-muted)',
302
+ marginBottom: '6px',
303
+ textTransform: 'uppercase' as const,
304
+ letterSpacing: '0.05em',
305
+ },
306
+ messageContent: {
307
+ lineHeight: 1.6,
308
+ whiteSpace: 'pre-wrap' as const,
309
+ },
310
+ inputContainer: {
311
+ padding: '16px 24px',
312
+ borderTop: '1px solid var(--border)',
313
+ },
314
+ inlineError: {
315
+ color: '#f87171',
316
+ fontSize: '0.85rem',
317
+ marginBottom: '8px',
318
+ },
319
+ inputRow: {
320
+ display: 'flex',
321
+ gap: '8px',
322
+ },
323
+ textarea: {
324
+ flex: 1,
325
+ background: 'var(--bg-tertiary)',
326
+ color: 'var(--text)',
327
+ border: '1px solid var(--border)',
328
+ borderRadius: '8px',
329
+ padding: '12px 16px',
330
+ fontSize: '1rem',
331
+ resize: 'none' as const,
332
+ outline: 'none',
333
+ fontFamily: 'inherit',
334
+ },
335
+ sendButton: {
336
+ background: 'var(--accent)',
337
+ color: '#fff',
338
+ border: 'none',
339
+ borderRadius: '8px',
340
+ padding: '12px 24px',
341
+ fontSize: '1rem',
342
+ fontWeight: 600,
343
+ cursor: 'pointer',
344
+ whiteSpace: 'nowrap' as const,
345
+ },
346
+ };
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./src/*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -0,0 +1,29 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ Multi-provider demo powered by [Byoky](https://byoky.com) — use Anthropic, OpenAI, and Gemini through a single wallet.
4
+
5
+ ## Setup
6
+
7
+ 1. Install the [Byoky wallet extension](https://byoky.com) and add your API keys
8
+ 2. Install dependencies and start the dev server:
9
+
10
+ ```bash
11
+ npm install
12
+ npm run dev
13
+ ```
14
+
15
+ 3. Open [http://localhost:5173](http://localhost:5173)
16
+ 4. Click **Connect Wallet** and approve the connection
17
+ 5. Test each connected provider
18
+
19
+ ## How it works
20
+
21
+ - The app requests access to all three providers (all optional)
22
+ - Byoky shows which providers the user has configured
23
+ - Each provider gets its own `createFetch()` that proxies API calls through the wallet
24
+ - API keys never leave the extension
25
+
26
+ ## Learn more
27
+
28
+ - [Byoky Documentation](https://byoky.com/dev)
29
+ - [Vite Documentation](https://vite.dev)
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{PROJECT_NAME}}</title>
7
+ <link rel="stylesheet" href="/src/style.css" />
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>