@web-applets/sdk 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Web Applets
2
+
3
+ > An open SDK to create interoperable actions & views for agents – _a web of capabilities!_
4
+
5
+ ## What is it?
6
+
7
+ Web Applets is a specification for modular, local web software that can be read and used by both humans and machines. Web Applets aims to be an interoperabe application layer for agents – instead of chatbots that can only interact in plain text, Web Applets allow them to actuate real software, read the results, and render rich, graphical views in response.
8
+
9
+ Did we mention it's _interoperable_? We think the future of software should be open & collaborative, not locked down to a single platform.
10
+
11
+ ## Getting started
12
+
13
+ First, clone this repo and run `npm run install`. There are a few sample applets included in `/applets`. To install these applets and start the playground, run `npm run playground`.
14
+
15
+ The fastest way to create a new applet is by duplicating one of the applet folders in `/applets`. The folder title will be part of the URL of your applet, so make sure it doesn't include any spaces or other non-URL-safe characters.
16
+
17
+ Inside your applet folder, you'll find a basic web app setup:
18
+
19
+ - `public/manifest.json`: This file describes the Applet, and tells the model what actions are available and what parameters each action takes
20
+ - `index.html`: Much like a website, this holds the main page for your applet
21
+ - `main.js`: Declares functions that respond to each action, and a render function that updates the view based on state
22
+
23
+ > Want to use React? Svelte? Vue? – No problem, just install the dependencies and create an app the way you normally would in a website. So long as you're receiving the action events, it will all just work.
24
+
25
+ Let's say we want our applet to respond to a "set_name" action and render the user's name. In our `manifest.json` file we can write:
26
+
27
+ ```js
28
+ {
29
+ // ...
30
+ "actions": [
31
+ {
32
+ "id": "set_name",
33
+ "description": "Sets the name of the user to be greeted",
34
+ "params": {
35
+ "name": {
36
+ "type": "string",
37
+ "description": "The name of the user"
38
+ }
39
+ }
40
+ }
41
+ ]
42
+ }
43
+ ```
44
+
45
+ Now let's update `main.ts` to assign an action handler:
46
+
47
+ ```js
48
+ // First, import the SDK
49
+ import { appletContext } from '../../sdk/src';
50
+
51
+ // Now connect to the applet runtime
52
+ const applet = appletContext.connect();
53
+
54
+ // Attach the action handler, and update the state
55
+ applet.setActionHandler('set_name', ({ name }) => {
56
+ applet.setState({ name });
57
+ });
58
+ ```
59
+
60
+ When this state updates, it will inform the client which can then store the state somewhere, for example in a database so the applet will persist between uses.
61
+
62
+ Finally, we need to render the applet whenever a render signal is received. Again in `main.ts`:
63
+
64
+ ```js
65
+ // ...
66
+
67
+ applet.onrender = () => {
68
+ document.body.innerText = `Hello, ${applet.state.name}!`;
69
+ };
70
+ ```
71
+
72
+ Now if you run `npm run playground` from the project root, you should be able to test out your new applet action directly. This applet will now work in any environment where the SDK is installed.
73
+
74
+ ![A screenshot showing the 'playground' editing UI, with a web applets showing 'Hello, Web Applets'](docs/assets/web-applets-playground.png)
75
+
76
+ ## Integrating Web Applets into your client
77
+
78
+ Integrating Web Applets is just as easy as creating one. First, in your project, make sure you have the sdk installed:
79
+
80
+ ```bash
81
+ npm install @unternet/web-applets
82
+ ```
83
+
84
+ In your code, you can import the applets client:
85
+
86
+ ```js
87
+ import { applets } from '@unternet/web-applets';
88
+ ```
89
+
90
+ Now you can create a new applet from a URL:
91
+
92
+ ```js
93
+ const applet = await applets.load(
94
+ `https://unternet.co/applets/helloworld.applet`
95
+ );
96
+ applet.onstateupdated = (state) => console.log(state);
97
+ applet.dispatchAction('set_name', { name: 'Web Applets' });
98
+ // console.log: { name: "Web Applets" }
99
+ ```
100
+
101
+ The above applet is actually running headless, but we can get it to display by attaching it to an iframe. For the loading step, instead run:
102
+
103
+ ```js
104
+ const container = document.createElement('iframe');
105
+ document.body.appendChild(container);
106
+ const applet = await applets.load(
107
+ `https://unternet.co/applets/helloworld.applet`,
108
+ container
109
+ );
110
+ ```
111
+
112
+ To load pre-existing saved state into an applet, simply set the state property:
113
+
114
+ ```js
115
+ applet.state = { name: 'Ada Lovelace' };
116
+ // console.log: { name: "Ada Lovelace" }
117
+ ```
118
+
119
+ It may also be helpful to check available applets at a domain, or in your local public folder if you've downloaded a set of Web Applets you want your product to use. For that you can extract the applet headers from the App Manifest at the public root (`/manifest.json`), and see the available applets and a shorthand for the actions you can take in them. This is automatically created when you build your applets.
120
+
121
+ ```js
122
+ const headers = await applets.getHeaders('/');
123
+ ```
124
+
125
+ This headers object looks like:
126
+
127
+ ```js
128
+ [
129
+ {
130
+ name: 'Hello World',
131
+ description: 'Displays a greeting to the user.',
132
+ url: '/applets/helloworld.applet',
133
+ actions: [
134
+ {
135
+ id: 'set_name',
136
+ description: 'Sets the name of the user to be greeted',
137
+ params: {
138
+ name: 'The name of the user',
139
+ },
140
+ },
141
+ ],
142
+ },
143
+ // ...
144
+ ];
145
+ ```
146
+
147
+ You can use it to present a quick summary of available tools to your model, and then decide on an applet and action to use.
148
+
149
+ ## Feedback & Community
150
+
151
+ ## License
152
+
153
+ [MIT](./LICENSE.md)
154
+
155
+ ---
156
+
157
+ Built by [Unternet](https://unternet.co).
@@ -0,0 +1,24 @@
1
+ import { AppletAction, AppletHeader, AppletMessage, ActionParams, AppletManifest } from './types';
2
+ export declare function getHeaders(url: string): Promise<AppletHeader[]>;
3
+ export declare function getManifests(url: string): Promise<any[]>;
4
+ export declare function load(url: string, container: HTMLIFrameElement): Promise<Applet>;
5
+ export declare class Applet<T = unknown> extends EventTarget {
6
+ #private;
7
+ actions: AppletAction[];
8
+ manifest: AppletManifest;
9
+ container: HTMLIFrameElement;
10
+ constructor();
11
+ get state(): T;
12
+ set state(state: T);
13
+ toJson(): {
14
+ [k: string]: any;
15
+ };
16
+ resizeContainer(dimensions: {
17
+ height: number;
18
+ width: number;
19
+ }): void;
20
+ onstateupdated(event: CustomEvent): void;
21
+ disconnect(): void;
22
+ dispatchAction(actionId: string, params: ActionParams): Promise<AppletMessage<any>>;
23
+ }
24
+ export declare function loadManifest(url: string): Promise<AppletManifest>;
package/dist/client.js ADDED
@@ -0,0 +1,151 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _Applet_state;
13
+ import { AppletMessage, } from './types';
14
+ const hiddenRoot = document.createElement('div');
15
+ hiddenRoot.style.display = 'none';
16
+ document.body.appendChild(hiddenRoot);
17
+ export async function getHeaders(url) {
18
+ url = parseUrl(url);
19
+ try {
20
+ const request = await fetch(`${url}/manifest.json`);
21
+ const appManifest = await request.json();
22
+ const appletHeaders = appManifest.applets;
23
+ return appletHeaders ?? [];
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ }
29
+ export async function getManifests(url) {
30
+ url = parseUrl(url);
31
+ const request = await fetch(`${url}/manifest.json`);
32
+ const headers = (await request.json()).applets;
33
+ const manifests = await Promise.all(headers.map(async (header) => {
34
+ const appletUrl = parseUrl(header.url);
35
+ const request = await fetch(`${appletUrl}/manifest.json`);
36
+ return await request.json();
37
+ }));
38
+ return manifests ?? [];
39
+ }
40
+ export async function load(url, container) {
41
+ url = parseUrl(url);
42
+ const manifest = await loadManifest(`${url}`);
43
+ const applet = new Applet();
44
+ applet.manifest = manifest;
45
+ applet.actions = manifest.actions; // let the events set this later
46
+ applet.container = container;
47
+ container.src = applet.manifest.entrypoint;
48
+ if (!container.isConnected)
49
+ hiddenRoot.appendChild(container);
50
+ return new Promise((resolve) => {
51
+ window.addEventListener('message', (message) => {
52
+ if (message.source !== container.contentWindow)
53
+ return;
54
+ if (message.data.type === 'ready')
55
+ resolve(applet);
56
+ });
57
+ });
58
+ }
59
+ export class Applet extends EventTarget {
60
+ constructor() {
61
+ super();
62
+ this.actions = [];
63
+ _Applet_state.set(this, void 0);
64
+ window.addEventListener('message', (message) => {
65
+ if (message.source !== this.container.contentWindow)
66
+ return;
67
+ if (message.data.type === 'state') {
68
+ __classPrivateFieldSet(this, _Applet_state, message.data.state, "f");
69
+ this.dispatchEvent(new CustomEvent('stateupdated', { detail: message.data.detail }));
70
+ this.onstateupdated(message.data.state);
71
+ }
72
+ if (message.data.type === 'resize') {
73
+ this.resizeContainer(message.data.dimensions);
74
+ }
75
+ this.container.contentWindow?.postMessage({
76
+ type: 'resolve',
77
+ id: message.data.id,
78
+ }, '*');
79
+ });
80
+ }
81
+ get state() {
82
+ return __classPrivateFieldGet(this, _Applet_state, "f");
83
+ }
84
+ set state(state) {
85
+ __classPrivateFieldSet(this, _Applet_state, state, "f");
86
+ const stateMessage = new AppletMessage('state', { state });
87
+ this.container.contentWindow?.postMessage(stateMessage.toJson(), '*');
88
+ }
89
+ toJson() {
90
+ return Object.fromEntries(Object.entries(this).filter(([_, value]) => {
91
+ try {
92
+ JSON.stringify(value);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }));
99
+ }
100
+ resizeContainer(dimensions) {
101
+ this.container.style.height = `${dimensions.height}px`;
102
+ // if (!this.#styleOverrides) {
103
+ // this.#container.style.height = `${dimensions.height}px`;
104
+ // }
105
+ }
106
+ onstateupdated(event) { }
107
+ disconnect() { }
108
+ async dispatchAction(actionId, params) {
109
+ const requestMessage = new AppletMessage('action', {
110
+ actionId,
111
+ params,
112
+ });
113
+ this.container.contentWindow?.postMessage(requestMessage.toJson(), '*');
114
+ return new Promise((resolve) => {
115
+ const listener = (messageEvent) => {
116
+ if (messageEvent.source !== this.container.contentWindow)
117
+ return;
118
+ const responseMessage = new AppletMessage(messageEvent.data.type, messageEvent.data);
119
+ if (responseMessage.type === 'resolve' &&
120
+ responseMessage.id === requestMessage.id) {
121
+ window.removeEventListener('message', listener);
122
+ resolve(responseMessage);
123
+ }
124
+ };
125
+ window.addEventListener('message', listener);
126
+ });
127
+ }
128
+ }
129
+ _Applet_state = new WeakMap();
130
+ function parseUrl(url, base) {
131
+ if (['http', 'https'].includes(url.split('://')[0])) {
132
+ return url;
133
+ }
134
+ let path = url;
135
+ if (path.startsWith('/'))
136
+ path = path.slice(1);
137
+ if (path.endsWith('/'))
138
+ path = path.slice(0, -1);
139
+ url = `${base || window.location.origin}/${path}`;
140
+ return url;
141
+ }
142
+ export async function loadManifest(url) {
143
+ url = parseUrl(url);
144
+ const request = await fetch(`${url}/manifest.json`);
145
+ const appletManifest = await request.json();
146
+ if (appletManifest.type !== 'applet') {
147
+ throw new Error("URL doesn't point to a valid applet manifest.");
148
+ }
149
+ appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
150
+ return appletManifest;
151
+ }
@@ -0,0 +1,24 @@
1
+ import { ActionHandlerDict, AppletState, AppletMessage, AppletMessageType, AppletMessageCallback, ActionParams, ActionHandler } from './types';
2
+ /**
3
+ * Context
4
+ */
5
+ export declare class AppletContext<StateType = AppletState> extends EventTarget {
6
+ client: AppletClient;
7
+ actionHandlers: ActionHandlerDict;
8
+ state: StateType;
9
+ connect(): this;
10
+ setActionHandler<T extends ActionParams>(actionId: string, handler: ActionHandler<T>): void;
11
+ setState(state: StateType): Promise<void>;
12
+ onload(): Promise<void> | void;
13
+ onready(): Promise<void> | void;
14
+ onrender(): void;
15
+ }
16
+ /**
17
+ * Client
18
+ */
19
+ declare class AppletClient {
20
+ on(messageType: AppletMessageType, callback: AppletMessageCallback): void;
21
+ send(message: AppletMessage): Promise<void>;
22
+ }
23
+ export declare const appletContext: AppletContext<AppletState>;
24
+ export {};
@@ -0,0 +1,103 @@
1
+ import { AppletMessage, } from './types';
2
+ /**
3
+ * Context
4
+ */
5
+ export class AppletContext extends EventTarget {
6
+ constructor() {
7
+ super(...arguments);
8
+ this.actionHandlers = {};
9
+ }
10
+ connect() {
11
+ this.client = new AppletClient();
12
+ const startup = async () => {
13
+ await this.onload();
14
+ this.client.send(new AppletMessage('ready'));
15
+ this.dispatchEvent(new CustomEvent('ready'));
16
+ await this.onready();
17
+ };
18
+ if (document.readyState === 'complete' ||
19
+ document.readyState === 'interactive') {
20
+ setTimeout(startup, 1);
21
+ }
22
+ else {
23
+ window.addEventListener('DOMContentLoaded', startup);
24
+ }
25
+ const resizeObserver = new ResizeObserver((entries) => {
26
+ for (let entry of entries) {
27
+ const message = new AppletMessage('resize', {
28
+ dimensions: {
29
+ width: entry.contentRect.width,
30
+ height: entry.contentRect.height,
31
+ },
32
+ });
33
+ this.client.send(message);
34
+ }
35
+ });
36
+ resizeObserver.observe(document.querySelector('html'));
37
+ this.client.on('state', (message) => {
38
+ if (!isStateMessage(message)) {
39
+ throw new TypeError("Message doesn't match type StateMessage");
40
+ }
41
+ this.setState(message.state);
42
+ });
43
+ this.client.on('action', async (message) => {
44
+ if (!isActionMessage(message)) {
45
+ throw new TypeError("Message doesn't match type AppletMessage.");
46
+ }
47
+ if (Object.keys(this.actionHandlers).includes(message.actionId)) {
48
+ await this.actionHandlers[message.actionId](message.params);
49
+ }
50
+ message.resolve();
51
+ });
52
+ return this;
53
+ }
54
+ setActionHandler(actionId, handler) {
55
+ this.actionHandlers[actionId] = handler;
56
+ }
57
+ async setState(state) {
58
+ const message = new AppletMessage('state', { state });
59
+ await this.client.send(message);
60
+ this.state = state;
61
+ this.dispatchEvent(new CustomEvent('render'));
62
+ this.onrender(); // TODO: Should come from client? Or stay here, and only activate if mounted? Need a control for mounting.
63
+ }
64
+ onload() { }
65
+ onready() { }
66
+ onrender() { }
67
+ }
68
+ function isActionMessage(message) {
69
+ return message.type === 'action';
70
+ }
71
+ function isStateMessage(message) {
72
+ return message.type === 'state';
73
+ }
74
+ /**
75
+ * Client
76
+ */
77
+ class AppletClient {
78
+ on(messageType, callback) {
79
+ window.addEventListener('message', (messageEvent) => {
80
+ if (messageEvent.data.type !== messageType)
81
+ return;
82
+ const message = new AppletMessage(messageEvent.data.type, messageEvent.data);
83
+ message.resolve = () => {
84
+ window.parent.postMessage(new AppletMessage('resolve', { id: message.id }), '*');
85
+ };
86
+ callback(message);
87
+ });
88
+ }
89
+ send(message) {
90
+ window.parent.postMessage(message.toJson(), '*');
91
+ return new Promise((resolve) => {
92
+ const listener = (messageEvent) => {
93
+ if (messageEvent.data.type === 'resolve' &&
94
+ messageEvent.data.id === message.id) {
95
+ window.removeEventListener('message', listener);
96
+ resolve();
97
+ }
98
+ };
99
+ window.addEventListener('message', listener);
100
+ });
101
+ }
102
+ }
103
+ export const appletContext = new AppletContext();
@@ -0,0 +1,5 @@
1
+ export * from './utils';
2
+ export * from './types';
3
+ export * as applets from './client';
4
+ export { Applet } from './client';
5
+ export * from './context';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './utils';
2
+ export * from './types';
3
+ import * as applets_1 from './client';
4
+ export { applets_1 as applets };
5
+ export { Applet } from './client';
6
+ export * from './context';
@@ -0,0 +1,57 @@
1
+ export interface AppletManifest {
2
+ type: 'applet';
3
+ name: string;
4
+ description: string;
5
+ icon?: string;
6
+ entrypoint: string;
7
+ actions: AppletAction[];
8
+ }
9
+ export interface AppletAction {
10
+ id: string;
11
+ description: string;
12
+ params?: ActionParamSchema;
13
+ }
14
+ export interface AppletHeader {
15
+ name: string;
16
+ description: string;
17
+ url: string;
18
+ params: {
19
+ [key: string]: string;
20
+ };
21
+ }
22
+ export type AppletState = Record<string, Serializable>;
23
+ export type ActionParamSchema = Record<string, {
24
+ description: string;
25
+ type: 'string';
26
+ }>;
27
+ export type ActionParams = Record<string, unknown>;
28
+ export type ActionHandlerDict = {
29
+ [key: string]: ActionHandler<any>;
30
+ };
31
+ export type ActionHandler<T extends ActionParams> = (params: T) => void | Promise<void>;
32
+ export type AnyAppletMessage = AppletMessage | AppletStateMessage | AppletActionMessage;
33
+ export interface AppletStateMessage<T = any> extends AppletMessage {
34
+ type: 'state';
35
+ state: T;
36
+ }
37
+ export interface AppletActionMessage<T = any> extends AppletMessage {
38
+ type: 'action';
39
+ actionId: string;
40
+ params: T;
41
+ }
42
+ export declare class AppletMessage<T = any> {
43
+ type: AppletMessageType;
44
+ id: string;
45
+ timeStamp: number;
46
+ constructor(type: AppletMessageType, values?: T);
47
+ toJson(): {
48
+ [k: string]: any;
49
+ };
50
+ resolve(): void;
51
+ }
52
+ export type AppletMessageType = 'action' | 'render' | 'state' | 'ready' | 'resolve' | 'resize';
53
+ export type AppletMessageCallback = (message: AnyAppletMessage) => void;
54
+ type Serializable = string | number | boolean | null | Serializable[] | {
55
+ [key: string]: Serializable;
56
+ };
57
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1,21 @@
1
+ export class AppletMessage {
2
+ constructor(type, values) {
3
+ this.timeStamp = Date.now();
4
+ this.type = type;
5
+ this.id = crypto.randomUUID();
6
+ if (values)
7
+ Object.assign(this, values);
8
+ }
9
+ toJson() {
10
+ return Object.fromEntries(Object.entries(this).filter(([_, value]) => {
11
+ try {
12
+ JSON.stringify(value);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }));
19
+ }
20
+ resolve() { }
21
+ }
@@ -0,0 +1,3 @@
1
+ import { type AppletManifest } from './types';
2
+ export declare function getAppletsList(url: string): Promise<any>;
3
+ export declare function loadAppletManifest(url: string): Promise<AppletManifest>;
package/dist/utils.js ADDED
@@ -0,0 +1,33 @@
1
+ function parseUrl(url, base) {
2
+ if (['http', 'https'].includes(url.split('://')[0])) {
3
+ return url;
4
+ }
5
+ let path = url;
6
+ if (path.startsWith('/'))
7
+ path = path.slice(1);
8
+ if (path.endsWith('/'))
9
+ path = path.slice(0, -1);
10
+ url = `${base || window.location.origin}/${path}`;
11
+ return url;
12
+ }
13
+ export async function getAppletsList(url) {
14
+ url = parseUrl(url);
15
+ try {
16
+ const request = await fetch(`${url}/manifest.json`);
17
+ const appManifest = await request.json();
18
+ return appManifest.applets ? appManifest.applets : [];
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ export async function loadAppletManifest(url) {
25
+ url = parseUrl(url);
26
+ const request = await fetch(`${url}/manifest.json`);
27
+ const appletManifest = await request.json();
28
+ if (appletManifest.type !== 'applet') {
29
+ throw new Error("URL doesn't point to a valid applet manifest.");
30
+ }
31
+ appletManifest.entrypoint = parseUrl(appletManifest.entrypoint, url);
32
+ return appletManifest;
33
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@web-applets/sdk",
3
+ "version": "0.0.4",
4
+ "description": "The Web Applets SDK, for creating & hosting Web Applets.",
5
+ "author": "Rupert Manfredi <rupert@unternet.co>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/unternet-co/web-applets.git"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc && cp ../README.md ./README.md",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.6.2"
26
+ },
27
+ "dependencies": {
28
+ "vite": "^5.4.7"
29
+ }
30
+ }