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:
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),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user