From 932280eb53c4215ba9efe72f8a684a965bb7fea4 Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 19 Nov 2025 09:49:24 +0200 Subject: [PATCH 1/2] Switch AgentFS SDK to use identifier-based API This switches the AgentFS SDK to use identifier-based API instead of SQLite database files in preparation for adding sync to Turso Cloud. With explicit agent filesystem identifiers, we can have a naming convention to manage database files on the cloud, reducing agent configuration. Before this change, you would open an agent filesystem with: ```typescript // Persisten filesystem: const agent = await AgentFS.open('./agent.db'); // Ephemeral filesystem: const agent = await AgentFS.open(':memory':); ``` Now you do the same with: ```typescript // Persistent filesystem (creates `.agentfs/my-agent.db`): const agent = await AgentFS.open({ id: 'my-agent' }); // Ephemeral filesystem: const agent = await AgentFS.open(); ``` --- README.md | 26 +++-- cli/src/main.rs | 99 ++++++++++++------- .../research-assistant/.env.example | 4 +- .../research-assistant/.gitignore | 6 +- .../research-assistant/src/utils/agentfs.ts | 4 +- examples/mastra/research-assistant/.gitignore | 3 + .../src/mastra/utils/agentfs.ts | 4 +- .../research-assistant/.env.example | 4 +- .../research-assistant/.gitignore | 3 + .../research-assistant/src/utils/agentfs.ts | 4 +- sdk/rust/.gitignore | 3 + sdk/rust/src/lib.rs | 98 +++++++++++++++++- sdk/typescript/.gitignore | 3 + sdk/typescript/examples/filesystem/index.ts | 3 +- sdk/typescript/examples/kvstore/index.ts | 4 +- sdk/typescript/examples/toolcalls/index.ts | 4 +- sdk/typescript/src/index.ts | 54 +++++++++- sdk/typescript/tests/index.test.ts | 80 +++++++++------ 18 files changed, 311 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index a3a3abe..6daf5e8 100644 --- a/README.md +++ b/README.md @@ -39,17 +39,26 @@ Read more about the motivation for AgentFS in the announcement [blog post](https Initialize an agent filesystem: ```bash -$ agentfs init -Created agent filesystem: agent.db +$ agentfs init my-agent +Created agent filesystem: .agentfs/my-agent.db +Agent ID: my-agent ``` -Inspect the agent filesystem from outside: +Inspect the agent filesystem: ```bash -$ agentfs fs ls +$ agentfs fs ls my-agent +Using agent: my-agent f hello.txt -$ agentfs fs cat hello.txt +$ agentfs fs cat my-agent hello.txt +hello from agent +``` + +You can also use a database path directly: + +```bash +$ agentfs fs cat .agentfs/my-agent.db hello.txt hello from agent ``` @@ -80,7 +89,12 @@ Use it in your agent code: ```typescript import { AgentFS } from 'agentfs-sdk'; -const agent = await AgentFS.open('./agent.db'); +// Persistent storage with identifier +const agent = await AgentFS.open({ id: 'my-agent' }); +// Creates: .agentfs/my-agent.db + +// Or use ephemeral in-memory database +const ephemeralAgent = await AgentFS.open(); // Key-value operations await agent.kv.set('user:preferences', { theme: 'dark' }); diff --git a/cli/src/main.rs b/cli/src/main.rs index 73ac3b2..69f7901 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -48,9 +48,8 @@ struct Args { enum Commands { /// Initialize a new agent filesystem Init { - /// SQLite file to create (default: agent.db) - #[arg(default_value = "agent.db")] - filename: PathBuf, + /// Agent identifier (if not provided, generates a unique one) + id: Option, /// Overwrite existing file if it exists #[arg(long)] @@ -83,51 +82,87 @@ enum Commands { enum FsCommands { /// List files in the filesystem Ls { - /// Filesystem to use (default: agent.db) - #[arg(long = "filesystem", default_value = "agent.db")] - filesystem: PathBuf, + /// Agent ID or database path + id_or_path: String, /// Path to list (default: /) #[arg(default_value = "/")] - path: String, + fs_path: String, }, /// Display file contents Cat { - /// Filesystem to use (default: agent.db) - #[arg(long = "filesystem", default_value = "agent.db")] - filesystem: PathBuf, + /// Agent ID or database path + id_or_path: String, - /// Path to the file - path: String, + /// Path to the file in the filesystem + file_path: String, }, } -async fn init_database(db_path: &Path, force: bool) -> AnyhowResult<()> { - // Check if file already exists +fn resolve_agent_id(id_or_path: String) -> AnyhowResult<(String, PathBuf)> { + let agentfs_dir = Path::new(".agentfs"); + + // Check if it looks like a path (contains / or ends with .db) + if id_or_path.contains('/') || id_or_path.ends_with(".db") { + // Treat as a database path + let db_path = PathBuf::from(&id_or_path); + if !db_path.exists() { + anyhow::bail!("Database '{}' not found", db_path.display()); + } + let id = db_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("unknown") + .to_string(); + Ok((id, db_path)) + } else { + // Treat as an agent ID + let db_path = agentfs_dir.join(format!("{}.db", id_or_path)); + if !db_path.exists() { + anyhow::bail!("Agent '{}' not found at '{}'", id_or_path, db_path.display()); + } + Ok((id_or_path, db_path)) + } +} + +async fn init_database(id: Option, force: bool) -> AnyhowResult<()> { + use agentfs_sdk::AgentFSOptions; + use std::time::{SystemTime, UNIX_EPOCH}; + + // Generate ID if not provided + let id = id.unwrap_or_else(|| { + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + format!("agent-{}", timestamp) + }); + + // Check if agent already exists + let db_path = Path::new(".agentfs").join(format!("{}.db", id)); if db_path.exists() && !force { anyhow::bail!( - "File '{}' already exists. Use --force to overwrite.", + "Agent '{}' already exists at '{}'. Use --force to overwrite.", + id, db_path.display() ); } - let db_path_str = db_path.to_str().context("Invalid database path")?; - // Use the SDK to initialize the database - this ensures consistency - // with how `agentfs run` initializes the database - AgentFS::new(db_path_str) + // The SDK will create .agentfs directory and database file + AgentFS::open(AgentFSOptions::with_id(&id)) .await .context("Failed to initialize database")?; eprintln!("Created agent filesystem: {}", db_path.display()); + eprintln!("Agent ID: {}", id); Ok(()) } -async fn ls_filesystem(db_path: &Path, path: &str) -> AnyhowResult<()> { - if !db_path.exists() { - anyhow::bail!("Filesystem '{}' does not exist", db_path.display()); - } +async fn ls_filesystem(id: Option, path: &str) -> AnyhowResult<()> { + let (agent_id, db_path) = resolve_agent_id(id)?; + eprintln!("Using agent: {}", agent_id); let db_path_str = db_path.to_str().context("Invalid filesystem path")?; @@ -212,10 +247,8 @@ async fn ls_filesystem(db_path: &Path, path: &str) -> AnyhowResult<()> { Ok(()) } -async fn cat_filesystem(db_path: &Path, path: &str) -> AnyhowResult<()> { - if !db_path.exists() { - anyhow::bail!("Filesystem '{}' does not exist", db_path.display()); - } +async fn cat_filesystem(id: Option, path: &str) -> AnyhowResult<()> { + let (_agent_id, db_path) = resolve_agent_id(id)?; let db_path_str = db_path.to_str().context("Invalid filesystem path")?; @@ -326,23 +359,23 @@ async fn main() { let args = Args::parse(); match args.command { - Commands::Init { filename, force } => { - if let Err(e) = init_database(&filename, force).await { + Commands::Init { id, force } => { + if let Err(e) = init_database(id, force).await { eprintln!("Error: {}", e); std::process::exit(1); } std::process::exit(0); } Commands::Fs { command } => match command { - FsCommands::Ls { filesystem, path } => { - if let Err(e) = ls_filesystem(&filesystem, &path).await { + FsCommands::Ls { id_or_path, fs_path } => { + if let Err(e) = ls_filesystem(id_or_path, &fs_path).await { eprintln!("Error: {}", e); std::process::exit(1); } std::process::exit(0); } - FsCommands::Cat { filesystem, path } => { - if let Err(e) = cat_filesystem(&filesystem, &path).await { + FsCommands::Cat { id_or_path, file_path } => { + if let Err(e) = cat_filesystem(id_or_path, &file_path).await { eprintln!("Error: {}", e); std::process::exit(1); } diff --git a/examples/claude-agent/research-assistant/.env.example b/examples/claude-agent/research-assistant/.env.example index ef6b3fc..9328d82 100644 --- a/examples/claude-agent/research-assistant/.env.example +++ b/examples/claude-agent/research-assistant/.env.example @@ -1,5 +1,5 @@ # Anthropic API Key (required) ANTHROPIC_API_KEY=your-api-key-here -# Optional: Custom AgentFS database path -# AGENTFS_DB=agentfs.db +# Optional: Custom AgentFS agent ID +# AGENTFS_ID=research-assistant diff --git a/examples/claude-agent/research-assistant/.gitignore b/examples/claude-agent/research-assistant/.gitignore index 6a6cc91..1d90bee 100644 --- a/examples/claude-agent/research-assistant/.gitignore +++ b/examples/claude-agent/research-assistant/.gitignore @@ -8,10 +8,8 @@ dist/ .env .env.local -# AgentFS database -agentfs.db -agentfs.db-shm -agentfs.db-wal +# AgentFS local databases +.agentfs/ # IDE .vscode/ diff --git a/examples/claude-agent/research-assistant/src/utils/agentfs.ts b/examples/claude-agent/research-assistant/src/utils/agentfs.ts index 164171b..5bb7985 100644 --- a/examples/claude-agent/research-assistant/src/utils/agentfs.ts +++ b/examples/claude-agent/research-assistant/src/utils/agentfs.ts @@ -4,8 +4,8 @@ let instance: AgentFS | null = null; export async function getAgentFS(): Promise { if (!instance) { - const dbPath = process.env.AGENTFS_DB || 'agentfs.db'; - instance = await AgentFS.open(dbPath); + const id = process.env.AGENTFS_ID || 'research-assistant'; + instance = await AgentFS.open({ id }); } return instance; } diff --git a/examples/mastra/research-assistant/.gitignore b/examples/mastra/research-assistant/.gitignore index 5c90d73..139896d 100644 --- a/examples/mastra/research-assistant/.gitignore +++ b/examples/mastra/research-assistant/.gitignore @@ -6,3 +6,6 @@ dist .env *.db *.db-* + +# AgentFS local databases +.agentfs/ diff --git a/examples/mastra/research-assistant/src/mastra/utils/agentfs.ts b/examples/mastra/research-assistant/src/mastra/utils/agentfs.ts index 650179b..63cfcf9 100644 --- a/examples/mastra/research-assistant/src/mastra/utils/agentfs.ts +++ b/examples/mastra/research-assistant/src/mastra/utils/agentfs.ts @@ -4,8 +4,8 @@ let instance: AgentFS | null = null; export async function getAgentFS(): Promise { if (!instance) { - const dbPath = process.env.AGENTFS_DB || 'agentfs.db'; - instance = await AgentFS.open(dbPath); + const id = process.env.AGENTFS_ID || 'research-assistant'; + instance = await AgentFS.open({ id }); } return instance; } diff --git a/examples/openai-agents/research-assistant/.env.example b/examples/openai-agents/research-assistant/.env.example index 44f7b55..47c567a 100644 --- a/examples/openai-agents/research-assistant/.env.example +++ b/examples/openai-agents/research-assistant/.env.example @@ -1,5 +1,5 @@ # OpenAI API Key (required) OPENAI_API_KEY=your-api-key-here -# Optional: Custom AgentFS database path -# AGENTFS_DB=agentfs.db +# Optional: Custom AgentFS agent ID +# AGENTFS_ID=research-assistant diff --git a/examples/openai-agents/research-assistant/.gitignore b/examples/openai-agents/research-assistant/.gitignore index 91e505f..6407ce9 100644 --- a/examples/openai-agents/research-assistant/.gitignore +++ b/examples/openai-agents/research-assistant/.gitignore @@ -3,3 +3,6 @@ dist/ .env *.db *.log + +# AgentFS local databases +.agentfs/ diff --git a/examples/openai-agents/research-assistant/src/utils/agentfs.ts b/examples/openai-agents/research-assistant/src/utils/agentfs.ts index 164171b..5bb7985 100644 --- a/examples/openai-agents/research-assistant/src/utils/agentfs.ts +++ b/examples/openai-agents/research-assistant/src/utils/agentfs.ts @@ -4,8 +4,8 @@ let instance: AgentFS | null = null; export async function getAgentFS(): Promise { if (!instance) { - const dbPath = process.env.AGENTFS_DB || 'agentfs.db'; - instance = await AgentFS.open(dbPath); + const id = process.env.AGENTFS_ID || 'research-assistant'; + instance = await AgentFS.open({ id }); } return instance; } diff --git a/sdk/rust/.gitignore b/sdk/rust/.gitignore index eb5a316..db7d859 100644 --- a/sdk/rust/.gitignore +++ b/sdk/rust/.gitignore @@ -1 +1,4 @@ target + +# AgentFS local databases +.agentfs/ diff --git a/sdk/rust/src/lib.rs b/sdk/rust/src/lib.rs index e2cee1a..5798222 100644 --- a/sdk/rust/src/lib.rs +++ b/sdk/rust/src/lib.rs @@ -3,6 +3,7 @@ pub mod kvstore; pub mod toolcalls; use anyhow::Result; +use std::path::Path; use std::sync::Arc; use turso::{Builder, Connection}; @@ -10,6 +11,29 @@ pub use filesystem::{Filesystem, Stats}; pub use kvstore::KvStore; pub use toolcalls::{ToolCall, ToolCallStats, ToolCallStatus, ToolCalls}; +/// Configuration options for opening an AgentFS instance +#[derive(Debug, Clone, Default)] +pub struct AgentFSOptions { + /// Optional unique identifier for the agent. + /// - If Some(id): Creates persistent storage at `.agentfs/{id}.db` + /// - If None: Uses ephemeral in-memory database + pub id: Option, +} + +impl AgentFSOptions { + /// Create options for a persistent agent with the given ID + pub fn with_id(id: impl Into) -> Self { + Self { + id: Some(id.into()), + } + } + + /// Create options for an ephemeral in-memory agent + pub fn ephemeral() -> Self { + Self { id: None } + } +} + /// The main AgentFS SDK struct /// /// This provides a unified interface to the filesystem, key-value store, @@ -22,10 +46,62 @@ pub struct AgentFS { } impl AgentFS { - /// Create a new AgentFS instance + /// Open an AgentFS instance + /// + /// # Arguments + /// * `options` - Configuration options (use Default::default() for ephemeral) + /// + /// # Examples + /// ```no_run + /// use agentfs_sdk::{AgentFS, AgentFSOptions}; + /// + /// # async fn example() -> anyhow::Result<()> { + /// // Persistent storage + /// let agent = AgentFS::open(AgentFSOptions::with_id("my-agent")).await?; + /// + /// // Ephemeral in-memory + /// let agent = AgentFS::open(AgentFSOptions::ephemeral()).await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn open(options: AgentFSOptions) -> Result { + // Determine database path based on id + let db_path = if let Some(id) = options.id { + // Ensure .agentfs directory exists + let agentfs_dir = Path::new(".agentfs"); + if !agentfs_dir.exists() { + std::fs::create_dir_all(agentfs_dir)?; + } + format!(".agentfs/{}.db", id) + } else { + // No id = ephemeral in-memory database + ":memory:".to_string() + }; + + let db = Builder::new_local(&db_path).build().await?; + let conn = db.connect()?; + let conn = Arc::new(conn); + + let kv = KvStore::from_connection(conn.clone()).await?; + let fs = Filesystem::from_connection(conn.clone()).await?; + let tools = ToolCalls::from_connection(conn.clone()).await?; + + Ok(Self { + conn, + kv, + fs, + tools, + }) + } + + /// Create a new AgentFS instance (deprecated, use `open` instead) /// /// # Arguments /// * `db_path` - Path to the SQLite database file (use ":memory:" for in-memory database) + #[deprecated( + since = "0.2.0", + note = "Use AgentFS::open with AgentFSOptions instead" + )] pub async fn new(db_path: &str) -> Result { let db = Builder::new_local(db_path).build().await?; let conn = db.connect()?; @@ -55,14 +131,26 @@ mod tests { #[tokio::test] async fn test_agentfs_creation() { - let agentfs = AgentFS::new(":memory:").await.unwrap(); + let agentfs = AgentFS::open(AgentFSOptions::ephemeral()).await.unwrap(); // Just verify we can get the connection let _conn = agentfs.get_connection(); } + #[tokio::test] + async fn test_agentfs_with_id() { + let agentfs = AgentFS::open(AgentFSOptions::with_id("test-agent")) + .await + .unwrap(); + // Just verify we can get the connection + let _conn = agentfs.get_connection(); + + // Cleanup + let _ = std::fs::remove_file(".agentfs/test-agent.db"); + } + #[tokio::test] async fn test_kv_operations() { - let agentfs = AgentFS::new(":memory:").await.unwrap(); + let agentfs = AgentFS::open(AgentFSOptions::ephemeral()).await.unwrap(); // Set a value agentfs.kv.set("test_key", &"test_value").await.unwrap(); @@ -81,7 +169,7 @@ mod tests { #[tokio::test] async fn test_filesystem_operations() { - let agentfs = AgentFS::new(":memory:").await.unwrap(); + let agentfs = AgentFS::open(AgentFSOptions::ephemeral()).await.unwrap(); // Create a directory agentfs.fs.mkdir("/test_dir").await.unwrap(); @@ -115,7 +203,7 @@ mod tests { #[tokio::test] async fn test_tool_calls() { - let agentfs = AgentFS::new(":memory:").await.unwrap(); + let agentfs = AgentFS::open(AgentFSOptions::ephemeral()).await.unwrap(); // Start a tool call let id = agentfs diff --git a/sdk/typescript/.gitignore b/sdk/typescript/.gitignore index 1dc6d18..3d88779 100644 --- a/sdk/typescript/.gitignore +++ b/sdk/typescript/.gitignore @@ -19,6 +19,9 @@ dist/ *.db-shm *.db-wal +# AgentFS local databases +.agentfs/ + # Logs *.log npm-debug.log* diff --git a/sdk/typescript/examples/filesystem/index.ts b/sdk/typescript/examples/filesystem/index.ts index 25d3490..f6fbc33 100644 --- a/sdk/typescript/examples/filesystem/index.ts +++ b/sdk/typescript/examples/filesystem/index.ts @@ -1,7 +1,8 @@ import { AgentFS } from "agentfs-sdk"; async function main() { - const agentfs = await AgentFS.open(":memory:"); + // Initialize AgentFS with persistent storage + const agentfs = await AgentFS.open({ id: "filesystem-demo" }); // Write a file console.log("Writing file..."); diff --git a/sdk/typescript/examples/kvstore/index.ts b/sdk/typescript/examples/kvstore/index.ts index d359b8b..331826b 100644 --- a/sdk/typescript/examples/kvstore/index.ts +++ b/sdk/typescript/examples/kvstore/index.ts @@ -1,8 +1,8 @@ import { AgentFS } from "agentfs-sdk"; async function main() { - // Initialize AgentFS (in-memory for this example) - const agentfs = await AgentFS.open(":memory:"); + // Initialize AgentFS with persistent storage + const agentfs = await AgentFS.open({ id: "kvstore-demo" }); console.log("=== KvStore Example ===\n"); diff --git a/sdk/typescript/examples/toolcalls/index.ts b/sdk/typescript/examples/toolcalls/index.ts index 0d3ba72..dc7478b 100644 --- a/sdk/typescript/examples/toolcalls/index.ts +++ b/sdk/typescript/examples/toolcalls/index.ts @@ -1,8 +1,8 @@ import { AgentFS } from '../../src'; async function main() { - // Create an agent with an in-memory database - const agentfs = await AgentFS.open(':memory:'); + // Create an agent with persistent storage + const agentfs = await AgentFS.open({ id: 'toolcalls-demo' }); console.log('=== Tool Call Tracking Example ===\n'); diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index da00b51..764444b 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -1,8 +1,23 @@ import { Database } from '@tursodatabase/database'; +import { existsSync, mkdirSync } from 'fs'; import { KvStore } from './kvstore'; import { Filesystem } from './filesystem'; import { ToolCalls } from './toolcalls'; +/** + * Configuration options for opening an AgentFS instance + */ +export interface AgentFSOptions { + /** + * Optional unique identifier for the agent. + * - If provided: Creates persistent storage at `.agentfs/{id}.db` + * - If omitted: Uses ephemeral in-memory database + */ + id?: string; + // Future: sync configuration will be added here + // sync?: SyncConfig; +} + export class AgentFS { private db: Database; @@ -22,10 +37,45 @@ export class AgentFS { /** * Open an agent filesystem - * @param dbPath Path to the database file (defaults to ':memory:') + * @param options Configuration options (optional id for persistent storage) * @returns Fully initialized AgentFS instance + * @example + * ```typescript + * // Persistent storage + * const agent = await AgentFS.open({ id: 'my-agent' }); + * // Creates: .agentfs/my-agent.db + * + * // Ephemeral in-memory database + * const agent = await AgentFS.open(); + * ``` */ - static async open(dbPath: string = ':memory:'): Promise { + static async open(options?: AgentFSOptions): Promise { + // Error handling for old API usage + if (typeof options === 'string') { + throw new Error( + `AgentFS.open() no longer accepts string paths. ` + + `Please use: AgentFS.open({ id: 'your-id' }) for persistent storage, ` + + `or AgentFS.open({}) for ephemeral in-memory database. ` + + `See migration guide: https://github.com/tursodatabase/agentfs#migration-from-01x-to-02x` + ); + } + + const { id } = options || {}; + + // Determine database path based on id + let dbPath: string; + if (!id) { + // No id = ephemeral in-memory database + dbPath = ':memory:'; + } else { + // Ensure .agentfs directory exists + const dir = '.agentfs'; + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + dbPath = `${dir}/${id}.db`; + } + const db = new Database(dbPath); // Connect to the database to ensure it's created diff --git a/sdk/typescript/tests/index.test.ts b/sdk/typescript/tests/index.test.ts index dc36bac..ee8e00e 100644 --- a/sdk/typescript/tests/index.test.ts +++ b/sdk/typescript/tests/index.test.ts @@ -1,76 +1,96 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { AgentFS } from '../src/index'; -import { mkdtempSync, rmSync } from 'fs'; -import { tmpdir } from 'os'; -import { join } from 'path'; +import { existsSync, rmSync } from 'fs'; describe('AgentFS Integration Tests', () => { let agent: AgentFS; - let tempDir: string; - let dbPath: string; + const testId = 'test-agent'; beforeEach(async () => { - // Create temporary directory for test database - tempDir = mkdtempSync(join(tmpdir(), 'agentfs-test-')); - dbPath = join(tempDir, 'test.db'); - - // Initialize AgentFS - agent = await AgentFS.open(dbPath); + // Initialize AgentFS with a test id + agent = await AgentFS.open({ id: testId }); }); - afterEach(() => { - // Clean up temporary directories + afterEach(async () => { + // Close the agent + await agent.close(); + + // Clean up test database file + const dbPath = `.agentfs/${testId}.db`; try { - rmSync(tempDir, { recursive: true, force: true }); + if (existsSync(dbPath)) { + rmSync(dbPath, { force: true }); + } + // Clean up SQLite WAL files if they exist + if (existsSync(`${dbPath}-shm`)) { + rmSync(`${dbPath}-shm`, { force: true }); + } + if (existsSync(`${dbPath}-wal`)) { + rmSync(`${dbPath}-wal`, { force: true }); + } } catch { // Ignore cleanup errors } }); describe('Initialization', () => { - it('should successfully initialize with a file path', async () => { + it('should successfully initialize with an id', async () => { expect(agent).toBeDefined(); expect(agent).toBeInstanceOf(AgentFS); }); - it('should initialize with in-memory database', async () => { - const memoryAgent = await AgentFS.open(':memory:'); + it('should initialize with ephemeral in-memory database', async () => { + const memoryAgent = await AgentFS.open(); expect(memoryAgent).toBeDefined(); expect(memoryAgent).toBeInstanceOf(AgentFS); + await memoryAgent.close(); }); - it('should allow multiple instances with different databases', async () => { - const dbPath2 = join(tempDir, 'test2.db'); - const agent2 = await AgentFS.open(dbPath2); + it('should allow multiple instances with different ids', async () => { + const agent2 = await AgentFS.open({ id: 'test-agent-2' }); expect(agent).toBeDefined(); expect(agent2).toBeDefined(); expect(agent).not.toBe(agent2); + + await agent2.close(); + // Clean up second agent's database + const dbPath2 = '.agentfs/test-agent-2.db'; + if (existsSync(dbPath2)) { + rmSync(dbPath2, { force: true }); + } }); }); describe('Database Persistence', () => { - it('should persist database file to disk', async () => { - // Use the agent from beforeEach - // Check that database file exists - const fs = require('fs'); - expect(fs.existsSync(dbPath)).toBe(true); + it('should persist database file to .agentfs directory', async () => { + // Check that database file exists in .agentfs directory + const dbPath = `.agentfs/${testId}.db`; + expect(existsSync(dbPath)).toBe(true); }); - it('should reuse existing database file', async () => { + it('should reuse existing database file with same id', async () => { // Create first instance and write data - const testDbPath = join(tempDir, 'persistence-test.db'); - const agent1 = await AgentFS.open(testDbPath); + const persistenceTestId = 'persistence-test'; + const agent1 = await AgentFS.open({ id: persistenceTestId }); await agent1.kv.set('test', 'value1'); await agent1.close(); - // Create second instance with same path - should be able to read the data - const agent2 = await AgentFS.open(testDbPath); + // Create second instance with same id - should be able to read the data + const agent2 = await AgentFS.open({ id: persistenceTestId }); const value = await agent2.kv.get('test'); expect(agent1).toBeDefined(); expect(agent2).toBeDefined(); expect(value).toBe('value1'); + + await agent2.close(); + + // Clean up + const dbPath = `.agentfs/${persistenceTestId}.db`; + if (existsSync(dbPath)) { + rmSync(dbPath, { force: true }); + } }); }); From 4e4276cbb0393cbea21bd415e8f66ce63e89e68b Mon Sep 17 00:00:00 2001 From: Pekka Enberg Date: Wed, 19 Nov 2025 13:14:26 +0200 Subject: [PATCH 2/2] Add support for syncing filesystem to Turso Cloud --- sdk/typescript/examples/turso-cloud/README.md | 187 ++++++++++++++ sdk/typescript/examples/turso-cloud/custom.ts | 108 ++++++++ sdk/typescript/examples/turso-cloud/simple.ts | 71 ++++++ sdk/typescript/package.json | 10 +- sdk/typescript/src/index.ts | 53 +++- sdk/typescript/src/providers/turso.ts | 234 ++++++++++++++++++ sdk/typescript/src/sync.ts | 124 ++++++++++ 7 files changed, 779 insertions(+), 8 deletions(-) create mode 100644 sdk/typescript/examples/turso-cloud/README.md create mode 100644 sdk/typescript/examples/turso-cloud/custom.ts create mode 100644 sdk/typescript/examples/turso-cloud/simple.ts create mode 100644 sdk/typescript/src/providers/turso.ts create mode 100644 sdk/typescript/src/sync.ts diff --git a/sdk/typescript/examples/turso-cloud/README.md b/sdk/typescript/examples/turso-cloud/README.md new file mode 100644 index 0000000..114e990 --- /dev/null +++ b/sdk/typescript/examples/turso-cloud/README.md @@ -0,0 +1,187 @@ +# Turso Cloud Sync Examples + +These examples demonstrate how to use AgentFS with Turso Cloud for automatic synchronization between local and cloud databases. + +## Prerequisites + +1. **Turso Account**: Sign up at [turso.tech](https://turso.tech) +2. **API Token**: Get your API token from the [Turso dashboard](https://turso.tech/app) +3. **Install Dependencies**: + ```bash + npm install agentfs-sdk @tursodatabase/api @tursodatabase/sync + ``` + +## Environment Setup + +Set your Turso API token: + +```bash +export TURSO_API_TOKEN="your-token-here" +``` + +Optional environment variables: + +```bash +export TURSO_API_ORG="your-org-name" # Default: your primary org +export TURSO_DATABASE_URL="database-name" # Default: agent id +export TURSO_AUTO_SYNC="true" # Default: true +export TURSO_SYNC_INTERVAL="60000" # Default: 60000 (60s) +``` + +## Examples + +### 1. Simple Example (`simple.ts`) + +Minimal setup using environment variables and defaults. + +```bash +# Run the example +npx tsx simple.ts +``` + +**What it does:** +- Creates a database named `simple-agent` in Turso Cloud +- Stores data locally +- Syncs to cloud +- Enables auto-sync every 60 seconds + +**Code:** +```typescript +const agent = await AgentFS.open({ + id: 'simple-agent', + sync: tursoSync() +}); + +await agent.kv.set('key', 'value'); +await agent.sync.push(); +``` + +### 2. Custom Configuration (`custom.ts`) + +Advanced configuration with custom options. + +```bash +# Run the example +npx tsx custom.ts +``` + +**What it does:** +- Demonstrates manual sync control +- Shows custom sync intervals +- Explains all sync operations (pull/push/sync) +- Displays sync status + +**Code:** +```typescript +const agent = await AgentFS.open({ + id: 'agent-custom', + sync: tursoSync({ + org: 'my-org', + databaseUrl: 'custom-db-name', + autoSync: false, + interval: 10000 + }) +}); + +await agent.sync.pull(); // Pull from cloud +await agent.sync.push(); // Push to cloud +await agent.sync.sync(); // Bidirectional +``` + +## Sync Operations + +### `agent.sync.push()` +Push local changes to the cloud database. + +### `agent.sync.pull()` +Pull remote changes from the cloud database to local. + +### `agent.sync.sync()` +Bidirectional sync (pull + push). + +### `agent.sync.getStatus()` +Get current sync status: +```typescript +{ + state: 'idle' | 'syncing' | 'error', + lastSync?: Date, + lastError?: string +} +``` + +## Configuration Options + +All options are optional with smart defaults: + +| Option | Environment Variable | Default | Description | +|--------|---------------------|---------|-------------| +| `org` | `TURSO_API_ORG` | - | Turso organization name | +| `apiToken` | `TURSO_API_TOKEN` | - | **Required**: API token | +| `databaseUrl` | `TURSO_DATABASE_URL` | agent id | Database name or URL | +| `autoSync` | `TURSO_AUTO_SYNC` | `true` | Enable automatic sync | +| `interval` | `TURSO_SYNC_INTERVAL` | `60000` | Auto-sync interval (ms) | + +## Use Cases + +### Local-First Development +```typescript +const agent = await AgentFS.open({ + id: 'dev-agent', + sync: tursoSync({ autoSync: false }) +}); + +// Work locally, sync manually when ready +await agent.sync.push(); +``` + +### Real-Time Collaboration +```typescript +const agent = await AgentFS.open({ + id: 'collab-agent', + sync: tursoSync({ interval: 5000 }) // Sync every 5s +}); +``` + +### Backup & Recovery +```typescript +const agent = await AgentFS.open({ + id: 'backup-agent', + sync: tursoSync({ interval: 300000 }) // Sync every 5 minutes +}); +``` + +## Important Notes + +⚠️ **@tursodatabase/sync is ALPHA**: The sync package is in alpha stage. While functional, it's recommended for development and testing only. Do not use for critical production data yet. + +### Sync Behavior +- **Remote is source of truth**: Conflicts are resolved with Last-Push-Wins strategy +- **Eventually consistent**: Local replica becomes byte-identical to remote +- **Automatic provisioning**: Databases are created automatically if they don't exist + +## Troubleshooting + +### Error: Missing TURSO_API_TOKEN +Set the environment variable: +```bash +export TURSO_API_TOKEN="your-token" +``` + +### Error: @tursodatabase/api not found +Install optional dependencies: +```bash +npm install @tursodatabase/api @tursodatabase/sync +``` + +### Sync Errors +Check sync status: +```typescript +const status = agent.sync.getStatus(); +console.log(status.lastError); +``` + +## Next Steps + +- Read the [Turso Documentation](https://docs.turso.tech) +- Explore [AgentFS API Reference](../../README.md) +- Join the [Turso Discord](https://discord.gg/turso) diff --git a/sdk/typescript/examples/turso-cloud/custom.ts b/sdk/typescript/examples/turso-cloud/custom.ts new file mode 100644 index 0000000..67af732 --- /dev/null +++ b/sdk/typescript/examples/turso-cloud/custom.ts @@ -0,0 +1,108 @@ +import { AgentFS, tursoSync } from "agentfs-sdk"; + +/** + * Advanced Turso Cloud Sync Example + * + * This example demonstrates custom configuration options for Turso Cloud sync: + * - Explicit organization and API token + * - Custom database name + * - Disabled auto-sync (manual control only) + * - Custom sync interval + * + * Use cases: + * - Production environments with specific requirements + * - Testing different sync strategies + * - Connecting to existing databases + * - Multiple agents with different sync policies + */ +async function main() { + console.log("=== Advanced Turso Cloud Sync Example ===\n"); + + // Example 1: Custom configuration with manual sync + console.log("Example 1: Manual sync control\n"); + + const agent1 = await AgentFS.open({ + id: "agent-manual", + sync: tursoSync({ + org: process.env.TURSO_API_ORG || "default", + apiToken: process.env.TURSO_API_TOKEN!, + databaseUrl: "agent-manual-db", // Custom database name + autoSync: false, // Disable auto-sync + }), + }); + + console.log("✓ Agent with manual sync created"); + + // Store data + await agent1.kv.set("mode", "manual"); + await agent1.kv.set("sync_strategy", "on-demand"); + + console.log("✓ Data stored locally"); + + // Manually control when to sync + console.log("Pushing changes to cloud..."); + await agent1.sync!.push(); + console.log("✓ Changes pushed\n"); + + await agent1.close(); + + // Example 2: Custom sync interval + console.log("Example 2: Fast auto-sync (every 10 seconds)\n"); + + const agent2 = await AgentFS.open({ + id: "agent-fast-sync", + sync: tursoSync({ + databaseUrl: "agent-fast-sync-db", + autoSync: true, + interval: 10000, // Sync every 10 seconds + }), + }); + + console.log("✓ Agent with fast auto-sync created"); + + await agent2.kv.set("sync_interval", "10s"); + await agent2.kv.set("use_case", "real-time collaboration"); + + console.log("✓ Data stored (will auto-sync every 10 seconds)"); + + // Demonstrate sync operations + console.log("\nDemonstrating sync operations:"); + + // Pull (get latest from cloud) + console.log("- Pulling latest changes..."); + await agent2.sync!.pull(); + console.log(" ✓ Pull complete"); + + // Push (send local changes to cloud) + console.log("- Pushing local changes..."); + await agent2.sync!.push(); + console.log(" ✓ Push complete"); + + // Sync (bidirectional - pull + push) + console.log("- Bidirectional sync..."); + await agent2.sync!.sync(); + console.log(" ✓ Sync complete"); + + // Check status + const status = agent2.sync!.getStatus(); + console.log("\nSync status:", { + state: status.state, + lastSync: status.lastSync?.toISOString(), + lastError: status.lastError || "none", + }); + + await agent2.close(); + + console.log("\n=== Examples Complete ==="); + console.log("\nConfiguration options summary:"); + console.log("- org: Turso organization (env: TURSO_API_ORG)"); + console.log("- apiToken: API token (env: TURSO_API_TOKEN)"); + console.log("- databaseUrl: Database name or URL (env: TURSO_DATABASE_URL)"); + console.log("- autoSync: Enable/disable auto-sync (env: TURSO_AUTO_SYNC)"); + console.log("- interval: Sync interval in ms (env: TURSO_SYNC_INTERVAL)"); +} + +main().catch((error) => { + console.error("Error:", error.message); + process.exit(1); +}); diff --git a/sdk/typescript/examples/turso-cloud/simple.ts b/sdk/typescript/examples/turso-cloud/simple.ts new file mode 100644 index 0000000..69bf979 --- /dev/null +++ b/sdk/typescript/examples/turso-cloud/simple.ts @@ -0,0 +1,71 @@ +import { AgentFS, tursoSync } from "agentfs-sdk"; + +/** + * Simple Turso Cloud Sync Example + * + * This example demonstrates the minimal setup for syncing an AgentFS instance + * with Turso Cloud using environment variables. + * + * Prerequisites: + * - Set TURSO_API_TOKEN environment variable with your Turso API token + * - Optionally set TURSO_API_ORG (defaults to your primary org) + * + * The sync provider will automatically: + * 1. Create a database named 'simple-agent' if it doesn't exist + * 2. Generate an auth token for the database + * 3. Setup bidirectional sync + * 4. Enable auto-sync every 60 seconds + */ +async function main() { + console.log("=== Simple Turso Cloud Sync Example ===\n"); + + // Check for required environment variable + if (!process.env.TURSO_API_TOKEN) { + console.error("Error: TURSO_API_TOKEN environment variable is required"); + console.error("Get your token from: https://turso.tech/app"); + process.exit(1); + } + + // Open AgentFS with Turso Cloud sync + // This will create a database named 'simple-agent' in Turso Cloud + console.log("Opening AgentFS with Turso Cloud sync..."); + const agent = await AgentFS.open({ + id: "simple-agent", + sync: tursoSync(), // Uses all defaults from environment variables + }); + + console.log("✓ Connected to Turso Cloud\n"); + + // Store some data + console.log("Storing data locally..."); + await agent.kv.set("greeting", "Hello from AgentFS!"); + await agent.kv.set("timestamp", new Date().toISOString()); + await agent.kv.set("counter", 42); + + console.log("✓ Data stored locally\n"); + + // Manual sync to cloud + console.log("Syncing to Turso Cloud..."); + await agent.sync!.push(); + console.log("✓ Data pushed to cloud\n"); + + // Check sync status + const status = agent.sync!.getStatus(); + console.log("Sync status:", { + state: status.state, + lastSync: status.lastSync?.toISOString(), + }); + + console.log("\n=== Auto-sync is enabled ==="); + console.log("Your data will automatically sync every 60 seconds."); + console.log("Try modifying the cloud database and run this again to see changes pulled down.\n"); + + // Cleanup + await agent.close(); + console.log("✓ Connection closed"); +} + +main().catch((error) => { + console.error("Error:", error.message); + process.exit(1); +}); diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index d4c6810..c56815f 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -1,6 +1,6 @@ { "name": "agentfs-sdk", - "version": "0.1.2", + "version": "0.2.0", "description": "AgentFS SDK", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -20,7 +20,9 @@ "turso", "sqlite", "key-value", - "filesystem" + "filesystem", + "sync", + "cloud" ], "author": "", "license": "MIT", @@ -33,6 +35,10 @@ "dependencies": { "@tursodatabase/database": "^0.3.2" }, + "optionalDependencies": { + "@tursodatabase/api": "^1.9.0", + "@tursodatabase/sync": "^0.3.2" + }, "files": [ "dist" ] diff --git a/sdk/typescript/src/index.ts b/sdk/typescript/src/index.ts index 764444b..2cc5549 100644 --- a/sdk/typescript/src/index.ts +++ b/sdk/typescript/src/index.ts @@ -3,6 +3,8 @@ import { existsSync, mkdirSync } from 'fs'; import { KvStore } from './kvstore'; import { Filesystem } from './filesystem'; import { ToolCalls } from './toolcalls'; +import type { SyncProvider, TursoSyncFactory } from './sync'; +import { TursoSyncProvider } from './providers/turso'; /** * Configuration options for opening an AgentFS instance @@ -14,8 +16,13 @@ export interface AgentFSOptions { * - If omitted: Uses ephemeral in-memory database */ id?: string; - // Future: sync configuration will be added here - // sync?: SyncConfig; + + /** + * Optional sync configuration + * - Pass a TursoSyncFactory from tursoSync() for Turso Cloud sync + * - Pass a custom SyncProvider for other backends + */ + sync?: TursoSyncFactory | SyncProvider; } export class AgentFS { @@ -24,15 +31,23 @@ export class AgentFS { public readonly kv: KvStore; public readonly fs: Filesystem; public readonly tools: ToolCalls; + public readonly sync?: SyncProvider; /** * Private constructor - use AgentFS.open() instead */ - private constructor(db: Database, kv: KvStore, fs: Filesystem, tools: ToolCalls) { + private constructor( + db: Database, + kv: KvStore, + fs: Filesystem, + tools: ToolCalls, + sync?: SyncProvider + ) { this.db = db; this.kv = kv; this.fs = fs; this.tools = tools; + this.sync = sync; } /** @@ -47,6 +62,13 @@ export class AgentFS { * * // Ephemeral in-memory database * const agent = await AgentFS.open(); + * + * // With Turso Cloud sync + * import { tursoSync } from 'agentfs-sdk'; + * const agent = await AgentFS.open({ + * id: 'my-agent', + * sync: tursoSync() + * }); * ``` */ static async open(options?: AgentFSOptions): Promise { @@ -60,7 +82,7 @@ export class AgentFS { ); } - const { id } = options || {}; + const { id, sync } = options || {}; // Determine database path based on id let dbPath: string; @@ -91,8 +113,22 @@ export class AgentFS { await fs.ready(); await tools.ready(); + // Initialize sync if configured + let syncProvider: SyncProvider | undefined; + if (sync) { + if ('__type' in sync && sync.__type === 'turso-sync-factory') { + // Turso sync factory + syncProvider = new TursoSyncProvider(db, id || 'default', sync.config); + } else { + // Custom sync provider + syncProvider = sync as SyncProvider; + } + + await syncProvider.initialize(); + } + // Return fully initialized instance - return new AgentFS(db, kv, fs, tools); + return new AgentFS(db, kv, fs, tools, syncProvider); } /** @@ -103,9 +139,12 @@ export class AgentFS { } /** - * Close the database connection + * Close the database connection and cleanup sync */ async close(): Promise { + if (this.sync) { + await this.sync.cleanup(); + } await this.db.close(); } } @@ -115,3 +154,5 @@ export { Filesystem } from './filesystem'; export type { Stats } from './filesystem'; export { ToolCalls } from './toolcalls'; export type { ToolCall, ToolCallStats } from './toolcalls'; +export { tursoSync } from './sync'; +export type { SyncProvider, TursoSyncConfig, SyncStatus } from './sync'; diff --git a/sdk/typescript/src/providers/turso.ts b/sdk/typescript/src/providers/turso.ts new file mode 100644 index 0000000..19b3529 --- /dev/null +++ b/sdk/typescript/src/providers/turso.ts @@ -0,0 +1,234 @@ +import type { Database } from '@tursodatabase/database'; +import type { SyncProvider, SyncStatus, TursoSyncConfig } from '../sync'; + +/** + * Turso Cloud sync provider + * Handles database provisioning via @tursodatabase/api and sync via @tursodatabase/sync + */ +export class TursoSyncProvider implements SyncProvider { + private apiClient: any; + private syncDb: any; + private status: SyncStatus = { state: 'idle' }; + private autoSyncTimer?: NodeJS.Timeout; + private resolvedConfig: Required; + private dbUrl?: string; + private dbAuthToken?: string; + + constructor( + private db: Database, + private agentId: string, + config: TursoSyncConfig + ) { + // Resolve config with precedence: explicit > env > defaults + this.resolvedConfig = { + org: config.org || process.env.TURSO_API_ORG || '', + apiToken: config.apiToken || process.env.TURSO_API_TOKEN || '', + databaseUrl: + config.databaseUrl || + process.env.TURSO_DATABASE_URL || + agentId, + autoSync: + config.autoSync ?? + (process.env.TURSO_AUTO_SYNC === 'false' ? false : true), + interval: + config.interval || + parseInt(process.env.TURSO_SYNC_INTERVAL || '60000', 10), + }; + } + + async initialize(): Promise { + try { + // 1. Validate required config + if (!this.resolvedConfig.apiToken) { + throw new Error( + 'Turso API token required. Set TURSO_API_TOKEN environment variable or pass apiToken in config.' + ); + } + + // 2. Check if optional dependencies are available + let createClient: any; + let connect: any; + + try { + const apiModule = await import('@tursodatabase/api'); + createClient = apiModule.createClient; + } catch (error) { + throw new Error( + '@tursodatabase/api is required for Turso sync. Install it with: npm install @tursodatabase/api' + ); + } + + try { + const syncModule = await import('@tursodatabase/sync'); + connect = syncModule.connect; + } catch (error) { + throw new Error( + '@tursodatabase/sync is required for Turso sync. Install it with: npm install @tursodatabase/sync' + ); + } + + // 3. Setup API client + this.apiClient = createClient({ + org: this.resolvedConfig.org, + token: this.resolvedConfig.apiToken, + }); + + // 4. Determine if we need to provision or connect to existing database + const databaseUrl = this.resolvedConfig.databaseUrl; + + if (databaseUrl.startsWith('http://') || databaseUrl.startsWith('https://')) { + // Existing database URL provided + this.dbUrl = databaseUrl; + // For existing DB, user should provide token via env or config + // We'll try to get it, but may need user to provide it + console.warn( + 'Using existing database URL. Ensure database auth token is available via TURSO_DB_TOKEN environment variable if needed.' + ); + } else { + // Database name - provision it + await this.provisionDatabase(databaseUrl); + } + + // 5. Setup sync connection + // Get the database file path + const dbPath = (this.db as any).path || '.agentfs/' + this.agentId + '.db'; + + this.syncDb = await connect({ + path: dbPath, + url: this.dbUrl!, + authToken: this.dbAuthToken || process.env.TURSO_DB_TOKEN || '', + clientName: `agentfs-${this.agentId}`, + }); + + // 6. Start auto-sync if enabled + if (this.resolvedConfig.autoSync) { + this.startAutoSync(); + } + + this.status = { state: 'idle' }; + } catch (error) { + this.status = { + state: 'error', + lastError: error instanceof Error ? error.message : String(error), + }; + throw error; + } + } + + private async provisionDatabase(dbName: string): Promise { + try { + // Check if database exists + try { + const dbInfo = await this.apiClient.databases.get(dbName); + this.dbUrl = dbInfo.hostname; + console.log(`Using existing Turso database: ${dbName}`); + } catch (error) { + // Database doesn't exist, create it + console.log(`Creating Turso database: ${dbName}...`); + const newDb = await this.apiClient.databases.create(dbName); + this.dbUrl = newDb.hostname; + console.log(`Created Turso database: ${dbName}`); + } + + // Create/get auth token for the database + const tokenResponse = await this.apiClient.databases.createToken(dbName, { + expiration: 'never', + authorization: 'full-access', + }); + + this.dbAuthToken = tokenResponse.jwt; + } catch (error) { + throw new Error( + `Failed to provision Turso database '${dbName}': ${error instanceof Error ? error.message : String(error)}` + ); + } + } + + async pull(): Promise { + if (!this.syncDb) { + throw new Error('Sync not initialized. Call initialize() first.'); + } + + this.status = { state: 'syncing' }; + try { + await this.syncDb.pull(); + this.status = { state: 'idle', lastSync: new Date() }; + } catch (error) { + this.status = { + state: 'error', + lastError: error instanceof Error ? error.message : String(error), + }; + throw error; + } + } + + async push(): Promise { + if (!this.syncDb) { + throw new Error('Sync not initialized. Call initialize() first.'); + } + + this.status = { state: 'syncing' }; + try { + await this.syncDb.push(); + this.status = { state: 'idle', lastSync: new Date() }; + } catch (error) { + this.status = { + state: 'error', + lastError: error instanceof Error ? error.message : String(error), + }; + throw error; + } + } + + async sync(): Promise { + if (!this.syncDb) { + throw new Error('Sync not initialized. Call initialize() first.'); + } + + this.status = { state: 'syncing' }; + try { + await this.syncDb.sync(); + this.status = { state: 'idle', lastSync: new Date() }; + } catch (error) { + this.status = { + state: 'error', + lastError: error instanceof Error ? error.message : String(error), + }; + throw error; + } + } + + getStatus(): SyncStatus { + return { ...this.status }; + } + + private startAutoSync(): void { + this.autoSyncTimer = setInterval(async () => { + // Only sync if not currently syncing + if (this.status.state !== 'syncing') { + try { + await this.sync(); + } catch (error) { + console.error('Auto-sync failed:', error); + } + } + }, this.resolvedConfig.interval); + + // Ensure timer doesn't keep process alive + if (this.autoSyncTimer.unref) { + this.autoSyncTimer.unref(); + } + } + + async cleanup(): Promise { + // Stop auto-sync + if (this.autoSyncTimer) { + clearInterval(this.autoSyncTimer); + this.autoSyncTimer = undefined; + } + + // Close sync connection if available + // Note: @tursodatabase/sync may not have explicit close method + // The connection will be cleaned up when the process exits + } +} diff --git a/sdk/typescript/src/sync.ts b/sdk/typescript/src/sync.ts new file mode 100644 index 0000000..edccaa3 --- /dev/null +++ b/sdk/typescript/src/sync.ts @@ -0,0 +1,124 @@ +/** + * Sync status information + */ +export interface SyncStatus { + state: 'idle' | 'syncing' | 'error'; + lastSync?: Date; + lastError?: string; +} + +/** + * Interface for sync providers + * Implement this to create custom sync backends (S3, PostgreSQL, etc.) + */ +export interface SyncProvider { + /** + * Initialize the sync provider + */ + initialize(): Promise; + + /** + * Pull changes from remote to local + */ + pull(): Promise; + + /** + * Push changes from local to remote + */ + push(): Promise; + + /** + * Bidirectional sync (pull + push) + */ + sync(): Promise; + + /** + * Get current sync status + */ + getStatus(): SyncStatus; + + /** + * Cleanup resources (stop auto-sync, close connections, etc.) + */ + cleanup(): Promise; +} + +/** + * Configuration for Turso Cloud sync + * All fields are optional with smart defaults from environment variables + */ +export interface TursoSyncConfig { + /** + * Turso organization name + * Default: process.env.TURSO_API_ORG + */ + org?: string; + + /** + * Turso API token for database management + * Default: process.env.TURSO_API_TOKEN + * Required: At least one of these must be set + */ + apiToken?: string; + + /** + * Database URL or name + * - If URL (starts with http): Connect to existing database + * - If name: Create database with this name if it doesn't exist + * Default: process.env.TURSO_DATABASE_URL || agent id + */ + databaseUrl?: string; + + /** + * Enable automatic background sync + * Default: process.env.TURSO_AUTO_SYNC !== 'false' (true) + */ + autoSync?: boolean; + + /** + * Auto-sync interval in milliseconds + * Default: process.env.TURSO_SYNC_INTERVAL || 60000 (60s) + */ + interval?: number; +} + +/** + * Internal factory type for Turso sync + * @internal + */ +export interface TursoSyncFactory { + __type: 'turso-sync-factory'; + config: TursoSyncConfig; +} + +/** + * Create a Turso Cloud sync provider + * + * @param config Optional configuration (uses env vars and defaults if not provided) + * @returns Sync provider factory + * + * @example + * ```typescript + * // Minimal - uses environment variables + * const agent = await AgentFS.open({ + * id: 'my-agent', + * sync: tursoSync() + * }); + * + * // With custom config + * const agent = await AgentFS.open({ + * id: 'my-agent', + * sync: tursoSync({ + * org: 'my-org', + * databaseUrl: 'https://existing.turso.io', + * autoSync: false + * }) + * }); + * ``` + */ +export function tursoSync(config: TursoSyncConfig = {}): TursoSyncFactory { + return { + __type: 'turso-sync-factory', + config + }; +}