Skip to content

Latest commit

 

History

History
381 lines (289 loc) · 11.5 KB

File metadata and controls

381 lines (289 loc) · 11.5 KB

Extending SynthOS

This guide explains how to use SynthOS as an npm dependency and build your own white-labeled product on top of it.

Installation

npm install synthos

Quick start

Create your entry point (e.g. src/index.ts):

import { Customizer, createConfig, init, server } from 'synthos';

class MyApp extends Customizer {
  get productName() { return 'MyApp'; }
  get localFolder() { return '.myapp'; }
}

const customizer = new MyApp();

async function main() {
  const config = await createConfig(customizer.localFolder, {}, customizer);
  await init(config);
  server(config, customizer).listen(4242, () => {
    console.log('MyApp running on http://localhost:4242');
  });
}

main();

Run it:

npx ts-node src/index.ts
# or compile with tsc and run the JS output

That's it. You now have a fully working instance with your own branding.


The Customizer class

Customizer is the single configuration surface. Subclass it and override getters to change behavior. Every getter has a sensible default so you only override what you need.

Branding

Getter Default Purpose
productName 'SynthOS' Name used in LLM prompts, chat messages, and the brainstorm assistant.
localFolder '.synthos' Name of the data folder created in the user's working directory.
tabsListRoute '/pages' Route that outdated pages redirect to.
class MyApp extends Customizer {
  get productName() { return 'Acme Builder'; }
  get localFolder() { return '.acme'; }
}

productName is used everywhere: LLM prompts, chat message prefixes (<strong>Acme Builder:</strong>), the brainstorm assistant, first-run greeting, and error messages. All built-in required pages (builder, settings, pages gallery, etc.) have their "SynthOS" references automatically replaced at serve time when productName differs from the default.

Content folders

Override these to supply your own default pages, themes, scripts, etc. Each getter returns a string[] array of folder paths. The sentinel value 'default' is resolved at config time to the built-in SynthOS folder for that getter. This lets forks layer custom folders alongside (or instead of) the defaults — first folder wins on name collisions.

Getter Default Contents
requiredPagesFolders ['default'] Built-in system pages (builder, settings).
defaultPagesFolders ['default'] Starter page templates copied on first init.
defaultThemesFolders ['default'] Theme CSS/JSON files.
defaultScriptsFolders ['default'] Platform-specific terminal scripts.
staticFilesFolders ['default'] Versioned static files (page-v2.js, etc.).
serviceConnectorsFolders ['default'] Connector JSON definitions.
import path from 'path';

class MyApp extends Customizer {
  // Custom pages take priority, built-in pages used as fallback
  get defaultPagesFolders() {
    return [path.join(__dirname, '../my-pages'), 'default'];
  }
  // Replace built-in themes entirely
  get defaultThemesFolders() {
    return [path.join(__dirname, '../my-themes')];
  }
}

When you don't override a folder, the ['default'] array resolves to the built-in SynthOS assets from the npm package.

Feature flags

SynthOS has built-in feature groups that can be toggled on or off:

  • pages — Page serving and transformation
  • api — Core API routes (settings, images, completions)
  • data — Per-page table storage
  • brainstorm — Brainstorm chat endpoint
  • search — Web search (Brave Search)
  • scripts — User script execution
  • connectors — REST API connector proxy
  • agents — A2A and OpenClaw agent routes

Disable groups you don't need:

const customizer = new MyApp();
customizer.disable('agents', 'connectors', 'search');

Re-enable later if needed:

customizer.enable('search');

Check at runtime:

if (customizer.isEnabled('brainstorm')) {
  // brainstorm is active
}

Custom routes

Add your own Express routes that the server will mount alongside the built-in ones:

customizer.addRoutes(
  (config, app) => {
    app.get('/api/my-endpoint', (req, res) => {
      res.json({ hello: 'world' });
    });
  }
);

To make the LLM aware of your routes (so pages can call them), pass route hints:

customizer.addRoutes({
  installer: (config, app) => {
    app.get('/api/weather/:city', async (req, res) => {
      // ... fetch weather
      res.json(result);
    });
  },
  hints: `GET /api/weather/:city
description: Get current weather for a city
response: { temp: number, condition: string }`
});

You can also add route hints without routes (useful if you mount routes elsewhere):

customizer.addRouteHints(
  `POST /api/my-custom-action
description: Does something custom
request: { input: string }
response: { result: string }`
);

Overriding required pages

Required pages (builder, settings, pages gallery, scripts, APIs) can be overridden by placing your own versions in a custom folder and listing it before 'default' in requiredPagesFolders. The first folder wins on name collisions:

class MyApp extends Customizer {
  get requiredPagesFolders() {
    return [path.join(__dirname, '../my-pages'), 'default'];
  }
}

To override the builder page, create my-pages/builder/page.html. For the settings page, create my-pages/settings/page.html, etc. Any page not found in your folder falls back to the built-in version.

Custom context sections

Add custom context sections that are included in every builder LLM call. These appear alongside the built-in sections (SERVER_APIS, THEME, FLUENTLM_COMPONENTS, etc.):

customizer.addContextSections({
  title: '<COMPANY_GUIDELINES>',
  content: 'All pages must include the Acme Corp logo...',
  instructions: 'Follow the guidelines in <COMPANY_GUIDELINES> when creating pages.',
});

Custom transform instructions

Append additional instructions to the LLM prompt that transforms pages. These are added after the built-in instructions on every page transformation call:

customizer.addTransformInstructions(
  'Always include a footer with "Powered by Acme" at the bottom of the viewer panel.',
  'Never use red as a primary color.'
);

Startup lifecycle

The three steps to start a SynthOS-based server:

// 1. Create config — resolves folder paths, discovers required pages
const config = await createConfig(
  customizer.localFolder,   // data folder name (e.g. '.myapp')
  { debug: false, debugPageUpdates: false },
  customizer                // your Customizer subclass
);

// 2. Init — creates the data folder, copies default pages/themes/scripts
//    Returns true on first run, false if folder already exists.
const firstRun = await init(config);

// 3. Start the Express server
const app = server(config, customizer);
app.listen(4242);

Config options

Option Type Default Purpose
debug boolean false Log every HTTP request with timing.
debugPageUpdates boolean false Log full LLM input/output for page transformations.

Full example

A complete white-labeled app with custom pages, disabled features, and extra routes:

import path from 'path';
import { Customizer, createConfig, init, server } from 'synthos';

class AcmeBuilder extends Customizer {
  get productName() { return 'Acme Builder'; }
  get localFolder() { return '.acme'; }

  get defaultPagesFolders() {
    return [path.join(__dirname, '../acme-pages'), 'default'];
  }

  get defaultThemesFolders() {
    return [path.join(__dirname, '../acme-themes')];
  }
}

async function main() {
  const customizer = new AcmeBuilder();

  // Disable features we don't need
  customizer.disable('agents', 'connectors');

  // Add a custom API endpoint (with LLM-visible hints)
  customizer.addRoutes({
    installer: (config, app) => {
      app.get('/api/company/info', (_req, res) => {
        res.json({ name: 'Acme Corp', plan: 'enterprise' });
      });
    },
    hints: `GET /api/company/info
description: Returns company information
response: { name: string, plan: string }`
  });

  // Tell the LLM to always use Acme branding
  customizer.addTransformInstructions(
    'All new pages should include "Acme Corp" in the header.'
  );

  const config = await createConfig(customizer.localFolder, { debug: true }, customizer);
  await init(config);

  const port = process.env.PORT ? parseInt(process.env.PORT) : 4242;
  server(config, customizer).listen(port, () => {
    console.log(`Acme Builder running on http://localhost:${port}`);
  });
}

main();

Project structure

A typical extending project looks like this:

my-app/
  package.json
  tsconfig.json
  src/
    index.ts          # Entry point (createConfig + init + server)
  acme-pages/         # Custom default pages (optional)
    dashboard.html
    dashboard.json
  acme-themes/        # Custom themes (optional)
    acme-dark-v1.css
    acme-dark-v1.json

Your package.json depends on synthos:

{
  "name": "acme-builder",
  "dependencies": {
    "synthos": "^0.9.0"
  },
  "scripts": {
    "start": "ts-node src/index.ts"
  }
}

What you don't need to touch

These are handled internally and npm consumers won't encounter them:

  • synthos-cli.ts — The built-in CLI. You write your own entry point instead.
  • migrations.ts — Legacy v1-to-v2 page migration. Only applies to pre-existing SynthOS installs.
  • sshTunnelManager.ts — Internal temp file names. Not user-facing.

API reference

Exports from synthos

Export Type Purpose
Customizer Class Base class to subclass for configuration.
RouteInstaller Type (config: SynthOSConfig, app: Application) => void
createConfig Function Builds the config object from customizer + options.
init Function Initializes the data folder (pages, themes, scripts).
server Function Creates and returns the Express app.
SynthOSConfig Interface The resolved config object passed throughout the system.

Customizer getters (override in subclass)

Getter Returns Default
productName string 'SynthOS'
localFolder string '.synthos'
requiredPagesFolders string[] ['default'] → built-in required-pages
defaultPagesFolders string[] ['default'] → built-in default-pages
defaultThemesFolders string[] ['default'] → built-in default-themes
defaultScriptsFolders string[] ['default'] → built-in default-scripts
staticFilesFolders string[] ['default'] → built-in static-files
serviceConnectorsFolders string[] ['default'] → built-in service-connectors
tabsListRoute string '/pages'

Customizer methods (call on instance)

Method Purpose
disable(...groups) Turn off feature groups.
enable(...groups) Turn feature groups back on.
isEnabled(group) Check if a group is active.
addRoutes(...installers) Register custom Express routes.
addRouteHints(...hints) Add LLM-visible API documentation.
addTransformInstructions(...instructions) Append rules to the page transform prompt.
addContextSections(...sections) Add custom context sections to every builder LLM call.