freesail 0.0.1 → 0.1.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 (54) hide show
  1. package/README.md +190 -5
  2. package/docs/A2UX_Protocol.md +183 -0
  3. package/docs/Agents.md +218 -0
  4. package/docs/Architecture.md +285 -0
  5. package/docs/CatalogReference.md +377 -0
  6. package/docs/GettingStarted.md +230 -0
  7. package/examples/demo/package.json +21 -0
  8. package/examples/demo/public/index.html +381 -0
  9. package/examples/demo/server.js +253 -0
  10. package/package.json +38 -5
  11. package/packages/core/package.json +48 -0
  12. package/packages/core/src/functions.ts +403 -0
  13. package/packages/core/src/index.ts +214 -0
  14. package/packages/core/src/parser.ts +270 -0
  15. package/packages/core/src/protocol.ts +254 -0
  16. package/packages/core/src/store.ts +452 -0
  17. package/packages/core/src/transport.ts +439 -0
  18. package/packages/core/src/types.ts +209 -0
  19. package/packages/core/tsconfig.json +10 -0
  20. package/packages/lit-ui/package.json +44 -0
  21. package/packages/lit-ui/src/catalogs/standard/catalog.json +405 -0
  22. package/packages/lit-ui/src/catalogs/standard/elements/Badge.ts +96 -0
  23. package/packages/lit-ui/src/catalogs/standard/elements/Button.ts +147 -0
  24. package/packages/lit-ui/src/catalogs/standard/elements/Card.ts +78 -0
  25. package/packages/lit-ui/src/catalogs/standard/elements/Checkbox.ts +94 -0
  26. package/packages/lit-ui/src/catalogs/standard/elements/Column.ts +66 -0
  27. package/packages/lit-ui/src/catalogs/standard/elements/Divider.ts +59 -0
  28. package/packages/lit-ui/src/catalogs/standard/elements/Image.ts +54 -0
  29. package/packages/lit-ui/src/catalogs/standard/elements/Input.ts +125 -0
  30. package/packages/lit-ui/src/catalogs/standard/elements/Progress.ts +79 -0
  31. package/packages/lit-ui/src/catalogs/standard/elements/Row.ts +68 -0
  32. package/packages/lit-ui/src/catalogs/standard/elements/Select.ts +110 -0
  33. package/packages/lit-ui/src/catalogs/standard/elements/Spacer.ts +37 -0
  34. package/packages/lit-ui/src/catalogs/standard/elements/Spinner.ts +76 -0
  35. package/packages/lit-ui/src/catalogs/standard/elements/Text.ts +86 -0
  36. package/packages/lit-ui/src/catalogs/standard/elements/index.ts +18 -0
  37. package/packages/lit-ui/src/catalogs/standard/index.ts +17 -0
  38. package/packages/lit-ui/src/index.ts +84 -0
  39. package/packages/lit-ui/src/renderer.ts +211 -0
  40. package/packages/lit-ui/src/types.ts +49 -0
  41. package/packages/lit-ui/src/utils/define-props.ts +157 -0
  42. package/packages/lit-ui/src/utils/index.ts +2 -0
  43. package/packages/lit-ui/src/utils/registry.ts +139 -0
  44. package/packages/lit-ui/tsconfig.json +11 -0
  45. package/packages/server/package.json +61 -0
  46. package/packages/server/src/adapters/index.ts +5 -0
  47. package/packages/server/src/adapters/langchain.ts +175 -0
  48. package/packages/server/src/adapters/openai.ts +209 -0
  49. package/packages/server/src/catalog-loader.ts +311 -0
  50. package/packages/server/src/index.ts +142 -0
  51. package/packages/server/src/stream.ts +329 -0
  52. package/packages/server/tsconfig.json +11 -0
  53. package/tsconfig.base.json +23 -0
  54. package/index.js +0 -3
@@ -0,0 +1,209 @@
1
+ /**
2
+ * OpenAI Adapter
3
+ *
4
+ * Converts Freesail tools to OpenAI function calling format.
5
+ * This adapter allows using Freesail with the OpenAI API directly.
6
+ *
7
+ * NOTE: This adapter requires 'openai' as a peer dependency.
8
+ */
9
+
10
+ import type { FreesailStream } from '../stream.js';
11
+ import type { CatalogToToolConverter, OpenAITool } from '../catalog-loader.js';
12
+ import { toolCallToMessages, updateDataToMessage } from '../catalog-loader.js';
13
+
14
+ // =============================================================================
15
+ // Types
16
+ // =============================================================================
17
+
18
+ export interface OpenAIAdapterOptions {
19
+ /** The SSE stream to send messages to */
20
+ stream: FreesailStream;
21
+ /** Catalog ID being used */
22
+ catalogId: string;
23
+ /** Tool converter instance */
24
+ converter: CatalogToToolConverter;
25
+ }
26
+
27
+ export interface ToolCallResult {
28
+ success: boolean;
29
+ message: string;
30
+ surfaceId?: string;
31
+ }
32
+
33
+ // =============================================================================
34
+ // OpenAI Adapter
35
+ // =============================================================================
36
+
37
+ /**
38
+ * OpenAIAdapter handles tool calls from OpenAI and converts them
39
+ * to A2UX messages sent via the stream.
40
+ */
41
+ export class OpenAIAdapter {
42
+ private stream: FreesailStream;
43
+ private catalogId: string;
44
+ private converter: CatalogToToolConverter;
45
+
46
+ constructor(options: OpenAIAdapterOptions) {
47
+ this.stream = options.stream;
48
+ this.catalogId = options.catalogId;
49
+ this.converter = options.converter;
50
+ }
51
+
52
+ /**
53
+ * Get tools in OpenAI format
54
+ */
55
+ getTools(): OpenAITool[] {
56
+ return this.converter.generateOpenAITools(this.catalogId);
57
+ }
58
+
59
+ /**
60
+ * Handle a tool call from OpenAI
61
+ */
62
+ handleToolCall(
63
+ toolName: string,
64
+ args: Record<string, unknown>
65
+ ): ToolCallResult {
66
+ switch (toolName) {
67
+ case 'render_ui':
68
+ return this.handleRenderUI(args);
69
+ case 'update_data':
70
+ return this.handleUpdateData(args);
71
+ default:
72
+ return {
73
+ success: false,
74
+ message: `Unknown tool: ${toolName}`
75
+ };
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Handle render_ui tool call
81
+ */
82
+ private handleRenderUI(args: Record<string, unknown>): ToolCallResult {
83
+ const { surfaceId, components } = args as {
84
+ surfaceId: string;
85
+ components: Array<{ id: string; component: string; [key: string]: unknown }>;
86
+ };
87
+
88
+ if (!surfaceId || !components) {
89
+ return {
90
+ success: false,
91
+ message: 'Missing required parameters: surfaceId, components'
92
+ };
93
+ }
94
+
95
+ try {
96
+ const messages = toolCallToMessages({ surfaceId, components }, this.catalogId);
97
+
98
+ for (const message of messages) {
99
+ this.stream.send(message);
100
+ }
101
+
102
+ return {
103
+ success: true,
104
+ message: `UI surface "${surfaceId}" created with ${components.length} components.`,
105
+ surfaceId,
106
+ };
107
+ } catch (error) {
108
+ return {
109
+ success: false,
110
+ message: `Failed to render UI: ${error}`,
111
+ };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Handle update_data tool call
117
+ */
118
+ private handleUpdateData(args: Record<string, unknown>): ToolCallResult {
119
+ const { surfaceId, path, value, operation } = args as {
120
+ surfaceId: string;
121
+ path?: string;
122
+ value: unknown;
123
+ operation?: string;
124
+ };
125
+
126
+ if (!surfaceId || value === undefined) {
127
+ return {
128
+ success: false,
129
+ message: 'Missing required parameters: surfaceId, value',
130
+ };
131
+ }
132
+
133
+ try {
134
+ const message = updateDataToMessage({ surfaceId, path, value, operation });
135
+ this.stream.send(message);
136
+
137
+ return {
138
+ success: true,
139
+ message: `Data updated at path "${path || '/'}" in surface "${surfaceId}".`,
140
+ surfaceId,
141
+ };
142
+ } catch (error) {
143
+ return {
144
+ success: false,
145
+ message: `Failed to update data: ${error}`,
146
+ };
147
+ }
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Create an OpenAI adapter instance
153
+ */
154
+ export function createOpenAIAdapter(options: OpenAIAdapterOptions): OpenAIAdapter {
155
+ return new OpenAIAdapter(options);
156
+ }
157
+
158
+ // =============================================================================
159
+ // OpenAI Chat Completions Helper
160
+ // =============================================================================
161
+
162
+ /**
163
+ * Process OpenAI chat completion response and handle tool calls
164
+ *
165
+ * @example
166
+ * ```ts
167
+ * const response = await openai.chat.completions.create({
168
+ * model: 'gpt-4',
169
+ * messages,
170
+ * tools: adapter.getTools(),
171
+ * });
172
+ *
173
+ * const results = processOpenAIResponse(response, adapter);
174
+ * ```
175
+ */
176
+ export function processOpenAIResponse(
177
+ response: {
178
+ choices: Array<{
179
+ message: {
180
+ tool_calls?: Array<{
181
+ function: { name: string; arguments: string }
182
+ }>
183
+ }
184
+ }>
185
+ },
186
+ adapter: OpenAIAdapter
187
+ ): ToolCallResult[] {
188
+ const results: ToolCallResult[] = [];
189
+ const toolCalls = response.choices[0]?.message?.tool_calls;
190
+
191
+ if (!toolCalls) {
192
+ return results;
193
+ }
194
+
195
+ for (const toolCall of toolCalls) {
196
+ try {
197
+ const args = JSON.parse(toolCall.function.arguments);
198
+ const result = adapter.handleToolCall(toolCall.function.name, args);
199
+ results.push(result);
200
+ } catch (error) {
201
+ results.push({
202
+ success: false,
203
+ message: `Failed to parse tool call arguments: ${error}`,
204
+ });
205
+ }
206
+ }
207
+
208
+ return results;
209
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Catalog Loader & Tool Converter
3
+ *
4
+ * Converts catalog definitions to LLM-compatible tool schemas.
5
+ * This is the core of the "Context Injector" - generating
6
+ * JSON Schema tools from component catalogs.
7
+ */
8
+
9
+ import type { ServerToClientMessage, A2UXComponent } from '@freesail/core';
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ export interface CatalogComponent {
16
+ description?: string;
17
+ properties?: Record<string, PropertySchema>;
18
+ required?: string[];
19
+ }
20
+
21
+ export interface PropertySchema {
22
+ type: string;
23
+ description?: string;
24
+ enum?: string[];
25
+ default?: unknown;
26
+ items?: PropertySchema;
27
+ }
28
+
29
+ export interface CatalogDefinition {
30
+ id: string;
31
+ version: string;
32
+ name?: string;
33
+ description?: string;
34
+ components: Record<string, CatalogComponent>;
35
+ }
36
+
37
+ export interface ToolSchema {
38
+ name: string;
39
+ description: string;
40
+ parameters: {
41
+ type: 'object';
42
+ properties: Record<string, unknown>;
43
+ required: string[];
44
+ };
45
+ }
46
+
47
+ export interface OpenAITool {
48
+ type: 'function';
49
+ function: ToolSchema;
50
+ }
51
+
52
+ // =============================================================================
53
+ // Catalog Loader
54
+ // =============================================================================
55
+
56
+ /**
57
+ * Load a catalog from a URL or file path
58
+ */
59
+ export async function loadCatalog(source: string): Promise<CatalogDefinition> {
60
+ if (source.startsWith('http://') || source.startsWith('https://')) {
61
+ const response = await fetch(source);
62
+ if (!response.ok) {
63
+ throw new Error(`Failed to load catalog: ${response.statusText}`);
64
+ }
65
+ return response.json() as Promise<CatalogDefinition>;
66
+ }
67
+
68
+ // For file paths, use dynamic import (works in ESM)
69
+ const fs = await import('fs/promises');
70
+ const content = await fs.readFile(source, 'utf-8');
71
+ return JSON.parse(content) as CatalogDefinition;
72
+ }
73
+
74
+ // =============================================================================
75
+ // Tool Converter
76
+ // =============================================================================
77
+
78
+ /**
79
+ * CatalogToToolConverter generates LLM tool definitions from catalog schemas.
80
+ * It's the bridge between UI catalogs and AI function calling.
81
+ */
82
+ export class CatalogToToolConverter {
83
+ private catalogs: Map<string, CatalogDefinition> = new Map();
84
+
85
+ /**
86
+ * Register a catalog
87
+ */
88
+ addCatalog(catalog: CatalogDefinition): void {
89
+ this.catalogs.set(catalog.id, catalog);
90
+ }
91
+
92
+ /**
93
+ * Get registered catalog
94
+ */
95
+ getCatalog(catalogId: string): CatalogDefinition | undefined {
96
+ return this.catalogs.get(catalogId);
97
+ }
98
+
99
+ /**
100
+ * Generate the render_ui tool schema for a catalog
101
+ */
102
+ generateRenderUITool(catalogId: string): ToolSchema {
103
+ const catalog = this.catalogs.get(catalogId);
104
+ if (!catalog) {
105
+ throw new Error(`Catalog not found: ${catalogId}`);
106
+ }
107
+
108
+ // Build component union schema
109
+ const componentSchemas = this.buildComponentSchemas(catalog);
110
+
111
+ return {
112
+ name: 'render_ui',
113
+ description: `Render a user interface using the ${catalog.name || catalogId} component catalog. ` +
114
+ `Use this tool to create visual UI elements. Components are specified as a flat list with ` +
115
+ `parent-child relationships defined by ID references in the 'children' array.`,
116
+ parameters: {
117
+ type: 'object',
118
+ properties: {
119
+ surfaceId: {
120
+ type: 'string',
121
+ description: 'Unique identifier for this UI surface',
122
+ },
123
+ components: {
124
+ type: 'array',
125
+ description: 'Array of component definitions. First component should have id "root".',
126
+ items: {
127
+ type: 'object',
128
+ properties: {
129
+ id: {
130
+ type: 'string',
131
+ description: 'Unique identifier for this component',
132
+ },
133
+ component: {
134
+ type: 'string',
135
+ description: 'Component type from catalog',
136
+ enum: Object.keys(catalog.components),
137
+ },
138
+ children: {
139
+ type: 'array',
140
+ description: 'Array of child component IDs (for container components)',
141
+ items: { type: 'string' },
142
+ },
143
+ ...componentSchemas,
144
+ },
145
+ required: ['id', 'component'],
146
+ },
147
+ },
148
+ },
149
+ required: ['surfaceId', 'components'],
150
+ },
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Generate update_data tool schema
156
+ */
157
+ generateUpdateDataTool(): ToolSchema {
158
+ return {
159
+ name: 'update_data',
160
+ description: 'Update data in a UI surface without recreating components. ' +
161
+ 'Use this to change text, values, or state in an existing UI.',
162
+ parameters: {
163
+ type: 'object',
164
+ properties: {
165
+ surfaceId: {
166
+ type: 'string',
167
+ description: 'The surface ID to update',
168
+ },
169
+ path: {
170
+ type: 'string',
171
+ description: 'JSON Pointer path to the data to update (e.g., "/user/name")',
172
+ },
173
+ value: {
174
+ description: 'The new value to set',
175
+ },
176
+ operation: {
177
+ type: 'string',
178
+ enum: ['add', 'replace', 'remove'],
179
+ description: 'The operation to perform (default: replace)',
180
+ },
181
+ },
182
+ required: ['surfaceId', 'value'],
183
+ },
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Generate all tools for a catalog as OpenAI format
189
+ */
190
+ generateOpenAITools(catalogId: string): OpenAITool[] {
191
+ return [
192
+ { type: 'function', function: this.generateRenderUITool(catalogId) },
193
+ { type: 'function', function: this.generateUpdateDataTool() },
194
+ ];
195
+ }
196
+
197
+ /**
198
+ * Generate system prompt section describing available components
199
+ */
200
+ generateSystemPromptSection(catalogId: string): string {
201
+ const catalog = this.catalogs.get(catalogId);
202
+ if (!catalog) {
203
+ throw new Error(`Catalog not found: ${catalogId}`);
204
+ }
205
+
206
+ let prompt = `## Available UI Components (${catalog.name || catalogId})\n\n`;
207
+ prompt += `You can create UIs using the following components:\n\n`;
208
+
209
+ for (const [name, component] of Object.entries(catalog.components)) {
210
+ prompt += `### ${name}\n`;
211
+ prompt += `${component.description || 'No description'}\n\n`;
212
+
213
+ if (component.properties) {
214
+ prompt += `Properties:\n`;
215
+ for (const [propName, prop] of Object.entries(component.properties)) {
216
+ const required = component.required?.includes(propName) ? ' (required)' : '';
217
+ const enumValues = prop.enum ? ` [${prop.enum.join(', ')}]` : '';
218
+ prompt += `- \`${propName}\` (${prop.type}${required}): ${prop.description || ''}${enumValues}\n`;
219
+ }
220
+ prompt += '\n';
221
+ }
222
+ }
223
+
224
+ prompt += `\n## UI Structure\n`;
225
+ prompt += `Components are defined as a flat array with relationships via 'children' IDs:\n`;
226
+ prompt += `\`\`\`json
227
+ {
228
+ "surfaceId": "my_surface",
229
+ "components": [
230
+ { "id": "root", "component": "Column", "children": ["title", "content"] },
231
+ { "id": "title", "component": "Text", "text": "Hello", "variant": "h1" },
232
+ { "id": "content", "component": "Text", "text": "World" }
233
+ ]
234
+ }
235
+ \`\`\`\n`;
236
+
237
+ return prompt;
238
+ }
239
+
240
+ // ===========================================================================
241
+ // Private Methods
242
+ // ===========================================================================
243
+
244
+ private buildComponentSchemas(catalog: CatalogDefinition): Record<string, unknown> {
245
+ const allProperties: Record<string, unknown> = {};
246
+
247
+ for (const component of Object.values(catalog.components)) {
248
+ if (component.properties) {
249
+ for (const [propName, prop] of Object.entries(component.properties)) {
250
+ // Merge property schemas (last wins for duplicates)
251
+ allProperties[propName] = this.convertPropertySchema(prop);
252
+ }
253
+ }
254
+ }
255
+
256
+ return allProperties;
257
+ }
258
+
259
+ private convertPropertySchema(prop: PropertySchema): Record<string, unknown> {
260
+ const schema: Record<string, unknown> = {
261
+ type: prop.type === 'integer' ? 'number' : prop.type,
262
+ };
263
+
264
+ if (prop.description) {
265
+ schema.description = prop.description;
266
+ }
267
+
268
+ if (prop.enum) {
269
+ schema.enum = prop.enum;
270
+ }
271
+
272
+ if (prop.items) {
273
+ schema.items = this.convertPropertySchema(prop.items);
274
+ }
275
+
276
+ return schema;
277
+ }
278
+ }
279
+
280
+ // =============================================================================
281
+ // Tool Execution Helpers
282
+ // =============================================================================
283
+
284
+ /**
285
+ * Convert render_ui tool call to A2UX messages
286
+ */
287
+ export function toolCallToMessages(
288
+ toolCall: { surfaceId: string; components: A2UXComponent[] },
289
+ catalogId: string
290
+ ): ServerToClientMessage[] {
291
+ return [
292
+ { createSurface: { surfaceId: toolCall.surfaceId, catalogId } },
293
+ { updateComponents: { surfaceId: toolCall.surfaceId, components: toolCall.components } },
294
+ ];
295
+ }
296
+
297
+ /**
298
+ * Convert update_data tool call to A2UX message
299
+ */
300
+ export function updateDataToMessage(
301
+ toolCall: { surfaceId: string; path?: string; value: unknown; operation?: string }
302
+ ): ServerToClientMessage {
303
+ return {
304
+ updateDataModel: {
305
+ surfaceId: toolCall.surfaceId,
306
+ path: toolCall.path ?? '/',
307
+ op: (toolCall.operation as 'add' | 'replace' | 'remove') ?? 'replace',
308
+ value: toolCall.value,
309
+ },
310
+ };
311
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Freesail Server
3
+ *
4
+ * SSE Stream Engine & Framework Adapters for A2UX Protocol.
5
+ */
6
+
7
+ // Stream
8
+ export { FreesailStream, StreamStore } from './stream.js';
9
+ export type { StreamOptions, StreamStats } from './stream.js';
10
+
11
+ // Catalog Loader
12
+ export {
13
+ CatalogToToolConverter,
14
+ loadCatalog,
15
+ toolCallToMessages,
16
+ updateDataToMessage,
17
+ } from './catalog-loader.js';
18
+ export type {
19
+ CatalogDefinition,
20
+ CatalogComponent,
21
+ PropertySchema,
22
+ ToolSchema,
23
+ OpenAITool,
24
+ } from './catalog-loader.js';
25
+
26
+ // Adapters
27
+ export {
28
+ // LangChain
29
+ createLangChainTools,
30
+ createLangChainRenderUITool,
31
+ createLangChainUpdateDataTool,
32
+ // OpenAI
33
+ OpenAIAdapter,
34
+ createOpenAIAdapter,
35
+ processOpenAIResponse,
36
+ } from './adapters/index.js';
37
+
38
+ export type {
39
+ LangChainAdapterOptions,
40
+ OpenAIAdapterOptions,
41
+ ToolCallResult,
42
+ } from './adapters/index.js';
43
+
44
+ // =============================================================================
45
+ // Express Middleware Helper
46
+ // =============================================================================
47
+
48
+ import type { IncomingMessage, ServerResponse } from 'http';
49
+ import { FreesailStream, StreamStore } from './stream.js';
50
+
51
+ export interface ExpressRequest extends IncomingMessage {
52
+ body?: unknown;
53
+ }
54
+
55
+ export interface ExpressResponse extends ServerResponse {
56
+ // Express adds these
57
+ }
58
+
59
+ const globalStreamStore = new StreamStore();
60
+
61
+ /**
62
+ * Create SSE endpoint middleware for Express
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import express from 'express';
67
+ * import { createSSEHandler } from '@freesail/server';
68
+ *
69
+ * const app = express();
70
+ *
71
+ * app.get('/api/stream', createSSEHandler(async (stream, req) => {
72
+ * // Your agent logic here
73
+ * stream.createSurface('main', 'standard_catalog_v1');
74
+ * stream.updateComponents('main', [...]);
75
+ * }));
76
+ * ```
77
+ */
78
+ export function createSSEHandler(
79
+ handler: (stream: FreesailStream, req: ExpressRequest) => Promise<void>,
80
+ options?: { debug?: boolean }
81
+ ): (req: ExpressRequest, res: ExpressResponse) => void {
82
+ return async (req: ExpressRequest, res: ExpressResponse) => {
83
+ const stream = globalStreamStore.create({
84
+ response: res,
85
+ debug: options?.debug,
86
+ });
87
+
88
+ try {
89
+ await handler(stream, req);
90
+ } catch (error) {
91
+ console.error('[Freesail SSE Handler Error]', error);
92
+ stream.sendRaw('error', { message: String(error) });
93
+ }
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Create action endpoint middleware for Express
99
+ * Handles userAction messages from clients.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * app.post('/api/action', express.json(), createActionHandler(async (action, req) => {
104
+ * console.log('User action:', action);
105
+ * // Handle action and potentially trigger new UI updates
106
+ * }));
107
+ * ```
108
+ */
109
+ export function createActionHandler(
110
+ handler: (
111
+ action: { surfaceId: string; action: string; context: Record<string, unknown> },
112
+ req: ExpressRequest
113
+ ) => Promise<void>
114
+ ): (req: ExpressRequest, res: ExpressResponse) => void {
115
+ return async (req: ExpressRequest, res: ExpressResponse) => {
116
+ try {
117
+ const body = req.body as { userAction?: { surfaceId: string; action: string; context: Record<string, unknown> } };
118
+
119
+ if (!body?.userAction) {
120
+ res.statusCode = 400;
121
+ res.end(JSON.stringify({ error: 'Invalid request: missing userAction' }));
122
+ return;
123
+ }
124
+
125
+ await handler(body.userAction, req);
126
+
127
+ res.statusCode = 200;
128
+ res.end(JSON.stringify({ success: true }));
129
+ } catch (error) {
130
+ console.error('[Freesail Action Handler Error]', error);
131
+ res.statusCode = 500;
132
+ res.end(JSON.stringify({ error: String(error) }));
133
+ }
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Get the global stream store
139
+ */
140
+ export function getStreamStore(): StreamStore {
141
+ return globalStreamStore;
142
+ }