nitrostack 1.0.1 → 1.0.3

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/cli/index.js +4 -1
  3. package/dist/cli/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/studio/README.md +140 -0
  6. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  7. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  8. package/src/studio/app/api/chat/route.ts +123 -0
  9. package/src/studio/app/api/health/checks/route.ts +42 -0
  10. package/src/studio/app/api/health/route.ts +13 -0
  11. package/src/studio/app/api/init/route.ts +85 -0
  12. package/src/studio/app/api/ping/route.ts +13 -0
  13. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  14. package/src/studio/app/api/prompts/route.ts +13 -0
  15. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  16. package/src/studio/app/api/resources/route.ts +13 -0
  17. package/src/studio/app/api/roots/route.ts +13 -0
  18. package/src/studio/app/api/sampling/route.ts +14 -0
  19. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  20. package/src/studio/app/api/tools/route.ts +23 -0
  21. package/src/studio/app/api/widget-examples/route.ts +44 -0
  22. package/src/studio/app/auth/callback/page.tsx +160 -0
  23. package/src/studio/app/auth/page.tsx +543 -0
  24. package/src/studio/app/chat/page.tsx +530 -0
  25. package/src/studio/app/chat/page.tsx.backup +390 -0
  26. package/src/studio/app/globals.css +410 -0
  27. package/src/studio/app/health/page.tsx +177 -0
  28. package/src/studio/app/layout.tsx +48 -0
  29. package/src/studio/app/page.tsx +337 -0
  30. package/src/studio/app/page.tsx.backup +346 -0
  31. package/src/studio/app/ping/page.tsx +204 -0
  32. package/src/studio/app/prompts/page.tsx +228 -0
  33. package/src/studio/app/resources/page.tsx +313 -0
  34. package/src/studio/components/EnlargeModal.tsx +116 -0
  35. package/src/studio/components/Sidebar.tsx +133 -0
  36. package/src/studio/components/ToolCard.tsx +108 -0
  37. package/src/studio/components/WidgetRenderer.tsx +99 -0
  38. package/src/studio/lib/api.ts +207 -0
  39. package/src/studio/lib/llm-service.ts +361 -0
  40. package/src/studio/lib/mcp-client.ts +168 -0
  41. package/src/studio/lib/store.ts +192 -0
  42. package/src/studio/lib/theme-provider.tsx +50 -0
  43. package/src/studio/lib/types.ts +107 -0
  44. package/src/studio/lib/widget-loader.ts +90 -0
  45. package/src/studio/middleware.ts +27 -0
  46. package/src/studio/next.config.js +16 -0
  47. package/src/studio/package-lock.json +2696 -0
  48. package/src/studio/package.json +34 -0
  49. package/src/studio/postcss.config.mjs +10 -0
  50. package/src/studio/tailwind.config.ts +67 -0
  51. package/src/studio/tsconfig.json +42 -0
@@ -0,0 +1,204 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useStudioStore } from '@/lib/store';
5
+ import { api } from '@/lib/api';
6
+ import { Wifi, Activity, Clock, TrendingUp, Radio } from 'lucide-react';
7
+
8
+ export default function PingPage() {
9
+ const { pingHistory, addPingResult } = useStudioStore();
10
+ const [pinging, setPinging] = useState(false);
11
+ const [lastLatency, setLastLatency] = useState<number | null>(null);
12
+
13
+ const handlePing = async () => {
14
+ setPinging(true);
15
+ const startTime = Date.now();
16
+
17
+ try {
18
+ await api.ping();
19
+ const latency = Date.now() - startTime;
20
+ setLastLatency(latency);
21
+ addPingResult({ time: new Date(), latency });
22
+ } catch (error) {
23
+ console.error('Ping failed:', error);
24
+ } finally {
25
+ setPinging(false);
26
+ }
27
+ };
28
+
29
+ const averageLatency =
30
+ pingHistory.length > 0
31
+ ? Math.round(pingHistory.reduce((sum, p) => sum + p.latency, 0) / pingHistory.length)
32
+ : null;
33
+
34
+ const minLatency =
35
+ pingHistory.length > 0
36
+ ? Math.min(...pingHistory.map((p) => p.latency))
37
+ : null;
38
+
39
+ const maxLatency =
40
+ pingHistory.length > 0
41
+ ? Math.max(...pingHistory.map((p) => p.latency))
42
+ : null;
43
+
44
+ const getLatencyColor = (latency: number) => {
45
+ if (latency < 100) return 'text-emerald-500';
46
+ if (latency < 500) return 'text-amber-500';
47
+ return 'text-rose-500';
48
+ };
49
+
50
+ const getLatencyBg = (latency: number) => {
51
+ if (latency < 100) return 'bg-emerald-500/10 border-emerald-500/20';
52
+ if (latency < 500) return 'bg-amber-500/10 border-amber-500/20';
53
+ return 'bg-rose-500/10 border-rose-500/20';
54
+ };
55
+
56
+ const getLatencyDotColor = (latency: number) => {
57
+ if (latency < 100) return 'bg-emerald-500';
58
+ if (latency < 500) return 'bg-amber-500';
59
+ return 'bg-rose-500';
60
+ };
61
+
62
+ return (
63
+ <div className="min-h-screen bg-background p-8">
64
+ {/* Header */}
65
+ <div className="mb-8">
66
+ <div className="flex items-center gap-3 mb-4">
67
+ <div className="w-12 h-12 rounded-lg bg-gradient-to-br from-cyan-500 to-blue-500 flex items-center justify-center">
68
+ <Wifi className="w-6 h-6 text-white" />
69
+ </div>
70
+ <div>
71
+ <h1 className="text-3xl font-bold text-foreground">Ping</h1>
72
+ <p className="text-muted-foreground mt-1">Test server connectivity and latency</p>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ {/* Main Ping Card */}
78
+ <div className="card card-hover p-8 mb-8 text-center bg-gradient-to-br from-card via-card to-muted/20">
79
+ <button
80
+ onClick={handlePing}
81
+ disabled={pinging}
82
+ className="btn btn-primary btn-lg mx-auto px-12 py-4 text-lg gap-3 shadow-lg hover:shadow-xl transition-all"
83
+ >
84
+ {pinging ? (
85
+ <>
86
+ <Radio className="w-6 h-6 animate-pulse" />
87
+ Pinging...
88
+ </>
89
+ ) : (
90
+ <>
91
+ <Wifi className="w-6 h-6" />
92
+ Send Ping
93
+ </>
94
+ )}
95
+ </button>
96
+
97
+ {lastLatency !== null && (
98
+ <div className="mt-8 animate-fade-in">
99
+ <div className={`inline-block px-8 py-4 rounded-2xl border ${getLatencyBg(lastLatency)}`}>
100
+ <div className={`text-6xl font-bold ${getLatencyColor(lastLatency)}`}>
101
+ {lastLatency}ms
102
+ </div>
103
+ <div className="text-muted-foreground mt-2 flex items-center justify-center gap-2">
104
+ <Clock className="w-4 h-4" />
105
+ Last ping latency
106
+ </div>
107
+ </div>
108
+ </div>
109
+ )}
110
+ </div>
111
+
112
+ {/* Statistics Grid */}
113
+ {pingHistory.length > 0 && (
114
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
115
+ {/* Average */}
116
+ <div className="card card-hover p-6">
117
+ <div className="flex items-center gap-3 mb-3">
118
+ <div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
119
+ <TrendingUp className="w-5 h-5 text-blue-500" />
120
+ </div>
121
+ <h3 className="font-semibold text-foreground">Average</h3>
122
+ </div>
123
+ <div className="text-3xl font-bold text-foreground">{averageLatency}ms</div>
124
+ <p className="text-sm text-muted-foreground mt-1">{pingHistory.length} total pings</p>
125
+ </div>
126
+
127
+ {/* Min */}
128
+ <div className="card card-hover p-6">
129
+ <div className="flex items-center gap-3 mb-3">
130
+ <div className="w-10 h-10 rounded-lg bg-emerald-500/10 flex items-center justify-center">
131
+ <Activity className="w-5 h-5 text-emerald-500" />
132
+ </div>
133
+ <h3 className="font-semibold text-foreground">Fastest</h3>
134
+ </div>
135
+ <div className="text-3xl font-bold text-emerald-500">{minLatency}ms</div>
136
+ <p className="text-sm text-muted-foreground mt-1">Best response time</p>
137
+ </div>
138
+
139
+ {/* Max */}
140
+ <div className="card card-hover p-6">
141
+ <div className="flex items-center gap-3 mb-3">
142
+ <div className="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center">
143
+ <Clock className="w-5 h-5 text-amber-500" />
144
+ </div>
145
+ <h3 className="font-semibold text-foreground">Slowest</h3>
146
+ </div>
147
+ <div className="text-3xl font-bold text-amber-500">{maxLatency}ms</div>
148
+ <p className="text-sm text-muted-foreground mt-1">Worst response time</p>
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ {/* Ping History */}
154
+ {pingHistory.length > 0 && (
155
+ <div>
156
+ <h2 className="text-2xl font-semibold text-foreground mb-6 flex items-center gap-2">
157
+ <Activity className="w-6 h-6 text-primary" />
158
+ Recent Pings
159
+ </h2>
160
+ <div className="space-y-3">
161
+ {pingHistory.slice().reverse().map((ping, idx) => (
162
+ <div
163
+ key={idx}
164
+ className="card card-hover p-5 flex items-center justify-between animate-fade-in"
165
+ >
166
+ <div className="flex items-center gap-3">
167
+ <div className={`w-3 h-3 rounded-full ${getLatencyDotColor(ping.latency)} shadow-lg`} />
168
+ <span className="text-sm text-muted-foreground font-mono">
169
+ {ping.time.toLocaleTimeString()}
170
+ </span>
171
+ </div>
172
+ <div className="flex items-center gap-3">
173
+ <span className={`text-lg font-bold ${getLatencyColor(ping.latency)}`}>
174
+ {ping.latency}ms
175
+ </span>
176
+ <span className={`badge ${
177
+ ping.latency < 100
178
+ ? 'badge-success'
179
+ : ping.latency < 500
180
+ ? 'badge-warning'
181
+ : 'badge-error'
182
+ }`}>
183
+ {ping.latency < 100 ? 'Excellent' : ping.latency < 500 ? 'Good' : 'Slow'}
184
+ </span>
185
+ </div>
186
+ </div>
187
+ ))}
188
+ </div>
189
+ </div>
190
+ )}
191
+
192
+ {/* Empty State */}
193
+ {pingHistory.length === 0 && !pinging && (
194
+ <div className="empty-state">
195
+ <Wifi className="empty-state-icon" />
196
+ <p className="empty-state-title">No ping history yet</p>
197
+ <p className="empty-state-description">
198
+ Click the "Send Ping" button above to test your connection
199
+ </p>
200
+ </div>
201
+ )}
202
+ </div>
203
+ );
204
+ }
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useStudioStore } from '@/lib/store';
5
+ import { api } from '@/lib/api';
6
+ import type { Prompt } from '@/lib/types';
7
+ import { FileText, RefreshCw, X, Play, AlertCircle } from 'lucide-react';
8
+
9
+ export default function PromptsPage() {
10
+ const { prompts, setPrompts, loading, setLoading } = useStudioStore();
11
+ const [searchQuery, setSearchQuery] = useState('');
12
+ const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
13
+ const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
14
+ const [promptResult, setPromptResult] = useState<any>(null);
15
+ const [executing, setExecuting] = useState(false);
16
+
17
+ useEffect(() => {
18
+ loadPrompts();
19
+ }, []);
20
+
21
+ const loadPrompts = async () => {
22
+ setLoading('prompts', true);
23
+ try {
24
+ const data = await api.getPrompts();
25
+ setPrompts(data.prompts || []);
26
+ } catch (error) {
27
+ console.error('Failed to load prompts:', error);
28
+ } finally {
29
+ setLoading('prompts', false);
30
+ }
31
+ };
32
+
33
+ const handleExecutePrompt = async (e: React.FormEvent) => {
34
+ e.preventDefault();
35
+ if (!selectedPrompt) return;
36
+
37
+ setExecuting(true);
38
+ setPromptResult(null);
39
+
40
+ try {
41
+ const result = await api.executePrompt(selectedPrompt.name, promptArgs);
42
+ setPromptResult(result);
43
+ } catch (error) {
44
+ console.error('Prompt execution failed:', error);
45
+ setPromptResult({ error: 'Execution failed' });
46
+ } finally {
47
+ setExecuting(false);
48
+ }
49
+ };
50
+
51
+ const filteredPrompts = prompts.filter((prompt) =>
52
+ prompt.name.toLowerCase().includes(searchQuery.toLowerCase())
53
+ );
54
+
55
+ return (
56
+ <div className="min-h-screen bg-background p-8">
57
+ {/* Header */}
58
+ <div className="mb-8">
59
+ <div className="flex items-center justify-between mb-6">
60
+ <div className="flex items-center gap-3">
61
+ <div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-500 flex items-center justify-center">
62
+ <FileText className="w-6 h-6 text-white" />
63
+ </div>
64
+ <div>
65
+ <h1 className="text-3xl font-bold text-foreground">Prompts</h1>
66
+ <p className="text-muted-foreground mt-1">Browse and execute MCP prompts</p>
67
+ </div>
68
+ </div>
69
+ <button onClick={loadPrompts} className="btn btn-primary gap-2">
70
+ <RefreshCw className="w-4 h-4" />
71
+ Refresh
72
+ </button>
73
+ </div>
74
+
75
+ <input
76
+ type="text"
77
+ placeholder="Search prompts..."
78
+ value={searchQuery}
79
+ onChange={(e) => setSearchQuery(e.target.value)}
80
+ className="input"
81
+ />
82
+ </div>
83
+
84
+ {/* Prompts Grid */}
85
+ {loading.prompts ? (
86
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
87
+ {[1, 2, 3].map((i) => (
88
+ <div key={i} className="card skeleton h-40"></div>
89
+ ))}
90
+ </div>
91
+ ) : filteredPrompts.length === 0 ? (
92
+ <div className="empty-state">
93
+ <AlertCircle className="empty-state-icon" />
94
+ <p className="empty-state-title">
95
+ {searchQuery ? 'No prompts found' : 'No prompts available'}
96
+ </p>
97
+ <p className="empty-state-description">
98
+ {searchQuery ? 'Try a different search term' : 'No prompts have been registered'}
99
+ </p>
100
+ </div>
101
+ ) : (
102
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
103
+ {filteredPrompts.map((prompt) => (
104
+ <div
105
+ key={prompt.name}
106
+ className="card card-hover p-6 cursor-pointer animate-fade-in"
107
+ onClick={() => {
108
+ setSelectedPrompt(prompt);
109
+ setPromptArgs({});
110
+ setPromptResult(null);
111
+ }}
112
+ >
113
+ <div className="flex items-center gap-3 mb-3">
114
+ <div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
115
+ <FileText className="w-5 h-5 text-blue-500" />
116
+ </div>
117
+ <h3 className="font-semibold text-foreground">{prompt.name}</h3>
118
+ </div>
119
+
120
+ <p className="text-sm text-muted-foreground line-clamp-2 mb-3">
121
+ {prompt.description || 'No description'}
122
+ </p>
123
+
124
+ {prompt.arguments && prompt.arguments.length > 0 && (
125
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
126
+ <span className="badge badge-secondary">
127
+ {prompt.arguments.length} argument{prompt.arguments.length !== 1 ? 's' : ''}
128
+ </span>
129
+ </div>
130
+ )}
131
+ </div>
132
+ ))}
133
+ </div>
134
+ )}
135
+
136
+ {/* Prompt Executor Modal */}
137
+ {selectedPrompt && (
138
+ <div
139
+ className="fixed inset-0 z-50 flex items-center justify-center animate-fade-in"
140
+ style={{ backgroundColor: 'rgba(0, 0, 0, 0.85)' }}
141
+ onClick={() => setSelectedPrompt(null)}
142
+ >
143
+ <div
144
+ className="bg-card rounded-2xl p-6 w-[600px] max-h-[80vh] overflow-auto border border-border shadow-2xl animate-scale-in"
145
+ onClick={(e) => e.stopPropagation()}
146
+ >
147
+ <div className="flex items-center justify-between mb-4">
148
+ <div className="flex items-center gap-3">
149
+ <div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center">
150
+ <FileText className="w-5 h-5 text-blue-500" />
151
+ </div>
152
+ <h2 className="text-xl font-bold text-foreground">{selectedPrompt.name}</h2>
153
+ </div>
154
+ <button
155
+ onClick={() => setSelectedPrompt(null)}
156
+ className="btn btn-ghost w-10 h-10 p-0"
157
+ >
158
+ <X className="w-5 h-5" />
159
+ </button>
160
+ </div>
161
+
162
+ <p className="text-sm text-muted-foreground mb-6">
163
+ {selectedPrompt.description || 'No description'}
164
+ </p>
165
+
166
+ <form onSubmit={handleExecutePrompt}>
167
+ {selectedPrompt.arguments && selectedPrompt.arguments.length > 0 ? (
168
+ selectedPrompt.arguments.map((arg) => (
169
+ <div key={arg.name} className="mb-4">
170
+ <label className="block text-sm font-medium text-foreground mb-2">
171
+ {arg.name}
172
+ {arg.required && <span className="text-destructive ml-1">*</span>}
173
+ </label>
174
+ <input
175
+ type="text"
176
+ className="input"
177
+ value={promptArgs[arg.name] || ''}
178
+ onChange={(e) =>
179
+ setPromptArgs({ ...promptArgs, [arg.name]: e.target.value })
180
+ }
181
+ required={arg.required}
182
+ placeholder={arg.description || `Enter ${arg.name}`}
183
+ />
184
+ {arg.description && (
185
+ <p className="text-xs text-muted-foreground mt-1">{arg.description}</p>
186
+ )}
187
+ </div>
188
+ ))
189
+ ) : (
190
+ <div className="bg-muted/30 rounded-lg p-4 mb-4">
191
+ <p className="text-sm text-muted-foreground">No arguments required</p>
192
+ </div>
193
+ )}
194
+
195
+ <button type="submit" className="btn btn-primary w-full gap-2" disabled={executing}>
196
+ <Play className="w-4 h-4" />
197
+ {executing ? 'Executing...' : 'Execute Prompt'}
198
+ </button>
199
+ </form>
200
+
201
+ {promptResult && (
202
+ <div className="mt-6">
203
+ <h3 className="font-semibold text-foreground mb-3">Messages:</h3>
204
+ <div className="space-y-3">
205
+ {promptResult.messages?.map((msg: any, idx: number) => {
206
+ // Extract text from content (can be string or object with type/text)
207
+ const contentText = typeof msg.content === 'string'
208
+ ? msg.content
209
+ : msg.content?.text || JSON.stringify(msg.content);
210
+
211
+ return (
212
+ <div key={idx} className="bg-muted/30 border border-border p-4 rounded-lg">
213
+ <div className="text-xs text-muted-foreground mb-2 uppercase font-semibold">
214
+ {msg.role}
215
+ </div>
216
+ <div className="text-sm whitespace-pre-wrap text-foreground">{contentText}</div>
217
+ </div>
218
+ );
219
+ })}
220
+ </div>
221
+ </div>
222
+ )}
223
+ </div>
224
+ </div>
225
+ )}
226
+ </div>
227
+ );
228
+ }