create-githat-app 1.7.0 → 1.8.0

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 (63) hide show
  1. package/dist/cli.js +3 -3
  2. package/package.json +6 -2
  3. package/templates/agent/app/(auth)/forgot-password/page.tsx.hbs +11 -0
  4. package/templates/agent/app/(auth)/reset-password/page.tsx.hbs +39 -0
  5. package/templates/agent/app/(auth)/verify-email/page.tsx.hbs +41 -0
  6. package/templates/agent/app/admin/agent/page.tsx.hbs +159 -62
  7. package/templates/agent/app/admin/layout.tsx.hbs +82 -0
  8. package/templates/agent/app/admin/mcp/page.tsx.hbs +156 -0
  9. package/templates/agent/app/dashboard/agents/page.tsx.hbs +9 -0
  10. package/templates/agent/app/dashboard/layout.tsx.hbs +9 -0
  11. package/templates/agent/app/dashboard/mcp/page.tsx.hbs +9 -0
  12. package/templates/agent/app/dashboard/page.tsx.hbs +128 -0
  13. package/templates/agent/app/globals.css.hbs +14 -9
  14. package/templates/agent/app/layout.tsx.hbs +7 -2
  15. package/templates/agent/app/page.tsx.hbs +127 -70
  16. package/templates/agent/app/verify/agent/page.tsx.hbs +124 -0
  17. package/templates/agent/next.config.ts.hbs +4 -5
  18. package/templates/agent/public/HERO_IMAGE.md +23 -0
  19. package/templates/base/githat/api/agents.ts.hbs +6 -6
  20. package/templates/base/githat/config.ts.hbs +7 -9
  21. package/templates/base/githat/dashboard/overview.tsx.hbs +106 -16
  22. package/templates/classroom/app/layout.tsx.hbs +6 -1
  23. package/templates/classroom/next.config.ts.hbs +4 -5
  24. package/templates/content/app/layout.tsx.hbs +6 -1
  25. package/templates/content/next.config.ts.hbs +4 -5
  26. package/templates/dashboard/app/admin/data/[entity]/page.tsx.hbs +2 -2
  27. package/templates/dashboard/app/layout.tsx.hbs +6 -1
  28. package/templates/dashboard/next.config.ts.hbs +4 -5
  29. package/templates/fullstack/apps-web-nextjs/app/layout.tsx.hbs +6 -1
  30. package/templates/fullstack/apps-web-nextjs/next.config.ts.hbs +5 -5
  31. package/templates/marketplace/app/layout.tsx.hbs +6 -1
  32. package/templates/marketplace/next.config.ts.hbs +4 -5
  33. package/templates/nextjs/app/(auth)/forgot-password/page.tsx.hbs +2 -54
  34. package/templates/nextjs/app/(auth)/reset-password/page.tsx.hbs +8 -75
  35. package/templates/nextjs/app/layout.tsx.hbs +6 -1
  36. package/templates/nextjs/next.config.ts.hbs +4 -5
  37. package/templates/plain/app/layout.tsx.hbs +6 -1
  38. package/templates/plain/next.config.ts.hbs +4 -5
  39. package/templates/portfolio/app/layout.tsx.hbs +6 -1
  40. package/templates/portfolio/next.config.ts.hbs +4 -5
  41. package/templates/saas/app/layout.tsx.hbs +6 -1
  42. package/templates/saas/next.config.ts.hbs +4 -5
  43. package/templates/agent/app/api/githat/[...path]/route.ts.hbs +0 -21
  44. package/templates/agent/proxy.ts.hbs +0 -10
  45. package/templates/classroom/app/api/githat/[...path]/route.ts.hbs +0 -21
  46. package/templates/classroom/proxy.ts.hbs +0 -10
  47. package/templates/content/app/api/githat/[...path]/route.ts.hbs +0 -21
  48. package/templates/content/proxy.ts.hbs +0 -10
  49. package/templates/dashboard/app/api/githat/[...path]/route.ts.hbs +0 -21
  50. package/templates/dashboard/proxy.ts.hbs +0 -10
  51. package/templates/fullstack/apps-web-nextjs/app/api/githat/[...path]/route.ts.hbs +0 -21
  52. package/templates/marketplace/app/(shop)/[slug]/p/[productId]/page.tsx.hbs +0 -99
  53. package/templates/marketplace/app/(shop)/[slug]/page.tsx.hbs +0 -90
  54. package/templates/marketplace/app/api/githat/[...path]/route.ts.hbs +0 -21
  55. package/templates/marketplace/proxy.ts.hbs +0 -10
  56. package/templates/nextjs/app/api/githat/[...path]/route.ts.hbs +0 -21
  57. package/templates/nextjs/proxy.ts.hbs +0 -10
  58. package/templates/plain/app/api/githat/[...path]/route.ts.hbs +0 -21
  59. package/templates/plain/proxy.ts.hbs +0 -10
  60. package/templates/portfolio/app/api/githat/[...path]/route.ts.hbs +0 -21
  61. package/templates/portfolio/proxy.ts.hbs +0 -10
  62. package/templates/saas/app/api/githat/[...path]/route.ts.hbs +0 -21
  63. package/templates/saas/proxy.ts.hbs +0 -10
package/dist/cli.js CHANGED
@@ -616,11 +616,11 @@ function getDefaults(projectName, publishableKey, typescript, fullstack, backend
616
616
  packageManager: detectPackageManager(),
617
617
  publishableKey: publishableKey || "",
618
618
  apiUrl: DEFAULT_API_URL,
619
- authFeatures: isMinimal ? [] : ["forgot-password"],
619
+ authFeatures: template === "agent" ? ["forgot-password", "email-verification", "mcp-servers", "ai-agents"] : isMinimal ? [] : ["forgot-password"],
620
620
  databaseChoice: "none",
621
621
  useTailwind: true,
622
- includeDashboard: !isMinimal,
623
- includeGithatFolder: !isMinimal && projectType === "frontend",
622
+ includeDashboard: template === "agent" ? true : !isMinimal,
623
+ includeGithatFolder: template === "agent" ? true : !isMinimal && projectType === "frontend",
624
624
  initGit: true,
625
625
  installDeps: true
626
626
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githat-app",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "GitHat CLI — scaffold apps and manage the skills marketplace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,7 +24,7 @@
24
24
  "figlet": "^1.8.0",
25
25
  "fs-extra": "^11.2.0",
26
26
  "gradient-string": "^3.0.0",
27
- "handlebars": "^4.7.8",
27
+ "handlebars": "^4.7.9",
28
28
  "ora": "^9.3.0",
29
29
  "unzipper": "^0.12.3"
30
30
  },
@@ -61,6 +61,10 @@
61
61
  "type": "git",
62
62
  "url": "git+https://github.com/GitHat-IO/create-githat-app.git"
63
63
  },
64
+ "overrides": {
65
+ "picomatch": ">=4.0.4",
66
+ "rollup": ">=4.59.0"
67
+ },
64
68
  "engines": {
65
69
  "node": ">=20.10"
66
70
  }
@@ -0,0 +1,11 @@
1
+ {{#if includeForgotPassword}}
2
+ import { ForgotPasswordForm } from '@githat/nextjs';
3
+
4
+ export default function ForgotPasswordPage() {
5
+ return (
6
+ <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}{{/if}}>
7
+ <ForgotPasswordForm signInUrl="/sign-in" />
8
+ </main>
9
+ );
10
+ }
11
+ {{/if}}
@@ -0,0 +1,39 @@
1
+ {{#if includeForgotPassword}}
2
+ 'use client';
3
+
4
+ import { Suspense } from 'react';
5
+ import { useSearchParams } from 'next/navigation';
6
+ import { ResetPasswordForm } from '@githat/nextjs';
7
+
8
+ function ResetPasswordContent() {
9
+ const searchParams = useSearchParams();
10
+ const token = searchParams.get('token');
11
+
12
+ if (!token) {
13
+ return (
14
+ <main {{#if useTailwind}}className="flex flex-col items-center justify-center min-h-screen gap-4"{{else}}style=\{{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', gap: '1rem', background: 'var(--bg)' }}{{/if}}>
15
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>No reset token found.</p>
16
+ <a href="/forgot-password" style=\{{ color: 'var(--primary)', fontSize: '0.875rem' }}>Request a new link →</a>
17
+ </main>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}{{/if}}>
23
+ <ResetPasswordForm token={token} signInUrl="/sign-in" />
24
+ </main>
25
+ );
26
+ }
27
+
28
+ export default function ResetPasswordPage() {
29
+ return (
30
+ <Suspense fallback={
31
+ <div style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)', color: 'var(--fg-muted)', fontSize: '0.875rem' }}>
32
+ Loading…
33
+ </div>
34
+ }>
35
+ <ResetPasswordContent />
36
+ </Suspense>
37
+ );
38
+ }
39
+ {{/if}}
@@ -0,0 +1,41 @@
1
+ {{#if includeEmailVerification}}
2
+ 'use client';
3
+
4
+ import { Suspense } from 'react';
5
+ import { useSearchParams } from 'next/navigation';
6
+ import { VerifyEmailStatus } from '@githat/nextjs';
7
+
8
+ function VerifyEmailContent() {
9
+ const searchParams = useSearchParams();
10
+ const token = searchParams.get('token');
11
+
12
+ if (!token) {
13
+ return (
14
+ <main style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}>
15
+ <div style=\{{ textAlign: 'center' }}>
16
+ <h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: 'var(--fg)', marginBottom: '0.5rem' }}>Invalid verification link</h1>
17
+ <p style=\{{ color: 'var(--fg-muted)' }}>The link may have expired. <a href="/sign-up" style=\{{ color: 'var(--primary)' }}>Try again</a>.</p>
18
+ </div>
19
+ </main>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}{{/if}}>
25
+ <VerifyEmailStatus token={token} signInUrl="/sign-in" />
26
+ </main>
27
+ );
28
+ }
29
+
30
+ export default function VerifyEmailPage() {
31
+ return (
32
+ <Suspense fallback={
33
+ <div style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)', color: 'var(--fg-muted)', fontSize: '0.875rem' }}>
34
+ Loading…
35
+ </div>
36
+ }>
37
+ <VerifyEmailContent />
38
+ </Suspense>
39
+ );
40
+ }
41
+ {{/if}}
@@ -1,86 +1,170 @@
1
1
  'use client';
2
2
 
3
+ import { useState, useEffect, useCallback } from 'react';
3
4
  import Link from 'next/link';
4
5
  import { useAuth } from '@githat/nextjs';
6
+ import { githatApi } from '../../../githat/api/client';
7
+ {{#if typescript}}
8
+ type AgentStatus = 'active' | 'paused' | 'revoked';
9
+
10
+ interface Agent {
11
+ name: string;
12
+ walletAddress: string;
13
+ status: AgentStatus;
14
+ capabilities: string[];
15
+ actionsLast24h: number;
16
+ verifyUrl: string;
17
+ }
18
+ {{/if}}
19
+
20
+ const STATUS_COLOR{{#if typescript}}: Record<string, string>{{/if}} = {
21
+ active: 'var(--success)',
22
+ paused: 'var(--warn)',
23
+ revoked: 'var(--danger)',
24
+ };
5
25
 
6
- /**
7
- * Agent operator dashboard — control the agent you registered.
8
- *
9
- * Three core actions an operator does:
10
- * 1. See live status (active / paused / revoked)
11
- * 2. Adjust capabilities (which tools the agent can call)
12
- * 3. Hit the kill switch
13
- *
14
- * Real apps fetch the agent record from GitHat's
15
- * GET /agent/{walletAddress}. Stub below shows the data shape.
16
- */
17
26
  export default function AgentAdminPage() {
18
27
  const { isSignedIn, isLoading } = useAuth();
28
+ const [agent, setAgent] = useState{{#if typescript}}<Agent | null>{{/if}}(null);
29
+ const [fetching, setFetching] = useState(true);
30
+ const [killing, setKilling] = useState(false);
31
+
32
+ const fetchAgent = useCallback(async () => {
33
+ try {
34
+ const data = await githatApi.get{{#if typescript}}<{
35
+ agents: Array<{
36
+ name: string;
37
+ walletAddress: string;
38
+ status: string;
39
+ allowedScopes: string[];
40
+ requests: number;
41
+ }>;
42
+ }>{{/if}}('/agent/list');
43
+ const first = data.agents[0];
44
+ if (first) {
45
+ setAgent({
46
+ name: first.name,
47
+ walletAddress: first.walletAddress,
48
+ status: first.status{{#if typescript}} as AgentStatus{{/if}},
49
+ capabilities: first.allowedScopes,
50
+ actionsLast24h: first.requests,
51
+ verifyUrl: `/verify/agent/${first.walletAddress}`,
52
+ });
53
+ }
54
+ } catch {
55
+ // no agents registered yet
56
+ } finally {
57
+ setFetching(false);
58
+ }
59
+ }, []);
60
+
61
+ useEffect(() => {
62
+ if (isSignedIn) fetchAgent();
63
+ else if (!isLoading) setFetching(false);
64
+ }, [isSignedIn, isLoading, fetchAgent]);
65
+
66
+ const handleKillSwitch = async () => {
67
+ if (!agent || killing) return;
68
+ if (!confirm('Revoke this agent? All existing tokens stop working immediately.')) return;
69
+
70
+ setKilling(true);
71
+ try {
72
+ await githatApi.post(`/agent/${agent.walletAddress}/suspend`);
73
+ setAgent((prev) => prev ? { ...prev, status: 'revoked' } : prev);
74
+ } finally {
75
+ setKilling(false);
76
+ }
77
+ };
78
+
79
+ if (isLoading || fetching) {
80
+ return <div style=\{{ padding: 'var(--space-8)', color: 'var(--fg-muted)', fontSize: '0.875rem' }}>Loading…</div>;
81
+ }
19
82
 
20
- if (isLoading) return <div style=\{{ padding: 'var(--space-8)', color: 'var(--fg-muted)' }}>Loading…</div>;
21
83
  if (!isSignedIn) {
22
84
  return (
23
85
  <div style=\{{ padding: 'var(--space-8)', textAlign: 'center' }}>
24
- <Link href="/sign-in" style=\{{ color: 'var(--primary)' }}>Sign in →</Link>
86
+ <Link href="/sign-in" style=\{{ color: 'var(--primary)', fontWeight: 600 }}>Sign in →</Link>
25
87
  </div>
26
88
  );
27
89
  }
28
90
 
29
- const agent = {
30
- name: 'My research agent',
31
- wallet: '0xAB12…CDEf',
32
- status: 'active' as 'active' | 'paused' | 'revoked',
33
- capabilities: ['web.search', 'web.fetch', 'arxiv.read'],
34
- actionsLast24h: 47,
35
- verifyUrl: 'https://githat.io/verify/agent/0xAB12...CDEf',
36
- };
91
+ if (!agent) {
92
+ return (
93
+ <div style=\{{ padding: 'var(--space-8) var(--space-6)', maxWidth: '32rem' }}>
94
+ <h1 style=\{{ fontSize: '1.5rem', fontWeight: 700, marginBottom: 'var(--space-3)' }}>No agent registered</h1>
95
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', marginBottom: 'var(--space-6)' }}>
96
+ Register a wallet-bound agent to get started. Each agent gets an Ethereum-style
97
+ key pair — it signs requests so clients can verify its identity on-chain.
98
+ </p>
99
+ <Link href="/sign-up" style=\{{
100
+ display: 'inline-block',
101
+ padding: 'var(--space-3) var(--space-5)',
102
+ borderRadius: 'var(--radius-md)',
103
+ background: 'var(--primary)',
104
+ color: '#fff',
105
+ fontWeight: 600,
106
+ textDecoration: 'none',
107
+ fontSize: '0.875rem',
108
+ }}>
109
+ Register agent →
110
+ </Link>
111
+ </div>
112
+ );
113
+ }
37
114
 
38
115
  return (
39
- <div style=\{{ padding: 'var(--space-8) var(--space-4)', maxWidth: '48rem', margin: '0 auto' }}>
40
- <h1 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '2rem', marginBottom: 'var(--space-2)' }}>
41
- {agent.name}
42
- </h1>
43
- <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem', marginBottom: 'var(--space-6)' }}>
44
- Wallet: <code>{agent.wallet}</code>
45
- </p>
116
+ <div style=\{{ padding: 'var(--space-8) var(--space-6)', maxWidth: '48rem' }}>
117
+ <div style=\{{ marginBottom: 'var(--space-6)' }}>
118
+ <h1 style=\{{ fontSize: '1.5rem', fontWeight: 700, marginBottom: 'var(--space-1)' }}>
119
+ {agent.name}
120
+ </h1>
121
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.8125rem', fontFamily: 'var(--font-mono)' }}>
122
+ {agent.walletAddress}
123
+ </p>
124
+ </div>
46
125
 
126
+ {/* Status card */}
47
127
  <section style=\{{
48
128
  padding: 'var(--space-5)',
49
- borderRadius: 'var(--radius-md, 0.5rem)',
129
+ borderRadius: 'var(--radius-lg)',
50
130
  border: '1px solid var(--border)',
51
131
  background: 'var(--surface)',
52
132
  marginBottom: 'var(--space-6)',
53
133
  }}>
54
- <div style=\{{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-3)' }}>
134
+ <div style=\{{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 'var(--space-4)' }}>
55
135
  <span style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)' }}>Status</span>
56
136
  <span style=\{{
57
137
  padding: 'var(--space-1) var(--space-3)',
58
- borderRadius: 'var(--radius-full, 9999px)',
59
- background: agent.status === 'active' ? 'var(--success)' : agent.status === 'paused' ? 'var(--warn)' : 'var(--danger)',
60
- color: 'var(--bg)',
61
- fontSize: '0.75rem',
138
+ borderRadius: 'var(--radius-full)',
139
+ background: STATUS_COLOR[agent.status] ?? 'var(--border)',
140
+ color: '#fff',
141
+ fontSize: '0.6875rem',
62
142
  fontWeight: 700,
143
+ textTransform: 'uppercase',
144
+ letterSpacing: '0.06em',
63
145
  }}>
64
- {agent.status.toUpperCase()}
146
+ {agent.status}
65
147
  </span>
66
148
  </div>
67
- <div style=\{{ fontSize: '2rem', fontWeight: 600, marginBottom: 'var(--space-1)' }}>
149
+ <div style=\{{ fontSize: '2rem', fontWeight: 700, marginBottom: 'var(--space-1)' }}>
68
150
  {agent.actionsLast24h}
69
151
  </div>
70
152
  <div style=\{{ fontSize: '0.75rem', color: 'var(--fg-subtle)' }}>actions in the last 24 hours</div>
71
153
  </section>
72
154
 
155
+ {/* Capabilities */}
73
156
  <section style=\{{ marginBottom: 'var(--space-6)' }}>
74
- <h2 style=\{{ fontSize: '1.125rem', marginBottom: 'var(--space-3)' }}>Capabilities</h2>
157
+ <h2 style=\{{ fontSize: '1rem', fontWeight: 600, marginBottom: 'var(--space-3)' }}>Capabilities</h2>
75
158
  <ul style=\{{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexWrap: 'wrap', gap: 'var(--space-2)' }}>
76
159
  {agent.capabilities.map((cap) => (
77
160
  <li key={cap} style=\{{
78
161
  padding: 'var(--space-2) var(--space-3)',
79
- borderRadius: 'var(--radius-md, 0.5rem)',
162
+ borderRadius: 'var(--radius-md)',
80
163
  border: '1px solid var(--border)',
81
164
  background: 'var(--surface)',
82
- fontSize: '0.875rem',
83
- fontFamily: 'var(--font-mono, monospace)',
165
+ fontSize: '0.8125rem',
166
+ fontFamily: 'var(--font-mono)',
167
+ color: 'var(--fg-muted)',
84
168
  }}>
85
169
  {cap}
86
170
  </li>
@@ -88,39 +172,52 @@ export default function AgentAdminPage() {
88
172
  </ul>
89
173
  </section>
90
174
 
175
+ {/* Actions */}
91
176
  <section style=\{{ display: 'flex', gap: 'var(--space-3)', flexWrap: 'wrap' }}>
92
- <a href={agent.verifyUrl} target="_blank" rel="noopener noreferrer" style=\{{
93
- padding: 'var(--space-3) var(--space-4)',
94
- borderRadius: 'var(--radius-md, 0.5rem)',
95
- border: '1px solid var(--border)',
96
- color: 'var(--fg)',
97
- textDecoration: 'none',
98
- }}>
177
+ <a
178
+ href={agent.verifyUrl}
179
+ target="_blank"
180
+ rel="noopener noreferrer"
181
+ style=\{{
182
+ padding: 'var(--space-3) var(--space-4)',
183
+ borderRadius: 'var(--radius-md)',
184
+ border: '1px solid var(--border)',
185
+ color: 'var(--fg)',
186
+ textDecoration: 'none',
187
+ fontSize: '0.875rem',
188
+ }}
189
+ >
99
190
  Public verify URL ↗
100
191
  </a>
101
192
  <Link href="/admin/mcp" style=\{{
102
193
  padding: 'var(--space-3) var(--space-4)',
103
- borderRadius: 'var(--radius-md, 0.5rem)',
194
+ borderRadius: 'var(--radius-md)',
104
195
  border: '1px solid var(--border)',
105
196
  color: 'var(--fg)',
106
197
  textDecoration: 'none',
198
+ fontSize: '0.875rem',
107
199
  }}>
108
200
  MCP servers
109
201
  </Link>
110
- <button
111
- onClick={() => { /* TODO: POST /api/agent/kill */ }}
112
- style=\{{
113
- padding: 'var(--space-3) var(--space-4)',
114
- borderRadius: 'var(--radius-md, 0.5rem)',
115
- border: '1px solid var(--danger)',
116
- background: 'transparent',
117
- color: 'var(--danger)',
118
- fontWeight: 600,
119
- cursor: 'pointer',
120
- }}
121
- >
122
- Kill switch
123
- </button>
202
+ {agent.status !== 'revoked' && (
203
+ <button
204
+ onClick={handleKillSwitch}
205
+ disabled={killing}
206
+ style=\{{
207
+ padding: 'var(--space-3) var(--space-4)',
208
+ borderRadius: 'var(--radius-md)',
209
+ border: '1px solid var(--danger)',
210
+ background: 'transparent',
211
+ color: 'var(--danger)',
212
+ fontWeight: 600,
213
+ fontSize: '0.875rem',
214
+ cursor: killing ? 'not-allowed' : 'pointer',
215
+ opacity: killing ? 0.6 : 1,
216
+ }}
217
+ >
218
+ {killing ? 'Revoking…' : 'Kill switch'}
219
+ </button>
220
+ )}
124
221
  </section>
125
222
  </div>
126
223
  );
@@ -0,0 +1,82 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { usePathname } from 'next/navigation';
5
+ import { useAuth, UserButton } from '@githat/nextjs';
6
+
7
+ const navItems = [
8
+ { label: 'Agent', href: '/admin/agent' },
9
+ { label: 'MCP Servers', href: '/admin/mcp' },
10
+ ];
11
+
12
+ export default function AdminLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
13
+ const pathname = usePathname();
14
+ const { isSignedIn, isLoading } = useAuth();
15
+
16
+ if (isLoading) {
17
+ return (
18
+ <div style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}>
19
+ <span style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>Loading…</span>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ if (!isSignedIn) {
25
+ return (
26
+ <div style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: 'var(--bg)' }}>
27
+ <Link href="/sign-in" style=\{{ color: 'var(--primary)', fontWeight: 600 }}>Sign in to access the admin panel →</Link>
28
+ </div>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <div style=\{{ display: 'flex', minHeight: '100vh', background: 'var(--bg)' }}>
34
+ <aside style=\{{
35
+ width: '14rem',
36
+ borderRight: '1px solid var(--border)',
37
+ padding: 'var(--space-6) var(--space-4)',
38
+ display: 'flex',
39
+ flexDirection: 'column',
40
+ gap: 'var(--space-2)',
41
+ }}>
42
+ <Link href="/" style=\{{
43
+ fontFamily: 'var(--font-wordmark)',
44
+ fontSize: '1.125rem',
45
+ fontWeight: 700,
46
+ color: 'var(--fg)',
47
+ marginBottom: 'var(--space-4)',
48
+ display: 'block',
49
+ }}>
50
+ {{businessName}}
51
+ </Link>
52
+ <nav style=\{{ display: 'flex', flexDirection: 'column', gap: 'var(--space-1)' }}>
53
+ {navItems.map((item) => (
54
+ <Link
55
+ key={item.href}
56
+ href={item.href}
57
+ style=\{{
58
+ padding: 'var(--space-2) var(--space-3)',
59
+ borderRadius: 'var(--radius-md)',
60
+ color: pathname === item.href ? 'var(--fg)' : 'var(--fg-muted)',
61
+ background: pathname === item.href ? 'var(--surface)' : 'transparent',
62
+ textDecoration: 'none',
63
+ fontSize: '0.875rem',
64
+ fontWeight: pathname === item.href ? 600 : 400,
65
+ border: pathname === item.href ? '1px solid var(--border)' : '1px solid transparent',
66
+ }}
67
+ >
68
+ {item.label}
69
+ </Link>
70
+ ))}
71
+ </nav>
72
+ <div style=\{{ marginTop: 'auto' }}>
73
+ <UserButton />
74
+ </div>
75
+ </aside>
76
+
77
+ <main style=\{{ flex: 1, overflowY: 'auto' }}>
78
+ {children}
79
+ </main>
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,156 @@
1
+ 'use client';
2
+
3
+ import { useAuth } from '@githat/nextjs';
4
+
5
+ const DEMO_SERVERS = [
6
+ {
7
+ name: 'web-search',
8
+ description: 'Search the web and fetch pages',
9
+ tools: ['web.search', 'web.fetch'],
10
+ status: 'connected' as const,
11
+ },
12
+ {
13
+ name: 'arxiv',
14
+ description: 'Read and search arXiv papers',
15
+ tools: ['arxiv.read', 'arxiv.search'],
16
+ status: 'connected' as const,
17
+ },
18
+ {
19
+ name: 'github',
20
+ description: 'Read and write GitHub repos',
21
+ tools: ['github.read', 'github.push'],
22
+ status: 'disconnected' as const,
23
+ },
24
+ ];
25
+
26
+ export default function McpAdminPage() {
27
+ const { isSignedIn, isLoading } = useAuth();
28
+
29
+ if (isLoading) return <div style=\{{ padding: 'var(--space-8)', color: 'var(--fg-muted)' }}>Loading…</div>;
30
+ if (!isSignedIn) return null;
31
+
32
+ return (
33
+ <div style=\{{ padding: 'var(--space-8) var(--space-6)', maxWidth: '52rem' }}>
34
+ <div style=\{{ marginBottom: 'var(--space-6)' }}>
35
+ <h1 style=\{{ fontSize: '1.5rem', fontWeight: 700, marginBottom: 'var(--space-2)' }}>MCP Servers</h1>
36
+ <p style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)' }}>
37
+ Model Context Protocol servers expose tools your agent can call. Connect servers
38
+ to grant your agent access to the tools they expose.
39
+ </p>
40
+ </div>
41
+
42
+ <div style=\{{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
43
+ {DEMO_SERVERS.map((server) => (
44
+ <div
45
+ key={server.name}
46
+ style=\{{
47
+ padding: 'var(--space-5)',
48
+ borderRadius: 'var(--radius-lg)',
49
+ border: '1px solid var(--border)',
50
+ background: 'var(--surface)',
51
+ display: 'flex',
52
+ alignItems: 'flex-start',
53
+ justifyContent: 'space-between',
54
+ gap: 'var(--space-4)',
55
+ }}
56
+ >
57
+ <div style=\{{ flex: 1 }}>
58
+ <div style=\{{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)', marginBottom: 'var(--space-2)' }}>
59
+ <code style=\{{ fontFamily: 'var(--font-mono)', fontSize: '0.9375rem', fontWeight: 600 }}>
60
+ {server.name}
61
+ </code>
62
+ <span style=\{{
63
+ padding: '0.125rem var(--space-2)',
64
+ borderRadius: 'var(--radius-full)',
65
+ fontSize: '0.6875rem',
66
+ fontWeight: 700,
67
+ textTransform: 'uppercase',
68
+ letterSpacing: '0.04em',
69
+ background: server.status === 'connected' ? 'var(--success)' : 'var(--border)',
70
+ color: server.status === 'connected' ? '#fff' : 'var(--fg-muted)',
71
+ }}>
72
+ {server.status}
73
+ </span>
74
+ </div>
75
+ <p style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-3)' }}>
76
+ {server.description}
77
+ </p>
78
+ <div style=\{{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-2)' }}>
79
+ {server.tools.map((tool) => (
80
+ <span
81
+ key={tool}
82
+ style=\{{
83
+ padding: 'var(--space-1) var(--space-2)',
84
+ borderRadius: 'var(--radius-md)',
85
+ border: '1px solid var(--border)',
86
+ fontSize: '0.75rem',
87
+ fontFamily: 'var(--font-mono)',
88
+ color: 'var(--fg-muted)',
89
+ }}
90
+ >
91
+ {tool}
92
+ </span>
93
+ ))}
94
+ </div>
95
+ </div>
96
+ <button
97
+ style=\{{
98
+ flexShrink: 0,
99
+ padding: 'var(--space-2) var(--space-4)',
100
+ borderRadius: 'var(--radius-md)',
101
+ border: '1px solid var(--border)',
102
+ background: 'transparent',
103
+ color: server.status === 'connected' ? 'var(--danger)' : 'var(--primary)',
104
+ fontSize: '0.875rem',
105
+ fontWeight: 600,
106
+ cursor: 'pointer',
107
+ }}
108
+ >
109
+ {server.status === 'connected' ? 'Disconnect' : 'Connect'}
110
+ </button>
111
+ </div>
112
+ ))}
113
+ </div>
114
+
115
+ <div style=\{{
116
+ marginTop: 'var(--space-8)',
117
+ padding: 'var(--space-5)',
118
+ borderRadius: 'var(--radius-lg)',
119
+ border: '1px dashed var(--border)',
120
+ textAlign: 'center',
121
+ }}>
122
+ <p style=\{{ fontSize: '0.875rem', color: 'var(--fg-muted)', marginBottom: 'var(--space-3)' }}>
123
+ Connect a custom MCP server by URL
124
+ </p>
125
+ <div style=\{{ display: 'flex', gap: 'var(--space-2)', maxWidth: '28rem', margin: '0 auto' }}>
126
+ <input
127
+ type="url"
128
+ placeholder="https://my-mcp-server.com/sse"
129
+ style=\{{
130
+ flex: 1,
131
+ padding: 'var(--space-2) var(--space-3)',
132
+ borderRadius: 'var(--radius-md)',
133
+ border: '1px solid var(--border)',
134
+ background: 'var(--bg)',
135
+ color: 'var(--fg)',
136
+ fontSize: '0.875rem',
137
+ fontFamily: 'var(--font-mono)',
138
+ }}
139
+ />
140
+ <button style=\{{
141
+ padding: 'var(--space-2) var(--space-4)',
142
+ borderRadius: 'var(--radius-md)',
143
+ border: 'none',
144
+ background: 'var(--primary)',
145
+ color: '#fff',
146
+ fontSize: '0.875rem',
147
+ fontWeight: 600,
148
+ cursor: 'pointer',
149
+ }}>
150
+ Add
151
+ </button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ }