109 lines
6.6 KiB
JavaScript
109 lines
6.6 KiB
JavaScript
import { TerminalToolRegistry } from './terminal-agent.mjs';
|
|
|
|
export function createTerminalToolRegistry({
|
|
getData,
|
|
getHealth,
|
|
getDelta,
|
|
buildBrief,
|
|
intelligenceStore,
|
|
securityProfileStore,
|
|
triggerSweep,
|
|
isSweepInProgress,
|
|
telegramAlerter,
|
|
} = {}) {
|
|
const dataFor = runtime => runtime?.data || getData?.() || null;
|
|
const deltaFor = runtime => runtime?.delta || getDelta?.() || null;
|
|
return new TerminalToolRegistry([
|
|
tool('get_system_status', 'Get server, sweep, LLM, Telegram, source, and memory health.', {}, async () => getHealth?.() || {}),
|
|
tool('get_security_profile', 'Get the operator-approved Security Manager profile for personal risk relevance, location, dependencies, mobility, alert preferences, and response language.', {}, async () => ({
|
|
available: Boolean(securityProfileStore?.exists),
|
|
profile: securityProfileStore?.getAgentProfile() || null,
|
|
})),
|
|
tool('get_latest_brief', 'Build the latest operator intelligence brief.', {}, async (_args, runtime) => {
|
|
const data = dataFor(runtime);
|
|
return { brief: data && buildBrief ? buildBrief(data) : 'No completed sweep.' };
|
|
}),
|
|
tool('get_sweep_delta', 'Inspect changes, escalations, de-escalations, and direction from the latest sweep.', {}, async (_args, runtime) => compactDelta(deltaFor(runtime))),
|
|
tool('get_market_snapshot', 'Get key rates, volatility, energy, metals, and current generated ideas.', {}, async (_args, runtime) => compactMarkets(dataFor(runtime))),
|
|
tool('get_source_health', 'Inspect healthy, degraded, or failed sources. Optional arguments: status, name, limit.', { status: 'string', name: 'string', limit: 'number' }, async (args, runtime) => {
|
|
const data = dataFor(runtime);
|
|
const status = clean(args.status, 30).toLowerCase();
|
|
const name = clean(args.name, 80).toLowerCase();
|
|
const limit = bounded(args.limit, 1, 25, 12);
|
|
return (data?.sourceHealth || data?.health || [])
|
|
.filter(item => !status || String(item.status || '').toLowerCase() === status)
|
|
.filter(item => !name || String(item.name || item.n || '').toLowerCase().includes(name))
|
|
.slice(0, limit)
|
|
.map(item => ({ name: item.name || item.n, status: item.status, ms: item.ms, error: clean(item.error || item.message, 240) || null }));
|
|
}),
|
|
tool('get_evidence', 'Search recent news, feed items, and urgent OSINT. Arguments: query, limit.', { query: 'string', limit: 'number' }, async (args, runtime) => {
|
|
const data = dataFor(runtime);
|
|
const query = clean(args.query, 120).toLowerCase();
|
|
const limit = bounded(args.limit, 1, 20, 8);
|
|
const rows = [
|
|
...(data?.news || []),
|
|
...(data?.newsFeed || []),
|
|
...(data?.tg?.urgent || []).map(item => ({ ...item, title: item.text, source: item.source || 'Telegram OSINT' })),
|
|
];
|
|
return rows.filter(item => !query || `${item.headline || item.title || item.text || ''} ${item.source || ''}`.toLowerCase().includes(query))
|
|
.slice(0, limit)
|
|
.map(item => ({ title: clean(item.headline || item.title || item.text, 400), source: clean(item.source, 100), url: clean(item.url, 500) || null, timestamp: item.timestamp || item.date || null }));
|
|
}),
|
|
tool('search_memory', 'Search persisted cross-sweep events. Arguments: query, limit.', { query: 'string', limit: 'number' }, async args => intelligenceStore?.queryMemory({ q: clean(args.query, 120), limit: bounded(args.limit, 1, 25, 8) }) || { available: false }),
|
|
tool('list_predictions', 'List persisted predictions and their current outcome states. Arguments: state, limit.', { state: 'string', limit: 'number' }, async args => intelligenceStore?.listPredictions({ state: clean(args.state, 30) || null, limit: bounded(args.limit, 1, 25, 8) }) || { available: false }),
|
|
tool('get_scenarios', 'Inspect current scenario watchlist states and confidence.', {}, async (_args, runtime) => {
|
|
const scenarios = dataFor(runtime)?.scenarios || {};
|
|
return { summary: scenarios.summary || null, items: (scenarios.items || scenarios.scenarios || []).slice(0, 20), changed: (scenarios.changed || []).slice(0, 10) };
|
|
}),
|
|
tool('get_trade_ideas', 'Inspect current LLM-generated ideas. Optional argument: ticker.', { ticker: 'string' }, async (args, runtime) => {
|
|
const ticker = clean(args.ticker, 30).toLowerCase();
|
|
return (dataFor(runtime)?.ideas || []).filter(item => !ticker || String(item.ticker || '').toLowerCase().includes(ticker)).slice(0, 10);
|
|
}),
|
|
tool('trigger_sweep', 'Start a new full intelligence sweep.', {}, async (_args, runtime) => {
|
|
if (!runtime.confirmed) throw new Error('Operator confirmation required');
|
|
if (isSweepInProgress?.()) return { accepted: false, status: 'already_running' };
|
|
triggerSweep?.();
|
|
return { accepted: true, status: 'started' };
|
|
}, true),
|
|
tool('mute_alerts', 'Mute proactive Telegram alerts for a bounded number of hours.', { hours: 'number' }, async (args, runtime) => {
|
|
if (!runtime.confirmed) throw new Error('Operator confirmation required');
|
|
const hours = Math.max(0.25, Math.min(24, Number(args.hours) || 1));
|
|
telegramAlerter?.muteAlerts(hours);
|
|
return { muted: true, hours };
|
|
}, true),
|
|
tool('unmute_alerts', 'Resume proactive Telegram alerts.', {}, async (_args, runtime) => {
|
|
if (!runtime.confirmed) throw new Error('Operator confirmation required');
|
|
telegramAlerter?.unmuteAlerts();
|
|
return { muted: false };
|
|
}, true),
|
|
]);
|
|
}
|
|
|
|
function tool(name, description, parameters, handler, mutating = false) {
|
|
return { name, description, parameters, handler, mutating };
|
|
}
|
|
|
|
function compactDelta(delta) {
|
|
return {
|
|
summary: delta?.summary || null,
|
|
new: (delta?.signals?.new || []).slice(0, 15),
|
|
escalated: (delta?.signals?.escalated || []).slice(0, 15),
|
|
deescalated: (delta?.signals?.deescalated || []).slice(0, 15),
|
|
};
|
|
}
|
|
|
|
function compactMarkets(data) {
|
|
if (!data) return { available: false };
|
|
const fred = Object.fromEntries((data.fred || []).filter(item => ['VIXCLS', 'DFF', 'DGS10', 'DGS2', 'T10Y2Y', 'BAMLH0A0HYM2'].includes(item.id)).map(item => [item.id, item.value]));
|
|
return { available: true, generatedAt: data.meta?.generatedAt || data.meta?.timestamp, fred, energy: data.energy || null, metals: data.metals || null, ideasSource: data.ideasSource, ideas: (data.ideas || []).slice(0, 8) };
|
|
}
|
|
|
|
function clean(value, maxLength) {
|
|
return String(value || '').replace(/[\u0000-\u001f]/g, ' ').trim().slice(0, maxLength);
|
|
}
|
|
|
|
function bounded(value, min, max, fallback) {
|
|
const parsed = Number.parseInt(value, 10);
|
|
return Number.isFinite(parsed) ? Math.max(min, Math.min(max, parsed)) : fallback;
|
|
}
|