Implementação completa de funcionalidades pendentes

- Settings persistentes (modelo padrão, workdir, max concurrent)
- Import/export de agentes via JSON
- Agendamentos persistentes com restore no startup
- Edição de agendamentos e tarefas existentes
- Filtros e busca em todas as seções
- Isolamento de WebSocket por clientId
- Autenticação via AUTH_TOKEN e CORS configurável
- Graceful shutdown com cancelamento de execuções
- Correção: --max-tokens removido (flag inválida do CLI)
- Correção: pipeline agora verifica exit code e propaga erros
- Correção: streaming de output em pipelines via WebSocket
- Permission mode bypassPermissions como padrão
- Página de configurações do sistema
- Contagem diária de execuções no dashboard
- Histórico de execuções recentes
This commit is contained in:
Frederico Castro
2026-02-26 01:24:51 -03:00
parent 723a08d2e1
commit 2f7a9d4c56
18 changed files with 1104 additions and 115 deletions

View File

@@ -1,11 +1,36 @@
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { v4 as uuidv4 } from 'uuid';
import { settingsStore } from '../store/db.js';
const CLAUDE_BIN = '/home/fred/.local/bin/claude';
const DEFAULT_MODEL = 'claude-sonnet-4-6';
const CLAUDE_BIN = resolveBin();
const activeExecutions = new Map();
function resolveBin() {
if (process.env.CLAUDE_BIN) return process.env.CLAUDE_BIN;
const home = process.env.HOME || '';
const candidates = [
`${home}/.local/bin/claude`,
'/usr/local/bin/claude',
'/usr/bin/claude',
];
for (const p of candidates) {
if (existsSync(p)) return p;
}
return 'claude';
}
function sanitizeText(str) {
if (typeof str !== 'string') return '';
return str
.replace(/\x00/g, '')
.replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
.slice(0, 50000);
}
function cleanEnv() {
const env = { ...process.env };
delete env.CLAUDECODE;
@@ -14,20 +39,33 @@ function cleanEnv() {
}
function buildArgs(agentConfig, prompt) {
const model = agentConfig.model || DEFAULT_MODEL;
const model = agentConfig.model || 'claude-sonnet-4-6';
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--model', model];
if (agentConfig.systemPrompt) {
args.push('--system-prompt', agentConfig.systemPrompt);
}
if (agentConfig.maxTurns && agentConfig.maxTurns > 0) {
args.push('--max-turns', String(agentConfig.maxTurns));
}
if (agentConfig.allowedTools && agentConfig.allowedTools.length > 0) {
const tools = Array.isArray(agentConfig.allowedTools)
? agentConfig.allowedTools.join(',')
: agentConfig.allowedTools;
args.push('--allowedTools', tools);
}
args.push('--permission-mode', agentConfig.permissionMode || 'bypassPermissions');
return args;
}
function buildPrompt(task, instructions) {
const parts = [];
if (task) parts.push(task);
if (instructions) parts.push(`\nInstruções adicionais:\n${instructions}`);
if (task) parts.push(sanitizeText(task));
if (instructions) parts.push(`\nInstruções adicionais:\n${sanitizeText(instructions)}`);
return parts.join('\n');
}
@@ -74,7 +112,19 @@ function extractText(event) {
return null;
}
function getMaxConcurrent() {
const s = settingsStore.get();
return s.maxConcurrent || 5;
}
export function execute(agentConfig, task, callbacks = {}) {
const maxConcurrent = getMaxConcurrent();
if (activeExecutions.size >= maxConcurrent) {
const err = new Error(`Limite de ${maxConcurrent} execuções simultâneas atingido`);
if (callbacks.onError) callbacks.onError(err, uuidv4());
return null;
}
const executionId = uuidv4();
const { onData, onError, onComplete } = callbacks;
@@ -96,7 +146,7 @@ export function execute(agentConfig, task, callbacks = {}) {
}
console.log(`[executor] Iniciando: ${executionId}`);
console.log(`[executor] Modelo: ${agentConfig.model || DEFAULT_MODEL}`);
console.log(`[executor] Modelo: ${agentConfig.model || 'claude-sonnet-4-6'}`);
console.log(`[executor] cwd: ${spawnOptions.cwd || process.cwd()}`);
const child = spawn(CLAUDE_BIN, args, spawnOptions);
@@ -180,6 +230,13 @@ export function cancel(executionId) {
return true;
}
export function cancelAllExecutions() {
for (const [id, exec] of activeExecutions) {
exec.process.kill('SIGTERM');
}
activeExecutions.clear();
}
export function getActiveExecutions() {
return Array.from(activeExecutions.entries()).map(([id, exec]) => ({
executionId: id,
@@ -187,3 +244,7 @@ export function getActiveExecutions() {
agentConfig: exec.agentConfig,
}));
}
export function getBinPath() {
return CLAUDE_BIN;
}

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { agentsStore } from '../store/db.js';
import { agentsStore, schedulesStore } from '../store/db.js';
import * as executor from './executor.js';
import * as scheduler from './scheduler.js';
@@ -7,10 +7,32 @@ const DEFAULT_CONFIG = {
model: 'claude-sonnet-4-6',
systemPrompt: '',
workingDirectory: '',
maxTokens: 16000,
temperature: 1,
maxTurns: 0,
permissionMode: 'bypassPermissions',
allowedTools: '',
};
let dailyExecutionCount = 0;
let dailyCountDate = new Date().toDateString();
function incrementDailyCount() {
const today = new Date().toDateString();
if (today !== dailyCountDate) {
dailyExecutionCount = 0;
dailyCountDate = today;
}
dailyExecutionCount++;
}
export function getDailyExecutionCount() {
const today = new Date().toDateString();
if (today !== dailyCountDate) {
dailyExecutionCount = 0;
dailyCountDate = today;
}
return dailyExecutionCount;
}
function validateAgent(data) {
const errors = [];
if (!data.agent_name || typeof data.agent_name !== 'string') {
@@ -22,6 +44,13 @@ function validateAgent(data) {
return errors;
}
function sanitizeTags(tags) {
if (!Array.isArray(tags)) return [];
return tags
.filter((t) => typeof t === 'string' && t.length > 0 && t.length <= 50)
.slice(0, 20);
}
export function getAllAgents() {
return agentsStore.getAll();
}
@@ -39,6 +68,7 @@ export function createAgent(data) {
const agentData = {
agent_name: data.agent_name,
description: data.description || '',
tags: sanitizeTags(data.tags),
tasks: data.tasks || [],
config: { ...DEFAULT_CONFIG, ...(data.config || {}) },
status: data.status || 'active',
@@ -56,6 +86,7 @@ export function updateAgent(id, data) {
const updateData = {};
if (data.agent_name !== undefined) updateData.agent_name = data.agent_name;
if (data.description !== undefined) updateData.description = data.description;
if (data.tags !== undefined) updateData.tags = sanitizeTags(data.tags);
if (data.tasks !== undefined) updateData.tasks = data.tasks;
if (data.status !== undefined) updateData.status = data.status;
if (data.assigned_host !== undefined) updateData.assigned_host = data.assigned_host;
@@ -78,6 +109,7 @@ export function executeTask(agentId, task, instructions, wsCallback) {
const executionRecord = {
executionId: null,
agentId,
agentName: agent.agent_name,
task: typeof task === 'string' ? task : task.description,
startedAt: new Date().toISOString(),
status: 'running',
@@ -122,7 +154,12 @@ export function executeTask(agentId, task, instructions, wsCallback) {
}
);
if (!executionId) {
throw new Error('Limite de execuções simultâneas atingido');
}
executionRecord.executionId = executionId;
incrementDailyCount();
const updatedAgent = agentsStore.getById(agentId);
const executions = [...(updatedAgent.executions || []), executionRecord];
@@ -151,11 +188,49 @@ export function scheduleTask(agentId, taskDescription, cronExpression, wsCallbac
const scheduleId = uuidv4();
const items = schedulesStore.getAll();
items.push({
id: scheduleId,
agentId,
agentName: agent.agent_name,
taskDescription,
cronExpression,
active: true,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
schedulesStore.save(items);
scheduler.schedule(scheduleId, cronExpression, () => {
executeTask(agentId, taskDescription, null, wsCallback);
}, false);
return { scheduleId, agentId, agentName: agent.agent_name, taskDescription, cronExpression };
}
export function updateScheduleTask(scheduleId, data, wsCallback) {
const stored = schedulesStore.getById(scheduleId);
if (!stored) return null;
const agentId = data.agentId || stored.agentId;
const agent = agentsStore.getById(agentId);
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
const taskDescription = data.taskDescription || stored.taskDescription;
const cronExpression = data.cronExpression || stored.cronExpression;
scheduler.updateSchedule(scheduleId, cronExpression, () => {
executeTask(agentId, taskDescription, null, wsCallback);
});
return { scheduleId, agentId, taskDescription, cronExpression };
schedulesStore.update(scheduleId, {
agentId,
agentName: agent.agent_name,
taskDescription,
cronExpression,
});
return schedulesStore.getById(scheduleId);
}
export function cancelExecution(executionId) {
@@ -166,20 +241,59 @@ export function getActiveExecutions() {
return executor.getActiveExecutions();
}
export function getRecentExecutions(limit = 20) {
const agents = agentsStore.getAll();
const all = agents.flatMap((a) =>
(a.executions || []).map((e) => ({
...e,
agentName: a.agent_name,
agentId: a.id,
}))
);
all.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
return all.slice(0, limit);
}
export function exportAgent(agentId) {
const agent = agentsStore.getById(agentId);
if (!agent) return null;
return {
id: agent.id,
agent_name: agent.agent_name,
description: agent.description,
tags: agent.tags || [],
tasks: agent.tasks,
config: agent.config,
status: agent.status,
assigned_host: agent.assigned_host,
created_at: agent.created_at,
updated_at: agent.updated_at,
executions: agent.executions || [],
};
}
export function importAgent(data) {
if (!data.agent_name) {
throw new Error('agent_name é obrigatório para importação');
}
const agentData = {
agent_name: data.agent_name,
description: data.description || '',
tags: sanitizeTags(data.tags),
tasks: data.tasks || [],
config: { ...DEFAULT_CONFIG, ...(data.config || {}) },
status: data.status || 'active',
assigned_host: data.assigned_host || 'localhost',
executions: [],
};
return agentsStore.create(agentData);
}
export function restoreSchedules(wsCallback) {
scheduler.restoreSchedules((agentId, taskDescription) => {
try {
executeTask(agentId, taskDescription, null, wsCallback);
} catch (err) {
console.log(`[manager] Erro ao executar tarefa agendada: ${err.message}`);
}
});
}

View File

@@ -32,27 +32,56 @@ function buildSteps(steps) {
.sort((a, b) => a.order - b.order);
}
function enrichStepsWithAgentNames(steps) {
const agents = agentsStore.getAll();
const agentMap = new Map(agents.map((a) => [a.id, a.agent_name]));
return steps.map((s) => ({
...s,
agentName: agentMap.get(s.agentId) || s.agentId,
}));
}
function applyTemplate(template, input) {
if (!template) return input;
return template.replace(/\{\{input\}\}/g, input);
}
function executeStepAsPromise(agentConfig, prompt, pipelineState) {
function executeStepAsPromise(agentConfig, prompt, pipelineState, wsCallback, pipelineId, stepIndex) {
return new Promise((resolve, reject) => {
const executionId = executor.execute(
agentConfig,
{ description: prompt },
{
onData: () => {},
onData: (parsed, execId) => {
if (wsCallback) {
wsCallback({
type: 'pipeline_step_output',
pipelineId,
stepIndex,
executionId: execId,
data: parsed,
});
}
},
onError: (err) => {
reject(err);
},
onComplete: (result) => {
if (result.exitCode !== 0 && !result.result) {
reject(new Error(result.stderr || `Processo encerrado com código ${result.exitCode}`));
return;
}
resolve(result.result || '');
},
}
);
if (!executionId) {
reject(new Error('Limite de execuções simultâneas atingido'));
return;
}
pipelineState.currentExecutionId = executionId;
});
}
@@ -97,7 +126,7 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
});
}
const result = await executeStepAsPromise(agent.config, prompt, pipelineState);
const result = await executeStepAsPromise(agent.config, prompt, pipelineState, wsCallback, pipelineId, i);
if (pipelineState.canceled) break;
@@ -200,5 +229,9 @@ export function getPipeline(id) {
}
export function getAllPipelines() {
return pipelinesStore.getAll();
const pipelines = pipelinesStore.getAll();
return pipelines.map((p) => ({
...p,
steps: enrichStepsWithAgentNames(p.steps || []),
}));
}

View File

@@ -1,5 +1,6 @@
import cron from 'node-cron';
import { EventEmitter } from 'events';
import { schedulesStore } from '../store/db.js';
const HISTORY_LIMIT = 50;
const schedules = new Map();
@@ -13,9 +14,54 @@ function addToHistory(entry) {
}
}
export function schedule(taskId, cronExpr, callback) {
function matchesCronPart(part, value) {
if (part === '*') return true;
if (part.startsWith('*/')) return value % parseInt(part.slice(2)) === 0;
if (part.includes(',')) return part.split(',').map(Number).includes(value);
if (part.includes('-')) {
const [start, end] = part.split('-').map(Number);
return value >= start && value <= end;
}
return parseInt(part) === value;
}
function nextCronDate(cronExpr) {
const parts = cronExpr.split(' ');
if (parts.length !== 5) return null;
const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
const candidate = new Date();
candidate.setSeconds(0);
candidate.setMilliseconds(0);
candidate.setMinutes(candidate.getMinutes() + 1);
for (let i = 0; i < 525600; i++) {
const m = candidate.getMinutes();
const h = candidate.getHours();
const dom = candidate.getDate();
const mon = candidate.getMonth() + 1;
const dow = candidate.getDay();
if (
matchesCronPart(minute, m) &&
matchesCronPart(hour, h) &&
matchesCronPart(dayOfMonth, dom) &&
matchesCronPart(month, mon) &&
matchesCronPart(dayOfWeek, dow)
) {
return candidate.toISOString();
}
candidate.setMinutes(candidate.getMinutes() + 1);
}
return null;
}
export function schedule(taskId, cronExpr, callback, persist = true) {
if (schedules.has(taskId)) {
unschedule(taskId);
unschedule(taskId, false);
}
if (!cron.validate(cronExpr)) {
@@ -44,12 +90,32 @@ export function schedule(taskId, cronExpr, callback) {
return { taskId, cronExpr };
}
export function unschedule(taskId) {
export function unschedule(taskId, persist = true) {
const entry = schedules.get(taskId);
if (!entry) return false;
entry.task.stop();
schedules.delete(taskId);
if (persist) {
schedulesStore.delete(taskId);
}
return true;
}
export function updateSchedule(taskId, cronExpr, callback) {
const entry = schedules.get(taskId);
if (!entry) return false;
entry.task.stop();
schedules.delete(taskId);
if (!cron.validate(cronExpr)) {
throw new Error(`Expressão cron inválida: ${cronExpr}`);
}
schedule(taskId, cronExpr, callback, false);
return true;
}
@@ -68,13 +134,49 @@ export function setActive(taskId, active) {
}
export function getSchedules() {
return Array.from(schedules.values()).map(({ task: _, ...rest }) => rest);
const stored = schedulesStore.getAll();
const result = [];
for (const s of stored) {
const inMemory = schedules.get(s.id);
result.push({
...s,
cronExpr: s.cronExpression || s.cronExpr,
active: inMemory ? inMemory.active : false,
nextRun: nextCronDate(s.cronExpression || s.cronExpr || ''),
});
}
return result;
}
export function getHistory() {
return [...history];
}
export function restoreSchedules(executeFn) {
const stored = schedulesStore.getAll();
let restored = 0;
for (const s of stored) {
if (!s.active) continue;
const cronExpr = s.cronExpression || s.cronExpr;
try {
schedule(s.id, cronExpr, () => {
executeFn(s.agentId, s.taskDescription);
}, false);
restored++;
} catch (err) {
console.log(`[scheduler] Falha ao restaurar agendamento ${s.id}: ${err.message}`);
}
}
if (restored > 0) {
console.log(`[scheduler] ${restored} agendamento(s) restaurado(s)`);
}
}
export function on(event, listener) {
emitter.on(event, listener);
}

View File

@@ -1,21 +1,58 @@
import { Router } from 'express';
import { execSync } from 'child_process';
import os from 'os';
import * as manager from '../agents/manager.js';
import { tasksStore } from '../store/db.js';
import { tasksStore, settingsStore } from '../store/db.js';
import * as scheduler from '../agents/scheduler.js';
import * as pipeline from '../agents/pipeline.js';
import { getBinPath } from '../agents/executor.js';
const router = Router();
let wsbroadcast = null;
let wsBroadcastTo = null;
export function setWsBroadcast(fn) {
wsbroadcast = fn;
}
function wsCallback(message) {
if (wsbroadcast) wsbroadcast(message);
export function setWsBroadcastTo(fn) {
wsBroadcastTo = fn;
}
function wsCallback(message, clientId) {
if (clientId && wsBroadcastTo) {
wsBroadcastTo(clientId, message);
} else if (wsbroadcast) {
wsbroadcast(message);
}
}
router.get('/settings', (req, res) => {
try {
res.json(settingsStore.get());
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.put('/settings', (req, res) => {
try {
const allowed = ['defaultModel', 'defaultWorkdir', 'maxConcurrent'];
const data = {};
for (const key of allowed) {
if (req.body[key] !== undefined) data[key] = req.body[key];
}
if (data.maxConcurrent !== undefined) {
data.maxConcurrent = Math.max(1, Math.min(20, parseInt(data.maxConcurrent) || 5));
}
const saved = settingsStore.save(data);
res.json(saved);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
router.get('/agents', (req, res) => {
try {
res.json(manager.getAllAgents());
@@ -43,6 +80,15 @@ router.post('/agents', (req, res) => {
}
});
router.post('/agents/import', (req, res) => {
try {
const agent = manager.importAgent(req.body);
res.status(201).json(agent);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
router.put('/agents/:id', (req, res) => {
try {
const agent = manager.updateAgent(req.params.id, req.body);
@@ -68,7 +114,11 @@ router.post('/agents/:id/execute', (req, res) => {
const { task, instructions } = req.body;
if (!task) return res.status(400).json({ error: 'task é obrigatório' });
const executionId = manager.executeTask(req.params.id, task, instructions, wsCallback);
const clientId = req.headers['x-client-id'] || null;
const executionId = manager.executeTask(
req.params.id, task, instructions,
(msg) => wsCallback(msg, clientId)
);
res.status(202).json({ executionId, status: 'started' });
} catch (err) {
const status = err.message.includes('não encontrado') ? 404 : 400;
@@ -140,7 +190,8 @@ router.post('/schedules', (req, res) => {
if (!agentId || !taskDescription || !cronExpression) {
return res.status(400).json({ error: 'agentId, taskDescription e cronExpression são obrigatórios' });
}
const result = manager.scheduleTask(agentId, taskDescription, cronExpression, wsCallback);
const clientId = req.headers['x-client-id'] || null;
const result = manager.scheduleTask(agentId, taskDescription, cronExpression, (msg) => wsCallback(msg, clientId));
res.status(201).json(result);
} catch (err) {
const status = err.message.includes('não encontrado') ? 404 : 400;
@@ -148,6 +199,14 @@ router.post('/schedules', (req, res) => {
}
});
router.get('/schedules/history', (req, res) => {
try {
res.json(scheduler.getHistory());
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/schedules', (req, res) => {
try {
res.json(scheduler.getSchedules());
@@ -156,6 +215,18 @@ router.get('/schedules', (req, res) => {
}
});
router.put('/schedules/:id', (req, res) => {
try {
const clientId = req.headers['x-client-id'] || null;
const updated = manager.updateScheduleTask(req.params.id, req.body, (msg) => wsCallback(msg, clientId));
if (!updated) return res.status(404).json({ error: 'Agendamento não encontrado' });
res.json(updated);
} catch (err) {
const status = err.message.includes('não encontrado') ? 404 : 400;
res.status(status).json({ error: err.message });
}
});
router.delete('/schedules/:taskId', (req, res) => {
try {
const removed = scheduler.unschedule(req.params.taskId);
@@ -218,7 +289,8 @@ router.post('/pipelines/:id/execute', (req, res) => {
const { input } = req.body;
if (!input) return res.status(400).json({ error: 'input é obrigatório' });
pipeline.executePipeline(req.params.id, input, wsCallback).catch(() => {});
const clientId = req.headers['x-client-id'] || null;
pipeline.executePipeline(req.params.id, input, (msg) => wsCallback(msg, clientId)).catch(() => {});
res.status(202).json({ pipelineId: req.params.id, status: 'started' });
} catch (err) {
const status = err.message.includes('não encontrado') ? 404 : 400;
@@ -252,6 +324,7 @@ router.get('/system/status', (req, res) => {
},
executions: {
active: activeExecutions.length,
today: manager.getDailyExecutionCount(),
list: activeExecutions,
},
schedules: {
@@ -269,6 +342,25 @@ router.get('/system/status', (req, res) => {
}
});
router.get('/system/info', (req, res) => {
try {
let claudeVersion = 'N/A';
try {
claudeVersion = execSync(`${getBinPath()} --version`, { timeout: 5000 }).toString().trim();
} catch {}
res.json({
serverVersion: '1.0.0',
nodeVersion: process.version,
claudeVersion,
platform: `${os.platform()} ${os.arch()}`,
uptime: Math.floor(process.uptime()),
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/executions/active', (req, res) => {
try {
res.json(manager.getActiveExecutions());
@@ -277,4 +369,13 @@ router.get('/executions/active', (req, res) => {
}
});
router.get('/executions/recent', (req, res) => {
try {
const limit = parseInt(req.query.limit) || 20;
res.json(manager.getRecentExecutions(limit));
} catch (err) {
res.status(500).json({ error: err.message });
}
});
export default router;

View File

@@ -1,4 +1,4 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { v4 as uuidv4 } from 'uuid';
@@ -8,6 +8,17 @@ const DATA_DIR = `${__dirname}/../../data`;
const AGENTS_FILE = `${DATA_DIR}/agents.json`;
const TASKS_FILE = `${DATA_DIR}/tasks.json`;
const PIPELINES_FILE = `${DATA_DIR}/pipelines.json`;
const SCHEDULES_FILE = `${DATA_DIR}/schedules.json`;
const SETTINGS_FILE = `${DATA_DIR}/settings.json`;
const DEFAULT_SETTINGS = {
defaultModel: 'claude-sonnet-4-6',
defaultWorkdir: '',
maxConcurrent: 5,
};
const writeLocks = new Map();
const fileCache = new Map();
function ensureDataDir() {
if (!existsSync(DATA_DIR)) {
@@ -15,22 +26,47 @@ function ensureDataDir() {
}
}
function loadFile(filePath) {
function getCacheMtime(filePath) {
const cached = fileCache.get(filePath);
if (!cached) return null;
return cached.mtime;
}
function loadFile(filePath, defaultValue = []) {
ensureDataDir();
if (!existsSync(filePath)) {
writeFileSync(filePath, JSON.stringify([]), 'utf8');
return [];
writeFileSync(filePath, JSON.stringify(defaultValue, null, 2), 'utf8');
fileCache.set(filePath, { data: defaultValue, mtime: Date.now() });
return JSON.parse(JSON.stringify(defaultValue));
}
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
const stat = statSync(filePath);
const mtime = stat.mtimeMs;
const cached = fileCache.get(filePath);
if (cached && cached.mtime === mtime) {
return JSON.parse(JSON.stringify(cached.data));
}
const data = JSON.parse(readFileSync(filePath, 'utf8'));
fileCache.set(filePath, { data, mtime });
return JSON.parse(JSON.stringify(data));
} catch {
return [];
return JSON.parse(JSON.stringify(defaultValue));
}
}
function saveFile(filePath, data) {
ensureDataDir();
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
const prev = writeLocks.get(filePath) || Promise.resolve();
const next = prev.then(() => {
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
const stat = statSync(filePath);
fileCache.set(filePath, { data: JSON.parse(JSON.stringify(data)), mtime: stat.mtimeMs });
}).catch(() => {});
writeLocks.set(filePath, next);
return next;
}
function createStore(filePath) {
@@ -84,6 +120,21 @@ function createStore(filePath) {
};
}
function createSettingsStore(filePath) {
return {
get: () => loadFile(filePath, DEFAULT_SETTINGS),
save: (data) => {
const current = loadFile(filePath, DEFAULT_SETTINGS);
const merged = { ...current, ...data };
saveFile(filePath, merged);
return merged;
},
};
}
export const agentsStore = createStore(AGENTS_FILE);
export const tasksStore = createStore(TASKS_FILE);
export const pipelinesStore = createStore(PIPELINES_FILE);
export const schedulesStore = createStore(SCHEDULES_FILE);
export const settingsStore = createSettingsStore(SETTINGS_FILE);