feat(cli): command architecture — agents, missions, gateway-aware prdy (#158)
Some checks failed
ci/woodpecker/push/ci Pipeline failed

Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #158.
This commit is contained in:
2026-03-15 23:10:23 +00:00
committed by jason.woltje
parent 82c10a7b33
commit 4da255bf04
28 changed files with 1747 additions and 394 deletions

View File

@@ -19,6 +19,9 @@ export interface TuiAppProps {
sessionCookie?: string;
initialModel?: string;
initialProvider?: string;
agentId?: string;
agentName?: string;
projectId?: string;
}
export function TuiApp({
@@ -27,6 +30,9 @@ export function TuiApp({
sessionCookie,
initialModel,
initialProvider,
agentId,
agentName,
projectId: _projectId,
}: TuiAppProps) {
const { exit } = useApp();
const gitInfo = useGitInfo();
@@ -38,6 +44,7 @@ export function TuiApp({
initialConversationId: conversationId,
initialModel,
initialProvider,
agentId,
});
const conversations = useConversations({ gatewayUrl, sessionCookie });
@@ -211,7 +218,7 @@ export function TuiApp({
modelName={socket.modelName}
thinkingLevel={socket.thinkingLevel}
contextWindow={socket.tokenUsage.contextWindow}
agentName="default"
agentName={agentName ?? 'default'}
connected={socket.connected}
connecting={socket.connecting}
/>

View File

@@ -1,5 +1,5 @@
/**
* Minimal gateway REST API client for the TUI.
* Minimal gateway REST API client for the TUI and CLI commands.
*/
export interface ModelInfo {
@@ -30,10 +30,88 @@ export interface SessionListResult {
total: number;
}
/**
* Fetch the list of available models from the gateway.
* Returns an empty array on network or auth errors so the TUI can still function.
*/
// ── Agent Config types ──
export interface AgentConfigInfo {
id: string;
name: string;
provider: string;
model: string;
status: string;
projectId: string | null;
ownerId: string | null;
systemPrompt: string | null;
allowedTools: string[] | null;
skills: string[] | null;
isSystem: boolean;
config: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
}
// ── Project types ──
export interface ProjectInfo {
id: string;
name: string;
description: string | null;
status: string;
ownerId: string | null;
createdAt: string;
updatedAt: string;
}
// ── Mission types ──
export interface MissionInfo {
id: string;
name: string;
description: string | null;
status: string;
projectId: string | null;
userId: string | null;
phase: string | null;
milestones: Record<string, unknown>[] | null;
config: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
}
// ── Mission Task types ──
export interface MissionTaskInfo {
id: string;
missionId: string;
taskId: string | null;
userId: string;
status: string;
description: string | null;
notes: string | null;
pr: string | null;
createdAt: string;
updatedAt: string;
}
// ── Helpers ──
function headers(sessionCookie: string, gatewayUrl: string) {
return { Cookie: sessionCookie, Origin: gatewayUrl };
}
function jsonHeaders(sessionCookie: string, gatewayUrl: string) {
return { ...headers(sessionCookie, gatewayUrl), 'Content-Type': 'application/json' };
}
async function handleResponse<T>(res: Response, errorPrefix: string): Promise<T> {
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`${errorPrefix} (${res.status}): ${body}`);
}
return (await res.json()) as T;
}
// ── Provider / Model endpoints ──
export async function fetchAvailableModels(
gatewayUrl: string,
sessionCookie?: string,
@@ -53,10 +131,6 @@ export async function fetchAvailableModels(
}
}
/**
* Fetch the list of providers (with their models) from the gateway.
* Returns an empty array on network or auth errors.
*/
export async function fetchProviders(
gatewayUrl: string,
sessionCookie?: string,
@@ -76,28 +150,18 @@ export async function fetchProviders(
}
}
/**
* Fetch the list of active agent sessions from the gateway.
* Throws on network or auth errors.
*/
// ── Session endpoints ──
export async function fetchSessions(
gatewayUrl: string,
sessionCookie: string,
): Promise<SessionListResult> {
const res = await fetch(`${gatewayUrl}/api/sessions`, {
headers: { Cookie: sessionCookie, Origin: gatewayUrl },
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to list sessions (${res.status}): ${body}`);
}
return (await res.json()) as SessionListResult;
return handleResponse<SessionListResult>(res, 'Failed to list sessions');
}
/**
* Destroy (terminate) an agent session on the gateway.
* Throws on network or auth errors.
*/
export async function deleteSession(
gatewayUrl: string,
sessionCookie: string,
@@ -105,10 +169,220 @@ export async function deleteSession(
): Promise<void> {
const res = await fetch(`${gatewayUrl}/api/sessions/${encodeURIComponent(sessionId)}`, {
method: 'DELETE',
headers: { Cookie: sessionCookie, Origin: gatewayUrl },
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok && res.status !== 204) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to destroy session (${res.status}): ${body}`);
}
}
// ── Agent Config endpoints ──
export async function fetchAgentConfigs(
gatewayUrl: string,
sessionCookie: string,
): Promise<AgentConfigInfo[]> {
const res = await fetch(`${gatewayUrl}/api/agents`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<AgentConfigInfo[]>(res, 'Failed to list agents');
}
export async function fetchAgentConfig(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<AgentConfigInfo> {
const res = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(id)}`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<AgentConfigInfo>(res, 'Failed to get agent');
}
export async function createAgentConfig(
gatewayUrl: string,
sessionCookie: string,
data: {
name: string;
provider: string;
model: string;
projectId?: string;
systemPrompt?: string;
allowedTools?: string[];
skills?: string[];
config?: Record<string, unknown>;
},
): Promise<AgentConfigInfo> {
const res = await fetch(`${gatewayUrl}/api/agents`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<AgentConfigInfo>(res, 'Failed to create agent');
}
export async function updateAgentConfig(
gatewayUrl: string,
sessionCookie: string,
id: string,
data: Record<string, unknown>,
): Promise<AgentConfigInfo> {
const res = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<AgentConfigInfo>(res, 'Failed to update agent');
}
export async function deleteAgentConfig(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<void> {
const res = await fetch(`${gatewayUrl}/api/agents/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok && res.status !== 204) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to delete agent (${res.status}): ${body}`);
}
}
// ── Project endpoints ──
export async function fetchProjects(
gatewayUrl: string,
sessionCookie: string,
): Promise<ProjectInfo[]> {
const res = await fetch(`${gatewayUrl}/api/projects`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<ProjectInfo[]>(res, 'Failed to list projects');
}
// ── Mission endpoints ──
export async function fetchMissions(
gatewayUrl: string,
sessionCookie: string,
): Promise<MissionInfo[]> {
const res = await fetch(`${gatewayUrl}/api/missions`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<MissionInfo[]>(res, 'Failed to list missions');
}
export async function fetchMission(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<MissionInfo> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(id)}`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<MissionInfo>(res, 'Failed to get mission');
}
export async function createMission(
gatewayUrl: string,
sessionCookie: string,
data: {
name: string;
description?: string;
projectId?: string;
status?: string;
phase?: string;
milestones?: Record<string, unknown>[];
config?: Record<string, unknown>;
},
): Promise<MissionInfo> {
const res = await fetch(`${gatewayUrl}/api/missions`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<MissionInfo>(res, 'Failed to create mission');
}
export async function updateMission(
gatewayUrl: string,
sessionCookie: string,
id: string,
data: Record<string, unknown>,
): Promise<MissionInfo> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(id)}`, {
method: 'PATCH',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<MissionInfo>(res, 'Failed to update mission');
}
export async function deleteMission(
gatewayUrl: string,
sessionCookie: string,
id: string,
): Promise<void> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(id)}`, {
method: 'DELETE',
headers: headers(sessionCookie, gatewayUrl),
});
if (!res.ok && res.status !== 204) {
const body = await res.text().catch(() => '');
throw new Error(`Failed to delete mission (${res.status}): ${body}`);
}
}
// ── Mission Task endpoints ──
export async function fetchMissionTasks(
gatewayUrl: string,
sessionCookie: string,
missionId: string,
): Promise<MissionTaskInfo[]> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(missionId)}/tasks`, {
headers: headers(sessionCookie, gatewayUrl),
});
return handleResponse<MissionTaskInfo[]>(res, 'Failed to list mission tasks');
}
export async function createMissionTask(
gatewayUrl: string,
sessionCookie: string,
missionId: string,
data: {
description?: string;
status?: string;
notes?: string;
pr?: string;
taskId?: string;
},
): Promise<MissionTaskInfo> {
const res = await fetch(`${gatewayUrl}/api/missions/${encodeURIComponent(missionId)}/tasks`, {
method: 'POST',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
});
return handleResponse<MissionTaskInfo>(res, 'Failed to create mission task');
}
export async function updateMissionTask(
gatewayUrl: string,
sessionCookie: string,
missionId: string,
taskId: string,
data: Record<string, unknown>,
): Promise<MissionTaskInfo> {
const res = await fetch(
`${gatewayUrl}/api/missions/${encodeURIComponent(missionId)}/tasks/${encodeURIComponent(taskId)}`,
{
method: 'PATCH',
headers: jsonHeaders(sessionCookie, gatewayUrl),
body: JSON.stringify(data),
},
);
return handleResponse<MissionTaskInfo>(res, 'Failed to update mission task');
}

View File

@@ -43,6 +43,7 @@ export interface UseSocketOptions {
initialConversationId?: string;
initialModel?: string;
initialProvider?: string;
agentId?: string;
}
export interface UseSocketReturn {
@@ -80,7 +81,14 @@ const EMPTY_USAGE: TokenUsage = {
};
export function useSocket(opts: UseSocketOptions): UseSocketReturn {
const { gatewayUrl, sessionCookie, initialConversationId, initialModel, initialProvider } = opts;
const {
gatewayUrl,
sessionCookie,
initialConversationId,
initialModel,
initialProvider,
agentId,
} = opts;
const [connected, setConnected] = useState(false);
const [connecting, setConnecting] = useState(true);
@@ -231,6 +239,7 @@ export function useSocket(opts: UseSocketOptions): UseSocketReturn {
content,
...(initialProvider ? { provider: initialProvider } : {}),
...(initialModel ? { modelId: initialModel } : {}),
...(agentId ? { agentId } : {}),
});
},
[conversationId, isStreaming],