191 lines
6.1 KiB
TypeScript
191 lines
6.1 KiB
TypeScript
/**
|
|
* Container Sync
|
|
*
|
|
* Handles synchronization of persistent workspace stores with the user container
|
|
* via MCP tools. Persistent stores (chartStore, userPreferences, etc.) are
|
|
* stored in the container and loaded/saved via MCP tool calls.
|
|
*
|
|
* Container-side storage: /data/workspace/{store_name}.json
|
|
*
|
|
* MCP Tools used:
|
|
* - workspace_read(store_name) -> dict
|
|
* - workspace_write(store_name, data) -> None
|
|
* - workspace_patch(store_name, patch) -> dict (new state)
|
|
*/
|
|
|
|
import type { FastifyBaseLogger } from 'fastify';
|
|
import type { Operation as JsonPatchOp } from 'fast-json-patch';
|
|
import type { MCPClientConnector } from '../harness/mcp-client.js';
|
|
|
|
/**
|
|
* Result of loading a store from the container.
|
|
*/
|
|
export interface LoadResult {
|
|
exists: boolean;
|
|
state?: unknown;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Result of saving a store to the container.
|
|
*/
|
|
export interface SaveResult {
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Result of patching a store in the container.
|
|
*/
|
|
export interface PatchResult {
|
|
success: boolean;
|
|
newState?: unknown;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* Handles synchronization with the user's container via MCP.
|
|
*/
|
|
export class ContainerSync {
|
|
private mcpClient: MCPClientConnector;
|
|
private logger: FastifyBaseLogger;
|
|
|
|
constructor(mcpClient: MCPClientConnector, logger: FastifyBaseLogger) {
|
|
this.mcpClient = mcpClient;
|
|
this.logger = logger.child({ component: 'ContainerSync' });
|
|
}
|
|
|
|
/**
|
|
* Load a workspace store from the container.
|
|
* Returns the stored state or indicates the store doesn't exist.
|
|
*/
|
|
async loadStore(storeName: string): Promise<LoadResult> {
|
|
if (!this.mcpClient.isConnected()) {
|
|
this.logger.warn({ store: storeName }, 'MCP client not connected, cannot load store');
|
|
return { exists: false, error: 'MCP client not connected' };
|
|
}
|
|
|
|
try {
|
|
this.logger.debug({ store: storeName }, 'Loading store from container');
|
|
|
|
const result = (await this.mcpClient.callTool('workspace_read', {
|
|
store_name: storeName,
|
|
})) as { exists: boolean; data?: unknown; error?: string };
|
|
|
|
if (result.error) {
|
|
this.logger.warn({ store: storeName, error: result.error }, 'Container returned error');
|
|
return { exists: false, error: result.error };
|
|
}
|
|
|
|
if (!result.exists) {
|
|
this.logger.debug({ store: storeName }, 'Store does not exist in container');
|
|
return { exists: false };
|
|
}
|
|
|
|
this.logger.debug({ store: storeName }, 'Loaded store from container');
|
|
return { exists: true, state: result.data };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
this.logger.error({ store: storeName, error: message }, 'Failed to load store from container');
|
|
return { exists: false, error: message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save a workspace store to the container.
|
|
* Overwrites any existing state.
|
|
*/
|
|
async saveStore(storeName: string, state: unknown): Promise<SaveResult> {
|
|
if (!this.mcpClient.isConnected()) {
|
|
this.logger.warn({ store: storeName }, 'MCP client not connected, cannot save store');
|
|
return { success: false, error: 'MCP client not connected' };
|
|
}
|
|
|
|
try {
|
|
this.logger.debug({ store: storeName }, 'Saving store to container');
|
|
|
|
const result = (await this.mcpClient.callTool('workspace_write', {
|
|
store_name: storeName,
|
|
data: state,
|
|
})) as { success: boolean; error?: string };
|
|
|
|
if (result.error || !result.success) {
|
|
this.logger.warn({ store: storeName, error: result.error }, 'Failed to save store');
|
|
return { success: false, error: result.error || 'Unknown error' };
|
|
}
|
|
|
|
this.logger.debug({ store: storeName }, 'Saved store to container');
|
|
return { success: true };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
this.logger.error({ store: storeName, error: message }, 'Failed to save store to container');
|
|
return { success: false, error: message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply a JSON patch to a store in the container.
|
|
* Returns the new state after applying the patch.
|
|
*/
|
|
async patchStore(storeName: string, patch: JsonPatchOp[]): Promise<PatchResult> {
|
|
if (!this.mcpClient.isConnected()) {
|
|
this.logger.warn({ store: storeName }, 'MCP client not connected, cannot patch store');
|
|
return { success: false, error: 'MCP client not connected' };
|
|
}
|
|
|
|
try {
|
|
this.logger.debug({ store: storeName, patchOps: patch.length }, 'Patching store in container');
|
|
|
|
const result = (await this.mcpClient.callTool('workspace_patch', {
|
|
store_name: storeName,
|
|
patch,
|
|
})) as { success: boolean; data?: unknown; error?: string };
|
|
|
|
if (result.error || !result.success) {
|
|
this.logger.warn({ store: storeName, error: result.error }, 'Failed to patch store');
|
|
return { success: false, error: result.error || 'Unknown error' };
|
|
}
|
|
|
|
this.logger.debug({ store: storeName }, 'Patched store in container');
|
|
return { success: true, newState: result.data };
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
this.logger.error({ store: storeName, error: message }, 'Failed to patch store in container');
|
|
return { success: false, error: message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load all persistent stores from the container.
|
|
* Returns a map of store name -> state.
|
|
*/
|
|
async loadAllStores(storeNames: string[]): Promise<Map<string, unknown>> {
|
|
const states = new Map<string, unknown>();
|
|
|
|
for (const storeName of storeNames) {
|
|
const result = await this.loadStore(storeName);
|
|
if (result.exists && result.state !== undefined) {
|
|
states.set(storeName, result.state);
|
|
}
|
|
}
|
|
|
|
return states;
|
|
}
|
|
|
|
/**
|
|
* Save all persistent stores to the container.
|
|
*/
|
|
async saveAllStores(stores: Map<string, unknown>): Promise<void> {
|
|
for (const [storeName, state] of stores) {
|
|
await this.saveStore(storeName, state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if MCP client is connected.
|
|
*/
|
|
isConnected(): boolean {
|
|
return this.mcpClient.isConnected();
|
|
}
|
|
}
|