Compare commits

..
160 Commits
Author SHA1 Message Date
pancake 8dcc6f0418 Release 1.3.6 2026-04-11 18:06:11 +02:00
pancake b68cdd1171 Fix empty query bug in r2ai -i 2026-04-10 16:00:58 +02:00
pancake 3f935a46be Do not silently discards caller-provided message history 2026-04-10 15:53:19 +02:00
pancake fd46390d41 Break auto mode when receiving unknown actions 2026-04-10 15:45:42 +02:00
pancake 63106508dc In decai: Restore model state when decopipe execution fails 2026-04-10 15:45:00 +02:00
pancake 46f42d3dd9 Ignore comment lines in decai.txt (decai -E) 2026-04-09 21:39:08 +02:00
pancake 836fae3d91 Use github actions to install r2 in the ci 2026-04-09 11:02:00 +02:00
pancake 66dd3dced7 Oops missing file 2026-04-09 10:44:16 +02:00
pancake c016fc72ed Release 1.3.4 2026-03-31 18:32:01 +02:00
Abhi 779b787f2d Fix URL formatting for OpenAI compatible API 2026-03-31 16:49:22 +02:00
pancake ca210f149b Add zen, ollamacloud and lmstudio providers to r2ai, sync with decai 2026-03-30 17:46:10 +02:00
pancake 99e99b1797 Reduce some LOCs in decai 2026-03-30 16:51:16 +02:00
pancake fb5427238b Add 'decai -E' for the defaults 2026-03-30 16:45:46 +02:00
pancake d7c2dacd7a Add decai timeout, zen provider, improve prompt, custom headers and more 2026-03-30 16:38:10 +02:00
pancake d6c32f4b72 decai: tighten types and simplify provider/config handling 2026-03-30 15:26:23 +02:00
dependabot[bot] 0a083e7231 Bump actions/download-artifact from 4 to 8 2026-03-30 15:20:45 +02:00
dependabot[bot] 1a067fbbfc Bump softprops/action-gh-release from 1 to 2 2026-03-30 15:18:54 +02:00
dependabot[bot] 8a724d3682 Bump actions/upload-artifact from 4 to 7 2026-03-30 15:18:30 +02:00
dependabot[bot] 8f7d152aa3 Bump actions/checkout from 5 to 6 2026-03-30 15:17:48 +02:00
pancake 7120c325da Add dependabot 2026-03-30 13:47:53 +02:00
pancake 14c991cd10 Update ci actions 2026-03-30 13:47:16 +02:00
pancake d1ef1e065e Welcome to the initial r2clippy wizard 2026-03-30 13:45:06 +02:00
pancake 6f8ec21a39 Release 1.3.2 2026-03-19 14:47:39 +01:00
pancake 3db0744c91 Fix shell injection in curl command construction 2026-03-07 12:20:28 +01:00
pancake 3306e412e2 rawtools: require explicit TOOL prefix for tool execution 2026-03-07 12:20:13 +01:00
pancake 20f5e16164 fix: detect multiline after adding command annotations 2026-03-07 12:20:03 +01:00
pancake 660249a31f Fix double free and refactor the complex logic into a separate function 2026-03-06 17:03:30 +01:00
Nitan Alexandru Marcel 312921c2a1 Add r2agent reference in the README 2026-02-08 23:32:54 +01:00
pancake 2356dbef9e Release 1.3.0 2026-01-04 19:17:05 +01:00
pancake 7612a80a9c Warn about invalid syntax/lines in apikeys.txt 2025-12-10 20:05:55 +01:00
pancake 12d8bc9e22 Strip "_API_KEY" from providers in apikeys.txt 2025-12-10 20:03:25 +01:00
pancake d4be5f8569 Add .pkgname for the r2ai plugin 2025-12-10 01:01:45 +01:00
pancake a9333c8c38 Only one function doing curl and remove aliased providers in decai 2025-12-06 12:36:04 +01:00
pancake 29bdb2fcea Remove the hardcoded model names for providers with callback 2025-12-05 17:48:42 +01:00
pancake 078376c5d2 Refactor the model listing code in decai 2025-12-05 17:30:11 +01:00
pancake aa24702335 Refactor the providers logic in decai 2025-12-05 17:05:18 +01:00
pancake 9aa40216a2 Initial rewrite of decai in TypeScript 2025-12-05 16:44:09 +01:00
pancake 96e454e0c6 Fix tool calling message construction bug 2025-11-27 11:07:16 +01:00
pancake 219b034261 Don't feed system messages to Anthropic 2025-11-21 21:23:24 +01:00
pancake 797c967fb0 Remove redudant provider fields 2025-11-11 16:21:55 +01:00
pancake e61b7db8a5 Fix #219 - Add deepseek provider for r2ai.c 2025-11-11 16:03:10 +01:00
pancake 3a39f9cd9d Add decai -b to setup the baseurl 2025-11-07 00:25:59 +01:00
pancake 2b5e50d8f0 Workaround MANDIR for old r2 2025-11-06 16:36:45 +01:00
pancake bb737f5be6 Add decai -K to edit apikeys (requires latest r2 from git) 2025-11-06 16:33:05 +01:00
pancake 3e5f91e631 Load decai apikeys from ~/.config/r2ai/apikeys.txt 2025-11-06 14:19:11 +01:00
pancake a3154bdcaf Simplify and fix the gemini support in decai 2025-11-06 14:10:19 +01:00
pancake a6be6e6555 Add decai -p to select provider 2025-11-06 14:01:53 +01:00
pancake ad0bdbb902 Fix res error in gemini for decai 2025-11-06 13:44:55 +01:00
pancake 4651d3b1b3 Update the README 2025-11-05 17:40:51 +01:00
pancake 572e02c4ec Change symbol visibility to please Windows builds 2025-11-05 17:18:50 +01:00
pancake 75c787ef41 Fix the meson build 2025-11-05 12:52:25 +01:00
pancake 731f1933a9 Refactor the apikeys code into a separate file and use sample config key by default 2025-11-05 12:43:15 +01:00
pancake 573755ccef Add -e to r2ai main 2025-11-01 21:30:12 +01:00
pancake ecca7825fe Add r2ai -i to run scripts 2025-10-31 20:15:32 +01:00
pancake 95117608fb Small improvements for the clippy repl 2025-10-31 20:13:30 +01:00
pancake b3958b5f7d Initial clippy repl for the r2ai cli tool 2025-10-31 19:45:12 +01:00
pancake d0eaf37764 Fix -m? reported crash 2025-10-31 16:34:49 +01:00
pancake 65692edd54 Bring back the support for Gemini provider to r2ai.c 2025-10-30 20:08:33 +01:00
pancake 50f55bb5d2 Add r2ai -K to edit api keys and remove the unnecessary compatdb 2025-10-29 22:21:33 +01:00
pancake fe1768fcc3 Fix apikey error message 2025-10-29 16:50:30 +01:00
pancake f241fba17d Add WIP decai logo 2025-10-29 10:45:55 +01:00
pancake e5252960e0 Implement r2ai.auto.slim to make disasm slimmier for auto 2025-10-29 10:45:21 +01:00
pancake 8a45dadbde Rename r2ai.auto.hide_tool_output to r2ai.auto.verbose 2025-10-29 10:34:31 +01:00
pancake 53d32efbbe Deprecate r2ai.chat.show_cost 2025-10-29 10:28:48 +01:00
pancake f06faba9d6 Release 1.2.6 2025-10-29 00:02:52 +01:00
pancake 8b52c2f6e2 Fix ninja build 2025-10-28 23:55:39 +01:00
pancake b37436fb8d Fix more warnings 2025-10-28 23:41:45 +01:00
pancake 2524e66825 Fix warning unused variable 2025-10-28 23:35:41 +01:00
pancake 519017a75e Fix editing r2js scripts in auto mode 2025-10-28 23:34:05 +01:00
pancake 6f30d6ca37 Handle comments in commands in all the auto calls 2025-10-28 20:49:24 +01:00
pancake 08ea4d11b2 Better last call 2025-10-28 19:51:22 +01:00
pancake 0281308434 Fix the build 2025-10-28 19:42:11 +01:00
pancake 17a59d2057 Add r2ai.debug to see the llm requests and responses 2025-10-28 19:37:01 +01:00
pancake eac0b8c6ef Prompt before leaving the interrupted auto mode 2025-10-28 19:30:11 +01:00
pancake 24aee426b4 Comments in commands are now used as annotations for the conversation 2025-10-28 19:28:05 +01:00
pancake b31bc5c116 Suggested simpler auto prompt 2025-10-28 19:24:40 +01:00
pancake 01738210f5 Increase timeout to 4 minutes 2025-10-28 19:13:05 +01:00
pancake 7b19c4ff99 Resizing the terminal is no longer interrupting the network calls 2025-10-28 19:10:50 +01:00
pancake 1986dd88bd r2ai needs an argument to work 2025-10-28 12:57:20 +01:00
pancake 1ed40401dd Implement r2ai -C to compact the message log 2025-10-28 12:55:46 +01:00
pancake 6ac0ff6352 Add compact prompt 2025-10-28 12:17:49 +01:00
pancake 4158e5f157 Add an option to disable the js_execute tool 2025-10-28 00:05:30 +01:00
pancake 225ff58228 Add r2ai.auto.think to disable reasoning 2025-10-28 00:03:34 +01:00
pancake a51be44c68 Improve native tool calling prompt 2025-10-28 00:00:04 +01:00
pancake 911e455f72 Only capture ONE tool call from the auto response 2025-10-27 23:40:03 +01:00
pancake 70eb975b7f Fix the set_prompt for auto mode 2025-10-27 23:21:58 +01:00
pancake a0d36cc909 Implement the -LR command to create a log report 2025-10-27 23:08:51 +01:00
pancake 030551816b Fix windows build 2025-10-27 23:08:51 +01:00
pancake 88c44c871b Fix tool calling, at least for OpenAI 2025-10-27 23:08:51 +01:00
pancake 9218e3ea27 Capture the thinking 2025-10-27 23:08:51 +01:00
pancake 6f7ffcae1a Oh great now we have a bug in the json serializer of r2 2025-10-27 23:08:51 +01:00
pancake fe12d2f8ee Add r2ai -b for the shell 2025-10-27 23:08:51 +01:00
pancake ae55b2f204 Proper use of RList -20LOC 2025-10-27 23:08:51 +01:00
pancake 7d4e949240 Use RList instead of hardcoded NEWS vectors 2025-10-27 23:08:51 +01:00
pancake 9b97d39c7a Add r2ai -b to change the baseurl 2025-10-27 19:04:16 +01:00
pancake 33f72a997e Refactorings to avoid hardcoded ansi colors. Using Color macros instead 2025-10-27 18:47:49 +01:00
pancake 36a8f13131 Use R2_CURL on Windows 2025-10-27 18:29:01 +01:00
pancake 5c2ded0171 Fix r2ai -E and loading script 2025-10-27 17:46:18 +01:00
pancake d5a39816a8 Use R2__WINDOWS__ instead of defined(_WIN32) 2025-10-27 13:02:36 +01:00
pancake 5aab38ab76 Proper command escapes for curl and pwsh http backends 2025-10-27 13:00:35 +01:00
pancake 9f15ec6c73 Release 1.2.4 2025-10-26 15:04:03 +01:00
pancake 68baac9911 Transform all promptfiles into the new md format 2025-10-26 15:02:48 +01:00
pancake 54a280952e Load API keys from ~/.config/r2ai/apikeys.txt too 2025-10-26 14:46:20 +01:00
pancake efc2368af1 Move the examples into the attic 2025-10-26 14:31:46 +01:00
pancake 28ddcbf638 Refactor and cleanup the http.backend implementations 2025-10-26 14:04:58 +01:00
pancake 85e9ed5c65 Do not accept invalid values in r2ai.http.backend 2025-10-25 19:56:10 +02:00
pancake 8886365925 Release 1.2.2 2025-10-25 11:03:12 +02:00
pancake 01d8e29420 Remove the r2-5.x compat macros 2025-10-25 11:02:39 +02:00
pancake e8b556ee5d Use r_sys_sleep and simplify the overengineered retry_sleep logic 2025-10-25 10:50:05 +02:00
pancake 6c336f48f7 Fix the meson build 2025-10-24 02:35:34 +02:00
pancake 53d61831b2 Initial support for vscode stype prompt files 2025-10-24 02:31:03 +02:00
pancake cbcff6b9dd Add r2ai -E in the cli 2025-10-24 02:29:34 +02:00
pancake f04bf8162e Add the manpage for r2ai.1 2025-10-24 02:04:36 +02:00
pancake 0206ceb390 Add support for r2ai -E and ~/.config/r2ai/rc 2025-10-24 02:00:15 +02:00
pancake ff1f788c48 Minor fix 2025-10-24 01:36:58 +02:00
pancake df135b2c70 Use jan-nano by default and fix the undefined in decai 2025-10-24 01:27:24 +02:00
pancake c152777bbe Dont do rawtools by default yet 2025-10-23 19:06:09 +02:00
pancake 3e084ce797 Implement r2ai.auto.raw to use tool calling with non-tools models 2025-10-23 02:12:36 +02:00
pancake 7fce8788b5 Release 1.2.0 2025-10-21 20:49:18 +02:00
pancake 585e30470e Aim to fix the execute_js tool for r2ai -a 2025-10-21 20:48:28 +02:00
pancake 5594e64ef3 r2ai-q must be public on windows 2025-10-21 20:44:07 +02:00
pancake 904a356290 Update READMEs 2025-10-21 20:40:17 +02:00
pancake d1e2bcf911 Use the powershell http fallback when needed 2025-10-21 20:38:10 +02:00
pancake 21528f7ea8 Use ollama/gpt-oss:20b as default provider/model pair 2025-10-21 20:34:58 +02:00
pancake 2883d134aa Add a WIP decompiler prompt 2025-10-21 20:18:58 +02:00
pancake ec96ba3329 Improve the behaviour of 'r2ai -m' to show, list and select models 2025-10-21 20:12:43 +02:00
pancake f92ea17b01 Move r2ai -n into r2ai -q autoname 2025-10-21 20:07:03 +02:00
pancake 2dc92031e7 Move r2ai -v into r2ai -q varnames 2025-10-21 20:03:16 +02:00
pancake 55d54b4953 Move r2ai -s into r2ai -q signature 2025-10-21 19:58:16 +02:00
pancake 35460e639c Move r2ai -x into r2ai -q explain 2025-10-21 19:49:47 +02:00
pancake a5382c17c4 Move r2ai -V to 'r2ai -q vulns' 2025-10-21 19:41:10 +02:00
pancake f4bdb847db Fix null deref when ^C in r2ai 2025-10-21 19:40:40 +02:00
pancake a7c3d3767c Use r2 -NNH instead of r2 -H to fix build when r2 have broken plugins 2025-10-21 19:29:09 +02:00
pancake 97c47ab28a Add -m, -p, -q to choose model, provider and query from the cli
* Fix the ollama provider
2025-10-21 19:24:11 +02:00
pancake c36bc1500c Dont pass R2AIState if we can pass the RCorePluginSession 2025-10-21 18:58:16 +02:00
pancake 179cb6ca58 Remove statistics global variable 2025-10-21 18:45:49 +02:00
pancake 402c31cc96 Handle -v and -h in the r2ai executable 2025-10-21 18:32:47 +02:00
pancake be14729424 Stop using the deprecated r_cons_singleton 2025-10-21 18:28:09 +02:00
pancake aa9b8e0fba Refactoring with the aim to reduce LOCs in r2ai_http.c 2025-10-21 18:24:09 +02:00
pancake 96225cd555 Update AGENTS.md 2025-10-21 17:45:39 +02:00
pancake 321ca1bb41 Move the LLM related functions from r2ai.c into llm.c 2025-10-21 17:29:14 +02:00
pancake d94ed5a088 Fix r2ai's main and use cps everywhere 2025-10-21 16:40:57 +02:00
pancake 3537d006de Added support to query custom user prompts in r2ai 2025-10-21 00:10:28 +02:00
pancake bdcee9e736 Remove global states, requires r2-6.0 at least 2025-10-20 21:54:48 +02:00
pancake 074b7a6c9f Add AGENTS.md for r2ai.c 2025-10-20 21:25:24 +02:00
pancake 6697df8437 Use RList instead of vector for messages. Fix message count in r2ai -L 2025-10-20 21:04:30 +02:00
pancake ff0fb4c802 Add version information in decai help message and use rcons 2025-10-20 19:49:40 +02:00
pancake ca7d02e03c Refactor the api/providers logic for easier maintainance 2025-10-20 17:35:43 +02:00
pancake bd105b2b0e Fix #176 - Do not accept invalid values in "r2ai -e api=" 2025-10-20 16:57:53 +02:00
pancake 5e18525403 Use clang-format-radare2 instead of the hacky indent.py 2025-10-20 16:48:42 +02:00
pancake c9d0756e74 Fallback to powershell when curl is not found on Windows 2025-10-20 11:05:20 +02:00
Abhi d3fc79f9d1 Fix tmp dir on android(termux) 2025-10-14 09:27:50 +02:00
pancake 8f4c972beb Remove pkg-config hard dependency for r2 >= 6.0.5 2025-10-08 13:51:34 +02:00
pancake f95cda42f7 Release 1.1.2 2025-10-06 23:25:27 +02:00
pancake b241f30432 Fix leak in openai_url 2025-10-06 23:25:01 +02:00
pancake 8eb50e466b Include the r2ai plugin in the meson ci builds 2025-10-06 23:22:01 +02:00
pancake 14b5dcbfb9 Fix make install and warnings 2025-10-06 22:59:22 +02:00
cryptax e264f8f0c8 More references In the README.md 2025-10-06 15:45:10 +02:00
pancake 46c2ce6c62 Fix r2ai install when old symlinks are around 2025-10-03 22:34:36 +02:00
pancake 693ad9270c Handle ollama errors 2025-10-03 18:21:35 +02:00
116 changed files with 7529 additions and 4778 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- buildsystem
+15 -44
View File
@@ -1,8 +1,5 @@
name: CI and Release
env:
R2V: 6.0.4
on:
push:
branches:
@@ -26,7 +23,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup Meson/Ninja
if: matrix.build_system == 'meson'
@@ -34,38 +31,9 @@ jobs:
python -m pip install --upgrade pip
pip install meson ninja
- name: Install radare2 on Linux
if: matrix.os == 'ubuntu-24.04'
run: |
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2_${R2V}_amd64.deb"
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2-dev_${R2V}_amd64.deb"
sudo dpkg -i "radare2_${R2V}_amd64.deb"
sudo dpkg -i "radare2-dev_${R2V}_amd64.deb"
- name: Install radare2 on macOS
if: matrix.os == 'macos-latest'
run: |
git clone --depth=1 --branch "${{env.R2V}}" https://github.com/radareorg/radare2
cd radare2
sys/install.sh
- name: Install radare2 on Windows
if: matrix.os == 'windows-latest'
shell: powershell
run: |
$url = "https://github.com/radareorg/radare2/releases/download/${{ env.R2V }}/radare2-${{ env.R2V }}-w64.zip"
$dest = "$env:TEMP\radare2.zip"
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
$prefix = 'C:\radare2'
if (Test-Path $prefix) { Remove-Item -Path $prefix -Recurse -Force }
Expand-Archive -Path $dest -DestinationPath $prefix -Force
# Move contents from subdirectory if present
$subdirs = Get-ChildItem -Path $prefix -Directory
if ($subdirs.Count -eq 1) {
$subdir = $subdirs[0]
Get-ChildItem -Path $subdir.FullName | Move-Item -Destination $prefix -Force
Remove-Item -Path $subdir.FullName -Recurse -Force
}
- name: Install radare2
id: r2
uses: radareorg/github-actions@v1
- name: Build with Make
if: matrix.build_system == 'make'
@@ -75,7 +43,7 @@ jobs:
if: matrix.build_system == 'meson' && matrix.os != 'windows-latest'
run: |
cd src
meson setup build -Dr2_prefix=${{ secrets.R2_PREFIX }} || meson configure build
meson setup build || meson configure build
meson compile -C build
- name: Build with Meson (Windows)
@@ -83,7 +51,7 @@ jobs:
shell: powershell
run: |
cd src
$prefix = 'C:\radare2'
$prefix = "${{ steps.r2.outputs.prefix }}"
meson setup build --backend ninja --prefix="$prefix" -Dr2_prefix="$prefix"
if ($LASTEXITCODE -ne 0) { meson configure build }
meson compile -C build
@@ -93,8 +61,11 @@ jobs:
shell: bash
run: |
mkdir -p release
cp -rf src/r2ai.dll src/r2ai.exe src/r2ai src/r2ai.dylib src/r2ai.so release/ || true
cp -rf src/build/r2ai src/build/r2ai.dylib src/build/r2ai.so release/ || true
# Collect common binary artefact names produced by Make/Meson
# Include both "r2ai.*" and "libr2ai.*" variants for DLL/dylib/so on all platforms.
find src -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
# Also check top-level build output directories
find . -maxdepth 3 -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
cp -f decai/decai.r2.js release/ || true
if command -v zip >/dev/null 2>&1; then
zip -j "release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip" release/*
@@ -104,7 +75,7 @@ jobs:
- name: Upload artifacts (tags only)
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
@@ -124,7 +95,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Verify tag is on master
run: |
@@ -135,7 +106,7 @@ jobs:
fi
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: release-artifacts
@@ -143,7 +114,7 @@ jobs:
run: cp decai/decai.r2.js release-artifacts/
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: |
release-artifacts/**/*.zip
File renamed without changes.
File renamed without changes.
+14
View File
@@ -0,0 +1,14 @@
SHELL=/bin/bash
MODELS=gpt-oss:20b gemma3:4b gemma3:12b
MODELS+=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
MODELS=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
LANGS=Dart Perl Ruby Javascript PHP C
all:
for a in ${MODELS} ; do \
declare -a ARGS; \
for l in ${LANGS} ; do \
ARGS+=("echo \"decai -e model=$$a;decai -e lang=$$l;s 0x100000924;af;decai -d\" | r2 -q /bin/ls"); \
done ; \
./run.sh "$${ARGS[@]}" > ${a//\//_}.html ; done
File renamed without changes.
@@ -1,6 +1,5 @@
#!/bin/bash
cp decai/BENCH.txt tmp/BENCH.txt
# Find all directories containing BENCH.txt
dirs=$(find . -name "BENCH.txt" -type f | xargs dirname | sort | uniq)
@@ -47,4 +46,4 @@ cat <<EOF >> benchmark_index.html
<br />
<br />
</html>
EOF
EOF
@@ -1,55 +1,12 @@
#!/bin/bash
# Generate HTML output for command executions with collapsible sections and width control
# If PLAIN=1, output plain text instead
if [ $# -lt 4 ]; then
echo "Usage: $0 <binary> <model> <provider> <lang1> [lang2 ...]"
echo "Or: $0 command1 [command2 ...] (legacy)"
if [ $# -eq 0 ]; then
echo "Usage: $0 command1 [command2 ...]"
exit 1
fi
# Check if first arg is a file (binary), assume new format
if [ -f "$1" ]; then
binary="$1"
model="$2"
provider="$3"
shift 3
langs=("$@")
commands=()
for lang in "${langs[@]}"; do
# commands+=("# $lang echo \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" | r2 -q $binary 2> /dev/null")
commands+=("# $lang r2 -e scr.color=0 -qcq -qc \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" $binary")
done
set -- "${commands[@]}"
else
# Legacy mode
commands=("$@")
fi
if [ "$PLAIN" = "1" ]; then
# In plain mode, execute each command and print raw output.
# Also, if TXTFILE is set, save combined raw output there.
tmp_plain=$(mktemp)
for cmd in "$@"; do
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
exec_cmd="${BASH_REMATCH[1]}"
else
exec_cmd="$cmd"
fi
tmpout=$(mktemp)
eval "$exec_cmd" > "$tmpout" 2>&1 || true
mv "$tmpout" "$tmp_plain"
rm -f "$tmpout"
done
if [ -n "$TXTFILE" ]; then
mv "$tmp_plain" "$TXTFILE"
else
rm -f "$tmp_plain"
fi
exit 0
fi
# Timeout in seconds for each command (configurable via TIMEOUT environment variable)
TIMEOUT=${TIMEOUT:-30}
@@ -152,27 +109,24 @@ EOF
# Process each command
for cmd in "$@"; do
# Extract title: if command starts with #, parse title and command, else use the command
if [[ "$cmd" =~ ^#[[:space:]]*([^[:space:]]+)[[:space:]]*(.*) ]]; then
# Extract title: if command starts with #, use the rest as title, else use the command
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
title="${BASH_REMATCH[1]}"
exec_cmd="${BASH_REMATCH[2]}"
exec_cmd="${cmd#*#}"
exec_cmd="${exec_cmd#"${exec_cmd%%[![:space:]]*}"}" # trim leading spaces
else
title="$cmd"
exec_cmd="$cmd"
fi
echo "RUN $exec_cmd" > /dev/stderr
# Execute command and time it
start_time=$(date +%s.%N)
tmpfile=$(mktemp)
# echo "eval $exec_cmd > $tmpfile" > /dev/stder
eval "$exec_cmd" > "$tmpfile" 2> /dev/null &
eval "$exec_cmd" > "$tmpfile" 2>&1 &
pid=$!
(sleep $TIMEOUT && kill $pid 2>/dev/null) &
wait $pid 2>/dev/null
output=$(cat "$tmpfile")
#echo "$output" > /dev/stderr
rm "$tmpfile"
exit_code=$?
end_time=$(date +%s.%N)
@@ -185,16 +139,8 @@ for cmd in "$@"; do
execution_time=$(printf "%.3f" "$execution_time")
fi
# Save plain output to .txt file if in new format
if [ -n "$binary" ]; then
txtfile="tmp/${binary##*/}_${model//\//_}_${title}.txt"
mkdir -p "$(dirname "$txtfile")" 2>/dev/null || true
echo "$output" > "$txtfile"
fi
# Escape HTML in output
#escaped_output=$(echo "$output" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
escaped_output="$output"
escaped_output=$(echo "$output" | sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g')
cat << EOF
<div class="command-section expanded">
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
-1
View File
@@ -1 +0,0 @@
recursive-include r2ai/ui *.tcss
+3 -1
View File
@@ -1,4 +1,6 @@
TARGETS=all clean mrproper user-install install user-uninstall uninstall
TARGETS=all clean mrproper user-install install user-uninstall uninstall fmt indent
$(TARGETS):
$(MAKE) -C src $@
.PHONY: $(TARGETS)
+38 -19
View File
@@ -19,14 +19,17 @@
This repository contains two plugins for radare2:
* **r2ai** - native plugin for radare2
* **decai** - r2js plugin with special focus on decompilation
* **r2ai** - native [AI plugin](./src/README.md) for radare2
* **decai** - r2js plugin with special [focus on decompilation](./decai/README.md)
If you are looking to use radare2 with other agents via MCP:
* **r2mcp** - the [official radare2 mcp](https://github.com/radare2/radare2-mcp)
* **r2copilot** - the mcp with focus on CTF [r2copilot](https://github.com/darallium/r2-copilot)
If you are looking for a radare2 focused autonomous agent:
* **r2agent** - automate radare2 workflows through autonomous agents. [r2agent](https://github.com/nitanmarcel/r2agent)
## Features
* Configure different roles and customize prompts
@@ -37,12 +40,21 @@ If you are looking to use radare2 with other agents via MCP:
* RAG markdown, code or textfiles using its native vector database
* Embed the output of an r2 command and resolve questions on the given data
## Practical Examples
## User defined Prompts
* Enhanced decompilation with `r2ai -d`
* Autoname functions with `r2ai -n`
* Explain function with `r2ai -x`
* Find vulnerabilities with `r2ai -V`
```console
[0x00000000]> r2ai -q
explain: Explain the current function -
devices: Find and explain devices used -
libs: Group imports by Libraries -
varnames: Better variable names -
autoname: Automatically suggest a better name for this function -
vulns: Find vulnerabilities or bugs in the current function -
signature: Suggest an improved function signature -
dlopen: List libraries loaded with dlopen - Some libraries are loaded
decompile: Augmented decompilation based on LLM -
[0x00000000]>
```
## Installation
@@ -58,39 +70,46 @@ $ r2pm -Uci decai
- Adds the **r2ai** command to the radare2 shell: `r2 -qc r2ai`
- You can also run the wrapper in $PATH: `r2pm -r r2ai`
Drop your API keys in environment variables or files in your home:
Drop your API keys in environment variables or use the configuration file:
```console
$ cat ~/.r2ai.anthropic-key
sk-ant-api03-CENSORED
$ export ANTHROPIC_API_KEY=sk-ant-api03-CENSORED
$ export OPENAI_API_KEY=sk-proj-6rlSPS-zN1v...
```
| AI | API key |
| --------- | -------------------------- |
| OpenAI | `$HOME/.r2ai.openai-key` |
| Gemini | `$HOME/.r2ai.gemini-key` |
| Anthropic | `$HOME/.r2ai.anthropic-key` |
| Mistral | `$HOME/.r2ai.mistral-key` |
Or edit the api keys file `~/.config/r2ai/apikeys.txt` run:
```console
$ r2ai -K
```
## Saving settings
You may customize and save your configuration settings using your OS's default settings file (e.g `~/.radare2rc` on Linux).
For example, the following configuration sets Claude 3.7 by default, with max output tokens to 64000.
```console
$ r2ai -E
```
then you can type the commands you want to run when the r2ai plugin is loaded:
```console
r2ai -e api=anthropic
r2ai -e model=claude-3-7-sonnet-20250219
r2ai -e max_tokens=64000
```
## Documentation
## Further Reading
* There's [a chapter](https://book.rada.re/plugins/r2ai.html) in the official r2book
* Cryptax on [lmstudio+gptoss](https://cryptax.medium.com/r2ai-with-lmstudio-and-gpt-oss-08efa5ea2476) blog post
* Malware analysis [with r2ai](https://github.com/cryptax/talks/blob/master/BSidesKristiansand-2025/r2ai.pdf) by Cryptax
* Malware analysis [with r2ai](https://arxiv.org/pdf/2504.07574) by Cryptax and Daniel Nakov
* Analysis of [Linux/Trigona ransomware](https://cryptax.medium.com/linux-trigona-analysis-with-r2ai-3e2bd1815e52), [Linux/Prometei botnet](https://cryptax.medium.com/reversing-a-prometei-botnet-binary-with-r2-and-ai-part-one-3cdb3dc6ffab) and [W32/SkyAI](https://cryptax.medium.com/w32-skyai-uses-ai-so-do-i-d33f04d63534with) with r2ai
## Videos
- https://infosec.exchange/@radareorg/111946255058894583
- [Solving a crackme](https://infosec.exchange/@radareorg/111946255058894583)
- [De-obfuscation of malware Linux/Ladvix](https://asciinema.org/a/724126)
- [Analysis of the /fast option inside Linux/Trigona ransomware](https://asciinema.org/a/pBPEaJhp6cunWSKFpBUDTgPt4)
+1 -1
View File
@@ -1 +1 @@
1.1.1
1.3.6
+4 -1
View File
@@ -1,3 +1,6 @@
#!/bin/sh
vim src/r2ai.c
vim decai/src/constants.ts
make -C decai/src
vim src/r2ai.h
vim src/meson.build
vim VERSION
+7
View File
@@ -0,0 +1,7 @@
# Decompiler based AI
## Actions
* Build: `make -C src`
* Install: `make user-install`
* Test: `r2 -A -qc "decai -e api=ollamacloud;decai -e model=glm-5;decai -d @ 0x100000924" /bin/ls`
+5 -6
View File
@@ -1,17 +1,16 @@
PD=$(shell r2 -H R2_USER_PLUGINS)
SPD=$(shell r2 -H R2_LIBR_PLUGINS)
#BF=/bin/dd
BF=a.out
BF=/bin/dd
all:
@echo "Run: make user-install"
@echo "Run: 'make user-install' to install it in your home"
@echo "Run: 'make -C src' to recompile the r2.js"
test:
@echo 'You can now use 'decai' command'
r2 -i decai.r2.js -c 'decai -e deterministic=true' -caf $(BF)
fmt indent:
deno fmt
$(MAKE) -C src fmt
user-install:
mkdir -p "$(PD)"
@@ -27,4 +26,4 @@ uninstall:
make user-uninstall PD=$(SPD)
vs open:
open -a "Visual Studio Code" .
open -a "Visual Studio Code" src
+14
View File
@@ -37,6 +37,7 @@ Usage: decai (-h) ...
decai -dd [..] - same as above, but ignoring cache
decai -dD [query]- decompile current function with given extra query
decai -e - display and change eval config vars
decai -E - edit decai.txt
decai -h - show this help
decai -i [f] [q] - include given file and query
decai -n - suggest better function name
@@ -59,6 +60,7 @@ decai -e host=http://localhost
decai -e port=11434
decai -e prompt=Rewrite this function and respond ONLY with code, NO explanations, NO markdown, Change 'goto' into if/else/for/while, Simplify as much as possible, use better variable names, take function arguments and strings from comments like 'string:'
decai -e ctxfile=
decai -e headers=
decai -e cmds=pdc
decai -e cache=false
decai -e lang=C
@@ -96,6 +98,10 @@ decai -e ctxfile=
...
```
Persist user defaults in `~/.config/r2ai/decai.txt` with one `key=value` entry
per line, then edit it with `decai -E`. That file is loaded on the first
`decai` command in a session and reloaded after `decai -E` exits.
List possible APIs to discuss with AI: `decai -e api=?`:
```
@@ -106,6 +112,14 @@ openapi
...
```
Extra headers can be injected globally with `decai -e headers=...` or from the
environment with `DECAI_HEADERS` / `R2AI_HEADERS`. Use one header per line, for
example:
```console
[0x00406cac]> decai -e headers=Authorization: Bearer sk-123\nUser-Agent: curl/8.7.1
```
### Example using a local model and ollama
For example, if Ollama serves model codegeex4:latest (`ollama ls`), set decai
+58 -1244
View File
File diff suppressed because it is too large. Load diff
+18
View File
@@ -0,0 +1,18 @@
PLUGIN_NAME=decai
R2F_C_FLAGS=-c -S
all: build
build:
r2pm -r r2frida-compile $(R2F_C_FLAGS) -o ../$(PLUGIN_NAME).r2.js main.ts
test: build
r2 -i ../$(PLUGIN_NAME).r2.js -q -c 'af;decai -d' /bin/ls
fmt:
deno fmt
clean:
rm -rf out node_modules ../$(PLUGIN_NAME).r2.js
.PHONY: all build test clean
+63
View File
@@ -0,0 +1,63 @@
import { API_KEYS_PATH, DECAI_CONFIG_DIR } from "./constants";
import { ApiKeyResult } from "./types";
import { ensurePath, fileExists, parseEnvLikeString } from "./utils";
const PROVIDER_ENV_MAP: Record<string, string> = {
mistral: "MISTRAL_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
huggingface: "HUGGINGFACE_API_KEY",
openai: "OPENAI_API_KEY",
gemini: "GEMINI_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
xai: "XAI_API_KEY",
ollama: "OLLAMA_API_KEY",
ollamacloud: "OLLAMA_API_KEY",
opencode: "OPENCODE_API_KEY",
zen: "OPENCODE_API_KEY",
openrouter: "OPENROUTER_API_KEY",
groq: "GROQ_API_KEY",
};
export function getApiKey(provider: string, envvar: string): ApiKeyResult {
const keyEnv = r2.cmd("'%" + envvar).trim();
if (!keyEnv.includes("=") && keyEnv !== "") {
return [keyEnv.trim(), null, "env"];
}
const providerLower = provider.toLowerCase();
const keysPath = API_KEYS_PATH;
if (fileExists(keysPath)) {
const keyFile = r2.cmd("'cat " + keysPath);
const kv = parseEnvLikeString(keyFile);
if (providerLower in kv) {
return [kv[providerLower], null, "txt"];
}
}
const keyPath = "~/.r2ai." + providerLower + "-key";
if (fileExists(keyPath)) {
const keyFile = r2.cmd("'cat " + keyPath);
return keyFile === ""
? [null, "Cannot read " + keyPath, "no"]
: [keyFile.trim(), null, "file"];
}
return [null, "Not available", "nope"];
}
export function editApiKeys(): void {
ensurePath(DECAI_CONFIG_DIR, API_KEYS_PATH);
r2.cmd("'ed " + API_KEYS_PATH);
}
export function listApiKeys(): void {
Object.entries(PROVIDER_ENV_MAP).forEach(([key, env]) => {
const status = getApiKey(key, env)[2];
console.log(status, "\t", key);
});
}
export function getProviderEnvVar(provider: string): string {
return PROVIDER_ENV_MAP[provider.toLowerCase()] || "";
}
+542
View File
@@ -0,0 +1,542 @@
import { AutoReply } from "./types";
import { state } from "./state";
import { COMMAND, DEFAULT_PROMPT, HELP_TEXT, VERSION } from "./constants";
import {
b64,
debugLog,
filterResponse,
trimAnsi,
trimDown,
trimJson,
} from "./utils";
import { editApiKeys, listApiKeys } from "./apiKeys";
import { editRcConfig, evalConfig, listAllConfig } from "./config";
import { r2ai } from "./r2ai";
export function showHelp(): void {
let helpmsg = "";
const msg = (m: string) => (helpmsg += " " + COMMAND + " " + m + "\n");
helpmsg += "Usage: " + COMMAND + " (-h) ...\n";
helpmsg += "Version: " + VERSION + "\n";
msg("-a [query] - solve query with auto mode");
msg("-b [url] - set base URL (alias for decai -e baseurl)");
msg("-d [f1 ..] - decompile given functions");
msg("-dd [..] - same as above, but ignoring cache");
msg("-dD [query] - decompile current function with given extra query");
msg("-dr - decompile function and its called ones (recursive)");
msg("-e - display and change eval config vars");
msg("-E - edit and run decai.txt");
msg("-h - show this help");
msg("-H - help setting up r2ai");
msg("-i [f] [q] - include given file and query");
msg("-k - list API key status");
msg("-K - edit apikeys.txt");
msg("-m [model] - use -m? or -e model=? to list the available models");
msg("-n - suggest better function name");
msg("-p [provider] - same as decai -e api (will be provider)");
msg("-q [text] - query language model with given text");
msg("-Q [text] - query on top of the last output");
msg("-r [prompt] - change role prompt (same as: decai -e prompt)");
msg("-R - reset role prompt to default prompt");
msg("-s - function signature");
msg("-v - show local variables");
msg("-V - find vulnerabilities");
msg("-x[*] - eXplain current function (-x* for r2 script)");
r2.log(helpmsg.trim());
}
export function decompile(
args: string,
extraQuery: boolean,
useCache: boolean,
recursiveCalls: boolean,
): string | undefined {
if (useCache) {
const cachedAnnotation = r2.cmd("anos").trim();
if (cachedAnnotation.length > 0) {
return cachedAnnotation;
}
}
let context = "";
if (recursiveCalls) {
const at = r2.cmd("s");
context += "## Context functions:\n";
const funcs = r2.cmdAt("axff~^C[2]~$$", at);
for (const addr of funcs.split(/\n/g)) {
context += r2.cmd("pdc@" + addr);
}
r2.cmd("s " + at);
}
const appendQuery = extraQuery ? " " + args : "";
const origColor = r2.cmd("e scr.color");
try {
const parsedArgs = args.slice(2).trim();
let count = 0;
let text = "";
if (
state.contextFile !== "" &&
r2.cmd2("test -f " + state.contextFile).value === 0
) {
text += "## Context:\n[START]\n" +
r2.cmd("cat " + state.contextFile) +
"\n[END]\n";
}
r2.cmd("e scr.color=0");
let body = "## Before:\n";
for (const c of state.commands.split(",")) {
if (c.trim() === "") continue;
const oneliner = extraQuery || parsedArgs.trim().length === 0
? c
: c + "@@= " + parsedArgs;
const output = r2.cmd(oneliner);
if (output.length > 5) {
body += "Output of " + c + ":\n[START]\n" + output + "\n[END]\n";
count++;
}
}
body += "## After:\n";
r2.cmd("e scr.color=" + origColor);
if (count === 0) {
console.error("Nothing to do.");
return;
}
let out = "";
if (state.decopipe.use) {
const origModel = state.model;
try {
const dpipe = state.decopipe[state.decopipe.default as string] as {
pipeline: Array<{ model?: string; query: string }>;
globalQuery: string;
};
let code = text + body;
for (const dp of dpipe.pipeline) {
if (dp.model) state.model = dp.model;
const query = dp.query + ". " + dpipe.globalQuery;
out = r2ai(query, code, true);
if (state.debug) {
console.log("QUERY\n", query, "\nINPUT\n", code, "\nOUTPUT\n", out);
}
code = out;
}
out = code;
} finally {
state.model = origModel;
}
} else {
const query = appendQuery;
text += body + context;
out = r2ai(query, text, false);
state.lastOutput = out;
}
if (useCache && out.length > 1) {
r2.call("ano=base64:" + b64(out));
}
if (out.startsWith("```")) {
out = out.replace(/```.*\n/, "").replace(/```$/, "");
}
return out.trim();
} catch (e) {
r2.cmd("e scr.color=" + origColor);
const err = e as Error;
console.error(err, err.stack);
return;
}
}
export function explainFunction(): string {
const origColor = r2.cmd("e scr.color");
r2.cmd("e scr.color=0");
const hints = "[START]" +
state.commands.split(",").map((c) => r2.cmd(c)).join("\n") + "[END]";
r2.cmd("e scr.color=" + origColor);
const res = r2ai(
"Analyze function calls, references, comments and strings, loops and ignore registers and memory accesses. Explain the purpose of this function in a single short sentence. /no_think Do not introduce or argue the response, translation of the explanation in " +
state.humanLanguage,
hints,
true,
);
const lines = res.trim().split(/\n/g);
return lines[lines.length - 1].trim();
}
export function suggestSignature(): string {
const tmp = state.language;
const code = r2.cmd("afv;pdc");
state.language = "C";
let out = "'afs " +
r2ai(
"analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the return. Do NOT print the function body, ONLY output the function signature, like if it was going to be used in a C header",
code,
false,
);
const brace = out.indexOf("{");
if (brace !== -1) {
out = out.substring(0, brace);
}
state.language = tmp;
return out;
}
export function autoMode(
queryText: string,
mainHandler: (args: string) => boolean,
): void {
const replies: string[] = [];
while (true) {
let q = HELP_TEXT.auto;
if (replies.length > 0) {
q += "## Command Results\n\n";
for (const rep of replies) {
const rp = JSON.parse(rep);
q += "### " + rp.command + "\n\n```\n" + rp.response + "\n```\n";
}
}
q += "\n\n## User Prompt\n\n" + queryText;
if (state.debug) {
console.log("#### input\n", q, "\n#### /input");
}
console.log("Thinking...");
const out = r2ai("", q, true);
if (state.debug) {
console.log("#### output\n", out, "\n#### /output");
}
try {
const o: AutoReply = JSON.parse(
trimJson(trimDown(filterResponse(out))),
);
if (
o.action === "r2cmd" ||
o.action === "response" ||
o.action === o.command
) {
const ocmd = o.command || "";
if (o.reason) {
console.log("[r2cmd] Reasoning: " + o.reason);
if (state.tts) {
r2.syscmd("pkill say");
r2.syscmd(
"say -v Alex -r 250 '" + o.reason.replace(/'/g, "") + "' &",
);
}
}
console.log("[r2cmd] Action: " + o.description);
console.log("[r2cmd] Command: " + ocmd);
let cmd = ocmd;
if (!state.yolo) {
cmd = autoRepl(ocmd, mainHandler);
}
console.log("[r2cmd] Running: " + cmd);
const obj = r2.cmd2(cmd);
const logs = obj.logs
? obj.logs.map((x: { type: string; message: string }) =>
x.type + ": " + x.message
).join("\n")
: "";
const res = (obj.res + logs).trim();
console.log(res);
const cleanRes = trimAnsi(res);
if (state.debug) {
console.log("<r2output>\n", cleanRes, "\n<(r2output>");
}
replies.push(
JSON.stringify({
action: "response",
command: cmd,
description: o.description,
response: cleanRes,
}),
);
} else if (o.action === "reply") {
console.log("Done\n", o.response);
break;
} else {
console.log("Unknown response\n", JSON.stringify(out));
break;
}
} catch (e) {
const response = out.indexOf('response": "');
if (response !== -1) {
const res = out
.slice(response + 12)
.replace(/\\n/g, "\n")
.replace(/\\/g, "");
console.log(res);
} else {
console.log(out);
console.error(e);
}
break;
}
}
}
function autoRepl(
ocmd: string,
mainHandler: (args: string) => boolean,
): string {
while (true) {
const cmd = r2.cmd("'?ie Tweak command? ('?' for help)").trim();
if (cmd === "q!") {
console.error("Break!");
break;
}
if (cmd === "?") {
autoHelp();
continue;
} else if (cmd.startsWith(":")) {
console.log(r2.cmd(cmd.slice(1)));
continue;
} else if (cmd.startsWith("-e")) {
mainHandler(cmd);
continue;
} else if (cmd === "!") {
return "?e do NOT execute '" + ocmd + "' again, continue without it";
} else if (cmd.startsWith("!")) {
console.log(r2.syscmd(cmd.slice(1)));
continue;
} else if (cmd === "q") {
return "?e All data collected!. Do not call more commands, reply the solutions";
} else if (!cmd) {
return ocmd;
} else {
const comment = cmd.indexOf("#");
if (comment !== -1) {
const command = cmd.slice(0, comment).trim();
return command;
}
return cmd;
}
}
return ocmd;
}
function autoHelp(): void {
console.log(" '!' do not run this command");
console.log(" '!c' run system command");
console.log(" 'q' to quit auto and try to solve");
console.log(" 'q!' quit auto without solving");
console.log(" 'c # C' use given command with comment");
console.log(" ':c' run r2 command without feeding auto");
console.log(" '-e' set decai configuration variables");
}
function getFlagValue(args: string): string {
return args.slice(2).trim();
}
export function handleCommand(
args: string,
mainHandler: (args: string) => boolean,
): string | undefined {
if (args === "" || !args.startsWith("-")) {
showHelp();
return;
}
let output = "";
const flag = args[1];
const argValue = getFlagValue(args);
switch (flag) {
case "H":
console.log(HELP_TEXT.decai);
break;
case "a":
autoMode(argValue, mainHandler);
break;
case "m": {
if (argValue === "=") {
evalConfig("model=");
} else if (argValue) {
evalConfig("model=" + argValue);
} else {
evalConfig("model");
}
break;
}
case "n":
case "f": {
output = r2.cmd("axff~$[3]");
const considerations = r2
.cmd("fd.")
.trim()
.split(/\n/)
.filter((x: string) => !x.startsWith("secti"))
.join(",");
output = r2ai(
"give me a better name for this function. the output must be: 'afn NEWNAME'. do not include the function code, only the afn line. consider: " +
considerations,
output,
false,
).trim();
output += " @ " + r2.cmd("?v $FB").trim();
break;
}
case "v":
output = r2.cmd("afv;pdc");
output = r2ai(
"guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",
output,
false,
);
break;
case "i": {
const parts = argValue.split(/\s+/).filter(Boolean);
if (parts.length >= 2) {
const file = parts[0];
const query = parts.slice(1).join(" ");
const fileData = r2.cmd("cat " + file);
console.log(r2ai(query, fileData, true));
} else {
console.log("Use: decai -i [file] [query ...]");
}
break;
}
case "p": {
if (argValue) {
evalConfig("api=" + argValue);
} else {
listApiKeys();
}
break;
}
case "r": {
if (argValue) {
state.prompt = argValue;
} else {
console.log(state.prompt);
}
break;
}
case "R":
state.prompt = DEFAULT_PROMPT;
break;
case "s":
output = suggestSignature();
break;
case "V":
output = r2ai(
"find vulnerabilities, dont show the code, only show the response, provide a sample exploit",
state.lastOutput,
false,
);
break;
case "K":
editApiKeys();
break;
case "E":
editRcConfig();
break;
case "k":
listApiKeys();
break;
case "b": {
if (argValue) {
evalConfig("baseurl=" + argValue);
} else {
console.log(state.baseurl);
}
break;
}
case "e": {
if (argValue) {
evalConfig(argValue);
} else {
listAllConfig();
}
break;
}
case "q":
try {
output = r2ai(argValue, null, true);
} catch (e) {
const err = e as Error;
console.error(err, err.stack);
}
break;
case "Q":
output = r2ai(argValue, state.lastOutput, false);
break;
case "x":
output = explainFunction();
if (args[2] === "*" || args[2] === "r") {
output = "'CC " + output;
}
break;
case "d":
if (args[2] === "r") {
output = decompile(args.slice(2), true, state.cache, true) || "";
} else if (args[2] === "d") {
output = decompile(args, false, false, false) || "";
} else if (args[2] === "D") {
output = decompile(args, true, false, false) || "";
} else {
output = decompile(args, false, state.cache, false) || "";
}
break;
default:
showHelp();
break;
}
return output || undefined;
}
+215
View File
@@ -0,0 +1,215 @@
import { ConfigHandlers } from "./types";
import { DECAI_CONFIG_DIR, DECAI_CONFIG_PATH } from "./constants";
import { defaultState, state } from "./state";
import { listModelsFor } from "./models";
import { listProviders } from "./providers";
import { formatHeaders, parseHeaders } from "./headers";
import { ensurePath, fileExists } from "./utils";
let rcConfigLoaded = false;
function parseBoolean(value: string): boolean {
return value === "true" || value === "1";
}
export const configHandlers: ConfigHandlers = {
pipeline: {
get: () => state.pipeline,
set: (v: string) => {
state.pipeline = v;
try {
state.decopipe = JSON.parse(r2.cmd("cat " + v));
} catch (e) {
console.error(e);
}
},
},
model: {
get: () => state.model,
set: (v: string) => {
if (v === "?") {
listModelsFor(state.api);
} else {
state.model = v.trim();
}
},
},
deterministic: {
get: () => state.deterministic,
set: (v: string) => {
state.deterministic = parseBoolean(v);
},
},
files: {
get: () => state.useFiles,
set: (v: string) => {
state.useFiles = parseBoolean(v);
},
},
think: {
get: () => state.think,
set: (v: string) => {
state.think = v === "true" ? 1 : v === "false" ? 0 : +v;
},
},
debug: {
get: () => state.debug,
set: (v: string) => {
state.debug = parseBoolean(v);
},
},
timeout: {
get: () => state.timeout,
set: (v: string) => {
state.timeout = Math.max(0, parseInt(v, 10) || 0);
},
},
api: {
get: () => state.api,
set: (v: string) => {
if (v === "?") {
const providersList = listProviders().join("\n");
console.error(providersList);
} else {
state.api = v;
}
},
},
lang: {
get: () => state.language,
set: (v: string) => {
state.language = v;
},
},
hlang: {
get: () => state.humanLanguage,
set: (v: string) => {
state.humanLanguage = v;
},
},
cache: {
get: () => state.cache,
set: (v: string) => {
state.cache = parseBoolean(v);
},
},
cmds: {
get: () => state.commands,
set: (v: string) => {
state.commands = v;
},
},
tts: {
get: () => state.tts,
set: (v: string) => {
state.tts = parseBoolean(v);
},
},
yolo: {
get: () => state.yolo,
set: (v: string) => {
state.yolo = parseBoolean(v);
},
},
prompt: {
get: () => state.prompt,
set: (v: string) => {
state.prompt = v;
},
},
ctxfile: {
get: () => state.contextFile,
set: (v: string) => {
state.contextFile = v;
},
},
baseurl: {
get: () => state.baseurl,
set: (v: string) => {
state.baseurl = v;
},
},
headers: {
get: () => formatHeaders(state.extraHeaders),
set: (v: string) => {
state.extraHeaders = parseHeaders(v);
},
},
maxtokens: {
get: () => state.maxInputTokens,
set: (v: string) => {
state.maxInputTokens = parseInt(v, 10) || -1;
},
},
};
function resetRcConfigState(): void {
const keep = { host: state.host, port: state.port, lastOutput: state.lastOutput };
Object.assign(state, defaultState, keep);
state.decopipe = { ...defaultState.decopipe };
state.extraHeaders = [...defaultState.extraHeaders];
}
function normalizeRcLine(line: string): string | null {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
return null;
}
return trimmed.startsWith("decai -e ")
? trimmed.slice("decai -e ".length).trim()
: trimmed;
}
export function evalConfig(arg: string): void {
const eqIndex = arg.indexOf("=");
const k = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
const v = eqIndex === -1 ? undefined : arg.slice(eqIndex + 1);
const handler = configHandlers[k];
if (!handler) {
console.error("Unknown config key");
return;
}
if (typeof v !== "undefined") {
handler.set(v);
} else {
console.log(handler.get());
}
}
export function listAllConfig(): void {
Object.entries(configHandlers).forEach(([key, handler]) => {
const value = handler.get();
console.log("decai -e " + key + "=" + value);
});
}
export function loadRcConfig(): void {
resetRcConfigState();
if (!fileExists(DECAI_CONFIG_PATH)) {
return;
}
const rcFile = r2.call("cat " + DECAI_CONFIG_PATH);
for (const line of rcFile.split(/\r?\n/)) {
const rcLine = normalizeRcLine(line);
if (rcLine && rcLine[0] != '#') {
evalConfig(rcLine);
}
}
}
export function ensureRcConfigLoaded(): void {
if (rcConfigLoaded) {
return;
}
loadRcConfig();
rcConfigLoaded = true;
}
export function editRcConfig(): void {
ensurePath(DECAI_CONFIG_DIR, DECAI_CONFIG_PATH);
r2.cmd("'ed " + DECAI_CONFIG_PATH);
loadRcConfig();
rcConfigLoaded = true;
}
+128
View File
@@ -0,0 +1,128 @@
export const VERSION = "1.3.6";
export const COMMAND = "decai";
export const DECAI_CONFIG_DIR = "~/.config/r2ai";
export const API_KEYS_PATH = DECAI_CONFIG_DIR + "/apikeys.txt";
export const DECAI_CONFIG_PATH = DECAI_CONFIG_DIR + "/decai.txt";
export const DEFAULT_PROMPT =
"Rewrite this pseudocode into concise and clean code. Output only the provided function. Do not add wrappers, helper examples, test code, or main-like functions. Replace goto with structured control flow, simplify as much as possible, infer types and use better names for variables and parameters, some strings may be appearing as comments, preserve only what is implied by the input, and remove dead code.";
export const HELP_TEXT = {
decai: `# Using Decai
Decai is the radare2 plugin for decompiling functions with the help of language models.
By default uses a local ollama server, but can you can pick any other service by using 'decai -e api=?'.
[0x00000000]> decai -e api=?
r2ai deepseek anthropic claude gemini groq hf lmstudio mistral ollama ollamacloud openapi opencode openai openrouter vllm xai zen
## Using Ollama
* Visit https://ollama.com to install it.
* Download the model of choice: 'ollama run llama3.3'
* Configure decai to use the given model with: 'decai -e model=?'
These are the most recommended models for decompiling in local:
* hhao/qwen2.5-coder-tools:latest (18GB of ram)
* hhao/qwen2.5-coder-tools:32b (24GB of ram required)
## Common Options
* 'decai -e baseurl=<url>' override default host and port for API endpoint (e.g., 'http://localhost:11434')
* 'decai -e headers=Authorization: Bearer ...\\nUser-Agent: curl/8.7.1' add or override HTTP headers
* "export DECAI_HEADERS='Authorization: Bearer ...\\nUser-Agent: curl/8.7.1'" set extra headers from the environment
* 'decai -e timeout=0' disable curl timeouts, or set a positive number of seconds
* 'decai -e deterministic=true' to remove randomness from decompilation responses
* 'decai -e lang=Python' to output the decompilation in Python instead of C
* 'decai -e hlang=Catalan' to add comments or explanations in that language (instead of English)
* 'decai -e cmds=pdd,pdg' use r2dec and r2ghidra instead of r2's pdc as input for decompiling
* 'decai -e prompt=..' default prompt must be fine for most models and binaries, feel free to tweak it
## API Keys
Remove services like OpenAI, Mistral, Anthropic, Grok, Gemini, .. require API keys to work.
See 'decai -k' to list the status of available APIkeys
Use 'decai -K' to edit apikeys.txt and 'decai -E' to edit decai.txt.
Decai will pick them from the environment or the config files in your home:
* echo KEY > ~/.r2ai.openai-key
* export OPENAI_API_KEY=...
* put one 'key=value' pair per line in ~/.config/r2ai/decai.txt
## Using the R2AI Server:
Install r2ai or r2ai-server with r2pm:
[0x0000000]> decai -e api=r2ai
[0x0000000]> r2pm -ci r2ai
Choose one of the recommended models (after r2pm -r r2ai):
* -m ibm-granite/granite-20b-code-instruct-8k-GGUF
* -m QuantFactory/granite-8b-code-instruct-4k-GGUF
* -m TheBloke/Mistral-7B-Instruct-v0.2-GGUF
Start the webserver:
$ r2pm -r r2ai-server -l r2ai -m granite-8...
`,
auto: `# Radare2 Auto Mode
Use function calling to execute radare2 commands in order to resolve the user request defined in the "User Prompt" section, analyze the responses attached in the "Command Results" section.
## Function Calling
Respond ONLY using plain JSON. Process user query and decide which function calls are necessary to solve the task.
1. Analyze the user request to determine if we need to run commands to extend the knowledge and context of the problem.
2. If function call is needed, construct the JSON like this:
- Fill the "action" key with the "r2cmd" value.
- Specify the "command" as a string.
- Optionally, provide a "reason" and "description"
3. If the answer can be provided and no more function calls are required:
- Use the key "action": "reply".
- Include "response" with the direct answer to the user query.
Return the result as a JSON object.
### Sample Function Calling Communication
Command Results: already performed actions with their responses
User Prompt: "Count how many functions we have here."
Response:
{
"action": "r2cmd",
"command": "aflc",
"description": "Count functions"
"reason": "Evaluate if the program is analyzed before running aaa"
}
## Rules
Use radare2 to resolve user requests.
* Explain each step in the "reason" field of the JSON.
* Follow the initial analysis instructions.
* Output only valid JSON as specified.
* Decompile and inspect functions, starting from main.
* Run only the needed commands to gather info.
* Use "@ (address|symbol)" to seek temporarily.
* Output should be a verbose markdown report.
* Use "sym." or "fcn." prefixes if "pdc" is empty.
* If a seek fails, use "f~name" to find the symbol's address.
### Initial Analysis
1. Run "aflc" to count the number of functions
2. If the output of "aflc" is "0" run "aaa" once, then "aflc" again
3. Run only one command at a time (do not use ";")
### Special cases
* On Swift binaries run...
`,
};
+100
View File
@@ -0,0 +1,100 @@
import { state } from "./state";
const HEADER_ENV_VARS = ["DECAI_HEADERS", "R2AI_HEADERS"];
interface ParsedHeader {
name: string;
value: string;
}
function readEnvVar(name: string): string {
const value = r2.cmd("'%" + name).trim();
return value !== "" && !value.includes("=") ? value : "";
}
function normalizeHeaderName(name: string): string {
return name.trim().toLowerCase();
}
function parseHeaderLine(line: string): ParsedHeader | null {
const colon = line.indexOf(":");
const equals = line.indexOf("=");
const sep = colon !== -1 && (equals === -1 || colon < equals)
? colon
: equals;
if (sep === -1) {
return null;
}
const name = line.slice(0, sep).trim();
if (name === "") {
return null;
}
return {
name,
value: line.slice(sep + 1).trim(),
};
}
function formatHeader(header: ParsedHeader): string {
return header.value === ""
? `${header.name}:`
: `${header.name}: ${header.value}`;
}
export function parseHeaders(value: string): string[] {
const headers = new Map<string, string>();
const normalized = value.replace(/\\n/g, "\n");
for (const rawLine of normalized.split(/\r?\n/g)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) {
continue;
}
const header = parseHeaderLine(line);
if (!header) {
continue;
}
headers.set(normalizeHeaderName(header.name), formatHeader(header));
}
return Array.from(headers.values());
}
export function formatHeaders(headers: string[]): string {
return headers.join("\\n");
}
export function mergeHeaders(...groups: string[][]): string[] {
const merged = new Map<string, string>();
for (const group of groups) {
for (const rawHeader of group) {
const header = parseHeaderLine(rawHeader);
if (!header) {
continue;
}
merged.set(normalizeHeaderName(header.name), formatHeader(header));
}
}
return Array.from(merged.values());
}
function readConfiguredEnvHeaders(): string[] {
for (const name of HEADER_ENV_VARS) {
const value = readEnvVar(name);
if (value) {
return parseHeaders(value);
}
}
return [];
}
export function getConfiguredHeaders(): string[] {
return mergeHeaders(readConfiguredEnvHeaders(), state.extraHeaders);
}
+72
View File
@@ -0,0 +1,72 @@
import { HttpResponse } from "./types";
import { state } from "./state";
import { mergeHeaders } from "./headers";
import { debugLog } from "./utils";
interface HttpRequestOptions {
method: "GET" | "POST";
url: string;
headers: string[];
payload?: string;
}
function shellEscape(arg: string): string {
return `'${arg.replace(/'/g, `'"'"'`)}'`;
}
function executeCurl(options: HttpRequestOptions): string {
const { method, url, headers, payload } = options;
const cmdParts = ["curl", "-s"];
if (state.timeout > 0) {
cmdParts.push("--max-time", String(state.timeout));
}
const requestHeaders = mergeHeaders(
["Content-Type: application/json"],
headers,
);
requestHeaders.forEach((h) => cmdParts.push("-H", shellEscape(h)));
if (method === "POST") {
if (!payload) throw new Error("Payload required for POST requests");
const tmpfile = r2.fdump(payload);
cmdParts.push("--data-binary", "@-", shellEscape(url));
const cmd = cmdParts.join(" ") + " < " + shellEscape(tmpfile) +
" && rm " + shellEscape(tmpfile);
debugLog(cmd);
return r2.syscmds(cmd);
} else {
cmdParts.push(shellEscape(url));
const cmd = cmdParts.join(" ");
debugLog(cmd);
return r2.syscmds(cmd);
}
}
export function httpRequest(options: HttpRequestOptions): HttpResponse {
try {
const output = executeCurl(options).trim();
if (output === "") {
return { error: "empty response" };
}
try {
return JSON.parse(output) as HttpResponse;
} catch {
return { error: output, rawOutput: output };
}
} catch (e) {
const err = e as Error;
return { error: err.message };
}
}
export function httpGet(url: string, headers: string[]): HttpResponse {
return httpRequest({ method: "GET", url, headers });
}
export function httpPost(
url: string,
headers: string[],
payload: string,
): HttpResponse {
return httpRequest({ method: "POST", url, headers, payload });
}
+10
View File
@@ -0,0 +1,10 @@
### 3. HTTP Request Patterns
**Location**: `http.ts`, used throughout providers and models
**Problems**:
- Two different curl methods (`curlArgs` vs `curlFile`) with similar but
separate logic
- Error handling in HTTP layer vs. application layer
- No centralized request/response abstraction
+34
View File
@@ -0,0 +1,34 @@
import { COMMAND } from "./constants";
import { handleCommand } from "./commands";
import { ensureRcConfigLoaded } from "./config";
function main(args: string): boolean {
ensureRcConfigLoaded();
const output = handleCommand(args, main);
if (output) {
r2.log(output);
}
return true;
}
function installPlugin(): void {
r2.unload("core", COMMAND);
r2.plugin("core", function () {
function coreCall(cmd: string): boolean {
if (cmd.startsWith(COMMAND)) {
const args = cmd.slice(COMMAND.length).trim();
return main(args);
}
return false;
}
return {
name: COMMAND,
license: "MIT",
desc: "r2 decompiler based on r2ai",
call: coreCall,
};
});
}
installPlugin();
+34
View File
@@ -0,0 +1,34 @@
import { getProvider, listModels } from "./providers";
export function listModelsFor(api: string): void {
const providerConfig = getProvider(api);
if (!providerConfig) {
console.error(`Unknown provider: ${api}`);
return;
}
try {
const models = listModels(api);
if (models) {
console.log(models);
}
if (providerConfig.hardcodedModels) {
providerConfig.hardcodedModels.forEach((model) => console.log(model));
}
if (api === "mistral") {
console.log("ministral-8b-latest");
}
if (!models && !providerConfig.hardcodedModels) {
console.log(providerConfig.defaultModel);
}
} catch (e) {
const err = e as Error;
console.error(`Error listing models for ${api}:`, err.message);
console.log(providerConfig.defaultModel);
}
}
+546
View File
@@ -0,0 +1,546 @@
import {
ApiKeyResult,
ApiStyle,
AuthStyle,
HttpResponse,
JsonObject,
ModelDataEntry,
OllamaModelEntry,
ProviderConfig,
ProviderRegistry,
} from "./types";
import { state } from "./state";
import { filterResponse, padRight } from "./utils";
import { getApiKey } from "./apiKeys";
import { getConfiguredHeaders, mergeHeaders } from "./headers";
import { httpGet, httpPost } from "./http";
interface ProviderRuntime {
buildPayload: (model: string, query: string) => JsonObject;
parseResponse: (response: HttpResponse) => string;
buildUrl: (baseUrl: string, model: string, apiKey?: string) => string;
requiresUrlApiKey?: boolean;
}
function getApiKeyName(authKey: string): string {
return authKey.split("_")[0].toLowerCase();
}
function readProviderKey(provider: ProviderConfig): ApiKeyResult | undefined {
if (!provider.authKey) {
return undefined;
}
const keyNames = Array.from(
new Set(
[provider.keyName, getApiKeyName(provider.authKey)].filter(Boolean),
),
) as string[];
let fallback: ApiKeyResult | undefined;
for (const keyName of keyNames) {
const result = getApiKey(keyName, provider.authKey);
if (result[0]) {
return result;
}
if (!fallback && result[2] !== "nope") {
fallback = result;
}
}
return fallback ||
getApiKey(getApiKeyName(provider.authKey), provider.authKey);
}
function getErrorMessage(error: HttpResponse["error"]): string | undefined {
if (!error) {
return undefined;
}
return typeof error === "string" ? error : error.message;
}
function getProviderBaseUrl(provider: ProviderConfig): string {
return state.baseurl || provider.defaultBaseUrl;
}
function getProviderHeaders(
providerHeaders: string[],
extraHeaders: string[] = getConfiguredHeaders(),
): string[] {
return mergeHeaders(providerHeaders, extraHeaders);
}
function buildBearerHeaders(apiKey: string | null): string[] {
return apiKey ? ["Authorization: Bearer " + apiKey] : [];
}
function buildAnthropicHeaders(apiKey: string | null): string[] {
const headers = ["anthropic-version: 2023-06-01"];
return apiKey ? mergeHeaders(headers, ["x-api-key: " + apiKey]) : headers;
}
function buildAuthHeaders(
provider: ProviderConfig,
apiKey: string | null,
): string[] {
const authStyle: AuthStyle = provider.authStyle || "none";
switch (authStyle) {
case "bearer":
return buildBearerHeaders(apiKey);
case "anthropic":
return buildAnthropicHeaders(apiKey);
case "none":
default:
return [];
}
}
function listDataModels(
url: string,
headers: string[],
mapper: (model: ModelDataEntry) => string,
): string {
const response = httpGet(url, headers);
const error = getErrorMessage(response.error);
if (error) {
console.error(error);
return "error invalid response";
}
return response.data?.map(mapper).join("\n") || "";
}
function uniqueBy<T>(items: T[], getKey: (item: T) => string): T[] {
const seen = new Set<string>();
return items.filter((item) => {
const key = getKey(item);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
function listOpenAIModels(provider: ProviderConfig): string {
const apiKey = readProviderKey(provider);
const baseUrl = getProviderBaseUrl(provider);
const headers = getProviderHeaders(
buildAuthHeaders(provider, apiKey?.[0] || null),
);
return listDataModels(
baseUrl + "/v1/models",
headers,
(model) => model.id,
);
}
function listAnthropicModels(provider: ProviderConfig): string {
const apiKey = readProviderKey(provider);
const baseUrl = getProviderBaseUrl(provider);
const headers = getProviderHeaders(
buildAuthHeaders(provider, apiKey?.[0] || null),
);
return listDataModels(
baseUrl + "/v1/models",
headers,
(model) => model.id,
);
}
function listOllamaModels(provider: ProviderConfig): string {
const response = httpGet(
getProviderBaseUrl(provider) + "/api/tags",
getProviderHeaders(
buildAuthHeaders(provider, readProviderKey(provider)?.[0] || null),
),
);
const error = getErrorMessage(response.error);
if (error) {
console.error(error);
return "error invalid response";
}
return response.models?.map((model: OllamaModelEntry) => model.name).join(
"\n",
) || "";
}
function listMistralModels(provider: ProviderConfig): string {
const apiKey = readProviderKey(provider);
const response = httpGet(
getProviderBaseUrl(provider) + "/v1/models",
getProviderHeaders(buildAuthHeaders(provider, apiKey?.[0] || null)),
);
if (response.data) {
return uniqueBy(response.data, (model) => model.name || model.id)
.map((model) =>
[
padRight(model.name || model.id, 30),
padRight("" + (model.max_context_length || ""), 10),
model.description || "",
].join(" ")
)
.join("\n");
}
return "";
}
const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
openai: {
buildPayload: (model, query) => ({
stream: false,
model,
messages: [{ role: "user", content: query }],
}),
parseResponse: (response) => {
if (response.error) {
const error = typeof response.error === "object"
? response.error.message
: response.error;
throw new Error(error || "Unknown error");
}
if (response.choices && response.choices[0]?.message?.content) {
return filterResponse(response.choices[0].message.content);
}
throw new Error("Invalid response format");
},
buildUrl: (baseUrl) => baseUrl + "/v1/chat/completions",
},
anthropic: {
buildPayload: (model, query) => {
const payload: {
model: string;
max_tokens: number;
messages: Array<{ role: string; content: string }>;
temperature?: number;
top_p?: number;
top_k?: number;
} = {
model,
max_tokens: 5128,
messages: [{ role: "user", content: query }],
};
if (state.deterministic) {
Object.assign(payload, { temperature: 0, top_p: 0, top_k: 1 });
}
return payload;
},
parseResponse: (response) => {
if (response.content && response.content[0]?.text) {
return filterResponse(response.content[0].text);
}
if (response.error) {
const error = typeof response.error === "object"
? response.error.message
: response.error;
throw new Error(error || "Unknown error");
}
throw new Error("Invalid response format");
},
buildUrl: (baseUrl) => baseUrl + "/v1/messages",
},
ollama: {
buildPayload: (model, query) => {
const payload: {
stream: boolean;
model: string;
messages: Array<{ role: string; content: string }>;
options?: {
repeat_last_n: number;
top_p: number;
top_k: number;
temperature: number;
repeat_penalty: number;
seed: number;
};
} = {
stream: false,
model,
messages: [{ role: "user", content: query }],
};
if (state.deterministic) {
payload.options = {
repeat_last_n: 0,
top_p: 1.0,
top_k: 1.0,
temperature: 0.0,
repeat_penalty: 1.0,
seed: 123,
};
}
return payload;
},
parseResponse: (response) => {
if (response.error) {
const error = typeof response.error === "string"
? response.error
: JSON.stringify(response.error);
throw new Error(error);
}
if (response.message?.content) {
return filterResponse(response.message.content);
}
throw new Error(JSON.stringify(response));
},
buildUrl: (baseUrl) => baseUrl + "/api/chat",
},
gemini: {
buildPayload: (model, query) => {
const payload: {
contents: Array<{ parts: Array<{ text: string }> }>;
generationConfig?: {
temperature: number;
topP: number;
topK: number;
};
} = {
contents: [{ parts: [{ text: query }] }],
};
if (state.deterministic) {
payload.generationConfig = {
temperature: 0.0,
topP: 1.0,
topK: 1,
};
}
return payload;
},
parseResponse: (response) => {
if (
response.candidates &&
response.candidates[0]?.content?.parts?.[0]?.text
) {
return filterResponse(response.candidates[0].content.parts[0].text);
}
if (response.error) {
throw new Error(
typeof response.error === "string"
? response.error
: JSON.stringify(response.error),
);
}
console.log(JSON.stringify(response));
throw new Error("Invalid response format");
},
buildUrl: (baseUrl, model, apiKey) =>
`${baseUrl}/v1beta/models/${model}:generateContent?key=${apiKey}`,
requiresUrlApiKey: true,
},
};
export const providerRegistry: ProviderRegistry = {
anthropic: {
defaultModel: "claude-3-7-sonnet-20250219",
defaultBaseUrl: "https://api.anthropic.com",
authKey: "ANTHROPIC_API_KEY",
keyName: "anthropic",
authStyle: "anthropic",
apiStyle: "anthropic",
},
claude: {
defaultModel: "claude-3-7-sonnet-20250219",
defaultBaseUrl: "https://api.anthropic.com",
authKey: "ANTHROPIC_API_KEY",
keyName: "claude",
authStyle: "anthropic",
apiStyle: "anthropic",
},
openai: {
defaultModel: "gpt-4o-mini",
defaultBaseUrl: "https://api.openai.com",
authKey: "OPENAI_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
},
ollama: {
defaultModel: "qwen2.5-coder:latest",
defaultBaseUrl: "http://localhost:11434",
authStyle: "none",
apiStyle: "ollama",
},
ollamacloud: {
defaultModel: "gpt-oss:120b",
defaultBaseUrl: "https://ollama.com",
authKey: "OLLAMA_API_KEY",
keyName: "ollamacloud",
authStyle: "bearer",
apiStyle: "ollama",
},
opencode: {
defaultModel: "big-pickle",
defaultBaseUrl: "https://opencode.ai/zen",
authKey: "OPENCODE_API_KEY",
keyName: "opencode",
authStyle: "bearer",
apiStyle: "openai",
hardcodedModels: ["big-pickle", "glm-5", "kimi-k2.5"],
},
zen: {
defaultModel: "big-pickle",
defaultBaseUrl: "https://opencode.ai/zen",
authKey: "OPENCODE_API_KEY",
keyName: "zen",
authStyle: "bearer",
apiStyle: "openai",
hardcodedModels: ["big-pickle", "glm-5", "kimi-k2.5"],
},
gemini: {
defaultModel: "gemini-2.5-flash",
defaultBaseUrl: "https://generativelanguage.googleapis.com",
authKey: "GEMINI_API_KEY",
authStyle: "none",
apiStyle: "gemini",
hardcodedModels: [
"gemini-2.0-flash",
"gemini-2.0-flash-lite",
"gemini-2.5-pro",
"gemini-2.5-flash",
"gemini-2.5-flash-lite",
],
},
mistral: {
defaultModel: "codestral-latest",
defaultBaseUrl: "https://api.mistral.ai",
authKey: "MISTRAL_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
},
xai: {
defaultModel: "grok-beta",
defaultBaseUrl: "https://api.x.ai",
authKey: "XAI_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
hardcodedModels: ["grok-2", "grok-beta"],
},
lmstudio: {
defaultModel: "local-model",
defaultBaseUrl: "http://127.0.0.1:1234",
authStyle: "none",
apiStyle: "openai",
},
deepseek: {
defaultModel: "deepseek-coder",
defaultBaseUrl: "https://api.deepseek.com",
authKey: "DEEPSEEK_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
hardcodedModels: ["deepseek-coder", "deepseek-chat"],
},
openrouter: {
defaultModel: "openai/gpt-4o-mini",
defaultBaseUrl: "https://openrouter.ai/api",
authKey: "OPENROUTER_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
},
groq: {
defaultModel: "llama-3.3-70b-versatile",
defaultBaseUrl: "https://api.groq.com/openai",
authKey: "GROQ_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
},
};
export function getProvider(api: string): ProviderConfig | undefined {
return providerRegistry[api];
}
export function listProviders(): string[] {
return Object.keys(providerRegistry);
}
export function listModels(providerName: string): string {
const provider = getProvider(providerName);
if (!provider) {
throw new Error(`Unknown provider: ${providerName}`);
}
if (providerName === "mistral") {
return listMistralModels(provider);
}
switch (provider.apiStyle) {
case "openai":
return listOpenAIModels(provider);
case "anthropic":
return listAnthropicModels(provider);
case "ollama":
return listOllamaModels(provider);
case "gemini":
return "";
default:
return "";
}
}
function buildQuery(msg: string, hideprompt: boolean): string {
let query = msg;
if (state.think >= 0) {
if (state.think === 0) {
query +=
' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".';
query += " /no_think";
} else if (state.think > 0) {
query =
"Think step by step and explain the reasoning process, When answering, first output your reasoning inside <think> and </think> tags, then give the final answer." +
query;
}
}
return hideprompt ? query : state.prompt + languagePrompt() + query;
}
function languagePrompt(): string {
return "\n.Translate the code into " + state.language +
" programming language\n";
}
function callRuntime(
provider: ProviderConfig,
runtime: ProviderRuntime,
msg: string,
hideprompt: boolean,
): string {
const model = state.model || provider.defaultModel;
const query = buildQuery(msg, hideprompt);
const apiKey = readProviderKey(provider);
if (runtime.requiresUrlApiKey && apiKey?.[1] && provider.authKey) {
return `Cannot read ~/.r2ai.${getApiKeyName(provider.authKey)}-key`;
}
const payload = runtime.buildPayload(model, query);
const headers = getProviderHeaders(
buildAuthHeaders(provider, apiKey?.[0] || null),
);
const url = runtime.buildUrl(
getProviderBaseUrl(provider),
model,
apiKey?.[0] || undefined,
);
try {
return runtime.parseResponse(
httpPost(url, headers, JSON.stringify(payload)),
);
} catch (error) {
return "ERROR: " + (error as Error).message;
}
}
export function callProvider(msg: string, hideprompt: boolean): string {
const provider = getProvider(state.api);
if (!provider) {
return `Unknown value for 'decai -e api'. Available: ${
listProviders().join(", ")
}`;
}
return callRuntime(
provider,
providerRuntimes[provider.apiStyle],
msg,
hideprompt,
);
}
+44
View File
@@ -0,0 +1,44 @@
import { state } from "./state";
import { fileDump, tmpdir } from "./utils";
import { callProvider } from "./providers";
import { httpGet } from "./http";
export function r2ai(
queryText: string,
fileData: string | null,
hideprompt: boolean = false,
): string {
const cleanFileData = (fileData || "").replace(/`/g, "");
const cleanQuery = queryText.replace(/'/g, "");
// Handle r2ai server mode
if (state.api === "r2" || state.api === "r2ai") {
const fileName = tmpdir(".pdc.txt");
fileDump(fileName, cleanFileData);
const q = cleanQuery.startsWith("-")
? cleanQuery
: ["-i", fileName, cleanQuery].join(" ");
const host = state.baseurl
? state.baseurl + "/cmd"
: state.host + ":" + state.port + "/cmd";
const url = host + "/" + encodeURIComponent(q);
const response = httpGet(url, []);
if (response.error) {
return `Error: ${response.error}`;
}
return response.result || JSON.stringify(response) ||
"Cannot curl, use r2ai-server or r2ai -w";
}
if (cleanQuery.startsWith("-")) return "";
let q = cleanQuery + ":\n" + cleanFileData;
if (state.maxInputTokens > 0 && q.length > state.maxInputTokens) {
q = q.slice(0, state.maxInputTokens);
}
return callProvider(q, hideprompt);
}
+275
View File
@@ -0,0 +1,275 @@
export type R2Address = number | string | unknown;
export interface R2LogEntry {
type: string;
message: string;
}
export interface R2CommandResult {
res?: string;
value?: number;
logs?: R2LogEntry[];
[key: string]: unknown;
}
export interface R2Plugin {
name: string;
license: string;
desc: string;
call: (command: string) => boolean;
}
export type R2PluginFactory = () => R2Plugin;
export class R2PipeSyncFromSync {
r2p: R2Pipe;
constructor(r2p: R2Pipe) {
this.r2p = r2p;
}
/**
* Run a command in the associated instance of radare2 and return the output as a string
*
* @param {string} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmd(command: string): string {
return this.r2p.cmd(command);
}
cmd2(command: string): R2CommandResult {
const output = this.r2p.cmd(command);
return typeof output === "string" ? { res: output } : output;
}
cmdAt(
command: string,
address: R2Address,
): string {
return this.r2p.cmdAt(command, address);
}
cmdj(cmd: string): unknown {
return this.r2p.cmdj(cmd);
}
call(command: string): string {
return this.r2p.call(command);
}
callj(cmd: string): unknown {
return this.r2p.cmdj(cmd);
}
callAt(
command: string,
address: R2Address,
): string {
return this.r2p.cmdAt(command, address);
}
syscmd(command: string): string {
return this.r2p.syscmd(command);
}
syscmds(command: string): string {
return this.r2p.syscmds(command);
}
fdump(data: string): string {
return this.r2p.fdump(data);
}
log(msg: string): void {
return this.r2p.log(msg);
}
plugin(type: string, maker: R2PluginFactory): boolean {
return this.r2p.plugin(type, maker);
}
unload(type: string, name: string): boolean {
return this.r2p.unload(type, name);
}
}
export function newAsyncR2PipeFromSync(r2p: R2Pipe): R2PipeSync {
const asyncR2Pipe = new R2PipeSyncFromSync(r2p);
return asyncR2Pipe as R2PipeSync;
}
/**
* Generic interface to interact with radare2, abstracts the access to the associated
* instance of the tool, which could be native via rlang or remote via pipes or tcp/http.
*
* @typedef R2Pipe
*/
export interface R2Pipe {
/**
* Run a command in the associated instance of radare2 and return the output as a string
*
* @param {string} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmd(cmd: string): string;
/**
* Run a radare2 command in a different address. Same as `.cmd(x + '@ ' + a)`
*
* @param {string} command to be executed inside radare2.
* @param {number|string|NativePointer} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmd2(cmd: string): R2CommandResult;
cmdAt(cmd: string, address: R2Address): string;
/**
* Run a radare2 command expecting the output to be JSON
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @returns {object} the JSON decoded object from the output of the command
*/
cmdj(cmd: string): unknown;
/**
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
* which is safer and faster but you cannot use any special modifier like `@`, `~`, ...
*
* See R2Pipe.callAt() to call a command on a different address
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @returns {object} the JSON decoded object from the output of the command
*/
call(cmd: string): string;
/**
* Call a radare2 command in a different address
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @param {NativePointer|string|number} where to seek to execute this command (previous offset is restored after executing it)
* @returns {string} the string containing the output of the command
*/
callAt(cmd: string, address: R2Address): string;
syscmd(cmd: string): string;
syscmds(cmd: string): string;
fdump(data: string): string;
/**
* Same as cmdj but using .call which avoids command injection problems
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @returns {object} the JSON decoded object from the command output
*/
callj(cmd: string): unknown;
/**
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
*
* @param {string} text to be displayed
*/
log(msg: string): void;
/**
* Instantiate a new radare2 plugin with the given type and constructor method.
*
* @param {string} type of plugin ("core", "io", "arch", ...)
* @param {string} function that returns the plugin definition
* @returns {boolean} true if successful
*/
plugin(type: string, maker: R2PluginFactory): boolean;
/**
* Unload the plugin associated with a `type` and a `name`.
*
* @param {string} type of plugin ("core", "io", "arch", ...)
* @param {string} name of the plugin
* @returns {boolean} true if successful
*/
unload(type: string, name: string): boolean;
}
/**
* Generic interface to interact with radare2, abstracts the access to the associated
* instance of the tool, which could be native via rlang or remote via pipes or tcp/http.
*
* @typedef R2PipeSync
*/
export interface R2PipeSync {
/**
* Run a command in the associated instance of radare2
*
* @param {string} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmd(cmd: string): string;
/**
* Run a radare2 command in a different address. Same as `.cmd(x + '@ ' + a)`
*
* @param {string} command to be executed inside radare2.
* @param {number|string|NativePointer} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmd2(cmd: string): R2CommandResult;
cmdAt(cmd: string, address: R2Address): string;
/**
* Run a radare2 command expecting the output to be JSON
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @returns {object} the JSON decoded object from the output of the command
*/
cmdj(cmd: string): unknown;
/**
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
* which is safer and faster but you cannot use any special modifier like `@`, `~`, ...
*
* See R2Pipe.callAt() to call a command on a different address
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @returns {object} the JSON decoded object from the output of the command
*/
call(cmd: string): string;
/**
* Call a radare2 command in a different address
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @param {NativePointer|string|number} where to seek to execute this command (previous offset is restored after executing it)
* @returns {string} the string containing the output of the command
*/
callAt(cmd: string, address: R2Address): string;
syscmd(cmd: string): string;
syscmds(cmd: string): string;
fdump(data: string): string;
/**
* Same as cmdj but using .call which avoids command injection problems
*
* @param {string} command to be executed inside radare2. The given command should end with `j`
* @returns {object} the JSON decoded object from the command output
*/
callj(cmd: string): unknown;
/**
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
*
* @param {string} text to be displayed
*/
log(msg: string): void;
/**
* Instantiate a new radare2 plugin with the given type and constructor method.
*
* @param {string} type of plugin ("core", "io", "arch", ...)
* @param {string} function that returns the plugin definition
* @returns {boolean} true if successful
*/
plugin(type: string, maker: R2PluginFactory): boolean;
/**
* Unload the plugin associated with a `type` and a `name`.
*
* @param {string} type of plugin ("core", "io", "arch", ...)
* @param {string} name of the plugin
* @returns {boolean} true if successful
*/
unload(type: string, name: string): boolean;
}
/**
* A global instance of R2Pipe associated with the current instance of radare2
*
* @type {R2PipeSync}
*/
export declare const r2: R2Pipe;
+34
View File
@@ -0,0 +1,34 @@
import { AppState } from "./types";
import { DEFAULT_PROMPT } from "./constants";
export const defaultState: AppState = {
decopipe: { use: false },
host: "http://localhost",
port: "11434",
baseurl: "",
extraHeaders: [],
api: "ollama",
pipeline: "",
commands: "pdc",
yolo: false,
tts: false,
language: "C",
humanLanguage: "English",
deterministic: true,
debug: false,
timeout: 180,
think: -1,
useFiles: false,
contextFile: "",
model: "",
cache: false,
maxInputTokens: -1,
prompt: DEFAULT_PROMPT,
lastOutput: "",
};
export const state: AppState = {
...defaultState,
decopipe: { ...defaultState.decopipe },
extraHeaders: [...defaultState.extraHeaders],
};
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["esnext"],
"module": "commonjs",
"outDir": "out",
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"**/*.ts"
]
}
+135
View File
@@ -0,0 +1,135 @@
import type { R2PipeSync } from "./r2pipe";
interface ConsoleLike {
log: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
}
declare global {
const r2: R2PipeSync;
const console: ConsoleLike;
function btoa(str: string): string;
}
export type ApiStyle = "openai" | "anthropic" | "ollama" | "gemini";
export type AuthStyle = "none" | "bearer" | "anthropic";
export interface ProviderConfig {
defaultModel: string;
defaultBaseUrl: string;
authKey?: string;
authStyle?: AuthStyle;
keyName?: string;
apiStyle: ApiStyle;
hardcodedModels?: string[];
}
export interface ProviderRegistry {
[key: string]: ProviderConfig;
}
export interface DecoPipeConfig {
use: boolean;
default?: string;
[key: string]: unknown;
}
export interface AppState {
decopipe: DecoPipeConfig;
host: string;
port: string;
baseurl: string;
extraHeaders: string[];
api: string;
pipeline: string;
commands: string;
yolo: boolean;
tts: boolean;
language: string;
humanLanguage: string;
deterministic: boolean;
debug: boolean;
timeout: number;
think: number;
useFiles: boolean;
contextFile: string;
model: string;
cache: boolean;
maxInputTokens: number;
prompt: string;
lastOutput: string;
}
export interface ConfigHandler {
get: () => string | number | boolean;
set: (v: string) => void;
}
export interface ConfigHandlers {
[key: string]: ConfigHandler;
}
export type ApiKeyResult = [string | null, string | null, string];
export interface ApiError {
message?: string;
[key: string]: unknown;
}
export interface OpenAIChoice {
message?: {
content?: string;
};
}
export interface AnthropicContentBlock {
text?: string;
}
export interface OllamaMessage {
content?: string;
}
export interface GeminiPart {
text?: string;
}
export interface GeminiCandidate {
content?: {
parts?: GeminiPart[];
};
}
export interface ModelDataEntry {
id: string;
name?: string;
max_context_length?: number;
description?: string;
}
export interface OllamaModelEntry {
name: string;
}
export interface HttpResponse {
error?: ApiError | string;
rawOutput?: string;
choices?: OpenAIChoice[];
content?: AnthropicContentBlock[];
message?: OllamaMessage;
candidates?: GeminiCandidate[];
data?: ModelDataEntry[];
models?: OllamaModelEntry[];
result?: string;
[key: string]: unknown;
}
export type JsonObject = Record<string, unknown>;
export interface AutoReply {
action: string;
command?: string;
description?: string;
reason?: string;
response?: string;
}
+86
View File
@@ -0,0 +1,86 @@
import { state } from "./state";
export function tmpdir(path: string): string {
const dir = r2.cmd("-e dir.tmp").trim() ?? ".";
return dir + "/" + path;
}
export function ensurePath(dir: string, file: string): void {
r2.cmd("'mkdir -p " + dir);
r2.cmd("'touch " + file);
}
export function fileExists(path: string): boolean {
const helpMessage = r2.cmd2("test -h").logs?.[0]?.message ?? "";
if (helpMessage.includes("-fdx")) {
return true; // r2 is old
}
return r2.cmd("'test -vf " + path).startsWith("found");
}
export function padRight(str: string, length: number): string {
return str + " ".repeat(Math.max(0, length - str.length));
}
export function trimAnsi(str: string): string {
return str.replace(/\x1b\[[0-9;]*m/g, "");
}
export function trimDown(out: string): string {
const jsonMatch = out.match(/```json\s*([\s\S]*?)```/);
return jsonMatch && jsonMatch[1] ? jsonMatch[1].trim() : out;
}
export function trimJson(out: string): string {
let result = out;
const bob = result.indexOf("{");
if (bob !== -1) result = result.slice(bob);
const eob = result.lastIndexOf("}");
if (eob !== -1) result = result.slice(0, eob + 1);
return result;
}
export function b64(str: string): string {
return btoa(str);
}
export function fileDump(fileName: string, fileData: string): void {
const d = b64(fileData);
r2.cmd("p6ds " + d + " > " + fileName);
}
export function filterResponse(msg: string): string {
let result = msg;
if (state.think !== 2) {
result = result.replace(/<think>[\s\S]*?<\/think>/gi, "");
}
return result
.split("\n")
.filter((line) => !line.trim().startsWith("```"))
.join("\n");
}
export function debugLog(msg: string): void {
if (state.debug) {
console.log(msg);
}
}
export function parseEnvLikeString(input: string): Record<string, string> {
const result: Record<string, string> = {};
for (const line of input.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
const [key, ...rest] = trimmed.split("=");
if (!key || rest.length === 0) {
continue;
}
const value = rest.join("=").trim();
// Normalize key: strip trailing _API_KEY (case-insensitive)
const normKey = key.toLowerCase().replace(/_api_key$/i, "");
result[normKey] = value;
}
return result;
}
+130
View File
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
sodipodi:docname="decai.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.0482005"
inkscape:cx="318.64133"
inkscape:cy="426.92214"
inkscape:window-width="1512"
inkscape:window-height="960"
inkscape:window-x="222"
inkscape:window-y="44"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="rect2"
width="186.28354"
height="176.18687"
x="1.5145004"
y="23.979588"
ry="0.047831953" />
<path
sodipodi:type="star"
style="fill:#1a1a1a;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="path1"
inkscape:flatsided="false"
sodipodi:sides="10"
sodipodi:cx="71.751205"
sodipodi:cy="102.09375"
sodipodi:r1="53.804771"
sodipodi:r2="49.845222"
sodipodi:arg1="0.65927727"
sodipodi:arg2="0.97343657"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 114.28039,135.05163 -14.49317,8.25532 -13.001341,10.44831 -16.577574,-1.84017 -16.659669,0.81086 -12.329911,-11.23279 -13.95457,-9.1363 -3.37264,-16.33486 -5.919299,-15.59371 6.872864,-15.197565 4.376943,-16.094859 14.493168,-8.255319 13.001341,-10.448311 16.577574,1.840177 16.659669,-0.810865 12.329915,11.232788 13.95457,9.136304 3.37264,16.334856 5.91929,15.593714 -6.87286,15.19757 z"
transform="matrix(1.2188073,0,0,1.2188073,1.600691,-15.424646)" />
<path
sodipodi:type="star"
style="fill:#37662e;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="path1-5"
inkscape:flatsided="false"
sodipodi:sides="10"
sodipodi:cx="71.751205"
sodipodi:cy="102.09375"
sodipodi:r1="35.902484"
sodipodi:r2="49.845222"
sodipodi:arg1="0.32368816"
sodipodi:arg2="0.63784749"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 105.78923,113.51309 6.0066,18.26187 -19.219401,-0.43572 -5.874621,18.30475 L 71.4091,137.9946 55.897164,149.35045 50.372443,130.93707 31.148226,131.00645 37.501747,112.86236 21.908246,101.61878 37.71318,90.674414 31.706582,72.412538 l 19.2194,0.435725 5.874621,-18.304757 15.292708,11.649389 15.511935,-11.355844 5.524722,18.413382 19.224212,-0.06938 -6.35352,18.144089 15.5935,11.243582 z"
transform="matrix(1.2188073,0,0,1.2188073,0.97362416,-14.941195)" />
<path
sodipodi:type="star"
style="fill:#800000;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
id="path2"
inkscape:flatsided="false"
sodipodi:sides="10"
sodipodi:cx="69.252403"
sodipodi:cy="183.84013"
sodipodi:r1="76.18972"
sodipodi:r2="39.770834"
sodipodi:arg1="0.94510027"
sodipodi:arg2="1.2659605"
inkscape:rounded="0.029345222"
inkscape:randomized="0"
d="m 113.8738,245.59614 c -0.96661,0.6886 -31.555039,-24.18254 -32.684714,-23.81876 -1.120974,0.36099 -10.958876,38.2632 -12.136488,38.25221 -1.186753,-0.0111 -11.314419,-38.11167 -12.442172,-38.48137 -1.119068,-0.36685 -31.356457,24.51411 -32.302709,23.81304 -0.953595,-0.70651 13.247923,-37.48344 12.552854,-38.44541 -0.689714,-0.95456 -39.7769375,1.40147 -40.1303916,0.27809 -0.3561976,-1.13209 32.7500076,-22.5378 32.7531166,-23.7246 0.0031,-1.17766 -33.0039804,-22.24649 -32.6296302,-23.36307 0.3772559,-1.12525 39.7427032,1.0165 40.4428022,0.0582 0.694707,-0.95093 -13.624624,-37.39704 -12.665459,-38.08034 0.966611,-0.68859 31.555037,24.18255 32.684712,23.81876 1.120974,-0.36098 10.958875,-38.26319 12.136488,-38.2522 1.186752,0.0111 11.314419,38.11167 12.442172,38.48137 1.119067,0.36685 31.356459,-24.51411 32.302709,-23.81304 0.95359,0.70651 -13.24792,37.48344 -12.55285,38.44541 0.68971,0.95456 39.77693,-1.40147 40.13039,-0.2781 0.35619,1.13209 -32.75001,22.53781 -32.75312,23.72461 -0.003,1.17766 33.00398,22.24649 32.62963,23.36307 -0.37726,1.12524 -39.7427,-1.01651 -40.4428,-0.0582 -0.69471,0.95093 13.62462,37.39704 12.66546,38.08033 z"
transform="translate(19.276442,-75.320913)" />
<path
sodipodi:type="star"
style="fill:#d45500;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path2-0"
inkscape:flatsided="false"
sodipodi:sides="10"
sodipodi:cx="69.252403"
sodipodi:cy="183.84013"
sodipodi:r1="3.2911112"
sodipodi:r2="71.359137"
sodipodi:arg1="1.3521275"
sodipodi:arg2="1.5760036"
inkscape:rounded="0.029345222"
inkscape:randomized="0"
d="m 69.966345,187.05287 c -1.9053,0.60813 0.905346,68.33625 -1.085527,68.14543 -1.996473,-0.19135 0.964985,-67.70967 -0.939225,-68.33936 -1.898868,-0.62792 -39.434599,55.81734 -40.933089,54.49275 -1.502705,-1.3283 40.579437,-54.21107 39.409022,-55.83977 -1.167132,-1.62412 -64.7118681,21.9781 -65.1456023,20.0257 -0.43495449,-1.95789 64.6939243,-20.00568 64.7043623,-22.01128 0.01041,-1.99997 -65.27140303,-20.25603 -64.4747102,-22.09049 0.7989341,-1.83963 64.0975312,21.84119 65.2848352,20.22477 1.183973,-1.61189 -40.89948,-54.75304 -39.17667,-55.76887 1.727657,-1.01869 39.01806,55.34548 40.92872,54.73564 1.905301,-0.60812 -0.905346,-68.33625 1.085527,-68.14543 1.996474,0.19136 -0.964984,67.70968 0.939225,68.33937 1.898868,0.62792 39.434597,-55.81734 40.933087,-54.49276 1.50271,1.32831 -40.579435,54.21108 -39.409019,55.83977 1.167131,1.62413 64.711869,-21.97809 65.145599,-20.0257 0.43496,1.9579 -64.693921,20.00569 -64.704359,22.01129 -0.01041,1.99997 65.271399,20.25602 64.474709,22.09049 -0.79893,1.83963 -64.09753,-21.8412 -65.284834,-20.22477 -1.183973,1.61189 40.899484,54.75304 39.176674,55.76887 -1.72766,1.01869 -39.018064,-55.34548 -40.928725,-54.73565 z"
transform="translate(19.25264,-74.484719)" />
<path
sodipodi:type="star"
style="fill:#ffb380;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
id="path2-0-7"
inkscape:flatsided="false"
sodipodi:sides="10"
sodipodi:cx="69.252403"
sodipodi:cy="183.84013"
sodipodi:r1="2.0329688"
sodipodi:r2="36.653175"
sodipodi:arg1="1.3051581"
sodipodi:arg2="1.5885803"
inkscape:rounded="0.029345222"
inkscape:randomized="0"
d="m 69.786109,185.8018 c -0.974051,0.29744 -0.168255,34.73512 -1.18551,34.68571 -1.01845,-0.0495 0.894533,-34.41437 -0.06946,-34.74665 -0.962858,-0.3319 -20.552916,28.00241 -21.346851,27.36451 -0.794867,-0.63865 20.951954,-27.31602 20.367382,-28.15147 -0.583886,-0.83446 -33.087061,10.57373 -33.354419,9.59099 -0.267673,-0.98389 33.006439,-9.78387 33.024571,-10.80336 0.01811,-1.01829 -32.983073,-10.89376 -32.621734,-11.84596 0.361764,-0.95332 32.453587,11.48538 33.067497,10.67125 0.61319,-0.81317 -20.280673,-28.2002 -19.428655,-28.75816 0.853019,-0.55861 19.504568,28.36761 20.479763,28.06981 0.974051,-0.29744 0.168255,-34.73512 1.18551,-34.68572 1.01845,0.0495 -0.894534,34.41438 0.06946,34.74666 0.962858,0.3319 20.552915,-28.00241 21.34685,-27.36451 0.794868,0.63865 -20.951953,27.31602 -20.367382,28.15146 0.583886,0.83446 33.087059,-10.57372 33.354419,-9.59099 0.26767,0.98389 -33.006439,9.78388 -33.024571,10.80337 -0.01811,1.01829 32.983071,10.89376 32.621731,11.84596 -0.36176,0.95332 -32.453584,-11.48538 -33.067494,-10.67125 -0.613189,0.81317 20.280674,28.2002 19.428655,28.75816 C 89.412852,214.43022 70.761304,185.504 69.786109,185.8018 Z"
transform="rotate(-17.984043,-157.18948,86.992365)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.3 KiB

-41
View File
@@ -1,41 +0,0 @@
SHELL=/bin/bash
PROVIDER=ollama
MODELS=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
MODELS=gemma3:1b
LANGS=Dart Perl Ruby Javascript PHP C
BINARIES=files/complex1 files/complex2 files/complex3
MODELS=gpt-4o
PROVIDER=openai
all:
mkdir -p tmp
for b in $(BINARIES) ; do \
for a in $(MODELS) ; do \
echo "$$b $$a"; \
./run.sh $$b $$a ${PROVIDER} ${LANGS} > tmp/$${b##*/}_$${a//\//_}.html ; \
done ; \
done
.PHONY: run index evaluate
compile: $(BINARIES)
files/complex1: files/complex1.c
gcc -o $@ $<
files/complex2: files/complex2.c
gcc -o $@ $<
files/complex3: files/complex3.c
gcc -o $@ $<
lmstudio:
for b in $(BINARIES) ; do \
for a in $(MODELS) ; do \
./run.sh $$b $$a lmstudio ${LANGS} > $${b##*/}_$${a//\//_}.html ; \
done ; \
done
clean:
rm -rf tmp
-59
View File
@@ -1,59 +0,0 @@
#!/bin/bash
# Evaluate decompiled outputs using mai and determine best model+lang
echo "Evaluating decompiled outputs..."
tmpfile=$(mktemp)
MAI="mai -q -p openai -m gpt-4o"
# MAI="mai -q"
for orig in files/complex1.c files/complex2.c files/complex3.c; do
base=$(basename $orig .c)
original_code=$(cat $orig)
for f in tmp/${base}_*.txt; do
if [ -f "$f" ]; then
decompiled_code=$(cat $f)
#echo "Comparing $orig $f"
result=`echo n | ${MAI} -q -r "/template files/CMPROMPT.txt original=@${orig} decompiled=@${f}"`
score=`echo "$result" | head -n 1 | grep -oE '[0-9]+'` # |head -n 1`
perfect=`echo "$result" | grep 'No sign' && echo yes`
[ -z "$score" ] && score=0
if [ "$perfect" = yes ]; then
score=100
else
score="$((0+${score}))"
fi
echo "$score\t$f"
if [ -n "$score" ]; then
key="${f%.*}" # remove .txt
# Check if key exists in tmpfile
existing=$(grep "^$key " $tmpfile | awk '{print $2}')
if [ -n "$existing" ]; then
new_score=$(( existing + score ))
sed -i "s/^$key .*/$key $new_score/" $tmpfile
else
echo "$key $score" >> $tmpfile
fi
fi
fi
done
done
# Find the best (highest total score)
best_key=""
best_score=0
while read key score; do
if [ "$score" -gt "$best_score" ]; then
best_score=$score
best_key=$key
fi
done < $tmpfile
rm $tmpfile
if [ -n "$best_key" ]; then
echo "Best model+language combination: $best_key with total score $best_score"
else
echo "No scores found"
fi
-17
View File
@@ -1,17 +0,0 @@
Compare the two provided code in C with the decompiled counterpart. Ignore function or variable names, syntax differences, language-specific constructs, and formatting. Focus solely on what the code does: the operations performed, control flow, logic, and functional behavior. Determine if they are functionally equivalent in terms of their effects and computations.
Output only:
- A numeric score from 1 to 100 (100 being identical logic and functionality)
- Followed by a single sentence describing the most relevant semantic differences (or "No significant semantic differences" if score is 95+)
**Original Source Code:**
```C
{original}
```
**Decompiled Code:**
```
{decompiled}
```
-21
View File
@@ -1,21 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
int main() {
int n = 5;
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) return 1;
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
for (int j = 0; j < i; j++) {
sum -= j;
}
}
printf("Sum: %d\n", sum);
free(arr);
return 0;
}
-39
View File
@@ -1,39 +0,0 @@
#include <stdio.h>
#include <stdlib.h>
struct Node {
int data;
struct Node *next;
};
int main() {
struct Node *head = NULL;
struct Node *tail = NULL;
for (int i = 0; i < 10; i++) {
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
if (newNode == NULL) return 1;
newNode->data = i * 2;
newNode->next = NULL;
if (head == NULL) {
head = newNode;
tail = newNode;
} else {
tail->next = newNode;
tail = newNode;
}
}
struct Node *current = head;
int sum = 0;
while (current != NULL) {
sum += current->data;
current = current->next;
}
printf("Sum: %d\n", sum);
current = head;
while (current != NULL) {
struct Node *temp = current;
current = current->next;
free(temp);
}
return 0;
}
-33
View File
@@ -1,33 +0,0 @@
#include <stdio.h>
int main() {
int x = 42;
int y = 0;
switch (x & 15) {
case 0:
y = x << 1;
break;
case 1:
y = x >> 1;
break;
case 2:
y = x ^ 255;
break;
case 3:
y = ~x;
break;
default:
y = x | 128;
break;
}
if ((y & 1) == 0) {
y += 10;
} else {
y -= 5;
}
for (int i = 0; i < 5; i++) {
y = (y << 2) | (y >> 30);
}
printf("Result: %d\n", y);
return 0;
}
+43
View File
@@ -0,0 +1,43 @@
# R2AI Prompts
This directory contains predefined prompt files for the r2ai plugin.
## Format
Each prompt is stored in a `.r2ai` file using a simple Key: Value format.
Lines starting with `#` are comments and ignored.
Supported keys:
- `Title`: The title of the prompt (required)
- `Author`: The author of the prompt
- `Description`: A brief description
- `Command` or `Commands`: Commands to execute before sending the prompt, separated by `;` (required)
- `Prompt` or `Query`: The main prompt text sent to the LLM
- `Requires`: Requirements for the prompt (e.g., "analysis" to ensure binary analysis)
- `If-Empty`: Alternative prompt or message if the command output is empty
- `If-Command`: Additional commands to run if the main command produces output
## Variable Substitution
In the `Prompt` field, you can use:
- `${VAR}`: Replaced with the value of the environment variable `VAR`
- `$(cmd)`: Replaced with the output of the r2 command `cmd`
## Usage
- `r2ai -q`: List all available prompts
- `r2ai -q name`: Run the prompt named `name`
- `r2ai -q name extra text`: Run the prompt with additional text appended
## Conditionals
- `If-Empty`: If the command output is empty, use this as the prompt instead
- `If-Command`: If the command produces output, run these additional commands and append their output
## Future Features
- Support for r2js scripts
- More advanced conditionals
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Automatically suggest a better name for this function'
version: 0.1
author: pancake
command: afv;r2ai -d
if-empty: exit
---
Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Compact conversation history into a summary'
version: 0.1
author: pancake
command: r2ai -C
if-empty: exit
---
Create a compact summary of this conversation that preserves all key information, insights, and context for future reference. Focus on essential details about the binary analysis, tools used, findings, and any important conclusions.
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Augmented decompilation based on LLM'
version: 0.1
author: pancake
command: pdc
if-empty: exit
---
$(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Find and explain devices used'
version: 0.1
author: pancake
command: izqq~^/dev
if-empty: exit
---
explain in one line the purpose of each of these devices
+8
View File
@@ -0,0 +1,8 @@
---
description: 'List libraries loaded with dlopen'
version: 0.1
author: pancake
command: iiq~dlopen
if-empty: This binary is not using dlopen
---
Some libraries are loaded
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Explain the current function'
version: 0.1
author: pancake
command: r2ai -d
if-empty: exit
---
Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Group imports by Libraries'
version: 0.1
author: pancake
command: iiq;il
if-empty: exit
---
Group the imports that belong to each library and describe its purpose and usage scope
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Suggest an improved function signature'
version: 0.1
author: pancake
command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
if-empty: exit
---
Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
+10
View File
@@ -0,0 +1,10 @@
---
description: 'Decompile this code into PHP'
version: 0.1
author: pancake
command: pdg
if-empty: exit
---
Your goal is to $(-e r2ai.prompt).
Translate the code into $(-e r2ai.lang) programming language.
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Better variable names'
version: 0.1
author: pancake
command: afv
if-empty: exit
---
Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
+8
View File
@@ -0,0 +1,8 @@
---
description: 'Find vulnerabilities or bugs in the current function'
version: 0.1
author: pancake
command: pdc
if-empty: exit
---
Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
-40
View File
@@ -1,40 +0,0 @@
BasedOnStyle: LLVM
Language: Cpp
PointerAlignment: Right
AlwaysBreakAfterDefinitionReturnType: None
BinPackParameters: true
MaxEmptyLinesToKeep: 1
SpaceInEmptyParentheses: false
SpacesInContainerLiterals: true
SpaceBeforeParens: Custom
SpaceBeforeParensOptions:
AfterIfMacros: true
AfterFunctionDefinitionName: false
AfterFunctionDeclarationName: false
AfterForeachMacros: true
AfterControlStatements: true
BeforeNonEmptyParentheses: false
SpacesInParentheses: false
InsertBraces: true
ContinuationIndentWidth: 8
IndentCaseLabels: false
IndentFunctionDeclarationAfterType: false
IndentWidth: 8
UseTab: ForContinuationAndIndentation
ColumnLimit: 0
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
SpaceBeforeSquareBrackets: false
SpaceInEmptyBlock: false
AllowShortIfStatementsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: true
AllowShortFunctionsOnASingleLine: Inline
AllowShortLoopsOnASingleLine: false
AlignAfterOpenBracket: DontAlign
AlignEscapedNewlines: DontAlign
AlignConsecutiveMacros: true
AlignTrailingComments: false
AlignOperands: false
Cpp11BracedListStyle: false
ForEachMacros: ['r_list_foreach', 'ls_foreach', 'fcn_tree_foreach_intersect', 'r_skiplist_foreach', 'graph_foreach_anode', 'r_list_foreach_safe', 'r_pvector_foreach', 'r_rbtree_foreach', 'r_interval_ tree_foreach']
SortIncludes: false
+33
View File
@@ -0,0 +1,33 @@
# R2AI Plugin for radare2
## Locations
- All the source code of the plugin is in the current directory
- Radare2 headers use to be in /usr/local/include/libr
## Coding Style
- `R_NEW` and `R_NEW0` calls never return NULL, so there's no need to check after calling them
- Use the radare2 coding style rules
- Always add a whitespace before `(` in function calls
- Indent the code with tabs
- Indent comments with spaces
- Do not define variables inside the `for` parenthesis
- Do not check for null before calling free
## Test
Run `r2ai` cli tool or radare2 oneliners running the `r2ai` command.
* `r2pm -r r2ai -h` -> help message for the r2ai tool
* `r2 -qc 'r2ai -h' /bin/ls -> help message of the r2ai plugin
* `r2 -qc 'r2ai -a what arch is used here' -> testing auto mode
* `r2 -e r2ai.model=gemma3:12b -e r2ai.auto.raw=true -qc 'r2ai -a what arch is used here' -> testing auto mode
## Actions
Run the following commands to perform the action described
- FORMAT: `make fmt`
- COMPILE: `make` (with no extra arguments)
- INSTALL: `make user-install`
+43 -12
View File
@@ -1,13 +1,24 @@
DOTEXE=
# DOTEXE=.exe
DOTLIB=.$(shell r2 -H R2_LIBEXT)
DOTLIB=.$(shell r2 -NNH R2_LIBEXT)
CFLAGS+=$(shell pkg-config --cflags r_core)
LDFLAGS+=$(shell pkg-config --libs r_core)
R2SYSPLUGDIR=$(shell r2 -H R2_LIBR_PLUGINS)
R2USRPLUGDIR=$(shell r2 -H R2_USER_PLUGINS)
R2SYSPLUGDIR=$(shell r2 -NNH R2_LIBR_PLUGINS)
R2USRPLUGDIR=$(shell r2 -NNH R2_USER_PLUGINS)
R2BINDIR=$(shell r2pm -H R2PM_BINDIR)
R2MANDIR=$(shell r2pm -H R2PM_MANDIR)
ifeq ($(R2MANDIR),)
R2MANDIR=$(shell r2 -NNH R2_PREFIX)/share/man
endif
R2_CFLAGS=$(shell pkg-config --cflags r_core)
R2_LDFLAGS=$(shell pkg-config --libs r_core)
ifeq ($(R2_CFLAGS),)
R2_CFLAGS=$(shell r2 -NNH R2_CFLAGS)
R2_LDFLAGS=$(shell r2 -NNH R2_LDFLAGS)
endif
CFLAGS+=$(R2_CFLAGS)
LDFLAGS+=$(R2_LDFLAGS)
CFLAGS+=-I. -fPIC
CFLAGS+=-Wall
CFLAGS+=-g
@@ -31,14 +42,21 @@ ifeq ($(USE_LIBCURL),1)
endif
OBJS=r2ai.o
OBJS+=apikeys.o
OBJS+=llm.o
OBJS+=auto.o
OBJS+=json.o
OBJS+=vdb.o
OBJS+=tools.o
OBJS+=messages.o
OBJS+=anthropic.o
OBJS+=r2ai_http.o
OBJS+=gemini.o
OBJS+=http.o
OBJS+=openai.o
OBJS+=markdown.o
OBJS+=query.o
OBJS+=rawtools.o
OBJS+=wizard.o
all: $(OBJS) r2check
$(CC) -fPIC -shared -o r2ai$(DOTLIB) $(OBJS) $(CFLAGS) $(LDFLAGS)
@@ -54,24 +72,37 @@ doc-install:
vdb.o: vdb_embed.inc.c
indent:
for a in *.c ; do python indent.py $$a ; done
fmt format indent:
clang-format-radare2 *.c http/*.c
user-install: user-uninstall
mkdir -p $(R2USRPLUGDIR)
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)
cp -f r2ai$(DOTEXE) $(R2BINDIR)
mkdir -p $(R2BINDIR)
mkdir -p $(R2MANDIR)/man1
mkdir -p $(HOME)/.config/r2ai/prompts
$(MAKE) user-uninstall
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)/r2ai$(DOTLIB)
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
cp -f r2ai.1 $(R2MANDIR)/man1/r2ai.1
cp -r ../prompts/* $(HOME)/.config/r2ai/prompts/
user-uninstall:
rm -f $(R2USRPLUGDIR)/r2ai$(DOTLIB)
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
rm -f $(R2MANDIR)/man1/r2ai.1
install:
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)
$(MAKE) uninstall
mkdir -p $(R2USRPLUGDIR)
mkdir -p $(R2BINDIR)
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
uninstall:
rm -f $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
r2check:
@r2 -qcq --
@r2 -NN -qcq --
clean:
rm -f *.o *.d
+44 -18
View File
@@ -1,12 +1,10 @@
# C-R2AI
# R2AI
This directory contains the C rewrite of the r2ai plugin for radare2.
The r2ai plugin for radare2.
## Installation
Requires radare2 from git if you want to use the stream HTTP APIs.
Otherwise the only requirement is a C compiler.
Requires a C compiler and modern version of radare2 if you want to use the stream HTTP APIs. It will fallback to curl or powershell if not available and `R2_CURL` environment is set to `1`.
```console
make
@@ -37,24 +35,54 @@ Usage: r2ai [-args] [...]
| r2ai -d [query] Ask a question on the current function
| r2ai -dr Decompile current function (+ 1 level of recursivity)
| r2ai -a [query] Resolve question using auto mode
| r2ai -e Same as '-e r2ai.'
| r2ai -e (k(=v)) Same as '-e r2ai.'
| r2ai -h Show this help message
| r2ai -i [file] [query] read file and ask the llm with the given query
| r2ai -m show selected model, list suggested ones, choose one
| r2ai -n suggest a better name for the current function
| r2ai -p [provider] set LLM provider (openai, anthropic, gemini, etc.)
| r2ai -q list available query prompts
| r2ai -q [name] (inst) run predefined prompt with optional instructions
| r2ai -r enter the chat repl
| r2ai -w launch the interactive setup wizard
| r2ai -L show chat logs (See -Lj for json). Only for auto mode.
| r2ai -L-[N] delete the last (or N last messages from the chat history)
| r2ai -R reset the chat conversation context
| r2ai -Rq ([text]) refresh and query embeddings (see r2ai.data)
| r2ai -s function signature
| r2ai -x explain current function
| r2ai -v suggest better variables names and types
| r2ai -V[r] find vulnerabilities in the decompiled code (-Vr uses -dr)
| r2ai [arg] send a post request to talk to r2ai and print the output
| r2ai [query] query the selected model+provider with the given query
[0x100003f58]>
```
## Query Prompts
Query prompts are predefined templates that combine radare2 commands with structured prompts to the LLM for common reverse engineering tasks. They are stored in the `prompts/` directory as `.r2ai.txt` files.
### Available Prompts
- `autoname`: Automatically rename functions and variables
- `decompile`: Decompile the current function
- `devices`: Analyze device-related code
- `dlopen`: Analyze dynamic library loading
- `explain`: Explain the purpose of the current function
- `libs`: Analyze library usage
- `signature`: Improve function signatures
- `varnames`: Rename variables based on context
- `vulns`: Find vulnerabilities in the current function
### Usage
- `r2ai -q`: List all available query prompts
- `r2ai -q [name]`: Run the specified prompt on the current function
- `r2ai -q [name] [instructions]`: Run the prompt with additional custom instructions
### Example
```console
[0x00001303]> r2ai -q explain
The main function implements a simple substitution cipher program that reads user input, encrypts it using a hardcoded alphabet, and then decrypts it back to demonstrate the algorithm.
```
Query prompts automate repetitive analysis tasks by executing predefined radare2 commands and sending context-aware prompts to the LLM.
## Configuration
These can be set with `r2ai -e <keyname>=<value>`
@@ -70,13 +98,14 @@ These can be set with `r2ai -e <keyname>=<value>`
| r2ai.lang | Tells LLM which programming language to use for decompilation result. Only works for `r2ai -d` |
| r2ai.hlang | Tells LLM in which language to speak |
| r2ai.prompt | User prompt to send to LLM with `r2ai -d` |
| r2ai.clippy | Display chat replies using `r2clippy` instead of plain text |
| r2ai.wizard | Auto-start the setup wizard on the first interactive chat session |
| r2ai.auto.max_runs | Maximum loops when using auto mode `r2ai -a` |
| r2ai.auto.hide_tool_output | Only for auto mode `r2ai -a`. Will not show the output of the tool which ran locally |
| r2ai.auto.verbose | Only for auto mode `r2ai -a`. Will show the output of the tool which ran locally |
| r2ai.auto.yolo | Set this to true if you don't want r2ai to ask you for approval before running commands sent by the LLM. This is **dangerous**. Recommendation: **leave this to false** unless you fully trust your LLM not to create havoc! |
| r2ai.http.use_files | If you get CURL argument too long error, set this to true and arguments will be stored in a file.|
## Example
- Get the [substitute example binary](https://github.com/radareorg/r2ai/tree/master/examples)
@@ -215,8 +244,6 @@ Note: System prompt is applied automatically but not stored in history
{"command":"pdf @ main"}
```
## TODO
* add "undo" command to drop the last message
@@ -225,7 +252,6 @@ Note: System prompt is applied automatically but not stored in history
* keep context between direct commands unless explicitly reset
* modify context if tool command is user edited
## Messages API
The r2ai project includes a new API for managing messages and tool calls in conversations with AI models. This API is implemented in `messages.c` and `messages.h`.
@@ -268,4 +294,4 @@ In r2ai, as lm-studio uses an OpenAI-like API, configure:
- `r2ai -e api=openai`
- `r2ai -e baseurl=http://LM-STUDIO-IP:PORT`
- `r2ai -e model=?` to list available models
- `r2ai -e model=?` to list available models
+51 -42
View File
@@ -1,14 +1,17 @@
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
static const char *modelname(const char *model_name) {
return model_name ? model_name : "claude-3-7-sonnet-20250219";
return model_name? model_name: "claude-3-7-sonnet-20250219";
}
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
const char *model = args.model;
char **error = args.error;
const R2AI_Tools *tools = args.tools;
R2AI_Messages *messages_input = args.messages;
RList *tools = args.tools;
RList *messages_input = args.messages;
if (error) {
*error = NULL;
@@ -40,7 +43,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
// Create messages JSON
char *messages_json = NULL;
if (messages_input && messages_input->n_messages > 0) {
if (messages_input && !r_list_empty (messages_input)) {
// Convert directly to JSON without filtering
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
if (!messages_json) {
@@ -60,7 +63,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
// Convert tools to Anthropic format if available
char *anthropic_tools_json = NULL;
if (tools && tools->n_tools > 0) {
if (tools && !r_list_empty (tools)) {
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
}
@@ -98,10 +101,26 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
free (messages_json);
// Save the full JSON for debugging
r_file_dump ("/tmp/r2ai_anthropic_request.json", (const ut8 *)data, strlen (data), 0);
R_LOG_DEBUG ("Full request saved to /tmp/r2ai_anthropic_request.json");
char *tmpdir = r_file_tmpdir ();
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_request.json", tmpdir);
r_file_dump (req_path, (const ut8 *)data, strlen (data), 0);
R_LOG_DEBUG ("Full request saved to %s", req_path);
free (req_path);
free (tmpdir);
R_LOG_DEBUG ("Anthropic API request data: %s", data);
if (r_config_get_b (core->config, "r2ai.debug")) {
// Generate curl command for debugging
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
for (int i = 0; headers[i]; i++) {
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
}
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", data, anthropic_url);
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
r_strbuf_free (curl_cmd);
}
// Make the API call
int code = 0;
char *res = r2ai_http_post (core, anthropic_url, headers, data, &code, NULL);
@@ -120,19 +139,19 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
}
// Save the response for inspection
r_file_dump ("/tmp/r2ai_anthropic_response.json", (const ut8 *)res, strlen (res), 0);
R_LOG_DEBUG ("Anthropic API response saved to /tmp/r2ai_anthropic_response.json");
R_LOG_DEBUG ("Anthropic API response: %s", res);
tmpdir = r_file_tmpdir ();
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_response.json", tmpdir);
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
R_LOG_DEBUG ("Anthropic API response saved to %s", res_path);
free (res_path);
free (tmpdir);
if (r_config_get_b (core->config, "r2ai.debug")) {
R_LOG_DEBUG ("Anthropic API response: %s", res);
}
// Parse the response
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
if (!result) {
free (res);
if (error) {
*error = strdup ("Failed to allocate memory for response");
}
return NULL;
}
R2AI_Message *message = NULL;
R2AI_Usage *usage = NULL;
@@ -184,9 +203,9 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
}
}
// Allocate tool_calls array if needed
// Allocate tool_calls list if needed
if (has_tool_use && n_tool_calls > 0) {
message->tool_calls = R_NEWS0 (R2AI_ToolCall, n_tool_calls);
message->tool_calls = r_list_new ();
if (!message->tool_calls) {
if (error) {
*error = strdup ("Failed to allocate memory for tool calls");
@@ -197,13 +216,13 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
free (res);
return NULL;
}
message->n_tool_calls = n_tool_calls;
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
}
// Process each content item
int tool_idx = 0;
if (content_array && content_array->type == R_JSON_ARRAY) {
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
RList *cb = r2ai_content_blocks_new ();
if (!cb) {
r_json_free (jres);
free (response_copy);
@@ -211,22 +230,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
free (res);
return NULL;
}
cb->n_blocks = content_array->children.count;
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
if (!cb->blocks) {
free (cb);
r_json_free (jres);
free (response_copy);
r2ai_message_free (message);
free (res);
return NULL;
}
int block_idx = 0;
const RJson *content_item = content_array->children.first;
while (content_item && block_idx < cb->n_blocks) {
while (content_item) {
const RJson *type = r_json_get (content_item, "type");
if (type && type->type == R_JSON_STRING) {
R2AI_ContentBlock *block = &cb->blocks[block_idx];
R2AI_ContentBlock *block = R_NEW0 (R2AI_ContentBlock);
if (!strcmp (type->str_value, "text")) {
// Text content
const RJson *text = r_json_get (content_item, "text");
@@ -242,29 +250,30 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
const RJson *input = r_json_get (content_item, "input");
block->type = strdup ("tool_use");
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
if (name && name->type == R_JSON_STRING) {
block->name = strdup (name->str_value);
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
tc->name = strdup (name->str_value);
}
if (id && id->type == R_JSON_STRING) {
block->id = strdup (id->str_value);
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
tc->id = strdup (id->str_value);
}
if (input && input->type == R_JSON_OBJECT) {
#if 1
char *input_str = r_json_to_string (input);
if (input_str) {
R_LOG_DEBUG ("Input string: %s", input_str);
block->input = strdup (input_str);
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
tc->arguments = strdup (input_str);
free (input_str);
}
#endif
}
r_list_append (message->tool_calls, tc);
tool_idx++;
} else if (!strcmp (type->str_value, "thinking")) {
const RJson *data = r_json_get (content_item, "data");
@@ -281,11 +290,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
}
block->type = strdup ("thinking");
r_strbuf_append (content_buf, "\n\x1b[90m<thinking>\n");
r_strbuf_append (content_buf, "\n" Color_GRAY "<thinking>\n");
r_strbuf_append (content_buf, block->thinking);
r_strbuf_append (content_buf, "\n</thinking>\x1b[0m\n");
r_strbuf_append (content_buf, "\n</thinking>" Color_RESET "\n");
}
block_idx++;
r_list_append (cb, block);
}
content_item = content_item->next;
}
@@ -296,7 +305,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
message->content = r_strbuf_drain (content_buf);
// If there's no content and no tool calls, clean up
if (!message->content && !message->n_tool_calls) {
if (!message->content && (!message->tool_calls || r_list_empty (message->tool_calls))) {
r2ai_message_free (message);
message = NULL;
}
+88
View File
@@ -0,0 +1,88 @@
/* r2ai - Copyright 2023-2026 pancake */
#define R_LOG_ORIGIN "r2ai"
#include "r2ai.h"
#include "r2ai_priv.h"
/* Return the path to the apikeys.txt file */
R_API char *r2ai_apikeys_path(bool *exists) {
char *path = r_file_home (".config/r2ai/apikeys.txt");
if (exists) {
*exists = r_file_exists (path);
}
return path;
}
/* Edit the API keys file */
R_API void r2ai_apikeys_edit(RCorePluginSession *cps) {
RCore *core = cps->core;
bool exists = false;
char *keys_path = r2ai_apikeys_path (&exists);
char *config_dir = r_file_home (".config/r2ai");
r_sys_mkdirp (config_dir);
free (config_dir);
if (!exists) {
const char *template = "# API Keys configuration for r2ai\n"
"\n"
"# Specify your API keys in the format: PROVIDER=KEY\n"
"# OpenAI=sk-your-openai-api-key-here\n"
"# MISTRAL=your-mistral-api-key-here\n";
r_file_dump (keys_path, (const ut8 *)template, strlen (template), 0);
}
r_cons_editor (core->cons, keys_path, NULL);
free (keys_path);
}
/* Return a malloc'd API key read from ~/.config/r2ai/apikeys.txt
* Provider matching is case-insensitive */
R_API char *r2ai_apikeys_get(const char *provider) {
if (!provider) {
return NULL;
}
bool exists = false;
char *config_file = r2ai_apikeys_path (&exists);
if (!exists) {
free (config_file);
return NULL;
}
char *content = r_file_slurp (config_file, NULL);
free (config_file);
if (!content) {
return NULL;
}
RList *lines = r_str_split_list (content, "\n", 0);
char *key = NULL;
if (lines) {
RListIter *iter;
char *line;
r_list_foreach (lines, iter, line) {
const char *trimmed = r_str_trim_head_ro (line);
if (R_STR_ISEMPTY (trimmed) || *trimmed == '#') {
continue;
}
char *eq = strchr (trimmed, '=');
if (eq) {
*eq = 0;
char *lower = strdup (trimmed);
r_str_case (lower, false); /* lowercase */
char *tail = strstr (lower, "_api_key");
if (tail) {
*tail = 0;
}
if (r_str_casecmp (lower, provider) == 0) {
key = strdup (eq + 1);
r_str_trim (key);
free (lower);
break;
}
free (lower);
} else if (*trimmed) {
R_LOG_WARN ("Invalid syntax in apikeys.txt: %s", line);
}
}
r_list_free (lines);
}
free (content);
return key;
}
+419 -183
View File
@@ -1,76 +1,54 @@
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
#include <time.h>
// Forward declaration of the r2ai_llmcall function
extern R2AI_ChatResponse *r2ai_llmcall(RCore *core, R2AIArgs args);
// Add a global structure to track timing and costs
typedef struct {
double total_cost;
double run_cost;
int total_tokens;
int run_tokens;
int total_prompt_tokens;
int run_prompt_tokens;
int total_completion_tokens;
int run_completion_tokens;
time_t start_time;
time_t total_start_time;
} R2AIStats;
static R2AIStats stats = { 0 };
// Stats are now stored in R2AI_State->stats, no longer need global variable
// Helper function to format time duration
static char *format_time_duration(time_t seconds) {
if (seconds < 60) {
return r_str_newf ("%llds", (long long)seconds);
return r_str_newf ("%" PFMT64d "s", (ut64)seconds);
}
if (seconds < 3600) {
return r_str_newf ("%lldm%llds", (long long)(seconds / 60), (long long)(seconds % 60));
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (ut64) (seconds / 60), (ut64) (seconds % 60));
}
return r_str_newf ("%lldh%lldm%llds",
(long long)(seconds / 3600),
(long long)((seconds % 3600) / 60),
(long long)(seconds % 60));
return r_str_newf ("%" PFMT64d "h%" PFMT64d "m%" PFMT64d "s",
(ut64) (seconds / 3600),
(ut64) ((seconds % 3600) / 60),
(ut64) (seconds % 60));
}
// Initialize timing and cost tracking for a run
static void r2ai_stats_init_run(int n_run) {
// Initialize timing tracking for a run
static void r2ai_stats_init_run(R2AI_State *state, int n_run) {
time_t run_start = time (NULL);
if (n_run == 1) {
// First run, initialize total timing
stats.total_cost = 0.0;
stats.run_cost = 0.0;
stats.total_start_time = run_start;
stats.total_tokens = 0;
stats.run_tokens = 0;
stats.total_prompt_tokens = 0;
stats.run_prompt_tokens = 0;
stats.total_completion_tokens = 0;
stats.run_completion_tokens = 0;
state->stats.total_start_time = run_start;
state->stats.total_tokens = 0;
state->stats.run_tokens = 0;
state->stats.total_prompt_tokens = 0;
state->stats.run_prompt_tokens = 0;
state->stats.total_completion_tokens = 0;
state->stats.run_completion_tokens = 0;
}
stats.start_time = run_start;
state->stats.start_time = run_start;
}
// Print a simple run indicator at the start
static void r2ai_print_run_end(RCore *core, const R2AI_Usage *usage, int n_run, int max_runs) {
static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage, int n_run, int max_runs) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
(void)n_run;
(void)max_runs;
time_t run_time = time (NULL) - stats.start_time;
time_t total_time = time (NULL) - stats.total_start_time;
time_t run_time = time (NULL) - state->stats.start_time;
time_t total_time = time (NULL) - state->stats.total_start_time;
if (usage) {
stats.run_tokens = usage->total_tokens;
stats.run_prompt_tokens = usage->prompt_tokens;
stats.run_completion_tokens = usage->completion_tokens;
stats.total_tokens += usage->total_tokens;
stats.total_prompt_tokens += usage->prompt_tokens;
stats.total_completion_tokens += usage->completion_tokens;
}
if (r_config_get_b (core->config, "r2ai.chat.show_cost") == true) {
// TODO: calculate cost
stats.run_cost = 0.0 * run_time;
stats.total_cost += stats.run_cost;
state->stats.run_tokens = usage->total_tokens;
state->stats.run_prompt_tokens = usage->prompt_tokens;
state->stats.run_completion_tokens = usage->completion_tokens;
state->stats.total_tokens += usage->total_tokens;
state->stats.total_prompt_tokens += usage->prompt_tokens;
state->stats.total_completion_tokens += usage->completion_tokens;
}
// Format times for display
@@ -78,85 +56,197 @@ static void r2ai_print_run_end(RCore *core, const R2AI_Usage *usage, int n_run,
char *total_time_str = format_time_duration (total_time);
// Print detailed stats
R2_PRINTF ("\x1b[1;34m%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s\x1b[0m\n",
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n",
r_config_get (core->config, "r2ai.model"),
stats.total_tokens,
stats.total_prompt_tokens,
stats.total_completion_tokens,
stats.run_tokens,
stats.run_prompt_tokens,
stats.run_completion_tokens,
state->stats.total_tokens,
state->stats.total_prompt_tokens,
state->stats.total_completion_tokens,
state->stats.run_tokens,
state->stats.run_prompt_tokens,
state->stats.run_completion_tokens,
run_time_str,
total_time_str);
R2_NEWLINE ();
R2_FLUSH ();
r_cons_newline (core->cons);
r_cons_flush (core->cons);
free (run_time_str);
free (total_time_str);
}
const char *Gprompt_auto = "You are a reverse engineer and you are using radare2 to analyze a binary.\n"
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
"\n"
"# Guidelines\n"
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
"- Do not repeat commands if you already know the answer.\n"
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
"- You must keep going until you have a final answer.\n"
"- Double check that final answer. Make sure you didn't miss anything.\n"
"- Make sure you call tools and functions correctly.\n";
// Helper function to handle final response attempt without tools
static void handle_final_response_attempt(RCorePluginSession *cps, RList *messages, const char *effective_prompt, char **error) {
RCore *core = cps->core;
if (r_cons_yesno (core->cons, 1, "Try to produce response without tool calling with collected information? (Y/n)")) {
char *final_system_prompt = r_str_newf ("%s\n\nIMPORTANT: Do not use any tools. Provide a direct answer based on the collected information.", effective_prompt);
R2AIArgs args_final = {
.messages = messages,
.error = error,
.dorag = true,
.tools = NULL, // No tools for final response
.system_prompt = final_system_prompt
};
R2AI_ChatResponse *final_response = r2ai_llmcall (cps, args_final);
if (final_response && final_response->message) {
const R2AI_Message *final_msg = final_response->message;
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
if (final_msg->reasoning_content) {
r_cons_printf (core->cons, Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", final_msg->reasoning_content);
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
if (final_msg->content) {
r_cons_printf (core->cons, "%s", final_msg->content);
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
// Add final response to messages for completeness
r2ai_msgs_add (messages, final_msg);
free (final_response);
}
free (final_system_prompt);
} else {
r_cons_printf (core->cons, "Auto mode interrupted without final response.\n");
r_cons_flush (core->cons);
}
}
static void update_tool_call_argument(R2AI_Message *msg, const char *tool_call_id, const char *field_name, const char *field_value) {
R_RETURN_IF_FAIL (msg && tool_call_id && field_name && field_value);
if (!msg->tool_calls || r_list_empty (msg->tool_calls)) {
return;
}
RListIter *iter;
R2AI_ToolCall *tc;
r_list_foreach (msg->tool_calls, iter, tc) {
if (!tc->id || strcmp (tc->id, tool_call_id) || !tc->arguments) {
continue;
}
char *args_dup = strdup (tc->arguments);
if (!args_dup) {
return;
}
RJson *args_json = r_json_parse (args_dup);
if (!args_json) {
free (args_dup);
return;
}
RJson *field_json = (RJson *)r_json_get (args_json, field_name);
if (field_json && field_json->str_value) {
const char *orig_value = field_json->str_value;
field_json->str_value = field_value;
char *new_args = r_json_to_string (args_json);
field_json->str_value = orig_value;
if (new_args) {
free ((void *)tc->arguments);
tc->arguments = new_args;
}
}
r_json_free (args_json);
free (args_dup);
return;
}
}
#if 0
static const char *Gprompt_auto =
"You are a reverse engineer using radare2.\n"
"Answer questions about the loaded binary using r2cmd tool calls.\n"
"\n"
"Rules:\n"
"- Use r2cmd tool to run radare2 commands when you need information\n"
"- Make tool calls immediately when you need data\n"
"- Provide final answer only after gathering needed information\n"
"- Keep responses short and direct\n"
;
#else
const char *Gprompt_auto =
"You are a reverse engineer and you are using radare2 to analyze a binary.\n"
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
"\n"
"# Guidelines\n"
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
"- IMPORTANT: You must make only ONE tool call per response. Do not make multiple tool calls in a single response.\n"
"- Keep your reasoning concise and avoid repeating the same thoughts. If you have a clear next step, execute it immediately with a tool call.\n"
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written "
"and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
"- Do not repeat commands if you already know the answer.\n"
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
"- You must keep going until you have a final answer.\n"
"- Double check that final answer. Make sure you didn't miss anything.\n"
"- Make sure you call tools and functions correctly.\n"
"- When calling tools, use the exact format: {\"tool_calls\": [{\"id\": \"call_123\", \"type\": \"function\", \"function\": {\"name\": \"r2cmd\", \"arguments\": \"{\\\"command\\\":\\\"aa\\\"}\"}}]}\n"
"- Arguments must be a JSON string, not an object.\n";
#endif
// Helper function to process messages and handle tool calls recursively
R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *system_prompt, int n_run) {
R_API void process_messages(RCorePluginSession *cps, RList *messages, const char *system_prompt, int n_run) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
char *error = NULL;
bool interrupted = false;
const int max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
if (n_run > max_runs) {
R2_PRINTF ("\x1b[1;31m[r2ai] Max runs reached\x1b[0m\n");
R2_FLUSH ();
R_LOG_WARN ("Max runs reached");
r_cons_flush (core->cons);
return;
}
const char *effective_prompt = system_prompt? system_prompt: Gprompt_auto;
if (!system_prompt) {
const char *init_commands = r_config_get (core->config, "r2ai.auto.init_commands");
if (R_STR_ISNOTEMPTY (init_commands)) {
char *edited_command = NULL;
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command);
if (cmd_output) {
system_prompt = r_str_newf ("%s\n\nHere is some information about the binary to get you started:\n>%s\n%s", Gprompt_auto, edited_command, cmd_output);
char *comment = NULL;
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command, &comment);
if (R_STR_ISNOTEMPTY (cmd_output)) {
char *display_command = strip_command_comment (init_commands, NULL);
char *content = r_str_newf ("Here is some information about the binary to get you started:\n>%s\n%s", display_command, cmd_output);
if (comment && *comment) {
char *new_content = r_str_newf ("%s\nHINT: %s", content, comment);
free (content);
content = new_content;
}
R2AI_Message init_msg = {
.role = "system",
.content = content
};
r2ai_msgs_add (messages, &init_msg);
free (display_command);
free (cmd_output);
}
free (edited_command);
} else {
system_prompt = Gprompt_auto;
free (comment);
}
}
r2ai_stats_init_run (n_run);
r2ai_stats_init_run (state, n_run);
r_cons_printf (core->cons, Color_BLUE "About to call r2ai_llmcall with n_run=%d%s\n", n_run, Color_RESET);
r_cons_flush (core->cons);
// Set up args for r2ai_llmcall call with tools directly
R2AIArgs args = {
.messages = messages,
.error = &error,
.dorag = true,
.tools = r2ai_get_tools (),
.system_prompt = system_prompt
.tools = r2ai_get_tools (core, cps->data), // Always send tools in auto mode
.system_prompt = effective_prompt
};
// Call r2ai_llmcall to get a response
R2AI_ChatResponse *response = r2ai_llmcall (core, args);
R2AI_ChatResponse *response = r2ai_llmcall (cps, args);
if (!response) {
R_LOG_ERROR ("No response from llmcall");
handle_final_response_attempt (cps, messages, effective_prompt, &error);
return;
}
@@ -166,21 +256,41 @@ R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *sy
if (!message) {
R_LOG_ERROR ("No message in response");
free (response);
handle_final_response_attempt (cps, messages, effective_prompt, &error);
return;
}
// Debug logging for rawtools
R_LOG_DEBUG ("Model response - Role: %s", message->role? message->role: "null");
if (message->content) {
R_LOG_DEBUG ("Content: %s", message->content);
}
if (message->reasoning_content) {
R_LOG_DEBUG ("Reasoning: %s", message->reasoning_content);
}
if (message->tool_calls && r_list_length (message->tool_calls) > 0) {
R_LOG_DEBUG ("Tool calls: %d", r_list_length (message->tool_calls));
RListIter *iter;
R2AI_ToolCall *tc;
int i = 0;
r_list_foreach (message->tool_calls, iter, tc) {
R_LOG_DEBUG ("Tool %d: %s - %s", i, tc->name? tc->name: "null", tc->arguments? tc->arguments: "null");
i++;
}
}
// Process the response - we need to add it to our messages array
if (message->content || message->reasoning_content) {
R2_PRINTF ("\x1b[31m[Assistant]\x1b[0m\n\n");
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
if (message->reasoning_content) {
R2_PRINTF ("\x1b[90m<thinking>\n%s\n</thinking>\x1b[0m\n", message->reasoning_content);
R2_NEWLINE ();
R2_FLUSH ();
r_cons_printf (core->cons, Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", message->reasoning_content);
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
if (message->content) {
R2_PRINTF ("%s", message->content);
R2_NEWLINE ();
R2_FLUSH ();
r_cons_printf (core->cons, "%s", message->content);
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
}
@@ -189,17 +299,21 @@ R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *sy
r2ai_msgs_add (messages, message);
// Check for tool calls and process them
if (message->tool_calls && message->n_tool_calls > 0) {
R_LOG_DEBUG ("Found %d tool call (s)", message->n_tool_calls);
if (message->tool_calls && r_list_length (message->tool_calls) > 0) {
R_LOG_DEBUG ("Found %d tool call(s)", r_list_length (message->tool_calls));
// Process each tool call
for (int i = 0; i < message->n_tool_calls; i++) {
const R2AI_ToolCall *tool_call = &message->tool_calls[i];
// Process only the first tool call
RListIter *iter;
R2AI_ToolCall *tool_call;
int i = 0;
r_list_foreach (message->tool_calls, iter, tool_call) {
if (!tool_call->name || !tool_call->arguments || !tool_call->id) {
R_LOG_DEBUG ("Skipping invalid tool call %d", i);
i++;
continue;
}
R_LOG_DEBUG ("Tool call: %s", tool_call->name);
R_LOG_DEBUG ("Processing first tool call %d: %s with args: %s", i, tool_call->name, tool_call->arguments);
// Don't log the full arguments which might get truncated
char *tool_name = strdup (tool_call->name);
char *tool_args = strdup (tool_call->arguments);
@@ -211,54 +325,87 @@ R_API void process_messages(RCore *core, R2AI_Messages *messages, const char *sy
}
char *cmd_output = NULL;
char *comment = NULL;
if (interrupted) {
cmd_output = strdup ("<user interrupted>");
} else {
char *edited_command = NULL;
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command);
// TODO: need to edit the R2AI_Messages* and modify the command of the last tool_use
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command, &comment);
if (edited_command) {
// Update the last message's tool call arguments with the edited command
R2AI_Message *last_msg = r_list_get_n (messages, r_list_length (messages) - 1);
if (last_msg) {
if (!strcmp (tool_name, "r2cmd")) {
update_tool_call_argument (last_msg, tool_call->id, "command", edited_command);
} else if (!strcmp (tool_name, "execute_js")) {
update_tool_call_argument (last_msg, tool_call->id, "script", edited_command);
}
}
}
free (edited_command);
}
free (tool_name);
free (tool_args);
if (strcmp (cmd_output, "R2AI_SIGINT") == 0) {
R2_PRINTF ("\n\n\x1b[1;31m[r2ai] Processing interrupted after tool execution\x1b[0m\n\n");
R2_FLUSH ();
r_cons_printf (core->cons, "\n\n\x1b[1" Color_RED "[r2ai] Processing interrupted after tool execution" Color_RESET "\n\n");
r_cons_flush (core->cons);
free (cmd_output);
cmd_output = strdup ("<user interrupted>");
interrupted = true;
handle_final_response_attempt (cps, messages, effective_prompt, &error);
}
if (comment) {
char *msg = r_str_newf ("HINT: %s\n%s", comment, cmd_output);
free (cmd_output);
cmd_output = msg;
}
free (comment);
// Create a tool call response message
R2AI_Message tool_response = {
.role = "tool",
.tool_call_id = tool_call->id,
.tool_call_id = strdup (tool_call->id),
.content = cmd_output
};
// Add the tool response to our messages array
r2ai_msgs_add (messages, &tool_response);
R_LOG_DEBUG ("Added tool response to messages: %s", cmd_output? cmd_output: "null");
r_cons_printf (core->cons, Color_GREEN "Tool result: %s" Color_RESET, cmd_output? cmd_output: "no output");
free (cmd_output);
i++;
#if 0
// Only process the first valid tool call
break;
#endif
}
r2ai_print_run_end (core, usage, n_run, max_runs);
r2ai_print_run_end (cps, usage, n_run, max_runs);
// Check if we should continue with recursion
if (!interrupted && message->tool_calls && message->n_tool_calls > 0) {
process_messages (core, messages, system_prompt, n_run + 1);
if (!interrupted && message->tool_calls && r_list_length (message->tool_calls) > 0) {
R_LOG_DEBUG ("Recursing to process_messages with n_run=%d", n_run + 1);
process_messages (cps, messages, effective_prompt, n_run + 1);
} else {
R_LOG_DEBUG ("Auto mode loop ending - no more tool calls or interrupted");
}
} else {
r2ai_print_run_end (core, usage, n_run, max_runs);
r2ai_print_run_end (cps, usage, n_run, max_runs);
}
// Free the response struct itself since r2ai_message_free doesn't do it anymore
free (response);
}
R_IPI void cmd_r2ai_a(RCore *core, const char *user_query) {
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
r_cons_printf (core->cons, Color_CYAN "cmd_r2ai_a called with query: %s" Color_RESET "\n", user_query);
r_cons_flush (core->cons);
// Get conversation
R2AI_Messages *messages = r2ai_conversation_get ();
RList *messages = r2ai_conversation_get (state);
if (!messages) {
R_LOG_ERROR ("Conversation not initialized");
return;
@@ -266,51 +413,44 @@ R_IPI void cmd_r2ai_a(RCore *core, const char *user_query) {
// Add user query to the conversation (no system prompt)
// If this is the first message in a new conversation, clear previous history
if (messages->n_messages == 0 || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
if (r_list_empty (messages) || r_config_get_b (core->config, "r2ai.auto.reset_on_query")) {
r2ai_msgs_clear (messages);
}
// Add user query
R2AI_Message user_msg = {
.role = "user",
.content = user_query
.content = (char *)user_query
};
r2ai_msgs_add (messages, &user_msg);
process_messages (core, messages, NULL, 1);
const char *system_prompt = NULL;
if (!r_config_get_b (core->config, "r2ai.auto.think")) {
system_prompt = r_str_newf ("/no_think\nReasoning: Low\n%s", Gprompt_auto);
}
process_messages (cps, messages, system_prompt, 1);
if (system_prompt) {
free ((char *)system_prompt);
}
}
// Helper function to display content with length indication for long content
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg, bool always_show_length) {
if (!content || *content == '\0') {
R2_PRINTF ("%s\n", empty_msg ? empty_msg : "<no content>");
return;
}
size_t content_len = strlen (content);
const size_t max_display = 200;
if (content_len > max_display) {
// Truncate long content and show length
char *truncated = r_str_ndup (content, max_display);
R2_PRINTF ("%s... \x1b[1;37m(length: %zu chars)\x1b[0m\n",
truncated, content_len);
free (truncated);
} else if (always_show_length) {
// Always show length for certain types (like tool responses)
R2_PRINTF ("%s \x1b[1;37m(length: %zu chars)\x1b[0m\n",
content, content_len);
static void print_content_with_length(RCore *core, const char *content, const char *empty_msg) {
if (R_STR_ISEMPTY (content)) {
r_cons_println (core->cons, empty_msg? empty_msg: "<no content>");
} else {
R2_PRINTF ("%s\n", content);
r_cons_println (core->cons, content);
}
}
// Add this function right after cmd_r2ai_a
R_IPI void cmd_r2ai_logs(RCore *core) {
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
// Get conversation
R2AI_Messages *messages = r2ai_conversation_get ();
if (!messages || messages->n_messages == 0) {
R2_PRINTF ("No conversation history available\n");
RList *messages = r2ai_conversation_get (state);
if (!messages || r_list_empty (messages)) {
r_cons_printf (core->cons, "No conversation history available\n");
return;
}
@@ -326,88 +466,184 @@ R_IPI void cmd_r2ai_logs(RCore *core) {
pj_a (pj);
for (int i = 0; i < messages->n_messages; i++) {
const R2AI_Message *msg = &messages->messages[i];
RListIter *iter;
const R2AI_Message *msg;
r_list_foreach (messages, iter, msg) {
pj_o (pj);
pj_ks (pj, "role", msg->role ? msg->role : "unknown");
pj_ks (pj, "content", msg->content ? msg->content : "");
if (msg->tool_calls && msg->n_tool_calls > 0) {
pj_ks (pj, "role", msg->role? msg->role: "unknown");
pj_ks (pj, "content", msg->content? msg->content: "");
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
pj_ka (pj, "tool_calls");
for (int j = 0; j < msg->n_tool_calls; j++) {
const R2AI_ToolCall *tc = &msg->tool_calls[j];
RListIter *iter;
R2AI_ToolCall *tc;
r_list_foreach (msg->tool_calls, iter, tc) {
pj_o (pj);
if (tc->name) {
pj_ks (pj, "name", tc->name);
}
if (tc->arguments) {
pj_ks (pj, "arguments", tc->arguments);
}
pj_end (pj);
}
pj_end (pj);
}
pj_end (pj);
}
pj_end (pj);
char *json_str = pj_drain (pj);
R2_PRINTF ("%s\n", json_str);
r_cons_printf (core->cons, "%s\n", json_str);
free (json_str);
return;
}
R2_PRINTF ("\x1b[1;34m[r2ai] Chat Logs (%d messages)\x1b[0m\n",
core->cons, messages->n_messages);
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n",
r_list_length (messages));
R2_PRINTF ("\x1b[1;33mNote: System prompt is applied automatically but not stored in history\x1b[0m\n\n");
r_cons_printf (core->cons, "\x1b[1" Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n");
// Display each message in the conversation
for (int i = 0; i < messages->n_messages; i++) {
const R2AI_Message *msg = &messages->messages[i];
RListIter *iter;
const R2AI_Message *msg;
r_list_foreach (messages, iter, msg) {
const char *role = msg->role;
// Format based on role
if (!strcmp (role, "user")) {
R2_PRINTF ("\x1b[1;32m[user]:\x1b[0m ");
print_content_with_length (core, msg->content, "<no content>", false);
r_cons_printf (core->cons, "\x1b[1" Color_GREEN "[user]:" Color_RESET " ");
print_content_with_length (core, msg->content, "<no content>");
} else if (!strcmp (role, "assistant")) {
R2_PRINTF ("\x1b[1;36m[assistant]:\x1b[0m ");
print_content_with_length (core, msg->content, "<no content>", false);
r_cons_printf (core->cons, "\x1b[1" Color_CYAN "[assistant]:" Color_RESET " ");
print_content_with_length (core, msg->content, "<no content>");
// Show tool calls if present
if (msg->tool_calls && msg->n_tool_calls > 0) {
for (int j = 0; j < msg->n_tool_calls; j++) {
const R2AI_ToolCall *tc = &msg->tool_calls[j];
R2_PRINTF (" \x1b[1;35m[tool call]:\x1b[0m %s\n",
tc->name ? tc->name : "<unnamed>");
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
RListIter *iter;
R2AI_ToolCall *tc;
r_list_foreach (msg->tool_calls, iter, tc) {
r_cons_printf (core->cons, " \x1b[1" Color_MAGENTA "[tool call]:" Color_RESET " %s\n",
tc->name? tc->name: "<unnamed>");
if (tc->arguments) {
R2_PRINTF (" %s\n", tc->arguments);
r_cons_printf (core->cons, " %s\n", tc->arguments);
}
}
}
} else if (!strcmp (role, "tool")) {
R2_PRINTF ("\x1b[1;35m[tool]:\x1b[0m ");
print_content_with_length (core, msg->content, "<no result>", true);
r_cons_printf (core->cons, "\x1b[1" Color_MAGENTA "[tool]:" Color_RESET " ");
print_content_with_length (core, msg->content, "<no result>");
// Don't show the tool call ID as requested
} else {
// Other roles (system, etc.)
R2_PRINTF ("\x1b[1;37m[%s]:\x1b[0m ", role);
print_content_with_length (core, msg->content, "<no content>", false);
r_cons_printf (core->cons, "\x1b[1" Color_WHITE "[%s]:" Color_RESET " ", role);
print_content_with_length (core, msg->content, "<no content>");
}
R2_NEWLINE ();
R2_FLUSH ();
r_cons_newline (core->cons);
r_cons_flush (core->cons);
}
}
// Helper function to format conversation log as string
static char *format_conversation_log(RList *messages) {
RStrBuf *sb = r_strbuf_new ("");
r_strbuf_append (sb, "Conversation Log:\n");
RListIter *iter;
const R2AI_Message *msg;
r_list_foreach (messages, iter, msg) {
const char *role = msg->role;
if (!strcmp (role, "user")) {
r_strbuf_appendf (sb, "[user]: %s\n", msg->content? msg->content: "<no content>");
} else if (!strcmp (role, "assistant")) {
r_strbuf_appendf (sb, "[assistant]: %s\n", msg->content? msg->content: "<no content>");
// Include tool calls if present
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
RListIter *iter_tc;
R2AI_ToolCall *tc;
r_list_foreach (msg->tool_calls, iter_tc, tc) {
r_strbuf_appendf (sb, " [tool call]: %s\n", tc->name? tc->name: "<unnamed>");
if (tc->arguments) {
r_strbuf_appendf (sb, " %s\n", tc->arguments);
}
}
}
} else if (!strcmp (role, "tool")) {
r_strbuf_appendf (sb, "[tool]: %s\n", msg->content? msg->content: "<no result>");
} else {
r_strbuf_appendf (sb, "[%s]: %s\n", role, msg->content? msg->content: "<no content>");
}
}
return r_strbuf_drain (sb);
}
// Helper function to process conversation with LLM and handle result
static void process_conversation_with_llm(RCorePluginSession *cps, bool compact) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
// Get conversation
RList *messages = r2ai_conversation_get (state);
if (!messages || r_list_empty (messages)) {
r_cons_printf (core->cons, "No conversation history available\n");
return;
}
// Format conversation as string
char *log_str = format_conversation_log (messages);
// Load the compact prompt from file
char *prompt_text = r2ai_load_prompt_text (core, "compact");
if (!prompt_text) {
// Fallback to hardcoded prompt
prompt_text = strdup (compact
? "Create a compact summary of this conversation that preserves all key information, insights, and context for future reference."
"Focus on essential details about the binary analysis, tools used, findings, and any important conclusions."
: "mai create a summary of all the information retrieved from the binary that is relevant for future work");
}
// Combine log and prompt
char *full_input = r_str_newf ("%s\n\n%s", log_str, prompt_text);
char *error = NULL;
char *res = r2ai (cps, (R2AIArgs){ .input = full_input, .error = &error, .dorag = false });
free (log_str);
free (full_input);
free (prompt_text);
if (error) {
R_LOG_ERROR ("%s", error);
free (error);
return;
}
if (res) {
if (compact) {
// Clear the conversation and add summary as system message
r2ai_msgs_clear (messages);
R2AI_Message summary_msg = {
.role = "system",
.content = res
};
r2ai_msgs_add (messages, &summary_msg);
r_cons_printf (core->cons, "Conversation compacted. Summary added to history.\n");
} else {
// Just print the result
r_cons_printf (core->cons, "%s\n", res);
}
free (res);
}
}
R_IPI void cmd_r2ai_c(RCorePluginSession *cps) {
process_conversation_with_llm (cps, true);
}
R_IPI void cmd_r2ai_lr(RCorePluginSession *cps) {
process_conversation_with_llm (cps, false);
}
+216
View File
@@ -0,0 +1,216 @@
/* r2ai - Copyright 2023-2026 pancake, dnakov */
#include "r2ai.h"
/* Gemini API implementation */
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
args.model = r_config_get (core->config, "r2ai.model");
const char *base_url = r2ai_get_provider_url (core, args.provider);
const char *model_name = args.model && strstr (args.model, "gemini")? args.model: "gemini-2.0-flash-exp";
char **error = args.error;
// Create a temp conversation to include the system prompt and the rest of the messages
RList *temp_msgs = r2ai_msgs_new ();
if (!temp_msgs) {
if (error) {
*error = strdup ("Failed to create temporary messages array");
}
return NULL;
}
// Get system prompt
const char *system_prompt = NULL;
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
system_prompt = args.system_prompt;
} else {
system_prompt = r_config_get (core->config, "r2ai.system");
}
if (args.messages) {
RListIter *iter;
R2AI_Message *msg;
r_list_foreach (args.messages, iter, msg) {
r2ai_msgs_add (temp_msgs, msg);
}
} else {
R_LOG_WARN ("No messages");
}
if (error) {
*error = NULL;
}
// Setup headers for Gemini API
const char **headers = NULL;
char *auth_header = NULL;
char *api_key_header = NULL;
if (R_STR_ISNOTEMPTY (args.api_key)) {
static const char *static_headers[] = { NULL, NULL, NULL };
headers = static_headers;
headers[0] = "Content-Type: application/json";
// Check if it's an OAuth token (starts with "ya29.")
if (r_str_startswith (args.api_key, "ya29.")) {
auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
headers[1] = auth_header;
} else {
api_key_header = r_str_newf ("x-goog-api-key: %s", args.api_key);
headers[1] = api_key_header;
}
}
// Build Gemini API URL
char *gemini_url = r_str_newf ("%s/models/%s:generateContent?key=%s",
base_url, model_name, args.api_key);
// Create Gemini-style request JSON
PJ *pj = pj_new ();
pj_o (pj);
// Build contents array
pj_ka (pj, "contents");
RListIter *iter;
R2AI_Message *msg;
int msg_index = 0;
r_list_foreach (temp_msgs, iter, msg) {
pj_o (pj);
// Gemini uses "model" for assistant, "user" for user
const char *role = "user";
if (strcmp (msg->role, "assistant") == 0) {
role = "model";
}
pj_ks (pj, "role", role);
pj_ka (pj, "parts");
pj_o (pj);
// Prepend system prompt to first user message
char *content = NULL;
if (system_prompt && strcmp (msg->role, "user") == 0 && msg_index == 0) {
content = r_str_newf ("System: %s\n\n%s", system_prompt, msg->content? msg->content: "");
} else {
content = msg->content? strdup (msg->content): strdup ("");
}
pj_ks (pj, "text", content);
free (content);
pj_end (pj); // end parts object
pj_end (pj); // end parts array
pj_end (pj); // end content object
msg_index++;
}
pj_end (pj); // end contents array
// Add generation config
pj_ko (pj, "generationConfig");
if (args.max_tokens > 0) {
pj_kn (pj, "maxOutputTokens", args.max_tokens);
}
if (args.temperature > 0) {
pj_kd (pj, "temperature", args.temperature);
}
pj_end (pj); // end generationConfig
pj_end (pj); // end root object
char *request_json = pj_drain (pj);
// Debug: save request
char *tmpdir = r_file_tmpdir ();
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_gemini_request.json", tmpdir);
r_file_dump (req_path, (const ut8 *)request_json, strlen (request_json), 0);
free (req_path);
free (tmpdir);
// Make the API call
char *response = NULL;
int code = 0;
response = r2ai_http_post (core, gemini_url, headers, request_json, &code, NULL);
free (request_json);
free (gemini_url);
free (auth_header);
free (api_key_header);
if (code != 200) {
R_LOG_ERROR ("Gemini API error %d", code);
if (response) {
R_LOG_ERROR ("Gemini API error response: %s", response);
}
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
// Debug: save response
tmpdir = r_file_tmpdir ();
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_gemini_response.json", tmpdir);
r_file_dump (res_path, (const ut8 *)response, strlen (response), 0);
free (res_path);
free (tmpdir);
// Parse Gemini response
RJson *jres = r_json_parse (response);
if (!jres) {
if (error) {
*error = strdup ("Failed to parse Gemini response JSON");
}
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
// Extract response content
const RJson *candidates = r_json_get (jres, "candidates");
if (!candidates || candidates->type != R_JSON_ARRAY || candidates->children.count == 0) {
r_json_free (jres);
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
const RJson *candidate = r_json_item (candidates, 0);
const RJson *content = r_json_get (candidate, "content");
const RJson *parts = r_json_get (content, "parts");
if (!parts || parts->type != R_JSON_ARRAY || parts->children.count == 0) {
r_json_free (jres);
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
const RJson *part = r_json_item (parts, 0);
const RJson *text = r_json_get (part, "text");
char *response_text = NULL;
if (text && text->type == R_JSON_STRING) {
response_text = strdup (text->str_value);
}
r_json_free (jres);
free (response);
r2ai_msgs_free (temp_msgs);
if (!response_text) {
return NULL;
}
// Create response
R2AI_Message *message = R_NEW0 (R2AI_Message);
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
if (message) {
message->role = strdup ("assistant");
message->content = response_text;
}
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
if (result) {
result->message = message;
result->usage = usage;
}
return result;
}
+240
View File
@@ -0,0 +1,240 @@
/* Copyright r2ai - 2023-2026 - pancake */
#define _GNU_SOURCE
// #define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include "r2ai.h"
// Global flag for tracking interrupt status
static volatile sig_atomic_t r2ai_http_interrupted = 0;
// Helper function to get HTTP configuration with defaults
static R2AI_HttpConfig get_http_config(RCore *core) {
R2AI_HttpConfig config = { 120, 10, 30 };
if (core) {
int t = r_config_get_i (core->config, "r2ai.http.timeout");
if (t > 0) {
config.timeout = t;
}
int mr = r_config_get_i (core->config, "r2ai.http.max_retries");
if (mr >= 0) {
config.max_retries = mr;
}
int mb = r_config_get_i (core->config, "r2ai.http.max_backoff");
if (mb > 0) {
config.max_backoff = mb;
}
}
return config;
}
#if R2__UNIX__
// Signal handler for timeout (SIGALRM)
static void r2ai_http_sigint_handler(int sig) {
if (sig == SIGALRM) {
r2ai_http_interrupted = 1;
}
}
#endif
// Portable break callback for r_cons_break (handles SIGINT)
static void r2ai_http_break_callback(void *user) {
(void)user;
r2ai_http_interrupted = 1;
}
// Function pointer type for HTTP request implementations
typedef HttpResponse(*HttpRequestFunc)(const HTTPRequest *request);
// Simple retry sleep
static void sleep_retry(int retry_count, int max_sleep_seconds) {
int delay_seconds = retry_count + 1; // sleep 1, 2, 3, ... seconds
if (delay_seconds > max_sleep_seconds) {
delay_seconds = max_sleep_seconds;
}
r_sys_sleep (delay_seconds);
}
// Generic HTTP request with retry logic
static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTTPRequest *request, RCore *core) {
// Install portable break handler for interruption
if (core && core->cons) {
r_cons_break_push (core->cons, r2ai_http_break_callback, NULL);
}
// Reset interrupt flag
r2ai_http_interrupted = 0;
#if R2__UNIX__
// Ignore SIGWINCH to prevent window resize from interrupting child processes
struct sigaction old_winch_act;
sigaction (SIGWINCH, &(struct sigaction){ .sa_handler = SIG_IGN }, &old_winch_act);
#endif
// Retry loop
HttpResponse result = { 0 };
int retry_count = 0;
bool success = false;
while (!success && retry_count <= request->config.max_retries && !r2ai_http_interrupted) {
result = func (request);
// Check if we were interrupted
if (r2ai_http_interrupted) {
R_LOG_DEBUG ("HTTP request was interrupted by user");
free (result.body);
result.body = NULL;
result.code = -1;
break; // Exit the retry loop
}
// Check for rate limiting or server errors (429, 500, 502, 503, 504)
if (result.code > 0) {
if (result.code == 429 || (result.code >= 500 && result.code < 600)) {
R_LOG_WARN ("Server returned %d response code", result.code);
free (result.body);
result.body = NULL;
result.code = 0;
if (retry_count < request->config.max_retries) {
retry_count++;
R_LOG_INFO ("Retrying request (%d/%d) after error...", retry_count, request->config.max_retries);
sleep_retry (retry_count, request->config.max_backoff);
continue;
}
result.code = -1;
break; // Exit the retry loop after max retries
}
}
// Check for other failures
if (result.code <= 0 || !result.body) {
free (result.body);
result.body = NULL;
result.code = -1;
if (retry_count < request->config.max_retries) {
retry_count++;
R_LOG_INFO ("Retrying request (%d/%d) after failure...", retry_count, request->config.max_retries);
sleep_retry (retry_count, request->config.max_backoff);
continue;
}
break; // Exit the retry loop after max retries
}
success = true;
}
// Restore the original break handler
if (core && core->cons) {
r_cons_break_pop (core->cons);
}
#if R2__UNIX__
// Restore SIGWINCH handler
sigaction (SIGWINCH, &old_winch_act, NULL);
#endif
return result;
}
#include "http/libcurl.inc.c"
#include "http/pwsh.inc.c"
#include "http/curl.inc.c"
#include "http/r2.inc.c"
static HttpRequestFunc select_backend(const char *backend, bool is_post) {
HttpRequestFunc func = NULL;
if (!strcmp (backend, "auto")) {
#if R2__WINDOWS__
// TODO: we should use pwsh or r2 by default, but for not it seems the only backend that works on Windows is r2curl+system
r_sys_setenv ("R2_CURL", "1");
backend = "system";
#elif USE_LIBCURL && HAVE_LIBCURL
backend = "libcurl";
#else
backend = "system";
#endif
R_LOG_DEBUG ("Auto-selected %s backend", backend);
}
if (!strcmp (backend, "system") || !strcmp (backend, "curl")) {
func = is_post? system_curl_post: system_curl_get;
} else if (!strcmp (backend, "libcurl")) {
#if USE_LIBCURL && HAVE_LIBCURL
func = is_post? curl_http_post: curl_http_get;
#else
R_LOG_WARN ("LibCurl requested but not available, falling back to socket implementation");
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
#endif
} else if (!strcmp (backend, "socket")) {
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
} else if (!strcmp (backend, "pwsh") || !strcmp (backend, "powershell")) {
#if R2__WINDOWS__
func = is_post? windows_http_post: windows_http_get;
#else
R_LOG_WARN ("powershell is only available on Windows");
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
#endif
} else if (!strcmp (backend, "r2curl")) {
r_sys_setenv ("R2_CURL", "1");
func = is_post? socket_http_post_with_interrupt: socket_http_get_with_interrupt;
}
return func;
}
// Generic HTTP request function that handles backend selection
static HttpResponse r2ai_http_request(const char *method, RCore *core, const char *url, const char *headers[], const char *data) {
(void)method;
bool is_post = (data != NULL);
HTTPRequest request = {
.config = get_http_config (core),
.url = url,
.data = data,
.headers = headers
};
const char *backend = r_config_get (core->config, "r2ai.http.backend");
HttpRequestFunc func = select_backend (backend, is_post);
if (func) {
return r2ai_http_request_with_retry (func, &request, core);
}
R_LOG_ERROR ("Cannot find a valid http backend");
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
}
R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[], const char *data, int *code, int *rlen) {
HttpResponse response = r2ai_http_request ("POST", core, url, headers, data);
if (response.code <= 0) {
free (response.body);
if (code) {
*code = response.code;
}
return NULL;
}
if (code) {
*code = response.code;
}
if (rlen) {
*rlen = response.length;
}
return response.body;
}
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen) {
HttpResponse response = r2ai_http_request ("GET", core, url, headers, NULL);
if (response.code <= 0) {
if (response.body) {
R_FREE (response.body);
}
if (code) {
*code = response.code;
}
return NULL;
}
if (code) {
*code = response.code;
}
if (rlen) {
*rlen = response.length;
}
return response.body;
}
+99
View File
@@ -0,0 +1,99 @@
// Helper to escape double quotes for cmd double-quoted strings
static char *escape_cmd_double_quotes(const char *str) {
if (!str) {
return NULL;
}
size_t len = strlen (str);
size_t count = 0;
for (size_t i = 0; i < len; i++) {
if (str[i] == '"') {
count++;
}
}
if (count == 0) {
return strdup (str);
}
char *escaped = malloc (len + count + 1);
if (!escaped) {
return NULL;
}
size_t j = 0;
for (size_t i = 0; i < len; i++) {
if (str[i] == '"') {
escaped[j++] = '\\';
escaped[j++] = '"';
} else {
escaped[j++] = str[i];
}
}
escaped[j] = '\0';
return escaped;
}
static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequest *request, const char *input_data) {
HttpResponse error = { .code = -1 };
int timeout = request->config.timeout;
if (!request->url) {
return error;
}
// Compose curl command
RStrBuf *cmd = r_strbuf_new (cmd_start);
// Add timeout
r_strbuf_appendf (cmd, " --connect-timeout %d --max-time %d", 10, timeout);
// Add headers
if (request->headers) {
for (int i = 0; request->headers[i] != NULL; i++) {
char *escaped_header = escape_cmd_double_quotes (request->headers[i]);
r_strbuf_appendf (cmd, " -H \"%s\"", escaped_header);
free (escaped_header);
}
}
char *escaped_url = escape_cmd_double_quotes (request->url);
r_strbuf_appendf (cmd, " -w \"\\n%%{http_code}\" \"%s\"", escaped_url);
free (escaped_url);
char *cmd_str = r_strbuf_drain (cmd);
R_LOG_DEBUG ("Running system curl: %s", cmd_str);
char *response = r_sys_cmd_str (cmd_str, input_data, NULL);
free (cmd_str);
if (!response) {
return error;
}
// Parse the response: body\ncode
char *last_nl = (char *)r_str_rchr (response, NULL, '\n');
int code = -1;
char *body = response;
if (last_nl) {
*last_nl = '\0';
code = atoi (last_nl + 1);
if (code == 0) {
code = -1; // Invalid code
}
} else {
// No newline, assume 200 if response
code = 200;
}
return (HttpResponse){ .body = body, .code = code, .length = strlen (body) };
}
HttpResponse system_curl_get(const HTTPRequest *request) {
R_LOG_DEBUG ("Using system curl GET");
return build_and_execute_curl ("curl -s", request, NULL);
}
HttpResponse system_curl_post(const HTTPRequest *request) {
HttpResponse error = { .code = -1 };
if (!request->url || !request->data) {
return error;
}
R_LOG_DEBUG ("Using system curl POST without files");
HttpResponse res = build_and_execute_curl ("curl -s -X POST -d @-", request, request->data);
return res;
}
+140
View File
@@ -0,0 +1,140 @@
#if HAVE_LIBCURL
#include <curl/curl.h>
// Struct to store response data
typedef struct {
char *data;
size_t size;
} CurlResponse;
// Callback function for curl to write response data
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
// Check if we've been interrupted
if (r2ai_http_interrupted) {
return 0; // Return 0 to abort the transfer
}
size_t realsize = size * nmemb;
CurlResponse *mem = (CurlResponse *)userp;
char *ptr = realloc (mem->data, mem->size + realsize + 1);
if (!ptr) {
R_LOG_ERROR ("Not enough memory for curl response");
return 0; // Out of memory
}
mem->data = ptr;
memcpy (&(mem->data[mem->size]), contents, realsize);
mem->size += realsize;
mem->data[mem->size] = 0;
return realsize;
}
// Curl progress callback function to handle interrupts
static int progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
return r2ai_http_interrupted? 1: 0;
}
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) {
HttpResponse error = { .code = -1 };
if (!request->url || (is_post && !request->data)) {
return error;
}
CURL *curl;
CURLcode res;
struct curl_slist *curl_headers = NULL;
CurlResponse response = { 0 };
// Initialize response
response.data = malloc (1);
if (!response.data) {
return error;
}
response.data[0] = '\0';
response.size = 0;
curl = curl_easy_init ();
if (!curl) {
free (response.data);
return error;
}
// Set URL
curl_easy_setopt (curl, CURLOPT_URL, request->url);
// Set method-specific options
if (is_post) {
curl_easy_setopt (curl, CURLOPT_POST, 1L);
curl_easy_setopt (curl, CURLOPT_POSTFIELDS, request->data);
curl_easy_setopt (curl, CURLOPT_POSTFIELDSIZE, (long)strlen (request->data));
} else {
curl_easy_setopt (curl, CURLOPT_HTTPGET, 1L);
}
// Set headers if provided
if (request->headers) {
for (int i = 0; request->headers[i] != NULL; i++) {
curl_headers = curl_slist_append (curl_headers, request->headers[i]);
}
curl_easy_setopt (curl, CURLOPT_HTTPHEADER, curl_headers);
}
// Set write callback
curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt (curl, CURLOPT_WRITEDATA, (void *)&response);
// Set progress callback to handle interrupts
curl_easy_setopt (curl, CURLOPT_NOPROGRESS, 0L);
curl_easy_setopt (curl, CURLOPT_XFERINFOFUNCTION, progress_callback);
curl_easy_setopt (curl, CURLOPT_XFERINFODATA, NULL);
// Set timeout options
curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, 10L); // 10 seconds connect timeout
curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long)request->config.timeout); // Use configured timeout
// Follow redirects
curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1L);
// Perform the request
res = curl_easy_perform (curl);
// Check for interruption
if (r2ai_http_interrupted) {
R_LOG_DEBUG ("HTTP request was interrupted by user");
free (response.data);
curl_slist_free_all (curl_headers);
curl_easy_cleanup (curl);
return error;
}
// Get response code
long http_code;
curl_easy_getinfo (curl, CURLINFO_RESPONSE_CODE, &http_code);
// Check for errors
if (res != CURLE_OK) {
R_LOG_ERROR ("curl_easy_perform () failed: %s", curl_easy_strerror (res));
free (response.data);
curl_slist_free_all (curl_headers);
curl_easy_cleanup (curl);
return error;
}
// If we get here, the request was successful
// Cleanup
curl_slist_free_all (curl_headers);
curl_easy_cleanup (curl);
return (HttpResponse){ .body = response.data, .code = (int)http_code, .length = response.size };
}
HttpResponse curl_http_get(const HTTPRequest *request) {
return curl_http_request (request, false);
}
HttpResponse curl_http_post(const HTTPRequest *request) {
return curl_http_request (request, true);
}
#endif // HAVE_LIBCURL
+118
View File
@@ -0,0 +1,118 @@
#if R2__WINDOWS__
// Helper to escape single quotes for PowerShell single-quoted strings
static char *escape_single_quotes(const char *str) {
if (!str) {
return NULL;
}
size_t len = strlen (str);
size_t count = 0;
for (size_t i = 0; i < len; i++) {
if (str[i] == '\'') {
count++;
}
}
if (count == 0) {
return strdup (str);
}
char *escaped = malloc (len + count + 1);
if (!escaped) {
return NULL;
}
size_t j = 0;
for (size_t i = 0; i < len; i++) {
if (str[i] == '\'') {
escaped[j++] = '\'';
escaped[j++] = '\'';
} else {
escaped[j++] = str[i];
}
}
escaped[j] = '\0';
return escaped;
}
// Helper to append headers to PowerShell command
static void append_headers_to_cmd(RStrBuf *cmd, const char *const *headers) {
r_strbuf_appendf (cmd, "$headers=@{");
if (headers) {
for (int i = 0; headers[i]; i++) {
char *header = strdup (headers[i]);
char *colon = strchr (header, ':');
if (colon) {
*colon = '\0';
char *key = r_str_trim_dup (header);
char *value = r_str_trim_dup (colon + 1);
char *escaped_key = escape_single_quotes (key);
char *escaped_value = escape_single_quotes (value);
r_strbuf_appendf (cmd, "'%s'='%s';", escaped_key, escaped_value);
free (escaped_key);
free (escaped_value);
free (key);
free (value);
}
free (header);
}
}
r_strbuf_appendf (cmd, "};");
}
// Helper to parse PowerShell response
static HttpResponse parse_powershell_response(char *full_response) {
if (full_response) {
char *newline = strchr (full_response, '\n');
if (newline) {
*newline = '\0';
int code = atoi (full_response);
char *body = newline + 1;
char *body_copy = strdup (body);
size_t length = strlen (body);
free (full_response);
return (HttpResponse){ .body = body_copy, .code = code, .length = length };
} else {
return (HttpResponse){ .body = full_response, .code = 200, .length = strlen (full_response) };
}
}
return (HttpResponse){ .code = -1 };
}
/**
* Windows-specific HTTP POST using PowerShell
*/
HttpResponse windows_http_post(const HTTPRequest *request) {
const char *url = request->url;
const char *const *headers = request->headers;
const char *data = request->data;
int timeout = request->config.timeout;
char *escaped_url = escape_single_quotes (url);
char *escaped_data = escape_single_quotes (data);
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
append_headers_to_cmd (cmd, headers);
r_strbuf_appendf (cmd, "$body='%s';", escaped_data);
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d;", escaped_url, timeout);
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
char *cmd_str = r_strbuf_drain (cmd);
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
free (cmd_str);
return parse_powershell_response (full_response);
}
/**
* Windows-specific HTTP GET using PowerShell
*/
HttpResponse windows_http_get(const HTTPRequest *request) {
const char *url = request->url;
const char *const *headers = request->headers;
int timeout = request->config.timeout;
char *escaped_url = escape_single_quotes (url);
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
append_headers_to_cmd (cmd, headers);
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Get -Uri '%s' -Headers $headers -TimeoutSec %d;", escaped_url, timeout);
r_strbuf_appendf (cmd, "Write-Host $r.StatusCode;$r.Content}catch{Write-Host 0;$_.Exception.Message}\"");
char *cmd_str = r_strbuf_drain (cmd);
R_LOG_DEBUG ("Running PowerShell: %s", cmd_str);
char *full_response = r_sys_cmd_str (cmd_str, NULL, NULL);
free (cmd_str);
return parse_powershell_response (full_response);
}
#endif
+49
View File
@@ -0,0 +1,49 @@
extern volatile sig_atomic_t r2ai_http_interrupted;
extern void r2ai_http_sigint_handler(int sig);
// Socket implementation with interrupt handling and retry logic
static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request) {
#if R2__WINDOWS__
r_sys_setenv ("R2_CURL", "1");
#endif
#if R2__UNIX__
signal (SIGALRM, r2ai_http_sigint_handler);
alarm (request->config.timeout); // Use configured timeout
#endif
int code = 0;
int rlen = 0;
char *result = r_socket_http_post (request->url, (const char **)request->headers, request->data, &code, &rlen);
#if R2__UNIX__
alarm (0);
#endif
if (r2ai_http_interrupted) {
R_LOG_DEBUG ("HTTP request was interrupted by user");
free (result);
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
}
return (HttpResponse){ .body = result, .code = code, .length = rlen };
}
// Socket implementation for GET requests
static HttpResponse socket_http_get_with_interrupt(const HTTPRequest *request) {
#if R2__WINDOWS__
r_sys_setenv ("R2_CURL", "1");
#endif
#if R2__UNIX__
signal (SIGALRM, r2ai_http_sigint_handler);
alarm (request->config.timeout); // Use configured timeout
#endif
// Make the request - use r_socket_http_get if available
int code = 0;
int rlen = 0;
char *result = r_socket_http_get (request->url, (const char **)request->headers, &code, &rlen);
#if R2__UNIX__
alarm (0);
#endif
if (r2ai_http_interrupted) {
R_LOG_DEBUG ("HTTP request was interrupted by user");
free (result);
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
}
return (HttpResponse){ .body = result, .code = code, .length = rlen };
}
-39
View File
@@ -1,39 +0,0 @@
import re
import sys
import subprocess
arg = sys.argv[1]
# Step 1: Format the file in-place using clang-format
subprocess.run(["clang-format", "-i", arg], check=True)
def is_function(s):
return s and not s[0].isspace()
def is_control_structure(s):
return s in {"if", "for", "while", "switch", "catch", "return"}
def fix_line(line):
# Skip lines that are empty or only whitespace
if not line.strip():
return line
# Match function calls like: foo(bar) => foo (bar)
# Avoid if/for/while/catch/return and function *definitions*
pattern = r'\b([a-zA-Z_]\w*)\('
def replacer(match):
name = match.group(1)
if is_control_structure(name) or is_function(line):
return match.group(0) # No change
return f'{name} ('
return re.sub(pattern, replacer, line)
# Step 2: Read the file, transform it, and write it back
with open(arg, "r", encoding="utf-8") as f:
lines = f.readlines()
with open(arg, "w", encoding="utf-8") as f:
for line in lines:
f.write(fix_line(line))
+153
View File
@@ -0,0 +1,153 @@
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
// TODO: move into r2/libr/util/json.c
//
// Helper function to convert RJson to PJ without draining
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj) {
if (!json) {
return existing_pj;
}
PJ *pj = existing_pj? existing_pj: pj_new ();
if (!pj) {
return NULL;
}
switch (json->type) {
case R_JSON_STRING:
pj_s (pj, json->str_value);
break;
case R_JSON_INTEGER:
pj_n (pj, json->num.u_value);
break;
case R_JSON_DOUBLE:
pj_d (pj, json->num.dbl_value);
break;
case R_JSON_BOOLEAN:
pj_b (pj, json->num.u_value);
break;
case R_JSON_NULL:
pj_null (pj);
break;
case R_JSON_OBJECT:
if (!existing_pj) {
pj_o (pj);
}
// Handle object's properties
const RJson *prop = json->children.first;
while (prop) {
if (prop->key) {
switch (prop->type) {
case R_JSON_STRING:
pj_ks (pj, prop->key, prop->str_value);
break;
case R_JSON_INTEGER:
pj_kn (pj, prop->key, prop->num.u_value);
break;
case R_JSON_DOUBLE:
pj_kd (pj, prop->key, prop->num.dbl_value);
break;
case R_JSON_BOOLEAN:
pj_kb (pj, prop->key, prop->num.u_value);
break;
case R_JSON_NULL:
pj_knull (pj, prop->key);
break;
case R_JSON_OBJECT:
pj_ko (pj, prop->key);
if (!r_json_to_pj (prop, pj)) {
if (!existing_pj) {
pj_free (pj);
}
return NULL;
}
pj_end (pj);
break;
case R_JSON_ARRAY:
pj_ka (pj, prop->key);
if (!r_json_to_pj (prop, pj)) {
if (!existing_pj) {
pj_free (pj);
}
return NULL;
}
pj_end (pj);
break;
default:
break;
}
}
prop = prop->next;
}
if (!existing_pj) {
pj_end (pj);
}
break;
case R_JSON_ARRAY:
if (!existing_pj) {
pj_a (pj);
}
// Handle array items
const RJson *item = json->children.first;
while (item) {
switch (item->type) {
case R_JSON_STRING:
pj_s (pj, item->str_value);
break;
case R_JSON_INTEGER:
pj_n (pj, item->num.u_value);
break;
case R_JSON_DOUBLE:
pj_d (pj, item->num.dbl_value);
break;
case R_JSON_BOOLEAN:
pj_b (pj, item->num.u_value);
break;
case R_JSON_NULL:
pj_null (pj);
break;
case R_JSON_OBJECT:
pj_o (pj);
if (!r_json_to_pj (item, pj)) {
if (!existing_pj) {
pj_free (pj);
}
return NULL;
}
pj_end (pj);
break;
case R_JSON_ARRAY:
pj_a (pj);
if (!r_json_to_pj (item, pj)) {
if (!existing_pj) {
pj_free (pj);
}
return NULL;
}
pj_end (pj);
break;
default:
break;
}
item = item->next;
}
if (!existing_pj) {
pj_end (pj);
}
break;
default:
if (!existing_pj) {
pj_free (pj);
}
return NULL;
}
return pj;
}
// Helper function to clone RJson to string
R_API char *r_json_to_string(const RJson *json) {
PJ *pj = r_json_to_pj (json, NULL);
return pj? pj_drain (pj): NULL;
}
+395
View File
@@ -0,0 +1,395 @@
/* Copyright r2ai - 2023-2026 - pancake */
#define R_LOG_ORIGIN "llm"
#include "r2ai.h"
#include "r2ai_priv.h"
#include <string.h>
static const R2AIProvider r2ai_providers[] = {
{ "openai", "https://api.openai.com/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "gemini", "https://generativelanguage.googleapis.com/v1beta", R2AI_API_GEMINI, true, false },
{ "anthropic", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
{ "claude", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
{ "ollama", "http://localhost:11434/api", R2AI_API_OLLAMA, false, true },
{ "ollamacloud", "https://ollama.com/api", R2AI_API_OLLAMA, true, true },
{ "openapi", "http://127.0.0.1:11434", R2AI_API_OPENAI_COMPATIBLE, false, false },
{ "opencode", "https://opencode.ai/zen/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "zen", "https://opencode.ai/zen/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "xai", "https://api.x.ai/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "openrouter", "https://openrouter.ai/api/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "groq", "https://api.groq.com/openai/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "mistral", "https://api.mistral.ai/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "lmstudio", "http://127.0.0.1:1234/v1", R2AI_API_OPENAI_COMPATIBLE, false, true },
{ "deepseek", "https://api.deepseek.com/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ NULL, NULL, R2AI_API_OPENAI_COMPATIBLE, false, false } // sentinel
};
R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
if (R_STR_ISEMPTY (name)) {
return NULL;
}
for (int i = 0; r2ai_providers[i].name; i++) {
if (!strcmp (name, r2ai_providers[i].name)) {
return &r2ai_providers[i];
}
}
return NULL;
}
// Forward declaration for rawtools
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
// Check if rawtools mode is enabled
bool rawtools_enabled = r_config_get_b (core->config, "r2ai.auto.raw");
const char *provider = args.provider? args.provider: r_config_get (core->config, "r2ai.api");
if (!provider) {
provider = "gemini";
}
if (rawtools_enabled && args.tools && r_list_length (args.tools) > 0) {
return r2ai_rawtools_llmcall (cps, args);
}
R2AI_State *state = cps->data;
R2AI_ChatResponse *res = NULL;
if (!args.model) {
const char *config_model = r_config_get (core->config, "r2ai.model");
args.model = strdup (config_model? config_model: "");
}
if (!args.provider) {
args.provider = strdup (provider);
}
if (!args.max_tokens) {
args.max_tokens = r_config_get_i (core->config, "r2ai.max_tokens");
}
if (!args.temperature) {
const char *configtemp = r_config_get (core->config, "r2ai.temperature");
args.temperature = configtemp? atof (configtemp): 0;
}
const R2AIProvider *prov = r2ai_get_provider (provider);
if (!prov) {
R_LOG_ERROR ("Unknown provider: %s", provider);
return NULL;
}
char *api_key = NULL;
if (prov->requires_api_key) {
api_key = r2ai_apikeys_get (provider);
if (api_key) {
args.api_key = api_key;
}
}
// Make sure we have an API key before proceeding
if (prov->requires_api_key) {
if (R_STR_ISEMPTY (args.api_key)) {
char *Provider = strdup (provider);
r_str_case (Provider, true);
R_LOG_ERROR ("No API key found for the %s provider. Use r2ai -K", provider);
free (Provider);
return NULL;
}
}
// Set system_prompt from config if it's not already set
if (!args.system_prompt) {
args.system_prompt = r_config_get (core->config, "r2ai.system");
}
if (!args.messages) {
args.messages = r2ai_msgs_new ();
}
int context_pullback = -1;
// context and user message
if (args.input && r_config_get_b (core->config, "r2ai.data")) {
const int K = r_config_get_i (core->config, "r2ai.data.nth");
if (!state->db) {
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
r2ai_refresh_embeddings (cps);
}
RStrBuf *sb = r_strbuf_new ("");
r_strbuf_appendf (sb, "\n ## Query\n\n%s\n ## Context\n", args.input);
RVdbResultSet *rs = r_vdb_query (state->db, args.input, K);
if (rs) {
int i;
for (i = 0; i < rs->size; i++) {
RVdbResult *r = &rs->results[i];
KDNode *n = r->node;
r_strbuf_appendf (sb, "- %s.\n", n->text);
}
r_vdb_result_free (rs);
}
char *m = r_strbuf_drain (sb);
R2AI_Message msg = { .role = "user", .content = m };
context_pullback = r_list_length (args.messages);
r2ai_msgs_add (args.messages, &msg);
free (m);
// TODO: we can save the msg without context
}
// Add the rest of the messages one by one
if (!args.messages && args.input) {
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
args.messages = r2ai_msgs_new ();
r2ai_msgs_add (args.messages, &msg);
}
args.thinking_tokens = r_config_get_i (core->config, "r2ai.thinking_tokens");
const R2AIProvider *p = r2ai_get_provider (provider);
if (!p) {
return NULL;
}
switch (p->api_type) {
case R2AI_API_ANTHROPIC:
res = r2ai_anthropic (cps, args);
break;
case R2AI_API_GEMINI:
res = r2ai_gemini (cps, args);
break;
case R2AI_API_OPENAI_COMPATIBLE:
case R2AI_API_OLLAMA:
default:
res = r2ai_openai (cps, args);
break;
}
if (context_pullback != -1) {
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
free ((char *)msg->content);
msg->content = strdup (args.input);
}
if (*args.error) {
R_LOG_ERROR ("%s", *args.error);
free (*args.error);
*args.error = NULL;
}
return res;
}
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
const R2AIProvider *p = r2ai_get_provider (provider);
if (!p) {
return NULL;
}
// Handle providers that support custom baseurl
if (p->supports_custom_baseurl) {
const char *host = r_config_get (core->config, "r2ai.baseurl");
if (R_STR_ISNOTEMPTY (host)) {
if (r_str_startswith (host, "http")) {
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
if (r_str_endswith (host, "/v1")) {
return r_str_newf ("%s", host);
}
return r_str_newf ("%s/v1", host);
}
return r_str_newf ("%s/api", host);
}
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
return r_str_newf ("http://%s/v1", host);
}
return r_str_newf ("http://%s/api", host);
}
}
return p->url;
}
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
if (!provider) {
return NULL;
}
const char *purl = r2ai_get_provider_url (core, provider);
if (!purl) {
return NULL;
}
// Get API key for authentication (except for providers that don't require it)
char *api_key = NULL;
const R2AIProvider *p = r2ai_get_provider (provider);
if (p && p->requires_api_key) {
// Consolidated helper to fetch the API key from env or file
api_key = r2ai_apikeys_get (provider);
}
char *models_url = NULL;
int code = 0;
char *response = NULL;
// Special handling for Gemini
if (!strcmp (provider, "gemini")) {
if (!api_key) {
return NULL;
}
models_url = r_str_newf ("%s/models?key=%s", purl, api_key);
const char *headers[2] = { "Content-Type: application/json", NULL };
R_LOG_DEBUG ("GET %s", models_url);
response = r2ai_http_get (core, models_url, headers, &code, NULL);
} else {
// Create models endpoint URL
const R2AIProvider *prov = r2ai_get_provider (provider);
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
if (api_key) {
const char *headers[4] = { "Content-Type: application/json", NULL, NULL, NULL };
char *auth_header = NULL;
char *version_header = NULL;
const R2AIProvider *prov = r2ai_get_provider (provider);
if (prov && prov->api_type == R2AI_API_ANTHROPIC) {
// Anthropic uses different header format
auth_header = r_str_newf ("x-api-key: %s", api_key);
version_header = strdup ("anthropic-version: 2023-06-01");
headers[1] = auth_header;
headers[2] = version_header;
} else {
// Standard OpenAI-compatible format
auth_header = r_str_newf ("Authorization: Bearer %s", api_key);
headers[1] = auth_header;
}
// Make HTTP GET request
R_LOG_DEBUG ("GET %s Headers: %s", models_url, headers);
response = r2ai_http_get (core, models_url, headers, &code, NULL);
free (auth_header);
free (version_header);
} else {
// We have no headers
R_LOG_DEBUG ("GET %s", models_url);
response = r2ai_http_get (core, models_url, NULL, &code, NULL);
}
}
free (models_url);
free (api_key);
if (!response || code != 200) {
R_LOG_DEBUG ("Failed to fetch models from %s (code: %d)", provider, code);
free (response);
return NULL;
}
// Parse JSON response
RList *models = r_list_newf (free);
if (!models) {
free (response);
return NULL;
}
RJson *json = r_json_parse (response);
if (json) {
const RJson *data = NULL;
if (!strcmp (provider, "gemini")) {
// Gemini has "models" array directly
data = r_json_get (json, "models");
} else {
const R2AIProvider *prov = r2ai_get_provider (provider);
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
data = r_json_get (json, usetags? "models": "data");
}
if (data && data->type == R_JSON_ARRAY) {
const RJson *model_item = data->children.first;
while (model_item) {
const RJson *id = NULL;
char *model_id = NULL;
if (!strcmp (provider, "gemini")) {
// Gemini: extract model ID from "name" field (e.g., "models/gemini-1.5-flash" -> "gemini-1.5-flash")
const RJson *name = r_json_get (model_item, "name");
if (name && name->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name->str_value)) {
char *s = strdup (name->str_value);
RList *parts = r_str_split_list (s, "/", 0);
if (parts && r_list_length (parts) > 1) {
model_id = strdup ((char *)r_list_get_n (parts, r_list_length (parts) - 1));
} else {
model_id = strdup (name->str_value);
}
r_list_free (parts);
// Only include Gemini models
if (!strstr (model_id, "gemini")) {
free (model_id);
model_id = NULL;
}
free (s);
}
} else {
const R2AIProvider *prov = r2ai_get_provider (provider);
if (prov && prov->api_type == R2AI_API_OLLAMA) {
id = r_json_get (model_item, "model");
} else {
id = r_json_get (model_item, "id");
}
if (id && id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (id->str_value)) {
model_id = strdup (id->str_value);
}
}
if (model_id) {
R_LOG_DEBUG ("Model: %s", model_id);
r_list_append (models, model_id);
}
model_item = model_item->next;
}
}
r_json_free (json);
}
free (response);
return models;
}
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb) {
for (size_t i = 0; r2ai_providers[i].name; i++) {
if (sb) {
if (i > 0) {
r_strbuf_append (sb, ", ");
}
r_strbuf_append (sb, r2ai_providers[i].name);
} else {
r_cons_println (core->cons, r2ai_providers[i].name);
}
}
}
R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
RListIter *iter, *iter2;
char *line;
char *file;
// refresh embeddings database
r_vdb_free (state->db);
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
// enumerate .txt files in directory
const char *path = r_config_get (core->config, "r2ai.data.path");
RList *files = r_sys_dir (path);
if (r_list_empty (files)) {
R_LOG_WARN ("Cannot find any file in r2ai.data.path");
}
r_list_foreach (files, iter, file) {
if (!r_str_endswith (file, ".txt")) {
continue;
}
R_LOG_DEBUG ("Index %s", file);
char *filepath = r_file_new (path, file, NULL);
char *text = r_file_slurp (filepath, NULL);
if (text) {
R_LOG_DEBUG ("Index %s", file);
RList *lines = r_str_split_list (text, "\n", -1);
r_list_foreach (lines, iter2, line) {
if (r_str_trim_head_ro (line)[0] == 0) {
continue;
}
r_vdb_insert (state->db, line);
R_LOG_DEBUG ("Insert %s", line);
}
r_list_free (lines);
}
free (filepath);
}
r_list_free (files);
}
+250 -19
View File
@@ -1,31 +1,262 @@
/* Copyright r2ai - 2023-2025 - pancake */
/* Copyright r2ai - 2023-2026 - pancake */
#include <r_core.h>
#include "r2ai.h"
#include "r2ai_priv.h"
int main(int argc, char **argv) {
if (argc < 2) {
eprintf ("Usage: r2ai <prompt>\n");
return 1;
#define CLIPPY "?E"
// #define CLIPPY "?EC" // pico le croco
static void show_help() {
printf (
"Usage: r2ai [-vhp:m:q:Eb:Kc:f:iew] <prompt>\n"
" -v Show version information\n"
" -h Show this help message\n"
" -p <provider> Select LLM provider\n"
" -m <model> Select LLM model\n"
" -q <query> Execute predefined prompt query (can be used multiple times)\n"
" -b <url> Set base URL for provider API\n"
" -c <command> Execute command after loading file (can be used multiple times)\n"
" -f <file> Load file with r2 API\n"
" -i <script> Load and interpret script file before executing commands\n"
" -e <var=value> Set configuration variable\n"
" -E Edit the r2ai rc file\n"
" -K Edit the API keys file\n"
" -w Launch interactive setup wizard\n");
}
static void show_version() {
printf ("r2ai " R2AI_VERSION "\n");
}
static char *build_conversation(RList *conversation) {
RListIter *iter;
char *msg;
RStrBuf *sb = r_strbuf_new ("");
r_list_foreach (conversation, iter, msg) {
r_strbuf_appendf (sb, "%s\n", msg);
}
return r_strbuf_drain (sb);
}
const char *prompt = argv[1];
static void r2ai_repl(RCorePluginSession *cps, const char *provider, const char *model, RList *conversation) {
RCore *core = cps->core;
r2ai_wizard_autorun (core);
r_line_set_prompt (core->cons->line, "[r2ai]> ");
while (true) {
const char *input = r_line_readline (core->cons);
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (input)) {
break;
}
if (input[0] == '!') {
// Execute shell command
system (input + 1);
} else if (input[0] == ':') {
// Run radare2 command
char *res = r_core_cmd_str (core, input + 1);
if (res) {
r_cons_println (core->cons, res);
free (res);
}
} else if (input[0] == 'q') {
if (r_cons_yesno (core->cons, 'y', "Do you want to quit? (Y/n)")) {
break;
}
} else if (input[0] == '-') {
cmd_r2ai (cps, input);
} else {
// Send message to LLM as part of conversation
r_list_append (conversation, r_str_newf ("User: %s", input));
char *full_prompt = build_conversation (conversation);
char *err = NULL;
R2AIArgs args = {
.input = full_prompt,
.provider = provider,
.model = model,
.error = &err,
.dorag = false,
};
char *res = r2ai (cps, args);
if (res) {
if (r_config_get_b (core->config, "r2ai.clippy")) {
char *cmd = r_str_newf (CLIPPY " %s", res);
r_core_cmd_call (core, cmd);
free (cmd);
} else {
r_cons_println (core->cons, res);
r_list_append (conversation, r_str_newf ("Assistant: %s", res));
}
free (res);
}
if (err) {
r_cons_println (core->cons, err);
free (err);
}
free (full_prompt);
}
r_cons_flush (core->cons);
}
}
int main(int argc, const char **argv) {
int c;
const char *provider = NULL;
const char *model = NULL;
const char *baseurl = NULL;
const char *filename = NULL;
const char *scriptfile = NULL;
RList *conversation = r_list_newf (free);
RList *queries = r_list_newf (free);
RList *commands = r_list_newf (free);
RCore *core = r_core_new ();
char *err = NULL;
R2AIArgs args = {
.input = prompt,
.error = &err,
.dorag = false,
RCorePluginSession cps = {
.core = core
};
r2ai_init (&cps);
char *res = r2ai (core, args);
if (res) {
R_LOG_INFO (res);
free (res);
RGetopt opt;
r_getopt_init (&opt, argc, argv, "vhp:m:q:Eb:Kc:f:ie:w");
while ((c = r_getopt_next (&opt)) != -1) {
switch (c) {
case 'p':
provider = opt.arg;
break;
case 'm':
model = opt.arg;
break;
case 'q':
r_list_append (queries, strdup (opt.arg));
break;
case 'b':
baseurl = opt.arg;
break;
case 'c':
r_list_append (commands, strdup (opt.arg));
break;
case 'f':
filename = opt.arg;
break;
case 'i':
scriptfile = opt.arg;
break;
case 'e':
{
char *var_value = strdup (opt.arg);
char *equals = strchr (var_value, '=');
if (equals) {
*equals = '\0';
const char *var = var_value;
const char *value = equals + 1;
r_config_set (core->config, var, value);
} else {
r_cons_println (core->cons, "Invalid format for -e. Use var=value.");
}
free (var_value);
}
break;
case 'E':
{
char *rc_path = r_file_home (".config/r2ai/rc");
r_cons_editor (core->cons, rc_path, NULL);
free (rc_path);
goto beach;
}
break;
case 'K':
{
r2ai_apikeys_edit (&cps);
goto beach;
}
break;
case 'w':
{
r2ai_wizard (core);
goto beach;
}
break;
case 'v':
show_version ();
goto beach;
case 'h':
show_help ();
goto beach;
default:
show_help ();
goto beach;
}
}
free (err);
if (baseurl) {
r_config_set (core->config, "r2ai.baseurl", baseurl);
}
if (filename) {
char *cmd = r_str_newf ("o %s", filename);
r_core_cmd_call (core, cmd);
free (cmd);
// TODO: use the C api instead of the 'o' command and check for error if file exists etc
} else {
r_core_cmd0 (core, CLIPPY " Interesting, no files to analyse. Use -f or :o");
r_cons_flush (core->cons);
}
if (scriptfile) {
char *cmd = r_str_newf (". %s", scriptfile);
r_core_cmd_call (core, cmd);
free (cmd);
}
if (!r_list_empty (commands)) {
RListIter *iter;
char *cmd;
r_list_foreach (commands, iter, cmd) {
char *res = r_core_cmd_str (core, cmd);
if (res) {
r_cons_println (core->cons, res);
free (res);
}
}
}
r_config_set_b (core->config, "r2ai.utf8", false);
r_config_set_b (core->config, "r2ai.clippy", true);
if (provider) {
r_config_set (core->config, "r2ai.api", provider);
}
if (model) {
r_config_set (core->config, "r2ai.model", model);
}
// Process queries if any
if (!r_list_empty (queries)) {
RListIter *iter;
char *query;
r_list_foreach (queries, iter, query) {
r2ai_cmd_q (&cps, query);
}
} else {
if (opt.ind >= argc) {
r2ai_repl (&cps, provider, model, conversation);
} else {
const char *prompt = argv[opt.ind];
char *err = NULL;
R2AIArgs args = {
.input = prompt,
.provider = provider,
.model = model,
.error = &err,
.dorag = false,
};
char *res = r2ai (&cps, args);
if (res) {
r_cons_println (core->cons, res);
free (res);
}
free (err);
r_cons_flush (core->cons);
}
}
beach:
r_list_free (conversation);
r_list_free (queries);
r_list_free (commands);
r2ai_fini (&cps);
r_core_free (core);
return 0;
}
Loaded 100 of 116 files, more files were not shown because too many files have changed in this diff. Show more