diff --git a/.gitignore b/.gitignore
index 2b6bc1871..dc69ede57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,6 +98,7 @@
!/QGIS/
!/n8n/
!/obsidian/
+!/unrealinsights/
# Step 5: Inside each software dir, ignore everything (including dotfiles)
/gimp/*
@@ -180,6 +181,8 @@
/n8n/.*
/obsidian/*
/obsidian/.*
+/unrealinsights/*
+/unrealinsights/.*
# Step 6: ...except agent-harness/
!/gimp/agent-harness/
@@ -228,6 +231,7 @@
!/obsidian/agent-harness/
!/safari/
!/safari/agent-harness/
+!/unrealinsights/agent-harness/
# Step 7: Ignore build artifacts within allowed dirs
**/__pycache__/
diff --git a/README.md b/README.md
index a7838e0c9..c91b8870a 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Software
-
+
@@ -51,6 +51,8 @@ CLI-Anything: Bridging the Gap Between AI Agents and the World's Softwareโ
40
+| ๐ Unreal Insights |
+Performance Profiling |
+cli-anything-unrealinsights |
+Background trace sessions + engine-matched UnrealInsights build + headless export |
+โ
50 |
+
+
| โ๏ธ CloudAnalyzer |
Point cloud / trajectory QA |
cli-anything-cloudanalyzer |
@@ -988,11 +997,11 @@ Each application received complete, production-ready CLI interfaces โ not demo
| Total |
-โ
2,152 |
+โ
2,202 |
-> **100% pass rate** across all 2,152 tests โ 1,564 unit tests + 569 end-to-end tests + 19 Node.js tests.
+> **100% pass rate** across all 2,202 tests โ 1,613 unit tests + 570 end-to-end tests + 19 Node.js tests.
---
@@ -1031,9 +1040,10 @@ sketch 19 passed โ
(19 jest, Node.js)
renderdoc 59 passed โ
(45 unit + 14 e2e)
cloudcompare 88 passed โ
(49 unit + 39 e2e)
openscreen 101 passed โ
(78 unit + 23 e2e)
+unrealinsights 50 passed โ
(49 unit + 1 e2e, 9 backend-gated e2e skipped)
cloudanalyzer 14 passed โ
(7 unit + 7 e2e)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-TOTAL 2,120 passed โ
100% pass rate
+TOTAL 2,202 passed โ
100% pass rate
```
---
@@ -1107,6 +1117,7 @@ cli-anything/
โโโ ๐ฎ godot/agent-harness/ # Godot Engine CLI (24 tests)
โโโ ๐จ sketch/agent-harness/ # Sketch CLI (19 tests, Node.js)
โโโ ๐ฌ renderdoc/agent-harness/ # RenderDoc CLI (59 tests)
+โโโ ๐ unrealinsights/agent-harness/ # Unreal Insights CLI (50 tests)
โโโ ๐ฌ videocaptioner/agent-harness/ # VideoCaptioner CLI (26 tests)
โโโ ๐ฌ openscreen/agent-harness/ # Openscreen CLI โ screen recording editor (101 tests)
โโโ โ๏ธ cloudcompare/agent-harness/ # CloudCompare CLI (88 tests)
@@ -1337,7 +1348,7 @@ MIT License โ free to use, modify, and distribute.
**CLI-Anything** โ *Make any software with a codebase Agent-native.*
-A methodology for the age of AI agents | 16 professional software demos | 1,839 passing tests
+A methodology for the age of AI agents | 16 professional software demos | 2,202 passing tests
diff --git a/README_CN.md b/README_CN.md
index a8ca794ee..1f7dfa03b 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -581,6 +581,13 @@ CLI-Anything ้็จไบไปปไฝๆไปฃ็ ๅบ็่ฝฏไปถ โโ ไธ้้ขๅ๏ผไธ้
โ
50 |
+| ๐ Unreal Insights |
+ๆง่ฝๅๆ |
+cli-anything-unrealinsights |
+ๅๅฐ trace ไผ่ฏ + ๅน้
็ UnrealInsights ๆๅปบ + ๆ ๅคดๅฏผๅบ |
+โ
50 |
+
+
| ๐จ Sketch |
UI ่ฎพ่ฎก |
sketch-cli |
@@ -589,11 +596,11 @@ CLI-Anything ้็จไบไปปไฝๆไปฃ็ ๅบ็่ฝฏไปถ โโ ไธ้้ขๅ๏ผไธ้
| ๅ่ฎก |
-โ
1,527 |
+โ
1,573 |
-> ๅ
จ้จ 1,628 ้กนๆต่ฏ **100% ้่ฟ** โโ 1,151 ้กนๅๅ
ๆต่ฏ + 458 ้กน็ซฏๅฐ็ซฏๆต่ฏ + 19 ้กน Node.js ๆต่ฏใ
+> ๅ
จ้จ 1,674 ้กนๆต่ฏ **100% ้่ฟ** โโ 1,197 ้กนๅๅ
ๆต่ฏ + 458 ้กน็ซฏๅฐ็ซฏๆต่ฏ + 19 ้กน Node.js ๆต่ฏใ
---
@@ -622,9 +629,10 @@ openscreen 101 passed โ
(78 unit + 23 e2e)
zoom 22 passed โ
(22 unit + 0 e2e)
drawio 138 passed โ
(116 unit + 22 e2e)
anygen 50 passed โ
(40 unit + 10 e2e)
+unrealinsights 50 passed โ
(49 unit + 1 e2e, 9 backend-gated e2e skipped)
sketch 19 passed โ
(19 jest, Node.js)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
-TOTAL 1,628 passed โ
100% pass rate
+TOTAL 1,674 passed โ
100% pass rate
```
---
@@ -691,6 +699,7 @@ cli-anything/
โโโ ๐ zoom/agent-harness/ # Zoom CLI๏ผ22 ้กนๆต่ฏ๏ผ
โโโ ๐ drawio/agent-harness/ # Draw.io CLI๏ผ138 ้กนๆต่ฏ๏ผ
โโโ โจ anygen/agent-harness/ # AnyGen CLI๏ผ50 ้กนๆต่ฏ๏ผ
+โโโ ๐ unrealinsights/agent-harness/ # Unreal Insights CLI๏ผ50 ้กนๆต่ฏ๏ผ
โโโ ๐จ sketch/agent-harness/ # Sketch CLI๏ผ19 ้กนๆต่ฏ๏ผNode.js๏ผ
```
diff --git a/registry.json b/registry.json
index ddcba5913..0c75ab437 100644
--- a/registry.json
+++ b/registry.json
@@ -2,7 +2,7 @@
"meta": {
"repo": "https://github.com/HKUDS/CLI-Anything",
"description": "CLI-Hub โ Agent-native stateful CLI interfaces for softwares, codebases, and Web Services",
- "updated": "2026-03-31"
+ "updated": "2026-04-16"
},
"clis": [
{
@@ -669,6 +669,25 @@
}
]
},
+ {
+ "name": "unrealinsights",
+ "display_name": "Unreal Insights",
+ "version": "0.1.0",
+ "description": "Windows-first Unreal trace capture, background session control, engine-matched UnrealInsights builds, and headless Timing Insights export workflows",
+ "requires": "Windows + Unreal Engine source or installed build with UnrealEditor and Unreal trace support",
+ "homepage": "https://dev.epicgames.com/documentation/en-us/unreal-engine/unreal-insights-in-unreal-engine",
+ "source_url": null,
+ "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=unrealinsights/agent-harness",
+ "entry_point": "cli-anything-unrealinsights",
+ "skill_md": "unrealinsights/agent-harness/cli_anything/unrealinsights/skills/SKILL.md",
+ "category": "debugging",
+ "contributors": [
+ {
+ "name": "AiMiDi",
+ "url": "https://github.com/AiMiDi"
+ }
+ ]
+ },
{
"name": "videocaptioner",
"display_name": "VideoCaptioner",
diff --git a/skills/cli-anything-unrealinsights/SKILL.md b/skills/cli-anything-unrealinsights/SKILL.md
new file mode 100644
index 000000000..5986f7444
--- /dev/null
+++ b/skills/cli-anything-unrealinsights/SKILL.md
@@ -0,0 +1,131 @@
+---
+name: "cli-anything-unrealinsights"
+description: "Capture Unreal Engine traces to .utrace files and export Unreal Insights timing/counter data in headless mode."
+---
+
+# cli-anything-unrealinsights
+
+Use this CLI when you need agent-friendly access to Unreal Insights trace capture
+and exporter workflows on Windows.
+
+## Prerequisites
+
+- Unreal Engine 5.5+ installed with `UnrealInsights.exe`
+- Windows
+- Optional explicit env vars:
+ - `UNREALINSIGHTS_EXE`
+ - `UNREAL_TRACE_SERVER_EXE`
+ - `UNREALINSIGHTS_TRACE`
+
+## Core Commands
+
+### Backend discovery
+
+```powershell
+cli-anything-unrealinsights --json backend info
+```
+
+To use a source-built engine's matching `UnrealInsights.exe`:
+
+```powershell
+cli-anything-unrealinsights --json backend ensure-insights `
+ --engine-root 'D:\code\D5\d5render-ue5_3'
+```
+
+This first looks for `Engine\Binaries\Win64\UnrealInsights.exe` under the
+specified engine root, then builds it with that engine's `Build.bat` if needed.
+
+### Trace session state
+
+```powershell
+cli-anything-unrealinsights trace set D:\captures\session.utrace
+cli-anything-unrealinsights --json trace info
+```
+
+### Capture orchestration
+
+```powershell
+cli-anything-unrealinsights --json capture run `
+ --project 'D:\Projects\MyGame\MyGame.uproject' `
+ --engine-root 'D:\Program Files\Epic Games\UE_5.5' `
+ --output-trace D:\captures\boot.utrace `
+ --channels "default,bookmark" `
+ --exec-cmd "Trace.Bookmark BootStart" `
+ --wait --timeout 300
+```
+
+You can also keep using the explicit form:
+
+```powershell
+cli-anything-unrealinsights --json capture run `
+ 'D:\Program Files\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe' `
+ --target-arg 'D:\Projects\MyGame\MyGame.uproject'
+```
+
+### Continuous capture session control
+
+```powershell
+cli-anything-unrealinsights --json capture start `
+ --project 'D:\Projects\MyGame\MyGame.uproject' `
+ --engine-root 'D:\Program Files\Epic Games\UE_5.5' `
+ --output-trace D:\captures\live_session.utrace
+
+cli-anything-unrealinsights --json capture status
+cli-anything-unrealinsights --json capture snapshot D:\captures\live_snapshot.utrace
+cli-anything-unrealinsights --json capture stop
+```
+
+This is the preferred flow when an agent needs to start profiling now and stop
+or snapshot later in a follow-up turn.
+
+If a tracked capture session is still running, `capture start` now requires
+`--replace` so the previous process is stopped before a new one is launched.
+
+### Offline exporters
+
+```powershell
+cli-anything-unrealinsights --json -t D:\captures\session.utrace export threads D:\out\threads.csv
+cli-anything-unrealinsights --json -t D:\captures\session.utrace export timer-stats D:\out\stats.csv --region "EXPORT_CAPTURE"
+cli-anything-unrealinsights --json -t D:\captures\session.utrace export counter-values D:\out\counter_values.csv --counter "*"
+```
+
+### Batch response files
+
+```powershell
+cli-anything-unrealinsights --json -t D:\captures\session.utrace batch run-rsp D:\out\exports.rsp
+```
+
+## JSON Output Guidance
+
+- Prefer `--json` for agent workflows.
+- Export commands return:
+ - `trace_path`
+ - `exec_command`
+ - `output_files`
+ - `log_path`
+ - `exit_code`
+ - `warnings`
+ - `errors`
+ - `succeeded`
+- Capture returns:
+ - `command`
+ - `trace_path`
+ - `trace_exists`
+ - `trace_size`
+ - `pid` or `exit_code`
+- Continuous capture status returns:
+ - `pid`
+ - `running`
+ - `target_exe`
+ - `project_path`
+ - `trace_path`
+ - `trace_size`
+ - `started_at`
+
+## Notes
+
+- v1 is Windows-first.
+- v1 supports file-mode capture orchestration only.
+- v1 does not control already-running UE instances or browse trace stores.
+- `capture stop` is a best-effort stop of the harness-launched process tree.
+- `capture snapshot` is a best-effort filesystem snapshot of the active trace.
diff --git a/unrealinsights/agent-harness/UNREALINSIGHTS.md b/unrealinsights/agent-harness/UNREALINSIGHTS.md
new file mode 100644
index 000000000..d549b74ae
--- /dev/null
+++ b/unrealinsights/agent-harness/UNREALINSIGHTS.md
@@ -0,0 +1,72 @@
+# UNREALINSIGHTS.md - Software-Specific SOP
+
+## About Unreal Insights
+
+Unreal Insights is Epic's trace analysis tool for Unreal Engine performance,
+profiling, timing, and counter data stored in `.utrace` files.
+
+This harness follows the CLI-Anything rule of using the real backend:
+
+- `UnrealInsights.exe` for headless analysis and CSV/TXT export
+- a traced Unreal Engine target executable for capture generation
+
+## Backend Model
+
+### Analysis backend
+
+`UnrealInsights.exe` accepts:
+
+- `-OpenTraceFile=`
+- `-NoUI`
+- `-AutoQuit`
+- `-ABSLOG=`
+- `-ExecOnAnalysisCompleteCmd=`
+
+The command may be:
+
+- a direct exporter command such as `TimingInsights.ExportThreads`
+- `@=` for batch execution
+
+This harness can also ensure an engine-matched analysis backend for custom
+source engines by locating or building `Engine/Binaries/Win64/UnrealInsights.exe`.
+
+### Capture backend
+
+UE targets can be launched with:
+
+- `-trace=`
+- `-tracefile=`
+- optional `-ExecCmds=`
+
+This harness supports two v1 launch shapes:
+
+- explicit target executable path
+- `--project + --engine-root` convenience mode, which resolves `UnrealEditor.exe`
+
+This harness only supports file-mode capture orchestration in v1.
+
+## CLI Coverage Map
+
+| Feature | CLI Command | Status |
+|--------|-------------|--------|
+| Resolve Insights binaries | `backend info` | v1 |
+| Set current trace | `trace set` | v1 |
+| Inspect current trace | `trace info` | v1 |
+| Launch traced target | `capture run` | v1 |
+| Export threads | `export threads` | v1 |
+| Export timers | `export timers` | v1 |
+| Export timing events | `export timing-events` | v1 |
+| Export timer statistics | `export timer-stats` | v1 |
+| Export timer callees | `export timer-callees` | v1 |
+| Export counter list | `export counters` | v1 |
+| Export counter values | `export counter-values` | v1 |
+| Batch response file | `batch run-rsp` | v1 |
+| Control live instances | โ | future |
+| Trace store browsing | โ | future |
+
+## Current Limitations
+
+- Windows-first discovery only
+- No SessionServices control of already-running UE instances
+- No trace store session enumeration
+- Capture orchestration assumes the target executable accepts standard UE trace flags
diff --git a/unrealinsights/agent-harness/cli_anything/unrealinsights/README.md b/unrealinsights/agent-harness/cli_anything/unrealinsights/README.md
new file mode 100644
index 000000000..1539ba0e9
--- /dev/null
+++ b/unrealinsights/agent-harness/cli_anything/unrealinsights/README.md
@@ -0,0 +1,256 @@
+# cli-anything-unrealinsights
+
+Command-line interface for Unreal Insights trace capture and export workflows.
+
+This harness wraps the real Unreal Engine tools:
+
+- `UnrealInsights.exe` for headless `.utrace` analysis and exporters
+- a traced UE/Game executable for file-mode capture generation
+
+## Installation
+
+```bash
+cd unrealinsights/agent-harness
+pip install -e .
+```
+
+## Prerequisites
+
+- Windows
+- Unreal Engine 5.5+ installed with `UnrealInsights.exe`
+- optional `UnrealTraceServer.exe` for backend reporting
+
+You can point the harness at explicit binaries:
+
+```powershell
+$env:UNREALINSIGHTS_EXE='D:\Program Files\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealInsights.exe'
+$env:UNREAL_TRACE_SERVER_EXE='D:\Program Files\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealTraceServer.exe'
+```
+
+If those are not set, the harness auto-discovers common UE installs under
+`:\Program Files\Epic Games\UE_*`.
+
+## Quick Start
+
+```powershell
+# Inspect resolved backend binaries
+cli-anything-unrealinsights --json backend info
+
+# Find or build UnrealInsights.exe for a custom engine root
+cli-anything-unrealinsights --json backend ensure-insights `
+ --engine-root 'D:\code\D5\d5render-ue5_3'
+
+# Bind a trace file for the current session
+cli-anything-unrealinsights trace set D:\captures\session.utrace
+
+# Start a background capture session and keep it running
+cli-anything-unrealinsights --json capture start `
+ --project 'D:\Projects\MyGame\MyGame.uproject' `
+ --engine-root 'D:\Program Files\Epic Games\UE_5.5' `
+ --output-trace D:\captures\live_session.utrace
+
+# If a tracked capture is already running, replace it explicitly
+cli-anything-unrealinsights --json capture start --replace `
+ --project 'D:\Projects\MyGame\MyGame.uproject' `
+ --engine-root 'D:\Program Files\Epic Games\UE_5.5' `
+ --output-trace D:\captures\replacement_session.utrace
+
+# Check current capture status
+cli-anything-unrealinsights --json capture status
+
+# Create a best-effort snapshot without ending the session
+cli-anything-unrealinsights --json capture snapshot D:\captures\live_snapshot.utrace
+
+# Stop the tracked capture session
+cli-anything-unrealinsights --json capture stop
+
+# Export threads
+cli-anything-unrealinsights --json -t D:\captures\session.utrace export threads D:\out\threads.csv
+
+# Export timer statistics for a region
+cli-anything-unrealinsights --json -t D:\captures\session.utrace export timer-stats `
+ D:\out\timer_stats.csv --threads "GameThread" --timers "*" --region "EXPORT_CAPTURE"
+
+# Execute a response file
+cli-anything-unrealinsights --json -t D:\captures\session.utrace batch run-rsp D:\out\export.rsp
+
+# Launch a traced UE target and wait for completion
+cli-anything-unrealinsights --json capture run `
+ --project 'D:\Projects\MyGame\MyGame.uproject' `
+ --engine-root 'D:\Program Files\Epic Games\UE_5.5' `
+ --output-trace D:\captures\editor_boot.utrace `
+ --channels "default,bookmark" `
+ --exec-cmd "Trace.Bookmark BootStart" `
+ --wait --timeout 300
+
+# Start REPL (default behavior)
+cli-anything-unrealinsights
+```
+
+## Command Groups
+
+- `backend`
+ - `info`
+ - `ensure-insights`
+- `trace`
+ - `set`
+ - `info`
+- `capture`
+ - `run`
+ - `start`
+ - `status`
+ - `stop`
+ - `snapshot`
+- `export`
+ - `threads`
+ - `timers`
+ - `timing-events`
+ - `timer-stats`
+ - `timer-callees`
+ - `counters`
+ - `counter-values`
+- `batch`
+ - `run-rsp`
+- `repl`
+
+## Global Options
+
+- `--json`: machine-readable output
+- `--debug`: include traceback details in errors
+- `--trace/-t`: current `.utrace` file
+- `--insights-exe`: explicit `UnrealInsights.exe` path
+- `--trace-server-exe`: explicit `UnrealTraceServer.exe` path
+
+## Engine-Matched Insights
+
+If you need an `UnrealInsights.exe` matching a custom source engine, use:
+
+```powershell
+cli-anything-unrealinsights --json backend ensure-insights `
+ --engine-root 'D:\code\D5\d5render-ue5_3'
+```
+
+Behavior:
+
+- looks for `Engine\Binaries\Win64\UnrealInsights.exe` under the given engine root
+- if missing, runs that engine's `Engine\Build\BatchFiles\Build.bat UnrealInsights Win64 Development -WaitMutex`
+- returns the resolved path plus the build log path when a build was attempted
+
+## Capture Convenience Layer
+
+`capture run` supports two launch styles:
+
+```powershell
+# 1. Convenience mode: infer UnrealEditor.exe from engine root
+cli-anything-unrealinsights capture run `
+ --project 'D:\Projects\MyGame\MyGame.uproject' `
+ --engine-root 'D:\Program Files\Epic Games\UE_5.5'
+
+# 2. Explicit mode: provide the exact executable yourself
+cli-anything-unrealinsights capture run `
+ 'D:\Program Files\Epic Games\UE_5.5\Engine\Binaries\Win64\UnrealEditor.exe' `
+ --target-arg 'D:\Projects\MyGame\MyGame.uproject'
+```
+
+Notes:
+
+- `--project` prepends the `.uproject` path to the target command line.
+- `--engine-root` accepts either the UE install root or its `Engine` subdirectory.
+- If `target_exe` is omitted, `capture run` resolves `UnrealEditor.exe` from `--engine-root`.
+- The original explicit `target_exe` path remains supported.
+
+## Continuous AI-Driven Capture
+
+For longer-running sessions, prefer this loop:
+
+```powershell
+cli-anything-unrealinsights --json capture start --project ... --engine-root ...
+cli-anything-unrealinsights --json capture status
+cli-anything-unrealinsights --json capture snapshot
+cli-anything-unrealinsights --json capture stop
+```
+
+Behavior:
+
+- `capture start` launches the target in the background and persists the tracked PID/trace/session metadata
+- `capture start` refuses to overwrite a still-running tracked session unless you pass `--replace`
+- `capture status` reads the persisted session state and reports whether the process is still running and how large the trace has grown
+- `capture snapshot` creates a best-effort copy of the current `.utrace` without requiring you to end the session first
+- `capture stop` performs a best-effort stop of the tracked process tree launched by the harness
+
+## Human + AI Workflow
+
+When a human is directing an AI agent, the best requests usually specify:
+
+1. engine root
+2. project path or target executable
+3. whether the focus is startup or runtime behavior
+4. the artifact or summary you want back
+
+Example prompts:
+
+```text
+Use D:\code\D5\d5render-ue5_3 to analyze startup performance for
+D:\code\D5\FusionEffectBuild.
+First ensure a matching UnrealInsights.exe exists, then capture a startup trace,
+export timer-stats, and summarize the top 20 hotspots.
+```
+
+```text
+Use D:\code\D5\d5render-ue5_3 and D:\code\D5\FusionEffectBuild
+to start a background performance capture.
+Do not block and do not exit the project immediately.
+Tell me the trace path and current status first.
+When I say "stop", stop the capture, make a snapshot, export timer-stats and
+timing-events, then summarize the results.
+```
+
+```text
+Start a background trace for this project, let me interact with the scene
+manually, and wait.
+When I say "stop now", export timer-stats and timing-events and focus on
+GameThread, RenderThread, and task-system waits.
+```
+
+Useful phrases in prompts:
+
+- `ensure matching UnrealInsights`
+- `background continuous capture`
+- `wait until I say stop`
+- `make a snapshot`
+- `export timer-stats`
+- `export timing-events`
+- `summarize top hotspots`
+- `look at GameThread / RenderThread / WaitForTasks`
+
+## Export Filters
+
+`timing-events` and `timer-stats` support:
+
+- `--columns`
+- `--threads`
+- `--timers`
+- `--start-time`
+- `--end-time`
+- `--region`
+
+`counter-values` supports:
+
+- `--counter`
+- `--columns`
+- `--start-time`
+- `--end-time`
+- `--region`
+
+## Testing
+
+```bash
+cd unrealinsights/agent-harness
+pytest cli_anything/unrealinsights/tests/test_core.py -v
+pytest cli_anything/unrealinsights/tests/test_full_e2e.py -v -s
+```
+
+Optional environment variables for E2E coverage:
+
+- `UNREALINSIGHTS_TEST_TRACE`
+- `UNREALINSIGHTS_TEST_TARGET_EXE`
diff --git a/unrealinsights/agent-harness/cli_anything/unrealinsights/__init__.py b/unrealinsights/agent-harness/cli_anything/unrealinsights/__init__.py
new file mode 100644
index 000000000..9fb92967c
--- /dev/null
+++ b/unrealinsights/agent-harness/cli_anything/unrealinsights/__init__.py
@@ -0,0 +1,5 @@
+"""cli-anything Unreal Insights harness."""
+
+__all__ = ["__version__"]
+
+__version__ = "0.1.0"
diff --git a/unrealinsights/agent-harness/cli_anything/unrealinsights/__main__.py b/unrealinsights/agent-harness/cli_anything/unrealinsights/__main__.py
new file mode 100644
index 000000000..a58b161ad
--- /dev/null
+++ b/unrealinsights/agent-harness/cli_anything/unrealinsights/__main__.py
@@ -0,0 +1,7 @@
+"""Module entry point for cli_anything.unrealinsights."""
+
+from cli_anything.unrealinsights.unrealinsights_cli import main
+
+
+if __name__ == "__main__":
+ main()
diff --git a/unrealinsights/agent-harness/cli_anything/unrealinsights/core/__init__.py b/unrealinsights/agent-harness/cli_anything/unrealinsights/core/__init__.py
new file mode 100644
index 000000000..5f4b90c4f
--- /dev/null
+++ b/unrealinsights/agent-harness/cli_anything/unrealinsights/core/__init__.py
@@ -0,0 +1 @@
+"""Core helpers for the Unreal Insights harness."""
diff --git a/unrealinsights/agent-harness/cli_anything/unrealinsights/core/capture.py b/unrealinsights/agent-harness/cli_anything/unrealinsights/core/capture.py
new file mode 100644
index 000000000..6bfa09704
--- /dev/null
+++ b/unrealinsights/agent-harness/cli_anything/unrealinsights/core/capture.py
@@ -0,0 +1,235 @@
+"""
+Capture orchestration helpers.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+import time
+from pathlib import Path
+from typing import Sequence
+
+from cli_anything.unrealinsights.utils import unrealinsights_backend as backend
+
+DEFAULT_CHANNELS = "default"
+EDITOR_BINARY_NAME = "UnrealEditor.exe"
+
+
+def normalize_trace_output_path(
+ target_exe: str,
+ output_trace: str | None = None,
+ current_trace: str | None = None,
+ cwd: str | None = None,
+) -> str:
+ """Resolve the output trace path for a capture workflow."""
+ if output_trace:
+ path = Path(output_trace).expanduser()
+ elif current_trace:
+ path = Path(current_trace).expanduser()
+ else:
+ base_dir = Path(cwd or os.getcwd()).resolve()
+ timestamp = time.strftime("%Y%m%d-%H%M%S")
+ path = base_dir / f"{Path(target_exe).stem}-{timestamp}.utrace"
+
+ if not path.suffix:
+ path = path.with_suffix(".utrace")
+ return str(path.resolve())
+
+
+def build_exec_cmds_arg(exec_cmds: Sequence[str] | None) -> str | None:
+ if not exec_cmds:
+ return None
+ commands = [cmd.strip() for cmd in exec_cmds if cmd.strip()]
+ return ",".join(commands) if commands else None
+
+
+def resolve_engine_root(engine_root: str) -> str:
+ """Normalize an Unreal Engine installation root."""
+ path = Path(engine_root).expanduser().resolve()
+ root = path.parent if path.name.lower() == "engine" else path
+
+ if not root.exists():
+ raise RuntimeError(f"Engine root not found: {root}")
+ if not (root / "Engine").is_dir():
+ raise RuntimeError(f"Engine root must contain an Engine directory: {root}")
+
+ return str(root)
+
+
+def resolve_editor_target(engine_root: str) -> str:
+ """Resolve UnrealEditor.exe from a UE install root or Engine directory."""
+ root = Path(resolve_engine_root(engine_root))
+ editor = root / "Engine" / "Binaries" / "Win64" / EDITOR_BINARY_NAME
+ if not editor.is_file():
+ raise RuntimeError(f"UnrealEditor.exe not found under engine root: {root}")
+ return str(editor.resolve())
+
+
+def resolve_capture_target(
+ target_exe: str | None,
+ project: str | None = None,
+ engine_root: str | None = None,
+ target_args: Sequence[str] | None = None,
+) -> tuple[str, list[str], dict[str, str | None]]:
+ """Resolve the effective target executable and launch args."""
+ resolved_project = None
+ resolved_engine_root = None
+ launch_args = list(target_args or [])
+
+ if project:
+ project_path = Path(project).expanduser().resolve()
+ if not project_path.is_file():
+ raise RuntimeError(f"Project file not found: {project_path}")
+ resolved_project = str(project_path)
+
+ if target_exe:
+ target_path = Path(target_exe).expanduser().resolve()
+ if not target_path.is_file():
+ raise RuntimeError(f"Target executable not found: {target_path}")
+ resolved_target = str(target_path)
+ else:
+ if not resolved_project:
+ raise RuntimeError("Either target_exe or --project must be provided.")
+ if not engine_root:
+ raise RuntimeError("--engine-root is required when inferring UnrealEditor.exe from --project.")
+ resolved_engine_root = resolve_engine_root(engine_root)
+ resolved_target = resolve_editor_target(resolved_engine_root)
+
+ if engine_root and resolved_engine_root is None:
+ resolved_engine_root = resolve_engine_root(engine_root)
+
+ if resolved_project and resolved_project not in launch_args:
+ launch_args = [resolved_project, *launch_args]
+
+ return resolved_target, launch_args, {
+ "project_path": resolved_project,
+ "engine_root": resolved_engine_root,
+ }
+
+
+def build_capture_command(
+ target_exe: str,
+ output_trace: str,
+ channels: str = DEFAULT_CHANNELS,
+ exec_cmds: Sequence[str] | None = None,
+ target_args: Sequence[str] | None = None,
+) -> list[str]:
+ """Build the traced target command line."""
+ target_path = Path(target_exe).expanduser().resolve()
+ if not target_path.is_file():
+ raise RuntimeError(f"Target executable not found: {target_path}")
+
+ command = [str(target_path)]
+ command.extend(target_args or [])
+ command.append(f"-trace={channels}")
+ command.append(f"-tracefile={Path(output_trace).expanduser().resolve()}")
+
+ exec_arg = build_exec_cmds_arg(exec_cmds)
+ if exec_arg:
+ command.append(f"-ExecCmds={exec_arg}")
+
+ return command
+
+
+def run_capture(
+ target_exe: str,
+ output_trace: str,
+ channels: str = DEFAULT_CHANNELS,
+ exec_cmds: Sequence[str] | None = None,
+ target_args: Sequence[str] | None = None,
+ wait: bool = False,
+ timeout: float | None = None,
+) -> dict[str, object]:
+ """Launch a traced target executable."""
+ backend.ensure_parent_dir(output_trace)
+ command = build_capture_command(
+ target_exe,
+ output_trace=output_trace,
+ channels=channels,
+ exec_cmds=exec_cmds,
+ target_args=target_args,
+ )
+ result = backend.run_process(command, timeout=timeout, wait=wait)
+
+ trace_path = Path(output_trace).expanduser().resolve()
+ trace_exists = trace_path.is_file()
+ trace_size = trace_path.stat().st_size if trace_exists else None
+ waited = bool(result.get("waited", wait))
+ succeeded = True
+ if waited:
+ succeeded = (
+ not bool(result.get("timed_out"))
+ and result.get("exit_code") == 0
+ and trace_exists
+ )
+
+ result.update(
+ {
+ "target_exe": str(Path(target_exe).expanduser().resolve()),
+ "target_args": list(target_args or []),
+ "trace_path": str(trace_path),
+ "channels": channels,
+ "trace_exists": trace_exists,
+ "trace_size": trace_size,
+ "succeeded": succeeded,
+ }
+ )
+ return result
+
+
+def capture_status(session) -> dict[str, object]:
+ """Return the current tracked capture status."""
+ info = session.capture_info()
+ pid = info.get("pid")
+ info["running"] = backend.is_process_running(pid) if pid else False
+ return info
+
+
+def stop_capture(session, force: bool = False, timeout: float | None = None) -> dict[str, object]:
+ """Stop the currently tracked capture process."""
+ info = capture_status(session)
+ pid = info.get("pid")
+ if not pid:
+ raise RuntimeError("No active capture session is being tracked.")
+
+ termination = backend.terminate_process(int(pid), force=force, timeout=timeout)
+ status = capture_status(session)
+ result = {
+ "termination": termination,
+ "capture": status,
+ }
+ if termination.get("stopped"):
+ session.clear_capture()
+ result["capture"] = session.capture_info()
+ if info.get("trace_path"):
+ session.set_trace(info["trace_path"])
+ return result
+
+
+def snapshot_capture(session, output_trace: str | None = None) -> dict[str, object]:
+ """Create a best-effort copy of the current trace file."""
+ info = capture_status(session)
+ source = info.get("trace_path")
+ if not source:
+ raise RuntimeError("No active capture trace is available to snapshot.")
+
+ source_path = Path(source).expanduser().resolve()
+ if not source_path.is_file():
+ raise RuntimeError(f"Trace file not found: {source_path}")
+
+ if output_trace:
+ output_path = Path(output_trace).expanduser().resolve()
+ else:
+ timestamp = time.strftime("%Y%m%d-%H%M%S")
+ output_path = source_path.with_name(f"{source_path.stem}-snapshot-{timestamp}{source_path.suffix}")
+
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(source_path, output_path)
+ return {
+ "source_trace": str(source_path),
+ "snapshot_trace": str(output_path),
+ "snapshot_exists": output_path.is_file(),
+ "snapshot_size": output_path.stat().st_size if output_path.is_file() else None,
+ "capture_running": info.get("running", False),
+ }
diff --git a/unrealinsights/agent-harness/cli_anything/unrealinsights/core/export.py b/unrealinsights/agent-harness/cli_anything/unrealinsights/core/export.py
new file mode 100644
index 000000000..92fd5979a
--- /dev/null
+++ b/unrealinsights/agent-harness/cli_anything/unrealinsights/core/export.py
@@ -0,0 +1,328 @@
+"""
+Exporter command construction and execution helpers.
+"""
+
+from __future__ import annotations
+
+import ctypes
+import os
+import re
+import shlex
+import tempfile
+from pathlib import Path
+
+from cli_anything.unrealinsights.utils import unrealinsights_backend as backend
+
+EXPORTER_COMMANDS = {
+ "threads": "TimingInsights.ExportThreads",
+ "timers": "TimingInsights.ExportTimers",
+ "timing-events": "TimingInsights.ExportTimingEvents",
+ "timer-stats": "TimingInsights.ExportTimerStatistics",
+ "timer-callees": "TimingInsights.ExportTimerCallees",
+ "counters": "TimingInsights.ExportCounters",
+ "counter-values": "TimingInsights.ExportCounterValues",
+}
+
+
+def _quote(value: str) -> str:
+ return f'"{value}"'
+
+
+def _is_legacy_unrealinsights(version: str | None) -> bool:
+ return bool(version and version.startswith("5.3"))
+
+
+def _windows_short_path(path: Path) -> str | None:
+ if os.name != "nt":
+ return None
+ buffer = ctypes.create_unicode_buffer(32768)
+ result = ctypes.windll.kernel32.GetShortPathNameW(str(path), buffer, len(buffer))
+ if result == 0:
+ return None
+ return buffer.value
+
+
+def _legacy_filename_arg(output_path: str) -> str:
+ path = Path(output_path).expanduser().resolve()
+ path_str = str(path)
+ if " " not in path_str:
+ return path_str
+
+ parent = path.parent
+ short_parent = _windows_short_path(parent)
+ if not short_parent:
+ raise RuntimeError(
+ f"Legacy UnrealInsights export requires a path without spaces or a resolvable short path: {path}"
+ )
+ if " " in path.name:
+ raise RuntimeError(
+ f"Legacy UnrealInsights export does not support spaces in the output filename: {path.name}"
+ )
+ return str(Path(short_parent) / path.name)
+
+
+def _modern_filename_arg(output_path: str) -> str:
+ """Build a filename token compatible with modern UnrealInsights builds."""
+ path = Path(output_path).expanduser().resolve()
+ path_str = str(path)
+ if os.name != "nt":
+ return _quote(path_str)
+ if " " not in path_str:
+ return path_str
+
+ short_path = _windows_short_path(path)
+ if short_path:
+ return short_path
+
+ raise RuntimeError(
+ "UnrealInsights export requires a path without spaces or a resolvable short path on Windows: "
+ f"{path}"
+ )
+
+
+def _filename_arg(output_path: str, insights_version: str | None = None) -> str:
+ output_abs = str(Path(output_path).expanduser().resolve())
+ if _is_legacy_unrealinsights(insights_version):
+ return _legacy_filename_arg(output_abs)
+ return _modern_filename_arg(output_abs)
+
+
+def build_export_exec_command(
+ exporter: str,
+ output_path: str,
+ *,
+ insights_version: str | None = None,
+ columns: str | None = None,
+ threads: str | None = None,
+ timers: str | None = None,
+ start_time: float | None = None,
+ end_time: float | None = None,
+ region: str | None = None,
+ counter: str | None = None,
+) -> str:
+ """Build a TimingInsights exporter command string."""
+ if exporter not in EXPORTER_COMMANDS:
+ raise RuntimeError(f"Unsupported exporter: {exporter}")
+
+ output_abs = str(Path(output_path).expanduser().resolve())
+ filename_token = _filename_arg(output_abs, insights_version=insights_version)
+
+ parts = [EXPORTER_COMMANDS[exporter], filename_token]
+
+ if counter:
+ parts.append(f"-counter={_quote(counter)}")
+ if columns:
+ parts.append(f"-columns={_quote(columns)}")
+ if threads:
+ parts.append(f"-threads={_quote(threads)}")
+ if timers:
+ parts.append(f"-timers={_quote(timers)}")
+ if start_time is not None:
+ parts.append(f"-startTime={start_time}")
+ if end_time is not None:
+ parts.append(f"-endTime={end_time}")
+ if region:
+ parts.append(f"-region={_quote(region)}")
+
+ return " ".join(parts)
+
+
+def build_rsp_exec_command(rsp_path: str) -> str:
+ """Build the response-file execution token."""
+ return f"@={Path(rsp_path).expanduser().resolve()}"
+
+
+def _normalize_rsp_line(line: str, insights_version: str | None = None) -> tuple[str, str | None]:
+ stripped = line.strip()
+ if not stripped or stripped.startswith("#"):
+ return line, None
+
+ match = re.match(r'^(?P\s*)(?P\S+)\s+(?P