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,157 @@
1
+ /**
2
+ * defineProps Utility
3
+ *
4
+ * Hydrates Lit component properties directly from the Catalog JSON.
5
+ * This is the core of the "Dry" Law - Schema-First Components.
6
+ */
7
+
8
+ import type { PropertyDeclaration } from 'lit';
9
+ import type { CatalogDefinition, PropertyDefinition } from '../types.js';
10
+
11
+ /**
12
+ * Convert JSON Schema type to Lit property type
13
+ */
14
+ function schemaTypeToLitType(schemaProp: PropertyDefinition): PropertyDeclaration {
15
+ let type: typeof String | typeof Number | typeof Boolean | typeof Array | typeof Object = String;
16
+
17
+ switch (schemaProp.type) {
18
+ case 'string':
19
+ type = String;
20
+ break;
21
+ case 'number':
22
+ case 'integer':
23
+ type = Number;
24
+ break;
25
+ case 'boolean':
26
+ type = Boolean;
27
+ break;
28
+ case 'array':
29
+ type = Array;
30
+ break;
31
+ case 'object':
32
+ type = Object;
33
+ break;
34
+ default:
35
+ type = String;
36
+ }
37
+
38
+ return {
39
+ type,
40
+ reflect: schemaProp.reflect ?? false,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Generate Lit property declarations from a catalog component definition
46
+ *
47
+ * @param catalog - The catalog JSON object
48
+ * @param componentName - Name of the component in the catalog
49
+ * @returns Property declarations for use with Lit's static properties
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import catalog from './catalog.json';
54
+ *
55
+ * export class FreesailText extends LitElement {
56
+ * static properties = defineProps(catalog, 'Text');
57
+ * }
58
+ * ```
59
+ */
60
+ export function defineProps(
61
+ catalog: CatalogDefinition,
62
+ componentName: string
63
+ ): Record<string, PropertyDeclaration> {
64
+ const componentDef = catalog.components?.[componentName];
65
+
66
+ if (!componentDef) {
67
+ console.warn(`[defineProps] Component "${componentName}" not found in catalog`);
68
+ return {};
69
+ }
70
+
71
+ const properties: Record<string, PropertyDeclaration> = {};
72
+
73
+ // Process component properties
74
+ if (componentDef.properties) {
75
+ for (const [propName, propDef] of Object.entries(componentDef.properties)) {
76
+ properties[propName] = schemaTypeToLitType(propDef);
77
+ }
78
+ }
79
+
80
+ return properties;
81
+ }
82
+
83
+ /**
84
+ * Get default values for a component from the catalog
85
+ */
86
+ export function getDefaults(
87
+ catalog: CatalogDefinition,
88
+ componentName: string
89
+ ): Record<string, unknown> {
90
+ const componentDef = catalog.components?.[componentName];
91
+
92
+ if (!componentDef) {
93
+ return {};
94
+ }
95
+
96
+ const defaults: Record<string, unknown> = {};
97
+
98
+ if (componentDef.properties) {
99
+ for (const [propName, propDef] of Object.entries(componentDef.properties)) {
100
+ if (propDef.default !== undefined) {
101
+ defaults[propName] = propDef.default;
102
+ }
103
+ }
104
+ }
105
+
106
+ return defaults;
107
+ }
108
+
109
+ /**
110
+ * Validate a component's properties against the catalog schema
111
+ */
112
+ export function validateProps(
113
+ catalog: CatalogDefinition,
114
+ componentName: string,
115
+ props: Record<string, unknown>
116
+ ): { valid: boolean; errors: string[] } {
117
+ const componentDef = catalog.components?.[componentName];
118
+ const errors: string[] = [];
119
+
120
+ if (!componentDef) {
121
+ errors.push(`Component "${componentName}" not found in catalog`);
122
+ return { valid: false, errors };
123
+ }
124
+
125
+ // Check required properties
126
+ if (componentDef.required) {
127
+ for (const required of componentDef.required) {
128
+ if (props[required] === undefined || props[required] === null) {
129
+ errors.push(`Missing required property: ${required}`);
130
+ }
131
+ }
132
+ }
133
+
134
+ // Type check properties
135
+ if (componentDef.properties) {
136
+ for (const [propName, propValue] of Object.entries(props)) {
137
+ const propDef = componentDef.properties[propName];
138
+
139
+ if (!propDef) {
140
+ // Allow additional properties unless explicitly forbidden
141
+ continue;
142
+ }
143
+
144
+ const actualType = Array.isArray(propValue) ? 'array' : typeof propValue;
145
+ const expectedType = propDef.type;
146
+
147
+ if (actualType !== expectedType &&
148
+ !(expectedType === 'integer' && actualType === 'number')) {
149
+ errors.push(
150
+ `Property "${propName}" expected ${expectedType}, got ${actualType}`
151
+ );
152
+ }
153
+ }
154
+ }
155
+
156
+ return { valid: errors.length === 0, errors };
157
+ }
@@ -0,0 +1,2 @@
1
+ export { defineProps, getDefaults, validateProps } from './define-props.js';
2
+ export { ComponentRegistry, registry } from './registry.js';
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Component Registry
3
+ *
4
+ * Manages the registration and retrieval of Web Components
5
+ * based on catalog definitions.
6
+ */
7
+
8
+ import type { LitElement } from 'lit';
9
+ import type { CatalogDefinition } from '../types.js';
10
+
11
+ type ComponentConstructor = typeof LitElement;
12
+
13
+ interface RegisteredCatalog {
14
+ definition: CatalogDefinition;
15
+ components: Map<string, ComponentConstructor>;
16
+ prefix: string;
17
+ }
18
+
19
+ /**
20
+ * ComponentRegistry manages Web Component registration by catalog.
21
+ * It ensures components are properly registered with unique prefixes
22
+ * and handles dynamic component resolution.
23
+ */
24
+ export class ComponentRegistry {
25
+ private catalogs: Map<string, RegisteredCatalog> = new Map();
26
+ private globalComponents: Map<string, ComponentConstructor> = new Map();
27
+
28
+ /**
29
+ * Register a catalog and its components
30
+ */
31
+ registerCatalog(
32
+ catalogId: string,
33
+ definition: CatalogDefinition,
34
+ components: Map<string, ComponentConstructor>,
35
+ prefix: string = 'fs'
36
+ ): void {
37
+ // Validate catalog
38
+ if (!definition.id || !definition.version) {
39
+ console.warn(`[Registry] Invalid catalog definition for ${catalogId}`);
40
+ }
41
+
42
+ // Store catalog
43
+ this.catalogs.set(catalogId, {
44
+ definition,
45
+ components,
46
+ prefix,
47
+ });
48
+
49
+ // Register each component as a custom element
50
+ for (const [componentName, ComponentClass] of components) {
51
+ const tagName = this.getTagName(prefix, componentName);
52
+
53
+ if (!customElements.get(tagName)) {
54
+ try {
55
+ customElements.define(tagName, ComponentClass);
56
+ } catch (error) {
57
+ console.error(`[Registry] Failed to register ${tagName}:`, error);
58
+ }
59
+ }
60
+
61
+ // Also store in global lookup
62
+ this.globalComponents.set(`${catalogId}:${componentName}`, ComponentClass);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get a component class by catalog and name
68
+ */
69
+ getComponent(catalogId: string, componentName: string): ComponentConstructor | undefined {
70
+ const catalog = this.catalogs.get(catalogId);
71
+ return catalog?.components.get(componentName);
72
+ }
73
+
74
+ /**
75
+ * Get the HTML tag name for a component
76
+ */
77
+ getTagName(prefix: string, componentName: string): string {
78
+ // Convert PascalCase to kebab-case
79
+ const kebab = componentName
80
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
81
+ .toLowerCase();
82
+ return `${prefix}-${kebab}`;
83
+ }
84
+
85
+ /**
86
+ * Get tag name for a component in a specific catalog
87
+ */
88
+ getTagNameForCatalog(catalogId: string, componentName: string): string | undefined {
89
+ const catalog = this.catalogs.get(catalogId);
90
+ if (!catalog) return undefined;
91
+ return this.getTagName(catalog.prefix, componentName);
92
+ }
93
+
94
+ /**
95
+ * Check if a catalog is registered
96
+ */
97
+ hasCatalog(catalogId: string): boolean {
98
+ return this.catalogs.has(catalogId);
99
+ }
100
+
101
+ /**
102
+ * Get catalog definition
103
+ */
104
+ getCatalogDefinition(catalogId: string): CatalogDefinition | undefined {
105
+ return this.catalogs.get(catalogId)?.definition;
106
+ }
107
+
108
+ /**
109
+ * Get all registered catalog IDs
110
+ */
111
+ getCatalogIds(): string[] {
112
+ return Array.from(this.catalogs.keys());
113
+ }
114
+
115
+ /**
116
+ * Get all component names in a catalog
117
+ */
118
+ getComponentNames(catalogId: string): string[] {
119
+ const catalog = this.catalogs.get(catalogId);
120
+ return catalog ? Array.from(catalog.components.keys()) : [];
121
+ }
122
+
123
+ /**
124
+ * Unregister a catalog (components remain in DOM but won't be found)
125
+ */
126
+ unregisterCatalog(catalogId: string): void {
127
+ const catalog = this.catalogs.get(catalogId);
128
+ if (catalog) {
129
+ // Remove from global lookup
130
+ for (const componentName of catalog.components.keys()) {
131
+ this.globalComponents.delete(`${catalogId}:${componentName}`);
132
+ }
133
+ this.catalogs.delete(catalogId);
134
+ }
135
+ }
136
+ }
137
+
138
+ // Singleton instance
139
+ export const registry = new ComponentRegistry();
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true,
7
+ "resolveJsonModule": true
8
+ },
9
+ "include": ["src/**/*", "src/**/*.json"],
10
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
11
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@freesail/server",
3
+ "version": "0.1.0",
4
+ "description": "Freesail Server - SSE Stream Engine & Framework Adapters",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./adapters/langchain": {
15
+ "types": "./dist/adapters/langchain.d.ts",
16
+ "import": "./dist/adapters/langchain.js"
17
+ },
18
+ "./adapters/openai": {
19
+ "types": "./dist/adapters/openai.d.ts",
20
+ "import": "./dist/adapters/openai.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev": "tsc --watch",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "lint": "eslint src --ext .ts"
32
+ },
33
+ "dependencies": {
34
+ "@freesail/core": "^0.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.10.0",
38
+ "typescript": "^5.3.3",
39
+ "vitest": "^1.2.0"
40
+ },
41
+ "peerDependencies": {
42
+ "langchain": ">=0.1.0",
43
+ "openai": ">=4.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "langchain": {
47
+ "optional": true
48
+ },
49
+ "openai": {
50
+ "optional": true
51
+ }
52
+ },
53
+ "keywords": [
54
+ "sse",
55
+ "stream",
56
+ "langchain",
57
+ "openai",
58
+ "a2ux"
59
+ ],
60
+ "license": "MIT"
61
+ }
@@ -0,0 +1,5 @@
1
+ export { createLangChainTools, createLangChainRenderUITool, createLangChainUpdateDataTool } from './langchain.js';
2
+ export type { LangChainAdapterOptions } from './langchain.js';
3
+
4
+ export { OpenAIAdapter, createOpenAIAdapter, processOpenAIResponse } from './openai.js';
5
+ export type { OpenAIAdapterOptions, ToolCallResult } from './openai.js';
@@ -0,0 +1,175 @@
1
+ /**
2
+ * LangChain Adapter
3
+ *
4
+ * Converts Freesail tools to LangChain DynamicStructuredTool format.
5
+ * This adapter allows using Freesail with LangChain agents.
6
+ *
7
+ * NOTE: This adapter requires 'langchain' as a peer dependency.
8
+ */
9
+
10
+ import type { ToolSchema } from '../catalog-loader.js';
11
+ import type { FreesailStream } from '../stream.js';
12
+ import { toolCallToMessages, updateDataToMessage } from '../catalog-loader.js';
13
+
14
+ // =============================================================================
15
+ // Types (Minimal LangChain interfaces to avoid direct import)
16
+ // =============================================================================
17
+
18
+ interface LangChainToolInput {
19
+ name: string;
20
+ description: string;
21
+ schema: unknown; // Zod schema
22
+ func: (input: unknown) => Promise<string>;
23
+ }
24
+
25
+ interface ZodObjectLike {
26
+ parse: (input: unknown) => unknown;
27
+ }
28
+
29
+ // =============================================================================
30
+ // LangChain Adapter
31
+ // =============================================================================
32
+
33
+ export interface LangChainAdapterOptions {
34
+ /** The SSE stream to send messages to */
35
+ stream: FreesailStream;
36
+ /** Catalog ID being used */
37
+ catalogId: string;
38
+ /** Callback after UI is rendered */
39
+ onRenderUI?: (surfaceId: string) => void;
40
+ /** Callback after data is updated */
41
+ onUpdateData?: (surfaceId: string) => void;
42
+ }
43
+
44
+ /**
45
+ * Creates a LangChain DynamicStructuredTool configuration for render_ui
46
+ */
47
+ export function createLangChainRenderUITool(
48
+ toolSchema: ToolSchema,
49
+ options: LangChainAdapterOptions,
50
+ z: { object: Function; string: Function; array: Function; any: Function }
51
+ ): LangChainToolInput {
52
+ const { stream, catalogId, onRenderUI } = options;
53
+
54
+ // Build Zod schema from tool schema
55
+ const zodSchema = buildZodSchema(toolSchema.parameters, z);
56
+
57
+ return {
58
+ name: toolSchema.name,
59
+ description: toolSchema.description,
60
+ schema: zodSchema,
61
+ func: async (input: unknown) => {
62
+ const { surfaceId, components } = input as {
63
+ surfaceId: string;
64
+ components: Array<{ id: string; component: string; [key: string]: unknown }>;
65
+ };
66
+
67
+ // Convert to A2UX messages and send
68
+ const messages = toolCallToMessages({ surfaceId, components }, catalogId);
69
+
70
+ for (const message of messages) {
71
+ stream.send(message);
72
+ }
73
+
74
+ onRenderUI?.(surfaceId);
75
+
76
+ return `UI surface "${surfaceId}" created with ${components.length} components.`;
77
+ },
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Creates a LangChain DynamicStructuredTool configuration for update_data
83
+ */
84
+ export function createLangChainUpdateDataTool(
85
+ toolSchema: ToolSchema,
86
+ options: LangChainAdapterOptions,
87
+ z: { object: Function; string: Function; optional: Function; any: Function; enum: Function }
88
+ ): LangChainToolInput {
89
+ const { stream, onUpdateData } = options;
90
+
91
+ // Build Zod schema
92
+ const zodSchema = z.object({
93
+ surfaceId: z.string(),
94
+ path: z.string().optional(),
95
+ value: z.any(),
96
+ operation: z.enum(['add', 'replace', 'remove']).optional(),
97
+ });
98
+
99
+ return {
100
+ name: toolSchema.name,
101
+ description: toolSchema.description,
102
+ schema: zodSchema,
103
+ func: async (input: unknown) => {
104
+ const toolCall = input as {
105
+ surfaceId: string;
106
+ path?: string;
107
+ value: unknown;
108
+ operation?: string;
109
+ };
110
+
111
+ const message = updateDataToMessage(toolCall);
112
+ stream.send(message);
113
+
114
+ onUpdateData?.(toolCall.surfaceId);
115
+
116
+ return `Data updated at path "${toolCall.path || '/'}" in surface "${toolCall.surfaceId}".`;
117
+ },
118
+ };
119
+ }
120
+
121
+ /**
122
+ * Create all Freesail tools for LangChain
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * import { z } from 'zod';
127
+ * import { DynamicStructuredTool } from 'langchain/tools';
128
+ *
129
+ * const toolConfigs = createLangChainTools(converter, catalogId, { stream, catalogId }, z);
130
+ * const tools = toolConfigs.map(config => new DynamicStructuredTool(config));
131
+ * ```
132
+ */
133
+ export function createLangChainTools(
134
+ converter: { generateRenderUITool: (id: string) => ToolSchema; generateUpdateDataTool: () => ToolSchema },
135
+ catalogId: string,
136
+ options: LangChainAdapterOptions,
137
+ z: unknown // Zod instance
138
+ ): LangChainToolInput[] {
139
+ const zod = z as { object: Function; string: Function; array: Function; any: Function; optional: Function; enum: Function };
140
+
141
+ return [
142
+ createLangChainRenderUITool(converter.generateRenderUITool(catalogId), options, zod),
143
+ createLangChainUpdateDataTool(converter.generateUpdateDataTool(), options, zod),
144
+ ];
145
+ }
146
+
147
+ // =============================================================================
148
+ // Helpers
149
+ // =============================================================================
150
+
151
+ function buildZodSchema(
152
+ schema: ToolSchema['parameters'],
153
+ z: { object: Function; string: Function; array: Function; any: Function; enum?: Function }
154
+ ): ZodObjectLike {
155
+ const properties: Record<string, unknown> = {};
156
+
157
+ for (const [key, value] of Object.entries(schema.properties)) {
158
+ const prop = value as { type: string; enum?: string[]; items?: unknown };
159
+
160
+ if (prop.type === 'string') {
161
+ properties[key] = (prop.enum && z.enum) ? z.enum(prop.enum as [string, ...string[]]) : z.string();
162
+ } else if (prop.type === 'array') {
163
+ properties[key] = z.array(z.any());
164
+ } else {
165
+ properties[key] = z.any();
166
+ }
167
+
168
+ // Make optional if not required
169
+ if (!schema.required.includes(key)) {
170
+ properties[key] = (properties[key] as { optional?: Function }).optional?.() ?? properties[key];
171
+ }
172
+ }
173
+
174
+ return z.object(properties) as ZodObjectLike;
175
+ }