@castlekit/castle 0.1.5 → 0.3.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 (68) hide show
  1. package/drizzle.config.ts +7 -0
  2. package/next.config.ts +1 -0
  3. package/package.json +25 -4
  4. package/src/app/api/avatars/[id]/route.ts +122 -25
  5. package/src/app/api/openclaw/agents/[id]/avatar/route.ts +216 -0
  6. package/src/app/api/openclaw/agents/route.ts +77 -41
  7. package/src/app/api/openclaw/agents/status/route.ts +55 -0
  8. package/src/app/api/openclaw/chat/attachments/route.ts +230 -0
  9. package/src/app/api/openclaw/chat/channels/route.ts +214 -0
  10. package/src/app/api/openclaw/chat/route.ts +272 -0
  11. package/src/app/api/openclaw/chat/search/route.ts +149 -0
  12. package/src/app/api/openclaw/chat/storage/route.ts +75 -0
  13. package/src/app/api/openclaw/config/route.ts +45 -4
  14. package/src/app/api/openclaw/events/route.ts +31 -2
  15. package/src/app/api/openclaw/logs/route.ts +20 -5
  16. package/src/app/api/openclaw/restart/route.ts +12 -4
  17. package/src/app/api/openclaw/session/status/route.ts +42 -0
  18. package/src/app/api/settings/avatar/route.ts +190 -0
  19. package/src/app/api/settings/route.ts +88 -0
  20. package/src/app/chat/[channelId]/error-boundary.tsx +64 -0
  21. package/src/app/chat/[channelId]/page.tsx +305 -0
  22. package/src/app/chat/layout.tsx +96 -0
  23. package/src/app/chat/page.tsx +52 -0
  24. package/src/app/globals.css +89 -2
  25. package/src/app/layout.tsx +7 -1
  26. package/src/app/page.tsx +147 -28
  27. package/src/app/settings/page.tsx +300 -0
  28. package/src/cli/onboarding.ts +202 -37
  29. package/src/components/chat/agent-mention-popup.tsx +89 -0
  30. package/src/components/chat/archived-channels.tsx +190 -0
  31. package/src/components/chat/channel-list.tsx +140 -0
  32. package/src/components/chat/chat-input.tsx +310 -0
  33. package/src/components/chat/create-channel-dialog.tsx +171 -0
  34. package/src/components/chat/markdown-content.tsx +205 -0
  35. package/src/components/chat/message-bubble.tsx +152 -0
  36. package/src/components/chat/message-list.tsx +508 -0
  37. package/src/components/chat/message-queue.tsx +68 -0
  38. package/src/components/chat/session-divider.tsx +61 -0
  39. package/src/components/chat/session-stats-panel.tsx +139 -0
  40. package/src/components/chat/storage-indicator.tsx +76 -0
  41. package/src/components/layout/sidebar.tsx +126 -45
  42. package/src/components/layout/user-menu.tsx +29 -4
  43. package/src/components/providers/presence-provider.tsx +8 -0
  44. package/src/components/providers/search-provider.tsx +81 -0
  45. package/src/components/search/search-dialog.tsx +269 -0
  46. package/src/components/ui/avatar.tsx +11 -9
  47. package/src/components/ui/dialog.tsx +10 -4
  48. package/src/components/ui/tooltip.tsx +25 -8
  49. package/src/components/ui/twemoji-text.tsx +37 -0
  50. package/src/lib/api-security.ts +188 -0
  51. package/src/lib/config.ts +36 -4
  52. package/src/lib/date-utils.ts +79 -0
  53. package/src/lib/db/__tests__/queries.test.ts +318 -0
  54. package/src/lib/db/index.ts +642 -0
  55. package/src/lib/db/queries.ts +1017 -0
  56. package/src/lib/db/schema.ts +160 -0
  57. package/src/lib/device-identity.ts +303 -0
  58. package/src/lib/gateway-connection.ts +273 -36
  59. package/src/lib/hooks/use-agent-status.ts +251 -0
  60. package/src/lib/hooks/use-chat.ts +775 -0
  61. package/src/lib/hooks/use-openclaw.ts +105 -70
  62. package/src/lib/hooks/use-search.ts +113 -0
  63. package/src/lib/hooks/use-session-stats.ts +57 -0
  64. package/src/lib/hooks/use-user-settings.ts +46 -0
  65. package/src/lib/types/chat.ts +186 -0
  66. package/src/lib/types/search.ts +60 -0
  67. package/src/middleware.ts +52 -0
  68. package/vitest.config.ts +13 -0
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useCallback, useRef } from "react";
3
+ import { useEffect, useRef, useCallback } from "react";
4
+ import useSWR from "swr";
4
5
 
5
6
  // ============================================================================
6
7
  // Types
@@ -32,74 +33,89 @@ interface GatewaySSEEvent {
32
33
  seq?: number;
33
34
  }
34
35
 
36
+ // ============================================================================
37
+ // Fetchers
38
+ // ============================================================================
39
+
40
+ const statusFetcher = async (url: string): Promise<OpenClawStatus> => {
41
+ const res = await fetch(url, { method: "POST" });
42
+ return res.json();
43
+ };
44
+
45
+ const agentsFetcher = async (url: string): Promise<OpenClawAgent[]> => {
46
+ const res = await fetch(url);
47
+ if (!res.ok) return [];
48
+ const data = await res.json();
49
+ return data.agents || [];
50
+ };
51
+
35
52
  // ============================================================================
36
53
  // Hook
37
54
  // ============================================================================
38
55
 
56
+ /**
57
+ * Shared OpenClaw connection status and agents hook.
58
+ * Uses SWR for cache-based sharing across all components —
59
+ * any component calling useOpenClaw() gets the same cached data without extra fetches.
60
+ *
61
+ * SSE subscription pushes real-time updates that trigger SWR cache invalidation.
62
+ */
39
63
  export function useOpenClaw() {
40
- const [status, setStatus] = useState<OpenClawStatus | null>(null);
41
- const [agents, setAgents] = useState<OpenClawAgent[]>([]);
42
- const [isLoading, setIsLoading] = useState(true);
43
- const [agentsLoading, setAgentsLoading] = useState(true);
44
64
  const eventSourceRef = useRef<EventSource | null>(null);
45
65
 
46
- // Fetch connection status
47
- const fetchStatus = useCallback(async () => {
48
- try {
49
- const res = await fetch("/api/openclaw/ping", { method: "POST" });
50
- const data: OpenClawStatus = await res.json();
51
- setStatus(data);
52
- return data;
53
- } catch {
54
- setStatus({ ok: false, configured: false, error: "Failed to reach Castle server" });
55
- return null;
56
- } finally {
57
- setIsLoading(false);
66
+ // ---------------------------------------------------------------------------
67
+ // SWR: Connection status
68
+ // ---------------------------------------------------------------------------
69
+ const {
70
+ data: status,
71
+ error: statusError,
72
+ isLoading: statusLoading,
73
+ mutate: mutateStatus,
74
+ } = useSWR<OpenClawStatus>(
75
+ "/api/openclaw/ping",
76
+ statusFetcher,
77
+ {
78
+ refreshInterval: 60000, // Background refresh every 60s
79
+ revalidateOnFocus: true,
80
+ dedupingInterval: 10000, // Dedup rapid calls within 10s
81
+ errorRetryCount: 2,
58
82
  }
59
- }, []);
60
-
61
- // Fetch agents
62
- const fetchAgents = useCallback(async () => {
63
- try {
64
- setAgentsLoading(true);
65
- const res = await fetch("/api/openclaw/agents");
66
- if (!res.ok) {
67
- setAgents([]);
68
- return;
69
- }
70
- const data = await res.json();
71
- setAgents(data.agents || []);
72
- } catch {
73
- setAgents([]);
74
- } finally {
75
- setAgentsLoading(false);
83
+ );
84
+
85
+ const isConnected = status?.ok ?? false;
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // SWR: Agents (conditional — only fetch when connected)
89
+ // ---------------------------------------------------------------------------
90
+ const {
91
+ data: agents,
92
+ isLoading: agentsLoading,
93
+ mutate: mutateAgents,
94
+ } = useSWR<OpenClawAgent[]>(
95
+ isConnected ? "/api/openclaw/agents" : null,
96
+ agentsFetcher,
97
+ {
98
+ refreshInterval: 300000, // Refresh agents every 5 min
99
+ revalidateOnFocus: false,
100
+ dedupingInterval: 30000, // Dedup within 30s
76
101
  }
77
- }, []);
102
+ );
78
103
 
79
- // Refresh everything
104
+ // ---------------------------------------------------------------------------
105
+ // Refresh helpers
106
+ // ---------------------------------------------------------------------------
80
107
  const refresh = useCallback(async () => {
81
- const newStatus = await fetchStatus();
82
- if (newStatus?.ok) {
83
- await fetchAgents();
108
+ await mutateStatus();
109
+ if (isConnected) {
110
+ await mutateAgents();
84
111
  }
85
- }, [fetchStatus, fetchAgents]);
112
+ }, [mutateStatus, mutateAgents, isConnected]);
86
113
 
87
- // Initial data fetch
88
- useEffect(() => {
89
- let cancelled = false;
90
-
91
- async function init() {
92
- const s = await fetchStatus();
93
- if (!cancelled && s?.ok) {
94
- await fetchAgents();
95
- }
96
- }
97
-
98
- init();
99
- return () => { cancelled = true; };
100
- }, [fetchStatus, fetchAgents]);
114
+ const refreshAgents = useCallback(() => mutateAgents(), [mutateAgents]);
101
115
 
116
+ // ---------------------------------------------------------------------------
102
117
  // SSE subscription for real-time events
118
+ // ---------------------------------------------------------------------------
103
119
  useEffect(() => {
104
120
  const es = new EventSource("/api/openclaw/events");
105
121
  eventSourceRef.current = es;
@@ -108,25 +124,39 @@ export function useOpenClaw() {
108
124
  try {
109
125
  const evt: GatewaySSEEvent = JSON.parse(e.data);
110
126
 
111
- // Handle Castle state changes
127
+ // Handle Castle state changes — update status via SWR
112
128
  if (evt.event === "castle.state") {
113
- const payload = evt.payload as { state: string; isConnected: boolean; server?: Record<string, unknown> };
114
- setStatus((prev) => ({
115
- ok: payload.isConnected,
116
- configured: prev?.configured ?? true,
117
- state: payload.state,
118
- server: payload.server as OpenClawStatus["server"],
119
- }));
129
+ const payload = evt.payload as {
130
+ state: string;
131
+ isConnected: boolean;
132
+ server?: Record<string, unknown>;
133
+ };
134
+
135
+ // Optimistically update the SWR cache with the SSE data
136
+ mutateStatus(
137
+ (prev) => ({
138
+ ok: payload.isConnected,
139
+ configured: prev?.configured ?? true,
140
+ state: payload.state,
141
+ server: payload.server as OpenClawStatus["server"],
142
+ }),
143
+ { revalidate: false }
144
+ );
120
145
 
121
146
  // Re-fetch agents when connection state changes to connected
122
147
  if (payload.isConnected) {
123
- fetchAgents();
148
+ mutateAgents();
124
149
  }
125
150
  }
126
151
 
127
- // Handle agent-related events -- re-fetch agent list
152
+ // Handle agent-related events re-fetch agent list
128
153
  if (evt.event?.startsWith("agent.")) {
129
- fetchAgents();
154
+ mutateAgents();
155
+ }
156
+
157
+ // Handle avatar update events
158
+ if (evt.event === "agentAvatarUpdated") {
159
+ mutateAgents();
130
160
  }
131
161
  } catch {
132
162
  // Ignore parse errors
@@ -135,29 +165,34 @@ export function useOpenClaw() {
135
165
 
136
166
  es.onerror = () => {
137
167
  // EventSource auto-reconnects, but update state
138
- setStatus((prev) => prev ? { ...prev, ok: false, state: "disconnected" } : prev);
168
+ mutateStatus(
169
+ (prev) => prev ? { ...prev, ok: false, state: "disconnected" } : prev,
170
+ { revalidate: false }
171
+ );
139
172
  };
140
173
 
141
174
  return () => {
142
175
  es.close();
143
176
  eventSourceRef.current = null;
144
177
  };
145
- }, [fetchAgents]);
178
+ }, [mutateStatus, mutateAgents]);
146
179
 
147
180
  return {
148
181
  // Status
149
182
  status,
150
- isLoading,
151
- isConnected: status?.ok ?? false,
183
+ isLoading: statusLoading,
184
+ isError: !!statusError,
185
+ isConnected,
152
186
  isConfigured: status?.configured ?? false,
153
187
  latency: status?.latency_ms,
154
188
  serverVersion: status?.server?.version,
155
189
 
156
190
  // Agents
157
- agents,
191
+ agents: agents ?? [],
158
192
  agentsLoading,
159
193
 
160
194
  // Actions
161
195
  refresh,
196
+ refreshAgents,
162
197
  };
163
198
  }
@@ -0,0 +1,113 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef, useEffect } from "react";
4
+ import type { SearchResult } from "@/lib/types/search";
5
+
6
+ // ============================================================================
7
+ // Constants
8
+ // ============================================================================
9
+
10
+ const DEBOUNCE_MS = 300;
11
+
12
+ // ============================================================================
13
+ // Hook
14
+ // ============================================================================
15
+
16
+ export function useSearch() {
17
+ const [query, setQuery] = useState("");
18
+ const [results, setResults] = useState<SearchResult[]>([]);
19
+ const [isSearching, setIsSearching] = useState(false);
20
+ const [recentSearches, setRecentSearches] = useState<string[]>([]);
21
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
22
+ const abortRef = useRef<AbortController | null>(null);
23
+ const recentLoaded = useRef(false);
24
+
25
+ // Load recent searches from DB on first mount
26
+ useEffect(() => {
27
+ if (recentLoaded.current) return;
28
+ recentLoaded.current = true;
29
+ fetch("/api/openclaw/chat/search?recent=1")
30
+ .then((res) => (res.ok ? res.json() : { recent: [] }))
31
+ .then((data) => setRecentSearches(data.recent ?? []))
32
+ .catch(() => {});
33
+ }, []);
34
+
35
+ // Debounced search
36
+ const search = useCallback((q: string) => {
37
+ setQuery(q);
38
+
39
+ // Cancel pending request
40
+ if (timerRef.current) clearTimeout(timerRef.current);
41
+ if (abortRef.current) abortRef.current.abort();
42
+
43
+ if (!q.trim()) {
44
+ setResults([]);
45
+ setIsSearching(false);
46
+ return;
47
+ }
48
+
49
+ setIsSearching(true);
50
+ timerRef.current = setTimeout(async () => {
51
+ const controller = new AbortController();
52
+ abortRef.current = controller;
53
+
54
+ try {
55
+ const res = await fetch(
56
+ `/api/openclaw/chat/search?q=${encodeURIComponent(q.trim())}`,
57
+ { signal: controller.signal }
58
+ );
59
+ if (res.ok) {
60
+ const data = await res.json();
61
+ const searchResults: SearchResult[] = data.results ?? [];
62
+ setResults(searchResults);
63
+
64
+ // Save to recent searches in DB if results found
65
+ if (searchResults.length > 0) {
66
+ const trimmed = q.trim();
67
+ fetch("/api/openclaw/chat/search", {
68
+ method: "POST",
69
+ headers: { "Content-Type": "application/json" },
70
+ body: JSON.stringify({ query: trimmed }),
71
+ })
72
+ .then(() => {
73
+ // Update local state to reflect the save
74
+ setRecentSearches((prev) => {
75
+ const filtered = prev.filter((s) => s !== trimmed);
76
+ return [trimmed, ...filtered].slice(0, 15);
77
+ });
78
+ })
79
+ .catch(() => {});
80
+ }
81
+ }
82
+ } catch (err) {
83
+ if ((err as Error).name !== "AbortError") {
84
+ setResults([]);
85
+ }
86
+ } finally {
87
+ setIsSearching(false);
88
+ }
89
+ }, DEBOUNCE_MS);
90
+ }, []);
91
+
92
+ const clearRecentSearches = useCallback(() => {
93
+ setRecentSearches([]);
94
+ fetch("/api/openclaw/chat/search", { method: "DELETE" }).catch(() => {});
95
+ }, []);
96
+
97
+ // Cleanup on unmount
98
+ useEffect(() => {
99
+ return () => {
100
+ if (timerRef.current) clearTimeout(timerRef.current);
101
+ if (abortRef.current) abortRef.current.abort();
102
+ };
103
+ }, []);
104
+
105
+ return {
106
+ query,
107
+ setQuery: search,
108
+ results,
109
+ isSearching,
110
+ recentSearches,
111
+ clearRecentSearches,
112
+ };
113
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { useCallback } from "react";
4
+ import useSWR from "swr";
5
+ import type { SessionStatus } from "@/lib/types/chat";
6
+
7
+ const fetcher = async (url: string): Promise<SessionStatus | null> => {
8
+ const res = await fetch(url);
9
+ if (res.status === 204) return null;
10
+ if (!res.ok) throw new Error("Failed to fetch session stats");
11
+ return res.json();
12
+ };
13
+
14
+ interface UseSessionStatsOptions {
15
+ sessionKey: string | null;
16
+ }
17
+
18
+ interface UseSessionStatsReturn {
19
+ stats: SessionStatus | null;
20
+ isLoading: boolean;
21
+ isError: boolean;
22
+ refresh: () => void;
23
+ }
24
+
25
+ /**
26
+ * SWR-based hook for fetching session statistics from the Gateway.
27
+ * Conditional fetching: only when sessionKey is non-null.
28
+ * Polls every 30s while active, dedupes within 5s.
29
+ */
30
+ export function useSessionStats({ sessionKey }: UseSessionStatsOptions): UseSessionStatsReturn {
31
+ const {
32
+ data,
33
+ isLoading,
34
+ error,
35
+ mutate,
36
+ } = useSWR<SessionStatus | null>(
37
+ sessionKey ? `/api/openclaw/session/status?sessionKey=${sessionKey}` : null,
38
+ fetcher,
39
+ {
40
+ refreshInterval: 30000,
41
+ dedupingInterval: 5000,
42
+ revalidateOnFocus: false,
43
+ errorRetryCount: 1,
44
+ }
45
+ );
46
+
47
+ const refresh = useCallback(() => {
48
+ mutate();
49
+ }, [mutate]);
50
+
51
+ return {
52
+ stats: data ?? null,
53
+ isLoading,
54
+ isError: !!error,
55
+ refresh,
56
+ };
57
+ }
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import useSWR from "swr";
4
+
5
+ const SETTINGS_KEY = "/api/settings";
6
+
7
+ const fetcher = (url: string) => fetch(url).then((r) => r.json());
8
+
9
+ interface UserSettings {
10
+ displayName?: string;
11
+ avatarPath?: string;
12
+ tooltips?: string;
13
+ }
14
+
15
+ /**
16
+ * Shared hook for user settings (display name, avatar, etc).
17
+ *
18
+ * All consumers share the same SWR cache. Call `refresh()` after
19
+ * updating settings to instantly propagate changes everywhere
20
+ * (user menu, chat headers, message avatars).
21
+ */
22
+ export function useUserSettings() {
23
+ const { data, mutate, isLoading } = useSWR<UserSettings>(
24
+ SETTINGS_KEY,
25
+ fetcher,
26
+ {
27
+ revalidateOnFocus: true,
28
+ dedupingInterval: 5000,
29
+ }
30
+ );
31
+
32
+ const displayName = data?.displayName || "";
33
+ const avatarUrl = data?.avatarPath
34
+ ? `/api/settings/avatar?v=${encodeURIComponent(data.avatarPath)}`
35
+ : null;
36
+ const tooltips = data?.tooltips !== "false"; // default true
37
+
38
+ return {
39
+ displayName,
40
+ avatarUrl,
41
+ tooltips,
42
+ isLoading,
43
+ /** Call after saving settings to refresh all consumers */
44
+ refresh: () => mutate(),
45
+ };
46
+ }
@@ -0,0 +1,186 @@
1
+ // ============================================================================
2
+ // Channel
3
+ // ============================================================================
4
+
5
+ export interface Channel {
6
+ id: string;
7
+ name: string;
8
+ defaultAgentId: string;
9
+ agents: string[]; // Agent IDs in this channel
10
+ createdAt: number; // unix ms
11
+ archivedAt?: number | null; // unix ms — null if active
12
+ }
13
+
14
+ // ============================================================================
15
+ // Session
16
+ // ============================================================================
17
+
18
+ export interface ChannelSession {
19
+ id: string;
20
+ channelId: string;
21
+ sessionKey: string | null; // Gateway session key
22
+ startedAt: number; // unix ms
23
+ endedAt: number | null; // unix ms
24
+ summary: string | null;
25
+ totalInputTokens: number;
26
+ totalOutputTokens: number;
27
+ }
28
+
29
+ // ============================================================================
30
+ // Message
31
+ // ============================================================================
32
+
33
+ export type MessageStatus = "complete" | "interrupted" | "aborted";
34
+
35
+ export interface ChatMessage {
36
+ id: string;
37
+ channelId: string;
38
+ sessionId: string | null;
39
+ senderType: "user" | "agent";
40
+ senderId: string;
41
+ senderName: string | null;
42
+ content: string;
43
+ status: MessageStatus;
44
+ mentionedAgentId: string | null;
45
+ runId: string | null; // Gateway run ID for streaming correlation
46
+ sessionKey: string | null; // Gateway session key
47
+ inputTokens: number | null;
48
+ outputTokens: number | null;
49
+ createdAt: number; // unix ms
50
+ attachments: MessageAttachment[];
51
+ reactions: MessageReaction[];
52
+ }
53
+
54
+ // ============================================================================
55
+ // Attachment
56
+ // ============================================================================
57
+
58
+ export interface MessageAttachment {
59
+ id: string;
60
+ messageId: string;
61
+ attachmentType: "image" | "audio";
62
+ filePath: string;
63
+ mimeType: string | null;
64
+ fileSize: number | null;
65
+ originalName: string | null;
66
+ createdAt: number; // unix ms
67
+ }
68
+
69
+ // ============================================================================
70
+ // Reaction
71
+ // ============================================================================
72
+
73
+ export interface MessageReaction {
74
+ id: string;
75
+ messageId: string;
76
+ agentId: string | null;
77
+ emoji: string;
78
+ emojiChar: string;
79
+ createdAt: number; // unix ms
80
+ }
81
+
82
+ // ============================================================================
83
+ // Streaming
84
+ // ============================================================================
85
+
86
+ /** A message currently being streamed from the Gateway */
87
+ export interface StreamingMessage {
88
+ runId: string;
89
+ agentId: string;
90
+ agentName: string;
91
+ sessionKey: string;
92
+ content: string; // Accumulated text so far
93
+ startedAt: number;
94
+ }
95
+
96
+ /** A single streaming delta from the Gateway SSE */
97
+ export interface ChatDelta {
98
+ runId: string;
99
+ sessionKey: string;
100
+ seq: number;
101
+ state: "delta" | "final" | "error";
102
+ text?: string;
103
+ errorMessage?: string;
104
+ message?: {
105
+ content?: Array<{ type?: string; text?: string }>;
106
+ role?: string;
107
+ inputTokens?: number;
108
+ outputTokens?: number;
109
+ };
110
+ }
111
+
112
+ // ============================================================================
113
+ // Message Queue
114
+ // ============================================================================
115
+
116
+ export interface QueuedMessage {
117
+ id: string; // Temp ID for tracking
118
+ content: string;
119
+ agentId?: string;
120
+ attachments?: File[];
121
+ addedAt: number;
122
+ }
123
+
124
+ // ============================================================================
125
+ // Session Status (from Gateway session.status RPC)
126
+ // ============================================================================
127
+
128
+ export interface SessionStatus {
129
+ sessionKey: string;
130
+ agentId: string;
131
+ model: string;
132
+ tokens: {
133
+ input: number;
134
+ output: number;
135
+ };
136
+ context: {
137
+ used: number;
138
+ limit: number;
139
+ percentage: number;
140
+ };
141
+ compactions: number;
142
+ runtime: string;
143
+ thinking: string;
144
+ updatedAt: number;
145
+ }
146
+
147
+ // ============================================================================
148
+ // Storage Stats
149
+ // ============================================================================
150
+
151
+ export interface StorageStats {
152
+ messages: number;
153
+ channels: number;
154
+ attachments: number;
155
+ totalAttachmentBytes: number;
156
+ dbSizeBytes?: number; // Size of castle.db file
157
+ }
158
+
159
+ // ============================================================================
160
+ // API Payloads
161
+ // ============================================================================
162
+
163
+ export interface ChatSendRequest {
164
+ channelId: string;
165
+ content: string;
166
+ agentId?: string;
167
+ attachmentIds?: string[];
168
+ }
169
+
170
+ export interface ChatSendResponse {
171
+ runId: string;
172
+ messageId: string;
173
+ sessionKey: string;
174
+ }
175
+
176
+ export interface ChatCompleteRequest {
177
+ runId: string;
178
+ channelId: string;
179
+ content: string;
180
+ sessionKey: string;
181
+ agentId: string;
182
+ agentName?: string;
183
+ status: MessageStatus;
184
+ inputTokens?: number;
185
+ outputTokens?: number;
186
+ }