Add read-only runtime inventory settings (#130)
* 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
This commit is contained in:
130
apps/dashboard/src/RuntimeInventorySettings.css
Normal file
130
apps/dashboard/src/RuntimeInventorySettings.css
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
.runtime-inventory {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-card,
|
||||||
|
.runtime-agent-card,
|
||||||
|
.runtime-skill-card {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: rgba(15, 23, 42, 0.48);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-card > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-card strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-card code,
|
||||||
|
.runtime-path-row code,
|
||||||
|
.runtime-skill-card code {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-agent-card {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-card-head,
|
||||||
|
.runtime-skill-card header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-card-head h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-path-list,
|
||||||
|
.runtime-skill-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 14px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-path-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.38);
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-path-row > span {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-skill-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-skill-card {
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-skill-card header > div {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-skill-list li {
|
||||||
|
display: grid;
|
||||||
|
gap: 3px;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.12);
|
||||||
|
padding-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-skill-list li:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-skill-list span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.runtime-summary-card,
|
||||||
|
.runtime-skill-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.runtime-card-head,
|
||||||
|
.runtime-skill-card header,
|
||||||
|
.runtime-path-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
apps/dashboard/src/RuntimeInventorySettings.tsx
Normal file
174
apps/dashboard/src/RuntimeInventorySettings.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchRuntimeInventory,
|
||||||
|
type RuntimeAgentInventory,
|
||||||
|
type RuntimeInventorySnapshot,
|
||||||
|
type RuntimePathSnapshot,
|
||||||
|
type RuntimeSkillDirSnapshot,
|
||||||
|
} from './api';
|
||||||
|
import { SettingsSectionHeading } from './SettingsPanelChrome';
|
||||||
|
import './RuntimeInventorySettings.css';
|
||||||
|
|
||||||
|
function ExistsBadge({ exists }: { exists: boolean }) {
|
||||||
|
return (
|
||||||
|
<span className={`settings-account-badge ${exists ? 'is-active' : ''}`}>
|
||||||
|
{exists ? '감지됨' : '없음'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PathRow({ item }: { item: RuntimePathSnapshot }) {
|
||||||
|
return (
|
||||||
|
<li className="runtime-path-row">
|
||||||
|
<span>
|
||||||
|
<strong>{item.label}</strong>
|
||||||
|
<code>{item.path}</code>
|
||||||
|
</span>
|
||||||
|
<ExistsBadge exists={item.exists} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkillDirCard({ dir }: { dir: RuntimeSkillDirSnapshot }) {
|
||||||
|
const preview = dir.skills.slice(0, 6);
|
||||||
|
return (
|
||||||
|
<article className="runtime-skill-card">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<strong>{dir.label}</strong>
|
||||||
|
<code>{dir.path}</code>
|
||||||
|
</div>
|
||||||
|
<span className="settings-account-badge is-active">
|
||||||
|
{dir.count} skills
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
{preview.length === 0 ? (
|
||||||
|
<p className="settings-hint">표시할 SKILL.md 없음</p>
|
||||||
|
) : (
|
||||||
|
<ul className="runtime-skill-list">
|
||||||
|
{preview.map((skill) => (
|
||||||
|
<li key={skill.path}>
|
||||||
|
<strong>{skill.name}</strong>
|
||||||
|
{skill.description ? <span>{skill.description}</span> : null}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{dir.count > preview.length ? (
|
||||||
|
<small className="settings-hint">
|
||||||
|
외 {dir.count - preview.length}개 더 있음
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgentInventoryCard({
|
||||||
|
title,
|
||||||
|
inventory,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
inventory: RuntimeAgentInventory;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<article className="runtime-agent-card">
|
||||||
|
<header className="runtime-card-head">
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<span className="settings-account-badge is-active">
|
||||||
|
MCP {inventory.mcp.ejclawConfigured ? '연결' : '미감지'}
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<ul className="runtime-path-list">
|
||||||
|
{inventory.configFiles.map((item) => (
|
||||||
|
<PathRow item={item} key={item.path} />
|
||||||
|
))}
|
||||||
|
<PathRow item={inventory.mcp.configPath} />
|
||||||
|
</ul>
|
||||||
|
<p className="settings-hint">
|
||||||
|
MCP servers {inventory.mcp.serverCount}개 · EJClaw section{' '}
|
||||||
|
{inventory.mcp.ejclawConfigured ? '있음' : '없음'}
|
||||||
|
</p>
|
||||||
|
<div className="runtime-skill-grid">
|
||||||
|
{inventory.skillDirs.map((dir) => (
|
||||||
|
<SkillDirCard dir={dir} key={dir.path} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RuntimeInventorySettings() {
|
||||||
|
const [snapshot, setSnapshot] = useState<RuntimeInventorySnapshot | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
fetchRuntimeInventory()
|
||||||
|
.then((value) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setSnapshot(value);
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (cancelled) return;
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-labelledby="settings-runtime-tab"
|
||||||
|
className="settings-section"
|
||||||
|
id="settings-runtime"
|
||||||
|
role="tabpanel"
|
||||||
|
>
|
||||||
|
<SettingsSectionHeading
|
||||||
|
detail="Runtime inventory"
|
||||||
|
title="런타임"
|
||||||
|
description="Codex/Claude Code 설정, 스킬, MCP 연결 상태를 읽기 전용으로 확인합니다."
|
||||||
|
/>
|
||||||
|
{error ? <p className="settings-error">{error}</p> : null}
|
||||||
|
{!snapshot ? (
|
||||||
|
<p className="settings-hint">런타임 정보를 불러오는 중…</p>
|
||||||
|
) : (
|
||||||
|
<div className="runtime-inventory">
|
||||||
|
<section className="runtime-summary-card">
|
||||||
|
<div>
|
||||||
|
<span className="settings-kicker">Current service</span>
|
||||||
|
<strong>{snapshot.service.id}</strong>
|
||||||
|
<small>
|
||||||
|
{snapshot.service.agentType} · session{' '}
|
||||||
|
{snapshot.service.sessionScope}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="settings-kicker">Project</span>
|
||||||
|
<code>{snapshot.projectRoot}</code>
|
||||||
|
<small>data {snapshot.dataDir}</small>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<AgentInventoryCard title="Codex" inventory={snapshot.codex} />
|
||||||
|
<AgentInventoryCard title="Claude Code" inventory={snapshot.claude} />
|
||||||
|
|
||||||
|
<article className="runtime-agent-card">
|
||||||
|
<header className="runtime-card-head">
|
||||||
|
<h4>EJClaw bridge</h4>
|
||||||
|
<ExistsBadge exists={snapshot.ejclaw.mcpServer.exists} />
|
||||||
|
</header>
|
||||||
|
<ul className="runtime-path-list">
|
||||||
|
<PathRow item={snapshot.ejclaw.mcpServer} />
|
||||||
|
<PathRow item={snapshot.ejclaw.runnerSkillDir} />
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -31,21 +31,24 @@ describe('SettingsPanel', () => {
|
|||||||
expect(html).toContain('English');
|
expect(html).toContain('English');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders model, MoA, fast mode, and account controls', () => {
|
it('renders model, runtime, MoA, fast mode, and account controls', () => {
|
||||||
const html = renderToStaticMarkup(createElement(SettingsPanel, baseProps));
|
const html = renderToStaticMarkup(createElement(SettingsPanel, baseProps));
|
||||||
|
|
||||||
expect(html).toContain('role="tablist"');
|
expect(html).toContain('role="tablist"');
|
||||||
expect(html).toContain('role="tabpanel"');
|
expect(html).toContain('role="tabpanel"');
|
||||||
expect(html).toContain('aria-selected="true"');
|
expect(html).toContain('aria-selected="true"');
|
||||||
expect(html).toContain('data-settings-target="settings-models"');
|
expect(html).toContain('data-settings-target="settings-models"');
|
||||||
|
expect(html).toContain('data-settings-target="settings-runtime"');
|
||||||
expect(html).toContain('data-settings-target="settings-moa"');
|
expect(html).toContain('data-settings-target="settings-moa"');
|
||||||
expect(html).toContain('data-settings-target="settings-codex"');
|
expect(html).toContain('data-settings-target="settings-codex"');
|
||||||
expect(html).toContain('data-settings-target="settings-accounts"');
|
expect(html).toContain('data-settings-target="settings-accounts"');
|
||||||
|
expect(html).toContain('aria-controls="settings-runtime"');
|
||||||
expect(html).toContain('aria-controls="settings-codex"');
|
expect(html).toContain('aria-controls="settings-codex"');
|
||||||
expect(html).not.toContain('href="#settings-codex"');
|
expect(html).not.toContain('href="#settings-codex"');
|
||||||
expect(html).toContain('settings-apply-card');
|
expect(html).toContain('settings-apply-card');
|
||||||
expect(html).not.toContain('settings-apply-bar');
|
expect(html).not.toContain('settings-apply-bar');
|
||||||
expect(html).toContain('저장 후 재시작');
|
expect(html).toContain('저장 후 재시작');
|
||||||
|
expect(html).toContain('런타임');
|
||||||
expect(html).toContain('Claude');
|
expect(html).toContain('Claude');
|
||||||
expect(html).toContain('계정');
|
expect(html).toContain('계정');
|
||||||
expect(html).toContain('스택 재시작');
|
expect(html).toContain('스택 재시작');
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from './api';
|
} from './api';
|
||||||
import { type Locale, type Messages } from './i18n';
|
import { type Locale, type Messages } from './i18n';
|
||||||
import { MoaSettingsPanel } from './MoaSettingsPanel';
|
import { MoaSettingsPanel } from './MoaSettingsPanel';
|
||||||
|
import { RuntimeInventorySettings } from './RuntimeInventorySettings';
|
||||||
import {
|
import {
|
||||||
GeneralSettings,
|
GeneralSettings,
|
||||||
SettingsApplyCard,
|
SettingsApplyCard,
|
||||||
@@ -83,6 +84,10 @@ export function SettingsPanel({
|
|||||||
<ModelSettings />
|
<ModelSettings />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div hidden={activeSection !== 'settings-runtime'}>
|
||||||
|
<RuntimeInventorySettings />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div hidden={activeSection !== 'settings-moa'}>
|
<div hidden={activeSection !== 'settings-moa'}>
|
||||||
<MoaSettingsPanel />
|
<MoaSettingsPanel />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export const SETTINGS_NAV_ITEMS = [
|
|||||||
title: '모델',
|
title: '모델',
|
||||||
detail: 'owner · reviewer · arbiter',
|
detail: 'owner · reviewer · arbiter',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
targetId: 'settings-runtime',
|
||||||
|
title: '런타임',
|
||||||
|
detail: 'skills · MCP · config',
|
||||||
|
},
|
||||||
{ targetId: 'settings-moa', title: 'MoA', detail: '참조 모델 · 연결 테스트' },
|
{ targetId: 'settings-moa', title: 'MoA', detail: '참조 모델 · 연결 테스트' },
|
||||||
{ targetId: 'settings-codex', title: 'Codex', detail: 'fast mode · /goal' },
|
{ targetId: 'settings-codex', title: 'Codex', detail: 'fast mode · /goal' },
|
||||||
{ targetId: 'settings-accounts', title: '계정', detail: 'Claude · Codex' },
|
{ targetId: 'settings-accounts', title: '계정', detail: 'Claude · Codex' },
|
||||||
|
|||||||
@@ -392,6 +392,52 @@ export interface CodexFeatureSnapshot {
|
|||||||
goals: boolean;
|
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 interface MoaReferenceStatus {
|
export interface MoaReferenceStatus {
|
||||||
model: string;
|
model: string;
|
||||||
checkedAt: string;
|
checkedAt: string;
|
||||||
@@ -546,6 +592,10 @@ export async function fetchCodexFeatures(): Promise<CodexFeatureSnapshot> {
|
|||||||
return fetchJson('/api/settings/codex-features');
|
return fetchJson('/api/settings/codex-features');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchRuntimeInventory(): Promise<RuntimeInventorySnapshot> {
|
||||||
|
return fetchJson('/api/settings/runtime-inventory');
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateCodexFeatures(
|
export async function updateCodexFeatures(
|
||||||
input: Partial<CodexFeatureSnapshot>,
|
input: Partial<CodexFeatureSnapshot>,
|
||||||
): Promise<CodexFeatureSnapshot> {
|
): Promise<CodexFeatureSnapshot> {
|
||||||
|
|||||||
@@ -47,6 +47,12 @@ async function main() {
|
|||||||
await assertVisible(page.locator('.settings-panel'));
|
await assertVisible(page.locator('.settings-panel'));
|
||||||
await assertVisible(page.locator('#settings-codex'));
|
await assertVisible(page.locator('#settings-codex'));
|
||||||
|
|
||||||
|
await openSettingsSection(page, 'settings-runtime');
|
||||||
|
assert.equal(page.url(), originalUrl);
|
||||||
|
await assertVisible(page.locator('#settings-runtime'));
|
||||||
|
await page.getByText('Runtime inventory').waitFor();
|
||||||
|
await page.getByText('EJClaw bridge').waitFor();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator(
|
.locator(
|
||||||
'.settings-nav button[data-settings-target="settings-accounts"]',
|
'.settings-nav button[data-settings-target="settings-accounts"]',
|
||||||
@@ -308,6 +314,11 @@ async function handleMockApi(route: Route, state: MockApiState) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (method === 'GET' && url.pathname === '/api/settings/runtime-inventory') {
|
||||||
|
await fulfillJson(route, runtimeInventoryFixture());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname === '/api/settings/codex-features') {
|
if (url.pathname === '/api/settings/codex-features') {
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
await fulfillJson(route, state.codexFeatures);
|
await fulfillJson(route, state.codexFeatures);
|
||||||
@@ -475,6 +486,103 @@ function mockStatusSnapshot() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runtimeSkill(name: string, description: string, skillPath: string) {
|
||||||
|
return { name, description, path: skillPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeInventoryFixture() {
|
||||||
|
const codexConfig = {
|
||||||
|
label: 'Codex config.toml',
|
||||||
|
path: '/home/.codex/config.toml',
|
||||||
|
exists: true,
|
||||||
|
};
|
||||||
|
const claudeSettings = {
|
||||||
|
label: 'Claude settings.json',
|
||||||
|
path: '/home/.claude/settings.json',
|
||||||
|
exists: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
projectRoot: '/repo',
|
||||||
|
dataDir: '/repo/data',
|
||||||
|
service: {
|
||||||
|
id: 'codex-main',
|
||||||
|
sessionScope: 'codex-main',
|
||||||
|
agentType: 'codex',
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
configFiles: [
|
||||||
|
codexConfig,
|
||||||
|
{
|
||||||
|
label: 'Codex auth.json',
|
||||||
|
path: '/home/.codex/auth.json',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skillDirs: [
|
||||||
|
{
|
||||||
|
label: 'Codex user skills',
|
||||||
|
path: '/home/.agents/skills',
|
||||||
|
exists: true,
|
||||||
|
count: 1,
|
||||||
|
skills: [
|
||||||
|
runtimeSkill(
|
||||||
|
'agent-browser',
|
||||||
|
'Browser automation',
|
||||||
|
'/home/.agents/skills/agent-browser',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mcp: { configPath: codexConfig, ejclawConfigured: true, serverCount: 1 },
|
||||||
|
},
|
||||||
|
claude: {
|
||||||
|
configFiles: [claudeSettings],
|
||||||
|
skillDirs: [
|
||||||
|
{
|
||||||
|
label: 'Claude user skills',
|
||||||
|
path: '/home/.claude/skills',
|
||||||
|
exists: true,
|
||||||
|
count: 1,
|
||||||
|
skills: [
|
||||||
|
runtimeSkill(
|
||||||
|
'review-helper',
|
||||||
|
'Review workflow',
|
||||||
|
'/home/.claude/skills/review-helper',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mcp: {
|
||||||
|
configPath: claudeSettings,
|
||||||
|
ejclawConfigured: false,
|
||||||
|
serverCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ejclaw: {
|
||||||
|
runnerSkillDir: {
|
||||||
|
label: 'EJClaw runner skills',
|
||||||
|
path: '/repo/runners/skills',
|
||||||
|
exists: true,
|
||||||
|
count: 1,
|
||||||
|
skills: [
|
||||||
|
runtimeSkill(
|
||||||
|
'agent-browser',
|
||||||
|
'Browser automation',
|
||||||
|
'/repo/runners/skills/agent-browser',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mcpServer: {
|
||||||
|
label: 'EJClaw IPC MCP server',
|
||||||
|
path: '/repo/runners/agent-runner/dist/ipc-mcp-stdio.js',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function parseJsonBody(body: string | null): Record<string, unknown> {
|
function parseJsonBody(body: string | null): Record<string, unknown> {
|
||||||
if (!body) return {};
|
if (!body) return {};
|
||||||
const parsed = JSON.parse(body) as unknown;
|
const parsed = JSON.parse(body) as unknown;
|
||||||
|
|||||||
91
src/runtime-inventory.test.ts
Normal file
91
src/runtime-inventory.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { getRuntimeInventory } from './runtime-inventory.js';
|
||||||
|
|
||||||
|
describe('runtime inventory', () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let homeDir: string;
|
||||||
|
let projectRoot: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-runtime-inv-'));
|
||||||
|
homeDir = path.join(tempDir, 'home');
|
||||||
|
projectRoot = path.join(tempDir, 'repo');
|
||||||
|
fs.mkdirSync(homeDir, { recursive: true });
|
||||||
|
fs.mkdirSync(projectRoot, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('summarizes safe Codex and Claude runtime metadata without reading secrets', () => {
|
||||||
|
const codexDir = path.join(homeDir, '.codex');
|
||||||
|
const claudeDir = path.join(homeDir, '.claude');
|
||||||
|
fs.mkdirSync(codexDir, { recursive: true });
|
||||||
|
fs.mkdirSync(claudeDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(codexDir, 'config.toml'),
|
||||||
|
'[mcp_servers.ejclaw]\ncommand = "node"\n[mcp_servers.other]\ncommand = "x"\n',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(claudeDir, 'settings.json'),
|
||||||
|
'{"fastMode":true,"secret":"should-not-leak"}\n',
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(codexDir, 'auth.json'),
|
||||||
|
'{"OPENAI_API_KEY":"sk-secret"}\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
const codexSkill = path.join(homeDir, '.agents', 'skills', 'browser');
|
||||||
|
fs.mkdirSync(codexSkill, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(codexSkill, 'SKILL.md'),
|
||||||
|
'---\nname: browser\ndescription: Browser automation\n---\n# Body\n',
|
||||||
|
);
|
||||||
|
|
||||||
|
const runnerSkill = path.join(projectRoot, 'runners', 'skills', 'runner');
|
||||||
|
fs.mkdirSync(runnerSkill, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(runnerSkill, 'SKILL.md'),
|
||||||
|
'---\nname: runner\ndescription: Runner skill\n---\n',
|
||||||
|
);
|
||||||
|
const mcpServerPath = path.join(
|
||||||
|
projectRoot,
|
||||||
|
'runners',
|
||||||
|
'agent-runner',
|
||||||
|
'dist',
|
||||||
|
'ipc-mcp-stdio.js',
|
||||||
|
);
|
||||||
|
fs.mkdirSync(path.dirname(mcpServerPath), { recursive: true });
|
||||||
|
fs.writeFileSync(mcpServerPath, 'console.log("mcp");\n');
|
||||||
|
|
||||||
|
const snapshot = getRuntimeInventory({
|
||||||
|
generatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
homeDir,
|
||||||
|
projectRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(snapshot.projectRoot).toBe(projectRoot);
|
||||||
|
expect(snapshot.codex.mcp).toMatchObject({
|
||||||
|
ejclawConfigured: true,
|
||||||
|
serverCount: 2,
|
||||||
|
});
|
||||||
|
expect(snapshot.codex.skillDirs[0]).toMatchObject({
|
||||||
|
count: 1,
|
||||||
|
skills: [{ name: 'browser', description: 'Browser automation' }],
|
||||||
|
});
|
||||||
|
expect(snapshot.ejclaw.runnerSkillDir).toMatchObject({
|
||||||
|
count: 1,
|
||||||
|
skills: [{ name: 'runner', description: 'Runner skill' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialized = JSON.stringify(snapshot);
|
||||||
|
expect(serialized).not.toContain('sk-secret');
|
||||||
|
expect(serialized).not.toContain('should-not-leak');
|
||||||
|
});
|
||||||
|
});
|
||||||
212
src/runtime-inventory.ts
Normal file
212
src/runtime-inventory.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CURRENT_RUNTIME_AGENT_TYPE,
|
||||||
|
DATA_DIR,
|
||||||
|
SERVICE_ID,
|
||||||
|
SERVICE_SESSION_SCOPE,
|
||||||
|
} from './config.js';
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuntimeInventoryOptions {
|
||||||
|
homeDir?: string;
|
||||||
|
projectRoot?: string;
|
||||||
|
generatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathSnapshot(label: string, filePath: string): RuntimePathSnapshot {
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
path: filePath,
|
||||||
|
exists: fs.existsSync(filePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSkillMetadata(skillMdPath: string): {
|
||||||
|
name: string | null;
|
||||||
|
description: string | null;
|
||||||
|
} {
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(skillMdPath, 'utf-8').slice(0, 8000);
|
||||||
|
} catch {
|
||||||
|
return { name: null, description: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = content.match(/^name:\s*(.+?)\s*$/m)?.[1]?.trim() ?? null;
|
||||||
|
const description =
|
||||||
|
content.match(/^description:\s*(.+?)\s*$/m)?.[1]?.trim() ?? null;
|
||||||
|
return { name, description };
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSkillDir(label: string, dirPath: string): RuntimeSkillDirSnapshot {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
return {
|
||||||
|
...pathSnapshot(label, dirPath),
|
||||||
|
count: 0,
|
||||||
|
skills: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const skills = fs
|
||||||
|
.readdirSync(dirPath, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => {
|
||||||
|
const skillDir = path.join(dirPath, entry.name);
|
||||||
|
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
||||||
|
if (!fs.existsSync(skillMdPath)) return null;
|
||||||
|
const metadata = parseSkillMetadata(skillMdPath);
|
||||||
|
return {
|
||||||
|
name: metadata.name || entry.name,
|
||||||
|
description: metadata.description,
|
||||||
|
path: skillDir,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((skill): skill is RuntimeSkillSummary => Boolean(skill))
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pathSnapshot(label, dirPath),
|
||||||
|
count: skills.length,
|
||||||
|
skills,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMcpSnapshot(
|
||||||
|
label: string,
|
||||||
|
configPath: string,
|
||||||
|
): RuntimeMcpSnapshot {
|
||||||
|
let content = '';
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
content = fs.readFileSync(configPath, 'utf-8');
|
||||||
|
} catch {
|
||||||
|
content = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverMatches = content.match(/^\s*\[mcp_servers\.[^\]]+\]/gm) ?? [];
|
||||||
|
return {
|
||||||
|
configPath: pathSnapshot(label, configPath),
|
||||||
|
ejclawConfigured: /^\s*\[mcp_servers\.ejclaw(?:\.[^\]]+)?\]/m.test(content),
|
||||||
|
serverCount: serverMatches.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRuntimeInventory(
|
||||||
|
options: RuntimeInventoryOptions = {},
|
||||||
|
): RuntimeInventorySnapshot {
|
||||||
|
const homeDir = options.homeDir ?? os.homedir();
|
||||||
|
const projectRoot = options.projectRoot ?? process.cwd();
|
||||||
|
const codexConfigPath = path.join(homeDir, '.codex', 'config.toml');
|
||||||
|
const claudeSettingsPath = path.join(homeDir, '.claude', 'settings.json');
|
||||||
|
const runnerSkillDir = path.join(projectRoot, 'runners', 'skills');
|
||||||
|
const mcpServerPath = path.join(
|
||||||
|
projectRoot,
|
||||||
|
'runners',
|
||||||
|
'agent-runner',
|
||||||
|
'dist',
|
||||||
|
'ipc-mcp-stdio.js',
|
||||||
|
);
|
||||||
|
|
||||||
|
const ejclawRunnerSkills = readSkillDir(
|
||||||
|
'EJClaw runner skills',
|
||||||
|
runnerSkillDir,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
generatedAt: options.generatedAt ?? new Date().toISOString(),
|
||||||
|
projectRoot,
|
||||||
|
dataDir: DATA_DIR,
|
||||||
|
service: {
|
||||||
|
id: SERVICE_ID,
|
||||||
|
sessionScope: SERVICE_SESSION_SCOPE,
|
||||||
|
agentType: CURRENT_RUNTIME_AGENT_TYPE,
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
configFiles: [
|
||||||
|
pathSnapshot('Codex config.toml', codexConfigPath),
|
||||||
|
pathSnapshot(
|
||||||
|
'Codex auth.json',
|
||||||
|
path.join(homeDir, '.codex', 'auth.json'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
skillDirs: [
|
||||||
|
readSkillDir(
|
||||||
|
'Codex user skills',
|
||||||
|
path.join(homeDir, '.agents', 'skills'),
|
||||||
|
),
|
||||||
|
ejclawRunnerSkills,
|
||||||
|
],
|
||||||
|
mcp: readMcpSnapshot('Codex config.toml', codexConfigPath),
|
||||||
|
},
|
||||||
|
claude: {
|
||||||
|
configFiles: [
|
||||||
|
pathSnapshot('Claude settings.json', claudeSettingsPath),
|
||||||
|
pathSnapshot(
|
||||||
|
'Claude credentials',
|
||||||
|
path.join(homeDir, '.claude', '.credentials.json'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
skillDirs: [
|
||||||
|
readSkillDir(
|
||||||
|
'Claude user skills',
|
||||||
|
path.join(homeDir, '.claude', 'skills'),
|
||||||
|
),
|
||||||
|
ejclawRunnerSkills,
|
||||||
|
],
|
||||||
|
mcp: readMcpSnapshot('Claude settings.json', claudeSettingsPath),
|
||||||
|
},
|
||||||
|
ejclaw: {
|
||||||
|
runnerSkillDir: ejclawRunnerSkills,
|
||||||
|
mcpServer: pathSnapshot('EJClaw IPC MCP server', mcpServerPath),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
FastModeSnapshot,
|
FastModeSnapshot,
|
||||||
ModelConfigSnapshot,
|
ModelConfigSnapshot,
|
||||||
} from './settings-store.js';
|
} from './settings-store.js';
|
||||||
|
import type { RuntimeInventorySnapshot } from './runtime-inventory.js';
|
||||||
import type { MoaSettingsSnapshot } from './settings-store-moa.js';
|
import type { MoaSettingsSnapshot } from './settings-store-moa.js';
|
||||||
|
|
||||||
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
function jsonResponse(value: unknown, init?: ResponseInit): Response {
|
||||||
@@ -44,6 +45,88 @@ const modelConfig: ModelConfigSnapshot = {
|
|||||||
|
|
||||||
const fastMode: FastModeSnapshot = { codex: true, claude: false };
|
const fastMode: FastModeSnapshot = { codex: true, claude: false };
|
||||||
const codexFeatures: CodexFeatureSnapshot = { goals: false };
|
const codexFeatures: CodexFeatureSnapshot = { goals: false };
|
||||||
|
const runtimeInventory: RuntimeInventorySnapshot = {
|
||||||
|
generatedAt: '2026-05-04T00:00:00.000Z',
|
||||||
|
projectRoot: '/repo',
|
||||||
|
dataDir: '/repo/data',
|
||||||
|
service: {
|
||||||
|
id: 'codex-main',
|
||||||
|
sessionScope: 'codex-main',
|
||||||
|
agentType: 'codex',
|
||||||
|
},
|
||||||
|
codex: {
|
||||||
|
configFiles: [
|
||||||
|
{
|
||||||
|
label: 'Codex config.toml',
|
||||||
|
path: '/home/.codex/config.toml',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skillDirs: [
|
||||||
|
{
|
||||||
|
label: 'Codex user skills',
|
||||||
|
path: '/home/.agents/skills',
|
||||||
|
exists: true,
|
||||||
|
count: 1,
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
name: 'agent-browser',
|
||||||
|
description: 'Browser automation',
|
||||||
|
path: '/home/.agents/skills/agent-browser',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mcp: {
|
||||||
|
configPath: {
|
||||||
|
label: 'Codex config.toml',
|
||||||
|
path: '/home/.codex/config.toml',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
ejclawConfigured: true,
|
||||||
|
serverCount: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
claude: {
|
||||||
|
configFiles: [
|
||||||
|
{
|
||||||
|
label: 'Claude settings.json',
|
||||||
|
path: '/home/.claude/settings.json',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
skillDirs: [],
|
||||||
|
mcp: {
|
||||||
|
configPath: {
|
||||||
|
label: 'Claude settings.json',
|
||||||
|
path: '/home/.claude/settings.json',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
ejclawConfigured: false,
|
||||||
|
serverCount: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ejclaw: {
|
||||||
|
runnerSkillDir: {
|
||||||
|
label: 'EJClaw runner skills',
|
||||||
|
path: '/repo/runners/skills',
|
||||||
|
exists: true,
|
||||||
|
count: 1,
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
name: 'agent-browser',
|
||||||
|
description: 'Browser automation',
|
||||||
|
path: '/repo/runners/skills/agent-browser',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
mcpServer: {
|
||||||
|
label: 'EJClaw IPC MCP server',
|
||||||
|
path: '/repo/runners/agent-runner/dist/ipc-mcp-stdio.js',
|
||||||
|
exists: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const moaSettings: MoaSettingsSnapshot = {
|
const moaSettings: MoaSettingsSnapshot = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -113,6 +196,7 @@ function makeDeps(
|
|||||||
getFastMode: () => fastMode,
|
getFastMode: () => fastMode,
|
||||||
getModelConfig: () => modelConfig,
|
getModelConfig: () => modelConfig,
|
||||||
getMoaSettings: () => moaSettings,
|
getMoaSettings: () => moaSettings,
|
||||||
|
getRuntimeInventory: () => runtimeInventory,
|
||||||
listClaudeAccounts: () => [makeClaudeAccount()],
|
listClaudeAccounts: () => [makeClaudeAccount()],
|
||||||
listCodexAccounts: () => [makeCodexAccount()],
|
listCodexAccounts: () => [makeCodexAccount()],
|
||||||
refreshAllCodexAccounts: async () => ({ refreshed: [1], failed: [] }),
|
refreshAllCodexAccounts: async () => ({ refreshed: [1], failed: [] }),
|
||||||
@@ -159,6 +243,14 @@ describe('web dashboard settings routes', () => {
|
|||||||
expect(mode?.status).toBe(200);
|
expect(mode?.status).toBe(200);
|
||||||
await expect(mode?.json()).resolves.toEqual(fastMode);
|
await expect(mode?.json()).resolves.toEqual(fastMode);
|
||||||
|
|
||||||
|
const inventory = await route('/api/settings/runtime-inventory');
|
||||||
|
expect(inventory?.status).toBe(200);
|
||||||
|
await expect(inventory?.json()).resolves.toMatchObject({
|
||||||
|
service: { id: 'codex-main', agentType: 'codex' },
|
||||||
|
codex: { mcp: { ejclawConfigured: true } },
|
||||||
|
ejclaw: { runnerSkillDir: { count: 1 } },
|
||||||
|
});
|
||||||
|
|
||||||
const features = await route('/api/settings/codex-features');
|
const features = await route('/api/settings/codex-features');
|
||||||
expect(features?.status).toBe(200);
|
expect(features?.status).toBe(200);
|
||||||
await expect(features?.json()).resolves.toEqual(codexFeatures);
|
await expect(features?.json()).resolves.toEqual(codexFeatures);
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
type FastModeSnapshot,
|
type FastModeSnapshot,
|
||||||
type ModelConfigSnapshot,
|
type ModelConfigSnapshot,
|
||||||
} from './settings-store.js';
|
} from './settings-store.js';
|
||||||
|
import {
|
||||||
|
getRuntimeInventory,
|
||||||
|
type RuntimeInventorySnapshot,
|
||||||
|
} from './runtime-inventory.js';
|
||||||
import {
|
import {
|
||||||
checkMoaModel,
|
checkMoaModel,
|
||||||
getMoaSettings,
|
getMoaSettings,
|
||||||
@@ -40,6 +44,7 @@ export interface SettingsRouteDependencies {
|
|||||||
getFastMode: typeof getFastMode;
|
getFastMode: typeof getFastMode;
|
||||||
getModelConfig: typeof getModelConfig;
|
getModelConfig: typeof getModelConfig;
|
||||||
getMoaSettings: typeof getMoaSettings;
|
getMoaSettings: typeof getMoaSettings;
|
||||||
|
getRuntimeInventory: typeof getRuntimeInventory;
|
||||||
listClaudeAccounts: typeof listClaudeAccounts;
|
listClaudeAccounts: typeof listClaudeAccounts;
|
||||||
listCodexAccounts: typeof listCodexAccounts;
|
listCodexAccounts: typeof listCodexAccounts;
|
||||||
refreshAllCodexAccounts: typeof refreshAllCodexAccounts;
|
refreshAllCodexAccounts: typeof refreshAllCodexAccounts;
|
||||||
@@ -67,6 +72,7 @@ const defaultSettingsRouteDependencies: SettingsRouteDependencies = {
|
|||||||
getFastMode,
|
getFastMode,
|
||||||
getModelConfig,
|
getModelConfig,
|
||||||
getMoaSettings,
|
getMoaSettings,
|
||||||
|
getRuntimeInventory,
|
||||||
listClaudeAccounts,
|
listClaudeAccounts,
|
||||||
listCodexAccounts,
|
listCodexAccounts,
|
||||||
refreshAllCodexAccounts,
|
refreshAllCodexAccounts,
|
||||||
@@ -207,6 +213,15 @@ function handleAccountsRoute(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRuntimeInventoryRoute(
|
||||||
|
request: Request,
|
||||||
|
jsonResponse: JsonResponse,
|
||||||
|
deps: SettingsRouteDependencies,
|
||||||
|
): Response | null {
|
||||||
|
if (!readMethod(request.method)) return null;
|
||||||
|
return jsonResponse(deps.getRuntimeInventory());
|
||||||
|
}
|
||||||
|
|
||||||
async function handleClaudeAccountAddRoute(
|
async function handleClaudeAccountAddRoute(
|
||||||
request: Request,
|
request: Request,
|
||||||
jsonResponse: JsonResponse,
|
jsonResponse: JsonResponse,
|
||||||
@@ -320,6 +335,9 @@ export async function handleSettingsRoute({
|
|||||||
if (url.pathname === '/api/settings/models') {
|
if (url.pathname === '/api/settings/models') {
|
||||||
return handleModelSettingsRoute(request, jsonResponse, deps);
|
return handleModelSettingsRoute(request, jsonResponse, deps);
|
||||||
}
|
}
|
||||||
|
if (url.pathname === '/api/settings/runtime-inventory') {
|
||||||
|
return handleRuntimeInventoryRoute(request, jsonResponse, deps);
|
||||||
|
}
|
||||||
if (url.pathname === '/api/settings/fast-mode') {
|
if (url.pathname === '/api/settings/fast-mode') {
|
||||||
return handleFastModeRoute(request, jsonResponse, deps);
|
return handleFastModeRoute(request, jsonResponse, deps);
|
||||||
}
|
}
|
||||||
@@ -366,4 +384,5 @@ export type {
|
|||||||
FastModeSnapshot,
|
FastModeSnapshot,
|
||||||
ModelConfigSnapshot,
|
ModelConfigSnapshot,
|
||||||
MoaSettingsSnapshot,
|
MoaSettingsSnapshot,
|
||||||
|
RuntimeInventorySnapshot,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user