summaryrefslogtreecommitdiff
path: root/makima/frontend/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/lib')
-rw-r--r--makima/frontend/src/lib/api.ts921
-rw-r--r--makima/frontend/src/lib/supabase.ts26
2 files changed, 933 insertions, 14 deletions
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 2657a95..a11f15e 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1,3 +1,5 @@
+import { supabase } from "./supabase";
+
const API_CONFIG = {
local: {
http: "http://localhost:8080",
@@ -33,8 +35,72 @@ const env = detectEnvironment();
export const API_BASE = API_CONFIG[env].http;
export const WS_BASE = API_CONFIG[env].ws;
+
+// =============================================================================
+// Authentication helpers
+// =============================================================================
+
+/** Storage key for API key */
+const API_KEY_STORAGE_KEY = "makima_api_key";
+
+/** Get stored API key from localStorage */
+export function getStoredApiKey(): string | null {
+ if (typeof window === "undefined") return null;
+ return localStorage.getItem(API_KEY_STORAGE_KEY);
+}
+
+/** Store API key in localStorage */
+export function setStoredApiKey(key: string): void {
+ if (typeof window === "undefined") return;
+ localStorage.setItem(API_KEY_STORAGE_KEY, key);
+}
+
+/** Remove stored API key */
+export function clearStoredApiKey(): void {
+ if (typeof window === "undefined") return;
+ localStorage.removeItem(API_KEY_STORAGE_KEY);
+}
+
+/** Get auth headers for API requests */
+async function getAuthHeaders(): Promise<HeadersInit> {
+ const headers: HeadersInit = {
+ "Content-Type": "application/json",
+ };
+
+ // Try Supabase session first
+ if (supabase) {
+ const { data: { session } } = await supabase.auth.getSession();
+ if (session?.access_token) {
+ headers["Authorization"] = `Bearer ${session.access_token}`;
+ return headers;
+ }
+ }
+
+ // Fall back to API key if available
+ const apiKey = getStoredApiKey();
+ if (apiKey) {
+ headers["X-Makima-API-Key"] = apiKey;
+ }
+
+ return headers;
+}
+
+/** Fetch with authentication headers */
+async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
+ const authHeaders = await getAuthHeaders();
+ const mergedHeaders = {
+ ...authHeaders,
+ ...options.headers,
+ };
+
+ return fetch(url, {
+ ...options,
+ headers: mergedHeaders,
+ });
+}
export const LISTEN_ENDPOINT = `${WS_BASE}/api/v1/listen`;
export const FILE_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/files/subscribe`;
+export const TASK_SUBSCRIBE_ENDPOINT = `${WS_BASE}/api/v1/mesh/tasks/subscribe`;
export function getEnvironment(): Environment {
return env;
@@ -57,6 +123,8 @@ export type ChartType = "line" | "bar" | "pie" | "area";
export type BodyElement =
| { type: "heading"; level: number; text: string }
| { type: "paragraph"; text: string }
+ | { type: "code"; language?: string; content: string }
+ | { type: "list"; ordered: boolean; items: string[] }
| {
type: "chart";
chartType: ChartType;
@@ -145,6 +213,7 @@ export interface ChatRequest {
message: string;
model?: LlmModel;
history?: ChatMessage[];
+ focusedElementIndex?: number;
}
export interface ToolCallInfo {
@@ -179,7 +248,7 @@ export interface ChatResponse {
// File API functions
export async function listFiles(): Promise<FileListResponse> {
- const res = await fetch(`${API_BASE}/api/v1/files`);
+ const res = await authFetch(`${API_BASE}/api/v1/files`);
if (!res.ok) {
throw new Error(`Failed to list files: ${res.statusText}`);
}
@@ -187,7 +256,7 @@ export async function listFiles(): Promise<FileListResponse> {
}
export async function getFile(id: string): Promise<FileDetail> {
- const res = await fetch(`${API_BASE}/api/v1/files/${id}`);
+ const res = await authFetch(`${API_BASE}/api/v1/files/${id}`);
if (!res.ok) {
throw new Error(`Failed to get file: ${res.statusText}`);
}
@@ -195,9 +264,8 @@ export async function getFile(id: string): Promise<FileDetail> {
}
export async function createFile(data: CreateFileRequest): Promise<FileDetail> {
- const res = await fetch(`${API_BASE}/api/v1/files`, {
+ const res = await authFetch(`${API_BASE}/api/v1/files`, {
method: "POST",
- headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) {
@@ -210,9 +278,8 @@ export async function updateFile(
id: string,
data: UpdateFileRequest
): Promise<FileDetail> {
- const res = await fetch(`${API_BASE}/api/v1/files/${id}`, {
+ const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, {
method: "PUT",
- headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
@@ -228,7 +295,7 @@ export async function updateFile(
}
export async function deleteFile(id: string): Promise<void> {
- const res = await fetch(`${API_BASE}/api/v1/files/${id}`, {
+ const res = await authFetch(`${API_BASE}/api/v1/files/${id}`, {
method: "DELETE",
});
if (!res.ok) {
@@ -241,7 +308,8 @@ export async function chatWithFile(
id: string,
message: string,
model?: LlmModel,
- history?: ChatMessage[]
+ history?: ChatMessage[],
+ focusedElementIndex?: number
): Promise<ChatResponse> {
const body: ChatRequest = { message };
if (model) {
@@ -250,9 +318,11 @@ export async function chatWithFile(
if (history && history.length > 0) {
body.history = history;
}
- const res = await fetch(`${API_BASE}/api/v1/files/${id}/chat`, {
+ if (focusedElementIndex !== undefined) {
+ body.focusedElementIndex = focusedElementIndex;
+ }
+ const res = await authFetch(`${API_BASE}/api/v1/files/${id}/chat`, {
method: "POST",
- headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
@@ -294,7 +364,7 @@ export interface RestoreVersionRequest {
// Version history API functions
export async function listFileVersions(fileId: string): Promise<FileVersionListResponse> {
- const res = await fetch(`${API_BASE}/api/v1/files/${fileId}/versions`);
+ const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions`);
if (!res.ok) {
throw new Error(`Failed to list versions: ${res.statusText}`);
}
@@ -302,7 +372,7 @@ export async function listFileVersions(fileId: string): Promise<FileVersionListR
}
export async function getFileVersion(fileId: string, version: number): Promise<FileVersion> {
- const res = await fetch(`${API_BASE}/api/v1/files/${fileId}/versions/${version}`);
+ const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/${version}`);
if (!res.ok) {
throw new Error(`Failed to get version: ${res.statusText}`);
}
@@ -314,9 +384,8 @@ export async function restoreFileVersion(
targetVersion: number,
currentVersion: number
): Promise<FileDetail> {
- const res = await fetch(`${API_BASE}/api/v1/files/${fileId}/versions/restore`, {
+ const res = await authFetch(`${API_BASE}/api/v1/files/${fileId}/versions/restore`, {
method: "POST",
- headers: { "Content-Type": "application/json" },
body: JSON.stringify({ targetVersion, currentVersion }),
});
@@ -396,3 +465,827 @@ export type LlmVersionToolResult =
| { name: "read_version"; result: ReadVersionToolOutput }
| { name: "list_versions"; result: ListVersionsToolOutput }
| { name: "restore_version"; result: RestoreVersionToolOutput };
+
+// =============================================================================
+// Mesh/Task Types for Claude Code Orchestration
+// =============================================================================
+
+export type TaskStatus =
+ | "pending"
+ | "initializing"
+ | "starting"
+ | "running"
+ | "paused"
+ | "blocked"
+ | "done"
+ | "failed"
+ | "merged";
+
+export type MergeMode = "pr" | "auto" | "manual";
+
+/** Action to perform when a task completes successfully */
+export type CompletionAction = "none" | "branch" | "merge" | "pr";
+
+export type DaemonStatus = "connected" | "disconnected" | "unhealthy";
+
+export interface TaskSummary {
+ id: string;
+ parentTaskId: string | null;
+ depth: number;
+ name: string;
+ status: TaskStatus;
+ priority: number;
+ progressSummary: string | null;
+ subtaskCount: number;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Task {
+ id: string;
+ ownerId: string;
+ parentTaskId: string | null;
+ depth: number;
+ name: string;
+ description: string | null;
+ status: TaskStatus;
+ priority: number;
+ plan: string;
+
+ // Daemon/container info
+ daemonId: string | null;
+ containerId: string | null;
+ overlayPath: string | null;
+
+ // Repository info
+ repositoryUrl: string | null;
+ baseBranch: string | null;
+ targetBranch: string | null;
+
+ // Merge settings
+ mergeMode: MergeMode | null;
+ prUrl: string | null;
+
+ // Completion action settings
+ /** Path to user's local repository for completion actions */
+ targetRepoPath: string | null;
+ /** Action on completion: "none", "branch", "merge", "pr" */
+ completionAction: CompletionAction | null;
+
+ // Progress tracking
+ progressSummary: string | null;
+ lastOutput: string | null;
+ errorMessage: string | null;
+
+ // Timestamps
+ startedAt: string | null;
+ completedAt: string | null;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface TaskWithSubtasks extends Task {
+ subtasks: TaskSummary[];
+}
+
+export interface TaskListResponse {
+ tasks: TaskSummary[];
+ total: number;
+}
+
+export interface CreateTaskRequest {
+ name: string;
+ description?: string;
+ plan: string;
+ parentTaskId?: string;
+ priority?: number;
+ repositoryUrl?: string;
+ baseBranch?: string;
+ targetBranch?: string;
+ mergeMode?: MergeMode;
+ /** Path to user's local repository for completion actions */
+ targetRepoPath?: string;
+ /** Action on completion: "none", "branch", "merge", "pr" */
+ completionAction?: CompletionAction;
+}
+
+export interface UpdateTaskRequest {
+ name?: string;
+ description?: string;
+ plan?: string;
+ status?: TaskStatus;
+ priority?: number;
+ progressSummary?: string;
+ lastOutput?: string;
+ errorMessage?: string;
+ mergeMode?: MergeMode;
+ prUrl?: string;
+ /** Path to user's local repository for completion actions */
+ targetRepoPath?: string;
+ /** Action on completion: "none", "branch", "merge", "pr" */
+ completionAction?: CompletionAction;
+ version?: number;
+}
+
+export interface TaskEvent {
+ id: string;
+ taskId: string;
+ eventType: string;
+ previousStatus: string | null;
+ newStatus: string | null;
+ eventData: Record<string, unknown> | null;
+ createdAt: string;
+}
+
+export interface TaskEventListResponse {
+ events: TaskEvent[];
+ total: number;
+}
+
+export interface Daemon {
+ id: string;
+ ownerId: string;
+ connectionId: string;
+ hostname: string | null;
+ machineId: string | null;
+ maxConcurrentTasks: number;
+ currentTaskCount: number;
+ status: DaemonStatus;
+ lastHeartbeatAt: string;
+ connectedAt: string;
+ disconnectedAt: string | null;
+}
+
+export interface DaemonListResponse {
+ daemons: Daemon[];
+ total: number;
+}
+
+// Mesh API functions
+export async function listTasks(): Promise<TaskListResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`);
+ if (!res.ok) {
+ throw new Error(`Failed to list tasks: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function getTask(id: string): Promise<TaskWithSubtasks> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`);
+ if (!res.ok) {
+ throw new Error(`Failed to get task: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function createTask(data: CreateTaskRequest): Promise<Task> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks`, {
+ method: "POST",
+ body: JSON.stringify(data),
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to create task: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function updateTask(
+ id: string,
+ data: UpdateTaskRequest
+): Promise<Task> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, {
+ method: "PUT",
+ body: JSON.stringify(data),
+ });
+
+ if (res.status === 409) {
+ const conflict = (await res.json()) as ConflictErrorResponse;
+ throw new VersionConflictError(conflict);
+ }
+
+ if (!res.ok) {
+ throw new Error(`Failed to update task: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function deleteTask(id: string): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to delete task: ${res.statusText}`);
+ }
+}
+
+export async function startTask(id: string): Promise<Task> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/start`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to start task: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function stopTask(id: string): Promise<Task> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${id}/stop`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to stop task: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+export interface SendMessageResponse {
+ success: boolean;
+ taskId: string;
+ messageLength: number;
+}
+
+/**
+ * Send a message to a running task's stdin.
+ * This can be used to provide input to Claude Code when it's waiting for user input,
+ * or to inject context/instructions into a running task.
+ */
+export async function sendTaskMessage(
+ taskId: string,
+ message: string
+): Promise<SendMessageResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/message`, {
+ method: "POST",
+ body: JSON.stringify({ message }),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to send message: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+export interface RetryCompletionResponse {
+ success: boolean;
+ taskId: string;
+ action: string;
+ targetRepoPath: string;
+ message: string;
+}
+
+/**
+ * Retry completion action for a completed task.
+ * This allows retrying a completion action (push branch, merge, create PR)
+ * after filling in the target_repo_path if it wasn't set when the task completed.
+ */
+export async function retryCompletionAction(
+ taskId: string
+): Promise<RetryCompletionResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/retry-completion`, {
+ method: "POST",
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to retry completion action: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+/** A suggested directory from a connected daemon */
+export interface DaemonDirectory {
+ /** Path to the directory */
+ path: string;
+ /** Display label for the directory */
+ label: string;
+ /** Type of directory: "working", "makima", "worktrees" */
+ directoryType: string;
+ /** Daemon hostname this directory is from */
+ hostname: string | null;
+ /** Whether the directory already exists (for validation) */
+ exists?: boolean;
+}
+
+export interface DaemonDirectoriesResponse {
+ directories: DaemonDirectory[];
+}
+
+/**
+ * Get suggested directories from connected daemons.
+ * These can be used as target_repo_path suggestions for completion actions.
+ */
+export async function getDaemonDirectories(): Promise<DaemonDirectoriesResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/directories`);
+ if (!res.ok) {
+ throw new Error(`Failed to get daemon directories: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Request to clone a worktree */
+export interface CloneWorktreeRequest {
+ targetDir: string;
+}
+
+/** Response from clone worktree */
+export interface CloneWorktreeResponse {
+ status: string;
+ taskId: string;
+ targetDir: string;
+}
+
+/**
+ * Clone a task's worktree to a target directory.
+ */
+export async function cloneWorktree(
+ taskId: string,
+ targetDir: string
+): Promise<CloneWorktreeResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/clone`, {
+ method: "POST",
+ body: JSON.stringify({ targetDir }),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to clone worktree: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+/** Request to check if target exists */
+export interface CheckTargetExistsRequest {
+ targetDir: string;
+}
+
+/** Response from check target exists */
+export interface CheckTargetExistsResponse {
+ status: string;
+ taskId: string;
+ targetDir: string;
+}
+
+/**
+ * Check if a target directory exists.
+ */
+export async function checkTargetExists(
+ taskId: string,
+ targetDir: string
+): Promise<CheckTargetExistsResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/check-target`, {
+ method: "POST",
+ body: JSON.stringify({ targetDir }),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to check target: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function listSubtasks(taskId: string): Promise<TaskListResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/subtasks`);
+ if (!res.ok) {
+ throw new Error(`Failed to list subtasks: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function listTaskEvents(
+ taskId: string
+): Promise<TaskEventListResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/events`);
+ if (!res.ok) {
+ throw new Error(`Failed to list task events: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/** A single output entry from a Claude Code task */
+export interface TaskOutputEntry {
+ id: string;
+ taskId: string;
+ /** Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw" */
+ messageType: string;
+ /** Main text content */
+ content: string;
+ /** Tool name if tool_use message */
+ toolName?: string;
+ /** Tool input JSON if tool_use message */
+ toolInput?: Record<string, unknown>;
+ /** Whether tool result was an error */
+ isError?: boolean;
+ /** Cost in USD if result message */
+ costUsd?: number;
+ /** Duration in ms if result message */
+ durationMs?: number;
+ /** Timestamp when this output was recorded */
+ createdAt: string;
+}
+
+/** Response from the task output endpoint */
+export interface TaskOutputResponse {
+ entries: TaskOutputEntry[];
+ total: number;
+ taskId: string;
+}
+
+/**
+ * Get task output history.
+ * Retrieves all recorded output from a task's Claude Code process.
+ * Use this to fetch missed output when subscribing late or reconnecting.
+ */
+export async function getTaskOutput(
+ taskId: string
+): Promise<TaskOutputResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/output`);
+ if (!res.ok) {
+ throw new Error(`Failed to get task output: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function listDaemons(): Promise<DaemonListResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons`);
+ if (!res.ok) {
+ throw new Error(`Failed to list daemons: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+export async function getDaemon(id: string): Promise<Daemon> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/daemons/${id}`);
+ if (!res.ok) {
+ throw new Error(`Failed to get daemon: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
+// Mesh Chat Types for Task Orchestration
+// =============================================================================
+
+export interface MeshChatMessage {
+ role: "user" | "assistant";
+ content: string;
+}
+
+export interface MeshChatRequest {
+ message: string;
+ model?: LlmModel;
+ history?: MeshChatMessage[];
+}
+
+export interface MeshToolCallInfo {
+ name: string;
+ result: {
+ success: boolean;
+ message: string;
+ };
+}
+
+export interface MeshChatResponse {
+ response: string;
+ toolCalls: MeshToolCallInfo[];
+ pendingQuestions?: UserQuestion[];
+}
+
+// Mesh Chat API functions
+
+// Top-level mesh chat (no specific task context)
+export async function chatWithMesh(
+ message: string,
+ model?: LlmModel,
+ history?: MeshChatMessage[]
+): Promise<MeshChatResponse> {
+ const body: MeshChatRequest = { message };
+ if (model) {
+ body.model = model;
+ }
+ if (history && history.length > 0) {
+ body.history = history;
+ }
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, {
+ method: "POST",
+ body: JSON.stringify(body),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+// Task-scoped mesh chat
+export async function chatWithTask(
+ taskId: string,
+ message: string,
+ model?: LlmModel,
+ history?: MeshChatMessage[]
+): Promise<MeshChatResponse> {
+ const body: MeshChatRequest = { message };
+ if (model) {
+ body.model = model;
+ }
+ if (history && history.length > 0) {
+ body.history = history;
+ }
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/tasks/${taskId}/chat`, {
+ method: "POST",
+ body: JSON.stringify(body),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
+// Mesh Chat History Types
+// =============================================================================
+
+export type MeshChatContextType = "mesh" | "task" | "subtask";
+
+export interface MeshChatContext {
+ type: MeshChatContextType;
+ taskId?: string;
+ parentTaskId?: string;
+}
+
+export interface MeshChatMessageRecord {
+ id: string;
+ conversationId: string;
+ role: "user" | "assistant" | "error";
+ content: string;
+ contextType: MeshChatContextType;
+ contextTaskId: string | null;
+ toolCalls: MeshToolCallInfo[] | null;
+ pendingQuestions: UserQuestion[] | null;
+ createdAt: string;
+}
+
+export interface MeshChatHistoryResponse {
+ conversationId: string;
+ messages: MeshChatMessageRecord[];
+}
+
+export interface MeshChatWithContextRequest {
+ message: string;
+ model?: LlmModel;
+ contextType?: MeshChatContextType;
+ contextTaskId?: string;
+}
+
+// =============================================================================
+// Mesh Chat History API Functions
+// =============================================================================
+
+/**
+ * Get the current chat history from the database
+ */
+export async function getMeshChatHistory(): Promise<MeshChatHistoryResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`);
+ if (!res.ok) {
+ throw new Error(`Failed to get chat history: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Clear chat history (archives current conversation, starts new one)
+ */
+export async function clearMeshChatHistory(): Promise<{ success: boolean; conversationId: string }> {
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/chat/history`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to clear chat history: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Chat with mesh using context (new approach with DB history)
+ */
+export async function chatWithMeshContext(
+ message: string,
+ context: MeshChatContext,
+ model?: LlmModel
+): Promise<MeshChatResponse> {
+ const body: MeshChatWithContextRequest = {
+ message,
+ contextType: context.type,
+ };
+
+ if (model) {
+ body.model = model;
+ }
+
+ // Set contextTaskId based on context type
+ if (context.type === "task" && context.taskId) {
+ body.contextTaskId = context.taskId;
+ } else if (context.type === "subtask" && context.taskId) {
+ body.contextTaskId = context.taskId;
+ }
+
+ // Use top-level endpoint (it now loads history from DB)
+ const res = await authFetch(`${API_BASE}/api/v1/mesh/chat`, {
+ method: "POST",
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Mesh chat failed: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
+// API Key Management
+// =============================================================================
+
+export interface ApiKeyInfo {
+ id: string;
+ prefix: string;
+ name: string | null;
+ lastUsedAt: string | null;
+ createdAt: string;
+}
+
+export interface CreateApiKeyResponse {
+ id: string;
+ key: string;
+ prefix: string;
+ name: string | null;
+ createdAt: string;
+}
+
+export interface RefreshApiKeyResponse {
+ id: string;
+ key: string;
+ prefix: string;
+ name: string | null;
+ createdAt: string;
+ previousKeyRevoked: boolean;
+}
+
+export interface RevokeApiKeyResponse {
+ message: string;
+ revokedKeyPrefix: string;
+}
+
+/**
+ * Get information about the current active API key.
+ */
+export async function getApiKey(): Promise<ApiKeyInfo | null> {
+ const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`);
+ if (res.status === 404) {
+ return null;
+ }
+ if (!res.ok) {
+ throw new Error(`Failed to get API key: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Create a new API key.
+ */
+export async function createApiKey(name?: string): Promise<CreateApiKeyResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, {
+ method: "POST",
+ body: JSON.stringify({ name }),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to create API key: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Refresh (rotate) the current API key.
+ */
+export async function refreshApiKey(name?: string): Promise<RefreshApiKeyResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys/refresh`, {
+ method: "POST",
+ body: JSON.stringify({ name }),
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to refresh API key: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Revoke the current API key.
+ */
+export async function revokeApiKey(): Promise<RevokeApiKeyResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/auth/api-keys`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
+ const errorText = await res.text();
+ throw new Error(`Failed to revoke API key: ${errorText || res.statusText}`);
+ }
+ return res.json();
+}
+
+// =============================================================================
+// User Account Management
+// =============================================================================
+
+export interface ChangePasswordRequest {
+ currentPassword: string;
+ newPassword: string;
+}
+
+export interface ChangePasswordResponse {
+ success: boolean;
+ message: string;
+}
+
+export interface ChangeEmailRequest {
+ password: string;
+ newEmail: string;
+}
+
+export interface ChangeEmailResponse {
+ success: boolean;
+ message: string;
+ verificationSent: boolean;
+}
+
+export interface DeleteAccountRequest {
+ password: string;
+ confirmation: string;
+}
+
+export interface DeleteAccountResponse {
+ success: boolean;
+ message: string;
+}
+
+/**
+ * Change the current user's password.
+ * Requires current password verification.
+ */
+export async function changePassword(
+ currentPassword: string,
+ newPassword: string
+): Promise<ChangePasswordResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/users/me/password`, {
+ method: "PUT",
+ body: JSON.stringify({ currentPassword, newPassword }),
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => null);
+ const errorMessage = errorData?.message || res.statusText;
+ throw new Error(errorMessage);
+ }
+ return res.json();
+}
+
+/**
+ * Change the current user's email address.
+ * Requires password verification.
+ */
+export async function changeEmail(
+ password: string,
+ newEmail: string
+): Promise<ChangeEmailResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/users/me/email`, {
+ method: "PUT",
+ body: JSON.stringify({ password, newEmail }),
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => null);
+ const errorMessage = errorData?.message || res.statusText;
+ throw new Error(errorMessage);
+ }
+ return res.json();
+}
+
+/**
+ * Delete the current user's account.
+ * Requires password verification and email confirmation.
+ */
+export async function deleteAccount(
+ password: string,
+ confirmation: string
+): Promise<DeleteAccountResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/users/me`, {
+ method: "DELETE",
+ body: JSON.stringify({ password, confirmation }),
+ });
+ if (!res.ok) {
+ const errorData = await res.json().catch(() => null);
+ const errorMessage = errorData?.message || res.statusText;
+ throw new Error(errorMessage);
+ }
+ return res.json();
+}
diff --git a/makima/frontend/src/lib/supabase.ts b/makima/frontend/src/lib/supabase.ts
new file mode 100644
index 0000000..eedff10
--- /dev/null
+++ b/makima/frontend/src/lib/supabase.ts
@@ -0,0 +1,26 @@
+import { createClient, SupabaseClient, Session, User } from "@supabase/supabase-js";
+
+// Supabase configuration from environment variables
+const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL as string | undefined;
+const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY as string | undefined;
+
+// Only create client if configuration is available
+let supabaseClient: SupabaseClient | null = null;
+
+if (SUPABASE_URL && SUPABASE_ANON_KEY) {
+ supabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
+ auth: {
+ autoRefreshToken: true,
+ persistSession: true,
+ detectSessionInUrl: true,
+ },
+ });
+}
+
+export const supabase = supabaseClient;
+
+export function isAuthConfigured(): boolean {
+ return supabaseClient !== null;
+}
+
+export type { Session, User };