/** * HTTP transport layer for the Huskies API client. * Provides the `callMcpTool` function for MCP JSON-RPC calls, the * `resolveWsHost` utility, and the `api` object exposing all endpoints. */ import { rpcCall } from "../rpc"; import type { OkResult, OpenProjectResult, SetAnthropicApiKeyParams, SetModelPreferenceParams, } from "../rpcContract"; import type { AllTokenUsageResponse, AnthropicModelInfo, FileEntry, OAuthStatus, TestResultsResponse, TokenCostResponse, WorkItemContent, } from "./types"; /** * Resolve the WebSocket host to connect to. * In development, uses the injected port (or 3001); in production, uses the * current page's host so the socket connects to the same origin. */ export function resolveWsHost( isDev: boolean, envPort: string | undefined, locationHost: string, ): string { return isDev ? `127.0.0.1:${envPort || "3001"}` : locationHost; } /** * Invoke an MCP tool via the server's JSON-RPC `/mcp` endpoint. * Returns the first text content block from the tool result, or an empty * string if the result has no content. */ export async function callMcpTool( toolName: string, args: Record, ): Promise { const res = await fetch("/mcp", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: toolName, arguments: args }, }), }); const json = await res.json(); if (json.error) { throw new Error(json.error.message); } const text = json.result?.content?.[0]?.text ?? ""; return text; } /** Typed wrappers for all Huskies server endpoints. */ export const api = { getCurrentProject(_baseUrl?: string) { return rpcCall("project.current"); }, getKnownProjects(_baseUrl?: string) { return rpcCall("project.known"); }, async forgetKnownProject(path: string, _baseUrl?: string) { const r = await rpcCall("project.forget", { path }); return r.ok; }, async openProject(path: string, _baseUrl?: string) { const r = await rpcCall("project.open", { path }); return r.path; }, async closeProject(_baseUrl?: string) { const r = await rpcCall("project.close"); return r.ok; }, getModelPreference(_baseUrl?: string) { return rpcCall("model.get_preference"); }, async setModelPreference(model: string, _baseUrl?: string) { const params: SetModelPreferenceParams = { model }; const r = await rpcCall("model.set_preference", params); return r.ok; }, getOllamaModels(baseUrlParam?: string, _baseUrl?: string) { return rpcCall( "ollama.list_models", baseUrlParam ? { base_url: baseUrlParam } : {}, ); }, getAnthropicApiKeyExists(_baseUrl?: string) { return rpcCall("anthropic.key_exists"); }, getAnthropicModels(_baseUrl?: string) { return rpcCall("anthropic.list_models"); }, async setAnthropicApiKey(api_key: string, _baseUrl?: string) { const params: SetAnthropicApiKeyParams = { api_key }; const r = await rpcCall("anthropic.set_api_key", params); return r.ok; }, readFile(path: string) { return rpcCall("io.read_file", { path }); }, listDirectoryAbsolute(path: string) { return rpcCall("io.list_directory_absolute", { path }); }, getHomeDirectory(_baseUrl?: string) { return rpcCall("io.home_directory"); }, listProjectFiles(_baseUrl?: string) { return rpcCall("io.list_project_files"); }, async cancelChat(_baseUrl?: string) { const r = await rpcCall("chat.cancel"); return r.ok; }, getWorkItemContent(storyId: string, _baseUrl?: string) { return rpcCall("work_items.get", { story_id: storyId }); }, getTestResults(storyId: string, _baseUrl?: string) { return rpcCall("work_items.test_results", { story_id: storyId, }); }, getTokenCost(storyId: string, _baseUrl?: string) { return rpcCall("work_items.token_cost", { story_id: storyId, }); }, getAllTokenUsage(_baseUrl?: string) { return rpcCall("token_usage.all"); }, /** Trigger a server rebuild and restart. */ rebuildAndRestart() { return callMcpTool("rebuild_and_restart", {}); }, /** Approve a story in QA, moving it to merge. */ approveQa(storyId: string) { return callMcpTool("approve_qa", { story_id: storyId }); }, /** Reject a story in QA, moving it back to current with notes. */ rejectQa(storyId: string, notes: string) { return callMcpTool("reject_qa", { story_id: storyId, notes }); }, /** Launch the QA app for a story's worktree. */ launchQaApp(storyId: string) { return callMcpTool("launch_qa_app", { story_id: storyId }); }, /** Delete a story from the pipeline, stopping any running agent and removing the worktree. */ deleteStory(storyId: string) { return callMcpTool("delete_story", { story_id: storyId }); }, /** Fetch OAuth status from the server. */ getOAuthStatus() { return rpcCall("oauth.status"); }, /** Execute a bot slash command without LLM invocation. Returns markdown response text. */ botCommand(command: string, args: string) { return rpcCall<{ response: string }>("bot.command", { command, args }); }, };