@chaaskit/client 0.1.1 → 0.1.2

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/dist/lib/index.js +970 -80
  3. package/dist/lib/index.js.map +1 -1
  4. package/dist/lib/routes/AdminPromoCodesRoute.js +19 -0
  5. package/dist/lib/routes/AdminPromoCodesRoute.js.map +1 -0
  6. package/dist/lib/routes/AdminWaitlistRoute.js +19 -0
  7. package/dist/lib/routes/AdminWaitlistRoute.js.map +1 -0
  8. package/dist/lib/routes.js +47 -37
  9. package/dist/lib/routes.js.map +1 -1
  10. package/dist/lib/ssr-utils.js +36 -16
  11. package/dist/lib/ssr-utils.js.map +1 -1
  12. package/dist/lib/styles.css +37 -0
  13. package/dist/lib/useExtensions-B5nX_8XD.js.map +1 -1
  14. package/package.json +20 -12
  15. package/src/components/MessageItem.tsx +35 -4
  16. package/src/components/MessageList.tsx +51 -5
  17. package/src/components/OAuthAppsSection.tsx +1 -1
  18. package/src/components/Sidebar.tsx +1 -3
  19. package/src/components/ToolCallDisplay.tsx +102 -11
  20. package/src/components/tool-renderers/DocumentListRenderer.tsx +44 -0
  21. package/src/components/tool-renderers/DocumentReadRenderer.tsx +33 -0
  22. package/src/components/tool-renderers/DocumentSaveRenderer.tsx +32 -0
  23. package/src/components/tool-renderers/DocumentSearchRenderer.tsx +33 -0
  24. package/src/components/tool-renderers/index.ts +36 -0
  25. package/src/components/tool-renderers/utils.ts +7 -0
  26. package/src/contexts/AuthContext.tsx +16 -6
  27. package/src/contexts/ConfigContext.tsx +22 -28
  28. package/src/extensions/registry.ts +2 -1
  29. package/src/hooks/__tests__/basePath.test.ts +42 -0
  30. package/src/index.tsx +5 -2
  31. package/src/pages/AdminDashboardPage.tsx +15 -1
  32. package/src/pages/AdminPromoCodesPage.tsx +378 -0
  33. package/src/pages/AdminTeamPage.tsx +29 -1
  34. package/src/pages/AdminTeamsPage.tsx +15 -1
  35. package/src/pages/AdminUsersPage.tsx +15 -1
  36. package/src/pages/AdminWaitlistPage.tsx +156 -0
  37. package/src/pages/RegisterPage.tsx +91 -9
  38. package/src/routes/AdminPromoCodesRoute.tsx +24 -0
  39. package/src/routes/AdminWaitlistRoute.tsx +24 -0
  40. package/src/routes/index.ts +2 -0
  41. package/src/ssr-utils.tsx +32 -12
  42. package/src/stores/chatStore.ts +5 -0
  43. package/dist/favicon.svg +0 -11
  44. package/dist/index.html +0 -17
  45. package/dist/logo.svg +0 -12
@@ -1,8 +1,9 @@
1
- import { useRef, useEffect } from 'react';
2
- import type { Message, MCPContent, UIResource } from '@chaaskit/shared';
1
+ import { useMemo, useRef, useEffect } from 'react';
2
+ import type { Message, MCPContent, UIResource, ToolCall, ToolResult } from '@chaaskit/shared';
3
3
  import { Bot } from 'lucide-react';
4
4
  import { useTheme } from '../contexts/ThemeContext';
5
5
  import { useConfig } from '../contexts/ConfigContext';
6
+ import { useExtensionTools } from '../extensions/useExtensions';
6
7
  import MessageItem from './MessageItem';
7
8
  import ToolCallDisplay, { UIResourceWidget } from './ToolCallDisplay';
8
9
 
@@ -17,6 +18,7 @@ interface CompletedToolCall extends PendingToolCall {
17
18
  result: MCPContent[];
18
19
  isError?: boolean;
19
20
  uiResource?: UIResource;
21
+ structuredContent?: Record<string, unknown>;
20
22
  }
21
23
 
22
24
  interface MessageListProps {
@@ -35,6 +37,12 @@ export default function MessageList({
35
37
  const bottomRef = useRef<HTMLDivElement>(null);
36
38
  const { theme } = useTheme();
37
39
  const config = useConfig();
40
+ const extensionTools = useExtensionTools();
41
+ const toolRendererMap = useMemo(() => {
42
+ const map = new Map<string, typeof extensionTools[number]>();
43
+ extensionTools.forEach((tool) => map.set(tool.name, tool));
44
+ return map;
45
+ }, [extensionTools]);
38
46
  const showToolCalls = config.mcp?.showToolCalls !== false;
39
47
 
40
48
  useEffect(() => {
@@ -45,8 +53,34 @@ export default function MessageList({
45
53
  const isStreaming = Boolean(streamingContent) || hasToolActivity;
46
54
 
47
55
  // Get UI resources from completed tool calls for separate rendering
56
+ const renderedToolCalls = completedToolCalls
57
+ .filter((call) =>
58
+ call.serverId === 'native' &&
59
+ !!toolRendererMap.get(call.name)?.resultRenderer
60
+ )
61
+ .map((call) => {
62
+ const renderer = toolRendererMap.get(call.name)!.resultRenderer!;
63
+ const toolCall: ToolCall = {
64
+ id: call.id,
65
+ serverId: call.serverId,
66
+ toolName: call.name,
67
+ arguments: call.input,
68
+ status: call.isError ? 'error' : 'completed',
69
+ };
70
+ const toolResult: ToolResult = {
71
+ toolCallId: call.id,
72
+ content: call.result,
73
+ isError: call.isError,
74
+ uiResource: call.uiResource,
75
+ structuredContent: call.structuredContent,
76
+ };
77
+ return { toolCall, toolResult, Renderer: renderer };
78
+ });
79
+
80
+ const renderedToolCallIds = new Set(renderedToolCalls.map((entry) => entry.toolCall.id));
81
+
48
82
  const uiResources = completedToolCalls
49
- .filter((tc) => tc.uiResource?.text)
83
+ .filter((tc) => tc.uiResource?.text && !renderedToolCallIds.has(tc.id))
50
84
  .map((tc) => tc.uiResource!);
51
85
 
52
86
  // Debug logging
@@ -96,6 +130,7 @@ export default function MessageList({
96
130
  toolCallId: call.id,
97
131
  content: call.result,
98
132
  isError: call.isError,
133
+ structuredContent: call.structuredContent,
99
134
  }}
100
135
  hideUiResource
101
136
  />
@@ -118,7 +153,18 @@ export default function MessageList({
118
153
  </div>
119
154
  )}
120
155
 
121
- {/* 2. UI Resource Widgets (outside bubble, full width) */}
156
+ {/* 2. Tool Renderers (native tools) */}
157
+ {renderedToolCalls.length > 0 && (
158
+ <div className="space-y-3">
159
+ {renderedToolCalls.map(({ toolCall, toolResult, Renderer }) => (
160
+ <div key={toolCall.id} className="rounded-lg border border-border bg-background-secondary/40 p-3">
161
+ <Renderer toolCall={toolCall} toolResult={toolResult} />
162
+ </div>
163
+ ))}
164
+ </div>
165
+ )}
166
+
167
+ {/* 3. UI Resource Widgets (outside bubble, full width) */}
122
168
  {uiResources.length > 0 && (
123
169
  <div className="space-y-3">
124
170
  {uiResources.map((uiResource, index) => (
@@ -127,7 +173,7 @@ export default function MessageList({
127
173
  </div>
128
174
  )}
129
175
 
130
- {/* 3. Text Response Bubble (with avatar) */}
176
+ {/* 4. Text Response Bubble (with avatar) */}
131
177
  {streamingContent && (
132
178
  <div className="group flex gap-3">
133
179
  {/* Avatar */}
@@ -18,7 +18,7 @@ export default function OAuthAppsSection() {
18
18
  const [revokingId, setRevokingId] = useState<string | null>(null);
19
19
 
20
20
  // Check if OAuth is enabled
21
- const oauthEnabled = config.mcp?.server?.oauth?.enabled;
21
+ const oauthEnabled = config.mcp?.servers?.some((server) => server.authMode === 'user-oauth');
22
22
 
23
23
  useEffect(() => {
24
24
  if (oauthEnabled) {
@@ -373,9 +373,7 @@ export default function Sidebar({ onClose, onOpenSearch }: SidebarProps) {
373
373
  )}
374
374
 
375
375
  {/* Admin Dashboard - show for site admins (config or database flag) */}
376
- {(user?.isAdmin || config.admin?.emails?.some(
377
- (email: string) => email.toLowerCase() === user?.email?.toLowerCase()
378
- )) && (
376
+ {(user?.isAdmin || config.auth?.isAdmin) && (
379
377
  <Link
380
378
  to={appPath('/admin')}
381
379
  className="mb-1 flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-xs text-text-secondary hover:bg-background-secondary hover:text-text-primary active:bg-background-secondary"
@@ -19,6 +19,8 @@ const AUTO_APPROVE_LABELS: Record<AutoApproveReason, string> = {
19
19
  thread_allowed: 'Allowed for this chat',
20
20
  };
21
21
 
22
+ const TOOL_UI_MESSAGE_SOURCE = 'chaaskit-tool-ui';
23
+
22
24
  // Generate the window.openai initialization script for OpenAI format resources
23
25
  function generateOpenAiScript(
24
26
  toolInput: Record<string, unknown>,
@@ -68,6 +70,46 @@ function generateOpenAiScript(
68
70
  </style>
69
71
  <script>
70
72
  (function() {
73
+ const MESSAGE_SOURCE = '${TOOL_UI_MESSAGE_SOURCE}';
74
+ let requestId = 0;
75
+ const pending = new Map();
76
+
77
+ function resolvePending(id, data) {
78
+ const entry = pending.get(id);
79
+ if (!entry) return;
80
+ pending.delete(id);
81
+ if (data && data.ok) {
82
+ entry.resolve(data.result);
83
+ return;
84
+ }
85
+ entry.resolve({ error: (data && data.error) || 'Tool UI bridge error' });
86
+ }
87
+
88
+ window.addEventListener('message', (event) => {
89
+ const data = event.data || {};
90
+ if (data.source !== MESSAGE_SOURCE || !data.id) return;
91
+ resolvePending(data.id, data);
92
+ });
93
+
94
+ function sendToParent(type, payload, timeoutMs = 15000) {
95
+ return new Promise((resolve) => {
96
+ const id = String(Date.now()) + '-' + String(requestId++);
97
+ pending.set(id, { resolve });
98
+ try {
99
+ window.parent.postMessage({ source: MESSAGE_SOURCE, type, id, payload }, '*');
100
+ } catch (err) {
101
+ pending.delete(id);
102
+ resolve({ error: 'Unable to reach host window' });
103
+ return;
104
+ }
105
+ setTimeout(() => {
106
+ if (!pending.has(id)) return;
107
+ pending.delete(id);
108
+ resolve({ error: 'Tool UI request timed out' });
109
+ }, timeoutMs);
110
+ });
111
+ }
112
+
71
113
  // Initialize window.openai with the OpenAI Apps SDK spec
72
114
  window.openai = {
73
115
  // Core globals
@@ -92,17 +134,20 @@ function generateOpenAiScript(
92
134
  // API methods
93
135
  callTool: async (name, args) => {
94
136
  console.log('window.openai.callTool called:', { name, args });
95
- // TODO: Implement actual tool calling via parent window messaging
96
- return {
97
- content: [{ type: 'text', text: 'Tool calling not yet implemented' }],
98
- isError: false
99
- };
137
+ const response = await sendToParent('callTool', { name, args });
138
+ if (response && response.error) {
139
+ return {
140
+ content: [{ type: 'text', text: response.error }],
141
+ isError: true
142
+ };
143
+ }
144
+ return response || { content: [{ type: 'text', text: 'No response from host' }], isError: true };
100
145
  },
101
146
 
102
147
  sendFollowUpMessage: async (args) => {
103
148
  console.log('window.openai.sendFollowUpMessage called:', args);
104
- // TODO: Implement via parent window messaging
105
- return {};
149
+ const response = await sendToParent('sendFollowUpMessage', args);
150
+ return response && response.error ? { error: response.error } : (response || {});
106
151
  },
107
152
 
108
153
  openExternal: (payload) => {
@@ -114,27 +159,41 @@ function generateOpenAiScript(
114
159
 
115
160
  requestDisplayMode: async (args) => {
116
161
  console.log('window.openai.requestDisplayMode called:', args);
117
- return { mode: args.mode };
162
+ const response = await sendToParent('requestDisplayMode', args);
163
+ if (response && response.error) {
164
+ return { mode: args.mode };
165
+ }
166
+ return response || { mode: args.mode };
118
167
  },
119
168
 
120
169
  setWidgetState: async (state) => {
121
170
  console.log('window.openai.setWidgetState called:', state);
122
171
  window.openai.widgetState = state;
123
- return {};
172
+ const response = await sendToParent('setWidgetState', state);
173
+ return response && response.error ? {} : (response || {});
124
174
  },
125
175
 
126
176
  requestClose: () => {
127
177
  console.log('window.openai.requestClose called');
178
+ sendToParent('requestClose', {});
128
179
  },
129
180
 
130
181
  getFileDownloadUrl: async ({ fileId }) => {
131
182
  console.log('window.openai.getFileDownloadUrl called:', fileId);
132
- return { url: '' };
183
+ const response = await sendToParent('getFileDownloadUrl', { fileId });
184
+ if (response && response.error) {
185
+ return { url: '' };
186
+ }
187
+ return response || { url: '' };
133
188
  },
134
189
 
135
190
  uploadFile: async (file) => {
136
191
  console.log('window.openai.uploadFile called:', file);
137
- return { fileId: '' };
192
+ const response = await sendToParent('uploadFile', { file });
193
+ if (response && response.error) {
194
+ return { fileId: '' };
195
+ }
196
+ return response || { fileId: '' };
138
197
  }
139
198
  };
140
199
 
@@ -199,6 +258,38 @@ export function UIResourceWidget({ uiResource, theme }: { uiResource: UIResource
199
258
  }
200
259
 
201
260
  export default function ToolCallDisplay({ toolCall, toolResult, isPending, uiResource, hideUiResource, autoApproveReason }: ToolCallDisplayProps) {
261
+ useEffect(() => {
262
+ async function handleMessage(event: MessageEvent) {
263
+ const data = event.data || {};
264
+ if (data.source !== TOOL_UI_MESSAGE_SOURCE || !data.id) return;
265
+
266
+ const targetOrigin = event.origin && event.origin !== 'null' ? event.origin : '*';
267
+
268
+ try {
269
+ const handler = (window as unknown as { chaaskitToolUiHandler?: (payload: unknown) => Promise<unknown> }).chaaskitToolUiHandler;
270
+ const result = typeof handler === 'function'
271
+ ? await handler({ type: data.type, payload: data.payload })
272
+ : { error: 'Tool UI bridge not configured' };
273
+
274
+ const sourceWindow = event.source as Window | null;
275
+ sourceWindow?.postMessage(
276
+ { source: TOOL_UI_MESSAGE_SOURCE, id: data.id, ok: true, result },
277
+ targetOrigin
278
+ );
279
+ } catch (err) {
280
+ const message = err instanceof Error ? err.message : 'Tool UI bridge error';
281
+ const sourceWindow = event.source as Window | null;
282
+ sourceWindow?.postMessage(
283
+ { source: TOOL_UI_MESSAGE_SOURCE, id: data.id, ok: false, error: message },
284
+ targetOrigin
285
+ );
286
+ }
287
+ }
288
+
289
+ window.addEventListener('message', handleMessage);
290
+ return () => window.removeEventListener('message', handleMessage);
291
+ }, []);
292
+
202
293
  // Check if we have HTML content to render
203
294
  const hasHtmlResource = !hideUiResource && uiResource?.text &&
204
295
  (uiResource.mimeType?.includes('html') || uiResource.text.trim().startsWith('<'));
@@ -0,0 +1,44 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface DocumentSummary {
5
+ id?: string;
6
+ path?: string;
7
+ name?: string;
8
+ mimeType?: string;
9
+ charCount?: number;
10
+ teamId?: string | null;
11
+ projectId?: string | null;
12
+ }
13
+
14
+ export default function DocumentListRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
15
+ const structured = toolResult.structuredContent as { documents?: DocumentSummary[] } | undefined;
16
+ const documents = structured?.documents ?? [];
17
+ const fallback = getTextContent(toolResult.content);
18
+
19
+ return (
20
+ <div className="space-y-2">
21
+ <div className="text-sm font-semibold text-text-primary">Documents</div>
22
+ {documents.length > 0 ? (
23
+ <ul className="space-y-2">
24
+ {documents.map((doc, index) => (
25
+ <li key={doc.id ?? doc.path ?? index} className="rounded-md border border-border bg-background px-3 py-2">
26
+ <div className="text-sm font-medium text-text-primary">{doc.path ?? doc.name ?? 'Untitled document'}</div>
27
+ <div className="text-xs text-text-muted">
28
+ {doc.mimeType ?? 'text/plain'}
29
+ {typeof doc.charCount === 'number' ? ` • ${doc.charCount} chars` : ''}
30
+ {doc.teamId ? ' • team' : ''}
31
+ {doc.projectId ? ' • project' : ''}
32
+ </div>
33
+ </li>
34
+ ))}
35
+ </ul>
36
+ ) : (
37
+ <div className="text-sm text-text-secondary">
38
+ {fallback ?? 'No documents found.'}
39
+ </div>
40
+ )}
41
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface ReadSummary {
5
+ path?: string;
6
+ offset?: number;
7
+ linesReturned?: number;
8
+ totalLines?: number;
9
+ truncated?: boolean;
10
+ }
11
+
12
+ export default function DocumentReadRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
13
+ const structured = toolResult.structuredContent as ReadSummary | undefined;
14
+ const fallback = getTextContent(toolResult.content);
15
+ const rangeText = structured
16
+ ? `Lines ${Number(structured.offset ?? 0) + 1}-${Number(structured.offset ?? 0) + Number(structured.linesReturned ?? 0)} of ${structured.totalLines ?? 'unknown'}`
17
+ : null;
18
+
19
+ return (
20
+ <div className="space-y-2">
21
+ <div className="text-sm font-semibold text-text-primary">Document Preview</div>
22
+ {structured ? (
23
+ <div className="rounded-md border border-border bg-background px-3 py-2 text-sm text-text-secondary">
24
+ <div className="font-medium text-text-primary">{structured.path ?? 'Document'}</div>
25
+ {rangeText && <div className="text-xs text-text-muted">{rangeText}{structured.truncated ? ' (truncated)' : ''}</div>}
26
+ </div>
27
+ ) : (
28
+ <div className="text-sm text-text-secondary">{fallback ?? 'No structured document details.'}</div>
29
+ )}
30
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,32 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface SaveSummary {
5
+ success?: boolean;
6
+ path?: string;
7
+ id?: string;
8
+ charCount?: number;
9
+ }
10
+
11
+ export default function DocumentSaveRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
12
+ const structured = toolResult.structuredContent as SaveSummary | undefined;
13
+ const fallback = getTextContent(toolResult.content);
14
+ const success = structured?.success !== false && !toolResult.isError;
15
+
16
+ return (
17
+ <div className="space-y-2">
18
+ <div className="text-sm font-semibold text-text-primary">Save Document</div>
19
+ {structured ? (
20
+ <div className={`rounded-md border px-3 py-2 text-sm ${success ? 'border-success/30 bg-success/10 text-success' : 'border-error/30 bg-error/10 text-error'}`}>
21
+ <div className="font-medium">{success ? 'Saved' : 'Failed'}</div>
22
+ {structured.path && <div className="text-xs">Path: {structured.path}</div>}
23
+ {structured.id && <div className="text-xs">ID: {structured.id}</div>}
24
+ {typeof structured.charCount === 'number' && <div className="text-xs">Size: {structured.charCount} chars</div>}
25
+ </div>
26
+ ) : (
27
+ <div className="text-sm text-text-secondary">{fallback ?? 'No structured save data.'}</div>
28
+ )}
29
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
30
+ </div>
31
+ );
32
+ }
@@ -0,0 +1,33 @@
1
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
+ import { getTextContent } from './utils';
3
+
4
+ interface SearchSummary {
5
+ path?: string;
6
+ query?: string;
7
+ matchCount?: number;
8
+ matchLines?: number[];
9
+ }
10
+
11
+ export default function DocumentSearchRenderer({ toolCall, toolResult }: { toolCall: ToolCall; toolResult: ToolResult }) {
12
+ const structured = toolResult.structuredContent as SearchSummary | undefined;
13
+ const fallback = getTextContent(toolResult.content);
14
+
15
+ return (
16
+ <div className="space-y-2">
17
+ <div className="text-sm font-semibold text-text-primary">Document Search</div>
18
+ {structured ? (
19
+ <div className="rounded-md border border-border bg-background px-3 py-2 text-sm text-text-secondary">
20
+ <div className="font-medium text-text-primary">{structured.path ?? 'Document'}</div>
21
+ <div className="text-xs text-text-muted">Query: {structured.query ?? 'unknown'}</div>
22
+ <div className="text-xs text-text-muted">Matches: {structured.matchCount ?? 0}</div>
23
+ {structured.matchLines && structured.matchLines.length > 0 && (
24
+ <div className="text-xs text-text-muted">Lines: {structured.matchLines.join(', ')}</div>
25
+ )}
26
+ </div>
27
+ ) : (
28
+ <div className="text-sm text-text-secondary">{fallback ?? 'No structured search data.'}</div>
29
+ )}
30
+ <div className="text-xs text-text-muted">Tool: {toolCall.toolName}</div>
31
+ </div>
32
+ );
33
+ }
@@ -0,0 +1,36 @@
1
+ import { clientRegistry } from '../../extensions/registry';
2
+ import DocumentListRenderer from './DocumentListRenderer';
3
+ import DocumentReadRenderer from './DocumentReadRenderer';
4
+ import DocumentSearchRenderer from './DocumentSearchRenderer';
5
+ import DocumentSaveRenderer from './DocumentSaveRenderer';
6
+
7
+ clientRegistry.registerTool({
8
+ name: 'list_documents',
9
+ description: 'Render document list results',
10
+ resultRenderer: DocumentListRenderer,
11
+ });
12
+
13
+ clientRegistry.registerTool({
14
+ name: 'read_document',
15
+ description: 'Render document read results',
16
+ resultRenderer: DocumentReadRenderer,
17
+ });
18
+
19
+ clientRegistry.registerTool({
20
+ name: 'search_in_document',
21
+ description: 'Render document search results',
22
+ resultRenderer: DocumentSearchRenderer,
23
+ });
24
+
25
+ clientRegistry.registerTool({
26
+ name: 'save_document',
27
+ description: 'Render document save results',
28
+ resultRenderer: DocumentSaveRenderer,
29
+ });
30
+
31
+ export {
32
+ DocumentListRenderer,
33
+ DocumentReadRenderer,
34
+ DocumentSearchRenderer,
35
+ DocumentSaveRenderer,
36
+ };
@@ -0,0 +1,7 @@
1
+ import type { MCPContent } from '@chaaskit/shared';
2
+
3
+ export function getTextContent(content?: MCPContent[]): string | null {
4
+ if (!content) return null;
5
+ const firstText = content.find((item) => item.type === 'text' && typeof item.text === 'string');
6
+ return firstText?.text ?? null;
7
+ }
@@ -20,9 +20,14 @@ interface AuthContextType {
20
20
  user: UserSession | null;
21
21
  isLoading: boolean;
22
22
  login: (email: string, password: string) => Promise<LoginResult>;
23
- register: (email: string, password: string, name?: string) => Promise<RegisterResult>;
23
+ register: (
24
+ email: string,
25
+ password: string,
26
+ name?: string,
27
+ options?: { inviteToken?: string; referralCode?: string }
28
+ ) => Promise<RegisterResult>;
24
29
  logout: () => Promise<void>;
25
- sendMagicLink: (email: string) => Promise<void>;
30
+ sendMagicLink: (email: string, inviteToken?: string) => Promise<void>;
26
31
  verifyEmail: (code: string) => Promise<void>;
27
32
  resendVerification: () => Promise<void>;
28
33
  }
@@ -59,12 +64,17 @@ export function AuthProvider({ children }: { children: ReactNode }) {
59
64
  return { requiresVerification: response.requiresVerification ?? false };
60
65
  }
61
66
 
62
- async function register(email: string, password: string, name?: string): Promise<RegisterResult> {
67
+ async function register(
68
+ email: string,
69
+ password: string,
70
+ name?: string,
71
+ options?: { inviteToken?: string; referralCode?: string }
72
+ ): Promise<RegisterResult> {
63
73
  const response = await api.post<{
64
74
  user: UserSession;
65
75
  token: string;
66
76
  requiresVerification?: boolean;
67
- }>('/api/auth/register', { email, password, name });
77
+ }>('/api/auth/register', { email, password, name, ...options });
68
78
  setUser(response.user);
69
79
  return { requiresVerification: response.requiresVerification ?? false };
70
80
  }
@@ -74,8 +84,8 @@ export function AuthProvider({ children }: { children: ReactNode }) {
74
84
  setUser(null);
75
85
  }
76
86
 
77
- async function sendMagicLink(email: string) {
78
- await api.post('/api/auth/magic-link', { email });
87
+ async function sendMagicLink(email: string, inviteToken?: string) {
88
+ await api.post('/api/auth/magic-link', { email, inviteToken });
79
89
  }
80
90
 
81
91
  async function verifyEmail(code: string) {
@@ -1,10 +1,10 @@
1
1
  import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
2
- import type { AppConfig } from '@chaaskit/shared';
2
+ import type { PublicAppConfig } from '@chaaskit/shared';
3
3
 
4
4
  // Declare global window property for SSR-injected config
5
5
  declare global {
6
6
  interface Window {
7
- __CHAASKIT_CONFIG__?: AppConfig;
7
+ __CHAASKIT_CONFIG__?: PublicAppConfig;
8
8
  }
9
9
  }
10
10
 
@@ -13,7 +13,7 @@ declare global {
13
13
  * This allows the config to be available immediately on page load,
14
14
  * avoiding flash of default values.
15
15
  */
16
- function getInjectedConfig(): AppConfig | undefined {
16
+ function getInjectedConfig(): PublicAppConfig | undefined {
17
17
  if (typeof window !== 'undefined' && window.__CHAASKIT_CONFIG__) {
18
18
  return window.__CHAASKIT_CONFIG__;
19
19
  }
@@ -21,11 +21,12 @@ function getInjectedConfig(): AppConfig | undefined {
21
21
  }
22
22
 
23
23
  // Default config - used as fallback while loading
24
- const defaultConfig: AppConfig = {
24
+ const defaultConfig: PublicAppConfig = {
25
25
  app: {
26
26
  name: 'AI Chat',
27
27
  description: 'Your AI assistant',
28
28
  url: 'http://localhost:5173',
29
+ basePath: '/chat',
29
30
  },
30
31
  ui: {
31
32
  welcomeTitle: 'Welcome to AI Chat',
@@ -109,27 +110,15 @@ const defaultConfig: AppConfig = {
109
110
  enabled: true,
110
111
  expiresInMinutes: 15,
111
112
  },
112
- },
113
- agent: {
114
- type: 'built-in',
115
- provider: 'anthropic',
116
- model: 'claude-sonnet-4-20250514',
117
- systemPrompt: 'You are a helpful AI assistant.',
118
- maxTokens: 4096,
113
+ gating: {
114
+ mode: 'open',
115
+ waitlistEnabled: false,
116
+ },
117
+ isAdmin: false,
119
118
  },
120
119
  payments: {
121
120
  enabled: false,
122
121
  provider: 'stripe',
123
- plans: [
124
- {
125
- id: 'free',
126
- name: 'Free',
127
- type: 'free',
128
- params: {
129
- monthlyMessageLimit: 20,
130
- },
131
- },
132
- ],
133
122
  },
134
123
  legal: {
135
124
  privacyPolicyUrl: '/privacy',
@@ -158,7 +147,6 @@ const defaultConfig: AppConfig = {
158
147
  },
159
148
  promptTemplates: {
160
149
  enabled: true,
161
- builtIn: [],
162
150
  allowUserTemplates: true,
163
151
  },
164
152
  teams: {
@@ -166,9 +154,6 @@ const defaultConfig: AppConfig = {
166
154
  },
167
155
  documents: {
168
156
  enabled: false,
169
- storage: {
170
- provider: 'database',
171
- },
172
157
  maxFileSizeMB: 10,
173
158
  hybridThreshold: 1000,
174
159
  acceptedTypes: ['text/plain', 'text/markdown', 'application/json'],
@@ -180,10 +165,19 @@ const defaultConfig: AppConfig = {
180
165
  api: {
181
166
  enabled: false,
182
167
  },
168
+ credits: {
169
+ enabled: false,
170
+ expiryEnabled: false,
171
+ promoEnabled: false,
172
+ },
173
+ metering: {
174
+ enabled: false,
175
+ recordPromptCompletion: true,
176
+ },
183
177
  };
184
178
 
185
179
  interface ConfigContextValue {
186
- config: AppConfig;
180
+ config: PublicAppConfig;
187
181
  configLoaded: boolean;
188
182
  }
189
183
 
@@ -199,7 +193,7 @@ interface ConfigProviderProps {
199
193
  * If provided, the config will not be fetched from /api/config.
200
194
  * Useful when config is available from SSR loaders.
201
195
  */
202
- initialConfig?: AppConfig;
196
+ initialConfig?: PublicAppConfig;
203
197
  }
204
198
 
205
199
  export function ConfigProvider({ children, initialConfig }: ConfigProviderProps) {
@@ -207,7 +201,7 @@ export function ConfigProvider({ children, initialConfig }: ConfigProviderProps)
207
201
  const injectedConfig = getInjectedConfig();
208
202
  const preloadedConfig = initialConfig || injectedConfig;
209
203
 
210
- const [config, setConfig] = useState<AppConfig>(
204
+ const [config, setConfig] = useState<PublicAppConfig>(
211
205
  preloadedConfig ? { ...defaultConfig, ...preloadedConfig } : defaultConfig
212
206
  );
213
207
  const [configLoaded, setConfigLoaded] = useState(!!preloadedConfig);
@@ -1,4 +1,5 @@
1
1
  import type { ComponentType } from 'react';
2
+ import type { ToolCall, ToolResult } from '@chaaskit/shared';
2
3
 
3
4
  /**
4
5
  * Page extension configuration
@@ -31,7 +32,7 @@ export interface ToolExtension {
31
32
  /** Description of the tool */
32
33
  description: string;
33
34
  /** Custom renderer for tool results */
34
- resultRenderer?: ComponentType<{ result: unknown }>;
35
+ resultRenderer?: ComponentType<{ toolCall: ToolCall; toolResult: ToolResult }>;
35
36
  }
36
37
 
37
38
  /**