* 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
257 lines
6.2 KiB
TypeScript
257 lines
6.2 KiB
TypeScript
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;
|
|
}
|
|
|
|
interface McpConfigSummary {
|
|
ejclawConfigured: boolean;
|
|
serverCount: number;
|
|
}
|
|
|
|
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 isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function summarizeTomlMcpConfig(content: string): McpConfigSummary {
|
|
const serverMatches = content.match(/^\s*\[mcp_servers\.[^\]]+\]/gm) ?? [];
|
|
return {
|
|
ejclawConfigured: /^\s*\[mcp_servers\.ejclaw(?:\.[^\]]+)?\]/m.test(content),
|
|
serverCount: serverMatches.length,
|
|
};
|
|
}
|
|
|
|
function summarizeJsonMcpConfig(content: string): McpConfigSummary | null {
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(content);
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (!isRecord(parsed)) return null;
|
|
|
|
const servers = parsed.mcpServers ?? parsed.mcp_servers;
|
|
if (!isRecord(servers)) {
|
|
return {
|
|
ejclawConfigured: false,
|
|
serverCount: 0,
|
|
};
|
|
}
|
|
|
|
const names = Object.keys(servers);
|
|
return {
|
|
ejclawConfigured: names.includes('ejclaw'),
|
|
serverCount: names.length,
|
|
};
|
|
}
|
|
|
|
function readMcpSnapshot(
|
|
label: string,
|
|
configPath: string,
|
|
): RuntimeMcpSnapshot {
|
|
let content = '';
|
|
if (fs.existsSync(configPath)) {
|
|
try {
|
|
content = fs.readFileSync(configPath, 'utf-8');
|
|
} catch {
|
|
content = '';
|
|
}
|
|
}
|
|
|
|
const summary =
|
|
path.extname(configPath) === '.json'
|
|
? (summarizeJsonMcpConfig(content) ?? summarizeTomlMcpConfig(content))
|
|
: summarizeTomlMcpConfig(content);
|
|
|
|
return {
|
|
configPath: pathSnapshot(label, configPath),
|
|
...summary,
|
|
};
|
|
}
|
|
|
|
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),
|
|
},
|
|
};
|
|
}
|