feat(agent-queue): Node live dashboard + bytelyst-cli integration

- dashboard.mjs: zero-dep Node TUI (running workers w/ engine, elapsed,
  cwd, last log line + recent done/failed); 'dash' subcommand execs it
- bytelyst-cli.sh: 'agent-queue' / 'aq' passthrough handled before the
  GITHUB_TOKEN + jq/curl gates; usage + interactive-menu entry
- README: document dash + bytelyst-cli usage
This commit is contained in:
saravanakumardb1 2026-05-28 21:39:25 -07:00
parent 8f725f8587
commit 169e944c3c
4 changed files with 225 additions and 4 deletions

View File

@ -83,10 +83,20 @@ already have a `---` block.
| `add <file> [--engine E] [--cwd P] [--yolo\|--no-yolo]` | queue a prompt into `inbox/` | | `add <file> [--engine E] [--cwd P] [--yolo\|--no-yolo]` | queue a prompt into `inbox/` |
| `run [--max N] [--engine E] [--once]` | process the inbox (foreground loop) | | `run [--max N] [--engine E] [--once]` | process the inbox (foreground loop) |
| `status` | kanban counts + running-worker table | | `status` | kanban counts + running-worker table |
| `watch [interval]` | live `status`, redrawn every N seconds (default 2) | | `watch [interval]` | live `status` (bash), redrawn every N seconds (default 2) |
| `dash [--interval N]` | richer **Node** live dashboard — running workers (engine, elapsed, last log line) + recent done/failed |
| `stop` | kill running workers + the run loop | | `stop` | kill running workers + the run loop |
| `logs <job> [-f]` | print / follow a job's log | | `logs <job> [-f]` | print / follow a job's log |
## Via `bytelyst-cli.sh`
Wired into the repo's unified CLI (no GitHub token required for this subcommand):
```bash
./bytelyst-cli.sh agent-queue run --max 2 # full passthrough
./bytelyst-cli.sh aq status # short alias
```
## Folder layout ## Folder layout
``` ```

View File

@ -303,6 +303,11 @@ cmd_watch() {
while true; do clear; cmd_status; sleep "$interval"; done while true; do clear; cmd_status; sleep "$interval"; done
} }
cmd_dash() {
command -v node >/dev/null 2>&1 || die "node not found — use 'watch' for the bash status view"
AGENT_QUEUE_ROOT="$QUEUE_ROOT" exec node "$SCRIPT_DIR/dashboard.mjs" "$@"
}
cmd_stop() { cmd_stop() {
ensure_dirs ensure_dirs
local killed=0 f pid local killed=0 f pid
@ -342,7 +347,8 @@ ${C_BOLD}COMMANDS${C_RESET}
run [--max N] [--engine E] [--once] run [--max N] [--engine E] [--once]
process inbox/ (foreground loop; Ctrl-C to stop) process inbox/ (foreground loop; Ctrl-C to stop)
status show kanban counts + running workers status show kanban counts + running workers
watch [interval] live status (default 2s) watch [interval] live status (default 2s, bash)
dash [--interval N] richer live Node dashboard (recent done/failed too)
stop kill running workers + the run loop stop kill running workers + the run loop
logs <job> [-f] print (or follow) a job's log logs <job> [-f] print (or follow) a job's log
help this message help this message
@ -370,6 +376,7 @@ main() {
run) cmd_run "$@";; run) cmd_run "$@";;
status) cmd_status "$@";; status) cmd_status "$@";;
watch) cmd_watch "$@";; watch) cmd_watch "$@";;
dash|dashboard) cmd_dash "$@";;
stop) cmd_stop "$@";; stop) cmd_stop "$@";;
logs) cmd_logs "$@";; logs) cmd_logs "$@";;
help|-h|--help) usage;; help|-h|--help) usage;;

192
agent-queue/dashboard.mjs Normal file
View File

@ -0,0 +1,192 @@
#!/usr/bin/env node
// agent-queue dashboard — a zero-dependency live TUI for the folder queue.
//
// Reads the same queue/ state written by agent-queue.sh and re-renders a board
// every interval: kanban counts, running workers (engine, elapsed, last log line),
// and the most recent done/failed jobs.
//
// Usage: node dashboard.mjs [--interval 2] [--root /path/to/queue]
// AGENT_QUEUE_ROOT=/path node dashboard.mjs
// Quit: q or Ctrl-C
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// ── args / config ───────────────────────────────────────────────────
const argv = process.argv.slice(2);
const getArg = (flag, def) => {
const i = argv.indexOf(flag);
return i !== -1 && argv[i + 1] ? argv[i + 1] : def;
};
const ROOT = path.resolve(getArg('--root', process.env.AGENT_QUEUE_ROOT || path.join(__dirname, 'queue')));
const INTERVAL = Math.max(1, parseInt(getArg('--interval', '2'), 10)) * 1000;
const DIRS = {
inbox: path.join(ROOT, 'inbox'),
doing: path.join(ROOT, 'doing'),
done: path.join(ROOT, 'done'),
failed: path.join(ROOT, 'failed'),
logs: path.join(ROOT, 'logs'),
state: path.join(ROOT, '.state'),
};
// ── ansi ────────────────────────────────────────────────────────────
const C = {
reset: '\x1b[0m', dim: '\x1b[2m', bold: '\x1b[1m',
red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
blue: '\x1b[34m', cyan: '\x1b[36m', gray: '\x1b[90m',
};
const c = (col, s) => `${C[col]}${s}${C.reset}`;
// ── helpers ─────────────────────────────────────────────────────────
const listMd = (dir) => {
try { return fs.readdirSync(dir).filter((f) => f.endsWith('.md')); }
catch { return []; }
};
const count = (dir) => listMd(dir).length;
const parseMeta = (file) => {
const out = {};
try {
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
const i = line.indexOf('=');
if (i > 0) out[line.slice(0, i)] = line.slice(i + 1);
}
} catch { /* ignore */ }
return out;
};
const pidAlive = (pid) => {
if (!pid) return false;
try { process.kill(Number(pid), 0); return true; } catch { return false; }
};
const lastLogLine = (job) => {
try {
const txt = fs.readFileSync(path.join(DIRS.logs, `${job}.log`), 'utf8');
const lines = txt.split('\n').map((l) => l.trim()).filter(Boolean);
return lines.length ? lines[lines.length - 1] : '';
} catch { return ''; }
};
const fmtElapsed = (startSec) => {
if (!startSec) return ' -- ';
const s = Math.max(0, Math.floor(Date.now() / 1000) - Number(startSec));
const m = Math.floor(s / 60);
const h = Math.floor(m / 60);
if (h > 0) return `${h}h${String(m % 60).padStart(2, '0')}m`;
return `${m}m${String(s % 60).padStart(2, '0')}s`;
};
const trunc = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
const shortPath = (p) => (p || '').replace(process.env.HOME || '~', '~');
const readMetas = () => {
let files = [];
try { files = fs.readdirSync(DIRS.state).filter((f) => f.endsWith('.meta')); }
catch { /* ignore */ }
return files.map((f) => parseMeta(path.join(DIRS.state, f)));
};
// ── render ──────────────────────────────────────────────────────────
const ENGINE_COLOR = { devin: 'cyan', claude: 'yellow', codex: 'green' };
function render() {
const metas = readMetas();
const running = metas.filter((m) => !m.ended && pidAlive(m.pid));
const finished = metas
.filter((m) => m.ended)
.sort((a, b) => Number(b.ended) - Number(a.ended));
const counts = {
inbox: count(DIRS.inbox), doing: count(DIRS.doing),
done: count(DIRS.done), failed: count(DIRS.failed),
};
const out = [];
out.push('');
out.push(` ${C.bold}AGENT QUEUE${C.reset} ${c('gray', ROOT)}`);
out.push(` ${c('gray', new Date().toLocaleTimeString())} refresh ${INTERVAL / 1000}s ${c('gray', 'press q to quit')}`);
out.push('');
out.push(
` ${c('blue', '▢ inbox')} ${String(counts.inbox).padEnd(3)}` +
` ${c('yellow', '◧ doing')} ${String(counts.doing).padEnd(3)}` +
` ${c('green', '▣ done')} ${String(counts.done).padEnd(3)}` +
` ${c('red', '✕ failed')} ${String(counts.failed).padEnd(3)}` +
` ${C.bold}running ${running.length}${C.reset}`
);
out.push('');
// running table
out.push(` ${C.bold}RUNNING${C.reset}`);
if (running.length === 0) {
out.push(` ${c('dim', 'no workers running')}`);
} else {
for (const m of running) {
const eng = m.engine || '?';
const engC = ENGINE_COLOR[eng] || 'gray';
const line =
` ${c('bold', trunc(m.job || '?', 30).padEnd(30))} ` +
`${c(engC, eng.padEnd(7))} ` +
`${fmtElapsed(m.started).padStart(7)} ` +
`${c('gray', 'pid ' + (m.pid || '?'))}`;
out.push(line);
out.push(` ${c('dim', trunc(shortPath(m.cwd || ''), 70))}`);
const last = lastLogLine(m.job);
if (last) out.push(` ${c('cyan', ' ')}${c('dim', trunc(last, 70))}`);
}
}
out.push('');
// recent finished
out.push(` ${C.bold}RECENT${C.reset}`);
const recent = finished.slice(0, 6);
if (recent.length === 0) {
out.push(` ${c('dim', 'nothing finished yet')}`);
} else {
for (const m of recent) {
const ok = m.result === 'done';
const mark = ok ? c('green', '▣') : c('red', '✕');
const when = m.ended ? new Date(Number(m.ended) * 1000).toLocaleTimeString() : '';
out.push(
` ${mark} ${trunc(m.job || '?', 34).padEnd(34)} ` +
`${c('gray', (m.engine || '').padEnd(7))} ` +
`${ok ? c('green', 'done') : c('red', 'failed rc=' + (m.exit || '?'))} ${c('gray', when)}`
);
}
}
out.push('');
// clear + paint
process.stdout.write('\x1b[2J\x1b[H' + out.join('\n') + '\n');
}
// ── main loop + key handling ────────────────────────────────────────
if (!fs.existsSync(ROOT)) {
process.stdout.write(`agent-queue: queue root not found: ${ROOT}\nRun \`agent-queue.sh init\` first.\n`);
process.exit(1);
}
render();
const timer = setInterval(render, INTERVAL);
const quit = () => {
clearInterval(timer);
try { if (process.stdin.isTTY) process.stdin.setRawMode(false); } catch { /* noop */ }
process.stdout.write(C.reset + '\n');
process.exit(0);
};
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.on('data', (key) => {
if (key === 'q' || key === '\u0003') quit(); // q or Ctrl-C
});
}
process.on('SIGINT', quit);
process.on('SIGTERM', quit);

View File

@ -16,6 +16,15 @@ YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4) BLUE=$(tput setaf 4)
RESET=$(tput sgr0) RESET=$(tput sgr0)
CLI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# agent-queue delegates to the standalone tool (no GitHub token / jq / curl needed),
# so handle it BEFORE the GITHUB_TOKEN + required-tools gates below.
if [[ "${1:-}" == "agent-queue" || "${1:-}" == "aq" ]]; then
shift
exec "$CLI_DIR/agent-queue/agent-queue.sh" "$@"
fi
REQUIRED_TOOLS=(jq curl) REQUIRED_TOOLS=(jq curl)
# Check for required tools # Check for required tools
@ -47,6 +56,7 @@ usage() {
echo " check-collaborators --input <input.json>" echo " check-collaborators --input <input.json>"
echo " export --type <repos|users> --output <file.json>" echo " export --type <repos|users> --output <file.json>"
echo " remove-user-from-all-repos --user <username> [--input <file.json>]" echo " remove-user-from-all-repos --user <username> [--input <file.json>]"
echo " agent-queue (aq) <init|add|run|status|watch|stop|logs> — agent prompt queue runner"
echo " help Show this help message" echo " help Show this help message"
echo "" echo ""
echo "If no command is given, an interactive menu will be shown." echo "If no command is given, an interactive menu will be shown."
@ -209,14 +219,15 @@ remove_user_from_all_repos() {
interactive_menu() { interactive_menu() {
echo "${BLUE}Bytelyst CLI Interactive Menu${RESET}" echo "${BLUE}Bytelyst CLI Interactive Menu${RESET}"
select opt in "List Public Repos" "List Private Repos" "Check Collaborators" "Export JSON" "Remove User from All Repos" "Exit"; do select opt in "List Public Repos" "List Private Repos" "Check Collaborators" "Export JSON" "Remove User from All Repos" "Agent Queue Status" "Exit"; do
case $REPLY in case $REPLY in
1) read -p "Enter GitHub username: " user; list_public_repos --user "$user";; 1) read -p "Enter GitHub username: " user; list_public_repos --user "$user";;
2) read -p "Enter GitHub org: " org; list_private_repos --org "$org";; 2) read -p "Enter GitHub org: " org; list_private_repos --org "$org";;
3) read -p "Enter path to input.json: " input; check_collaborators --input "$input";; 3) read -p "Enter path to input.json: " input; check_collaborators --input "$input";;
4) read -p "Export type (repos/users): " type; read -p "Output file: " output; export_json --type "$type" --output "$output";; 4) read -p "Export type (repos/users): " type; read -p "Output file: " output; export_json --type "$type" --output "$output";;
5) read -p "Enter GitHub username: " user; remove_user_from_all_repos --user "$user";; 5) read -p "Enter GitHub username: " user; remove_user_from_all_repos --user "$user";;
6) exit 0;; 6) "$CLI_DIR/agent-queue/agent-queue.sh" status;;
7) exit 0;;
*) echo "Invalid option.";; *) echo "Invalid option.";;
esac esac
done done
@ -234,6 +245,7 @@ case $1 in
check-collaborators) shift; check_collaborators "$@";; check-collaborators) shift; check_collaborators "$@";;
export) shift; export_json "$@";; export) shift; export_json "$@";;
remove-user-from-all-repos) shift; remove_user_from_all_repos "$@";; remove-user-from-all-repos) shift; remove_user_from_all_repos "$@";;
agent-queue|aq) shift; exec "$CLI_DIR/agent-queue/agent-queue.sh" "$@";;
help|--help|-h) usage;; help|--help|-h) usage;;
*) echo "${RED}Unknown command: $1${RESET}"; usage; exit 1;; *) echo "${RED}Unknown command: $1${RESET}"; usage; exit 1;;
esac esac