purposeos

SDKs

Services speak the socket protocol. Windows speak the JavaScript bridge. Use the language that fits each part.

LanguageForStatus
JavaScriptUIs (web pages)shipped
RustServices, native UIs, Dioxus-wasm UIsin the repo
GoServices; WASM UIs (for example Fyne) loaded by webhostSDK planned

JavaScript shipped

purpose.js ships with purpose-webhost. No dependencies; also loads as an ES module.

ui/app.js
// In a page hosted by purpose-webhost (purpose-app://<id>/). purpose.js is injected.
const entries = await purpose.call("journal.list", { filter: "today", limit: 20 });

const off = purpose.on("capture", (e) => console.log(e));  // returns unsubscribe

try {
  await purpose.call("journal.get", { entry: "abc123" });
} catch (e) {
  // e.name === "PurposeError"
  // e.kind: transient | unavailable | permanent | executor | bridge
}
  • purpose.call(action, input) returns a Promise. It can only reach your own services and those in permissions.call.
  • purpose.on(event, cb) returns an unsubscribe function.
  • A faulty listener never stops other listeners.

Rust in the repo

The crates exist in the repo: exec-plugin-service (server and protocol), exec-manifest (the same validator the installer uses) and exec-events (logging). They are not yet published as a standalone SDK. The snippet is shortened from the Journal service and was not compiled on its own.

store/src/main.rs
// Abridged from apps/journal/store/src/main.rs. Cargo: exec-plugin-service = { path = "..." }
use std::sync::{atomic::AtomicBool, Arc};
use exec_plugin_service::{server, ServiceError};

const NAME: &str = "hello-svc";
const ACTIONS: &[&str] = &["hello.greet"];

fn main() {
    // usage: hello-svc serve --socket PATH
    let socket = std::env::args().nth(3).expect("--socket PATH");
    let listener = server::bind(std::path::Path::new(&socket)).expect("bind");

    let handler: Arc<server::Handler> = Arc::new(|action, input| match action {
        "hello.greet" => {
            let name = input["name"].as_str().unwrap_or("world");
            Ok(serde_json::json!({ "text": format!("hello {name}") }))
        }
        _ => Err(ServiceError::permanent("unknown action")),
    });

    server::serve(listener, NAME, ACTIONS, handler, Arc::new(AtomicBool::new(false))).unwrap();
}
  • The Journal and Scratchpad services are written this way.
  • exec-manifest validates app.json exactly as the installer does. Use it in your tests.
  • Service handlers must be idempotent. Map failures to transient, unavailable or permanent so the executor can retry or fall back.

Go SDK planned

There is no Go package yet. The protocol is small, so a service needs only the standard library. This example is the one from the Quickstart: it builds as a static arm64 binary and answers the real wire protocol.

main.go
package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"net"
	"os"
)

type req struct {
	Op     string          `json:"op"`
	Action string          `json:"action"`
	Input  json.RawMessage `json:"input"`
}

var actions = []string{"hello.greet"}

// handle must be idempotent: the executor may retry a call.
func handle(action string, in json.RawMessage) (any, string, string) {
	if action == "hello.greet" {
		p := struct{ Name string `json:"name"` }{Name: "world"}
		_ = json.Unmarshal(in, &p)
		return map[string]string{"text": "hello " + p.Name}, "", ""
	}
	return nil, "permanent", "unknown action"
}

func main() {
	if len(os.Args) < 2 || os.Args[1] != "serve" {
		os.Exit(2)
	}
	fs := flag.NewFlagSet("serve", flag.ExitOnError)
	sock := fs.String("socket", "", "unix socket path")
	fs.Parse(os.Args[2:])
	os.Remove(*sock)
	l, err := net.Listen("unix", *sock)
	if err != nil {
		panic(err)
	}
	os.Chmod(*sock, 0o600)
	for {
		c, err := l.Accept()
		if err != nil {
			return
		}
		go func(c net.Conn) {
			defer c.Close()
			sc := bufio.NewScanner(c)
			sc.Buffer(make([]byte, 1<<20), 1<<20) // requests are capped at 1 MiB
			enc := json.NewEncoder(c)
			for sc.Scan() {
				var r req
				if json.Unmarshal(sc.Bytes(), &r) != nil {
					enc.Encode(map[string]any{"error": map[string]string{"kind": "permanent", "message": "bad json"}})
					continue
				}
				if r.Op == "describe" {
					enc.Encode(map[string]any{"ok": map[string]any{"protocol": 1, "name": "hello-svc", "actions": actions}})
					continue
				}
				v, kind, msg := handle(r.Action, r.Input)
				if kind != "" {
					enc.Encode(map[string]any{"error": map[string]string{"kind": kind, "message": msg}})
				} else {
					enc.Encode(map[string]any{"ok": v})
				}
			}
		}(c)
	}
}
  • Planned: a Go module wrapping bind, serve, error kinds and the manifest check.
  • Until then, copy the example. Accept serve --socket PATH, answer {"op":"describe"}, then one JSON reply per line.