feat(cli): add RemoteClient for network mode

This commit is contained in:
chahinebrini 2026-06-25 12:32:11 +02:00
parent a0af702344
commit fb3da93f59
2 changed files with 121 additions and 0 deletions

90
src/cli/remoteClient.ts Normal file
View File

@ -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<T>(baseUrl: string, method: string, path: string, body?: unknown): Promise<T> {
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<string> {
return request<{ body: string }>(baseUrl, 'GET', '/status').then((r) => r.body);
},
async updateStatus(baseUrl: string): Promise<string> {
return request<{ body: string }>(baseUrl, 'POST', '/status/update').then((r) => r.body);
},
async createTask(baseUrl: string, options: Partial<Task>): Promise<Task> {
return request<Task>(baseUrl, 'POST', '/tasks', options);
},
async listTasks(baseUrl: string, filters?: { status?: string; role?: string }): Promise<IndexEntry[]> {
const params = new URLSearchParams((filters ?? {}) as Record<string, string>);
const qs = params.toString();
return request<IndexEntry[]>(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<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'in_progress', assignedTo: agentName });
},
async doneTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'done' });
},
async createHandoff(baseUrl: string, options: Partial<Handoff>): Promise<Handoff> {
return request<Handoff>(baseUrl, 'POST', '/handoffs', options);
},
async listHandoffs(baseUrl: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(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<Decision>): Promise<Decision> {
return request<Decision>(baseUrl, 'POST', '/decisions', options);
},
async listDecisions(baseUrl: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(baseUrl, 'GET', '/decisions');
},
async addMemory(baseUrl: string, options: Partial<Memory>): Promise<Memory> {
return request<Memory>(baseUrl, 'POST', '/memory', options);
},
async searchMemory(baseUrl: string, query: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(baseUrl, 'GET', `/memory/search?q=${encodeURIComponent(query)}`);
},
async listMemory(baseUrl: string): Promise<IndexEntry[]> {
return request<IndexEntry[]>(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}`);
},
};

View File

@ -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);
});
});