diff --git a/src/cli/remoteClient.ts b/src/cli/remoteClient.ts new file mode 100644 index 0000000..ada1426 --- /dev/null +++ b/src/cli/remoteClient.ts @@ -0,0 +1,90 @@ +import type { Task, Handoff, Decision, Memory, IndexEntry } from '../core/schema.js'; + +export class RemoteError extends Error { + constructor(public status: number, message: string) { + super(message); + } +} + +async function request(baseUrl: string, method: string, path: string, body?: unknown): Promise { + const url = `${baseUrl}${path}`; + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: body ? JSON.stringify(body) : undefined, + }); + + const text = await res.text(); + if (!res.ok) { + throw new RemoteError(res.status, text || `HTTP ${res.status}`); + } + return text ? (JSON.parse(text) as T) : (undefined as T); +} + +export const remoteClient = { + async getStatus(baseUrl: string): Promise { + return request<{ body: string }>(baseUrl, 'GET', '/status').then((r) => r.body); + }, + + async updateStatus(baseUrl: string): Promise { + return request<{ body: string }>(baseUrl, 'POST', '/status/update').then((r) => r.body); + }, + + async createTask(baseUrl: string, options: Partial): Promise { + return request(baseUrl, 'POST', '/tasks', options); + }, + + async listTasks(baseUrl: string, filters?: { status?: string; role?: string }): Promise { + const params = new URLSearchParams((filters ?? {}) as Record); + const qs = params.toString(); + return request(baseUrl, 'GET', `/tasks${qs ? '?' + qs : ''}`); + }, + + async getTask(baseUrl: string, id: string): Promise<{ task: Task; body: string }> { + return request<{ task: Task; body: string }>(baseUrl, 'GET', `/tasks/${id}`); + }, + + async claimTask(baseUrl: string, id: string, agentName: string): Promise { + return request(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'in_progress', assignedTo: agentName }); + }, + + async doneTask(baseUrl: string, id: string): Promise { + return request(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'done' }); + }, + + async createHandoff(baseUrl: string, options: Partial): Promise { + return request(baseUrl, 'POST', '/handoffs', options); + }, + + async listHandoffs(baseUrl: string): Promise { + return request(baseUrl, 'GET', '/handoffs'); + }, + + async getHandoff(baseUrl: string, id: string): Promise<{ handoff: Handoff; body: string }> { + return request<{ handoff: Handoff; body: string }>(baseUrl, 'GET', `/handoffs/${id}`); + }, + + async createDecision(baseUrl: string, options: Partial): Promise { + return request(baseUrl, 'POST', '/decisions', options); + }, + + async listDecisions(baseUrl: string): Promise { + return request(baseUrl, 'GET', '/decisions'); + }, + + async addMemory(baseUrl: string, options: Partial): Promise { + return request(baseUrl, 'POST', '/memory', options); + }, + + async searchMemory(baseUrl: string, query: string): Promise { + return request(baseUrl, 'GET', `/memory/search?q=${encodeURIComponent(query)}`); + }, + + async listMemory(baseUrl: string): Promise { + return request(baseUrl, 'GET', '/memory'); + }, + + async delegate(baseUrl: string, auto: boolean): Promise<{ suggestion?: { task: IndexEntry; role: string; preferredAgent: string }; handoff?: Handoff }> { + return request<{ suggestion?: { task: IndexEntry; role: string; preferredAgent: string }; handoff?: Handoff }>(baseUrl, 'POST', `/delegate?auto=${auto}`); + }, +}; diff --git a/tests/remoteClient.test.ts b/tests/remoteClient.test.ts new file mode 100644 index 0000000..d82e75f --- /dev/null +++ b/tests/remoteClient.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { remoteClient, RemoteError } from '../src/cli/remoteClient.js'; + +describe('remoteClient', () => { + beforeEach(() => { + vi.restoreAllMocks(); + globalThis.fetch = vi.fn(); + }); + + it('creates a task remotely', async () => { + const task = { id: 'TSK-0001', title: 'Remote task', status: 'open' }; + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + text: () => Promise.resolve(JSON.stringify(task)), + } as Response); + + const result = await remoteClient.createTask('http://localhost:3377', { title: 'Remote task' }); + expect(result.id).toBe('TSK-0001'); + }); + + it('throws RemoteError on failure', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 500, + text: () => Promise.resolve('boom'), + } as Response); + + await expect(remoteClient.listTasks('http://localhost:3377')).rejects.toThrow(RemoteError); + }); +});