summaryrefslogblamecommitdiff
path: root/apps/mobile/lib/api.ts
blob: ec848b6f162240e5789ce2ecd14b40eebf18d8b7 (plain) (tree)
































































































































































































































































































                                                                                     
import { supabase } from './supabase';

// =============================================================================
// API Configuration
// =============================================================================

const API_CONFIG = {
  local: {
    http: 'http://localhost:8080',
    ws: 'ws://localhost:8080',
  },
  production: {
    http: 'https://api.makima.jp',
    ws: 'wss://api.makima.jp',
  },
} as const;

type Environment = 'local' | 'production';

// Detect environment based on __DEV__ flag
const env: Environment = __DEV__ ? 'local' : 'production';

export const API_BASE = API_CONFIG[env].http;
export const WS_BASE = API_CONFIG[env].ws;

export function getEnvironment(): Environment {
  return env;
}

// =============================================================================
// Authentication Helper
// =============================================================================

/**
 * Fetch wrapper that automatically adds authentication headers
 * Gets the access token from Supabase session
 */
async function authFetch(url: string, options: RequestInit = {}): Promise<Response> {
  const { data: { session } } = await supabase.auth.getSession();

  const headers: HeadersInit = {
    'Content-Type': 'application/json',
  };

  if (session?.access_token) {
    headers['Authorization'] = `Bearer ${session.access_token}`;
  }

  return fetch(`${API_BASE}${url}`, {
    ...options,
    headers: { ...headers, ...options.headers },
  });
}

// =============================================================================
// Mesh/Task Types
// =============================================================================

export type TaskStatus =
  | 'pending'
  | 'initializing'
  | 'starting'
  | 'running'
  | 'paused'
  | 'blocked'
  | 'done'
  | 'failed'
  | 'merged';

export type MergeMode = 'pr' | 'auto' | 'manual';
export type CompletionAction = 'none' | 'branch' | 'merge' | 'pr';
export type ContractPhase = 'research' | 'specify' | 'plan' | 'execute' | 'review';

export interface TaskSummary {
  id: string;
  contractId: string | null;
  contractName: string | null;
  contractPhase: ContractPhase | null;
  parentTaskId: string | null;
  depth: number;
  name: string;
  status: TaskStatus;
  priority: number;
  progressSummary: string | null;
  subtaskCount: number;
  isSupervisor: boolean;
  version: number;
  createdAt: string;
  updatedAt: string;
}

export interface Task {
  id: string;
  ownerId: string;
  contractId: string | null;
  parentTaskId: string | null;
  depth: number;
  name: string;
  description: string | null;
  status: TaskStatus;
  priority: number;
  plan: string;
  daemonId: string | null;
  containerId: string | null;
  overlayPath: string | null;
  repositoryUrl: string | null;
  baseBranch: string | null;
  targetBranch: string | null;
  mergeMode: MergeMode | null;
  prUrl: string | null;
  targetRepoPath: string | null;
  completionAction: CompletionAction | null;
  progressSummary: string | null;
  lastOutput: string | null;
  errorMessage: string | null;
  startedAt: string | null;
  completedAt: string | null;
  version: number;
  createdAt: string;
  updatedAt: string;
  isSupervisor: boolean;
}

export interface TaskWithSubtasks extends Task {
  subtasks: TaskSummary[];
}

export interface TaskListResponse {
  tasks: TaskSummary[];
  total: number;
}

export interface TaskOutputEntry {
  id: string;
  taskId: string;
  messageType: string;
  content: string;
  toolName?: string;
  toolInput?: Record<string, unknown>;
  isError?: boolean;
  costUsd?: number;
  durationMs?: number;
  createdAt: string;
}

export interface TaskOutputResponse {
  entries: TaskOutputEntry[];
  total: number;
  taskId: string;
}

export interface SendMessageResponse {
  success: boolean;
  taskId: string;
  messageLength: number;
}

// =============================================================================
// Pending Question Types
// =============================================================================

export interface PendingQuestion {
  questionId: string;
  taskId: string;
  contractId: string;
  question: string;
  choices: string[];
  context: string | null;
  createdAt: string;
}

export interface AnswerQuestionResponse {
  success: boolean;
}

// =============================================================================
// Task API Functions
// =============================================================================

/**
 * List all tasks for the current user
 */
export async function listTasks(): Promise<TaskListResponse> {
  const res = await authFetch('/api/v1/mesh/tasks');
  if (!res.ok) {
    throw new Error(`Failed to list tasks: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Get a specific task with its subtasks
 */
export async function getTask(id: string): Promise<TaskWithSubtasks> {
  const res = await authFetch(`/api/v1/mesh/tasks/${id}`);
  if (!res.ok) {
    throw new Error(`Failed to get task: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Start a pending task
 */
export async function startTask(id: string): Promise<Task> {
  const res = await authFetch(`/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();
}

/**
 * Stop a running task
 */
export async function stopTask(id: string): Promise<Task> {
  const res = await authFetch(`/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();
}

/**
 * Send a message to a running task's stdin
 */
export async function sendTaskMessage(
  taskId: string,
  message: string
): Promise<SendMessageResponse> {
  const res = await authFetch(`/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();
}

/**
 * Get task output history
 */
export async function getTaskOutput(taskId: string): Promise<TaskOutputResponse> {
  const res = await authFetch(`/api/v1/mesh/tasks/${taskId}/output`);
  if (!res.ok) {
    throw new Error(`Failed to get task output: ${res.statusText}`);
  }
  return res.json();
}

// =============================================================================
// Question API Functions
// =============================================================================

/**
 * Get all pending supervisor questions for the current user
 */
export async function listPendingQuestions(): Promise<PendingQuestion[]> {
  const res = await authFetch('/api/v1/mesh/questions');
  if (!res.ok) {
    throw new Error(`Failed to list questions: ${res.statusText}`);
  }
  return res.json();
}

/**
 * Answer a pending supervisor question
 */
export async function answerQuestion(
  questionId: string,
  response: string
): Promise<AnswerQuestionResponse> {
  const res = await authFetch(`/api/v1/mesh/questions/${questionId}/answer`, {
    method: 'POST',
    body: JSON.stringify({ response }),
  });
  if (!res.ok) {
    throw new Error(`Failed to answer question: ${res.statusText}`);
  }
  return res.json();
}