mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 17:27:37 +08:00
Broad drift audit against origin/main (b52b63396).
Reference pages (most user-visible drift):
- slash-commands: add /busy, /curator, /footer, /indicator, /redraw, /steer
that were missing; drop non-existent /terminal-setup; fix /q footnote
(resolves to /queue, not /quit); extend CLI-only list with all 24
CLI-only commands in the registry
- cli-commands: add dedicated sections for hermes curator / fallback /
hooks (new subcommands not previously documented); remove stale
hermes honcho standalone section (the plugin registers dynamically
via hermes memory); list curator/fallback/hooks in top-level table;
fix completion to include fish
- toolsets-reference: document the real 52-toolset count; split browser
vs browser-cdp; add discord / discord_admin / spotify / yuanbao;
correct hermes-cli tool count from 36 to 38; fix misleading claim
that hermes-homeassistant adds tools (it's identical to hermes-cli)
- tools-reference: bump tool count 55 -> 68; add 7 Spotify, 5 Yuanbao,
2 Discord toolsets; move browser_cdp/browser_dialog to their own
browser-cdp toolset section
- environment-variables: add 40+ user-facing HERMES_* vars that were
undocumented (--yolo, --accept-hooks, --ignore-*, inference model
override, agent/stream/checkpoint timeouts, OAuth trace, per-platform
batch tuning for Telegram/Discord/Matrix/Feishu/WeCom, cron knobs,
gateway restart/connect timeouts); dedupe the Cron Scheduler section;
replace stale QQ_SANDBOX with QQ_PORTAL_HOST
User-guide (top level):
- cli.md: compression preserves last 20 turns, not 4 (protect_last_n: 20)
- configuration.md: display.platforms is the canonical per-platform
override key; tool_progress_overrides is deprecated and auto-migrated
- profiles.md: model.default is the config key, not model.model
- sessions.md: CLI/TUI session IDs use 6-char hex, gateway uses 8
- checkpoints-and-rollback.md: destructive-command list now matches
_DESTRUCTIVE_PATTERNS (adds rmdir, cp, install, dd)
- docker.md: the container runs as non-root hermes (UID 10000) via
gosu; fix install command (uv pip); add missing --insecure on the
dashboard compose example (required for non-loopback bind)
- security.md: systemctl danger pattern also matches 'restart'
- index.md: built-in tool count 47 -> 68
- integrations/index.md: 6 STT providers, 8 memory providers
- integrations/providers.md: drop fictional dashscope/qwen aliases
Features:
- overview.md: 9 image models (not 8), 9 TTS providers (not 5),
8 memory providers (Supermemory was missing)
- tool-gateway.md: 9 image models
- tools.md: extend common-toolsets list with search / messaging /
spotify / discord / debugging / safe
- fallback-providers.md: add 6 real providers from PROVIDER_REGISTRY
(lmstudio, kimi-coding-cn, stepfun, alibaba-coding-plan,
tencent-tokenhub, azure-foundry)
- plugins.md: Available Hooks table now includes on_session_finalize,
on_session_reset, subagent_stop
- built-in-plugins.md: add the 7 bundled plugins the page didn't
mention (spotify, google_meet, three image_gen providers, two
dashboard examples)
- web-dashboard.md: add --insecure and --tui flags
- cron.md: hermes cron create takes positional schedule/prompt, not
flags
Messaging:
- telegram.md: TELEGRAM_WEBHOOK_SECRET is now REQUIRED when
TELEGRAM_WEBHOOK_URL is set (gateway refuses to start without it
per GHSA-3vpc-7q5r-276h). Biggest user-visible drift in the batch.
- discord.md: HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDS default
is 2.0, not 0.1
- dingtalk.md: document DINGTALK_REQUIRE_MENTION /
FREE_RESPONSE_CHATS / MENTION_PATTERNS / HOME_CHANNEL /
ALLOW_ALL_USERS that the adapter supports
- bluebubbles.md: drop fictional BLUEBUBBLES_SEND_READ_RECEIPTS env
var; the setting lives in platforms.bluebubbles.extra only
- qqbot.md: drop dead QQ_SANDBOX; add real QQ_PORTAL_HOST and
QQ_GROUP_ALLOWED_USERS
- wecom-callback.md: replace 'hermes gateway start' (service-only)
with 'hermes gateway' for first-time setup
Developer-guide:
- architecture.md: refresh tool/toolset counts (61/52), terminal
backend count (7), line counts for run_agent.py (~13.7k), cli.py
(~11.5k), main.py (~10.4k), setup.py (~3.5k), gateway/run.py
(~12.2k), mcp_tool.py (~3.1k); add yuanbao adapter, bump platform
adapter count 18 -> 20
- agent-loop.md: run_agent.py line count 10.7k -> 13.7k
- tools-runtime.md: add vercel_sandbox backend
- adding-tools.md: remove stale 'Discovery import added to
model_tools.py' checklist item (registry auto-discovery)
- adding-platform-adapters.md: mark send_typing / get_chat_info as
concrete base methods; only connect/disconnect/send are abstract
- acp-internals.md: ACP sessions now persist to SessionDB
(~/.hermes/state.db); acp.run_agent call uses
use_unstable_protocol=True
- cron-internals.md: gateway runs scheduler in a dedicated background
thread via _start_cron_ticker, not on a maintenance cycle; locking
is cross-process via fcntl.flock (Unix) / msvcrt.locking (Windows)
- gateway-internals.md: gateway/run.py ~12k lines
- provider-runtime.md: cron DOES support fallback (run_job reads
fallback_providers from config)
- session-storage.md: SCHEMA_VERSION = 11 (not 9); add migrations
10 and 11 (trigram FTS, inline-mode FTS5 re-index); add
api_call_count column to Sessions DDL; document messages_fts_trigram
and state_meta in the architecture tree
- context-compression-and-caching.md: remove the obsolete 'context
pressure warnings' section (warnings were removed for causing
models to give up early)
- context-engine-plugin.md: compress() signature now includes
focus_topic param
- extending-the-cli.md: _build_tui_layout_children signature now
includes model_picker_widget; add to default layout
Also fixed three pre-existing broken links/anchors the build warned
about (docker.md -> api-server.md, yuanbao.md -> cron-jobs.md and
tips#background-tasks, nix-setup.md -> #container-aware-cli).
Regenerated per-skill pages via website/scripts/generate-skill-docs.py
so catalog tables and sidebar are consistent with current SKILL.md
frontmatter.
docusaurus build: clean, no broken links or anchors.
337 lines
12 KiB
Markdown
337 lines
12 KiB
Markdown
---
|
|
title: "Node Inspect Debugger — Debug Node"
|
|
sidebar_label: "Node Inspect Debugger"
|
|
description: "Debug Node"
|
|
---
|
|
|
|
{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */}
|
|
|
|
# Node Inspect Debugger
|
|
|
|
Debug Node.js via --inspect + Chrome DevTools Protocol CLI.
|
|
|
|
## Skill metadata
|
|
|
|
| | |
|
|
|---|---|
|
|
| Source | Bundled (installed by default) |
|
|
| Path | `skills/software-development/node-inspect-debugger` |
|
|
| Version | `1.0.0` |
|
|
| Author | Hermes Agent |
|
|
| License | MIT |
|
|
| Tags | `debugging`, `nodejs`, `node-inspect`, `cdp`, `breakpoints`, `ui-tui` |
|
|
| Related skills | [`systematic-debugging`](/docs/user-guide/skills/bundled/software-development/software-development-systematic-debugging), [`python-debugpy`](/docs/user-guide/skills/bundled/software-development/software-development-python-debugpy), [`debugging-hermes-tui-commands`](/docs/user-guide/skills/bundled/software-development/software-development-debugging-hermes-tui-commands) |
|
|
|
|
## Reference: full SKILL.md
|
|
|
|
:::info
|
|
The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active.
|
|
:::
|
|
|
|
# Node.js Inspect Debugger
|
|
|
|
## Overview
|
|
|
|
When `console.log` isn't enough, drive Node's built-in V8 inspector programmatically from the terminal. You get real breakpoints, step in/over/out, call-stack walking, local/closure scope dumps, and arbitrary expression evaluation in the paused frame.
|
|
|
|
Two tools, pick one:
|
|
|
|
- **`node inspect`** — built-in, zero install, CLI REPL. Best for quick poking.
|
|
- **`ndb` / CDP via `chrome-remote-interface`** — scriptable from Node/Python; best when you want to automate many breakpoints, collect state across runs, or debug non-interactively from an agent loop.
|
|
|
|
**Prefer `node inspect` first.** It's always available and the REPL is fast.
|
|
|
|
## When to Use
|
|
|
|
- A Node test fails and you need to see intermediate state
|
|
- ui-tui crashes or behaves wrong and you want to inspect React/Ink state pre-render
|
|
- tui_gateway child processes (`_SlashWorker`, PTY bridge workers) misbehave
|
|
- You need to inspect a value in a closure that `console.log` can't reach without patching
|
|
- Perf: attach to a running process to capture a CPU profile or heap snapshot
|
|
|
|
**Don't use for:** things `console.log` solves in under a minute. Breakpoint-driven debugging is heavier; use it when the payoff is real.
|
|
|
|
## Quick Reference: `node inspect` REPL
|
|
|
|
Launch paused on first line:
|
|
|
|
```bash
|
|
node inspect path/to/script.js
|
|
# or with tsx
|
|
node --inspect-brk $(which tsx) path/to/script.ts
|
|
```
|
|
|
|
The `debug>` prompt accepts:
|
|
|
|
| Command | Action |
|
|
|---|---|
|
|
| `c` or `cont` | continue |
|
|
| `n` or `next` | step over |
|
|
| `s` or `step` | step into |
|
|
| `o` or `out` | step out |
|
|
| `pause` | pause running code |
|
|
| `sb('file.js', 42)` | set breakpoint at file.js line 42 |
|
|
| `sb(42)` | set breakpoint at line 42 of current file |
|
|
| `sb('functionName')` | break when function is called |
|
|
| `cb('file.js', 42)` | clear breakpoint |
|
|
| `breakpoints` | list all breakpoints |
|
|
| `bt` | backtrace (call stack) |
|
|
| `list(5)` | show 5 lines of source around current position |
|
|
| `watch('expr')` | evaluate expr on every pause |
|
|
| `watchers` | show watched expressions |
|
|
| `repl` | drop into REPL in current scope (Ctrl+C to exit REPL) |
|
|
| `exec expr` | evaluate expression once |
|
|
| `restart` | restart script |
|
|
| `kill` | kill the script |
|
|
| `.exit` | quit debugger |
|
|
|
|
**In the `repl` sub-mode:** type any JS expression, including access to locals/closure variables. `Ctrl+C` exits back to `debug>`.
|
|
|
|
## Attaching to a Running Process
|
|
|
|
When the process is already running (e.g. a long-lived dev server or the TUI gateway):
|
|
|
|
```bash
|
|
# 1. Send SIGUSR1 to enable the inspector on an existing process
|
|
kill -SIGUSR1 <pid>
|
|
# Node prints: Debugger listening on ws://127.0.0.1:9229/<uuid>
|
|
|
|
# 2. Attach the debugger CLI
|
|
node inspect -p <pid>
|
|
# or by URL
|
|
node inspect ws://127.0.0.1:9229/<uuid>
|
|
```
|
|
|
|
To start a process with the inspector from the beginning:
|
|
|
|
```bash
|
|
node --inspect script.js # listen on 127.0.0.1:9229, keep running
|
|
node --inspect-brk script.js # listen AND pause on first line
|
|
node --inspect=0.0.0.0:9230 script.js # custom host:port
|
|
```
|
|
|
|
For TypeScript via tsx:
|
|
|
|
```bash
|
|
node --inspect-brk --import tsx script.ts
|
|
# or older tsx
|
|
node --inspect-brk -r tsx/cjs script.ts
|
|
```
|
|
|
|
## Programmatic CDP (scripting from terminal)
|
|
|
|
When you want to automate — set many breakpoints, capture scope state, script a repro — use `chrome-remote-interface`:
|
|
|
|
```bash
|
|
npm i -g chrome-remote-interface # or project-local
|
|
# Start your target:
|
|
node --inspect-brk=9229 target.js &
|
|
```
|
|
|
|
Driver script (save as `/tmp/cdp-debug.js`):
|
|
|
|
```javascript
|
|
const CDP = require('chrome-remote-interface');
|
|
|
|
(async () => {
|
|
const client = await CDP({ port: 9229 });
|
|
const { Debugger, Runtime } = client;
|
|
|
|
Debugger.paused(async ({ callFrames, reason }) => {
|
|
const top = callFrames[0];
|
|
console.log(`PAUSED: ${reason} @ ${top.url}:${top.location.lineNumber + 1}`);
|
|
|
|
// Walk scopes for locals
|
|
for (const scope of top.scopeChain) {
|
|
if (scope.type === 'local' || scope.type === 'closure') {
|
|
const { result } = await Runtime.getProperties({
|
|
objectId: scope.object.objectId,
|
|
ownProperties: true,
|
|
});
|
|
for (const p of result) {
|
|
console.log(` ${scope.type}.${p.name} =`, p.value?.value ?? p.value?.description);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Evaluate an expression in the paused frame
|
|
const { result } = await Debugger.evaluateOnCallFrame({
|
|
callFrameId: top.callFrameId,
|
|
expression: 'typeof state !== "undefined" ? JSON.stringify(state) : "n/a"',
|
|
});
|
|
console.log('state =', result.value ?? result.description);
|
|
|
|
await Debugger.resume();
|
|
});
|
|
|
|
await Runtime.enable();
|
|
await Debugger.enable();
|
|
|
|
// Set a breakpoint by URL regex + line
|
|
await Debugger.setBreakpointByUrl({
|
|
urlRegex: '.*app\\.tsx$',
|
|
lineNumber: 119, // 0-indexed
|
|
columnNumber: 0,
|
|
});
|
|
|
|
await Runtime.runIfWaitingForDebugger();
|
|
})();
|
|
```
|
|
|
|
Run it:
|
|
|
|
```bash
|
|
node /tmp/cdp-debug.js
|
|
```
|
|
|
|
Hermes-specific note: `chrome-remote-interface` is NOT in `ui-tui/package.json`. Install it to a throwaway location if you don't want to dirty the project:
|
|
|
|
```bash
|
|
mkdir -p /tmp/cdp-tools && cd /tmp/cdp-tools && npm i chrome-remote-interface
|
|
NODE_PATH=/tmp/cdp-tools/node_modules node /tmp/cdp-debug.js
|
|
```
|
|
|
|
## Debugging Hermes ui-tui
|
|
|
|
The TUI is built Ink + tsx. Two common scenarios:
|
|
|
|
### Debugging a single Ink component under dev
|
|
|
|
`ui-tui/package.json` has `npm run dev` (tsx --watch). Add `--inspect-brk` by running tsx directly:
|
|
|
|
```bash
|
|
cd /home/bb/hermes-agent/ui-tui
|
|
npm run build # produce dist/ once so transpile isn't needed on first load
|
|
node --inspect-brk dist/entry.js
|
|
# In another terminal:
|
|
node inspect -p <node pid>
|
|
```
|
|
|
|
Then inside `debug>`:
|
|
|
|
```
|
|
sb('dist/app.js', 220) # or wherever the suspect render is
|
|
cont
|
|
```
|
|
|
|
When it pauses, `repl` → inspect `props`, state refs, `useInput` handler values, etc.
|
|
|
|
### Debugging a running `hermes --tui`
|
|
|
|
The TUI spawns Node from the Python CLI. Easiest path:
|
|
|
|
```bash
|
|
# 1. Launch TUI
|
|
hermes --tui &
|
|
TUI_PID=$(pgrep -f 'ui-tui/dist/entry' | head -1)
|
|
|
|
# 2. Enable inspector on that Node PID
|
|
kill -SIGUSR1 "$TUI_PID"
|
|
|
|
# 3. Find the WS URL
|
|
curl -s http://127.0.0.1:9229/json/list | jq -r '.[0].webSocketDebuggerUrl'
|
|
|
|
# 4. Attach
|
|
node inspect ws://127.0.0.1:9229/<uuid>
|
|
```
|
|
|
|
Interacting with the TUI (typing in its window) continues to advance execution; your debugger can pause it on a breakpoint at any `sb(...)`.
|
|
|
|
### Debugging `_SlashWorker` / PTY child processes
|
|
|
|
Those are Python, not Node — use the `python-debugpy` skill for them. Only Node portions (Ink UI, tui_gateway client, tsx-run tests under `ui-tui/`) use this skill.
|
|
|
|
## Running Vitest Tests Under the Debugger
|
|
|
|
```bash
|
|
cd /home/bb/hermes-agent/ui-tui
|
|
# Run a single test file paused on entry
|
|
node --inspect-brk ./node_modules/vitest/vitest.mjs run --no-file-parallelism src/app/foo.test.tsx
|
|
```
|
|
|
|
In another terminal: `node inspect -p <pid>`, then `sb('src/app/foo.tsx', 42)`, `cont`.
|
|
|
|
Use `--no-file-parallelism` (vitest) or `--runInBand` (jest) so only one worker exists — debugging a pool is painful.
|
|
|
|
## Heap Snapshots & CPU Profiles (Non-interactive)
|
|
|
|
From the CDP driver above, swap Debugger for `HeapProfiler` / `Profiler`:
|
|
|
|
```javascript
|
|
// CPU profile for 5 seconds
|
|
await client.Profiler.enable();
|
|
await client.Profiler.start();
|
|
await new Promise(r => setTimeout(r, 5000));
|
|
const { profile } = await client.Profiler.stop();
|
|
require('fs').writeFileSync('/tmp/cpu.cpuprofile', JSON.stringify(profile));
|
|
// Open /tmp/cpu.cpuprofile in Chrome DevTools → Performance tab
|
|
```
|
|
|
|
```javascript
|
|
// Heap snapshot
|
|
await client.HeapProfiler.enable();
|
|
const chunks = [];
|
|
client.HeapProfiler.addHeapSnapshotChunk(({ chunk }) => chunks.push(chunk));
|
|
await client.HeapProfiler.takeHeapSnapshot({ reportProgress: false });
|
|
require('fs').writeFileSync('/tmp/heap.heapsnapshot', chunks.join(''));
|
|
```
|
|
|
|
## Common Pitfalls
|
|
|
|
1. **Wrong line numbers in TS source.** Breakpoints hit the emitted JS, not the `.ts`. Either (a) break in the built `dist/*.js`, or (b) enable sourcemaps (`node --enable-source-maps`) and use `sb('src/app.tsx', N)` — but only with CDP clients that follow sourcemaps. `node inspect` CLI does not.
|
|
|
|
2. **`--inspect` vs `--inspect-brk`.** `--inspect` starts the inspector but doesn't pause; your script races past your first breakpoint if you attach too late. Use `--inspect-brk` when you need to set breakpoints before any code runs.
|
|
|
|
3. **Port collisions.** Default is `9229`. If multiple Node processes are inspecting, pass `--inspect=0` (random port) and read the actual URL from `/json/list`:
|
|
```bash
|
|
curl -s http://127.0.0.1:9229/json/list # lists all inspectable targets on the host
|
|
```
|
|
|
|
4. **Child processes.** `--inspect` on a parent does NOT inspect its children. Use `NODE_OPTIONS='--inspect-brk' node parent.js` to propagate to every child; be aware they all need unique ports (Node auto-increments when `NODE_OPTIONS='--inspect'` is inherited).
|
|
|
|
5. **Background kills.** If you `Ctrl+C` out of `node inspect` while the target is paused, the target stays paused. Either `cont` first, or `kill` the target explicitly.
|
|
|
|
6. **Running `node inspect` through an agent terminal.** It's a PTY-friendly REPL. In Hermes, launch it with `terminal(pty=true)` or `background=true` + `process(action='submit', data='...')`. Non-PTY foreground mode will work for one-shot commands but not for interactive stepping.
|
|
|
|
7. **Security.** `--inspect=0.0.0.0:9229` exposes arbitrary code execution. Always bind to `127.0.0.1` (the default) unless you have an isolated network.
|
|
|
|
## Verification Checklist
|
|
|
|
After setting up a debug session, verify:
|
|
|
|
- [ ] `curl -s http://127.0.0.1:9229/json/list` returns exactly the target you expect
|
|
- [ ] First breakpoint actually hits (if it doesn't, you likely missed `--inspect-brk` or attached after execution completed)
|
|
- [ ] Source listing at pause shows the right file (mismatch = sourcemap issue, see pitfall 1)
|
|
- [ ] `exec process.pid` in `repl` returns the PID you meant to attach to
|
|
|
|
## One-Shot Recipes
|
|
|
|
**"Why is this variable undefined at line X?"**
|
|
```bash
|
|
node --inspect-brk script.js &
|
|
node inspect -p $!
|
|
# debug>
|
|
sb('script.js', X)
|
|
cont
|
|
# paused. Now:
|
|
repl
|
|
> myVariable
|
|
> Object.keys(this)
|
|
```
|
|
|
|
**"What's the call path into this function?"**
|
|
```
|
|
debug> sb('suspectFn')
|
|
debug> cont
|
|
# paused on entry
|
|
debug> bt
|
|
```
|
|
|
|
**"This async chain hangs — where?"**
|
|
```
|
|
# Start with --inspect (no -brk), let it run to the hang, then:
|
|
debug> pause
|
|
debug> bt
|
|
# Now you see the stuck frame
|
|
```
|