Files
pearcord-agentctl/bin/agentctl.cjs
T
Raven ScottandCursor b0ea11ec86 Extend headless agentctl with waitEvent and document Phase 295 scenarios.
HeadlessSession supports waitEvent steps and optional hyperswarm DHT; README lists leave-voice-noop and settings-deep-link scenarios with smoke commands.

Co-authored-by: Cursor <[email protected]>
2026-05-23 06:40:26 -04:00

185 lines
5.7 KiB
JavaScript
Executable File

#!/usr/bin/env bare
'use strict'
const fs = require('bare-fs')
const path = require('bare-path')
const {
AgentClient,
HeadlessSession,
PearcordMeshCluster,
resolveAgentctlAddress
} = require('..')
function usage () {
console.log(`pearcord-agentctl — control a running Pearcord worker or run headless IPC
Usage:
agentctl ping [--host HOST] [--port PORT]
agentctl view [--light] [--host HOST] [--port PORT]
agentctl ipc '<json>' [--host HOST] [--port PORT]
agentctl wait '<match-json>' [--timeout MS] [--host HOST] [--port PORT]
agentctl wait-event '<match-json>' [--timeout MS] [--since MS]
agentctl log-tail [--lines N] [--host HOST] [--port PORT]
agentctl run <scenario.json> [--timeout MS] [--host HOST] [--port PORT]
agentctl headless run <scenario.json>
agentctl headless mesh run <scenario.json> [--peers N]
agentctl headless ipc '<json>'
agentctl mesh run <scenario.json> [--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 }
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 === '-h' || a === '--help') out.help = true
else out._.push(a)
}
return out
}
function clientFromArgs (args) {
const def = resolveAgentctlAddress()
return new AgentClient({
host: args.host || def.host,
port: args.port || def.port
})
}
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 === 'headless') {
const sub = args._[1]
if (sub === 'mesh') {
const meshSub = args._[2]
if (meshSub !== 'run') throw new Error('use: headless mesh run <scenario.json>')
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 meshSub = args._[1]
if (meshSub !== 'run') throw new Error('use: mesh run <scenario.json>')
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 === 'ping') {
const res = await client.ping()
console.log(JSON.stringify(res, 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) => {
console.error(err?.message || err)
process.exit(1)
})