Add bidirectional workspace synchronization with cloud storage providers (Dropbox, Google Drive, OneDrive, S3/R2) using rclone. Features: - Config-driven setup via workspace.sync in moltbot.json - Native CLI: moltbot workspace setup/sync/status/authorize/list - Session hooks: onSessionStart/onSessionEnd triggers (no LLM cost) - Background interval sync via gateway (no LLM cost) - Auto-install rclone when missing (Homebrew/official script) - Dropbox app folder support for ringfenced security - Secure defaults: syncs shared/ subdirectory only New files: - src/infra/rclone.ts - rclone wrapper - src/cli/workspace-cli.ts - CLI commands - src/hooks/bundled/workspace-sync/ - session hooks - src/gateway/workspace-sync-manager.ts - background sync - src/config/types.workspace.ts - config types - docs/gateway/workspace-sync.md - full documentation Tests: 23 unit tests for rclone helpers and hook handler
119 lines
3.3 KiB
TypeScript
119 lines
3.3 KiB
TypeScript
/**
|
|
* Workspace sync hook handler
|
|
*
|
|
* Syncs workspace with cloud storage on session start/end
|
|
* when workspace.sync is configured.
|
|
*/
|
|
|
|
import type { MoltbotConfig } from "../../../config/config.js";
|
|
import { resolveAgentWorkspaceDir } from "../../../agents/agent-scope.js";
|
|
import { resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
|
|
import type { HookHandler } from "../../hooks.js";
|
|
import {
|
|
isRcloneInstalled,
|
|
isRcloneConfigured,
|
|
resolveSyncConfig,
|
|
runBisync,
|
|
} from "../../../infra/rclone.js";
|
|
|
|
/**
|
|
* Sync workspace on session start or end
|
|
*/
|
|
const workspaceSyncHandler: HookHandler = async (event) => {
|
|
// Only handle session events
|
|
if (event.type !== "session") {
|
|
return;
|
|
}
|
|
|
|
const context = event.context || {};
|
|
const cfg = context.cfg as MoltbotConfig | undefined;
|
|
|
|
if (!cfg?.workspace?.sync) {
|
|
return;
|
|
}
|
|
|
|
const syncConfig = cfg.workspace.sync;
|
|
|
|
// Check if sync is enabled for this event
|
|
const isStart = event.action === "start";
|
|
const isEnd = event.action === "end" || event.action === "stop";
|
|
|
|
if (isStart && !syncConfig.onSessionStart) {
|
|
return;
|
|
}
|
|
|
|
if (isEnd && !syncConfig.onSessionEnd) {
|
|
return;
|
|
}
|
|
|
|
if (!isStart && !isEnd) {
|
|
return;
|
|
}
|
|
|
|
// Check if provider is configured
|
|
if (!syncConfig.provider || syncConfig.provider === "off") {
|
|
return;
|
|
}
|
|
|
|
console.log(`[workspace-sync] Triggered on session ${event.action}`);
|
|
|
|
try {
|
|
// Check if rclone is installed
|
|
const installed = await isRcloneInstalled();
|
|
if (!installed) {
|
|
console.warn("[workspace-sync] rclone not installed, skipping sync");
|
|
return;
|
|
}
|
|
|
|
// Resolve workspace and config
|
|
const agentId = resolveAgentIdFromSessionKey(event.sessionKey);
|
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
|
const stateDir = context.stateDir as string | undefined;
|
|
|
|
const resolved = resolveSyncConfig(syncConfig, workspaceDir, stateDir);
|
|
|
|
// Check if rclone is configured
|
|
if (!isRcloneConfigured(resolved.configPath, resolved.remoteName)) {
|
|
console.warn(
|
|
`[workspace-sync] rclone not configured for remote "${resolved.remoteName}", skipping sync`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log(
|
|
`[workspace-sync] Syncing ${resolved.remoteName}:${resolved.remotePath} <-> ${resolved.localPath}`,
|
|
);
|
|
|
|
// Run sync
|
|
const result = await runBisync({
|
|
configPath: resolved.configPath,
|
|
remoteName: resolved.remoteName,
|
|
remotePath: resolved.remotePath,
|
|
localPath: resolved.localPath,
|
|
conflictResolve: resolved.conflictResolve,
|
|
exclude: resolved.exclude,
|
|
verbose: false,
|
|
});
|
|
|
|
if (result.ok) {
|
|
console.log("[workspace-sync] Sync completed successfully");
|
|
if (result.filesTransferred) {
|
|
console.log(`[workspace-sync] Files transferred: ${result.filesTransferred}`);
|
|
}
|
|
} else {
|
|
// Check if this is a first-run issue
|
|
if (result.error?.includes("--resync")) {
|
|
console.warn(
|
|
"[workspace-sync] First sync requires manual --resync. Run: moltbot workspace sync --resync",
|
|
);
|
|
} else {
|
|
console.error(`[workspace-sync] Sync failed: ${result.error}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error("[workspace-sync] Error:", err instanceof Error ? err.message : String(err));
|
|
}
|
|
};
|
|
|
|
export default workspaceSyncHandler;
|