feat(cli): add RemoteClient for network mode
This commit is contained in:
parent
a0af702344
commit
fb3da93f59
90
src/cli/remoteClient.ts
Normal file
90
src/cli/remoteClient.ts
Normal 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}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
31
tests/remoteClient.test.ts
Normal file
31
tests/remoteClient.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user