#!/usr/bin/env bare 'use strict' const { isBareRuntime } = require('../runtime') const fs = isBareRuntime() ? require('bare-fs') : require('fs') const path = isBareRuntime() ? require('bare-path') : require('path') const { resolveAgentctlAddress, resolveLogPath } = require('../resolve') function loadRuntimeClientApi () { const { AgentClient } = require('../client') return { AgentClient } } function loadRuntimeHeadlessApi () { const { HeadlessSession, PearcordMeshCluster } = require('..') return { HeadlessSession, PearcordMeshCluster } } function usage () { console.log(`pearcord-agentctl — control a running Pearcord worker or run headless IPC Usage: agentctl preflight agentctl doctor [--host HOST] [--port PORT] agentctl ping [--host HOST] [--port PORT] [--retries N] [--retry-delay MS] [--wait-listening] agentctl wait-ready [--host HOST] [--port PORT] [--timeout MS] agentctl view [--light] [--host HOST] [--port PORT] agentctl ipc '' [--host HOST] [--port PORT] agentctl wait '' [--timeout MS] [--host HOST] [--port PORT] agentctl wait-event '' [--timeout MS] [--since MS] agentctl log-tail [--lines N] [--host HOST] [--port PORT] agentctl run [--timeout MS] [--host HOST] [--port PORT] agentctl headless run agentctl headless mesh run [--peers N] agentctl headless ipc '' agentctl mesh run [--peers N] Environment: PEARCORD_AGENTCTL=1 Enable TCP server in apps/pearcord worker PEARCORD_AGENTCTL_HOST Default 127.0.0.1 PEARCORD_AGENTCTL_PORT Default 39482 PEARCORD_AGENTCTL_TOKEN Optional shared secret for TCP ops PEARCORD_STORAGE Storage root (~/.config/pearcord) Start Pearcord with agentctl: PEARCORD_AGENTCTL=1 pear run `) } function parseArgs (argv) { const out = { _: [], host: null, port: null, light: false, timeout: 15000, lines: 40, retries: 3, retryDelay: 500, waitListening: false } for (let i = 2; i < argv.length; i++) { const a = argv[i] if (a === '--host') out.host = argv[++i] else if (a === '--port') out.port = Number(argv[++i]) else if (a === '--light') out.light = true else if (a === '--timeout') out.timeout = Number(argv[++i]) else if (a === '--lines') out.lines = Number(argv[++i]) else if (a === '--retries') out.retries = Math.max(1, Number(argv[++i]) || 1) else if (a === '--retry-delay') out.retryDelay = Math.max(50, Number(argv[++i]) || 500) else if (a === '--wait-listening') out.waitListening = true else if (a === '-h' || a === '--help') out.help = true else out._.push(a) } return out } function clientFromArgs (args) { const { AgentClient } = loadRuntimeClientApi() const def = resolveAgentctlAddress() return new AgentClient({ host: args.host || def.host, port: args.port || def.port }) } function sleep (ms) { return new Promise((resolve) => setTimeout(resolve, ms)) } function classifyCliError (err) { const msg = String(err?.message || err || '') if (msg.includes('Cannot find module')) return 'EMODULE_MISSING' if (msg.includes('startup timeout')) return 'ESTARTUP_TIMEOUT' if (msg.includes('connect timeout')) return 'ECONN_TIMEOUT' if (msg.includes('request timeout')) return 'EREQUEST_TIMEOUT' if (msg.includes('ECONNREFUSED') || msg.includes('connection refused')) return 'ECONN_REFUSED' if (msg.includes('EHOSTUNREACH')) return 'EHOST_UNREACHABLE' return 'EUNKNOWN' } function remediationFor (code, details = {}) { if (code === 'EMODULE_MISSING') { return [ 'Install local module links first:', 'cd ../../modules/pearcord-platform && npm install', 'cd ../pearcord-agentctl && npm install' ] } if (code === 'ECONN_REFUSED') { const hints = [ 'Ensure Pearcord is running with agentctl enabled:', 'PEARCORD_AGENTCTL=1 pear run' ] if (!process.env.PEARCORD_AGENTCTL) { hints.push('Current shell does not set PEARCORD_AGENTCTL=1.') } hints.push(`Verify host/port (${details.host}:${details.port}) and retry with --host/--port if needed.`) return hints } if (code === 'ECONN_TIMEOUT' || code === 'EREQUEST_TIMEOUT') { return [ 'Worker startup is still in progress or blocked.', 'Retry with --wait-listening or increase --timeout.', 'Check logs with: agentctl log-tail --lines 120' ] } if (code === 'ESTARTUP_TIMEOUT') { return [ 'Agentctl server did not become reachable before timeout.', 'Launch worker with PEARCORD_AGENTCTL=1 pear run and retry wait-ready.', 'Check logs with: agentctl log-tail --lines 120' ] } if (code === 'EHOST_UNREACHABLE') { return ['The host is unreachable; verify --host value and local network config.'] } return ['Inspect pearcord.log and retry with --wait-listening.'] } function printCliError (err, details = {}) { const code = classifyCliError(err) const payload = { ok: false, errorCode: code, message: String(err?.message || err || 'unknown error'), hints: remediationFor(code, details) } console.error(JSON.stringify(payload, null, 2)) } function runPreflightChecks () { const required = [ 'pearcord-platform', 'pearcord-ui-flow', 'pearcord-log', 'pearcord-guild-sidecar' ] const results = required.map((name) => { try { require.resolve(name) return { name, ok: true } } catch (err) { return { name, ok: false, error: err?.message || String(err) } } }) const failed = results.filter((r) => !r.ok) const out = { ok: failed.length === 0, checks: results } if (failed.length) { out.errorCode = 'EMODULE_MISSING' out.hints = remediationFor('EMODULE_MISSING') } return out } function doctorFromArgs (args) { const def = resolveAgentctlAddress() const host = args.host || def.host const port = args.port || def.port const listeningEvidence = findListeningEvidence(host, port) return { ok: true, mode: process.env.PEARCORD_AGENTCTL === '1' ? 'attached-enabled' : 'attached-disabled', expectedRuntime: { host, port, tokenMode: process.env.PEARCORD_AGENTCTL_TOKEN ? 'required' : 'off' }, listeningEvidence, env: { PEARCORD_AGENTCTL: process.env.PEARCORD_AGENTCTL || null, PEARCORD_AGENTCTL_HOST: process.env.PEARCORD_AGENTCTL_HOST || null, PEARCORD_AGENTCTL_PORT: process.env.PEARCORD_AGENTCTL_PORT || null, PEARCORD_AGENTCTL_TOKEN: process.env.PEARCORD_AGENTCTL_TOKEN ? '[set]' : null } } } function findListeningEvidence (host, port) { const logPath = resolveLogPath() let text = '' try { text = fs.readFileSync(logPath, 'utf8') } catch { return { seen: false, logPath, reason: 'log-not-found' } } const lines = text.split('\n') for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i] if (!line || !line.includes('agentctl listening')) continue const hostMatch = line.includes(`"host":"${host}"`) || line.includes(`host":"${host}"`) const portMatch = line.includes(`"port":${port}`) || line.includes(`port":${port}`) return { seen: true, hostMatches: hostMatch, portMatches: portMatch, matchesExpected: !!hostMatch && !!portMatch, logPath, line: line.trim() } } return { seen: false, logPath, reason: 'no-agentctl-listening-line' } } async function pingWithRetry (client, args) { const attempts = args.waitListening ? Math.max(args.retries, 8) : args.retries let lastErr = null for (let i = 1; i <= attempts; i++) { try { const res = await client.ping() return { ...res, attempts: i } } catch (err) { lastErr = err if (i < attempts) await sleep(Math.min(args.retryDelay * i, 2000)) } } throw lastErr } async function main () { const args = parseArgs(process.argv) if (args.help || !args._.length) { usage() process.exit(args.help ? 0 : 1) } const cmd = args._[0] if (cmd === 'preflight') { const out = runPreflightChecks() console.log(JSON.stringify(out, null, 2)) if (!out.ok) process.exit(2) return } if (cmd === 'doctor') { console.log(JSON.stringify(doctorFromArgs(args), null, 2)) return } if (cmd === 'headless') { const { HeadlessSession, PearcordMeshCluster } = loadRuntimeHeadlessApi() const sub = args._[1] if (sub === 'mesh') { const meshSub = args._[2] if (meshSub !== 'run') throw new Error('use: headless mesh run ') const file = args._[3] if (!file) throw new Error('scenario.json path required') const cluster = new PearcordMeshCluster({ peerCount: 3 }) try { const result = await cluster.runScenario(path.resolve(file)) console.log(JSON.stringify({ ok: true, result }, null, 2)) } finally { await cluster.close() } return } const session = new HeadlessSession() await session.start() try { if (sub === 'run') { const file = args._[2] if (!file) throw new Error('scenario.json path required') const results = await session.runScenario(path.resolve(file)) console.log(JSON.stringify({ ok: true, steps: results.length, view: session.lastView }, null, 2)) return } if (sub === 'ipc') { const raw = args._[2] if (!raw) throw new Error('ipc json required') const payload = JSON.parse(raw) const view = await session.ipc(payload) console.log(JSON.stringify({ ok: true, view }, null, 2)) return } throw new Error(`unknown headless subcommand: ${sub}`) } finally { await session.close() } } if (cmd === 'mesh') { const { PearcordMeshCluster } = loadRuntimeHeadlessApi() const meshSub = args._[1] if (meshSub !== 'run') throw new Error('use: mesh run ') const file = args._[2] if (!file) throw new Error('scenario.json path required') const cluster = new PearcordMeshCluster({ peerCount: 3 }) try { const result = await cluster.runScenario(path.resolve(file)) console.log(JSON.stringify({ ok: true, result }, null, 2)) } finally { await cluster.close() } return } const client = clientFromArgs(args) try { if (cmd === 'wait-ready') { const deadline = Date.now() + Math.max(args.timeout, 1000) let attempts = 0 while (Date.now() < deadline) { attempts++ try { const res = await client.ping() console.log(JSON.stringify({ ok: true, ready: true, attempts, host: client.host, port: client.port, tokenMode: client.token ? 'required' : 'off', response: res }, null, 2)) return } catch { await sleep(400) } } throw new Error('agentctl startup timeout: worker did not become ready') } if (cmd === 'ping') { const res = await pingWithRetry(client, args) console.log(JSON.stringify({ ...res, host: client.host, port: client.port, tokenMode: client.token ? 'required' : 'off' }, null, 2)) return } if (cmd === 'view') { const view = await client.view(args.light) console.log(JSON.stringify({ ok: true, view }, null, 2)) return } if (cmd === 'ipc') { const raw = args._[1] if (!raw) throw new Error('ipc json required') const payload = JSON.parse(raw) const res = await client.ipc(payload) console.log(JSON.stringify(res, null, 2)) return } if (cmd === 'wait') { const raw = args._[1] if (!raw) throw new Error('match json required') const match = JSON.parse(raw) const view = await client.wait(match, args.timeout, args.light) console.log(JSON.stringify({ ok: true, view }, null, 2)) return } if (cmd === 'log-tail') { const res = await client.logTail(args.lines) console.log(JSON.stringify(res, null, 2)) return } if (cmd === 'wait-event') { const raw = args._[1] if (!raw) throw new Error('match json required') const match = JSON.parse(raw) const event = await client.waitEvent(match, args.timeout, 0) console.log(JSON.stringify({ ok: true, event }, null, 2)) return } if (cmd === 'run') { const file = args._[1] if (!file) throw new Error('scenario.json path required') const res = await client.run(path.resolve(file), args.timeout || 120000) console.log(JSON.stringify(res, null, 2)) return } throw new Error(`unknown command: ${cmd}`) } finally { client.close() } } main().catch((err) => { const argv = typeof process !== 'undefined' && Array.isArray(process.argv) ? process.argv : ['agentctl'] const args = parseArgs(argv) const def = resolveAgentctlAddress() printCliError(err, { host: args.host || def.host, port: args.port || def.port }) if (typeof process !== 'undefined' && typeof process.exit === 'function') process.exit(1) throw err })