import { DISABLE_LOCAL_AUTH, IS_PLATFORM } from "../constants/config";
const normalizePathForUrl = (value) => String(value || '').replace(/\\/g, '/');
const getProjectRelativePath = (filePath, projectRoot) => {
const normalizedFilePath = normalizePathForUrl(filePath);
const normalizedRoot = normalizePathForUrl(projectRoot).replace(/\/+$/, '');
if (normalizedRoot && normalizedFilePath === normalizedRoot) {
return '';
}
if (normalizedRoot && normalizedFilePath.startsWith(normalizedRoot + '/')) {
return normalizedFilePath.slice(normalizedRoot.length + 1);
}
return normalizedFilePath.replace(/^\/+/, '');
};
const encodePathSegments = (relativePath) =>
String(relativePath || '')
.split('/')
.filter(Boolean)
.map((segment) => encodeURIComponent(segment))
.join('/');
const appendAuthToken = (url) => {
const token = localStorage.getItem('auth-token');
if (!token) return url;
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}token=${encodeURIComponent(token)}`;
};
export const authenticatedFetch = (url, options = {}) => {
const token = localStorage.getItem('auth-token');
const defaultHeaders = {};
if (!(options.body instanceof FormData)) {
defaultHeaders['Content-Type'] = 'application/json';
}
if (!IS_PLATFORM && !DISABLE_LOCAL_AUTH && token) {
defaultHeaders['Authorization'] = `Bearer ${token}`;
}
return fetch(url, {
...options,
headers: {
...defaultHeaders,
...options.headers,
},
}).then((response) => {
const refreshedToken = response.headers.get('X-Refreshed-Token');
if (refreshedToken) {
localStorage.setItem('auth-token', refreshedToken);
}
return response;
});
};
export const api = {
auth: {
status: () => fetch('/api/auth/status'),
login: (username, password) => fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
}),
register: (username, password) => fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
}),
user: () => authenticatedFetch('/api/auth/user'),
logout: () => authenticatedFetch('/api/auth/logout', { method: 'POST' }),
},
projects: () => authenticatedFetch('/api/projects'),
alwaysOnDashboardEvents: (limit = 200, since) =>
authenticatedFetch(`/api/always-on/events?limit=${encodeURIComponent(limit)}${since ? `&since=${encodeURIComponent(since)}` : ''}`),
allCronJobs: () =>
authenticatedFetch('/api/always-on/cron-jobs'),
cronRunNow: (taskId) =>
authenticatedFetch(`/api/always-on/cron-jobs/${encodeURIComponent(taskId)}/run-now`, { method: 'POST' }),
cronStop: (taskId) =>
authenticatedFetch(`/api/always-on/cron-jobs/${encodeURIComponent(taskId)}/stop`, { method: 'POST' }),
cronDelete: (taskId) =>
authenticatedFetch(`/api/always-on/cron-jobs/${encodeURIComponent(taskId)}`, { method: 'DELETE' }),
projectDiscoveryContext: (projectName) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/discovery-context`),
projectDiscoveryPlans: (projectName) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/discovery-plans`),
executeProjectDiscoveryPlan: (projectName, planId, body = {}) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/discovery-plans/${encodeURIComponent(planId)}/execute`, {
method: 'POST',
body: JSON.stringify(body),
}),
discoveryPlanReport: (projectName, planId) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/discovery-plans/${encodeURIComponent(planId)}/report`),
projectWorkCycles: (projectName) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/work-cycles`),
applyWorkCycle: (projectName, cycleId) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/work-cycles/${encodeURIComponent(cycleId)}/apply`, {
method: 'POST',
}),
archiveWorkCycle: (projectName, cycleId) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/work-cycles/${encodeURIComponent(cycleId)}/archive`, {
method: 'POST',
}),
sessions: (projectName, limit = 5, offset = 0) =>
authenticatedFetch(`/api/projects/${projectName}/sessions?limit=${limit}&offset=${offset}`),
unifiedSessionMessages: (sessionId, provider = 'claude', { projectName = '', projectPath = '', limit = null, offset = 0 } = {}) => {
const params = new URLSearchParams();
params.append('provider', provider);
if (projectName) params.append('projectName', projectName);
if (projectPath) params.append('projectPath', projectPath);
if (limit !== null) {
params.append('limit', String(limit));
params.append('offset', String(offset));
}
const queryString = params.toString();
return authenticatedFetch(`/api/sessions/${encodeURIComponent(sessionId)}/messages${queryString ? `?${queryString}` : ''}`);
},
renameProject: (projectName, displayName) =>
authenticatedFetch(`/api/projects/${projectName}/rename`, {
method: 'PUT',
body: JSON.stringify({ displayName }),
}),
deleteSession: (projectName, sessionId, opts = {}) => {
const params = new URLSearchParams();
if (opts.sessionKind) params.append('sessionKind', opts.sessionKind);
if (opts.parentSessionId) params.append('parentSessionId', opts.parentSessionId);
if (opts.relativeTranscriptPath) params.append('relativeTranscriptPath', opts.relativeTranscriptPath);
const query = params.toString();
return authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/sessions/${encodeURIComponent(sessionId)}${query ? `?${query}` : ''}`, {
method: 'DELETE',
});
},
renameSession: (sessionId, summary, provider) =>
authenticatedFetch(`/api/sessions/${sessionId}/rename`, {
method: 'PUT',
body: JSON.stringify({ summary, provider }),
}),
deleteProject: (projectName, force = false) =>
authenticatedFetch(`/api/projects/${projectName}${force ? '?force=true' : ''}`, {
method: 'DELETE',
}),
searchConversationsUrl: (query, limit = 50) => {
const token = localStorage.getItem('auth-token');
const params = new URLSearchParams({ q: query, limit: String(limit) });
if (token) params.set('token', token);
return `/api/search/conversations?${params.toString()}`;
},
createProject: (path) =>
authenticatedFetch('/api/projects/create', {
method: 'POST',
body: JSON.stringify({ path }),
}),
createWorkspace: (workspaceData) =>
authenticatedFetch('/api/projects/create-workspace', {
method: 'POST',
body: JSON.stringify(workspaceData),
}),
readFile: (projectName, filePath) =>
authenticatedFetch(`/api/projects/${projectName}/file?filePath=${encodeURIComponent(filePath)}`),
readFileBlob: (projectName, filePath) =>
authenticatedFetch(`/api/projects/${projectName}/files/content?path=${encodeURIComponent(filePath)}`),
saveFile: (projectName, filePath, content) =>
authenticatedFetch(`/api/projects/${projectName}/file`, {
method: 'PUT',
body: JSON.stringify({ filePath, content }),
}),
getFiles: (projectName, options = {}) =>
authenticatedFetch(`/api/projects/${projectName}/files`, options),
createFile: (projectName, { path, type, name }) =>
authenticatedFetch(`/api/projects/${projectName}/files/create`, {
method: 'POST',
body: JSON.stringify({ path, type, name }),
}),
renameFile: (projectName, { oldPath, newName }) =>
authenticatedFetch(`/api/projects/${projectName}/files/rename`, {
method: 'PUT',
body: JSON.stringify({ oldPath, newName }),
}),
deleteFile: (projectName, { path, type }) =>
authenticatedFetch(`/api/projects/${projectName}/files`, {
method: 'DELETE',
body: JSON.stringify({ path, type }),
}),
uploadFiles: (projectName, formData) =>
authenticatedFetch(`/api/projects/${projectName}/files/upload`, {
method: 'POST',
body: formData,
headers: {},
}),
projectPreviewUrl: (projectName, filePath, projectRoot) => {
const relativePath = getProjectRelativePath(filePath, projectRoot);
const encoded = encodePathSegments(relativePath);
return appendAuthToken(
`/api/projects/${encodeURIComponent(projectName)}/preview/${encoded}`,
);
},
downloadProjectZip: (projectName) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectName)}/download`),
fileDownloadUrl: (projectName, filePath) =>
appendAuthToken(
`/api/projects/${encodeURIComponent(projectName)}/files/content?path=${encodeURIComponent(filePath)}&download=1`,
),
taskmaster: {
init: (projectName) =>
authenticatedFetch(`/api/taskmaster/init/${projectName}`, {
method: 'POST',
}),
addTask: (projectName, { prompt, title, description, priority, dependencies }) =>
authenticatedFetch(`/api/taskmaster/add-task/${projectName}`, {
method: 'POST',
body: JSON.stringify({ prompt, title, description, priority, dependencies }),
}),
parsePRD: (projectName, { fileName, numTasks, append }) =>
authenticatedFetch(`/api/taskmaster/parse-prd/${projectName}`, {
method: 'POST',
body: JSON.stringify({ fileName, numTasks, append }),
}),
getTemplates: () =>
authenticatedFetch('/api/taskmaster/prd-templates'),
applyTemplate: (projectName, { templateId, fileName, customizations }) =>
authenticatedFetch(`/api/taskmaster/apply-template/${projectName}`, {
method: 'POST',
body: JSON.stringify({ templateId, fileName, customizations }),
}),
updateTask: (projectName, taskId, updates) =>
authenticatedFetch(`/api/taskmaster/update-task/${projectName}/${taskId}`, {
method: 'PUT',
body: JSON.stringify(updates),
}),
},
browseFilesystem: (dirPath = null) => {
const params = new URLSearchParams();
if (dirPath) params.append('path', dirPath);
return authenticatedFetch(`/api/browse-filesystem?${params}`);
},
createFolder: (folderPath) =>
authenticatedFetch('/api/create-folder', {
method: 'POST',
body: JSON.stringify({ path: folderPath }),
}),
user: {
gitConfig: () => authenticatedFetch('/api/user/git-config'),
updateGitConfig: (gitName, gitEmail) =>
authenticatedFetch('/api/user/git-config', {
method: 'POST',
body: JSON.stringify({ gitName, gitEmail }),
}),
onboardingStatus: () => authenticatedFetch('/api/user/onboarding-status'),
completeOnboarding: () =>
authenticatedFetch('/api/user/complete-onboarding', {
method: 'POST',
}),
},
get: (endpoint) => authenticatedFetch(`/api${endpoint}`),
post: (endpoint, body) => authenticatedFetch(`/api${endpoint}`, {
method: 'POST',
...(body instanceof FormData ? { body } : { body: JSON.stringify(body) }),
}),
put: (endpoint, body) => authenticatedFetch(`/api${endpoint}`, {
method: 'PUT',
body: JSON.stringify(body),
}),
delete: (endpoint, options = {}) => authenticatedFetch(`/api${endpoint}`, {
method: 'DELETE',
...options,
}),
};