Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 350 additions & 0 deletions extending_synthos.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
# 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

```bash
npm install synthos
```

## Quick start

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

```typescript
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:

```bash
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. |

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

### Content folders

Override these to supply your own default pages, themes, scripts, etc. Each getter returns an absolute path to a folder on disk.

| Getter | Default location | Contents |
|--------|-----------------|----------|
| `requiredPagesFolder` | `required-pages/` | Built-in system pages (builder, settings). |
| `defaultPagesFolder` | `default-pages/` | Starter page templates copied on first init. |
| `defaultThemesFolder` | `default-themes/` | Theme CSS/JSON files. |
| `defaultScriptsFolder` | `default-scripts/` | Platform-specific terminal scripts. |
| `pageScriptsFolder` | `page-scripts/` | Versioned page runtime scripts (page-v2.js, etc.). |
| `serviceConnectorsFolder` | `service-connectors/` | Connector JSON definitions. |

```typescript
import path from 'path';

class MyApp extends Customizer {
get defaultPagesFolder() {
return path.join(__dirname, '../my-pages');
}
get defaultThemesFolder() {
return path.join(__dirname, '../my-themes');
}
}
```

When you don't override a folder, SynthOS uses its own built-in 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:

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

Re-enable later if needed:

```typescript
customizer.enable('search');
```

Check at runtime:

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

### Custom routes

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

```typescript
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:

```typescript
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):

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

### 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:

```typescript
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:

```typescript
// 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:

```typescript
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 defaultPagesFolder() {
return path.join(__dirname, '../acme-pages');
}

get defaultThemesFolder() {
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`:

```json
{
"name": "acme-builder",
"dependencies": {
"synthos": "^0.8.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'` |
| `requiredPagesFolder` | `string` | Built-in required-pages |
| `defaultPagesFolder` | `string` | Built-in default-pages |
| `defaultThemesFolder` | `string` | Built-in default-themes |
| `defaultScriptsFolder` | `string` | Built-in default-scripts |
| `pageScriptsFolder` | `string` | Built-in page-scripts |
| `serviceConnectorsFolder` | `string` | 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. |
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"page-scripts",
"required-pages",
"service-connectors",
"migration-rules",
"tests"
]
}
5 changes: 4 additions & 1 deletion src/agents/openclaw/gatewayManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface GatewayConfig {
enabled: boolean;
role: 'operator';
scopes: string[];
productName?: string;
}

interface PendingRequest {
Expand Down Expand Up @@ -290,6 +291,7 @@ export async function connectAgent(agent: {
url: string;
token: string;
sshTunnel?: { enabled: boolean; command: string; password: string };
productName?: string;
}): Promise<GatewayConnection> {
// Start SSH tunnel first if configured
if (agent.sshTunnel?.enabled && agent.sshTunnel.command) {
Expand All @@ -308,6 +310,7 @@ export async function connectAgent(agent: {
enabled: true,
role: 'operator',
scopes: ['operator.read', 'operator.write', 'operator.approvals'],
productName: agent.productName,
};
return connect(gwConfig);
}
Expand Down Expand Up @@ -502,7 +505,7 @@ function sendConnectRequest(conn: GatewayConnection): void {
permissions: {},
auth: { token: conn.config.token },
locale: 'en-US',
userAgent: 'SynthOS/1.0',
userAgent: `${conn.config.productName ?? 'SynthOS'}/1.0`,
},
};

Expand Down
Loading