agenthub/src/cli/remoteClient.ts
chahinebrini 2c2193ec55 feat(agenthub): TSK-0074 — automatic agent progress logging to a task live console
- routes PATCH /tasks/🆔 uniformly appendTaskLog + publishLog for every status
  transition (claim/review/done/cancel/reopen) and on assign; best-effort, never
  fails the mutation. No double-emission (log rides the 'log' channel; fsWatch
  only watches .md so the .log append is not re-emitted).
- MCP agenthub_task_log tool + remoteClient.appendTaskLog + CLI 'task log <id>
  --text [--agent][--level]'
- agenthub_work LOOP reminder: 'Report meaningful progress with agenthub_task_log'
- taskDetail: Live Console panel — historic lines via readTaskLog + live tail via
  the named task-log SSE event filtered to this task id
- tests: +taskLog-live.test.ts (PATCH->one log event, no double change, historic
  render, POST /log, remoteClient)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 01:02:04 +02:00

161 lines
6.1 KiB
TypeScript

import type { Task, Handoff, Decision, Memory, Message, ActivityItem } from '../core/schema.js';
import type { IndexEntry } from '../core/index.js';
import type { InboxMessage } from '../core/services/messageService.js';
import type { TaskLogEntry } from '../core/services/taskLogService.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}`;
let res: Response;
try {
res = await fetch(url, {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new RemoteError(0, `Cannot reach AgentHub server at ${baseUrl}: ${message}`);
}
const text = await res.text();
if (!res.ok) {
throw new RemoteError(res.status, text || `HTTP ${res.status}`);
}
if (!text) {
throw new RemoteError(res.status, 'Empty response from server');
}
return JSON.parse(text) as T;
}
export const remoteClient = {
async getStatus(baseUrl: string): Promise<string> {
return request<{ body: string }>(baseUrl, 'GET', '/status').then((r) => r.body);
},
async announce(baseUrl: string, agent: string, role?: string, action: 'joined' | 'left' = 'joined'): Promise<void> {
await request<{ ok: boolean }>(baseUrl, 'POST', '/announce', { agent, role, action });
},
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,
meta?: { tokens?: number; duration?: number; by?: string },
): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, {
status: 'done',
doneTokens: meta?.tokens,
doneDuration: meta?.duration,
doneBy: meta?.by,
});
},
async assignTask(baseUrl: string, id: string, agentName: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { assignedTo: agentName });
},
async reviewTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'review' });
},
async reopenTask(baseUrl: string, id: string): Promise<Task> {
return request<Task>(baseUrl, 'PATCH', `/tasks/${id}`, { status: 'open' });
},
async getTaskActivity(baseUrl: string, id: string): Promise<ActivityItem[]> {
return request<ActivityItem[]>(baseUrl, 'GET', `/tasks/${id}/activity`);
},
async appendTaskLog(
baseUrl: string,
id: string,
entry: { text: string; agent?: string; level?: string },
): Promise<TaskLogEntry> {
return request<TaskLogEntry>(baseUrl, 'POST', `/tasks/${id}/log`, entry);
},
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 sendMessage(baseUrl: string, options: Partial<Message>): Promise<Message> {
return request<Message>(baseUrl, 'POST', '/messages', options);
},
async getInbox(baseUrl: string, agent: string, unreadOnly = false): Promise<InboxMessage[]> {
const q = `?agent=${encodeURIComponent(agent)}${unreadOnly ? '&unread=1' : ''}`;
return request<InboxMessage[]>(baseUrl, 'GET', `/messages${q}`);
},
async markMessageRead(baseUrl: string, id: string): Promise<Message> {
return request<Message>(baseUrl, 'POST', `/messages/${id}/read`);
},
async ackMessage(baseUrl: string, id: string, by?: string): Promise<Message> {
return request<Message>(baseUrl, 'POST', `/messages/${id}/ack`, by ? { by } : undefined);
},
async getMessage(baseUrl: string, id: string): Promise<{ message: Message; body: string }> {
return request<{ message: Message; body: string }>(baseUrl, 'GET', `/messages/${id}`);
},
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}`);
},
};