2026-04-23 15:46:39 +05:30
|
|
|
import { app, dialog } from "electron";
|
2026-04-24 01:45:13 +05:30
|
|
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
2026-04-23 17:23:38 +05:30
|
|
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
2026-04-23 15:46:39 +05:30
|
|
|
|
|
|
|
|
export type AgentFilesystemMode = "cloud" | "desktop_local_folder";
|
|
|
|
|
|
|
|
|
|
export interface AgentFilesystemSettings {
|
|
|
|
|
mode: AgentFilesystemMode;
|
2026-04-24 01:45:13 +05:30
|
|
|
localRootPaths: string[];
|
2026-04-23 15:46:39 +05:30
|
|
|
updatedAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const SETTINGS_FILENAME = "agent-filesystem-settings.json";
|
2026-04-24 01:45:13 +05:30
|
|
|
const MAX_LOCAL_ROOTS = 5;
|
2026-04-23 15:46:39 +05:30
|
|
|
|
|
|
|
|
function getSettingsPath(): string {
|
|
|
|
|
return join(app.getPath("userData"), SETTINGS_FILENAME);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDefaultSettings(): AgentFilesystemSettings {
|
|
|
|
|
return {
|
|
|
|
|
mode: "cloud",
|
2026-04-24 01:45:13 +05:30
|
|
|
localRootPaths: [],
|
2026-04-23 15:46:39 +05:30
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 01:45:13 +05:30
|
|
|
function normalizeLocalRootPaths(paths: unknown): string[] {
|
|
|
|
|
if (!Array.isArray(paths)) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
const uniquePaths = new Set<string>();
|
|
|
|
|
for (const path of paths) {
|
|
|
|
|
if (typeof path !== "string") continue;
|
|
|
|
|
const trimmed = path.trim();
|
|
|
|
|
if (!trimmed) continue;
|
|
|
|
|
uniquePaths.add(trimmed);
|
|
|
|
|
if (uniquePaths.size >= MAX_LOCAL_ROOTS) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [...uniquePaths];
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 15:46:39 +05:30
|
|
|
export async function getAgentFilesystemSettings(): Promise<AgentFilesystemSettings> {
|
|
|
|
|
try {
|
|
|
|
|
const raw = await readFile(getSettingsPath(), "utf8");
|
|
|
|
|
const parsed = JSON.parse(raw) as Partial<AgentFilesystemSettings>;
|
|
|
|
|
if (parsed.mode !== "cloud" && parsed.mode !== "desktop_local_folder") {
|
|
|
|
|
return getDefaultSettings();
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
mode: parsed.mode,
|
2026-04-24 01:45:13 +05:30
|
|
|
localRootPaths: normalizeLocalRootPaths(parsed.localRootPaths),
|
2026-04-23 15:46:39 +05:30
|
|
|
updatedAt: parsed.updatedAt ?? new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
} catch {
|
|
|
|
|
return getDefaultSettings();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function setAgentFilesystemSettings(
|
2026-04-24 01:45:13 +05:30
|
|
|
settings: {
|
|
|
|
|
mode?: AgentFilesystemMode;
|
|
|
|
|
localRootPaths?: string[] | null;
|
|
|
|
|
}
|
2026-04-23 15:46:39 +05:30
|
|
|
): Promise<AgentFilesystemSettings> {
|
|
|
|
|
const current = await getAgentFilesystemSettings();
|
|
|
|
|
const nextMode =
|
|
|
|
|
settings.mode === "cloud" || settings.mode === "desktop_local_folder"
|
|
|
|
|
? settings.mode
|
|
|
|
|
: current.mode;
|
|
|
|
|
const next: AgentFilesystemSettings = {
|
|
|
|
|
mode: nextMode,
|
2026-04-24 01:45:13 +05:30
|
|
|
localRootPaths:
|
|
|
|
|
settings.localRootPaths === undefined
|
|
|
|
|
? current.localRootPaths
|
|
|
|
|
: normalizeLocalRootPaths(settings.localRootPaths ?? []),
|
2026-04-23 15:46:39 +05:30
|
|
|
updatedAt: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const settingsPath = getSettingsPath();
|
|
|
|
|
await mkdir(dirname(settingsPath), { recursive: true });
|
|
|
|
|
await writeFile(settingsPath, JSON.stringify(next, null, 2), "utf8");
|
|
|
|
|
return next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function pickAgentFilesystemRoot(): Promise<string | null> {
|
|
|
|
|
const result = await dialog.showOpenDialog({
|
|
|
|
|
title: "Select local folder for Agent Filesystem",
|
|
|
|
|
properties: ["openDirectory"],
|
|
|
|
|
});
|
|
|
|
|
if (result.canceled || result.filePaths.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return result.filePaths[0] ?? null;
|
|
|
|
|
}
|
2026-04-23 17:23:38 +05:30
|
|
|
|
|
|
|
|
function resolveVirtualPath(rootPath: string, virtualPath: string): string {
|
|
|
|
|
if (!virtualPath.startsWith("/")) {
|
|
|
|
|
throw new Error("Path must start with '/'");
|
|
|
|
|
}
|
|
|
|
|
const normalizedRoot = resolve(rootPath);
|
|
|
|
|
const relativePath = virtualPath.replace(/^\/+/, "");
|
|
|
|
|
if (!relativePath) {
|
|
|
|
|
throw new Error("Path must refer to a file under the selected root");
|
|
|
|
|
}
|
|
|
|
|
const absolutePath = resolve(normalizedRoot, relativePath);
|
|
|
|
|
const rel = relative(normalizedRoot, absolutePath);
|
|
|
|
|
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
|
|
|
throw new Error("Path escapes selected local root");
|
|
|
|
|
}
|
|
|
|
|
return absolutePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toVirtualPath(rootPath: string, absolutePath: string): string {
|
|
|
|
|
const normalizedRoot = resolve(rootPath);
|
|
|
|
|
const rel = relative(normalizedRoot, absolutePath);
|
|
|
|
|
if (!rel || rel.startsWith("..") || isAbsolute(rel)) {
|
|
|
|
|
return "/";
|
|
|
|
|
}
|
|
|
|
|
return `/${rel.replace(/\\/g, "/")}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveCurrentRootPath(): Promise<string> {
|
|
|
|
|
const settings = await getAgentFilesystemSettings();
|
2026-04-24 01:45:13 +05:30
|
|
|
if (settings.localRootPaths.length === 0) {
|
|
|
|
|
throw new Error("No local filesystem roots selected");
|
|
|
|
|
}
|
|
|
|
|
return settings.localRootPaths[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function resolveCurrentRootPaths(): Promise<string[]> {
|
|
|
|
|
const settings = await getAgentFilesystemSettings();
|
|
|
|
|
if (settings.localRootPaths.length === 0) {
|
|
|
|
|
throw new Error("No local filesystem roots selected");
|
2026-04-23 17:23:38 +05:30
|
|
|
}
|
2026-04-24 01:45:13 +05:30
|
|
|
return settings.localRootPaths;
|
2026-04-23 17:23:38 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function readAgentLocalFileText(
|
|
|
|
|
virtualPath: string
|
|
|
|
|
): Promise<{ path: string; content: string }> {
|
2026-04-24 01:45:13 +05:30
|
|
|
const rootPaths = await resolveCurrentRootPaths();
|
|
|
|
|
for (const rootPath of rootPaths) {
|
|
|
|
|
const absolutePath = resolveVirtualPath(rootPath, virtualPath);
|
|
|
|
|
try {
|
|
|
|
|
const content = await readFile(absolutePath, "utf8");
|
|
|
|
|
return {
|
|
|
|
|
path: toVirtualPath(rootPath, absolutePath),
|
|
|
|
|
content,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Keep the same relative virtual path in the error context.
|
|
|
|
|
const fallbackRootPath = await resolveCurrentRootPath();
|
|
|
|
|
const fallbackAbsolutePath = resolveVirtualPath(fallbackRootPath, virtualPath);
|
|
|
|
|
const content = await readFile(fallbackAbsolutePath, "utf8");
|
2026-04-23 17:23:38 +05:30
|
|
|
return {
|
2026-04-24 01:45:13 +05:30
|
|
|
path: toVirtualPath(fallbackRootPath, fallbackAbsolutePath),
|
2026-04-23 17:23:38 +05:30
|
|
|
content,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function writeAgentLocalFileText(
|
|
|
|
|
virtualPath: string,
|
|
|
|
|
content: string
|
|
|
|
|
): Promise<{ path: string }> {
|
2026-04-24 01:45:13 +05:30
|
|
|
const rootPaths = await resolveCurrentRootPaths();
|
|
|
|
|
let selectedRootPath = rootPaths[0];
|
|
|
|
|
let selectedAbsolutePath = resolveVirtualPath(selectedRootPath, virtualPath);
|
|
|
|
|
|
|
|
|
|
for (const rootPath of rootPaths) {
|
|
|
|
|
const absolutePath = resolveVirtualPath(rootPath, virtualPath);
|
|
|
|
|
try {
|
|
|
|
|
await access(absolutePath);
|
|
|
|
|
selectedRootPath = rootPath;
|
|
|
|
|
selectedAbsolutePath = absolutePath;
|
|
|
|
|
break;
|
|
|
|
|
} catch {
|
|
|
|
|
// Keep searching for an existing file path across selected roots.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await mkdir(dirname(selectedAbsolutePath), { recursive: true });
|
|
|
|
|
await writeFile(selectedAbsolutePath, content, "utf8");
|
2026-04-23 17:23:38 +05:30
|
|
|
return {
|
2026-04-24 01:45:13 +05:30
|
|
|
path: toVirtualPath(selectedRootPath, selectedAbsolutePath),
|
2026-04-23 17:23:38 +05:30
|
|
|
};
|
|
|
|
|
}
|