#!/usr/bin/env node // Mac side of the Igneum relay (relay/ in this repo, https://relay.igneum.network). // node tools/relay.mjs the feed, newest first (last 50) // node tools/relay.mjs list [N] [--machine X] more of the feed // node tools/relay.mjs read print an item; its file is downloaded to --out (default $TMPDIR/igneum-relay) // node tools/relay.mjs drop "" | post a note or a file from the Mac [--to PC1] [--title "..."] [--body "..." with a file] // node tools/relay.mjs task "title" [file] [--body "..."] a task for a person or a Claude session on that PC // node tools/relay.mjs run "title" [--elevated] [--reboot-continue] a script the igneum-agent runs // node tools/relay.mjs watch [--since ] poll every 10 s and print new items (results included) // node tools/relay.mjs inbox [--ack] what that machine has not read yet // node tools/relay.mjs machines | role | name // node tools/relay.mjs ack | done | rm | url // Reads ~/.config/igneum/relay-token (the URL secret), relay-key (x-igneum-key), dl-token (for __DL_BASE__ in // playbooks) and relay-url (optional, default https://relay.igneum.network). Zero dependencies. import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync } from 'node:fs'; import { homedir, tmpdir, hostname } from 'node:os'; import { basename, join, resolve } from 'node:path'; process.stdout.on('error', e => { if (e.code === 'EPIPE') process.exit(0); throw e; }); const cfg = n => { try { return readFileSync(join(homedir(), '.config', 'igneum', n), 'utf8').trim(); } catch { return ''; } }; const TOKEN = cfg('relay-token'); const KEY = cfg('relay-key'); const DL = cfg('dl-token'); // relay-key is the relay's own key since 4 Oct 2026 (round 4, X23); the intake key no longer opens the relay const BASE = (cfg('relay-url') || 'https://relay.igneum.network').replace(/\/$/, ''); if (!TOKEN) { console.error('no ~/.config/igneum/relay-token'); process.exit(1); } const API = `${BASE}/r/${TOKEN}/api/`; const WEB = `${BASE}/r/${TOKEN}`; const SHOWN = `${BASE}/r/`; // printed in place of WEB everywhere but `url` (round 4, X24: the token in every terminal) const argv = process.argv.slice(2); const flags = {}; const pos = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a.startsWith('--')) { const k = a.slice(2); const next = argv[i + 1]; if (next !== undefined && !next.startsWith('--') && !['elevated', 'reboot-continue', 'ack', 'all'].includes(k)) { flags[k] = next; i++; } else flags[k] = true; } else pos.push(a); } const cmd = pos[0] && !/^\d+$/.test(pos[0]) ? pos[0] : (pos[0] ? 'read' : 'list'); if (cmd === 'read' && /^\d+$/.test(pos[0])) pos.unshift('read'); async function api(fn, { q, body } = {}) { const r = await fetch(API + fn + (q ? '?' + new URLSearchParams(q) : ''), body === undefined ? { headers: { 'x-igneum-key': KEY } } : { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-igneum-key': KEY }, body: JSON.stringify(body) }); const j = await r.json().catch(() => ({ ok: false, error: `http ${r.status}` })); if (!r.ok || j.ok === false) throw new Error(j.error || `http ${r.status}`); return j; } async function uploadFile(path) { const buf = readFileSync(path); const name = basename(path); const t = await api('upload', { body: { name, size: buf.length } }); if (buf.length > t.max) throw new Error(`${name} is ${buf.length} bytes, over the ${t.max} byte cap`); const r = await fetch(t.put_url, { method: 'PUT', body: buf, headers: { authorization: `Bearer ${t.token}`, 'x-api-version': t.api_version, 'x-add-random-suffix': '1', 'x-content-type': 'application/octet-stream' } }); const j = await r.json().catch(() => ({})); if (!r.ok || !j.url) throw new Error(`blob upload failed: ${r.status} ${JSON.stringify(j).slice(0, 200)}`); return { file_name: name, file_url: j.url, size: buf.length }; } const fmtSize = n => n < 1024 ? `${n} B` : n < 1048576 ? `${(n / 1024).toFixed(1)} KB` : `${(n / 1048576).toFixed(1)} MB`; const when = ts => new Date(ts).toISOString().replace('T', ' ').slice(5, 16); function line(it) { const route = it.to === 'all' ? it.from : `${it.from} > ${it.to}`; const st = it.done ? ' done' : it.read && (it.kind === 'task' || it.kind === 'run') ? ' read' : (it.kind === 'task' || it.kind === 'run') ? ' NEW' : ''; const first = (it.body || '').split('\n').find(l => l.trim()) || ''; const file = it.has_file ? ` [${it.file_name} ${fmtSize(it.size)}]` : ''; return `${('#' + it.id).padStart(5)} ${when(it.ts)} ${it.kind.padEnd(6)} ${route.padEnd(12)} ${it.title ? it.title + (first ? ': ' : '') : ''}${first.slice(0, 90)}${file}${st}`; } function printItem(it) { console.log(`===== #${it.id} ${it.kind} from ${it.from} to ${it.to} at ${it.ts}${it.title ? ` | ${it.title}` : ''} =====`); const fl = Object.entries(it.flags || {}).filter(([, v]) => v !== false).map(([k, v]) => v === true ? k : `${k}=${v}`).join(' '); if (fl || it.task_id) console.log(`[${[fl, it.task_id ? `task #${it.task_id}` : '', it.done ? 'done' : it.read ? 'read' : ''].filter(Boolean).join(' | ')}]`); if (it.body) process.stdout.write(it.body.endsWith('\n') ? it.body : it.body + '\n'); } const outDir = () => { const d = resolve(flags.out || process.env.RELAY_DOWNLOAD_DIR || join(tmpdir(), 'igneum-relay')); mkdirSync(d, { recursive: true }); return d; }; async function download(it) { const r = await fetch(`${API}file?id=${it.id}`, { headers: { 'x-igneum-key': KEY }, redirect: 'follow' }); if (!r.ok) throw new Error(`download http ${r.status}`); const buf = Buffer.from(await r.arrayBuffer()); const p = join(outDir(), `${it.id}-${it.file_name || 'file'}`); writeFileSync(p, buf); return p; } function playbook(path) { let s = readFileSync(path, 'utf8'); s = s.replace(/__DL_BASE__/g, DL ? `https://dl.igneum.network/dl/${DL}` : 'https://dl.igneum.network/dl/MISSING-DL-TOKEN'); return s; } try { if (cmd === 'url') { console.log(WEB); } else if (cmd === 'list') { const n = Number(pos[1]) || 50; const q = { limit: n }; if (flags.machine) q.machine = flags.machine; const j = await api('feed', { q }); const waiting = j.machines.filter(m => m.unread).map(m => `${m.name} ${m.unread}`).join(', '); console.log(`${SHOWN}\nmachines: ${j.machines.map(m => `${m.name}${m.role ? '/' + m.role : ''}${m.named === false ? ' (unnamed, hostname ' + m.hostname + ')' : ''}${m.last_seen ? ' seen ' + when(m.last_seen) : ''}`).join(' | ')}${waiting ? `\nwaiting: ${waiting}` : ''}`); if (!j.items.length) console.log('no items yet'); for (const it of j.items) console.log(line(it)); } else if (cmd === 'read') { const it = (await api('item', { q: { id: pos[1] } })).item; printItem(it); if (it.has_file) console.log(`file: ${await download(it)}`); } else if (cmd === 'drop') { const what = pos[1]; if (!what) throw new Error('drop "" or drop '); const o = { from: flags.from || 'Mac', to: flags.to || 'all', title: flags.title || '' }; if (existsSync(what) && statSync(what).isFile()) Object.assign(o, await uploadFile(what), { kind: 'file', body: typeof flags.body === 'string' ? flags.body : '' }); else { o.body = what; o.kind = flags.kind || 'text'; } const r = await api('drop', { body: o }); console.log(`sent #${r.id} (${r.kind})`); } else if (cmd === 'task' || cmd === 'run') { const [, to, title, file] = pos; if (!to || !title) throw new Error(`${cmd} "title" ${cmd === 'run' ? '