// cayce-core — el ciclo de la entidad. // 1. events watch — BTC Map v4 → tabla events (🟢) // 2. curación — estado del stream de hardcap (🟢) // 3. digest — GLM arma la promo diaria → publica vía cayce-nostr (🟢) // 4. métricas + audit log // Todo lo que hace queda en actions_log. "Lo no auditado no existió." import pg from 'pg' import { loadIdentityBlock } from './outbox.mjs' import { chat } from './llm.mjs' const { ZAI_API_KEY, ZAI_BASE = 'https://api.z.ai/api/paas/v4/chat/completions', CAYCE_MODEL = 'glm-4.7', PGURL = 'postgresql://hardcap@localhost:5432/cayce', HARD_CAP_API = 'https://api.hardcap.news', NOSTR_INTERNAL = 'http://127.0.0.1:3011', SUPERVISOR_NPUB_HEX, } = process.env const pgc = new pg.Pool({ connectionString: PGURL }) const log = (zone, action, detail = {}, result = 'ok') => pgc.query('INSERT INTO actions_log (zone, action, detail, result) VALUES ($1,$2,$3,$4)', [zone, action, detail, result]) async function glm(prompt) { // cadena de fallback en llm.mjs (zai → gemini); lanza si todo falla const t = await chat([{ role: 'user', content: prompt }], 3000) // una nota vacía en Nostr es peor que ningún post — fallar explícito if (!t) throw new Error('glm devolvió contenido vacío') return t } // ── 1. events watch: BTC Map v4 → upsert (🟢) async function eventsWatch() { const r = await fetch('https://api.btcmap.org/v4/events', { signal: AbortSignal.timeout(20000) }) if (!r.ok) throw new Error(`btcmap HTTP ${r.status}`) const evs = await r.json() const now = new Date() const upcoming = evs.filter(e => e.starts_at && new Date(e.ends_at || e.starts_at) >= now) let upserted = 0 for (const e of upcoming.slice(0, 200)) { const id = 'btcmap-' + e.id await pgc.query( `INSERT INTO events (id, name, type, city, country, starts_on, ends_on, url, source, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'btcmap',now()) ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name, starts_on=EXCLUDED.starts_on, ends_on=EXCLUDED.ends_on, url=EXCLUDED.url, updated_at=now()`, [id, e.name || 'Bitcoin event', 'meetup', e.city || null, e.country || null, (e.starts_at || '').slice(0, 10) || null, (e.ends_at || '').slice(0, 10) || null, e.website || null] ) upserted++ } await log('green', 'events_watch', { upcoming: upcoming.length, upserted }) return upserted } // ── 2. curación: estado del stream de hardcap (🟢, reporte) async function curate() { const r = await fetch(`${HARD_CAP_API}/api/articles?limit=1`, { signal: AbortSignal.timeout(15000) }) const d = await r.json() await log('green', 'curation_check', { total_published: d.total }) return d.total } // ── 3. digest promo: GLM arma el texto → publica (🟢) async function digest(totalPublished) { const arts = (await (await fetch(`${HARD_CAP_API}/api/articles?limit=8`, { signal: AbortSignal.timeout(15000) })).json()).articles const evs = (await pgc.query( `SELECT name, city, country, starts_on, url FROM events WHERE active AND starts_on >= CURRENT_DATE ORDER BY starts_on ASC LIMIT 6` )).rows const stories = arts.map(a => `- [${a.title}] — score ${a.relevanceScore ?? '?'} — https://hardcap.news/news/${a.id}/summary` ).join('\n') const eventLines = evs.map(e => `- ${e.starts_on} — ${e.name}${e.city ? ` (${e.city})` : ''}${e.url ? ` — ${e.url}` : ''}` ).join('\n') const prompt = loadIdentityBlock('digest') const input = `STORIES (from hardcap.news, published):\n${stories}\n\nUPCOMING EVENTS:\n${eventLines || '(none)'}\n\nTotal published articles today: ${totalPublished}` const text = await glm(prompt + '\n\n' + input) const pub = await fetch(`${NOSTR_INTERNAL}/publish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 1, content: text, type: 'digest' }), signal: AbortSignal.timeout(60000), }) const pr = await pub.json() await pgc.query('INSERT INTO digests (kind, body, nostr_event_id) VALUES ($1,$2,$3)', ['nostr_note', text, pr.id]) await log('green', 'digest_published', { nostr_event_id: pr.id, relays: pr.relays, length: text.length }) return { text, nostr: pr } } // ── 3b. posteos de opinión y events-post (modo --posts) async function opinionPost() { const arts = (await (await fetch(`${HARD_CAP_API}/api/articles?limit=10`, { signal: AbortSignal.timeout(15000) })).json()).articles const stories = arts.map(a => `- [${a.title}] — topic ${(a.topics || []).join('/')} — score ${a.relevanceScore ?? '?'} — resumen: ${(a.summary || '(sin resumen)').slice(0, 200)} — url: https://hardcap.news/news/${a.id}/summary` ).join('\n') const prompt = loadIdentityBlock('opinion') + '\n\nSTORIES:\n' + stories const text = (await glm(prompt)).trim() const pub = await fetch(`${NOSTR_INTERNAL}/publish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 1, content: text, type: 'opinion' }), signal: AbortSignal.timeout(60000), }) const pr = await pub.json() await pgc.query('INSERT INTO digests (kind, body, nostr_event_id) VALUES ($1,$2,$3)', ['nostr_opinion', text, pr.id]) await log('green', 'opinion_post', { nostr_event_id: pr.id, length: text.length }) return text } // ── 3c. reflexión diaria: la voz pura de Cayce, cero autopromoción (modo --posts) async function reflectionPost() { const arts = (await (await fetch(`${HARD_CAP_API}/api/articles?limit=8`, { signal: AbortSignal.timeout(15000) })).json().catch(() => ({}))).articles || [] const seed = arts.length ? arts.map(a => `- ${a.title}`).join('\n') : '(sin titulares hoy — reflexioná desde lo que ya sabés)' const prompt = loadIdentityBlock('reflection') + '\n' + seed const text = (await glm(prompt)).trim() const pub = await fetch(`${NOSTR_INTERNAL}/publish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 1, content: text, type: 'reflection' }), signal: AbortSignal.timeout(60000), }) const pr = await pub.json() await pgc.query('INSERT INTO digests (kind, body, nostr_event_id) VALUES ($1,$2,$3)', ['nostr_reflection', text, pr.id]) await log('green', 'reflection_post', { nostr_event_id: pr.id, length: text.length }) return text } async function eventsPost() { const evs = (await pgc.query( `SELECT name, city, country, starts_on, url FROM events WHERE active AND starts_on >= CURRENT_DATE ORDER BY starts_on ASC LIMIT 4` )).rows const prompt = loadIdentityBlock('events') + '\n\nEVENTS:\n' + evs.map(e => `- ${e.starts_on} — ${e.name}${e.city ? ` (${e.city}, ${e.country || ''})` : ''}${e.url ? ` — ${e.url}` : ''}`).join('\n') || '(no upcoming events)' const text = (await glm(prompt)).trim() const pub = await fetch(`${NOSTR_INTERNAL}/publish`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind: 1, content: text, type: 'events_post' }), signal: AbortSignal.timeout(60000), }) const pr = await pub.json() await pgc.query('INSERT INTO digests (kind, body, nostr_event_id) VALUES ($1,$2,$3)', ['nostr_events', text, pr.id]) await log('green', 'events_post', { nostr_event_id: pr.id, length: text.length }) return text } // ── 4. tesorería: snapshot del balance Blink (🟢, solo lectura) async function treasury() { if (!process.env.BLINK_API_KEY) return 'sin BLINK_API_KEY aún' const r = await fetch('https://api.blink.sv/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-KEY': process.env.BLINK_API_KEY }, body: JSON.stringify({ query: 'query me { me { defaultAccount { username wallets { id walletCurrency balance } } } }' }), signal: AbortSignal.timeout(15000), }) const d = await r.json() const acct = d?.data?.me?.defaultAccount if (!acct) return 'blink sin respuesta válida' for (const w of acct.wallets || []) { await pgc.query( 'INSERT INTO metrics (property, metric, value, note) VALUES ($1,$2,$3,$4)', ['cayce_wallet', `balance_${w.walletCurrency.toLowerCase()}`, w.balance, w.id] ) } const ln = acct.username ? `${acct.username}@blink.sv` : null await log('green', 'treasury_snapshot', { wallets: (acct.wallets || []).length, lightning_address: ln }) return `balance registrado${ln ? ' — ' + ln : ''}` } // ── main const started = Date.now() const results = {} if (process.argv.includes('--posts')) { // modo ligero: solo voz pública (opinión + reflection + events post) // espaciado: nunca 2 posteos seguidos — 45 min entre ellos (regla del supervisor) try { results.opinion = await opinionPost() } catch (e) { results.opinion = 'ERROR: ' + String(e); await log('red', 'opinion_post', { error: String(e).slice(0, 200) }, 'error') } await log('green', 'posts_spacing', {}, 'esperando 45 min antes del reflection post') await new Promise(r => setTimeout(r, 45 * 60 * 1000)) try { results.reflection = await reflectionPost() } catch (e) { results.reflection = 'ERROR: ' + String(e); await log('red', 'reflection_post', { error: String(e).slice(0, 200) }, 'error') } await log('green', 'posts_spacing', {}, 'esperando 45 min antes del events post') await new Promise(r => setTimeout(r, 45 * 60 * 1000)) try { results.events_post = await eventsPost() } catch (e) { results.events_post = 'ERROR: ' + String(e); await log('red', 'events_post', { error: String(e).slice(0, 200) }, 'error') } const hadError = Object.values(results).some(v => String(v).startsWith('ERROR')) await log('green', 'posts_cycle', results, hadError ? 'error' : `done in ${Date.now() - started}ms`) console.log(JSON.stringify(results, null, 2).slice(0, 1600)) await pgc.end() process.exit(hadError ? 1 : 0) } try { results.events = await eventsWatch() } catch (e) { results.events = 'ERROR: ' + String(e); await log('red', 'events_watch', { error: String(e).slice(0, 200) }, 'error') } try { results.published_total = await curate() } catch (e) { results.published_total = 'ERROR: ' + String(e); await log('red', 'curation_check', { error: String(e).slice(0, 200) }, 'error') } try { results.treasury = await treasury() } catch (e) { results.treasury = 'ERROR: ' + String(e); await log('red', 'treasury', { error: String(e).slice(0, 200) }, 'error') } try { const d = await digest(results.published_total ?? 0) results.digest = { nostr: d.nostr, preview: d.text.slice(0, 120) } } catch (e) { results.digest = 'ERROR: ' + String(e); await log('red', 'digest', { error: String(e).slice(0, 200) }, 'error') } const hadError = Object.values(results).some(v => String(v).startsWith('ERROR')) await log('green', 'cycle_complete', results, hadError ? 'error' : `done in ${Date.now() - started}ms`) console.log(JSON.stringify(results, null, 2)) await pgc.end() process.exit(hadError ? 1 : 0)