Files
EJClaw/apps/dashboard/src/api.ts
Eyejoker 85da2b918c Add room skill setting mutations (#133)
* add gated codex goals support

* sync README SDK versions

* bump claude agent sdk

* add codex goals settings toggle

* reuse status dashboard message and simplify settings actions

* refine settings page UX

* fix settings nav hash routing

* add dashboard UX verification

* refine dashboard settings and inbox UX

* remove inbox top-level navigation

* remove dashboard health top-level navigation

* fix: allow runtime image attachment paths

* feat: configure attachment allowlist dirs

* fix: clean duplicate dashboard status messages

* fix: poll dashboard duplicate cleanup between status updates

* fix: delete dashboard duplicates on create

* Refine settings IA with tabbed sections

* Add read-only runtime inventory settings

* Fix runtime inventory MCP JSON parsing

* Add room skill settings inventory

* Add room skill setting mutations
2026-05-04 03:27:35 +09:00

783 lines
19 KiB
TypeScript

export interface DashboardOverview {
generatedAt: string;
services: Array<{
serviceId: string;
assistantName: string;
agentType: string;
updatedAt: string;
totalRooms: number;
activeRooms: number;
}>;
rooms: {
total: number;
active: number;
waiting: number;
inactive: number;
};
tasks: {
total: number;
active: number;
paused: number;
completed: number;
watchers: {
active: number;
paused: number;
completed: number;
};
};
usage: {
rows: Array<{
name: string;
h5pct: number;
h5reset: string;
d7pct: number;
d7reset: string;
}>;
fetchedAt: string | null;
};
operations?: {
serviceRestarts: DashboardServiceRestart[];
};
inbox: Array<{
id: string;
groupKey: string;
kind:
| 'pending-room'
| 'reviewer-request'
| 'approval'
| 'arbiter-request'
| 'ci-failure'
| 'mention';
severity: 'info' | 'warn' | 'error';
title: string;
summary: string;
occurredAt: string;
lastOccurredAt: string;
createdAt: string;
occurrences: number;
source: 'status-snapshot' | 'paired-task' | 'scheduled-task';
roomJid?: string;
roomName?: string;
groupFolder?: string;
serviceId?: string;
taskId?: string;
taskStatus?: string;
}>;
}
export interface DashboardServiceRestart {
id: string;
target: 'stack';
requestedAt: string;
completedAt: string | null;
status: 'running' | 'success' | 'failed';
services: string[];
error?: string;
}
export interface StatusSnapshot {
serviceId: string;
agentType: string;
assistantName: string;
updatedAt: string;
entries: Array<{
jid: string;
name: string;
folder: string;
agentType: string;
status: 'processing' | 'waiting' | 'inactive';
elapsedMs: number | null;
pendingMessages: boolean;
pendingTasks: number;
}>;
}
export interface DashboardRoomActivity {
serviceId: string;
jid: string;
name: string;
folder: string;
agentType: string;
status: 'processing' | 'waiting' | 'inactive';
elapsedMs: number | null;
pendingMessages: boolean;
pendingTasks: number;
messages: Array<{
id: string;
sender: string;
senderName: string;
content: string;
attachments?: Array<{
path: string;
name?: string;
mime?: string;
}>;
timestamp: string;
isFromMe: boolean;
isBotMessage: boolean;
sourceKind: string;
}>;
pairedTask: {
id: string;
title: string | null;
status: string;
roundTripCount: number;
updatedAt: string;
currentTurn: {
turnId: string;
role: string;
intentKind: string;
state: string;
attemptNo: number;
executorServiceId: string | null;
executorAgentType: string | null;
activeRunId: string | null;
createdAt: string;
updatedAt: string;
completedAt: string | null;
lastError: string | null;
progressText: string | null;
progressUpdatedAt: string | null;
} | null;
outputs: Array<{
id: number;
turnNumber: number;
role: string;
verdict: string | null;
createdAt: string;
outputText: string;
attachments?: Array<{
path: string;
name?: string;
mime?: string;
}>;
}>;
} | null;
}
export interface DashboardTask {
id: string;
groupFolder: string;
chatJid: string;
agentType: string | null;
ciProvider: string | null;
ciMetadata: string | null;
scheduleType: string;
scheduleValue: string;
contextMode: string;
nextRun: string | null;
lastRun: string | null;
lastResult: string | null;
status: 'active' | 'paused' | 'completed';
suspendedUntil: string | null;
createdAt: string;
promptPreview: string;
promptLength: number;
isWatcher: boolean;
}
export type DashboardTaskAction = 'pause' | 'resume' | 'cancel';
export type DashboardInboxAction = 'run' | 'decline' | 'dismiss';
export type DashboardServiceAction = 'restart';
export type DashboardTaskScheduleType = 'cron' | 'interval' | 'once';
export type DashboardTaskContextMode = 'group' | 'isolated';
export interface CreateScheduledTaskInput {
roomJid: string;
prompt: string;
scheduleType: DashboardTaskScheduleType;
scheduleValue: string;
contextMode: DashboardTaskContextMode;
requestId?: string;
}
export interface UpdateScheduledTaskInput {
prompt?: string;
scheduleType: DashboardTaskScheduleType;
scheduleValue: string;
}
async function fetchJson<T>(path: string): Promise<T> {
const response = await fetch(path, {
headers: { accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`${path} failed: ${response.status}`);
}
return (await response.json()) as T;
}
async function postJson<T>(path: string, body: unknown): Promise<T> {
const response = await fetch(path, {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
let message = `${path} failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) message = payload.error;
} catch {
// Keep the status-based message when the server body is not JSON.
}
throw new Error(message);
}
return (await response.json()) as T;
}
export async function fetchDashboardData(): Promise<{
overview: DashboardOverview;
snapshots: StatusSnapshot[];
tasks: DashboardTask[];
}> {
const [overview, snapshots, tasks] = await Promise.all([
fetchJson<DashboardOverview>('/api/overview'),
fetchJson<StatusSnapshot[]>('/api/status-snapshots'),
fetchJson<DashboardTask[]>('/api/tasks'),
]);
return { overview, snapshots, tasks };
}
export async function fetchRoomTimeline(
roomJid: string,
): Promise<DashboardRoomActivity> {
return fetchJson<DashboardRoomActivity>(
`/api/rooms/${encodeURIComponent(roomJid)}/timeline`,
);
}
export async function fetchRoomsTimelineBatch(): Promise<
Record<string, DashboardRoomActivity>
> {
return fetchJson<Record<string, DashboardRoomActivity>>(
'/api/rooms-timeline',
);
}
export async function runScheduledTaskAction(
taskId: string,
action: DashboardTaskAction,
): Promise<{
ok: true;
id?: string;
deleted?: boolean;
task?: DashboardTask | null;
}> {
return postJson(`/api/tasks/${encodeURIComponent(taskId)}/actions`, {
action,
});
}
export async function createScheduledTask(
input: CreateScheduledTaskInput,
): Promise<{ ok: true; task: DashboardTask | null }> {
return postJson('/api/tasks', input);
}
export async function updateScheduledTask(
taskId: string,
input: UpdateScheduledTaskInput,
): Promise<{ ok: true; task: DashboardTask | null }> {
const response = await fetch(`/api/tasks/${encodeURIComponent(taskId)}`, {
method: 'PATCH',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let message = `/api/tasks/${taskId} failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) message = payload.error;
} catch {
// Keep the status-based message when the server body is not JSON.
}
throw new Error(message);
}
return (await response.json()) as { ok: true; task: DashboardTask | null };
}
export async function runInboxAction(
inboxId: string,
action: DashboardInboxAction,
options: { requestId?: string; lastOccurredAt?: string } = {},
): Promise<{
ok: true;
id: string;
taskId?: string;
intentKind?: string;
status?: string;
queued?: boolean;
dismissed?: boolean;
duplicate?: boolean;
}> {
return postJson(`/api/inbox/${encodeURIComponent(inboxId)}/actions`, {
action,
lastOccurredAt: options.lastOccurredAt,
requestId: options.requestId,
});
}
export async function runServiceAction(
serviceId: 'stack',
action: DashboardServiceAction,
options: { requestId?: string } = {},
): Promise<{
ok: true;
duplicate?: boolean;
restart: DashboardServiceRestart;
}> {
return postJson(`/api/services/${encodeURIComponent(serviceId)}/actions`, {
action,
requestId: options.requestId,
});
}
export async function sendRoomMessage(
roomJid: string,
text: string,
requestId: string,
nickname?: string | null,
): Promise<{ ok: true; id: string; queued: boolean }> {
return postJson(`/api/rooms/${encodeURIComponent(roomJid)}/messages`, {
requestId,
text,
nickname: nickname ?? undefined,
});
}
export interface ClaudeAccountSummary {
index: number;
expiresAt: number | null;
scopes: string[];
subscriptionType?: string;
rateLimitTier?: string;
exists: boolean;
}
export interface CodexAccountSummary {
index: number;
accountId: string | null;
email: string | null;
planType: string | null;
subscriptionUntil: string | null;
subscriptionLastChecked: string | null;
exists: boolean;
}
export interface ModelRoleConfig {
model: string;
effort: string;
}
export interface ModelConfigSnapshot {
owner: ModelRoleConfig;
reviewer: ModelRoleConfig;
arbiter: ModelRoleConfig;
}
export interface FastModeSnapshot {
codex: boolean;
claude: boolean;
}
export interface CodexFeatureSnapshot {
goals: boolean;
}
export interface RuntimePathSnapshot {
label: string;
path: string;
exists: boolean;
}
export interface RuntimeSkillSummary {
name: string;
description: string | null;
path: string;
}
export interface RuntimeSkillDirSnapshot extends RuntimePathSnapshot {
count: number;
skills: RuntimeSkillSummary[];
}
export interface RuntimeMcpSnapshot {
configPath: RuntimePathSnapshot;
ejclawConfigured: boolean;
serverCount: number;
}
export interface RuntimeAgentInventory {
configFiles: RuntimePathSnapshot[];
skillDirs: RuntimeSkillDirSnapshot[];
mcp: RuntimeMcpSnapshot;
}
export interface RuntimeInventorySnapshot {
generatedAt: string;
projectRoot: string;
dataDir: string;
service: {
id: string;
sessionScope: string;
agentType: string;
};
codex: RuntimeAgentInventory;
claude: RuntimeAgentInventory;
ejclaw: {
runnerSkillDir: RuntimeSkillDirSnapshot;
mcpServer: RuntimePathSnapshot;
};
}
export type RoomSkillScope = 'codex-user' | 'claude-user' | 'runner';
export interface RoomSkillCatalogItem {
id: string;
scope: RoomSkillScope;
name: string;
displayName: string;
description: string | null;
path: string;
agentTypes: Array<'claude-code' | 'codex'>;
}
export interface RoomSkillAgentPolicy {
agentType: 'claude-code' | 'codex';
mode: 'all-enabled' | 'custom';
availableSkillIds: string[];
disabledSkillIds: string[];
explicitEnabledSkillIds: string[];
effectiveEnabledSkillIds: string[];
}
export interface RoomSkillPolicyRoom {
jid: string;
name: string;
folder: string;
roomMode?: 'single' | 'tribunal';
agents: RoomSkillAgentPolicy[];
}
export interface RoomSkillSettingsSnapshot {
generatedAt: string;
catalog: RoomSkillCatalogItem[];
rooms: RoomSkillPolicyRoom[];
}
export interface RoomSkillSettingUpdateInput {
roomJid: string;
agentType: 'claude-code' | 'codex';
skillId: string;
enabled: boolean;
}
export interface MoaReferenceStatus {
model: string;
checkedAt: string;
ok: boolean;
error: string | null;
responseLength?: number;
}
export interface MoaModelSettingsSnapshot {
name: string;
enabled: boolean;
model: string;
baseUrl: string;
apiFormat: 'openai' | 'anthropic';
apiKeyConfigured: boolean;
lastStatus: MoaReferenceStatus | null;
}
export interface MoaSettingsSnapshot {
enabled: boolean;
referenceModels: string[];
models: MoaModelSettingsSnapshot[];
}
export async function fetchAccounts(): Promise<{
claude: ClaudeAccountSummary[];
codex: CodexAccountSummary[];
codexCurrentIndex?: number;
}> {
return fetchJson('/api/settings/accounts');
}
export async function fetchModelConfig(): Promise<ModelConfigSnapshot> {
return fetchJson('/api/settings/models');
}
export async function updateModels(
input: Partial<{
owner: Partial<ModelRoleConfig>;
reviewer: Partial<ModelRoleConfig>;
arbiter: Partial<ModelRoleConfig>;
}>,
): Promise<ModelConfigSnapshot> {
const response = await fetch('/api/settings/models', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update models failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as ModelConfigSnapshot;
}
export async function refreshCodexAccount(
index: number,
): Promise<CodexAccountSummary> {
const response = await fetch(
`/api/settings/accounts/codex/${index}/refresh`,
{ method: 'POST' },
);
if (!response.ok) {
let msg = `refresh failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
const json = (await response.json()) as { account: CodexAccountSummary };
return json.account;
}
export async function refreshAllCodexAccounts(): Promise<{
refreshed: number[];
failed: Array<{ index: number; error: string }>;
}> {
const response = await fetch('/api/settings/accounts/codex/refresh-all', {
method: 'POST',
});
if (!response.ok) {
throw new Error(`refresh-all failed: ${response.status}`);
}
return (await response.json()) as {
refreshed: number[];
failed: Array<{ index: number; error: string }>;
};
}
export async function setCurrentCodexAccount(
index: number,
): Promise<{ codexCurrentIndex: number }> {
const response = await fetch('/api/settings/accounts/codex/current', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ index }),
});
if (!response.ok) {
let msg = `switch failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as { codexCurrentIndex: number };
}
export async function fetchFastMode(): Promise<FastModeSnapshot> {
return fetchJson('/api/settings/fast-mode');
}
export async function updateFastMode(
input: Partial<FastModeSnapshot>,
): Promise<FastModeSnapshot> {
const response = await fetch('/api/settings/fast-mode', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update fast mode failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as FastModeSnapshot;
}
export async function fetchCodexFeatures(): Promise<CodexFeatureSnapshot> {
return fetchJson('/api/settings/codex-features');
}
export async function fetchRuntimeInventory(): Promise<RuntimeInventorySnapshot> {
return fetchJson('/api/settings/runtime-inventory');
}
export async function fetchRoomSkillSettings(): Promise<RoomSkillSettingsSnapshot> {
return fetchJson('/api/settings/room-skills');
}
export async function updateRoomSkillSetting(
input: RoomSkillSettingUpdateInput,
): Promise<RoomSkillSettingsSnapshot> {
const response = await fetch('/api/settings/room-skills', {
method: 'PATCH',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update room skill failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as RoomSkillSettingsSnapshot;
}
export async function updateCodexFeatures(
input: Partial<CodexFeatureSnapshot>,
): Promise<CodexFeatureSnapshot> {
const response = await fetch('/api/settings/codex-features', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update codex features failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as CodexFeatureSnapshot;
}
export async function fetchMoaSettings(): Promise<MoaSettingsSnapshot> {
return fetchJson('/api/settings/moa');
}
export async function updateMoaSettings(input: {
enabled?: boolean;
models?: Array<{
name: string;
enabled?: boolean;
model?: string;
baseUrl?: string;
apiFormat?: 'openai' | 'anthropic';
apiKey?: string;
}>;
}): Promise<MoaSettingsSnapshot> {
const response = await fetch('/api/settings/moa', {
method: 'PUT',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify(input),
});
if (!response.ok) {
let msg = `update MoA settings failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as MoaSettingsSnapshot;
}
export async function checkMoaModel(name: string): Promise<MoaReferenceStatus> {
const response = await fetch('/api/settings/moa/check', {
method: 'POST',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
body: JSON.stringify({ name }),
});
if (!response.ok) {
let msg = `check MoA model failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
const payload = (await response.json()) as {
status: MoaReferenceStatus;
};
return payload.status;
}
export async function deleteAccount(
provider: 'claude' | 'codex',
index: number,
): Promise<{ ok: true; provider: string; index: number }> {
const response = await fetch(`/api/settings/accounts/${provider}/${index}`, {
method: 'DELETE',
});
if (!response.ok) {
let msg = `delete account failed: ${response.status}`;
try {
const payload = (await response.json()) as { error?: string };
if (payload.error) msg = payload.error;
} catch {
/* ignore */
}
throw new Error(msg);
}
return (await response.json()) as {
ok: true;
provider: string;
index: number;
};
}
export async function addClaudeAccount(
token: string,
): Promise<{ ok: true; index: number; accountId: string | null }> {
return postJson('/api/settings/accounts/claude', { token });
}