Introduction
SaveState ships with adapters for ChatGPT, Claude, Gemini, and OpenAI Assistants API. But what if you're using a custom AI agent, an enterprise bot, or a platform we don't support yet?
That's where custom adapters come in.
This guide walks you through building a complete SaveState adapter from scratch. By the end, you'll have a working adapter that can:
- Detect your AI platform
- Extract conversations, memories, and configurations
- Restore snapshots back to the platform
- Integrate seamlessly with the SaveState CLI
Prerequisites
Before we start, make sure you have:
- Node.js 18+ (20+ recommended)
- TypeScript knowledge (intermediate level)
- SaveState CLI installed (
npm install -g savestate) - Familiarity with your target AI platform's data format
# Verify your setup
node --version # v20.x.x or higher
savestate --version # v0.2.x
Understanding the Adapter Interface
Every SaveState adapter implements the Adapter interface:
interface Adapter {
// Metadata
readonly id: string; // Unique identifier
readonly name: string; // Human-readable name
readonly platform: string; // Platform category
readonly version: string; // Adapter version
// Detection
detect(): Promise<boolean>;
identify(): Promise<PlatformMeta>;
// Core operations
extract(): Promise<Snapshot>;
restore(snapshot: Snapshot): Promise<void>;
// Capabilities (optional)
capabilities(): AdapterCapabilities;
}
| Method | Purpose |
|---|---|
detect() |
Returns true if this adapter can handle the current workspace |
identify() |
Returns metadata about the platform/account being backed up |
extract() |
Pulls all data from the platform into a Snapshot object |
restore() |
Pushes a Snapshot back to the platform |
capabilities() |
Declares what this adapter can and cannot do |
Project Setup
Let's create a new adapter project:
# Create project directory
mkdir savestate-adapter-myagent
cd savestate-adapter-myagent
# Initialize npm package
npm init -y
# Install dependencies
npm install typescript @types/node --save-dev
npm install savestate --save-peer
# Initialize TypeScript
npx tsc --init
Update your package.json:
{
"name": "@savestate/adapter-myagent",
"version": "0.1.0",
"description": "SaveState adapter for MyAgent platform",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"keywords": [
"savestate",
"savestate-adapter",
"ai-backup",
"myagent"
],
"peerDependencies": {
"savestate": "^0.2.0"
}
}
Important: The savestate-adapter keyword enables auto-discovery by the CLI.
Step 1: Define the Adapter Shell
Create src/index.ts:
import type { Adapter, Snapshot, PlatformMeta } from 'savestate';
export class MyAgentAdapter implements Adapter {
readonly id = 'myagent';
readonly name = 'MyAgent Platform';
readonly platform = 'myagent';
readonly version = '0.1.0';
async detect(): Promise<boolean> {
// TODO: Implement detection logic
return false;
}
async identify(): Promise<PlatformMeta> {
return {
platform: this.platform,
version: 'unknown',
accountId: 'local',
};
}
async extract(): Promise<Snapshot> {
// TODO: Implement extraction
throw new Error('Not implemented');
}
async restore(snapshot: Snapshot): Promise<void> {
// TODO: Implement restoration
throw new Error('Not implemented');
}
capabilities() {
return {
extract: ['identity', 'memory', 'conversations'],
restore: ['identity', 'memory'],
incremental: true,
search: true
};
}
}
export default MyAgentAdapter;
Step 2: Implement Detection
The detect() method tells SaveState whether this adapter can handle the current workspace.
import { access, readFile } from 'fs/promises';
import { join } from 'path';
async detect(): Promise<boolean> {
try {
// Check for MyAgent's signature file
const configPath = join(process.cwd(), '.myagent', 'config.json');
await access(configPath);
// Optionally verify file contents
const config = JSON.parse(await readFile(configPath, 'utf-8'));
return config.platform === 'myagent';
} catch {
return false;
}
}
Best practices for detection:
- Check for platform-specific files or directories
- Verify file contents when possible (not just existence)
- Return
falsegracefully on any error - Don't throw exceptions—just return
false
Step 3: Implement Extraction
This is the core of your adapter. The extract() method pulls all data into a Snapshot:
async extract(): Promise<Snapshot> {
// 1. Extract identity
const identity = await this.extractIdentity();
// 2. Extract memory
const memory = await this.extractMemory();
// 3. Extract conversations
const conversations = await this.extractConversations();
// 4. Build metadata
const meta = await this.buildMeta();
return {
identity,
memory,
conversations,
meta
};
}
private async extractIdentity() {
const personalityPath = join(process.cwd(), '.myagent', 'personality.md');
const configPath = join(process.cwd(), '.myagent', 'settings.json');
const personality = await this.safeReadFile(personalityPath, '');
const config = await this.safeReadJson(configPath, {});
return { personality, config, tools: {} };
}
private async safeReadFile(path: string, fallback: string) {
try {
return await readFile(path, 'utf-8');
} catch {
return fallback;
}
}
Handling Different Data Sources
File-based agents:
const data = await readFile('/path/to/data.json', 'utf-8');
API-based platforms:
const response = await fetch('https://api.platform.com/v1/conversations', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();
Database-backed agents:
import Database from 'better-sqlite3';
const db = new Database('/path/to/agent.db');
const rows = db.prepare('SELECT * FROM conversations').all();
Step 4: Implement Restoration
The restore() method writes a snapshot back to the platform:
async restore(snapshot: Snapshot): Promise<void> {
const basePath = join(process.cwd(), '.myagent');
// Ensure directory exists
await mkdir(basePath, { recursive: true });
// Restore identity
if (snapshot.identity.personality) {
await writeFile(
join(basePath, 'personality.md'),
snapshot.identity.personality
);
}
// Restore config
if (snapshot.identity.config) {
await writeFile(
join(basePath, 'settings.json'),
JSON.stringify(snapshot.identity.config, null, 2)
);
}
// Restore memory
if (snapshot.memory?.core) {
await mkdir(join(basePath, 'memory'), { recursive: true });
await writeFile(
join(basePath, 'memory', 'core.json'),
JSON.stringify(snapshot.memory.core, null, 2)
);
}
}
Handling Partial Restore
Not all platforms support full restoration. Declare your capabilities honestly:
capabilities() {
return {
extract: ['identity', 'memory', 'conversations'],
restore: ['identity', 'memory'], // Can only restore these
incremental: true,
search: true
};
}
Step 5: Publishing Your Adapter
Once your adapter is working, publish it to npm:
# Build
npm run build
# Test locally
npm link
savestate adapters # Should show your adapter
# Publish
npm publish --access public
Naming Convention
For automatic discovery, use one of these naming patterns:
@savestate/adapter-<name>(official namespace)savestate-adapter-<name>(community)- Any package with the
savestate-adapterkeyword
Contributing Back
Built an adapter for a popular platform? Consider contributing it to the SaveState organization:
- Fork
savestatedev/savestate - Add your adapter to
packages/adapters/<name>/ - Add tests
- Submit a PR
We're especially looking for adapters for:
- Microsoft Copilot
- Poe
- Character.ai
- Local LLMs (Ollama, LM Studio)
Your adapter could help thousands
The plugin system is designed to be simple, flexible, and discoverable. We can't wait to see what you build.
View on GitHubQuestions? Join our Discord community or open an issue on GitHub.