mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-09 17:46:36 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9077de9162 | ||
|
|
287fad0ecd | ||
|
|
08afde5117 | ||
|
|
a086a12f1d | ||
|
|
4a86d0fc46 | ||
|
|
0509bf6755 | ||
|
|
1d55b7f72a | ||
|
|
222f0b7410 | ||
|
|
065288e29f | ||
|
|
5824aa4482 | ||
|
|
3c7ed851d8 | ||
|
|
1586d27e93 | ||
|
|
898855d7ce | ||
|
|
20a3751624 | ||
|
|
d81957d856 | ||
|
|
b6d7c9424c | ||
|
|
c3efc53c27 | ||
|
|
b6d234c154 | ||
|
|
ef7f49a68e | ||
|
|
d1fcea6859 | ||
|
|
96dcc0dd4f | ||
|
|
659e14a4cd | ||
|
|
926ef545d0 | ||
|
|
1b11c648de | ||
|
|
394e55cf87 | ||
|
|
8db446cf31 | ||
|
|
65d7d87539 | ||
|
|
372b38380f | ||
|
|
72b9ff2f49 | ||
|
|
742d3f2a51 | ||
|
|
1f3f93fa35 | ||
|
|
3f1b91fbce | ||
|
|
acd0513ebb | ||
|
|
c44e838238 | ||
|
|
f4fa4ba432 | ||
|
|
561db876ac | ||
|
|
bcdc182d03 | ||
|
|
19cba835e4 | ||
|
|
87a1e89112 | ||
|
|
012118c25c | ||
|
|
e2030b31a1 | ||
|
|
53f437d55a | ||
|
|
66f202254e | ||
|
|
8e9555d144 | ||
|
|
a8caa51e76 | ||
|
|
021aa205f0 | ||
|
|
92c86f94e6 | ||
|
|
50c1d63a47 | ||
|
|
79f01945b7 | ||
|
|
9043638e01 | ||
|
|
6bd0227382 | ||
|
|
9f9a3b87ae | ||
|
|
b68ddaeaed | ||
|
|
d2663f75dc | ||
|
|
e24dbab677 | ||
|
|
c765269b02 | ||
|
|
55b856e70b | ||
|
|
5e148bc882 | ||
|
|
9f93754483 | ||
|
|
d262fabb2b | ||
|
|
d0a1f292f5 | ||
|
|
dad02ac243 | ||
|
|
02c561c415 | ||
|
|
9414cdf72b | ||
|
|
7cc61c4cc5 | ||
|
|
e4875eeb37 | ||
|
|
888251a2ea | ||
|
|
af6153ad64 | ||
|
|
87e4e7b232 | ||
|
|
cc3a003a64 | ||
|
|
f2be5cc3c2 | ||
|
|
e360148931 | ||
|
|
d06a9ee361 | ||
|
|
2e7b97b065 | ||
|
|
cd5d9a1640 | ||
|
|
e606392aaf | ||
|
|
8bf1169844 | ||
|
|
90e3bbafc5 | ||
|
|
e5a0639b88 | ||
|
|
80f27cd4d9 | ||
|
|
455da7bdba | ||
|
|
623e98ed3d | ||
|
|
17bf56b5e3 | ||
|
|
f50a7836eb | ||
|
|
2d9b69c624 | ||
|
|
de9c7c904c | ||
|
|
6b3ea982cd | ||
|
|
209d9257ee | ||
|
|
b840d002ee | ||
|
|
9d900b2a6b | ||
|
|
227b15e793 | ||
|
|
4b9dbf7686 | ||
|
|
8ca32e258f | ||
|
|
449c41eae6 | ||
|
|
5210fe6892 | ||
|
|
6222791075 | ||
|
|
05ce908e3a | ||
|
|
b8457c0bfa | ||
|
|
6a256b9210 | ||
|
|
8cc748ab4c | ||
|
|
17df397a3d | ||
|
|
fcd464f1cc | ||
|
|
1f41b6d2d7 | ||
|
|
adf8189541 | ||
|
|
cb8f3548ff | ||
|
|
b99034e5d8 | ||
|
|
fa1f6c1d3c | ||
|
|
d9fb7b18df | ||
|
|
e0b9ff235c | ||
|
|
e1bf58d4e4 | ||
|
|
51a63ac7ef | ||
|
|
c3a3eff046 | ||
|
|
37b5c2b6d0 | ||
|
|
0e6a8cc2e2 | ||
|
|
8dcc6f0418 | ||
|
|
b68cdd1171 | ||
|
|
3f935a46be | ||
|
|
fd46390d41 | ||
|
|
63106508dc | ||
|
|
46f42d3dd9 | ||
|
|
836fae3d91 | ||
|
|
66dd3dced7 | ||
|
|
c016fc72ed | ||
|
|
779b787f2d | ||
|
|
ca210f149b | ||
|
|
99e99b1797 | ||
|
|
fb5427238b | ||
|
|
d7c2dacd7a | ||
|
|
d6c32f4b72 | ||
|
|
0a083e7231 | ||
|
|
1a067fbbfc | ||
|
|
8a724d3682 | ||
|
|
8f7d152aa3 | ||
|
|
7120c325da | ||
|
|
14c991cd10 | ||
|
|
d1ef1e065e | ||
|
|
6f8ec21a39 | ||
|
|
3db0744c91 | ||
|
|
3306e412e2 | ||
|
|
20f5e16164 | ||
|
|
660249a31f | ||
|
|
312921c2a1 | ||
|
|
2356dbef9e | ||
|
|
7612a80a9c | ||
|
|
12d8bc9e22 | ||
|
|
d4be5f8569 | ||
|
|
a9333c8c38 | ||
|
|
29bdb2fcea | ||
|
|
078376c5d2 | ||
|
|
aa24702335 | ||
|
|
9aa40216a2 | ||
|
|
96e454e0c6 | ||
|
|
219b034261 | ||
|
|
797c967fb0 | ||
|
|
e61b7db8a5 | ||
|
|
3a39f9cd9d | ||
|
|
2b5e50d8f0 | ||
|
|
bb737f5be6 | ||
|
|
3e5f91e631 | ||
|
|
a3154bdcaf | ||
|
|
a6be6e6555 | ||
|
|
ad0bdbb902 | ||
|
|
4651d3b1b3 | ||
|
|
572e02c4ec | ||
|
|
75c787ef41 | ||
|
|
731f1933a9 | ||
|
|
573755ccef | ||
|
|
ecca7825fe | ||
|
|
95117608fb | ||
|
|
b3958b5f7d | ||
|
|
d0eaf37764 | ||
|
|
65692edd54 | ||
|
|
50f55bb5d2 | ||
|
|
fe1768fcc3 | ||
|
|
f241fba17d | ||
|
|
e5252960e0 | ||
|
|
8a45dadbde | ||
|
|
53d32efbbe | ||
|
|
f06faba9d6 | ||
|
|
8b52c2f6e2 | ||
|
|
b37436fb8d | ||
|
|
2524e66825 | ||
|
|
519017a75e | ||
|
|
6f30d6ca37 | ||
|
|
08ea4d11b2 | ||
|
|
0281308434 | ||
|
|
17a59d2057 | ||
|
|
eac0b8c6ef | ||
|
|
24aee426b4 | ||
|
|
b31bc5c116 | ||
|
|
01738210f5 | ||
|
|
7b19c4ff99 | ||
|
|
1986dd88bd | ||
|
|
1ed40401dd | ||
|
|
6ac0ff6352 | ||
|
|
4158e5f157 | ||
|
|
225ff58228 | ||
|
|
a51be44c68 | ||
|
|
911e455f72 | ||
|
|
70eb975b7f | ||
|
|
a0d36cc909 | ||
|
|
030551816b | ||
|
|
88c44c871b | ||
|
|
9218e3ea27 | ||
|
|
6f7ffcae1a | ||
|
|
fe12d2f8ee | ||
|
|
ae55b2f204 | ||
|
|
7d4e949240 | ||
|
|
9b97d39c7a | ||
|
|
33f72a997e | ||
|
|
36a8f13131 | ||
|
|
5c2ded0171 | ||
|
|
d5a39816a8 | ||
|
|
5aab38ab76 |
No files matched your search
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- buildsystem
|
||||
+85
-69
@@ -1,7 +1,7 @@
|
||||
name: CI and Release
|
||||
|
||||
env:
|
||||
R2V: 6.0.4
|
||||
R2V: 6.2.6
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
if: matrix.build_system == 'meson'
|
||||
@@ -34,38 +34,11 @@ 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@v3
|
||||
with:
|
||||
version: ${{ env.R2V }}
|
||||
|
||||
- name: Build with Make
|
||||
if: matrix.build_system == 'make'
|
||||
@@ -75,7 +48,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,51 +56,92 @@ 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
|
||||
|
||||
- name: Create release artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release
|
||||
# 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/*
|
||||
else
|
||||
powershell.exe -Command "Compress-Archive -Path release/* -DestinationPath 'release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip'"
|
||||
fi
|
||||
bindist:
|
||||
name: Build r2pm binary (${{ matrix.os }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Upload artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Install radare2
|
||||
uses: radareorg/github-actions@v3
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
|
||||
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
|
||||
version: ${{ env.R2V }}
|
||||
|
||||
# - name: Smoke test
|
||||
# run: |
|
||||
# if command -v r2 >/dev/null 2>&1; then
|
||||
# r2 -q -c 'r2ai -h' -- || true
|
||||
# fi
|
||||
- name: Package r2pm binary
|
||||
run: make -C src bindist
|
||||
|
||||
- name: Upload r2pm binary artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: bindist-${{ matrix.os }}
|
||||
path: r2ai-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
windows-bindist:
|
||||
name: Build r2pm binary (Windows)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install meson ninja
|
||||
|
||||
- name: Install radare2
|
||||
id: r2
|
||||
uses: radareorg/github-actions@v3
|
||||
with:
|
||||
version: ${{ env.R2V }}
|
||||
|
||||
- name: Build with Meson
|
||||
shell: powershell
|
||||
run: |
|
||||
cd src
|
||||
$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
|
||||
|
||||
- name: Package r2pm binary
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path bindist\plugins,bindist\bin,bindist\prompts | Out-Null
|
||||
Get-ChildItem src\build\ -Filter "*r2ai*.dll" -ErrorAction SilentlyContinue | Copy-Item -Destination bindist\plugins
|
||||
Get-ChildItem src\..\build\ -Filter "*r2ai*.dll" -ErrorAction SilentlyContinue | Copy-Item -Destination bindist\plugins
|
||||
Copy-Item src\build\r2ai.exe bindist\bin
|
||||
Copy-Item prompts\*.r2ai.md bindist\prompts
|
||||
$rv = (& radare2 -qv).Trim()
|
||||
Compress-Archive -Path bindist\* -DestinationPath "r2ai-$rv-windows-x86-64.zip" -Force
|
||||
|
||||
- name: Upload r2pm binary artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-bindist
|
||||
path: r2ai-*.zip
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
needs: build
|
||||
needs: [bindist, windows-bindist]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Verify tag is on master
|
||||
run: |
|
||||
@@ -137,18 +151,20 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
- name: Download r2pm binary artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: '*bindist*'
|
||||
path: release-artifacts
|
||||
merge-multiple: true
|
||||
|
||||
- name: Add decai.r2.js to release artifacts
|
||||
run: cp decai/decai.r2.js release-artifacts/
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
files: |
|
||||
release-artifacts/**/*.zip
|
||||
release-artifacts/*.zip
|
||||
release-artifacts/decai.r2.js
|
||||
generate_release_notes: true
|
||||
generate_release_notes: true
|
||||
@@ -12,3 +12,6 @@ venv
|
||||
*.dylib
|
||||
*.a
|
||||
third_party
|
||||
|
||||
# r2pm binary packages
|
||||
r2ai-*.zip
|
||||
@@ -23,12 +23,12 @@ if [ -f ~/.r2ai.openai-key ]; then
|
||||
OPENAI_KEY=$(cat ~/.r2ai.openai-key)
|
||||
fi
|
||||
CLAUDE_KEY=""
|
||||
CLAUDE_MODEL="claude-3-5-sonnet-20241022"
|
||||
CLAUDE_MODEL="claude-sonnet-5-5"
|
||||
if [ -f ~/.r2ai.anthropic-key ]; then
|
||||
CLAUDE_KEY=$(cat ~/.r2ai.anthropic-key)
|
||||
fi
|
||||
DEEPSEEK_KEY=""
|
||||
DEEPSEEK_MODEL="claude-3-5-sonnet-20241022"
|
||||
DEEPSEEK_MODEL="claude-sonnet-5-5"
|
||||
if [ -f ~/.r2ai.deepseek-key ]; then
|
||||
DEEPSEEK_KEY=$(cat ~/.r2ai.anthropic-key)
|
||||
fi
|
||||
@@ -138,7 +138,7 @@ GEMINI_KEY=~/.r2ai-gemini.key
|
||||
OPENAI_KEY=~/.r2ai-openai.key
|
||||
CLAUDE_KEY=~/.r2ai-anthropic.key
|
||||
DEEPSEEK_KEY=~/.r2ai-deepseek.key
|
||||
CLAUDE_MODEL=claude-3-5-sonnet-20241022
|
||||
CLAUDE_MODEL=claude-sonnet-5-5
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ python3 main.py
|
||||
**Example selecting a remote models:**
|
||||
|
||||
```
|
||||
[r2ai:0x00006aa0]> -m anthropic:claude-3-7-sonnet-20250219
|
||||
[r2ai:0x00006aa0]> -m anthropic:claude-sonnet-5-5
|
||||
[r2ai:0x00006aa0]> -m openai:gpt-4
|
||||
```
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Uncensored:
|
||||
Remote:
|
||||
-m openapi:http://localhost:5001
|
||||
-m openai:gpt-4
|
||||
-m anthropic:claude-3-7-sonnet-20250219
|
||||
-m anthropic:claude-sonnet-5-5
|
||||
-m bedrock:anthropic.claude-3-5-sonnet-20241022-v1
|
||||
-m kobaldcpp:http://localhost:5001
|
||||
"""
|
||||
@@ -80,12 +80,9 @@ OpenAI:
|
||||
-m openai:gpt-4
|
||||
-m openai:gpt-4-1106-preview
|
||||
Anthropic:
|
||||
-m anthropic:claude-2.1
|
||||
-m anthropic:claude-3-haiku-20240307
|
||||
-m anthropic:claude-3-opus-20240229
|
||||
-m anthropic:claude-3-sonnet-20240229
|
||||
-m anthropic:claude-3-5-sonnet-20241022
|
||||
-m anthropic:claude-3-7-sonnet-20250219
|
||||
-m anthropic:claude-opus-4-8
|
||||
-m anthropic:claude-sonnet-5-5
|
||||
-m anthropic:claude-haiku-4-5-20251001
|
||||
groq:
|
||||
-m groq:gemma-7b-it
|
||||
-m groq:llama2-70b-4096
|
||||
|
||||
@@ -170,8 +170,7 @@ def slurp_until(endword):
|
||||
def list_models():
|
||||
print("ollama/llama3:latest")
|
||||
print("ollama/granite-code:8b")
|
||||
print("anthropic/claude-3-5-sonnet-20240620")
|
||||
print("anthropic/claude-3-7-sonnet-20250219")
|
||||
print("anthropic/claude-sonnet-5-5")
|
||||
print("openai/gpt-4o")
|
||||
print("mistral/mistral-tiny")
|
||||
|
||||
|
||||
+14
-3
@@ -50,8 +50,16 @@ def stt(seconds, lang):
|
||||
return
|
||||
tts("(r2ai) listening for 5s... ", "digues?", lang)
|
||||
print(f"DEVICE IS {device}")
|
||||
os.system("rm -f .audiomsg.wav")
|
||||
rc = os.system(f"ffmpeg -f avfoundation -t 5 -i '{device}' .audiomsg.wav > /dev/null 2>&1")
|
||||
try:
|
||||
os.remove(".audiomsg.wav")
|
||||
except OSError:
|
||||
pass
|
||||
result = subprocess.run(
|
||||
["ffmpeg", "-f", "avfoundation", "-t", "5", "-i", device, ".audiomsg.wav"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
rc = result.returncode
|
||||
if rc != 0:
|
||||
tts("(r2ai)", "cannot record from microphone. missing permissions in terminal?", lang)
|
||||
return
|
||||
@@ -60,7 +68,10 @@ def stt(seconds, lang):
|
||||
result = model.transcribe(".audiomsg.wav")
|
||||
else:
|
||||
result = model.transcribe(".audiomsg.wav", language=lang)
|
||||
os.system("rm -f .audiomsg.wav")
|
||||
try:
|
||||
os.remove(".audiomsg.wav")
|
||||
except OSError:
|
||||
pass
|
||||
tts("(r2ai)", "ok", lang)
|
||||
text = result["text"].strip()
|
||||
if text == "you":
|
||||
|
||||
@@ -3,4 +3,7 @@ TARGETS=all clean mrproper user-install install user-uninstall uninstall fmt ind
|
||||
$(TARGETS):
|
||||
$(MAKE) -C src $@
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
test: all
|
||||
$(MAKE) -C test
|
||||
|
||||
.PHONY: $(TARGETS) test
|
||||
@@ -27,6 +27,9 @@ 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
|
||||
@@ -67,30 +70,34 @@ $ 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 model=claude-sonnet-5-5
|
||||
r2ai -e max_tokens=64000
|
||||
```
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
vim decai/decai.r2.js
|
||||
vim decai/src/constants.ts
|
||||
make -C decai/src
|
||||
vim src/r2ai.h
|
||||
vim src/meson.build
|
||||
vim VERSION
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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`
|
||||
|
||||
## Hints
|
||||
|
||||
* Never read the minified, autogenerated `decai.r2.js`
|
||||
* Focus and work only on the TypeScript source
|
||||
* Never run `tsc` or `npx`. Always use `make` targets to compile
|
||||
+5
-6
@@ -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
|
||||
@@ -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
|
||||
@@ -55,10 +56,12 @@ Configuration options:
|
||||
```console
|
||||
[0x00000000]> decai -e
|
||||
decai -e api=ollama
|
||||
decai -e apitype=chat
|
||||
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 +99,16 @@ 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.
|
||||
|
||||
Use `decai -e http=curl` to force system curl, `http=r2` to require `r2.http()`,
|
||||
or `http=auto` (the default) to use `r2.http()` when available and curl otherwise.
|
||||
Failed `r2.http()` calls return an error without falling back to curl. The
|
||||
`timeout` setting applies only to system curl. Save `http=curl` in
|
||||
`~/.config/r2ai/decai.txt` to persist the override.
|
||||
|
||||
List possible APIs to discuss with AI: `decai -e api=?`:
|
||||
|
||||
```
|
||||
@@ -106,6 +119,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
|
||||
@@ -197,3 +218,21 @@ void daemonize() {
|
||||
}
|
||||
...
|
||||
```
|
||||
|
||||
## WASM HTTP
|
||||
|
||||
With radare2's WASI HTTP build, decai defaults to `r2.http()` to reach the existing
|
||||
`r_socket_http_get/post` C APIs. Headers and JSON payloads go directly to the
|
||||
host HTTP adapter; no shell curl, temporary payload file or WebSocket proxy is
|
||||
needed. A failed/denied HTTP call is returned as an error, without a shell
|
||||
fallback. In `http=auto` mode, runtimes without `r2.http()` use system curl.
|
||||
|
||||
Enable HTTP in the embedding application (`enabled: true` in JavaScript or
|
||||
`network=True` in Python). The radare2 sandbox must permit both `socket` and
|
||||
`network`; process execution is not needed. Browser requests remain subject
|
||||
to CORS. The host adapter controls the HTTP deadline in WASM; decai's `timeout`
|
||||
setting still controls native curl only. Responses are buffered, not streamed.
|
||||
|
||||
This requires radareorg/radare2#27037 and the rebuilt decai script. For the
|
||||
C r2ai plugin compiled into WASM, the `auto` HTTP backend likewise selects
|
||||
radare2's C HTTP implementation; native backend selection is unchanged.
|
||||
+64
-1256
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
# decai r2plugin
|
||||
|
||||
Boilerplate to embed `decai.r2.js` inside the radare2 core library using the
|
||||
external plugin system (XPS). Unlike `r2ai/r2plugin`, this does not link any
|
||||
native code: the r2.js script is converted into a C buffer and registered as
|
||||
an embedded startup script, so r2 runs it at startup exactly like a file in
|
||||
the `plugins/*.r2.js` directory (honoring `cfg.plugins`).
|
||||
|
||||
The `decai.r2.js` bundle is built from the typescript sources with
|
||||
`r2frida-compile` (see `make -C src` in the decai directory). The C buffer is
|
||||
generated at build time with `rax2 -C` (make builds) or `js2c.py` (meson).
|
||||
|
||||
## Usage
|
||||
|
||||
decai lives inside the r2ai repository, so expose it as `libr/xps/p/decai`:
|
||||
|
||||
```console
|
||||
cd radare2/libr/xps/p
|
||||
git clone https://github.com/radareorg/r2ai
|
||||
ln -s r2ai/decai decai
|
||||
```
|
||||
|
||||
For acr/make builds:
|
||||
|
||||
```console
|
||||
cd radare2
|
||||
make -C libr/xps EXTERNAL_PLUGINS=decai
|
||||
./configure-plugins
|
||||
make
|
||||
```
|
||||
|
||||
For meson builds, make `libr/xps/p/meson.build` contain:
|
||||
|
||||
```meson
|
||||
subdir('decai/r2plugin')
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/decai.r2.js.o: \
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/decai.r2.js.c
|
||||
@@ -0,0 +1,3 @@
|
||||
#ifdef R2_CORE_H
|
||||
extern RCorePlugin r_core_plugin_decai;
|
||||
#endif
|
||||
@@ -0,0 +1,10 @@
|
||||
DECAI_CORE_WD=$(LIBR)/xps/p/decai
|
||||
DECAI_CORE_OBJ= \
|
||||
$(DECAI_CORE_WD)/r2plugin/core/plugin.o \
|
||||
$(DECAI_CORE_WD)/r2plugin/core/decai.r2.js.o
|
||||
EXTERNAL_STATIC_OBJS+=$(DECAI_CORE_OBJ)
|
||||
|
||||
# decai.r2.js can be rebuilt from typescript with r2frida-compile (make -C src)
|
||||
$(DECAI_CORE_WD)/r2plugin/core/decai.r2.js.c: $(DECAI_CORE_WD)/decai.r2.js
|
||||
rax2 -C < $(DECAI_CORE_WD)/decai.r2.js | \
|
||||
sed -e 's,^unsigned,const unsigned,' -e 's,buf,decai_qjs,g' > $@
|
||||
@@ -0,0 +1,35 @@
|
||||
/* decai - LGPL - Copyright 2026 - pancake */
|
||||
|
||||
#include <r_core.h>
|
||||
|
||||
// generated from ../decai.r2.js, see ../deps.mk
|
||||
extern const unsigned char decai_qjs[];
|
||||
extern const unsigned int decai_qjs_len;
|
||||
|
||||
static bool decai_init(RCorePluginSession *cps) {
|
||||
r_core_script_embed (cps->core, "decai", "qjs", (const char *)decai_qjs, (int)decai_qjs_len);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decai_call(RCorePluginSession *cps, const char *input) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RCorePlugin r_core_plugin_decai = {
|
||||
.meta = {
|
||||
.name = "decai",
|
||||
.desc = "AI decompiler (embedded decai.r2.js)",
|
||||
.author = "pancake",
|
||||
.license = "MIT",
|
||||
},
|
||||
.init = decai_init,
|
||||
.call = decai_call,
|
||||
};
|
||||
|
||||
#ifndef R2_PLUGIN_INCORE
|
||||
R_API RLibStruct radare_plugin = {
|
||||
.type = R_LIB_TYPE_CORE,
|
||||
.data = &r_core_plugin_decai,
|
||||
.version = R2_VERSION,
|
||||
};
|
||||
#endif
|
||||
@@ -0,0 +1,372 @@
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/plugin.o: \
|
||||
/Users/pancake/prg/radare2/libr/xps/p/decai/r2plugin/core/plugin.c \
|
||||
/Users/pancake/prg/radare2/libr/include/r_core.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_main.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types_base.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/ctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_ctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_symbol_aliasing.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_posix_availability.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/ptrcheck.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_bounds.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/runetype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_size_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ct_rune_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_rune_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_wchar_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_wint_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/appleapiopts.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/types.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int8_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int16_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int32_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_int64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int8_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int16_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int32_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_intptr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_uintptr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/__endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/libkern/_OSByteOrder.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/libkern/arm/_OSByteOrder.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_char.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_short.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_u_int.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_caddr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_dev_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_blkcnt_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_blksize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_gid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_in_addr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_in_port_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ino_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ino64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_key_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mode_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_nlink_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_id_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_pid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_off_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_uid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_clock_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ssize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_time_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_useconds_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_suseconds_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_rsize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_errno_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_def.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/Availability.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityVersions.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityInternal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityInternalLegacy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_setsize.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_set.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_clr.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_zero.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_isset.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fd_copy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_attr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_cond_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_condattr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_mutex_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_mutexattr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_once_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_rwlock_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_rwlockattr_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_pthread/_pthread_key_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fsblkcnt_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_fsfilcnt_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_limits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/syslimits.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stdint.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdint.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint8_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint16_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint32_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uint64_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_intmax_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_uintmax_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stdbool.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/math.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types_overflow.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_str_util.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_userconf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_version.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stddef.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stddef.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_header_macro.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_ptrdiff_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_size_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_rsize_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_wchar_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_null.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_max_align_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stddef_offsetof.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdlib.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdlib.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/wait.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/signal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/signal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/signal.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_mcontext.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_mcontext.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach/machine/_structs.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/mach/arm/_structs.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_sigaltstack.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_ucontext.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_sigset_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/resource.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timeval.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/alloca.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_null.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/malloc/_malloc.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/malloc/_malloc_type.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/malloc/_ptrcheck.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_abort.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/assert.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_assert.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_static_assert.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/inttypes.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/inttypes.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_inttypes.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_types_null.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_va_list.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_printf.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_seek_set.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_ctermid.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_stdio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_common.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/string.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_string.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_strings.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_strings.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/secure/_string.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/stdarg.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_header_macro.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg___gnuc_va_list.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_va_list.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_va_arg.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg___va_copy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/21/include/__stdarg_va_copy.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/fcntl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/fcntl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_o_sync.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_o_dsync.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_s_ifmt.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timespec.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_filesec_t.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_endian.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/stat.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/dirent.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/dirent.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/unistd.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/unistd.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_posix_vdisable.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/select.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_select.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_uuid_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/gethostuuid.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/time.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timeval64.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/time.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_time.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_getopt.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_regex.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_list.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/sdb.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/asserts.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/types.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/config.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_pp.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_inc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ls.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/heap.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/dict.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/cdb.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/cdb_make.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/buffer.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/version.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/rangstr.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_skiplist.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_th.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_w32.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_sys.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/errno.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/errno.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/semaphore.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/semaphore.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread/sched.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread/pthread_impl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/pthread/qos.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/qos.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mach_port_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/signal.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_event.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_up.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_vec.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_assert.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_log.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_arena.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_itv.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_signal.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_alloc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_rbtree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_trie.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strs.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_xml.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_new_rbtree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_intervaltree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/../r_types.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_big.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base32.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base36.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base64.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_base91.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_buf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_mem.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_ref.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bits.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bitmap.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bitset.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_time.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_debruijn.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_type.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_file.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_hex.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_name.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_num.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_table.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_graph.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_panels.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_pool.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_diff.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_cons.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/pj.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strbuf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_json.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_sandbox.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_stack.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_str.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/wchar.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_wchar.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_mbstate_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/__wctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/___wctype.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_types/_wctype_t.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_str_constpool.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/termios.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/termios.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ttycom.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ioccom.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ttydefaults.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ioctl.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/filio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/sockio.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/net/if.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/net/if_var.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_timeval32.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/net/net_kev.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/socket.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/constrained_ctypes.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/machine/_param.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arm/_param.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_sa_family_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_socklen_t.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/_types/_iovec_t.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_punycode.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_queue.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_range.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_spaces.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strs_store.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_bloom.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_strpool.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_tree.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_uleb128.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_utf8.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_utf16.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_utf32.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_idpool.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_asn1.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_graph_drawable.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_config.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/bplist.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_x509.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_pkcs7.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_protobuf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_token.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_axml.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_cfloat.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/libc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_socket.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bind.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netinet/in.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netinet6/in6.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/un.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/poll.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/poll.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/arpa/inet.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netdb.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/netinet/tcp.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_magic.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_agraph.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_io.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_lib.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/dlfcn.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_w32dw.h \
|
||||
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/ptrace.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_fs.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_egg.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_asm.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_arch.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bin.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_muta.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_hash.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_muta/r_sm4.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bin_dwarf.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_reg.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_anal/op.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_anal/hint.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_esil.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/ht_uu.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_anal.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_util/r_print.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_search.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_syscall.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_flag.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_codemeta.h \
|
||||
/Users/pancake/prg/radare2/libr/include/sdb/set.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_lang.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_cmd.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_sign.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_debug.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_bp.h \
|
||||
/Users/pancake/prg/radare2/libr/include/rvc.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_core_priv.h \
|
||||
/Users/pancake/prg/radare2/libr/include/r_core_task.h
|
||||
@@ -0,0 +1 @@
|
||||
include p/decai/r2plugin/core/deps.mk
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert a script file into a C buffer named decai_qjs (same output as `rax2 -C` + sed)."""
|
||||
|
||||
import sys
|
||||
|
||||
data = open(sys.argv[1], "rb").read()
|
||||
with open(sys.argv[2], "w") as f:
|
||||
f.write("const unsigned char decai_qjs[] = {\n")
|
||||
for i in range(0, len(data), 16):
|
||||
f.write(" %s,\n" % ", ".join("0x%02x" % b for b in data[i:i + 16]))
|
||||
f.write("};\nconst unsigned int decai_qjs_len = %d;\n" % len(data))
|
||||
@@ -0,0 +1,16 @@
|
||||
# decai XPS integration for radare2.
|
||||
# Included from libr/xps/meson.build when decai lives under libr/xps/p/decai/.
|
||||
# Unlike native plugins, decai ships no native code: decai.r2.js is embedded
|
||||
# as a buffer in libr_core and the tiny core plugin registers it with
|
||||
# r_core_script_embed() so r2 runs it at startup like any plugins/*.r2.js file.
|
||||
|
||||
core_plugins += ['decai']
|
||||
|
||||
decai_py3 = import('python').find_installation()
|
||||
|
||||
r_core_additional_sources += files('core/plugin.c')
|
||||
r_core_additional_sources += custom_target('decai_r2_js_c',
|
||||
input: '../decai.r2.js',
|
||||
output: 'decai.r2.js.c',
|
||||
command: [decai_py3, files('js2c.py'), '@INPUT@', '@OUTPUT@'],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
core.decai
|
||||
@@ -0,0 +1,19 @@
|
||||
PLUGIN_NAME=decai
|
||||
R2F_C_FLAGS=-c -S
|
||||
R2FRIDA_COMPILE?=r2pm -r r2frida-compile
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
$(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
|
||||
@@ -0,0 +1,53 @@
|
||||
import { API_KEYS_PATH, DECAI_CONFIG_DIR } from "./constants";
|
||||
import { providerRegistry } from "./providers";
|
||||
import { ApiKeyResult } from "./types";
|
||||
import { ensurePath, fileExists, parseEnvLikeString } from "./utils";
|
||||
|
||||
function getProviderEnvMap(): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(providerRegistry)
|
||||
.filter(([, v]) => v.authKey)
|
||||
.map(([k, v]) => [k, v.authKey!]),
|
||||
);
|
||||
}
|
||||
|
||||
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(getProviderEnvMap()).forEach(([key, env]) => {
|
||||
const status = getApiKey(key, env)[2];
|
||||
console.log(status, "\t", key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,568 @@
|
||||
import { AutoReply } from "./types";
|
||||
import { state } from "./state";
|
||||
import { COMMAND, DEFAULT_PROMPT, HELP_TEXT, VERSION } from "./constants";
|
||||
import {
|
||||
b64,
|
||||
filterResponse,
|
||||
trimAnsi,
|
||||
trimDown,
|
||||
trimJson,
|
||||
} from "./utils";
|
||||
import { editApiKeys, listApiKeys } from "./apiKeys";
|
||||
import { editRcConfig, evalConfig, listAllConfig } from "./config";
|
||||
import { r2ai } from "./r2ai";
|
||||
|
||||
function withColorDisabled<T>(fn: () => T): T {
|
||||
const origColor = r2.cmd("e scr.color");
|
||||
r2.cmd("e scr.color=0");
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
r2.cmd("e scr.color=" + origColor);
|
||||
}
|
||||
}
|
||||
|
||||
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 : "";
|
||||
|
||||
try {
|
||||
const parsedArgs = args.slice(2).trim();
|
||||
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";
|
||||
}
|
||||
|
||||
const { body, count } = withColorDisabled(() => {
|
||||
let body = "## Before:\n";
|
||||
let count = 0;
|
||||
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";
|
||||
return { body, count };
|
||||
});
|
||||
|
||||
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) {
|
||||
const err = e as Error;
|
||||
console.error(err, err.stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function explainFunction(): string {
|
||||
const hints = withColorDisabled(() =>
|
||||
"[START]" +
|
||||
state.commands.split(",").map((c) => r2.cmd(c)).join("\n") + "[END]"
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function currentFunctionExists(): boolean {
|
||||
return r2.cmd("afi.").trim().length > 0;
|
||||
}
|
||||
|
||||
const noFunctionMessage = "Cannot find function at current offset";
|
||||
|
||||
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": {
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
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": {
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
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":
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
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":
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { ConfigHandler, 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";
|
||||
}
|
||||
|
||||
type StateKey = keyof typeof state;
|
||||
|
||||
function boolHandler(key: StateKey): ConfigHandler {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const s = state as any;
|
||||
return {
|
||||
get: () => s[key] as boolean,
|
||||
set: (v: string) => { s[key] = parseBoolean(v); },
|
||||
};
|
||||
}
|
||||
|
||||
function stringHandler(key: StateKey): ConfigHandler {
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const s = state as any;
|
||||
return {
|
||||
get: () => s[key] as string,
|
||||
set: (v: string) => { s[key] = v; },
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeApiType(value: string): "chat" | "generate" | null {
|
||||
const apiType = value.trim();
|
||||
if (apiType === "chat") {
|
||||
return "chat";
|
||||
}
|
||||
if (apiType === "generate") {
|
||||
return "generate";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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: boolHandler("deterministic"),
|
||||
files: boolHandler("useFiles"),
|
||||
think: {
|
||||
get: () => state.think || "false",
|
||||
set: (v: string) => { state.think = v; },
|
||||
},
|
||||
apitype: {
|
||||
get: () => state.apitype,
|
||||
set: (v: string) => {
|
||||
const apiType = normalizeApiType(v);
|
||||
if (apiType) {
|
||||
state.apitype = apiType;
|
||||
} else {
|
||||
console.error("Invalid apitype. Use chat or generate.");
|
||||
}
|
||||
},
|
||||
},
|
||||
debug: boolHandler("debug"),
|
||||
http: {
|
||||
get: () => state.http,
|
||||
set: (v: string) => {
|
||||
const backend = v.trim();
|
||||
if (backend === "auto" || backend === "r2" || backend === "curl") {
|
||||
state.http = backend;
|
||||
} else {
|
||||
console.error("Invalid http backend. Use auto, r2 or curl.");
|
||||
}
|
||||
},
|
||||
},
|
||||
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 === "?") {
|
||||
console.error(listProviders().join("\n"));
|
||||
} else {
|
||||
state.api = v;
|
||||
}
|
||||
},
|
||||
},
|
||||
lang: stringHandler("language"),
|
||||
hlang: stringHandler("humanLanguage"),
|
||||
cache: boolHandler("cache"),
|
||||
cmds: stringHandler("commands"),
|
||||
tts: boolHandler("tts"),
|
||||
yolo: boolHandler("yolo"),
|
||||
prompt: stringHandler("prompt"),
|
||||
ctxfile: stringHandler("contextFile"),
|
||||
baseurl: stringHandler("baseurl"),
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
export const VERSION = "1.5.4";
|
||||
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 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 apitype=chat|generate' choose the Ollama API endpoint (default: chat)
|
||||
* '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 http=auto|r2|curl' select the HTTP transport (default: auto, prefers r2.http)
|
||||
* '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...
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
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}`;
|
||||
}
|
||||
|
||||
function collectHeaders(
|
||||
headerLines: string[],
|
||||
into: Map<string, string>,
|
||||
): void {
|
||||
for (const rawLine of headerLines) {
|
||||
const header = parseHeaderLine(rawLine.trim());
|
||||
if (header) {
|
||||
into.set(normalizeHeaderName(header.name), formatHeader(header));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function parseHeaders(value: string): string[] {
|
||||
const headers = new Map<string, string>();
|
||||
const lines = value.replace(/\\n/g, "\n").split(/\r?\n/g)
|
||||
.filter((l) => { const t = l.trim(); return t && !t.startsWith("#"); });
|
||||
collectHeaders(lines, headers);
|
||||
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) {
|
||||
collectHeaders(group, merged);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
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 executeRequest(options: HttpRequestOptions): string {
|
||||
const { method, url, headers, payload } = options;
|
||||
const requestHeaders = mergeHeaders(
|
||||
["Content-Type: application/json"],
|
||||
headers,
|
||||
);
|
||||
if (state.http !== "curl") {
|
||||
if (r2.http) {
|
||||
return r2.http(method, url, requestHeaders.join("\n"), payload ?? "").body;
|
||||
}
|
||||
if (state.http === "r2") {
|
||||
throw new Error("r2.http is not available in this runtime");
|
||||
}
|
||||
}
|
||||
const cmdParts = ["curl", "-s"];
|
||||
if (state.timeout > 0) {
|
||||
cmdParts.push("--max-time", String(state.timeout));
|
||||
}
|
||||
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(tmpfile), shellEscape(url));
|
||||
const cmd = cmdParts.join(" ") + "; rm " + shellEscape(tmpfile);
|
||||
debugLog(cmd);
|
||||
return r2.syscmds(cmd);
|
||||
} else {
|
||||
cmdParts.push(shellEscape(url));
|
||||
const cmd = cmdParts.join(" ");
|
||||
debugLog(cmd);
|
||||
return r2.syscmds(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
function parseNdjson(output: string): HttpResponse | null {
|
||||
const lines = output.split("\n").filter((l) => l.trim() !== "");
|
||||
if (lines.length <= 1) return null;
|
||||
|
||||
let content = "";
|
||||
let lastObj: Record<string, unknown> | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.error) return obj as HttpResponse;
|
||||
if (obj.message?.content !== undefined) {
|
||||
content += obj.message.content;
|
||||
}
|
||||
lastObj = obj;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastObj) {
|
||||
lastObj.message = { role: "assistant", content };
|
||||
return lastObj as HttpResponse;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function httpRequest(options: HttpRequestOptions): HttpResponse {
|
||||
try {
|
||||
const output = executeRequest(options).trim();
|
||||
if (output === "") {
|
||||
return { error: "empty response" };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(output) as HttpResponse;
|
||||
} catch {
|
||||
const ndjson = parseNdjson(output);
|
||||
if (ndjson) return ndjson;
|
||||
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 });
|
||||
}
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
import {
|
||||
AnthropicPayload,
|
||||
ApiKeyResult,
|
||||
ApiStyle,
|
||||
AuthStyle,
|
||||
GeminiPayload,
|
||||
HttpResponse,
|
||||
ModelDataEntry,
|
||||
OllamaModelEntry,
|
||||
OllamaPayload,
|
||||
ProviderConfig,
|
||||
ProviderRegistry,
|
||||
} from "./types";
|
||||
import { state } from "./state";
|
||||
import { filterResponse, joinUrl, padRight } from "./utils";
|
||||
import { getApiKey } from "./apiKeys";
|
||||
import { getConfiguredHeaders, mergeHeaders } from "./headers";
|
||||
import { httpGet, httpPost } from "./http";
|
||||
|
||||
interface ProviderRuntime {
|
||||
buildPayload: (model: string, query: string) => object;
|
||||
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;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
return error.message || JSON.stringify(error);
|
||||
}
|
||||
|
||||
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 getProviderRequest(provider: ProviderConfig): {
|
||||
baseUrl: string;
|
||||
headers: string[];
|
||||
} {
|
||||
const apiKey = readProviderKey(provider)?.[0] || null;
|
||||
return {
|
||||
baseUrl: getProviderBaseUrl(provider),
|
||||
headers: getProviderHeaders(buildAuthHeaders(provider, apiKey)),
|
||||
};
|
||||
}
|
||||
|
||||
function listV1Models(provider: ProviderConfig): string {
|
||||
const { baseUrl, headers } = getProviderRequest(provider);
|
||||
const url = joinUrl(baseUrl, "v1/models");
|
||||
return listDataModels(url, headers, (model) => model.id);
|
||||
}
|
||||
|
||||
function listOllamaModels(provider: ProviderConfig): string {
|
||||
const { baseUrl, headers } = getProviderRequest(provider);
|
||||
const response = httpGet(joinUrl(baseUrl, "api/tags"), headers);
|
||||
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 { baseUrl, headers } = getProviderRequest(provider);
|
||||
const response = httpGet(joinUrl(baseUrl, "v1/models"), headers);
|
||||
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) {
|
||||
throw new Error(getErrorMessage(response.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) => joinUrl(baseUrl, "v1/chat/completions"),
|
||||
},
|
||||
anthropic: {
|
||||
buildPayload: (model, query) => {
|
||||
const payload: AnthropicPayload = {
|
||||
model,
|
||||
max_tokens: 5128,
|
||||
messages: [{ role: "user", content: query }],
|
||||
};
|
||||
if (isThinkEnabled()) {
|
||||
payload.thinking = { type: "enabled", budget_tokens: 4096 };
|
||||
payload.max_tokens = 16000;
|
||||
}
|
||||
if (state.deterministic && !isThinkEnabled()) {
|
||||
Object.assign(payload, { temperature: 0, top_p: 0, top_k: 1 });
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
parseResponse: (response) => {
|
||||
if (response.content && Array.isArray(response.content)) {
|
||||
const parts: string[] = [];
|
||||
for (const block of response.content as Array<Record<string, unknown>>) {
|
||||
if (block.text) {
|
||||
parts.push(block.text as string);
|
||||
}
|
||||
}
|
||||
if (parts.length > 0) {
|
||||
return filterResponse(parts.join("\n"));
|
||||
}
|
||||
}
|
||||
if (response.error) {
|
||||
throw new Error(getErrorMessage(response.error) || "Unknown error");
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl) => joinUrl(baseUrl, "v1/messages"),
|
||||
},
|
||||
ollama: {
|
||||
buildPayload: (model, query) => {
|
||||
const payload: OllamaPayload = {
|
||||
stream: false,
|
||||
model,
|
||||
};
|
||||
if (state.apitype === "generate") {
|
||||
payload.prompt = query;
|
||||
} else {
|
||||
payload.messages = [{ role: "user", content: query }];
|
||||
}
|
||||
if (isThinkEnabled()) {
|
||||
if (state.think === "true" || state.think === "1") {
|
||||
payload.think = true;
|
||||
} else {
|
||||
// Pass reasoning levels like "low", "medium", "high" as-is
|
||||
payload.think = state.think;
|
||||
}
|
||||
} else {
|
||||
// Disable thinking by default — models like glm-5 enable it
|
||||
// implicitly, generating thousands of reasoning tokens
|
||||
payload.think = false;
|
||||
}
|
||||
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) {
|
||||
throw new Error(getErrorMessage(response.error));
|
||||
}
|
||||
if (typeof response.response === "string") {
|
||||
return filterResponse(response.response);
|
||||
}
|
||||
if (response.message?.content) {
|
||||
return filterResponse(response.message.content);
|
||||
}
|
||||
throw new Error(JSON.stringify(response));
|
||||
},
|
||||
buildUrl: (baseUrl) => joinUrl(baseUrl, "api/" + state.apitype),
|
||||
},
|
||||
gemini: {
|
||||
buildPayload: (_model, query) => {
|
||||
const payload: GeminiPayload = {
|
||||
contents: [{ parts: [{ text: query }] }],
|
||||
};
|
||||
const genConfig: Record<string, unknown> = {};
|
||||
if (state.deterministic) {
|
||||
Object.assign(genConfig, { temperature: 0.0, topP: 1.0, topK: 1 });
|
||||
}
|
||||
if (state.think !== "") {
|
||||
if (isThinkDisabled()) {
|
||||
genConfig.thinkingConfig = { thinkingBudget: 0 };
|
||||
} else {
|
||||
genConfig.thinkingConfig = { thinkingBudget: 8192 };
|
||||
}
|
||||
}
|
||||
if (Object.keys(genConfig).length > 0) {
|
||||
payload.generationConfig = genConfig;
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
parseResponse: (response) => {
|
||||
if (response.candidates && response.candidates[0]?.content?.parts) {
|
||||
const parts = response.candidates[0].content.parts as Array<Record<string, unknown>>;
|
||||
const textParts = parts
|
||||
.filter((p) => !p.thought && p.text)
|
||||
.map((p) => p.text as string);
|
||||
if (textParts.length > 0) {
|
||||
return filterResponse(textParts.join("\n"));
|
||||
}
|
||||
}
|
||||
if (response.error) {
|
||||
throw new Error(getErrorMessage(response.error));
|
||||
}
|
||||
console.log(JSON.stringify(response));
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl, model, apiKey) =>
|
||||
joinUrl(
|
||||
baseUrl,
|
||||
`v1beta/models/${model}:generateContent?key=${apiKey}`,
|
||||
),
|
||||
requiresUrlApiKey: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const providerRegistry: ProviderRegistry = {
|
||||
anthropic: {
|
||||
defaultModel: "claude-sonnet-5-5",
|
||||
defaultBaseUrl: "https://api.anthropic.com",
|
||||
authKey: "ANTHROPIC_API_KEY",
|
||||
keyName: "anthropic",
|
||||
authStyle: "anthropic",
|
||||
apiStyle: "anthropic",
|
||||
},
|
||||
claude: {
|
||||
defaultModel: "claude-sonnet-5-5",
|
||||
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",
|
||||
},
|
||||
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":
|
||||
case "anthropic":
|
||||
return listV1Models(provider);
|
||||
case "ollama":
|
||||
return listOllamaModels(provider);
|
||||
case "gemini":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isThinkDisabled(): boolean {
|
||||
return state.think === "false" || state.think === "0";
|
||||
}
|
||||
|
||||
function isThinkEnabled(): boolean {
|
||||
return state.think !== "" && !isThinkDisabled();
|
||||
}
|
||||
|
||||
function buildQuery(msg: string, hideprompt: boolean, apiStyle: ApiStyle): string {
|
||||
let query = msg;
|
||||
|
||||
// Only add prompt-based thinking hints for APIs without native think support
|
||||
if (state.think !== "" && apiStyle === "openai") {
|
||||
if (isThinkDisabled()) {
|
||||
query +=
|
||||
' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".';
|
||||
query += " /no_think";
|
||||
} else {
|
||||
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, provider.apiStyle);
|
||||
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 authHeaders = buildAuthHeaders(provider, apiKey?.[0] || null);
|
||||
if (provider.apiStyle === "ollama") {
|
||||
authHeaders.push("Accept: application/x-ndjson");
|
||||
}
|
||||
const headers = getProviderHeaders(authHeaders);
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { state } from "./state";
|
||||
import { fileDump, joinUrl, 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 baseUrl = state.baseurl || state.host + ":" + state.port;
|
||||
const url = joinUrl(baseUrl, "cmd/" + 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);
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
export type R2Address = number | string | unknown;
|
||||
|
||||
export interface R2HttpResponse {
|
||||
status: number;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export type R2HttpRequest = (
|
||||
method: "GET" | "POST",
|
||||
url: string,
|
||||
headers: string,
|
||||
data?: string,
|
||||
) => R2HttpResponse;
|
||||
|
||||
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 R2PipeAdapter {
|
||||
r2p: R2Pipe;
|
||||
http?: R2HttpRequest;
|
||||
constructor(r2p: R2Pipe) {
|
||||
this.r2p = r2p;
|
||||
this.http = r2p.http?.bind(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 newR2PipeSync(r2p: R2Pipe): R2PipeSync {
|
||||
return new R2PipeAdapter(r2p) 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;
|
||||
http?: R2HttpRequest;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
http?: R2HttpRequest;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -0,0 +1,36 @@
|
||||
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,
|
||||
http: "auto",
|
||||
think: "",
|
||||
apitype: "chat",
|
||||
useFiles: false,
|
||||
contextFile: "",
|
||||
model: "",
|
||||
cache: false,
|
||||
maxInputTokens: -1,
|
||||
prompt: DEFAULT_PROMPT,
|
||||
lastOutput: "",
|
||||
};
|
||||
|
||||
export const state: AppState = {
|
||||
...defaultState,
|
||||
decopipe: { ...defaultState.decopipe },
|
||||
extraHeaders: [...defaultState.extraHeaders],
|
||||
};
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
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 type OllamaApiType = "chat" | "generate";
|
||||
|
||||
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;
|
||||
http: "auto" | "r2" | "curl";
|
||||
think: string;
|
||||
apitype: OllamaApiType;
|
||||
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 AnthropicPayload {
|
||||
model: string;
|
||||
max_tokens: number;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
thinking?: { type: string; budget_tokens?: number };
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
top_k?: number;
|
||||
}
|
||||
|
||||
export interface OllamaPayload {
|
||||
stream: boolean;
|
||||
model: string;
|
||||
messages?: Array<{ role: string; content: string }>;
|
||||
prompt?: string;
|
||||
think?: boolean | string;
|
||||
options?: {
|
||||
repeat_last_n: number;
|
||||
top_p: number;
|
||||
top_k: number;
|
||||
temperature: number;
|
||||
repeat_penalty: number;
|
||||
seed: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GeminiPayload {
|
||||
contents: Array<{ parts: Array<{ text: string }> }>;
|
||||
generationConfig?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AutoReply {
|
||||
action: string;
|
||||
command?: string;
|
||||
description?: string;
|
||||
reason?: string;
|
||||
response?: string;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { state } from "./state";
|
||||
|
||||
export function tmpdir(path: string): string {
|
||||
const dir = r2.cmd("-e dir.tmp").trim() ?? ".";
|
||||
return dir + "/" + path;
|
||||
}
|
||||
|
||||
// r2's mkdir and touch commands do not expand '~'
|
||||
export function expandHome(path: string): string {
|
||||
if (!path.startsWith("~/")) {
|
||||
return path;
|
||||
}
|
||||
const home = r2.cmd("'%HOME").trim() || r2.cmd("'%USERPROFILE").trim();
|
||||
return home ? home + path.slice(1) : path;
|
||||
}
|
||||
|
||||
export function ensurePath(dir: string, file: string): void {
|
||||
r2.cmd("'mkdir -p " + expandHome(dir));
|
||||
r2.cmd("'touch " + expandHome(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 joinUrl(baseUrl: string, path: string): string {
|
||||
return baseUrl.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
|
||||
}
|
||||
|
||||
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 openBrace = result.indexOf("{");
|
||||
if (openBrace !== -1) result = result.slice(openBrace);
|
||||
const closeBrace = result.lastIndexOf("}");
|
||||
if (closeBrace !== -1) result = result.slice(0, closeBrace + 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;
|
||||
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;
|
||||
}
|
||||
@@ -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 |
+28
-14
@@ -4,24 +4,37 @@ 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.
|
||||
Each prompt is stored in a `.r2ai.md` file with YAML-style frontmatter and a markdown body.
|
||||
|
||||
Lines starting with `#` are comments and ignored.
|
||||
Example:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: 'Explain the current function'
|
||||
author: pancake
|
||||
command: r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Explain the purpose of this function in one or two short sentences.
|
||||
```
|
||||
|
||||
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
|
||||
- `title`: The title of the prompt
|
||||
- `author`: The author of the prompt
|
||||
- `description`: A brief description
|
||||
- `command`: Commands to execute before sending the prompt, separated by `;` (required)
|
||||
- `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
|
||||
- `model`: Model override for this prompt
|
||||
- `provider`: Provider override for this prompt
|
||||
|
||||
Legacy `.r2ai.txt` files using `Title:`, `Command:`, `Prompt:` or `Query:` are still accepted.
|
||||
|
||||
## Variable Substitution
|
||||
|
||||
In the `Prompt` field, you can use:
|
||||
In the markdown body, you can use:
|
||||
|
||||
- `${VAR}`: Replaced with the value of the environment variable `VAR`
|
||||
- `$(cmd)`: Replaced with the output of the r2 command `cmd`
|
||||
@@ -29,15 +42,16 @@ In the `Prompt` field, you can use:
|
||||
## Usage
|
||||
|
||||
- `r2ai -q`: List all available prompts
|
||||
- `r2ai -qj`: List all available prompts in JSON
|
||||
- `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
|
||||
- `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
|
||||
- More advanced conditionals
|
||||
@@ -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.
|
||||
@@ -0,0 +1,40 @@
|
||||
# r2plugin
|
||||
|
||||
This directory contains the make and meson files required to link r2ai
|
||||
inside radare2 through the external plugin system (XPS).
|
||||
|
||||
```console
|
||||
cd radare2
|
||||
make -C libr/xps EXTERNAL_PLUGINS=r2ai
|
||||
./configure-plugins
|
||||
make
|
||||
```
|
||||
|
||||
For meson builds, place this repository at `libr/xps/p/r2ai` and make
|
||||
`libr/xps/p/meson.build` include:
|
||||
|
||||
```meson
|
||||
subdir('r2ai/r2plugin')
|
||||
```
|
||||
|
||||
## WASI HTTP build
|
||||
|
||||
Use a radare2 checkout with the host HTTP bridge and place this repository at
|
||||
`libr/xps/p/r2ai`. From that radare2 checkout:
|
||||
|
||||
```sh
|
||||
echo 'EXTERNAL_PLUGINS+=r2ai' >> libr/xps/config.mk
|
||||
make -C libr/xps
|
||||
WASI_HTTP=1 sys/wasi-api.sh
|
||||
```
|
||||
|
||||
This links the C plugin into the reactor; it does not load a native shared
|
||||
library at runtime. Enable HTTP in the host adapter. The `auto` backend selects
|
||||
`r_socket_http_get/post`, preserving provider authentication headers and JSON
|
||||
bodies, subject to the radare2 sandbox. `--disable-network` also denies these calls.
|
||||
|
||||
WASI excludes POSIX signal/alarm handling; the host controls request timeouts,
|
||||
not `r2ai.http.timeout`. Normal queries and model listing use buffered HTTP.
|
||||
Vertex's automatic `gcloud` authentication and background tasks requiring
|
||||
threads are unavailable. Explicit shell backends (`system`/`curl`) cannot work;
|
||||
use `auto` or `socket`. Browser CORS and restricted-header rules still apply.
|
||||
@@ -0,0 +1,9 @@
|
||||
EXTERNAL_PLUGINS+=r2ai
|
||||
# EXTERNAL_PLUGINS+=hi
|
||||
|
||||
.PHONY: r2ai
|
||||
|
||||
r2ai: p/r2ai
|
||||
|
||||
p/r2ai:
|
||||
cd p && git clone https://github.com/radareorg/r2ai
|
||||
@@ -0,0 +1,3 @@
|
||||
#ifdef R2_CORE_H
|
||||
extern RCorePlugin r_core_plugin_r2ai;
|
||||
#endif
|
||||
@@ -0,0 +1,23 @@
|
||||
R2AI_CORE_WD=$(LIBR)/xps/p/r2ai
|
||||
CFLAGS+=-I$(R2AI_CORE_WD)/src
|
||||
R2AI_CORE_OBJ= \
|
||||
$(R2AI_CORE_WD)/src/anthropic.o \
|
||||
$(R2AI_CORE_WD)/src/apikeys.o \
|
||||
$(R2AI_CORE_WD)/src/async.o \
|
||||
$(R2AI_CORE_WD)/src/auto.o \
|
||||
$(R2AI_CORE_WD)/src/claw.o \
|
||||
$(R2AI_CORE_WD)/src/gemini.o \
|
||||
$(R2AI_CORE_WD)/src/http.o \
|
||||
$(R2AI_CORE_WD)/src/json.o \
|
||||
$(R2AI_CORE_WD)/src/llm.o \
|
||||
$(R2AI_CORE_WD)/src/markdown.o \
|
||||
$(R2AI_CORE_WD)/src/messages.o \
|
||||
$(R2AI_CORE_WD)/src/openai.o \
|
||||
$(R2AI_CORE_WD)/src/query.o \
|
||||
$(R2AI_CORE_WD)/src/r2ai.o \
|
||||
$(R2AI_CORE_WD)/src/rawtools.o \
|
||||
$(R2AI_CORE_WD)/src/tools.o \
|
||||
$(R2AI_CORE_WD)/src/vdb.o \
|
||||
$(R2AI_CORE_WD)/src/vertex.o \
|
||||
$(R2AI_CORE_WD)/src/wizard.o
|
||||
EXTERNAL_STATIC_OBJS+=$(R2AI_CORE_OBJ)
|
||||
@@ -0,0 +1,27 @@
|
||||
shared_library(
|
||||
'core_r2ai',
|
||||
'../src/anthropic.c',
|
||||
'../src/apikeys.c',
|
||||
'../src/async.c',
|
||||
'../src/auto.c',
|
||||
'../src/claw.c',
|
||||
'../src/gemini.c',
|
||||
'../src/http.c',
|
||||
'../src/json.c',
|
||||
'../src/llm.c',
|
||||
'../src/markdown.c',
|
||||
'../src/messages.c',
|
||||
'../src/openai.c',
|
||||
'../src/query.c',
|
||||
'../src/r2ai.c',
|
||||
'../src/rawtools.c',
|
||||
'../src/tools.c',
|
||||
'../src/vdb.c',
|
||||
'../src/vertex.c',
|
||||
'../src/wizard.c',
|
||||
include_directories: ['../src', include_dirs],
|
||||
dependencies: r2_core,
|
||||
override_options: ['werror=false'],
|
||||
install: true,
|
||||
install_dir: r2_plugdir,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
include p/r2ai/r2plugin/core/deps.mk
|
||||
@@ -0,0 +1,30 @@
|
||||
# r2ai XPS integration for radare2.
|
||||
# Included from libr/xps/meson.build when r2ai sources live under
|
||||
# libr/xps/p/r2ai/. Appends sources and include paths to the r_core
|
||||
# aggregate so the static plugin is linked into libr_core.
|
||||
|
||||
core_plugins += ['r2ai']
|
||||
|
||||
r_core_additional_inc += include_directories(join_paths('..', 'src'))
|
||||
|
||||
r_core_additional_sources += files(
|
||||
'../src/anthropic.c',
|
||||
'../src/apikeys.c',
|
||||
'../src/async.c',
|
||||
'../src/auto.c',
|
||||
'../src/claw.c',
|
||||
'../src/gemini.c',
|
||||
'../src/http.c',
|
||||
'../src/json.c',
|
||||
'../src/llm.c',
|
||||
'../src/markdown.c',
|
||||
'../src/messages.c',
|
||||
'../src/openai.c',
|
||||
'../src/query.c',
|
||||
'../src/r2ai.c',
|
||||
'../src/rawtools.c',
|
||||
'../src/tools.c',
|
||||
'../src/vdb.c',
|
||||
'../src/vertex.c',
|
||||
'../src/wizard.c',
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
#ifdef R2_CORE_H
|
||||
#include "core/deps.h"
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
core.r2ai
|
||||
+2
-1
@@ -5,8 +5,9 @@
|
||||
- All the source code of the plugin is in the current directory
|
||||
- Radare2 headers use to be in /usr/local/include/libr
|
||||
|
||||
## Formatting Style
|
||||
## 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
|
||||
|
||||
+58
-3
@@ -5,7 +5,13 @@ DOTLIB=.$(shell r2 -NNH R2_LIBEXT)
|
||||
R2SYSPLUGDIR=$(shell r2 -NNH R2_LIBR_PLUGINS)
|
||||
R2USRPLUGDIR=$(shell r2 -NNH R2_USER_PLUGINS)
|
||||
R2BINDIR=$(shell r2pm -H R2PM_BINDIR)
|
||||
R2INCDIR=$(shell r2 -NNH R2_INCDIR)/libr
|
||||
R2LIBDIR=$(shell r2 -NNH R2_LIBDIR)
|
||||
R2MANDIR=$(shell r2pm -H R2PM_MANDIR)
|
||||
R2AI_PROMPTSDIR=$(HOME)/.config/r2ai/prompts
|
||||
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)
|
||||
@@ -20,6 +26,8 @@ CFLAGS+=-I. -fPIC
|
||||
CFLAGS+=-Wall
|
||||
CFLAGS+=-g
|
||||
|
||||
AR?=ar
|
||||
|
||||
USE_R2_CURL=1
|
||||
USE_LIBCURL=0
|
||||
|
||||
@@ -39,22 +47,61 @@ ifeq ($(USE_LIBCURL),1)
|
||||
endif
|
||||
|
||||
OBJS=r2ai.o
|
||||
OBJS+=apikeys.o
|
||||
OBJS+=async.o
|
||||
OBJS+=claw.o
|
||||
OBJS+=llm.o
|
||||
OBJS+=auto.o
|
||||
OBJS+=json.o
|
||||
OBJS+=vdb.o
|
||||
OBJS+=tools.o
|
||||
OBJS+=messages.o
|
||||
OBJS+=anthropic.o
|
||||
OBJS+=gemini.o
|
||||
OBJS+=vertex.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)
|
||||
$(AR) rcs libr2ai.a $(OBJS)
|
||||
$(CC) -pie -o r2ai$(DOTEXE) $(OBJS) $(CFLAGS) $(LDFLAGS) -lm main.c
|
||||
|
||||
# Binary package for r2pm -bi: plugins/, bin/, man/ and prompts/ for this r2 version and platform
|
||||
# r2pm -H R2PM_* needs a recent r2pm; fall back to uname when missing
|
||||
R2PM_OS_SH=$(shell r2pm -H R2PM_OS 2>/dev/null)
|
||||
R2PM_ARCH_SH=$(shell r2pm -H R2PM_ARCH 2>/dev/null)
|
||||
R2PM_BITS_SH=$(shell r2pm -H R2PM_BITS 2>/dev/null)
|
||||
ifeq ($(R2PM_OS_SH),)
|
||||
ifeq ($(shell uname -s),Darwin)
|
||||
R2PM_OS_SH=darwin
|
||||
else
|
||||
R2PM_OS_SH=linux
|
||||
endif
|
||||
endif
|
||||
ifeq ($(R2PM_ARCH_SH),)
|
||||
ifeq ($(shell uname -m),x86_64)
|
||||
R2PM_ARCH_SH=x86
|
||||
else ifneq (,$(filter $(shell uname -m),aarch64 arm64))
|
||||
R2PM_ARCH_SH=arm
|
||||
else
|
||||
R2PM_ARCH_SH=$(shell uname -m)
|
||||
endif
|
||||
endif
|
||||
ifeq ($(R2PM_BITS_SH),)
|
||||
R2PM_BITS_SH=$(shell getconf LONG_BIT 2>/dev/null || echo 64)
|
||||
endif
|
||||
BINDIST_ZIP=r2ai-$(shell r2 -qv)-$(R2PM_OS_SH)-$(R2PM_ARCH_SH)-$(R2PM_BITS_SH).zip
|
||||
|
||||
bindist: all
|
||||
rm -rf build/bindist ../$(BINDIST_ZIP)
|
||||
$(MAKE) user-install R2USRPLUGDIR=$(CURDIR)/build/bindist/plugins R2BINDIR=$(CURDIR)/build/bindist/bin R2MANDIR=$(CURDIR)/build/bindist/man R2AI_PROMPTSDIR=$(CURDIR)/build/bindist/prompts
|
||||
cd build/bindist && zip -q -r $(CURDIR)/../$(BINDIST_ZIP) plugins bin man prompts
|
||||
|
||||
try: all
|
||||
$(MAKE) user-install
|
||||
$(MAKE) doc-install
|
||||
@@ -72,12 +119,12 @@ user-install: user-uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
mkdir -p $(R2BINDIR)
|
||||
mkdir -p $(R2MANDIR)/man1
|
||||
mkdir -p $(HOME)/.config/r2ai/prompts
|
||||
mkdir -p $(R2AI_PROMPTSDIR)
|
||||
$(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/
|
||||
cp -r ../prompts/* $(R2AI_PROMPTSDIR)/
|
||||
|
||||
user-uninstall:
|
||||
rm -f $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
@@ -86,16 +133,24 @@ user-uninstall:
|
||||
|
||||
install:
|
||||
$(MAKE) uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
mkdir -p $(R2SYSPLUGDIR)
|
||||
mkdir -p $(R2BINDIR)
|
||||
mkdir -p $(R2LIBDIR)
|
||||
mkdir -p $(R2INCDIR)
|
||||
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
cp -f libr2ai.a $(R2LIBDIR)/libr2ai.a
|
||||
cp -f r2ai.h $(R2INCDIR)/r2ai.h
|
||||
|
||||
uninstall:
|
||||
rm -f $(R2SYSPLUGDIR)/r2ai$(DOTLIB)
|
||||
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
rm -f $(R2LIBDIR)/libr2ai.a
|
||||
rm -f $(R2INCDIR)/r2ai.h
|
||||
|
||||
r2check:
|
||||
@r2 -NN -qcq --
|
||||
clean:
|
||||
rm -f *.o *.d
|
||||
|
||||
.PHONY: all bindist try doc-install fmt user-install user-uninstall install uninstall r2check clean test
|
||||
+17
-6
@@ -33,6 +33,7 @@ make user-install
|
||||
Usage: r2ai [-args] [...]
|
||||
| r2ai -d Decompile current function
|
||||
| r2ai -d [query] Ask a question on the current function
|
||||
| r2ai -do Decompile current function with source offsets
|
||||
| r2ai -dr Decompile current function (+ 1 level of recursivity)
|
||||
| r2ai -a [query] Resolve question using auto mode
|
||||
| r2ai -e (k(=v)) Same as '-e r2ai.'
|
||||
@@ -41,9 +42,11 @@ Usage: r2ai [-args] [...]
|
||||
| r2ai -m show selected model, list suggested ones, choose one
|
||||
| r2ai -p [provider] set LLM provider (openai, anthropic, gemini, etc.)
|
||||
| r2ai -q list available query prompts
|
||||
| r2ai -qj list available query prompts in JSON
|
||||
| r2ai -q [name] (inst) run predefined prompt with optional instructions
|
||||
| r2ai -r enter the chat repl
|
||||
| r2ai -L show chat logs (See -Lj for json). Only for auto mode.
|
||||
| r2ai -w launch the interactive setup wizard
|
||||
| r2ai -L show chat logs (See -Lj for json)
|
||||
| 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)
|
||||
@@ -53,7 +56,7 @@ Usage: r2ai [-args] [...]
|
||||
|
||||
## 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.
|
||||
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.md` files with YAML-style frontmatter. Legacy `.r2ai.txt` prompts are still supported.
|
||||
|
||||
### Available Prompts
|
||||
|
||||
@@ -70,6 +73,7 @@ Query prompts are predefined templates that combine radare2 commands with struct
|
||||
### Usage
|
||||
|
||||
- `r2ai -q`: List all available query prompts
|
||||
- `r2ai -qj`: List all available query prompts in JSON
|
||||
- `r2ai -q [name]`: Run the specified prompt on the current function
|
||||
- `r2ai -q [name] [instructions]`: Run the prompt with additional custom instructions
|
||||
|
||||
@@ -90,17 +94,24 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
|------------------|------------------------------------------------------------------------------------------------|
|
||||
| r2ai.api | Name of the provider e.g `openai`. List possibilities with `r2ai -e r2ai.api=?` |
|
||||
| r2ai.model | Model name. List possibilities with `r2ai -e r2ai.model=?` |
|
||||
| r2ai.baseurl | Remote LLM base URL. Specify host if necessary e.g http://127.0.0.1:11434. |
|
||||
| r2ai.baseurl | Provider API root. Use `/v1` for OpenAI-compatible chat or `/api` for native Ollama. |
|
||||
| r2ai.apitype | Use chat completions (default) or the native Ollama `generate` operation. |
|
||||
| r2ai.cacheck | Warn when chat requests are not append-only and may miss provider prompt-cache hits. |
|
||||
| r2ai.max_tokens | Maximum output tokens or maximum total tokens. Check the appropriate limits for your model |
|
||||
| r2ai.temperature | How creative the model should be. 0=not creative, 1=very creative |
|
||||
| r2ai.cmds | R2 command to issue and send output in context to model |
|
||||
| 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.markdown | Render LLM responses with the radare2 markdown colorizer (default: true) |
|
||||
| r2ai.wizard | Auto-start the setup wizard on the first interactive chat session |
|
||||
| r2ai.history | Keep direct queries (`r2ai [query]`, `r2ai -d`, `r2ai -i`) in the conversation context instead of sending each one alone. Inspect it with `r2ai -L`, reset it with `r2ai -R` (default: false) |
|
||||
| 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.|
|
||||
| r2ai.http.headers | Extra HTTP headers to inject in every provider request. One per line, '\n' separated, e.g. `Authorization: Bearer x\nUser-Agent: r2ai/1.0`. Defaults to `R2AI_HEADERS`. |
|
||||
|
||||
|
||||
## Example
|
||||
@@ -110,11 +121,11 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
```
|
||||
$ r2 ./substitute
|
||||
[0x000010a0]> r2ai -e api=anthropic
|
||||
[0x000010a0]> r2ai -e model=claude-3-7-sonnet-20250219
|
||||
[0x000010a0]> r2ai -e model=claude-sonnet-5-5
|
||||
[0x000010a0]> aa
|
||||
```
|
||||
|
||||
- Decompile the main function: `r2ai -d`
|
||||
- Decompile the main function: `r2ai -d` or `r2ai -do` to keep source offsets in the left column
|
||||
|
||||
```
|
||||
[0x000010a0]> s main
|
||||
|
||||
+119
-321
@@ -1,334 +1,132 @@
|
||||
#include "r2ai.h"
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
static const char *modelname(const char *model_name) {
|
||||
return model_name? model_name: "claude-3-7-sonnet-20250219";
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
// request body, vertex takes the api version in the body and the model in the url
|
||||
R_IPI char *r2ai_anthropic_request(const R2AIArgs *args, bool vertex) {
|
||||
char *messages = r_list_empty (args->messages)? NULL: r2ai_msgs_to_anthropic_json (args->messages);
|
||||
if (!messages) {
|
||||
return NULL;
|
||||
}
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
if (vertex) {
|
||||
pj_ks (pj, "anthropic_version", "vertex-2023-10-16");
|
||||
pj_kb (pj, "stream", false);
|
||||
} else {
|
||||
pj_ks (pj, "model", R_STR_ISNOTEMPTY (args->model)? args->model: "claude-sonnet-5-5");
|
||||
}
|
||||
pj_kn (pj, "max_tokens", args->max_tokens? args->max_tokens: 4096);
|
||||
if (args->thinking_tokens >= 1024) {
|
||||
pj_ko (pj, "thinking");
|
||||
pj_ks (pj, "type", "enabled");
|
||||
pj_kn (pj, "budget_tokens", args->thinking_tokens);
|
||||
pj_end (pj);
|
||||
} else if (args->deterministic) {
|
||||
// top_p is not sent because newer models reject it along with temperature
|
||||
pj_kn (pj, "temperature", 0);
|
||||
pj_kn (pj, "top_k", 1);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (args->system_prompt)) {
|
||||
pj_ks (pj, "system", args->system_prompt);
|
||||
}
|
||||
pj_k (pj, "messages");
|
||||
pj_raw (pj, messages);
|
||||
free (messages);
|
||||
char *tools = r_list_empty (args->tools)? NULL: r2ai_tools_to_anthropic_json (args->tools);
|
||||
if (tools) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, tools);
|
||||
free (tools);
|
||||
}
|
||||
pj_end (pj);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
// Setup HTTP headers
|
||||
char *auth_header = r_str_newf ("x-api-key: %s", args.api_key);
|
||||
char *anthropic_version = "anthropic-version: 2023-06-01";
|
||||
const char *headers[] = {
|
||||
"Content-Type: application/json",
|
||||
auth_header,
|
||||
anthropic_version,
|
||||
NULL
|
||||
};
|
||||
|
||||
const char *anthropic_url = "https://api.anthropic.com/v1/messages";
|
||||
|
||||
// Get system message if available
|
||||
const char *system_message = NULL;
|
||||
|
||||
// First check if it's provided in args.system_prompt
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
system_message = args.system_prompt;
|
||||
} else {
|
||||
// If no system_prompt in args, check config as fallback
|
||||
system_message = r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
// Create messages JSON
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (messages_input && r_list_length (messages_input->messages) > 0) {
|
||||
// Convert directly to JSON without filtering
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to convert messages to JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (error) {
|
||||
*error = strdup ("No input or messages provided");
|
||||
}
|
||||
free (auth_header);
|
||||
char *data = r2ai_anthropic_request (&args, false);
|
||||
if (!data) {
|
||||
*args.error = strdup ("No input or messages provided");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert tools to Anthropic format if available
|
||||
char *anthropic_tools_json = NULL;
|
||||
if (tools && tools->n_tools > 0) {
|
||||
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
|
||||
}
|
||||
|
||||
// Create the request JSON
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", modelname (model));
|
||||
pj_kn (pj, "max_tokens", args.max_tokens);
|
||||
if (args.thinking_tokens >= 1024) {
|
||||
pj_ko (pj, "thinking");
|
||||
pj_ks (pj, "type", "enabled");
|
||||
pj_kn (pj, "budget_tokens", args.thinking_tokens);
|
||||
pj_end (pj);
|
||||
}
|
||||
// Add system message if available
|
||||
if (system_message) {
|
||||
pj_ks (pj, "system", system_message);
|
||||
}
|
||||
|
||||
// Add messages
|
||||
pj_k (pj, "messages");
|
||||
// messages_json already contains the array itself, so we use raw
|
||||
pj_raw (pj, messages_json);
|
||||
|
||||
// Add tools if available
|
||||
if (anthropic_tools_json) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, anthropic_tools_json);
|
||||
free (anthropic_tools_json);
|
||||
}
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
char *data = pj_drain (pj);
|
||||
free (messages_json);
|
||||
|
||||
// Save the full JSON for debugging
|
||||
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);
|
||||
|
||||
// Make the API call
|
||||
char *auth = r_str_newf ("x-api-key: %s", args.api_key);
|
||||
const char *headers[] = { "Content-Type: application/json", auth, "anthropic-version: 2023-06-01", NULL };
|
||||
int code = 0;
|
||||
char *res = r2ai_http_post (core, anthropic_url, headers, data, &code, NULL);
|
||||
char *res = r2ai_post (cps->core, "anthropic", "https://api.anthropic.com/v1/messages", headers, data, &code, args.error);
|
||||
free (data);
|
||||
free (auth_header);
|
||||
|
||||
if (!res || code != 200) {
|
||||
R_LOG_ERROR ("Anthropic API error %d", code);
|
||||
if (error && res) {
|
||||
*error = strdup (res);
|
||||
} else if (error) {
|
||||
*error = strdup ("Failed to get response from Anthropic API");
|
||||
}
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
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);
|
||||
|
||||
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;
|
||||
|
||||
char *response_copy = strdup (res);
|
||||
if (response_copy) {
|
||||
RJson *jres = r_json_parse (response_copy);
|
||||
if (jres) {
|
||||
// Create a new message structure
|
||||
message = R_NEW0 (R2AI_Message);
|
||||
|
||||
const RJson *usage_json = r_json_get (jres, "usage");
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
usage = R_NEW0 (R2AI_Usage);
|
||||
if (usage) {
|
||||
const RJson *prompt_tokens = r_json_get (usage_json, "input_tokens");
|
||||
const RJson *completion_tokens = r_json_get (usage_json, "output_tokens");
|
||||
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = prompt_tokens->num.u_value;
|
||||
}
|
||||
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = completion_tokens->num.u_value;
|
||||
}
|
||||
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
if (message) {
|
||||
message->role = strdup ("assistant");
|
||||
|
||||
RStrBuf *content_buf = r_strbuf_new ("");
|
||||
|
||||
// Check for tool calls in Anthropic response
|
||||
int has_tool_use = 0;
|
||||
int n_tool_calls = 0;
|
||||
|
||||
// Count tool_use blocks to determine array size
|
||||
const RJson *content_array = r_json_get (jres, "content");
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
// First count the number of tool_use blocks
|
||||
const RJson *content_item = content_array->children.first;
|
||||
while (content_item) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING && !strcmp (type->str_value, "tool_use")) {
|
||||
has_tool_use = 1;
|
||||
n_tool_calls++;
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate tool_calls array if needed
|
||||
if (has_tool_use && n_tool_calls > 0) {
|
||||
message->tool_calls = R_NEWS0 (R2AI_ToolCall, n_tool_calls);
|
||||
if (!message->tool_calls) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for tool calls");
|
||||
}
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
message->n_tool_calls = n_tool_calls;
|
||||
}
|
||||
|
||||
// Process each content item
|
||||
int tool_idx = 0;
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
if (!cb) {
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
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) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING) {
|
||||
R2AI_ContentBlock *block = &cb->blocks[block_idx];
|
||||
if (!strcmp (type->str_value, "text")) {
|
||||
// Text content
|
||||
const RJson *text = r_json_get (content_item, "text");
|
||||
if (text && text->type == R_JSON_STRING) {
|
||||
r_strbuf_append (content_buf, text->str_value);
|
||||
block->type = strdup ("text");
|
||||
block->text = strdup (text->str_value);
|
||||
}
|
||||
} else if (!strcmp (type->str_value, "tool_use") && tool_idx < n_tool_calls) {
|
||||
// Tool call - convert from Anthropic format to OpenAI format
|
||||
const RJson *name = r_json_get (content_item, "name");
|
||||
const RJson *id = r_json_get (content_item, "id");
|
||||
const RJson *input = r_json_get (content_item, "input");
|
||||
|
||||
block->type = strdup ("tool_use");
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
tool_idx++;
|
||||
} else if (!strcmp (type->str_value, "thinking")) {
|
||||
const RJson *data = r_json_get (content_item, "data");
|
||||
const RJson *thinking = r_json_get (content_item, "thinking");
|
||||
const RJson *signature = r_json_get (content_item, "signature");
|
||||
if (data && data->type == R_JSON_STRING) {
|
||||
block->data = strdup (data->str_value);
|
||||
}
|
||||
if (thinking && thinking->type == R_JSON_STRING) {
|
||||
block->thinking = strdup (thinking->str_value);
|
||||
}
|
||||
if (signature && signature->type == R_JSON_STRING) {
|
||||
block->signature = strdup (signature->str_value);
|
||||
}
|
||||
block->type = strdup ("thinking");
|
||||
|
||||
r_strbuf_append (content_buf, "\n\x1b[90m<thinking>\n");
|
||||
r_strbuf_append (content_buf, block->thinking);
|
||||
r_strbuf_append (content_buf, "\n</thinking>\x1b[0m\n");
|
||||
}
|
||||
block_idx++;
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
message->content_blocks = cb;
|
||||
}
|
||||
|
||||
// Store the content
|
||||
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) {
|
||||
r2ai_message_free (message);
|
||||
message = NULL;
|
||||
}
|
||||
}
|
||||
r_json_free (jres);
|
||||
}
|
||||
free (response_copy);
|
||||
}
|
||||
|
||||
// Assign message and usage to result
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
|
||||
// Free the HTTP response body
|
||||
free (auth);
|
||||
R2AI_ChatResponse *result = res? r2ai_anthropic_parse_response (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
R_IPI char *r2ai_anthropic_stream(RCore *core, R2AIArgs args) {
|
||||
(void)core;
|
||||
(void)args;
|
||||
// Not implemented yet
|
||||
return NULL;
|
||||
static char *jstr(const RJson *j, const char *key) {
|
||||
return r_str_new (r_json_get_str (j, key));
|
||||
}
|
||||
|
||||
// fill the block from the content item and append its text to the message content
|
||||
static void parse_block(R2AI_ContentBlock *b, const RJson *item, R2AI_Message *msg, RStrBuf *sb) {
|
||||
if (!strcmp (b->type, "text")) {
|
||||
b->text = jstr (item, "text");
|
||||
r_strbuf_append (sb, r_str_get (b->text));
|
||||
} else if (!strcmp (b->type, "tool_use")) {
|
||||
const RJson *input = r_json_get (item, "input");
|
||||
b->id = jstr (item, "id");
|
||||
b->name = jstr (item, "name");
|
||||
b->input = (input && input->type == R_JSON_OBJECT)? r_json_to_string (input): NULL;
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
tc->id = r_str_new (b->id);
|
||||
tc->name = r_str_new (b->name);
|
||||
tc->arguments = r_str_new (b->input);
|
||||
if (!msg->tool_calls) {
|
||||
msg->tool_calls = r_list_newf ((RListFree)r2ai_tool_call_free);
|
||||
}
|
||||
r_list_append (msg->tool_calls, tc);
|
||||
} else if (!strcmp (b->type, "thinking")) {
|
||||
b->data = jstr (item, "data");
|
||||
b->thinking = jstr (item, "thinking");
|
||||
b->signature = jstr (item, "signature");
|
||||
r_strbuf_appendf (sb, "\n" Color_GRAY "<thinking>\n%s\n</thinking>" Color_RESET "\n", r_str_get (b->thinking));
|
||||
}
|
||||
}
|
||||
|
||||
// parses (and modifies) the json response of the anthropic and vertex-anthropic providers
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(char *json, char **error) {
|
||||
RJson *j = r_json_parse (json);
|
||||
if (!j) {
|
||||
*error = strdup ("Failed to parse Anthropic response JSON");
|
||||
return NULL;
|
||||
}
|
||||
R2AI_ChatResponse *res = R_NEW0 (R2AI_ChatResponse);
|
||||
const RJson *u = r_json_get (j, "usage");
|
||||
if (u) {
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
usage->prompt_tokens = r_json_get_num (u, "input_tokens");
|
||||
usage->completion_tokens = r_json_get_num (u, "output_tokens");
|
||||
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
|
||||
res->usage = usage;
|
||||
}
|
||||
R2AI_Message *msg = R_NEW0 (R2AI_Message);
|
||||
msg->role = strdup ("assistant");
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
const RJson *content = r_json_get (j, "content");
|
||||
if (content && content->type == R_JSON_ARRAY) {
|
||||
msg->content_blocks = r2ai_content_blocks_new ();
|
||||
const RJson *item;
|
||||
for (item = content->children.first; item; item = item->next) {
|
||||
char *type = jstr (item, "type");
|
||||
if (!type) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ContentBlock *b = R_NEW0 (R2AI_ContentBlock);
|
||||
b->type = type;
|
||||
parse_block (b, item, msg, sb);
|
||||
r_list_append (msg->content_blocks, b);
|
||||
}
|
||||
}
|
||||
msg->content = r_strbuf_drain (sb);
|
||||
res->message = msg;
|
||||
r_json_free (j);
|
||||
return res;
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/* 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);
|
||||
}
|
||||
r2ai_cons_editor (core->cons, keys_path, NULL);
|
||||
free (keys_path);
|
||||
}
|
||||
|
||||
static char *r2ai_apikeys_env(const char *provider) {
|
||||
char *upper = strdup (provider);
|
||||
r_str_case (upper, true);
|
||||
char *name = r_str_newf ("%s_API_KEY", upper);
|
||||
char *val = r_sys_getenv (name);
|
||||
if (val) {
|
||||
r_str_trim (val);
|
||||
}
|
||||
free (name);
|
||||
free (upper);
|
||||
if (R_STR_ISNOTEMPTY (val)) {
|
||||
return val;
|
||||
}
|
||||
free (val);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key for a provider.
|
||||
* Resolution order:
|
||||
* 1. environment variable "<PROVIDER>_API_KEY" (provider upper-cased)
|
||||
* 2. the ~/.config/r2ai/apikeys.txt file
|
||||
* Provider matching against the file is case-insensitive. */
|
||||
R_API char *r2ai_apikeys_get(const char *provider) {
|
||||
if (!provider) {
|
||||
return NULL;
|
||||
}
|
||||
char *env_key = r2ai_apikeys_env (provider);
|
||||
if (env_key) {
|
||||
return env_key;
|
||||
}
|
||||
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;
|
||||
}
|
||||
+870
@@ -0,0 +1,870 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai.async"
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
#include <r_th.h>
|
||||
|
||||
static const char *state_name(R2AITaskState s) {
|
||||
switch (s) {
|
||||
case R2AI_TASK_PENDING: return "pending";
|
||||
case R2AI_TASK_RUNNING: return "running";
|
||||
case R2AI_TASK_WAIT_APPROVE: return "wait-approve";
|
||||
case R2AI_TASK_WAIT_INPUT: return "wait-input";
|
||||
case R2AI_TASK_COMPLETE: return "complete";
|
||||
case R2AI_TASK_ERROR: return "error";
|
||||
case R2AI_TASK_CANCELLED: return "cancelled";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static const char *kind_name(R2AITaskKind k) {
|
||||
return k == R2AI_TASK_AUTO? "auto": "query";
|
||||
}
|
||||
|
||||
static void task_lock(R2AITask *t) {
|
||||
r_th_lock_enter (t->lock);
|
||||
}
|
||||
static void task_unlock(R2AITask *t) {
|
||||
r_th_lock_leave (t->lock);
|
||||
}
|
||||
|
||||
static bool task_is_live_locked(const R2AITask *t) {
|
||||
R2AITaskState s = t->state;
|
||||
return s == R2AI_TASK_PENDING || s == R2AI_TASK_RUNNING || s == R2AI_TASK_WAIT_APPROVE || s == R2AI_TASK_WAIT_INPUT;
|
||||
}
|
||||
|
||||
static bool is_done(R2AITaskState st) {
|
||||
return st == R2AI_TASK_COMPLETE || st == R2AI_TASK_ERROR || st == R2AI_TASK_CANCELLED;
|
||||
}
|
||||
|
||||
static R2AITaskQueue *queue(RCorePluginSession *cps) {
|
||||
return ((R2AI_State *)cps->data)->async;
|
||||
}
|
||||
|
||||
static void print_block(RCons *cons, const char *hdr, const char *text) {
|
||||
if (R_STR_ISEMPTY (text)) {
|
||||
return;
|
||||
}
|
||||
r_cons_printf (cons, "%s%s", hdr, text);
|
||||
if (!r_str_endswith (text, "\n")) {
|
||||
r_cons_newline (cons);
|
||||
}
|
||||
}
|
||||
|
||||
/* Append text to task->output. Takes task lock. */
|
||||
static void task_append_output(R2AITask *t, const char *text) {
|
||||
if (R_STR_ISEMPTY (text)) {
|
||||
return;
|
||||
}
|
||||
task_lock (t);
|
||||
r_strbuf_append (t->output, text);
|
||||
task_unlock (t);
|
||||
}
|
||||
|
||||
static void task_append_outputf(R2AITask *t, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start (ap, fmt);
|
||||
char *s = r_str_newvf (fmt, ap);
|
||||
va_end (ap);
|
||||
task_append_output (t, s);
|
||||
free (s);
|
||||
}
|
||||
|
||||
static void pending_clear(R2AITask *t) {
|
||||
R_FREE (t->pending_tool_name);
|
||||
R_FREE (t->pending_tool_args);
|
||||
R_FREE (t->pending_tool_call_id);
|
||||
}
|
||||
|
||||
static void task_free(R2AITask *t) {
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
if (t->thread) {
|
||||
r_th_wait (t->thread);
|
||||
r_th_free (t->thread);
|
||||
}
|
||||
if (t->gate) {
|
||||
r_th_sem_free (t->gate);
|
||||
}
|
||||
if (t->lock) {
|
||||
r_th_lock_free (t->lock);
|
||||
}
|
||||
if (t->messages) {
|
||||
r2ai_msgs_free (t->messages);
|
||||
}
|
||||
r_list_free (t->tools);
|
||||
r_config_free (t->llm_core->config);
|
||||
free (t->llm_core);
|
||||
free (t->title);
|
||||
free (t->query);
|
||||
free (t->system_prompt);
|
||||
free (t->model);
|
||||
free (t->provider);
|
||||
r_strbuf_free (t->output);
|
||||
free (t->error);
|
||||
pending_clear (t);
|
||||
free (t->tool_result);
|
||||
free (t);
|
||||
}
|
||||
|
||||
static void queue_lock(R2AITaskQueue *q) {
|
||||
r_th_lock_enter (q->lock);
|
||||
}
|
||||
static void queue_unlock(R2AITaskQueue *q) {
|
||||
r_th_lock_leave (q->lock);
|
||||
}
|
||||
|
||||
static void task_start(R2AITask *t) {
|
||||
task_lock (t);
|
||||
t->state = R2AI_TASK_RUNNING;
|
||||
t->started = time (NULL);
|
||||
task_unlock (t);
|
||||
}
|
||||
|
||||
static bool cancelled(R2AITask *t) {
|
||||
task_lock (t);
|
||||
bool res = t->cancel_req;
|
||||
task_unlock (t);
|
||||
return res;
|
||||
}
|
||||
|
||||
// terminate the worker, an earlier error reported by the llm call takes precedence
|
||||
static RThreadFunctionRet finish(R2AITask *t, R2AITaskState st, char *err) {
|
||||
task_lock (t);
|
||||
if (err && !t->error) {
|
||||
t->error = err;
|
||||
} else {
|
||||
free (err);
|
||||
}
|
||||
t->state = st;
|
||||
t->finished = time (NULL);
|
||||
task_unlock (t);
|
||||
return R_TH_STOP;
|
||||
}
|
||||
|
||||
/* Providers read core->config throughout a request. The HTTP server swaps
|
||||
* that pointer between its web and shell contexts, so use the task snapshot.
|
||||
* Tool execution still uses the original session on the main thread. */
|
||||
static R2AI_ChatResponse *run_llm_once(R2AITask *t) {
|
||||
char *error = NULL;
|
||||
R2AIArgs args = {
|
||||
.messages = t->messages,
|
||||
.system_prompt = t->system_prompt,
|
||||
.model = t->model,
|
||||
.provider = t->provider,
|
||||
.tools = t->tools,
|
||||
.error = &error,
|
||||
};
|
||||
RCorePluginSession worker_session = *t->cps;
|
||||
worker_session.core = t->llm_core;
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (&worker_session, args);
|
||||
if (error) {
|
||||
task_lock (t);
|
||||
free (t->error);
|
||||
t->error = error;
|
||||
task_unlock (t);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void dump_message(R2AITask *t, const R2AI_Message *m) {
|
||||
if (m->reasoning_content) {
|
||||
task_append_outputf (t, "<thinking>\n%s\n</thinking>\n", m->reasoning_content);
|
||||
}
|
||||
if (m->content) {
|
||||
task_append_outputf (t, "%s\n", m->content);
|
||||
}
|
||||
}
|
||||
|
||||
static RThreadFunctionRet worker_query(RThread *th) {
|
||||
R2AITask *t = (R2AITask *)th->user;
|
||||
task_start (t);
|
||||
R2AI_ChatResponse *res = run_llm_once (t);
|
||||
bool ok = res && res->message;
|
||||
bool cancel = cancelled (t);
|
||||
if (ok && !cancel) {
|
||||
dump_message (t, res->message);
|
||||
}
|
||||
r2ai_chat_response_free (res);
|
||||
if (cancel) {
|
||||
return finish (t, R2AI_TASK_CANCELLED, NULL);
|
||||
}
|
||||
return ok
|
||||
? finish (t, R2AI_TASK_COMPLETE, NULL)
|
||||
: finish (t, R2AI_TASK_ERROR, strdup ("llm call returned no response"));
|
||||
}
|
||||
|
||||
static R2AI_ToolCall *first_tool_call(const R2AI_Message *m) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (m->tool_calls, iter, tc) {
|
||||
if (tc->name && tc->arguments && tc->id) {
|
||||
return tc;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// hand the tool call to the main thread and block until it answers, returns false if cancelled
|
||||
static bool wait_tool(R2AITask *t, const R2AI_ToolCall *tc) {
|
||||
task_lock (t);
|
||||
pending_clear (t);
|
||||
R_FREE (t->tool_result);
|
||||
t->pending_tool_name = strdup (tc->name);
|
||||
t->pending_tool_args = strdup (tc->arguments);
|
||||
t->pending_tool_call_id = strdup (tc->id);
|
||||
t->state = R2AI_TASK_WAIT_APPROVE;
|
||||
task_unlock (t);
|
||||
|
||||
r_th_sem_wait (t->gate);
|
||||
|
||||
task_lock (t);
|
||||
if (t->cancel_req) {
|
||||
task_unlock (t);
|
||||
return false;
|
||||
}
|
||||
R2AI_Message msg = {
|
||||
.role = "tool",
|
||||
.tool_call_id = t->pending_tool_call_id,
|
||||
.content = t->tool_result? t->tool_result: "<no output>",
|
||||
};
|
||||
r2ai_msgs_add (t->messages, &msg);
|
||||
R_FREE (t->tool_result);
|
||||
pending_clear (t);
|
||||
task_unlock (t);
|
||||
return true;
|
||||
}
|
||||
|
||||
static RThreadFunctionRet worker_auto(RThread *th) {
|
||||
R2AITask *t = (R2AITask *)th->user;
|
||||
task_start (t);
|
||||
for (;;) {
|
||||
if (cancelled (t)) {
|
||||
return finish (t, R2AI_TASK_CANCELLED, NULL);
|
||||
}
|
||||
if (t->steps >= t->max_runs) {
|
||||
return finish (t, R2AI_TASK_ERROR, r_str_newf ("max runs (%d) reached", t->max_runs));
|
||||
}
|
||||
task_lock (t);
|
||||
t->steps++;
|
||||
task_unlock (t);
|
||||
|
||||
R2AI_ChatResponse *res = run_llm_once (t);
|
||||
if (!res || !res->message) {
|
||||
r2ai_chat_response_free (res);
|
||||
return finish (t, R2AI_TASK_ERROR, strdup ("llm call returned no response"));
|
||||
}
|
||||
const R2AI_Message *m = res->message;
|
||||
dump_message (t, m);
|
||||
r2ai_msgs_add (t->messages, m);
|
||||
if (r_list_empty (m->tool_calls)) {
|
||||
r2ai_chat_response_free (res);
|
||||
return finish (t, R2AI_TASK_COMPLETE, NULL);
|
||||
}
|
||||
R2AI_ToolCall *tc = first_tool_call (m);
|
||||
bool ok = tc && wait_tool (t, tc);
|
||||
r2ai_chat_response_free (res);
|
||||
if (!ok) {
|
||||
return tc
|
||||
? finish (t, R2AI_TASK_CANCELLED, NULL)
|
||||
: finish (t, R2AI_TASK_ERROR, strdup ("llm returned an invalid tool call"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_IPI void r2ai_async_init(R2AI_State *state) {
|
||||
if (!state || state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = R_NEW0 (R2AITaskQueue);
|
||||
q->tasks = r_list_newf ((RListFree)task_free);
|
||||
q->lock = r_th_lock_new (true);
|
||||
q->next_id = 1;
|
||||
state->async = q;
|
||||
}
|
||||
|
||||
R_IPI void r2ai_async_fini(R2AI_State *state) {
|
||||
if (!state || !state->async) {
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = state->async;
|
||||
int killed = 0;
|
||||
/* Force-kill any live workers so we don't block on inflight HTTP. */
|
||||
queue_lock (q);
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
task_lock (t);
|
||||
bool live = task_is_live_locked (t);
|
||||
t->cancel_req = true;
|
||||
/* take ownership of the thread while holding the task lock */
|
||||
RThread *th = live? t->thread: NULL;
|
||||
if (th) {
|
||||
t->thread = NULL;
|
||||
}
|
||||
task_unlock (t);
|
||||
if (t->gate) {
|
||||
r_th_sem_post (t->gate);
|
||||
}
|
||||
if (th) {
|
||||
/* kill_free joins the thread: never do it under the task lock */
|
||||
r_th_kill_free (th);
|
||||
killed++;
|
||||
}
|
||||
}
|
||||
queue_unlock (q);
|
||||
if (killed > 0) {
|
||||
R_LOG_INFO ("killed %d pending async task%s", killed, killed == 1? "": "s");
|
||||
}
|
||||
r_list_free (q->tasks);
|
||||
r_th_lock_free (q->lock);
|
||||
free (q);
|
||||
state->async = NULL;
|
||||
}
|
||||
|
||||
static R2AITask *task_new(RCorePluginSession *cps, R2AITaskKind kind, const char *title, const char *query, const char *system_prompt) {
|
||||
RCore *core = cps->core;
|
||||
R2AITask *t = R_NEW0 (R2AITask);
|
||||
t->cps = cps;
|
||||
/* Snapshot on the main thread, before it can mutate the shared core.
|
||||
* Only config is owned; providers borrow the remaining session fields. */
|
||||
t->llm_core = R_NEW (RCore);
|
||||
*t->llm_core = *core;
|
||||
t->llm_core->config = r_config_clone (core->config);
|
||||
t->kind = kind;
|
||||
t->state = R2AI_TASK_PENDING;
|
||||
t->title = strdup (title? title: "");
|
||||
t->query = strdup (query? query: "");
|
||||
// resolve the system prompt here, workers must not read the config
|
||||
if (R_STR_ISEMPTY (system_prompt) && kind == R2AI_TASK_AUTO) {
|
||||
t->system_prompt = r2ai_auto_system_prompt (cps);
|
||||
} else {
|
||||
t->system_prompt = r_str_new (r2ai_system_prompt (core, system_prompt));
|
||||
}
|
||||
const char *m = r_config_get (core->config, "r2ai.model");
|
||||
const char *p = r_config_get (core->config, "r2ai.api");
|
||||
t->model = m? strdup (m): NULL;
|
||||
t->provider = p? strdup (p): NULL;
|
||||
if (kind == R2AI_TASK_AUTO) {
|
||||
RList *tools = r2ai_get_tools (core, cps->data);
|
||||
t->tools = tools? r_list_clone (tools, NULL): NULL;
|
||||
t->max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
|
||||
}
|
||||
t->messages = r2ai_msgs_new ();
|
||||
R2AI_Message um = { .role = "user", .content = (char *)query };
|
||||
r2ai_msgs_add (t->messages, &um);
|
||||
t->lock = r_th_lock_new (false);
|
||||
t->gate = r_th_sem_new (0);
|
||||
t->output = r_strbuf_new (NULL);
|
||||
t->created = time (NULL);
|
||||
return t;
|
||||
}
|
||||
|
||||
static int queue_register(R2AITaskQueue *q, R2AITask *t) {
|
||||
queue_lock (q);
|
||||
t->id = q->next_id++;
|
||||
r_list_append (q->tasks, t);
|
||||
queue_unlock (q);
|
||||
return t->id;
|
||||
}
|
||||
|
||||
static int submit(RCorePluginSession *cps, R2AITaskKind kind, const char *title, const char *query, const char *sysp, RThreadFunction fn) {
|
||||
R2AITask *t = task_new (cps, kind, title, query, sysp);
|
||||
int id = queue_register (queue (cps), t);
|
||||
t->thread = r_th_new (fn, t, 0);
|
||||
if (t->thread) {
|
||||
r_th_start (t->thread);
|
||||
} else {
|
||||
finish (t, R2AI_TASK_ERROR, strdup ("failed to spawn worker"));
|
||||
}
|
||||
r_cons_printf (cps->core->cons, "[async] task %d queued (%s)\n", id, t->title);
|
||||
return id;
|
||||
}
|
||||
|
||||
R_IPI int r2ai_async_query(RCorePluginSession *cps,
|
||||
const char *title,
|
||||
const char *query,
|
||||
const char *sysp) {
|
||||
return submit (cps, R2AI_TASK_QUERY, title, query, sysp, worker_query);
|
||||
}
|
||||
|
||||
R_IPI int r2ai_async_auto(RCorePluginSession *cps,
|
||||
const char *title,
|
||||
const char *query,
|
||||
const char *sysp) {
|
||||
return submit (cps, R2AI_TASK_AUTO, title, query, sysp, worker_auto);
|
||||
}
|
||||
|
||||
static void purge_finished(RCorePluginSession *cps);
|
||||
|
||||
static void show_task_list(RCorePluginSession *cps, bool json) {
|
||||
RCore *core = cps->core;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
if (json) {
|
||||
PJ *pj = r_core_pj_new (cps->core);
|
||||
pj_o (pj);
|
||||
pj_ka (pj, "tasks");
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
task_lock (t);
|
||||
pj_o (pj);
|
||||
pj_ki (pj, "id", t->id);
|
||||
pj_ks (pj, "kind", kind_name (t->kind));
|
||||
pj_ks (pj, "state", state_name (t->state));
|
||||
pj_ks (pj, "title", t->title? t->title: "");
|
||||
pj_ki (pj, "steps", t->steps);
|
||||
pj_ki (pj, "age", (int) (time (NULL) - t->created));
|
||||
if (t->pending_tool_name) {
|
||||
pj_ks (pj, "pending_tool", t->pending_tool_name);
|
||||
}
|
||||
if (t->pending_tool_args) {
|
||||
pj_ks (pj, "pending_tool_args", t->pending_tool_args);
|
||||
}
|
||||
if (t->pending_tool_call_id) {
|
||||
pj_ks (pj, "pending_tool_call_id", t->pending_tool_call_id);
|
||||
}
|
||||
const char *out = r_strbuf_get (t->output);
|
||||
if (R_STR_ISNOTEMPTY (out)) {
|
||||
pj_ks (pj, "output", out);
|
||||
}
|
||||
if (t->error) {
|
||||
pj_ks (pj, "error", t->error);
|
||||
}
|
||||
pj_end (pj);
|
||||
task_unlock (t);
|
||||
}
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
char *s = pj_drain (pj);
|
||||
r_cons_println (core->cons, s);
|
||||
free (s);
|
||||
} else if (r_list_empty (q->tasks)) {
|
||||
r_cons_printf (core->cons, "No async tasks\n");
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%-4s %-6s %-13s %-4s %s\n", "id", "kind", "state", "age", "title");
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
task_lock (t);
|
||||
int age = (int) (time (NULL) - t->created);
|
||||
const char *extra = "";
|
||||
char *extrabuf = NULL;
|
||||
if (t->state == R2AI_TASK_WAIT_APPROVE && t->pending_tool_name) {
|
||||
extrabuf = r_str_newf (" [awaiting tool: %s]", t->pending_tool_name);
|
||||
extra = extrabuf;
|
||||
} else if (t->state == R2AI_TASK_ERROR && t->error) {
|
||||
extrabuf = r_str_newf (" [err: %s]", t->error);
|
||||
extra = extrabuf;
|
||||
}
|
||||
r_cons_printf (core->cons, "%-4d %-6s %-13s %-4d %s%s\n", t->id, kind_name (t->kind), state_name (t->state), age, t->title? t->title: "", extra);
|
||||
print_block (core->cons, "output:\n", r_strbuf_get (t->output));
|
||||
free (extrabuf);
|
||||
task_unlock (t);
|
||||
}
|
||||
}
|
||||
if (r_config_get_b (core->config, "r2ai.async.purge")) {
|
||||
purge_finished (cps);
|
||||
}
|
||||
queue_unlock (q);
|
||||
}
|
||||
|
||||
static bool task_is_actionable_locked(const R2AITask *t) {
|
||||
switch (t->state) {
|
||||
case R2AI_TASK_COMPLETE:
|
||||
case R2AI_TASK_ERROR:
|
||||
case R2AI_TASK_WAIT_APPROVE:
|
||||
case R2AI_TASK_WAIT_INPUT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Find first actionable task (id == 0) or the task with the given id if
|
||||
* actionable. Returns with queue lock held; caller must release via
|
||||
* queue_unlock. Returns NULL if none found (lock released). */
|
||||
static R2AITask *find_actionable(R2AITaskQueue *q, int id) {
|
||||
queue_lock (q);
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
if (id > 0 && t->id != id) {
|
||||
continue;
|
||||
}
|
||||
task_lock (t);
|
||||
bool actionable = task_is_actionable_locked (t);
|
||||
task_unlock (t);
|
||||
if (actionable) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
queue_unlock (q);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void task_unlink_disk(const R2AITask *t) {
|
||||
char *path = r_file_homef (".config/r2ai/tasks/%d.json", t->id);
|
||||
if (path) {
|
||||
r_file_rm (path);
|
||||
free (path);
|
||||
}
|
||||
}
|
||||
|
||||
static void drop_task_locked(R2AITaskQueue *q, R2AITask *t) {
|
||||
task_unlink_disk (t);
|
||||
r_list_delete_data (q->tasks, t);
|
||||
}
|
||||
|
||||
static bool answer_wait_approve(RCorePluginSession *cps, R2AITask *t, bool approve, bool interactive) {
|
||||
RCore *core = cps->core;
|
||||
task_lock (t);
|
||||
if (t->state != R2AI_TASK_WAIT_APPROVE) {
|
||||
task_unlock (t);
|
||||
return false;
|
||||
}
|
||||
char *tool_name = t->pending_tool_name? strdup (t->pending_tool_name): NULL;
|
||||
char *tool_args = t->pending_tool_args? strdup (t->pending_tool_args): NULL;
|
||||
task_unlock (t);
|
||||
|
||||
char *tool_output = NULL;
|
||||
if (approve) {
|
||||
bool old_yolo = r_config_get_b (core->config, "r2ai.auto.yolo");
|
||||
r_config_set_b (core->config, "r2ai.auto.yolo", true);
|
||||
R2AI_ToolResult tool_result = execute_tool (cps, tool_name, tool_args);
|
||||
r_config_set_b (core->config, "r2ai.auto.yolo", old_yolo);
|
||||
tool_output = tool_result.output;
|
||||
tool_result.output = NULL;
|
||||
r2ai_tool_result_fini (&tool_result);
|
||||
if (!tool_output) {
|
||||
tool_output = strdup ("<no output>");
|
||||
}
|
||||
task_append_outputf (t, "\nTool result (%s):\n%s\n", tool_name? tool_name: "?", tool_output);
|
||||
if (interactive) {
|
||||
r_cons_printf (core->cons, Color_GREEN "tool result:" Color_RESET " %s\n", tool_output);
|
||||
}
|
||||
} else {
|
||||
tool_output = strdup ("<user declined to run tool>");
|
||||
if (interactive) {
|
||||
r_cons_printf (core->cons, "declined.\n");
|
||||
} else {
|
||||
task_append_output (t, "\nTool declined by user.\n");
|
||||
}
|
||||
}
|
||||
|
||||
task_lock (t);
|
||||
free (t->tool_result);
|
||||
t->tool_result = tool_output;
|
||||
t->state = R2AI_TASK_RUNNING;
|
||||
task_unlock (t);
|
||||
r_th_sem_post (t->gate);
|
||||
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Interactive handler: act on first actionable task (or the given id). */
|
||||
static void interact_once(RCorePluginSession *cps, int id) {
|
||||
RCore *core = cps->core;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = find_actionable (q, id);
|
||||
if (!t) {
|
||||
/* Nothing to do - silent by default so cmd.prompt stays clean. */
|
||||
return;
|
||||
}
|
||||
/* queue lock still held */
|
||||
task_lock (t);
|
||||
R2AITaskState st = t->state;
|
||||
int tid = t->id;
|
||||
char *output = r_strbuf_drain_nofree (t->output);
|
||||
char *err = t->error;
|
||||
t->error = NULL;
|
||||
char *tool_name = t->pending_tool_name? strdup (t->pending_tool_name): NULL;
|
||||
char *tool_args = t->pending_tool_args? strdup (t->pending_tool_args): NULL;
|
||||
R2AITaskKind kind = t->kind;
|
||||
task_unlock (t);
|
||||
queue_unlock (q);
|
||||
|
||||
r_cons_printf (core->cons, "\n" Color_BLUE "[async task %d | %s | %s]" Color_RESET "\n", tid, kind_name (kind), state_name (st));
|
||||
print_block (core->cons, "", output);
|
||||
free (output);
|
||||
if (err) {
|
||||
r_cons_printf (core->cons, Color_RED "error: %s" Color_RESET "\n", err);
|
||||
free (err);
|
||||
}
|
||||
|
||||
if (is_done (st)) {
|
||||
/* Remove the task from the queue (join the thread on free). */
|
||||
queue_lock (q);
|
||||
drop_task_locked (q, t);
|
||||
queue_unlock (q);
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
return;
|
||||
}
|
||||
|
||||
if (st == R2AI_TASK_WAIT_APPROVE) {
|
||||
r_cons_printf (core->cons, Color_YELLOW "pending tool:" Color_RESET " %s\n", tool_name? tool_name: "?");
|
||||
if (tool_args) {
|
||||
r_cons_printf (core->cons, "args: %s\n", tool_args);
|
||||
}
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
bool yolo = r_config_get_b (core->config, "r2ai.auto.yolo");
|
||||
bool approve = yolo || r_cons_yesno (core->cons, 'y', "Run tool %s? (Y/n)", tool_name? tool_name: "?");
|
||||
answer_wait_approve (cps, t, approve, true);
|
||||
}
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
}
|
||||
|
||||
// lock the queue and find the task, reports and unlocks when it does not exist
|
||||
static R2AITask *lookup(RCorePluginSession *cps, int id) {
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
if (t->id == id) {
|
||||
return t;
|
||||
}
|
||||
}
|
||||
queue_unlock (q);
|
||||
r_cons_printf (cps->core->cons, "No task with id %d\n", id);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void answer_by_id(RCorePluginSession *cps, int id, bool approve) {
|
||||
RCore *core = cps->core;
|
||||
if (id <= 0) {
|
||||
r_cons_printf (core->cons, "Missing task id\n");
|
||||
return;
|
||||
}
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = lookup (cps, id);
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
task_lock (t);
|
||||
bool wait_approve = t->state == R2AI_TASK_WAIT_APPROVE;
|
||||
task_unlock (t);
|
||||
queue_unlock (q);
|
||||
|
||||
if (!wait_approve) {
|
||||
r_cons_printf (core->cons, "Task %d is not waiting for approval\n", id);
|
||||
return;
|
||||
}
|
||||
if (answer_wait_approve (cps, t, approve, false)) {
|
||||
r_cons_printf (core->cons, "%s task %d\n", approve? "Approved": "Declined", id);
|
||||
}
|
||||
}
|
||||
|
||||
static void kill_task_locked(R2AITaskQueue *q, R2AITask *t) {
|
||||
task_lock (t);
|
||||
t->cancel_req = true;
|
||||
bool live = task_is_live_locked (t);
|
||||
task_unlock (t);
|
||||
if (t->gate) {
|
||||
r_th_sem_post (t->gate);
|
||||
}
|
||||
if (live && t->thread) {
|
||||
r_th_kill_free (t->thread);
|
||||
t->thread = NULL;
|
||||
}
|
||||
drop_task_locked (q, t);
|
||||
}
|
||||
|
||||
static void kill_by_id(RCorePluginSession *cps, int id) {
|
||||
RCore *core = cps->core;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = lookup (cps, id);
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
kill_task_locked (q, t);
|
||||
queue_unlock (q);
|
||||
r_cons_printf (core->cons, "Killed task %d\n", id);
|
||||
}
|
||||
|
||||
static void kill_all(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
int n = 0;
|
||||
while (!r_list_empty (q->tasks)) {
|
||||
R2AITask *t = r_list_first (q->tasks);
|
||||
kill_task_locked (q, t);
|
||||
n++;
|
||||
}
|
||||
queue_unlock (q);
|
||||
r_cons_printf (core->cons, "Killed %d task%s\n", n, n == 1? "": "s");
|
||||
}
|
||||
|
||||
static void show_task_by_id(RCorePluginSession *cps, int id) {
|
||||
RCore *core = cps->core;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
R2AITask *t = lookup (cps, id);
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
task_lock (t);
|
||||
r_cons_printf (core->cons, "id: %d\nkind: %s\nstate: %s\ntitle: %s\nmodel: %s\nprov: %s\nsteps: %d\nage: %ds\n",
|
||||
t->id, kind_name (t->kind), state_name (t->state), t->title, r_str_get (t->model),
|
||||
r_str_get (t->provider), t->steps, (int) (time (NULL) - t->created));
|
||||
if (t->pending_tool_name) {
|
||||
r_cons_printf (core->cons, "tool: %s %s\n", t->pending_tool_name, t->pending_tool_args? t->pending_tool_args: "");
|
||||
}
|
||||
if (t->error) {
|
||||
r_cons_printf (core->cons, "error: %s\n", t->error);
|
||||
}
|
||||
print_block (core->cons, "output:\n", r_strbuf_get (t->output));
|
||||
task_unlock (t);
|
||||
queue_unlock (q);
|
||||
}
|
||||
|
||||
static void show_last_task(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
R2AITask *t = r_list_last (q->tasks);
|
||||
int id = t? t->id: 0;
|
||||
queue_unlock (q);
|
||||
if (!id) {
|
||||
r_cons_printf (core->cons, "No async tasks\n");
|
||||
return;
|
||||
}
|
||||
show_task_by_id (cps, id);
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
static RCoreHelpMessage help_msg_r2ai_s = {
|
||||
"Usage:", "r2ai", " -s[subcmd]",
|
||||
"r2ai", " -s", "list async tasks and completed output",
|
||||
"r2ai", " -s?", "show this help",
|
||||
"r2ai", " -sj", "list tasks as json, including completed output",
|
||||
"r2ai", " -ss", "show details of the last created task",
|
||||
"r2ai", " -si", "interactive: handle first actionable task",
|
||||
"r2ai", " -si <id>", "interactive: handle only task <id>",
|
||||
"r2ai", " -sy <id>", "approve pending tool call for task <id>",
|
||||
"r2ai", " -sn <id>", "decline pending tool call for task <id>",
|
||||
"r2ai", " -sa", "block until all tasks finish",
|
||||
"r2ai", " -sp", "purge finished/errored/cancelled tasks",
|
||||
"r2ai", " -s <id>", "show details of task <id>",
|
||||
"r2ai", " -sk", "kill all tasks",
|
||||
"r2ai", " -sk <id>", "kill task <id>",
|
||||
NULL
|
||||
};
|
||||
|
||||
static RCoreHelpMessage help_msg_r2ai_s_kill = {
|
||||
"r2ai", " -sk", "kill all tasks",
|
||||
"r2ai", " -sk <id>", "kill task <id>",
|
||||
NULL
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static void show_help(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
r2ai_cmd_help (core, help_msg_r2ai_s);
|
||||
}
|
||||
|
||||
static void purge_finished(RCorePluginSession *cps) {
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
queue_lock (q);
|
||||
RListIter *it, *tmp;
|
||||
R2AITask *t;
|
||||
r_list_foreach_safe (q->tasks, it, tmp, t) {
|
||||
task_lock (t);
|
||||
bool drop = is_done (t->state);
|
||||
task_unlock (t);
|
||||
if (drop) {
|
||||
drop_task_locked (q, t);
|
||||
}
|
||||
}
|
||||
queue_unlock (q);
|
||||
}
|
||||
|
||||
static void wait_all(RCorePluginSession *cps) {
|
||||
R2AITaskQueue *q = queue (cps);
|
||||
for (;;) {
|
||||
bool any = false;
|
||||
queue_lock (q);
|
||||
RListIter *it;
|
||||
R2AITask *t;
|
||||
r_list_foreach (q->tasks, it, t) {
|
||||
task_lock (t);
|
||||
bool busy = t->state == R2AI_TASK_PENDING || t->state == R2AI_TASK_RUNNING;
|
||||
task_unlock (t);
|
||||
if (busy) {
|
||||
any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
queue_unlock (q);
|
||||
if (!any) {
|
||||
break;
|
||||
}
|
||||
r_sys_usleep (100 * 1000); /* 100ms */
|
||||
}
|
||||
}
|
||||
|
||||
R_IPI void r2ai_async_cmd(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
const char *a = input? input: "";
|
||||
const char *arg = r_str_trim_head_ro (a + 1);
|
||||
int id = R_STR_ISEMPTY (arg)? 0: r_num_math (core->num, arg);
|
||||
switch (*a) {
|
||||
case '?':
|
||||
show_help (cps);
|
||||
break;
|
||||
case 'j':
|
||||
show_task_list (cps, true);
|
||||
break;
|
||||
case 's':
|
||||
show_last_task (cps);
|
||||
break;
|
||||
case 'i':
|
||||
interact_once (cps, id);
|
||||
break;
|
||||
case 'y':
|
||||
answer_by_id (cps, id, true);
|
||||
break;
|
||||
case 'n':
|
||||
answer_by_id (cps, id, false);
|
||||
break;
|
||||
case 'a':
|
||||
wait_all (cps);
|
||||
break;
|
||||
case 'p':
|
||||
purge_finished (cps);
|
||||
break;
|
||||
case 'k':
|
||||
if (R_STR_ISEMPTY (arg)) {
|
||||
kill_all (cps);
|
||||
} else {
|
||||
if (id > 0) {
|
||||
kill_by_id (cps, id);
|
||||
} else {
|
||||
r2ai_cmd_help (core, help_msg_r2ai_s_kill);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ' ':
|
||||
if (id > 0) {
|
||||
show_task_by_id (cps, id);
|
||||
} else {
|
||||
show_task_list (cps, false);
|
||||
}
|
||||
break;
|
||||
case 0:
|
||||
show_task_list (cps, false);
|
||||
break;
|
||||
default:
|
||||
r_core_return_invalid_command (core, "-s", *a);
|
||||
break;
|
||||
}
|
||||
}
|
||||
+299
-205
@@ -1,3 +1,5 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
// Stats are now stored in R2AI_State->stats, no longer need global variable
|
||||
@@ -5,24 +7,22 @@
|
||||
// Helper function to format time duration
|
||||
static char *format_time_duration(time_t seconds) {
|
||||
if (seconds < 60) {
|
||||
return r_str_newf ("%" PFMT64d "s", (long long)seconds);
|
||||
return r_str_newf ("%" PFMT64d "s", (ut64)seconds);
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
return r_str_newf ("%" PFMT64d "m%" PFMT64d "s", (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 ("%" PFMT64d "h%" PFMT64d "m%" PFMT64d "s",
|
||||
(long long) (seconds / 3600),
|
||||
(long long) ((seconds % 3600) / 60),
|
||||
(long long) (seconds % 60));
|
||||
(ut64) (seconds / 3600),
|
||||
(ut64) ((seconds % 3600) / 60),
|
||||
(ut64) (seconds % 60));
|
||||
}
|
||||
|
||||
// Initialize timing and cost tracking for a 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
|
||||
state->stats.total_cost = 0.0;
|
||||
state->stats.run_cost = 0.0;
|
||||
state->stats.total_start_time = run_start;
|
||||
state->stats.total_tokens = 0;
|
||||
state->stats.run_tokens = 0;
|
||||
@@ -51,27 +51,12 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
state->stats.total_completion_tokens += usage->completion_tokens;
|
||||
}
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.chat.show_cost") == true) {
|
||||
// TODO: calculate cost
|
||||
state->stats.run_cost = 0.0 * run_time;
|
||||
state->stats.total_cost += state->stats.run_cost;
|
||||
}
|
||||
|
||||
// Format times for display
|
||||
char *run_time_str = format_time_duration (run_time);
|
||||
char *total_time_str = format_time_duration (total_time);
|
||||
|
||||
// Print detailed stats
|
||||
r_cons_printf (core->cons, "\x1b[1;34m%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s\x1b[0m\n",
|
||||
r_config_get (core->config, "r2ai.model"),
|
||||
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);
|
||||
r_cons_printf (core->cons, Color_BOLD 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"), 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);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
@@ -79,28 +64,99 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
|
||||
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) {
|
||||
r2ai_print_response (core, final_msg->content);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
// Add final response to messages for completeness
|
||||
r2ai_msgs_add (messages, final_msg);
|
||||
}
|
||||
r2ai_chat_response_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;
|
||||
}
|
||||
}
|
||||
|
||||
static const char *auto_prompt_get(R2AI_State *state) {
|
||||
return (state && R_STR_ISNOTEMPTY (state->prompt_auto))? state->prompt_auto: "";
|
||||
}
|
||||
|
||||
R_IPI char *r2ai_auto_system_prompt(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
const char *base_system = r_config_get (core->config, "r2ai.system");
|
||||
const char *prompt_auto = auto_prompt_get (state);
|
||||
const char *think_prefix = r_config_get_b (core->config, "r2ai.auto.think")
|
||||
? ""
|
||||
: "/no_think\nReasoning: Low\n";
|
||||
return R_STR_ISNOTEMPTY (base_system)
|
||||
? r_str_newf ("%s%s\n\n%s", think_prefix, base_system, prompt_auto)
|
||||
: r_str_newf ("%s%s", think_prefix, prompt_auto);
|
||||
}
|
||||
|
||||
// Helper function to process messages and handle tool calls recursively
|
||||
R_API void process_messages(RCorePluginSession *cps, 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;
|
||||
@@ -108,32 +164,44 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
const int max_runs = r_config_get_i (core->config, "r2ai.auto.max_runs");
|
||||
|
||||
if (n_run > max_runs) {
|
||||
r_cons_printf (core->cons, "\x1b[1;31m[r2ai] Max runs reached\x1b[0m\n");
|
||||
R_LOG_WARN ("Max runs reached");
|
||||
r_cons_flush (core->cons);
|
||||
return;
|
||||
}
|
||||
|
||||
const char *effective_prompt = system_prompt? system_prompt: auto_prompt_get (state);
|
||||
if (!system_prompt) {
|
||||
system_prompt = Gprompt_auto;
|
||||
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 (R_STR_ISNOTEMPTY (cmd_output)) {
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "command", init_commands);
|
||||
pj_end (pj);
|
||||
char *tool_args = pj_drain (pj);
|
||||
R2AI_ToolResult tool_result = execute_tool (cps, "r2cmd", tool_args);
|
||||
free (tool_args);
|
||||
if (R_STR_ISNOTEMPTY (tool_result.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, tool_result.output);
|
||||
if (tool_result.comment && *tool_result.comment) {
|
||||
char *new_content = r_str_newf ("%s\nHINT: %s", content, tool_result.comment);
|
||||
free (content);
|
||||
content = new_content;
|
||||
}
|
||||
R2AI_Message init_msg = {
|
||||
.role = "system",
|
||||
.content = r_str_newf ("Here is some information about the binary to get you started:\n>%s\n%s", edited_command, cmd_output)
|
||||
.content = content
|
||||
};
|
||||
r2ai_msgs_add (messages, &init_msg);
|
||||
free (cmd_output);
|
||||
free (display_command);
|
||||
}
|
||||
free (edited_command);
|
||||
r2ai_tool_result_fini (&tool_result);
|
||||
}
|
||||
}
|
||||
|
||||
r2ai_stats_init_run (state, n_run);
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[35m[DEBUG] About to call r2ai_llmcall with n_run=%d\x1b[0m\n", n_run);
|
||||
R_LOG_DEBUG ("About to call r2ai_llmcall with n_run=%d", n_run);
|
||||
r_cons_flush (core->cons);
|
||||
|
||||
// Set up args for r2ai_llmcall call with tools directly
|
||||
@@ -141,13 +209,14 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
.messages = messages,
|
||||
.error = &error,
|
||||
.dorag = true,
|
||||
.tools = r2ai_get_tools (), // Always send tools in auto mode
|
||||
.system_prompt = system_prompt
|
||||
.tools = r2ai_get_tools (core, cps->data), // Always send tools in auto mode
|
||||
.system_prompt = effective_prompt
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -156,37 +225,40 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
|
||||
if (!message) {
|
||||
R_LOG_ERROR ("No message in response");
|
||||
free (response);
|
||||
r2ai_chat_response_free (response);
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug logging for rawtools
|
||||
R_LOG_DEBUG ("[DEBUG] Model response - Role: %s", message->role? message->role: "null");
|
||||
R_LOG_DEBUG ("Model response - Role: %s", message->role? message->role: "null");
|
||||
if (message->content) {
|
||||
R_LOG_DEBUG ("[DEBUG] Content: %s", message->content);
|
||||
R_LOG_DEBUG ("Content: %s", message->content);
|
||||
}
|
||||
if (message->reasoning_content) {
|
||||
R_LOG_DEBUG ("[DEBUG] Reasoning: %s", message->reasoning_content);
|
||||
R_LOG_DEBUG ("Reasoning: %s", message->reasoning_content);
|
||||
}
|
||||
if (message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("[DEBUG] Tool calls: %d", message->n_tool_calls);
|
||||
for (int i = 0; i < message->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *tc = &message->tool_calls[i];
|
||||
R_LOG_DEBUG ("[DEBUG] Tool %d: %s - %s", i, tc->name? tc->name: "null", tc->arguments? tc->arguments: "null");
|
||||
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) {
|
||||
r_cons_printf (core->cons, "\x1b[31m[Assistant]\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, Color_RED "[Assistant]" Color_RESET);
|
||||
if (message->reasoning_content) {
|
||||
r_cons_printf (core->cons, "\x1b[90m<thinking>\n%s\n</thinking>\x1b[0m\n", message->reasoning_content);
|
||||
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) {
|
||||
r_cons_printf (core->cons, "%s", message->content);
|
||||
r_cons_newline (core->cons);
|
||||
r2ai_print_response (core, message->content);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
}
|
||||
@@ -196,18 +268,21 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
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 %d: %s with args: %s", i, tool_call->name, tool_call->arguments);
|
||||
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);
|
||||
@@ -219,94 +294,94 @@ R_API void process_messages(RCorePluginSession *cps, R2AI_Messages *messages, co
|
||||
}
|
||||
|
||||
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);
|
||||
if (edited_command) {
|
||||
R2AI_ToolResult tool_result = execute_tool (cps, tool_name, tool_args);
|
||||
cmd_output = tool_result.output;
|
||||
tool_result.output = NULL;
|
||||
comment = tool_result.comment;
|
||||
tool_result.comment = NULL;
|
||||
if (tool_result.edited_command) {
|
||||
// Update the last message's tool call arguments with the edited command
|
||||
R2AI_Message *last_msg = r_list_get_n (messages->messages, r_list_length (messages->messages) - 1);
|
||||
if (last_msg && last_msg->tool_calls && last_msg->n_tool_calls > 0) {
|
||||
for (int j = 0; j < last_msg->n_tool_calls; j++) {
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&last_msg->tool_calls[j];
|
||||
if (tc->id && strcmp (tc->id, tool_call->id) == 0) {
|
||||
// For r2cmd, update the command in arguments
|
||||
if (strcmp (tool_name, "r2cmd") == 0) {
|
||||
char *args_dup = strdup (tc->arguments);
|
||||
RJson *args_json = r_json_parse (args_dup);
|
||||
if (args_json) {
|
||||
RJson *cmd_json = (RJson *)r_json_get (args_json, "command");
|
||||
if (cmd_json && cmd_json->str_value) {
|
||||
// Update the command field
|
||||
free ((char *)cmd_json->str_value);
|
||||
cmd_json->str_value = strdup (edited_command);
|
||||
// Serialize back to JSON
|
||||
char *new_args = r_json_to_string (args_json);
|
||||
if (new_args) {
|
||||
free ((void *)tc->arguments);
|
||||
tc->arguments = new_args;
|
||||
}
|
||||
}
|
||||
r_json_free (args_json);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
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", tool_result.edited_command);
|
||||
} else if (!strcmp (tool_name, "execute_js")) {
|
||||
update_tool_call_argument (last_msg, tool_call->id, "script", tool_result.edited_command);
|
||||
}
|
||||
}
|
||||
}
|
||||
free (edited_command);
|
||||
r2ai_tool_result_fini (&tool_result);
|
||||
}
|
||||
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
if (!cmd_output) {
|
||||
cmd_output = strdup ("<no output>");
|
||||
}
|
||||
if (strcmp (cmd_output, "R2AI_SIGINT") == 0) {
|
||||
r_cons_printf (core->cons, "\n\n\x1b[1;31m[r2ai] Processing interrupted after tool execution\x1b[0m\n\n");
|
||||
r_cons_printf (core->cons, "\n\n" Color_BOLD 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;
|
||||
}
|
||||
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 = strdup (tool_call->id),
|
||||
.tool_call_id = (char *)tool_call->id,
|
||||
.content = cmd_output
|
||||
};
|
||||
|
||||
// Add the tool response to our messages array
|
||||
r2ai_msgs_add (messages, &tool_response);
|
||||
R_LOG_DEBUG ("[DEBUG] Added tool response to messages: %s", cmd_output? cmd_output: "null");
|
||||
r_cons_printf (core->cons, "\x1b[32m[DEBUG] Tool result: %s\x1b[0m\n", cmd_output? cmd_output: "no output");
|
||||
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 (cps, usage, n_run, max_runs);
|
||||
|
||||
// Check if we should continue with recursion
|
||||
if (!interrupted && message->tool_calls && message->n_tool_calls > 0) {
|
||||
R_LOG_DEBUG ("[DEBUG] Recursing to process_messages with n_run=%d", n_run + 1);
|
||||
process_messages (cps, 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 ("[DEBUG] Auto mode loop ending - no more tool calls or interrupted");
|
||||
R_LOG_DEBUG ("Auto mode loop ending - no more tool calls or interrupted");
|
||||
if (interrupted) {
|
||||
// all tool responses are in place, so the history stays well ordered
|
||||
handle_final_response_attempt (cps, messages, effective_prompt, &error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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);
|
||||
// Free the response, including its message and usage
|
||||
r2ai_chat_response_free (response);
|
||||
}
|
||||
|
||||
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, "\x1b[35m[DEBUG] cmd_r2ai_a called with query: %s\x1b[0m\n", user_query);
|
||||
r_cons_flush (core->cons);
|
||||
R_LOG_DEBUG ("cmd_r2ai_a called with query: %s", user_query);
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages) {
|
||||
R_LOG_ERROR ("Conversation not initialized");
|
||||
return;
|
||||
@@ -314,8 +389,9 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, 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 (r_list_length (messages->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);
|
||||
R_FREE (state->cache_prefix);
|
||||
}
|
||||
|
||||
// Add user query
|
||||
@@ -325,66 +401,57 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
|
||||
};
|
||||
r2ai_msgs_add (messages, &user_msg);
|
||||
|
||||
process_messages (cps, messages, NULL, 1);
|
||||
char *system_prompt = r2ai_auto_system_prompt (cps);
|
||||
process_messages (cps, messages, system_prompt, 1);
|
||||
free (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') {
|
||||
r_cons_printf (core->cons, "%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);
|
||||
r_cons_printf (core->cons, "%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)
|
||||
r_cons_printf (core->cons, "%s \x1b[1;37m(length: %zu chars)\x1b[0m\n",
|
||||
content, content_len);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", content);
|
||||
static char *log_tostring(const RList *messages, bool color) {
|
||||
const char *bold = color? Color_BOLD: "";
|
||||
const char *reset = color? Color_RESET: "";
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
RListIter *iter, *iter2;
|
||||
const R2AI_Message *msg;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (messages, iter, msg) {
|
||||
const char *role = msg->role;
|
||||
bool tool = !strcmp (role, "tool");
|
||||
const char *clr = !color? "": !strcmp (role, "user")? Color_GREEN: !strcmp (role, "assistant")? Color_CYAN: tool? Color_MAGENTA: Color_WHITE;
|
||||
const char *text = R_STR_ISNOTEMPTY (msg->content)? msg->content: tool? "<no result>": "<no content>";
|
||||
r_strbuf_appendf (sb, "%s%s[%s]:%s %s\n", bold, clr, role, reset, text);
|
||||
r_list_foreach (msg->tool_calls, iter2, tc) {
|
||||
r_strbuf_appendf (sb, " %s%s[tool call]:%s %s\n", bold, color? Color_MAGENTA: "", reset, tc->name? tc->name: "<unnamed>");
|
||||
if (tc->arguments) {
|
||||
r_strbuf_appendf (sb, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
if (color) {
|
||||
r_strbuf_append (sb, "\n");
|
||||
}
|
||||
}
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// Add this function right after cmd_r2ai_a
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps, const char *flags) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Get conversation
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (cps->data);
|
||||
if (r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const char *input = r_core_cmd_str (core, "r2ai");
|
||||
bool json_mode = input && strstr (input, "-Lj");
|
||||
free ((char *)input);
|
||||
|
||||
if (json_mode) {
|
||||
if (flags && strchr (flags, 'j')) {
|
||||
PJ *pj = pj_new ();
|
||||
if (!pj) {
|
||||
return;
|
||||
}
|
||||
|
||||
pj_a (pj);
|
||||
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
RListIter *iter, *iter2;
|
||||
const R2AI_Message *msg;
|
||||
R2AI_ToolCall *tc;
|
||||
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", r_str_get_fail (msg->role, "unknown"));
|
||||
pj_ks (pj, "content", r_str_get (msg->content));
|
||||
if (!r_list_empty (msg->tool_calls)) {
|
||||
pj_ka (pj, "tool_calls");
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
r_list_foreach (msg->tool_calls, iter2, tc) {
|
||||
pj_o (pj);
|
||||
if (tc->name) {
|
||||
pj_ks (pj, "name", tc->name);
|
||||
@@ -398,57 +465,84 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
|
||||
}
|
||||
pj_end (pj);
|
||||
}
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
char *json_str = pj_drain (pj);
|
||||
r_cons_printf (core->cons, "%s\n", json_str);
|
||||
r_cons_println (core->cons, json_str);
|
||||
free (json_str);
|
||||
return;
|
||||
}
|
||||
char *log = log_tostring (messages, true);
|
||||
r_cons_printf (core->cons, Color_BOLD Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n"
|
||||
Color_BOLD Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n%s",
|
||||
r_list_length (messages), log);
|
||||
free (log);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1;34m[r2ai] Chat Logs (%d messages)\x1b[0m\n",
|
||||
r_list_length (messages->messages));
|
||||
// Format conversation as string
|
||||
char *log = log_tostring (messages, false);
|
||||
char *log_str = r_str_newf ("Conversation Log:\n%s", log);
|
||||
free (log);
|
||||
|
||||
r_cons_printf (core->cons, "\x1b[1;33mNote: System prompt is applied automatically but not stored in history\x1b[0m\n\n");
|
||||
// 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");
|
||||
}
|
||||
|
||||
// Display each message in the conversation
|
||||
for (int i = 0; i < r_list_length (messages->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (messages->messages, i);
|
||||
const char *role = msg->role;
|
||||
// Combine log and prompt
|
||||
char *full_input = r_str_newf ("%s\n\n%s", log_str, prompt_text);
|
||||
|
||||
// Format based on role
|
||||
if (!strcmp (role, "user")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;32m[user]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
} else if (!strcmp (role, "assistant")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;36m[assistant]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
// 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];
|
||||
r_cons_printf (core->cons, " \x1b[1;35m[tool call]:\x1b[0m %s\n",
|
||||
tc->name? tc->name: "<unnamed>");
|
||||
char *error = NULL;
|
||||
char *res = r2ai (cps, (R2AIArgs){ .input = full_input, .error = &error, .dorag = false });
|
||||
|
||||
if (tc->arguments) {
|
||||
r_cons_printf (core->cons, " %s\n", tc->arguments);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!strcmp (role, "tool")) {
|
||||
r_cons_printf (core->cons, "\x1b[1;35m[tool]:\x1b[0m ");
|
||||
print_content_with_length (core, msg->content, "<no result>", true);
|
||||
free (log_str);
|
||||
free (full_input);
|
||||
free (prompt_text);
|
||||
|
||||
// Don't show the tool call ID as requested
|
||||
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);
|
||||
R_FREE (state->cache_prefix);
|
||||
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 {
|
||||
// Other roles (system, etc.)
|
||||
r_cons_printf (core->cons, "\x1b[1;37m[%s]:\x1b[0m ", role);
|
||||
print_content_with_length (core, msg->content, "<no content>", false);
|
||||
// Just print the result
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
}
|
||||
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
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);
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
/* r2ai - Copyright 2026 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai.claw"
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
#define CLAW_DEFAULT_IDENTITY \
|
||||
"- Name: r2clippy\n" \
|
||||
"- A fun, useful reverse engineering sidekick living inside radare2\n" \
|
||||
"- Speaks short, make jokes, but always goes to the point\n" \
|
||||
"- Loves assembly, expert cracker, eats hex dumps for breakfast and it's smart building clever unix one-liners"
|
||||
|
||||
#define CLAW_DEFAULT_SOUL \
|
||||
"- Useful first, fun second\n" \
|
||||
"- Show, don't preach: prefer concrete r2 commands and runnable examples\n" \
|
||||
"- Curious about every binary, never bored\n" \
|
||||
"- Admit when something is unclear instead of guessing"
|
||||
|
||||
#define CLAW_PROMPT_RULES \
|
||||
"Output between 2 and 5 short markdown bullet points - be flexible, do not pad. " \
|
||||
"Output ONLY the bullets, no preface, no explanation, no code fences."
|
||||
|
||||
typedef struct {
|
||||
const char *rel;
|
||||
const char *tag;
|
||||
const char *def;
|
||||
const char *prompt;
|
||||
} ClawFile;
|
||||
|
||||
static const ClawFile CLAW_FILES[] = {
|
||||
{ ".config/r2ai/IDENTITY.md", "identity", CLAW_DEFAULT_IDENTITY, "Generate an IDENTITY for a fun and useful reverse engineering assistant living in radare2. "
|
||||
"IDENTITY defines WHO the assistant is: name, vibe, voice, style. "
|
||||
"Be invented, surprising and unique each time. " CLAW_PROMPT_RULES },
|
||||
{ ".config/r2ai/SOUL.md", "soul", CLAW_DEFAULT_SOUL, "Generate a SOUL for a fun and useful reverse engineering assistant living in radare2. "
|
||||
"SOUL defines HOW the assistant behaves: values, opinions, rules, character. "
|
||||
"Useful first, fun second; have a clear personal voice. " CLAW_PROMPT_RULES },
|
||||
};
|
||||
#define CLAW_NFILES ((int) (sizeof (CLAW_FILES) / sizeof (CLAW_FILES[0])))
|
||||
|
||||
R_API bool r2ai_claw_exists(void) {
|
||||
int i;
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
char *path = r_file_home (CLAW_FILES[i].rel);
|
||||
bool e = path && r_file_exists (path);
|
||||
free (path);
|
||||
if (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static char *claw_personality(void) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
int i;
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
char *path = r_file_home (CLAW_FILES[i].rel);
|
||||
char *content = path? r_file_slurp (path, NULL): NULL;
|
||||
free (path);
|
||||
r_strbuf_appendf (sb, "\n<%s>\n%s\n</%s>\n", CLAW_FILES[i].tag, content? content: CLAW_FILES[i].def, CLAW_FILES[i].tag);
|
||||
free (content);
|
||||
}
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
R_API char *r2ai_claw_system_prompt(const char *base) {
|
||||
if (!r2ai_claw_exists ()) {
|
||||
return strdup (base? base: "");
|
||||
}
|
||||
char *personality = claw_personality ();
|
||||
char *result = r_str_newf ("%s%s", base? base: "", personality);
|
||||
free (personality);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char *claw_generate(RCorePluginSession *cps, const char *prompt, const char *extra) {
|
||||
char *full = R_STR_ISEMPTY (extra)
|
||||
? strdup (prompt)
|
||||
: r_str_newf ("%s\n\nAdditional hints from the user: %s", prompt, extra);
|
||||
char *err = NULL;
|
||||
R2AIArgs args = {
|
||||
.input = full,
|
||||
.system_prompt = "You are a creative writer crafting concise personality files.",
|
||||
.error = &err,
|
||||
.dorag = false
|
||||
};
|
||||
char *res = r2ai (cps, args);
|
||||
free (full);
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
free (err);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
R_API void r2ai_claw_show(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
int i;
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
char *path = r_file_home (CLAW_FILES[i].rel);
|
||||
char *content = path? r_file_slurp (path, NULL): NULL;
|
||||
if (content) {
|
||||
r_cons_printf (core->cons, "\n=== %s ===\n%s\n", path, content);
|
||||
}
|
||||
free (content);
|
||||
free (path);
|
||||
}
|
||||
}
|
||||
|
||||
R_API void r2ai_claw_create(RCorePluginSession *cps, const char *user_extra) {
|
||||
if (!r2ai_claw_exists ()) {
|
||||
char *cfg = r_file_home (".config/r2ai");
|
||||
r_sys_mkdirp (cfg);
|
||||
free (cfg);
|
||||
int i;
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
const ClawFile *f = &CLAW_FILES[i];
|
||||
R_LOG_INFO ("Generating %s", f->tag);
|
||||
char *text = claw_generate (cps, f->prompt, user_extra);
|
||||
if (R_STR_ISEMPTY (text)) {
|
||||
R_LOG_ERROR ("Failed to generate %s", f->tag);
|
||||
free (text);
|
||||
continue;
|
||||
}
|
||||
char *path = r_file_home (f->rel);
|
||||
if (!path || !r_file_dump (path, (const ut8 *)text, strlen (text), false)) {
|
||||
R_LOG_ERROR ("Cannot write %s", path? path: f->rel);
|
||||
}
|
||||
free (path);
|
||||
free (text);
|
||||
}
|
||||
}
|
||||
r2ai_claw_show (cps);
|
||||
r_cons_printf (cps->core->cons, "\n%s.\n", R2AI_CLAW_HINT);
|
||||
}
|
||||
|
||||
R_API void r2ai_claw_edit(RCorePluginSession *cps) {
|
||||
if (!r2ai_claw_exists ()) {
|
||||
R_LOG_ERROR ("No personality files, run 'r2ai -id' to generate them first");
|
||||
return;
|
||||
}
|
||||
int i;
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
char *path = r_file_home (CLAW_FILES[i].rel);
|
||||
if (path && r_file_exists (path)) {
|
||||
r2ai_cons_editor (cps->core->cons, path, NULL);
|
||||
}
|
||||
free (path);
|
||||
}
|
||||
}
|
||||
|
||||
R_API void r2ai_claw_delete(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
if (!r2ai_claw_exists ()) {
|
||||
r_cons_printf (core->cons, "No personality files to delete.\n");
|
||||
return;
|
||||
}
|
||||
int i;
|
||||
for (i = 0; i < CLAW_NFILES; i++) {
|
||||
char *path = r_file_home (CLAW_FILES[i].rel);
|
||||
if (path && r_file_exists (path) && r_file_rm (path)) {
|
||||
r_cons_printf (core->cons, "Deleted %s\n", path);
|
||||
}
|
||||
free (path);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/* r2ai - Copyright 2023-2026 pancake, dnakov */
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
static void text_parts(PJ *pj, const char *text) {
|
||||
pj_ka (pj, "parts");
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "text", text);
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
}
|
||||
|
||||
// request body shared by the gemini and vertex-gemini providers
|
||||
R_IPI char *r2ai_gemini_request(const R2AIArgs *args) {
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
if (R_STR_ISNOTEMPTY (args->system_prompt)) {
|
||||
pj_ko (pj, "systemInstruction");
|
||||
text_parts (pj, args->system_prompt);
|
||||
pj_end (pj);
|
||||
}
|
||||
pj_ka (pj, "contents");
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args->messages, iter, msg) {
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "role", strcmp (msg->role, "assistant")? "user": "model");
|
||||
text_parts (pj, r_str_get (msg->content));
|
||||
pj_end (pj);
|
||||
}
|
||||
pj_end (pj);
|
||||
pj_ko (pj, "generationConfig");
|
||||
if (args->max_tokens > 0) {
|
||||
pj_kn (pj, "maxOutputTokens", args->max_tokens);
|
||||
}
|
||||
if (args->deterministic) {
|
||||
pj_kn (pj, "temperature", 0);
|
||||
pj_kn (pj, "topP", 1);
|
||||
pj_kn (pj, "topK", 1);
|
||||
} else if (args->temperature > 0) {
|
||||
pj_kd (pj, "temperature", args->temperature);
|
||||
}
|
||||
pj_end (pj);
|
||||
pj_end (pj);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
// parses (and modifies) the json response of the gemini and vertex-gemini providers
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini_parse(char *json, char **error) {
|
||||
RJson *j = r_json_parse (json);
|
||||
if (!j) {
|
||||
*error = strdup ("Failed to parse Gemini response JSON");
|
||||
return NULL;
|
||||
}
|
||||
const RJson *c = r_json_get (j, "candidates");
|
||||
c = c? r_json_item (c, 0): NULL;
|
||||
c = c? r_json_get (c, "content"): NULL;
|
||||
c = c? r_json_get (c, "parts"): NULL;
|
||||
c = c? r_json_item (c, 0): NULL;
|
||||
const char *text = c? r_json_get_str (c, "text"): NULL;
|
||||
if (!text) {
|
||||
r_json_free (j);
|
||||
return NULL;
|
||||
}
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
const RJson *u = r_json_get (j, "usageMetadata");
|
||||
if (u) {
|
||||
usage->prompt_tokens = r_json_get_num (u, "promptTokenCount");
|
||||
usage->completion_tokens = r_json_get_num (u, "candidatesTokenCount");
|
||||
usage->total_tokens = r_json_get_num (u, "totalTokenCount");
|
||||
}
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
message->role = strdup ("assistant");
|
||||
message->content = strdup (text);
|
||||
r_json_free (j);
|
||||
R2AI_ChatResponse *res = R_NEW0 (R2AI_ChatResponse);
|
||||
res->message = message;
|
||||
res->usage = usage;
|
||||
return res;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
const char *model = strstr (args.model, "gemini")? args.model: "gemini-2.0-flash-exp";
|
||||
char *base_url = r2ai_get_provider_url (core, args.provider);
|
||||
char *url = r_str_newf ("%s/models/%s:generateContent", base_url, model);
|
||||
free (base_url);
|
||||
// OAuth tokens start with "ya29.", the key travels in the headers and never in the url
|
||||
const bool oauth = r_str_startswith (args.api_key, "ya29.");
|
||||
char *auth = r_str_newf (oauth? "Authorization: Bearer %s": "x-goog-api-key: %s", args.api_key);
|
||||
const char *headers[] = { "Content-Type: application/json", auth, NULL };
|
||||
char *data = r2ai_gemini_request (&args);
|
||||
int code = 0;
|
||||
char *res = r2ai_post (core, "gemini", url, headers, data, &code, args.error);
|
||||
free (data);
|
||||
free (url);
|
||||
free (auth);
|
||||
R2AI_ChatResponse *result = res? r2ai_gemini_parse (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
+112
-26
@@ -1,6 +1,10 @@
|
||||
#include "r2ai.h"
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#define _GNU_SOURCE
|
||||
// #define _POSIX_C_SOURCE 200809L
|
||||
#include <signal.h>
|
||||
#include <time.h>
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
// Global flag for tracking interrupt status
|
||||
static volatile sig_atomic_t r2ai_http_interrupted = 0;
|
||||
@@ -25,7 +29,7 @@ static R2AI_HttpConfig get_http_config(RCore *core) {
|
||||
return config;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
// Signal handler for timeout (SIGALRM)
|
||||
static void r2ai_http_sigint_handler(int sig) {
|
||||
if (sig == SIGALRM) {
|
||||
@@ -62,6 +66,12 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
// Reset interrupt flag
|
||||
r2ai_http_interrupted = 0;
|
||||
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
// 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;
|
||||
@@ -73,9 +83,7 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
// Check if we were interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
if (result.body) {
|
||||
free (result.body);
|
||||
}
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = -1;
|
||||
break; // Exit the retry loop
|
||||
@@ -102,9 +110,7 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
|
||||
// Check for other failures
|
||||
if (result.code <= 0 || !result.body) {
|
||||
if (result.body) {
|
||||
free (result.body);
|
||||
}
|
||||
free (result.body);
|
||||
result.body = NULL;
|
||||
result.code = -1;
|
||||
if (retry_count < request->config.max_retries) {
|
||||
@@ -123,6 +129,11 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
r_cons_break_pop (core->cons);
|
||||
}
|
||||
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
// Restore SIGWINCH handler
|
||||
sigaction (SIGWINCH, &old_winch_act, NULL);
|
||||
#endif
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -133,11 +144,13 @@ static HttpResponse r2ai_http_request_with_retry(HttpRequestFunc func, const HTT
|
||||
|
||||
static HttpRequestFunc select_backend(const char *backend, bool is_post) {
|
||||
HttpRequestFunc func = NULL;
|
||||
// Select the appropriate backend function
|
||||
if (!strcmp (backend, "auto")) {
|
||||
// Auto-select the best available backend
|
||||
#if defined(_WIN32)
|
||||
backend = "pwsh";
|
||||
#if __wasi__
|
||||
backend = "socket";
|
||||
#elif 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
|
||||
@@ -157,7 +170,7 @@ static HttpRequestFunc select_backend(const char *backend, bool is_post) {
|
||||
} 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 defined(_WIN32)
|
||||
#if R2__WINDOWS__
|
||||
func = is_post? windows_http_post: windows_http_get;
|
||||
#else
|
||||
R_LOG_WARN ("powershell is only available on Windows");
|
||||
@@ -170,33 +183,71 @@ static HttpRequestFunc select_backend(const char *backend, bool is_post) {
|
||||
return func;
|
||||
}
|
||||
|
||||
static const char **custom_headers(RCore *core, const char **headers, char **extra) {
|
||||
const char *cfg = r_config_get (core->config, "r2ai.http.headers");
|
||||
if (R_STR_ISEMPTY (cfg)) {
|
||||
return headers;
|
||||
}
|
||||
*extra = strdup (cfg);
|
||||
int i = 0, j = 0;
|
||||
for (; headers && headers[i]; i++) {
|
||||
}
|
||||
const char **merged = R_NEWS0 (const char *, i + strlen (*extra) + 2);
|
||||
for (; j < i; j++) {
|
||||
merged[j] = headers[j];
|
||||
}
|
||||
char *p = *extra, *q = p;
|
||||
for (; *p; p++) {
|
||||
const bool escaped_n = (*p == '\\' && p[1] == 'n');
|
||||
if (*p != '\n' && !escaped_n) {
|
||||
continue;
|
||||
}
|
||||
*p = 0;
|
||||
if (*q) {
|
||||
merged[i++] = q;
|
||||
}
|
||||
q = p + (escaped_n? 2: 1);
|
||||
if (escaped_n) {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
if (*q) {
|
||||
merged[i] = q;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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);
|
||||
char *extra = NULL;
|
||||
const char **merged = custom_headers (core, headers, &extra);
|
||||
|
||||
HTTPRequest request = {
|
||||
.config = get_http_config (core),
|
||||
.url = url,
|
||||
.data = data,
|
||||
.headers = headers
|
||||
.headers = merged
|
||||
};
|
||||
|
||||
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);
|
||||
if (!func) {
|
||||
R_LOG_ERROR ("Cannot find a valid http backend");
|
||||
}
|
||||
R_LOG_ERROR ("Cannot find a valid http backend");
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
HttpResponse res = func? r2ai_http_request_with_retry (func, &request, core): (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
if (merged != headers) {
|
||||
free (merged);
|
||||
}
|
||||
free (extra);
|
||||
return res;
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[], const char *data, int *code, int *rlen) {
|
||||
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) {
|
||||
if (response.body) {
|
||||
free (response.body);
|
||||
}
|
||||
free (response.body);
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
}
|
||||
@@ -211,11 +262,11 @@ R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[],
|
||||
return response.body;
|
||||
}
|
||||
|
||||
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen) {
|
||||
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) {
|
||||
free (response.body);
|
||||
R_FREE (response.body);
|
||||
}
|
||||
if (code) {
|
||||
*code = response.code;
|
||||
@@ -230,3 +281,38 @@ R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], i
|
||||
}
|
||||
return response.body;
|
||||
}
|
||||
|
||||
static void dump_payload(const char *name, const char *kind, const char *data) {
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *path = r_str_newf ("%s" R_SYS_DIR "r2ai_%s_%s.json", tmpdir, name, kind);
|
||||
r_file_dump (path, (const ut8 *)data, -1, false);
|
||||
R_LOG_DEBUG ("%s %s saved to %s", name, kind, path);
|
||||
free (path);
|
||||
free (tmpdir);
|
||||
}
|
||||
|
||||
// post a provider request, returns the body on success or NULL with the reason in error
|
||||
R_IPI char *r2ai_post(RCore *core, const char *name, const char *url, const char **headers, const char *data, int *code, char **error) {
|
||||
dump_payload (name, "request", data);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
RStrBuf *sb = r_strbuf_new ("curl -X POST");
|
||||
int i;
|
||||
for (i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (sb, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (sb, " -d '%s' \"%s\"", data, url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (sb));
|
||||
r_strbuf_free (sb);
|
||||
}
|
||||
char *res = r2ai_http_post (core, url, headers, data, code, NULL);
|
||||
if (res && *code == 200) {
|
||||
dump_payload (name, "response", res);
|
||||
return res;
|
||||
}
|
||||
if (error) {
|
||||
free (*error);
|
||||
*error = r_str_newf ("%s API error %d%s%s", name, *code, res? ": ": "", r_str_get (res));
|
||||
}
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
+51
-4
@@ -1,5 +1,36 @@
|
||||
static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequest *request, const char *input_data) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
// 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;
|
||||
@@ -7,6 +38,9 @@ static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequ
|
||||
|
||||
// Compose curl command
|
||||
RStrBuf *cmd = r_strbuf_new (cmd_start);
|
||||
if (!cmd) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Add timeout
|
||||
r_strbuf_appendf (cmd, " --connect-timeout %d --max-time %d", 10, timeout);
|
||||
@@ -14,11 +48,24 @@ static HttpResponse build_and_execute_curl(const char *cmd_start, const HTTPRequ
|
||||
// Add headers
|
||||
if (request->headers) {
|
||||
for (int i = 0; request->headers[i] != NULL; i++) {
|
||||
r_strbuf_appendf (cmd, " -H '%s'", request->headers[i]);
|
||||
char *escaped_header = escape_cmd_double_quotes (request->headers[i]);
|
||||
if (escaped_header) {
|
||||
r_strbuf_appendf (cmd, " -H \"%s\"", escaped_header);
|
||||
free (escaped_header);
|
||||
}
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (cmd, " -w '\\n%%{http_code}' '%s'", request->url);
|
||||
char *escaped_url = escape_cmd_double_quotes (request->url);
|
||||
if (!escaped_url) {
|
||||
r_strbuf_free (cmd);
|
||||
return error;
|
||||
}
|
||||
r_strbuf_appendf (cmd, " -w \"\\n%%{http_code}\" \"%s\"", escaped_url);
|
||||
free (escaped_url);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
if (!cmd_str) {
|
||||
return error;
|
||||
}
|
||||
R_LOG_DEBUG ("Running system curl: %s", cmd_str);
|
||||
char *response = r_sys_cmd_str (cmd_str, input_data, NULL);
|
||||
|
||||
|
||||
+15
-31
@@ -36,8 +36,7 @@ static int progress_callback(void *clientp, curl_off_t dltotal, curl_off_t dlnow
|
||||
return r2ai_http_interrupted? 1: 0;
|
||||
}
|
||||
|
||||
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) {
|
||||
HttpResponse error = { .code = -1 };
|
||||
static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post) { HttpResponse error = { .code = -1 };
|
||||
if (!request->url || (is_post && !request->data)) {
|
||||
return error;
|
||||
}
|
||||
@@ -46,14 +45,13 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
CURLcode res;
|
||||
struct curl_slist *curl_headers = NULL;
|
||||
CurlResponse response = { 0 };
|
||||
HttpResponse result = { .code = -1 };
|
||||
|
||||
// Initialize response
|
||||
response.data = malloc (1);
|
||||
if (!response.data) {
|
||||
return error;
|
||||
}
|
||||
response.data[0] = '\0';
|
||||
response.size = 0;
|
||||
|
||||
curl = curl_easy_init ();
|
||||
if (!curl) {
|
||||
@@ -61,10 +59,8 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
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);
|
||||
@@ -73,7 +69,6 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
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]);
|
||||
@@ -81,53 +76,42 @@ static HttpResponse curl_http_request(const HTTPRequest *request, bool is_post)
|
||||
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_CONNECTTIMEOUT, 10L);
|
||||
curl_easy_setopt (curl, CURLOPT_TIMEOUT, (long)request->config.timeout);
|
||||
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;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Get response code
|
||||
long http_code;
|
||||
long http_code = 0;
|
||||
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;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// If we get here, the request was successful
|
||||
// Cleanup
|
||||
result.body = response.data;
|
||||
result.code = (int)http_code;
|
||||
result.length = (int)response.size;
|
||||
response.data = NULL;
|
||||
|
||||
cleanup:
|
||||
free (response.data);
|
||||
curl_slist_free_all (curl_headers);
|
||||
curl_easy_cleanup (curl);
|
||||
|
||||
return (HttpResponse){ .body = response.data, .code = (int)http_code, .length = response.size };
|
||||
return result;
|
||||
}
|
||||
|
||||
HttpResponse curl_http_get(const HTTPRequest *request) {
|
||||
|
||||
+123
-40
@@ -1,58 +1,125 @@
|
||||
#ifdef _WIN32
|
||||
#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, "$ProgressPreference='SilentlyContinue';$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);
|
||||
r_strbuf_appendf (cmd, "'%s'='%s';", key, value);
|
||||
free (key);
|
||||
free (value);
|
||||
}
|
||||
free (header);
|
||||
static void append_headers_to_cmd(RStrBuf *cmd, const char **headers) { r_strbuf_appendf (cmd, "$headers=@{");
|
||||
if (!headers) {
|
||||
r_strbuf_appendf (cmd, "};");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
char *header = strdup (headers[i]);
|
||||
if (!header) {
|
||||
continue;
|
||||
}
|
||||
char *colon = strchr (header, ':');
|
||||
if (!colon) {
|
||||
free (header);
|
||||
continue;
|
||||
}
|
||||
*colon = '\0';
|
||||
char *key = r_str_trim_dup (header);
|
||||
char *value = r_str_trim_dup (colon + 1);
|
||||
char *escaped_key = key ? escape_single_quotes (key) : NULL;
|
||||
char *escaped_value = value ? escape_single_quotes (value) : NULL;
|
||||
if (escaped_key && escaped_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) };
|
||||
}
|
||||
static HttpResponse parse_powershell_response(char *full_response) { if (!full_response) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
return (HttpResponse){ .code = -1 };
|
||||
char *newline = strchr (full_response, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
int code = atoi (full_response);
|
||||
char *body = newline + 1;
|
||||
char *body_copy = strdup (body);
|
||||
if (!body_copy) {
|
||||
free (full_response);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
size_t length = strlen (body);
|
||||
free (full_response);
|
||||
return (HttpResponse){ .body = body_copy, .code = code, .length = length };
|
||||
}
|
||||
return (HttpResponse){ .body = full_response, .code = 200, .length = strlen (full_response) };
|
||||
}
|
||||
/**
|
||||
* Windows-specific HTTP POST using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_post(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
const char *const *headers = request->headers;
|
||||
HttpResponse windows_http_post(const HTTPRequest *request) { const char *url = request->url;
|
||||
const char **headers = request->headers;
|
||||
const char *data = request->data;
|
||||
int timeout = request->config.timeout;
|
||||
if (!url || !data) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
char *escaped_data = escape_single_quotes (data);
|
||||
if (!escaped_url || !escaped_data) {
|
||||
free (escaped_url);
|
||||
free (escaped_data);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
if (!cmd) {
|
||||
free (escaped_url);
|
||||
free (escaped_data);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "$body='%s';", data);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Post -Uri '%s' -Headers $headers -Body $body -TimeoutSec %d;", url, timeout);
|
||||
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}\"");
|
||||
free (escaped_url);
|
||||
free (escaped_data);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
if (!cmd_str) {
|
||||
r_strbuf_free (cmd);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
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);
|
||||
@@ -61,15 +128,31 @@ HttpResponse windows_http_post(const HTTPRequest *request) {
|
||||
/**
|
||||
* Windows-specific HTTP GET using PowerShell
|
||||
*/
|
||||
HttpResponse windows_http_get(const HTTPRequest *request) {
|
||||
const char *url = request->url;
|
||||
const char *const *headers = request->headers;
|
||||
HttpResponse windows_http_get(const HTTPRequest *request) { const char *url = request->url;
|
||||
const char **headers = request->headers;
|
||||
int timeout = request->config.timeout;
|
||||
if (!url) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
char *escaped_url = escape_single_quotes (url);
|
||||
if (!escaped_url) {
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
|
||||
if (!cmd) {
|
||||
free (escaped_url);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
append_headers_to_cmd (cmd, headers);
|
||||
r_strbuf_appendf (cmd, "try{$r=Invoke-WebRequest -Method Get -Uri '%s' -Headers $headers -TimeoutSec %d;", url, timeout);
|
||||
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}\"");
|
||||
free (escaped_url);
|
||||
char *cmd_str = r_strbuf_drain (cmd);
|
||||
if (!cmd_str) {
|
||||
r_strbuf_free (cmd);
|
||||
return (HttpResponse){ .code = -1 };
|
||||
}
|
||||
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);
|
||||
|
||||
+18
-21
@@ -1,57 +1,54 @@
|
||||
extern volatile sig_atomic_t r2ai_http_interrupted;
|
||||
extern void r2ai_http_sigint_handler(int sig);
|
||||
|
||||
// TODO: r_sys_setenv ("R2_CURL", "1"); // Ensure R2 uses system curl
|
||||
|
||||
// Socket implementation with interrupt handling and retry logic
|
||||
static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request) {
|
||||
// Set an alarm to limit the request time
|
||||
#if R2__UNIX__
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
signal (SIGALRM, r2ai_http_sigint_handler);
|
||||
alarm (request->config.timeout); // Use configured timeout
|
||||
#endif
|
||||
|
||||
// Make the request
|
||||
int code = 0;
|
||||
int rlen = 0;
|
||||
char *result = r_socket_http_post (request->url, (const char **)request->headers, request->data, &code, &rlen);
|
||||
|
||||
// Clear the alarm
|
||||
#if R2__UNIX__
|
||||
char *result = r_socket_http_post (request->url, request->headers, request->data, &code, &rlen);
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
alarm (0);
|
||||
#endif
|
||||
|
||||
// Check if we were interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
|
||||
if (!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) {
|
||||
// Set an alarm to limit the request time
|
||||
#if R2__UNIX__
|
||||
#if R2__WINDOWS__
|
||||
r_sys_setenv ("R2_CURL", "1");
|
||||
#endif
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
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__
|
||||
char *result = r_socket_http_get (request->url, request->headers, &code, &rlen);
|
||||
#if R2__UNIX__ && !__wasi__
|
||||
alarm (0);
|
||||
#endif
|
||||
|
||||
// Check if we were interrupted
|
||||
if (r2ai_http_interrupted) {
|
||||
R_LOG_DEBUG ("HTTP request was interrupted by user");
|
||||
free (result);
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return (HttpResponse){ .body = NULL, .code = -1, .length = 0 };
|
||||
}
|
||||
return (HttpResponse){ .body = result, .code = code, .length = rlen };
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
// TODO: move into r2/libr/util/json.c
|
||||
|
||||
static void emit(PJ *pj, const RJson *j) {
|
||||
const RJson *c;
|
||||
switch (j->type) {
|
||||
case R_JSON_STRING:
|
||||
pj_s (pj, j->str_value);
|
||||
break;
|
||||
case R_JSON_INTEGER:
|
||||
if (j->num.dbl_value < 0) {
|
||||
pj_N (pj, j->num.s_value);
|
||||
} else {
|
||||
pj_n (pj, j->num.u_value);
|
||||
}
|
||||
break;
|
||||
case R_JSON_DOUBLE:
|
||||
pj_d (pj, j->num.dbl_value);
|
||||
break;
|
||||
case R_JSON_BOOLEAN:
|
||||
pj_b (pj, j->num.u_value);
|
||||
break;
|
||||
case R_JSON_NULL:
|
||||
pj_null (pj);
|
||||
break;
|
||||
case R_JSON_OBJECT:
|
||||
pj_o (pj);
|
||||
for (c = j->children.first; c; c = c->next) {
|
||||
if (c->key) {
|
||||
pj_k (pj, c->key);
|
||||
emit (pj, c);
|
||||
}
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
case R_JSON_ARRAY:
|
||||
pj_a (pj);
|
||||
for (c = j->children.first; c; c = c->next) {
|
||||
emit (pj, c);
|
||||
}
|
||||
pj_end (pj);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
R_API char *r_json_to_string(const RJson *json) {
|
||||
if (!json) {
|
||||
return NULL;
|
||||
}
|
||||
PJ *pj = pj_new ();
|
||||
emit (pj, json);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "llm"
|
||||
|
||||
#include "r2ai.h"
|
||||
@@ -5,16 +7,24 @@
|
||||
#include <string.h>
|
||||
|
||||
static const R2AIProvider r2ai_providers[] = {
|
||||
{ "openai", "https://api.openai.com/v1", true, false, false, false },
|
||||
{ "gemini", "https://generativelanguage.googleapis.com/v1beta/openai", true, false, false, false },
|
||||
{ "anthropic", "https://api.anthropic.com/v1", true, true, false, false },
|
||||
{ "ollama", "http://localhost:11434/api", false, false, true, true },
|
||||
{ "openapi", "http://127.0.0.1:11434", false, false, false, false },
|
||||
{ "xai", "https://api.x.ai/v1", true, false, false, false },
|
||||
{ "openrouter", "https://openrouter.ai/api/v1", true, false, false, false },
|
||||
{ "groq", "https://api.groq.com/openai/v1", true, false, false, false },
|
||||
{ "mistral", "https://api.mistral.ai/v1", true, false, false, false },
|
||||
{ NULL, NULL, false, false, false, false } // sentinel
|
||||
{ "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/v1", 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 },
|
||||
{ "vertex", NULL, R2AI_API_VERTEX_GEMINI, false, false },
|
||||
{ "vertex-anthropic", NULL, R2AI_API_VERTEX_ANTHROPIC, false, false },
|
||||
{ NULL, NULL, R2AI_API_OPENAI_COMPATIBLE, false, false } // sentinel
|
||||
};
|
||||
|
||||
R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
|
||||
@@ -29,27 +39,66 @@ R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Forward declaration for rawtools
|
||||
static bool is_vertex(const R2AIProvider *p) {
|
||||
return p->api_type == R2AI_API_VERTEX_GEMINI || p->api_type == R2AI_API_VERTEX_ANTHROPIC;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_send(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args) {
|
||||
switch (p->api_type) {
|
||||
case R2AI_API_ANTHROPIC:
|
||||
return r2ai_anthropic (cps, args);
|
||||
case R2AI_API_GEMINI:
|
||||
return r2ai_gemini (cps, args);
|
||||
case R2AI_API_VERTEX_GEMINI:
|
||||
return r2ai_vertex_gemini (cps, args);
|
||||
case R2AI_API_VERTEX_ANTHROPIC:
|
||||
return r2ai_vertex_anthropic (cps, args);
|
||||
default:
|
||||
return r2ai_openai (cps, args);
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_generate_api(RCore *core) {
|
||||
const char *apitype = r_config_get (core->config, "r2ai.apitype");
|
||||
return R_STR_ISNOTEMPTY (apitype) && !strcmp (apitype, "generate");
|
||||
}
|
||||
|
||||
static bool use_rawtools(RCore *core, const R2AIProvider *provider, const R2AIArgs *args) {
|
||||
if (!args || !args->tools || r_list_empty (args->tools)) {
|
||||
return false;
|
||||
}
|
||||
if (r_config_get_b (core->config, "r2ai.auto.raw")) {
|
||||
return true;
|
||||
}
|
||||
return provider && provider->api_type == R2AI_API_OLLAMA && is_generate_api (core);
|
||||
}
|
||||
|
||||
// the given system prompt, or r2ai.system when it is empty
|
||||
R_IPI const char *r2ai_system_prompt(RCore *core, const char *sysp) {
|
||||
return R_STR_ISNOTEMPTY (sysp)? sysp: r_config_get (core->config, "r2ai.system");
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
char *owned_model = NULL;
|
||||
char *owned_provider = NULL;
|
||||
char *owned_system_prompt = NULL;
|
||||
char *api_key = NULL;
|
||||
R2AI_ChatResponse *res = NULL;
|
||||
|
||||
// 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 && args.tools->n_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: "");
|
||||
owned_model = strdup (config_model? config_model: "");
|
||||
args.model = owned_model;
|
||||
}
|
||||
if (!args.provider) {
|
||||
args.provider = strdup (provider);
|
||||
owned_provider = strdup (provider);
|
||||
args.provider = owned_provider;
|
||||
}
|
||||
|
||||
if (!args.max_tokens) {
|
||||
@@ -59,41 +108,48 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
const char *configtemp = r_config_get (core->config, "r2ai.temperature");
|
||||
args.temperature = configtemp? atof (configtemp): 0;
|
||||
}
|
||||
args.deterministic = r_config_get_b (core->config, "r2ai.deterministic");
|
||||
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
if (!prov) {
|
||||
R_LOG_ERROR ("Unknown provider: %s", provider);
|
||||
return NULL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
char *api_key = NULL;
|
||||
if (prov->requires_api_key) {
|
||||
api_key = r2ai_get_api_key (core, provider);
|
||||
if (is_vertex (prov)) {
|
||||
const char *vtoken = r2ai_vertex_get_token (state);
|
||||
if (!vtoken) {
|
||||
goto cleanup;
|
||||
}
|
||||
args.api_key = vtoken;
|
||||
} else 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 %s provider. Please set one with: r2ai "
|
||||
"%s_API_KEY=YOUR_KEY",
|
||||
provider, Provider);
|
||||
free (Provider);
|
||||
return NULL;
|
||||
}
|
||||
if (prov->requires_api_key && R_STR_ISEMPTY (args.api_key)) {
|
||||
R_LOG_ERROR ("No API key found for the %s provider. Use r2ai -K", provider);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// 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");
|
||||
args.system_prompt = r2ai_system_prompt (core, args.system_prompt);
|
||||
int context_pullback = -1;
|
||||
if (use_rawtools (core, prov, &args)) {
|
||||
res = r2ai_rawtools_llmcall (cps, prov, args);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
owned_system_prompt = r2ai_claw_system_prompt (args.system_prompt);
|
||||
args.system_prompt = owned_system_prompt;
|
||||
if (!args.messages) {
|
||||
args.messages = r2ai_msgs_new ();
|
||||
}
|
||||
int context_pullback = -1;
|
||||
if (args.input && args.messages && r_list_empty (args.messages)) {
|
||||
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
|
||||
r2ai_msgs_add (args.messages, &msg);
|
||||
}
|
||||
// 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");
|
||||
@@ -114,30 +170,26 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
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->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);
|
||||
int last = r_list_length (args.messages) - 1;
|
||||
R2AI_Message *last_msg = (last >= 0)? r_list_get_n (args.messages, last): NULL;
|
||||
if (last_msg && last_msg->role && !strcmp (last_msg->role, "user")) {
|
||||
free ((char *)last_msg->content);
|
||||
last_msg->content = m;
|
||||
context_pullback = last;
|
||||
} else {
|
||||
R2AI_Message msg = { .role = "user", .content = m };
|
||||
context_pullback = r_list_length (args.messages);
|
||||
r2ai_msgs_add (args.messages, &msg);
|
||||
free (m);
|
||||
}
|
||||
}
|
||||
|
||||
args.thinking_tokens = r_config_get_i (core->config, "r2ai.thinking_tokens");
|
||||
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (p && p->uses_anthropic_header) {
|
||||
res = r2ai_anthropic (cps, args);
|
||||
} else {
|
||||
res = r2ai_openai (cps, args);
|
||||
}
|
||||
res = r2ai_send (cps, prov, args);
|
||||
finish:
|
||||
if (context_pullback != -1) {
|
||||
R2AI_Message *msg = r_list_get_n (args.messages->messages, context_pullback);
|
||||
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
|
||||
free ((char *)msg->content);
|
||||
msg->content = strdup (args.input);
|
||||
}
|
||||
@@ -147,151 +199,106 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
*args.error = NULL;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
free (api_key);
|
||||
free (owned_model);
|
||||
free (owned_provider);
|
||||
free (owned_system_prompt);
|
||||
return res;
|
||||
}
|
||||
|
||||
static char *r2ai_get_api_key_from_config(const char *provider) {
|
||||
char *config_file = r_file_abspath ("~/.config/r2ai/apikeys.txt");
|
||||
if (!r_file_exists (config_file)) {
|
||||
free (config_file);
|
||||
static char *provider_api_url(const char *host, const char *api_root, bool preserve_root) {
|
||||
char *url = r_str_startswith (host, "http")? strdup (host): r_str_newf ("http://%s", host);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
char *content = r_file_slurp (config_file, NULL);
|
||||
free (config_file);
|
||||
if (!content) {
|
||||
return NULL;
|
||||
r_str_trim (url);
|
||||
size_t len = strlen (url);
|
||||
while (len > 0 && url[len - 1] == '/') {
|
||||
url[--len] = 0;
|
||||
}
|
||||
RList *lines = r_str_split_list (content, "\n", 0);
|
||||
free (content);
|
||||
if (!lines) {
|
||||
return NULL;
|
||||
const bool has_root = r_str_endswith (url, "/v1") || r_str_endswith (url, "/api");
|
||||
if (has_root && (preserve_root || r_str_endswith (url, api_root))) {
|
||||
return url;
|
||||
}
|
||||
char *prefix = r_str_newf ("%s=", provider);
|
||||
char *key = NULL;
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
if (r_str_startswith (line, prefix)) {
|
||||
char *eq = strchr (line, '=');
|
||||
if (eq) {
|
||||
key = strdup (eq + 1);
|
||||
r_str_trim (key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (has_root) {
|
||||
url[len - 3] = 0;
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (prefix);
|
||||
return key;
|
||||
char *res = r_str_newf ("%s%s", url, api_root);
|
||||
free (url);
|
||||
return res;
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from the env, ~/.config/r2ai/apikeys.txt, or ~/.r2ai.<provider>-key
|
||||
* Caller is responsible for freeing the returned string (or NULL if not found). */
|
||||
R_IPI char *r2ai_get_api_key(RCore *core, const char *provider) {
|
||||
(void)core;
|
||||
char *api_key = NULL;
|
||||
char *api_key_env = r_str_newf ("%s_API_KEY", provider);
|
||||
r_str_case (api_key_env, true);
|
||||
char *s = r_sys_getenv (api_key_env);
|
||||
free (api_key_env);
|
||||
if (R_STR_ISNOTEMPTY (s)) {
|
||||
api_key = s;
|
||||
} else {
|
||||
free (s);
|
||||
api_key = r2ai_get_api_key_from_config (provider);
|
||||
if (!api_key) {
|
||||
char *api_key_filename = r_str_newf ("~/.r2ai.%s-key", provider);
|
||||
char *absolute_apikey = r_file_abspath (api_key_filename);
|
||||
if (r_file_exists (absolute_apikey)) {
|
||||
api_key = r_file_slurp (absolute_apikey, NULL);
|
||||
if (api_key) {
|
||||
r_str_trim (api_key);
|
||||
}
|
||||
}
|
||||
free (api_key_filename);
|
||||
free (absolute_apikey);
|
||||
}
|
||||
}
|
||||
return api_key;
|
||||
}
|
||||
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
|
||||
R_IPI 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 (!strcmp (provider, "openai") || !strcmp (provider, "ollama")) {
|
||||
const char *host = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISNOTEMPTY (host)) {
|
||||
if (r_str_startswith (host, "http")) {
|
||||
if (!strcmp (provider, "openai")) {
|
||||
return r_str_newf ("%s/v1", host);
|
||||
}
|
||||
return r_str_newf ("%s/api", host);
|
||||
}
|
||||
if (!strcmp (provider, "openai")) {
|
||||
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) {
|
||||
const char *purl = r2ai_get_provider_url (core, provider);
|
||||
if (!purl) {
|
||||
if (is_vertex (p)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create models endpoint URL
|
||||
const R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
const bool usetags = (prov && prov->uses_tags_endpoint);
|
||||
char *models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
|
||||
const bool is_ollama = p->api_type == R2AI_API_OLLAMA;
|
||||
const bool use_generate = is_ollama && is_generate_api (core);
|
||||
|
||||
// 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_get_api_key (core, provider);
|
||||
}
|
||||
|
||||
int code = 0;
|
||||
char *response = NULL;
|
||||
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->uses_anthropic_header) {
|
||||
// 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;
|
||||
if (p->supports_custom_baseurl) {
|
||||
const char *host = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISNOTEMPTY (host)) {
|
||||
const char *api_root = use_generate? "/api": "/v1";
|
||||
return provider_api_url (host, api_root, is_ollama && !use_generate);
|
||||
}
|
||||
|
||||
// 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);
|
||||
free (api_key);
|
||||
} else {
|
||||
// We have no headers
|
||||
R_LOG_DEBUG ("GET %s", models_url);
|
||||
response = r2ai_http_get (core, models_url, NULL, &code, NULL);
|
||||
}
|
||||
if (use_generate) {
|
||||
return provider_api_url (p->url, "/api", false);
|
||||
}
|
||||
|
||||
return p->url? strdup (p->url): NULL;
|
||||
}
|
||||
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
if (!provider) {
|
||||
return NULL;
|
||||
}
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (!p) {
|
||||
return NULL;
|
||||
}
|
||||
if (is_vertex (p)) {
|
||||
R_LOG_ERROR ("Model listing is not supported for Vertex AI providers");
|
||||
return NULL;
|
||||
}
|
||||
char *purl = r2ai_get_provider_url (core, provider);
|
||||
if (!purl) {
|
||||
return NULL;
|
||||
}
|
||||
const bool gemini = p->api_type == R2AI_API_GEMINI;
|
||||
const bool usetags = p->api_type == R2AI_API_OLLAMA && r_str_endswith (purl, "/api");
|
||||
char *api_key = p->requires_api_key? r2ai_apikeys_get (provider): NULL;
|
||||
if (gemini && !api_key) {
|
||||
free (purl);
|
||||
return NULL;
|
||||
}
|
||||
char *models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
|
||||
char *auth_header = NULL;
|
||||
const char *headers[4] = { "Content-Type: application/json", NULL, NULL, NULL };
|
||||
if (api_key) {
|
||||
if (p->api_type == R2AI_API_ANTHROPIC) {
|
||||
auth_header = r_str_newf ("x-api-key: %s", api_key);
|
||||
headers[2] = "anthropic-version: 2023-06-01";
|
||||
} else if (gemini) {
|
||||
auth_header = r_str_newf ("x-goog-api-key: %s", api_key);
|
||||
} else {
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", api_key);
|
||||
}
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
R_LOG_DEBUG ("GET %s", models_url);
|
||||
int code = 0;
|
||||
char *response = r2ai_http_get (core, models_url, headers, &code, NULL);
|
||||
free (auth_header);
|
||||
free (models_url);
|
||||
free (api_key);
|
||||
free (purl);
|
||||
|
||||
if (!response || code != 200) {
|
||||
R_LOG_DEBUG ("Failed to fetch models from %s (code: %d)", provider, code);
|
||||
@@ -299,38 +306,27 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
|
||||
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 R2AIProvider *prov = r2ai_get_provider (provider);
|
||||
const bool usetags = (prov && prov->uses_tags_endpoint);
|
||||
const RJson *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;
|
||||
if (prov && prov->uses_tags_endpoint) {
|
||||
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)) {
|
||||
R_LOG_DEBUG ("Model: %s", id->str_value);
|
||||
r_list_append (models, strdup (id->str_value));
|
||||
}
|
||||
model_item = model_item->next;
|
||||
const RJson *data = json? r_json_get (json, (gemini || usetags)? "models": "data"): NULL;
|
||||
const char *key = gemini? "name": usetags? "model": "id";
|
||||
const RJson *item;
|
||||
for (item = (data && data->type == R_JSON_ARRAY)? data->children.first: NULL; item; item = item->next) {
|
||||
const char *id = r_json_get_str (item, key);
|
||||
if (R_STR_ISEMPTY (id)) {
|
||||
continue;
|
||||
}
|
||||
if (gemini) {
|
||||
// "models/gemini-1.5-flash" -> "gemini-1.5-flash", skipping non gemini models
|
||||
const char *slash = strrchr (id, '/');
|
||||
id = slash? slash + 1: id;
|
||||
if (!strstr (id, "gemini")) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
r_json_free (json);
|
||||
r_list_append (models, strdup (id));
|
||||
}
|
||||
|
||||
r_json_free (json);
|
||||
free (response);
|
||||
return models;
|
||||
}
|
||||
@@ -349,20 +345,16 @@ R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb) {
|
||||
}
|
||||
|
||||
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");
|
||||
const char *path = r_config_get (cps->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");
|
||||
}
|
||||
RListIter *iter, *iter2;
|
||||
char *file, *line;
|
||||
r_list_foreach (files, iter, file) {
|
||||
if (!r_str_endswith (file, ".txt")) {
|
||||
continue;
|
||||
@@ -370,19 +362,18 @@ R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps) {
|
||||
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);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
RList *lines = r_str_split_list (text, "\n", -1);
|
||||
r_list_foreach (lines, iter2, line) {
|
||||
if (*r_str_trim_head_ro (line)) {
|
||||
r_vdb_insert (state->db, line);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (text);
|
||||
}
|
||||
r_list_free (files);
|
||||
}
|
||||
+292
-59
@@ -1,39 +1,166 @@
|
||||
/* Copyright r2ai - 2023-2025 - pancake */
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
#define CLIPPY "?E"
|
||||
// #define CLIPPY "?EC" // pico le croco
|
||||
|
||||
static void show_help() {
|
||||
printf ("Usage: r2ai [-vhp:m:q:E] <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"
|
||||
" -E Edit the r2ai rc file\n");
|
||||
printf (
|
||||
"Usage: r2ai [-vhdorEwK] [-p provider] [-m model] [-q query]\n"
|
||||
" [-b url] [-c command] [-f file] [-s addr]\n"
|
||||
" [-i script] [-e var=value] [prompt]\n"
|
||||
" -v Show version information\n"
|
||||
" -h Show this help message\n"
|
||||
" -d Decompile current function\n"
|
||||
" -d <query> Ask a question on the current function\n"
|
||||
" -do Decompile current function with source offsets\n"
|
||||
" -dr Decompile current function recursively\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"
|
||||
" -qj List predefined prompt queries as JSON\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"
|
||||
" -s <addr> Seek to address after loading file\n"
|
||||
" -i <script> Load and interpret script file before executing commands\n"
|
||||
" -e <var=value> Set configuration variable (same as r2ai.var)\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 *join_argv(int argc, const char **argv, int start) {
|
||||
if (start >= argc) {
|
||||
return strdup ("");
|
||||
}
|
||||
int i;
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
for (i = start; i < argc; i++) {
|
||||
if (i > start) {
|
||||
r_strbuf_append (sb, " ");
|
||||
}
|
||||
r_strbuf_append (sb, argv[i]);
|
||||
}
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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_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 *seekaddr = NULL;
|
||||
const char *scriptfile = NULL;
|
||||
bool list_queries_json = false;
|
||||
bool decompile = false;
|
||||
bool decompile_recursive = false;
|
||||
bool decompile_offsets = false;
|
||||
RList *conversation = r_list_newf (free);
|
||||
RList *queries = r_list_newf (free);
|
||||
RList *commands = r_list_newf (free);
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
|
||||
RGetopt opt;
|
||||
r_getopt_init (&opt, argc, argv, "vhp:m:q:E");
|
||||
r_getopt_init (&opt, argc, argv, "vhdorp:m:q:Eb:Kc:f:s:i:e:w");
|
||||
while ((c = r_getopt_next (&opt)) != -1) {
|
||||
switch (c) {
|
||||
case 'v':
|
||||
show_version ();
|
||||
r_list_free (queries);
|
||||
return 0;
|
||||
case 'h':
|
||||
show_help ();
|
||||
r_list_free (queries);
|
||||
return 0;
|
||||
case 'd':
|
||||
decompile = true;
|
||||
break;
|
||||
case 'o':
|
||||
if (!decompile) {
|
||||
show_help ();
|
||||
goto beach;
|
||||
}
|
||||
decompile_offsets = true;
|
||||
break;
|
||||
case 'r':
|
||||
if (!decompile) {
|
||||
show_help ();
|
||||
goto beach;
|
||||
}
|
||||
decompile_recursive = true;
|
||||
break;
|
||||
case 'p':
|
||||
provider = opt.arg;
|
||||
break;
|
||||
@@ -41,35 +168,140 @@ int main(int argc, const char **argv) {
|
||||
model = opt.arg;
|
||||
break;
|
||||
case 'q':
|
||||
r_list_append (queries, strdup (opt.arg));
|
||||
if (!strcmp (opt.arg, "j")) {
|
||||
list_queries_json = true;
|
||||
} else {
|
||||
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 's':
|
||||
seekaddr = 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;
|
||||
char *key = r_str_startswith (var, "r2ai")
|
||||
? strdup (var)
|
||||
: r_str_newf ("r2ai.%s", var);
|
||||
r_config_set (core->config, key, value);
|
||||
free (key);
|
||||
} else {
|
||||
r_cons_println (core->cons, "Invalid format for -e. Use var=value.");
|
||||
}
|
||||
free (var_value);
|
||||
}
|
||||
break;
|
||||
case 'E':
|
||||
{
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
r_cons_editor (core->cons, rc_path, NULL);
|
||||
r2ai_cons_editor (core->cons, rc_path, NULL);
|
||||
free (rc_path);
|
||||
r_list_free (queries);
|
||||
r2ai_fini (&cps);
|
||||
r_core_free (core);
|
||||
return 0;
|
||||
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 ();
|
||||
r_list_free (queries);
|
||||
return 1;
|
||||
goto beach;
|
||||
}
|
||||
}
|
||||
|
||||
RCore *core = r_core_new ();
|
||||
RCorePluginSession cps = {
|
||||
.core = core
|
||||
};
|
||||
r2ai_init (&cps);
|
||||
if (list_queries_json) {
|
||||
r2ai_cmd_qj (&cps, NULL);
|
||||
r_cons_flush (core->cons);
|
||||
goto beach;
|
||||
}
|
||||
if (decompile && decompile_offsets && decompile_recursive) {
|
||||
r_cons_println (core->cons, "Cannot combine -do and -dr.");
|
||||
r_cons_flush (core->cons);
|
||||
goto beach;
|
||||
}
|
||||
|
||||
if (baseurl) {
|
||||
r_config_set (core->config, "r2ai.baseurl", baseurl);
|
||||
}
|
||||
if (filename) {
|
||||
char *cmd = r_str_newf ("o %s", filename);
|
||||
r_core_call (core, cmd);
|
||||
free (cmd);
|
||||
// TODO: use the C api instead of the 'o' command and check for error if file exists etc
|
||||
if (seekaddr) {
|
||||
char *seekcmd = r_str_newf ("s %s", seekaddr);
|
||||
r_core_call (core, seekcmd);
|
||||
free (seekcmd);
|
||||
}
|
||||
} 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_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);
|
||||
}
|
||||
if (decompile) {
|
||||
const char *dflag = decompile_offsets? "-do": (decompile_recursive? "-dr": "-d");
|
||||
char *query = join_argv (argc, argv, opt.ind);
|
||||
char *dcmd = R_STR_ISNOTEMPTY (query)? r_str_newf ("%s %s", dflag, query): strdup (dflag);
|
||||
cmd_r2ai (&cps, dcmd);
|
||||
r_cons_flush (core->cons);
|
||||
free (dcmd);
|
||||
free (query);
|
||||
goto beach;
|
||||
}
|
||||
|
||||
// Process queries if any
|
||||
if (!r_list_empty (queries)) {
|
||||
@@ -80,34 +312,35 @@ int main(int argc, const char **argv) {
|
||||
}
|
||||
} else {
|
||||
if (opt.ind >= argc) {
|
||||
// List available prompts
|
||||
r2ai_cmd_q (&cps, "");
|
||||
r_list_free (queries);
|
||||
r2ai_fini (&cps);
|
||||
r_core_free (core);
|
||||
return 0;
|
||||
r2ai_repl (&cps, provider, model, conversation);
|
||||
} else {
|
||||
char *prompt = join_argv (argc, 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);
|
||||
}
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
free (err);
|
||||
}
|
||||
free (prompt);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
+76
-67
@@ -1,3 +1,5 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include <r_core.h>
|
||||
#include <r_cons.h>
|
||||
#include <r_types.h>
|
||||
@@ -14,10 +16,6 @@ typedef enum {
|
||||
STRIKE
|
||||
} MarkdownState;
|
||||
|
||||
// Global theme instance
|
||||
static RMarkdownTheme current_theme;
|
||||
static bool theme_initialized = false;
|
||||
|
||||
// Set default theme values
|
||||
R_API RMarkdownTheme r2ai_markdown_theme_default(void) {
|
||||
RMarkdownTheme theme = {
|
||||
@@ -28,70 +26,77 @@ R_API RMarkdownTheme r2ai_markdown_theme_default(void) {
|
||||
.code_inline = "\x1b[48;5;236m", // Dark gray background for inline code
|
||||
.code_block = "\x1b[48;5;235m", // Slightly darker gray for code blocks
|
||||
// Heading colors
|
||||
.heading1 = "\x1b[1;4;31m", // Bold, underlined, red
|
||||
.heading2 = "\x1b[1;4;32m", // Bold, underlined, green
|
||||
.heading3 = "\x1b[1;4;33m", // Bold, underlined, yellow
|
||||
.heading4 = "\x1b[1;4;34m", // Bold, underlined, blue
|
||||
.heading5 = "\x1b[1;4;35m", // Bold, underlined, magenta
|
||||
.heading6 = "\x1b[1;4;36m", // Bold, underlined, cyan
|
||||
// List items
|
||||
.heading1 = "\x1b[1;4" Color_RED, // Bold, underlined, red
|
||||
.heading2 = "\x1b[1;4" Color_GREEN, // Bold, underlined, green
|
||||
.heading3 = "\x1b[1;4" Color_YELLOW, // Bold, underlined, yellow
|
||||
.heading4 = "\x1b[1;4" Color_BLUE, // Bold, underlined, blue
|
||||
.heading5 = "\x1b[1;4" Color_MAGENTA, // Bold, underlined, magenta
|
||||
.heading6 = "\x1b[1;4" Color_CYAN, // Bold, underlined, cyan
|
||||
// List items
|
||||
.list_bullet = "• ", // Bullet character
|
||||
.list_number = "\x1b[1m%s.\x1b[0m ", // Bold number
|
||||
// Checkbox states
|
||||
.checkbox_checked = "\x1b[32m[✓]\x1b[0m ", // Green checkmark
|
||||
.checkbox_unchecked = "\x1b[90m[ ]\x1b[0m ", // Gray empty box
|
||||
// Reset code
|
||||
.reset = "\x1b[0m"
|
||||
.list_number = "\x1b[1m%s." Color_RESET " ", // Bold number
|
||||
// Checkbox states
|
||||
.checkbox_checked = Color_GREEN "[✓]" Color_RESET " ", // Green checkmark
|
||||
.checkbox_unchecked = Color_GRAY "[ ]" Color_RESET " ", // Gray empty box
|
||||
// Reset code
|
||||
.reset = Color_RESET
|
||||
};
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Initialize theme if not already done
|
||||
static void ensure_theme_initialized(void) {
|
||||
if (!theme_initialized) {
|
||||
current_theme = r2ai_markdown_theme_default ();
|
||||
theme_initialized = true;
|
||||
R_API void r2ai_markdown_init(RMarkdown *md) {
|
||||
if (!md) {
|
||||
return;
|
||||
}
|
||||
if (!md->initialized) {
|
||||
md->theme = r2ai_markdown_theme_default ();
|
||||
md->initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Set a custom theme
|
||||
R_API void r2ai_markdown_set_theme(const RMarkdownTheme *theme) {
|
||||
if (!theme) {
|
||||
current_theme = r2ai_markdown_theme_default ();
|
||||
} else {
|
||||
current_theme = *theme;
|
||||
R_API void r2ai_markdown_set_theme(RMarkdown *md, const RMarkdownTheme *theme) {
|
||||
if (!md) {
|
||||
return;
|
||||
}
|
||||
theme_initialized = true;
|
||||
if (!theme) {
|
||||
md->theme = r2ai_markdown_theme_default ();
|
||||
} else {
|
||||
md->theme = *theme;
|
||||
}
|
||||
md->initialized = true;
|
||||
}
|
||||
|
||||
// Get the current theme
|
||||
R_API const RMarkdownTheme *r2ai_markdown_get_theme(void) {
|
||||
ensure_theme_initialized ();
|
||||
return ¤t_theme;
|
||||
R_API const RMarkdownTheme *r2ai_markdown_get_theme(RMarkdown *md) {
|
||||
if (!md) {
|
||||
return NULL;
|
||||
}
|
||||
r2ai_markdown_init (md);
|
||||
return &md->theme;
|
||||
}
|
||||
|
||||
static void append_formatted(RStrBuf *sb, const char *text, int len, MarkdownState state) {
|
||||
ensure_theme_initialized ();
|
||||
static void append_formatted(RStrBuf *sb, const RMarkdownTheme *theme, const char *text, int len, MarkdownState state) {
|
||||
switch (state) {
|
||||
case BOLD:
|
||||
r_strbuf_append (sb, current_theme.bold);
|
||||
r_strbuf_append (sb, theme->bold);
|
||||
r_strbuf_append_n (sb, text, len);
|
||||
r_strbuf_append (sb, current_theme.reset);
|
||||
r_strbuf_append (sb, theme->reset);
|
||||
break;
|
||||
case ITALIC:
|
||||
r_strbuf_append (sb, current_theme.italic);
|
||||
r_strbuf_append (sb, theme->italic);
|
||||
r_strbuf_append_n (sb, text, len);
|
||||
r_strbuf_append (sb, current_theme.reset);
|
||||
r_strbuf_append (sb, theme->reset);
|
||||
break;
|
||||
case CODE:
|
||||
r_strbuf_append (sb, current_theme.code_inline);
|
||||
r_strbuf_append (sb, theme->code_inline);
|
||||
r_strbuf_append_n (sb, text, len);
|
||||
r_strbuf_append (sb, current_theme.reset);
|
||||
r_strbuf_append (sb, theme->reset);
|
||||
break;
|
||||
case STRIKE:
|
||||
r_strbuf_append (sb, current_theme.strike);
|
||||
r_strbuf_append (sb, theme->strike);
|
||||
r_strbuf_append_n (sb, text, len);
|
||||
r_strbuf_append (sb, current_theme.reset);
|
||||
r_strbuf_append (sb, theme->reset);
|
||||
break;
|
||||
default:
|
||||
r_strbuf_append_n (sb, text, len);
|
||||
@@ -109,12 +114,17 @@ static int count_indent(const char *str) {
|
||||
return count;
|
||||
}
|
||||
|
||||
R_API char *r2ai_markdown(const char *markdown) {
|
||||
R_API char *r2ai_markdown(RMarkdown *md, const char *markdown) {
|
||||
if (!markdown) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ensure_theme_initialized ();
|
||||
RMarkdown fallback = { 0 };
|
||||
if (!md) {
|
||||
md = &fallback;
|
||||
}
|
||||
r2ai_markdown_init (md);
|
||||
const RMarkdownTheme *theme = &md->theme;
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
if (!sb) {
|
||||
return NULL;
|
||||
@@ -134,7 +144,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
in_code_block = !in_code_block;
|
||||
if (in_code_block) {
|
||||
// Add padding for full-width code blocks
|
||||
r_strbuf_appendf (sb, "%s\033[K", current_theme.code_block);
|
||||
r_strbuf_appendf (sb, "%s\033[K", theme->code_block);
|
||||
|
||||
// Skip language specifier if present
|
||||
while (*p && *p != '\n') {
|
||||
@@ -142,7 +152,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
}
|
||||
|
||||
} else {
|
||||
r_strbuf_appendf (sb, "%s\n", current_theme.reset);
|
||||
r_strbuf_appendf (sb, "%s\n", theme->reset);
|
||||
}
|
||||
start = p;
|
||||
continue;
|
||||
@@ -152,7 +162,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
if (*p == '\n') {
|
||||
r_strbuf_append_n (sb, start, p - start);
|
||||
// Add the \033[K (EL - Erase in Line) sequence to extend the background to the end of line
|
||||
r_strbuf_appendf (sb, "\n%s\033[K", current_theme.code_block);
|
||||
r_strbuf_appendf (sb, "\n%s\033[K", theme->code_block);
|
||||
start = p + 1;
|
||||
line_start = 1;
|
||||
}
|
||||
@@ -181,30 +191,30 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
// Add heading with appropriate size
|
||||
switch (level) {
|
||||
case 1:
|
||||
r_strbuf_append (sb, current_theme.heading1);
|
||||
r_strbuf_append (sb, theme->heading1);
|
||||
break;
|
||||
case 2:
|
||||
r_strbuf_append (sb, current_theme.heading2);
|
||||
r_strbuf_append (sb, theme->heading2);
|
||||
break;
|
||||
case 3:
|
||||
r_strbuf_append (sb, current_theme.heading3);
|
||||
r_strbuf_append (sb, theme->heading3);
|
||||
break;
|
||||
case 4:
|
||||
r_strbuf_append (sb, current_theme.heading4);
|
||||
r_strbuf_append (sb, theme->heading4);
|
||||
break;
|
||||
case 5:
|
||||
r_strbuf_append (sb, current_theme.heading5);
|
||||
r_strbuf_append (sb, theme->heading5);
|
||||
break;
|
||||
case 6:
|
||||
r_strbuf_append (sb, current_theme.heading6);
|
||||
r_strbuf_append (sb, theme->heading6);
|
||||
break;
|
||||
default:
|
||||
r_strbuf_append (sb, current_theme.heading1);
|
||||
r_strbuf_append (sb, theme->heading1);
|
||||
break;
|
||||
}
|
||||
|
||||
r_strbuf_append_n (sb, start, heading_end - start);
|
||||
r_strbuf_appendf (sb, "%s\n", current_theme.reset);
|
||||
r_strbuf_appendf (sb, "%s\n", theme->reset);
|
||||
|
||||
if (*heading_end) {
|
||||
p = heading_end + 1;
|
||||
@@ -228,7 +238,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
|
||||
// Skip the indent spaces
|
||||
if (indent_level > 0) {
|
||||
append_formatted (sb, start, p - start, state);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
for (int i = 0; i < indent_level; i++) {
|
||||
r_strbuf_append (sb, " ");
|
||||
}
|
||||
@@ -238,8 +248,8 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
|
||||
// Handle bullet lists
|
||||
if (*p == '-' && p[1] == ' ') {
|
||||
append_formatted (sb, start, p - start, state);
|
||||
r_strbuf_append (sb, current_theme.list_bullet);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
r_strbuf_append (sb, theme->list_bullet);
|
||||
p += 2;
|
||||
start = p;
|
||||
line_start = 0;
|
||||
@@ -254,15 +264,14 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
}
|
||||
|
||||
if (*p == '.' && p[1] == ' ') {
|
||||
append_formatted (sb, start, digit_start - start, state);
|
||||
append_formatted (sb, theme, start, digit_start - start, state);
|
||||
int num_len = p - digit_start;
|
||||
char num_buf[16] = { 0 };
|
||||
r_str_ncpy (num_buf, digit_start, num_len < 15? num_len + 1: 15);
|
||||
|
||||
// Use the actual number from markdown and format it
|
||||
char formatted_number[32];
|
||||
snprintf (formatted_number, sizeof (formatted_number),
|
||||
current_theme.list_number, num_buf);
|
||||
snprintf (formatted_number, sizeof (formatted_number), theme->list_number, num_buf);
|
||||
r_strbuf_append (sb, formatted_number);
|
||||
|
||||
p += 2; // Skip ". "
|
||||
@@ -276,12 +285,12 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
|
||||
// Handle checklists
|
||||
if ((*p == '[' && (p[1] == ' ' || p[1] == 'x') && p[2] == ']' && p[3] == ' ')) {
|
||||
append_formatted (sb, start, p - start, state);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
|
||||
if (p[1] == 'x') {
|
||||
r_strbuf_append (sb, current_theme.checkbox_checked);
|
||||
r_strbuf_append (sb, theme->checkbox_checked);
|
||||
} else {
|
||||
r_strbuf_append (sb, current_theme.checkbox_unchecked);
|
||||
r_strbuf_append (sb, theme->checkbox_unchecked);
|
||||
}
|
||||
|
||||
p += 4; // Skip "[ ] " or "[x] "
|
||||
@@ -300,7 +309,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
if (p > markdown && !isalnum (*(p - 1)) && isalnum (p[1 + double_marker])) {
|
||||
// Opening marker
|
||||
if (p > start) {
|
||||
append_formatted (sb, start, p - start, state);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
}
|
||||
p += double_marker? 2: 1;
|
||||
start = p;
|
||||
@@ -316,7 +325,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
}
|
||||
} else if (p > markdown && isalnum (*(p - 1)) && !isalnum (p[1 + double_marker])) {
|
||||
// Closing marker
|
||||
append_formatted (sb, start, p - start, state);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
p += double_marker? 2: 1;
|
||||
start = p;
|
||||
state = NORMAL;
|
||||
@@ -324,7 +333,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
p++;
|
||||
}
|
||||
} else if (*p == '\n') {
|
||||
append_formatted (sb, start, p - start, state);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
r_strbuf_append (sb, "\n");
|
||||
p++;
|
||||
start = p;
|
||||
@@ -338,7 +347,7 @@ R_API char *r2ai_markdown(const char *markdown) {
|
||||
|
||||
// Handle any remaining text
|
||||
if (p > start) {
|
||||
append_formatted (sb, start, p - start, state);
|
||||
append_formatted (sb, theme, start, p - start, state);
|
||||
}
|
||||
|
||||
char *result = r_strbuf_drain (sb);
|
||||
|
||||
+38
-29
@@ -1,50 +1,59 @@
|
||||
#ifndef R2_MARKDOWN_H
|
||||
#define R2_MARKDOWN_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <r_types.h>
|
||||
|
||||
/* Theme configuration for markdown rendering */
|
||||
typedef struct r_markdown_theme_t {
|
||||
// Text styling
|
||||
const char *bold; // Bold text
|
||||
const char *italic; // Italic text
|
||||
const char *strike; // Strikethrough text
|
||||
const char *code_inline; // Inline code
|
||||
const char *code_block; // Code block background
|
||||
|
||||
// Heading colors (h1-h6)
|
||||
const char *heading1;
|
||||
const char *heading2;
|
||||
const char *heading3;
|
||||
const char *heading4;
|
||||
const char *heading5;
|
||||
const char *heading6;
|
||||
|
||||
// List items
|
||||
const char *list_bullet; // Bullet character for unordered lists
|
||||
const char *list_number; // Number formatting for ordered lists
|
||||
|
||||
// Checkbox states
|
||||
const char *checkbox_checked;
|
||||
const char *checkbox_unchecked;
|
||||
|
||||
// Reset code
|
||||
const char *reset;
|
||||
// Text styling
|
||||
const char *bold; // Bold text
|
||||
const char *italic; // Italic text
|
||||
const char *strike; // Strikethrough text
|
||||
const char *code_inline; // Inline code
|
||||
const char *code_block; // Code block background
|
||||
|
||||
// Heading colors (h1-h6)
|
||||
const char *heading1;
|
||||
const char *heading2;
|
||||
const char *heading3;
|
||||
const char *heading4;
|
||||
const char *heading5;
|
||||
const char *heading6;
|
||||
|
||||
// List items
|
||||
const char *list_bullet; // Bullet character for unordered lists
|
||||
const char *list_number; // Number formatting for ordered lists
|
||||
|
||||
// Checkbox states
|
||||
const char *checkbox_checked;
|
||||
const char *checkbox_unchecked;
|
||||
|
||||
// Reset code
|
||||
const char *reset;
|
||||
} RMarkdownTheme;
|
||||
|
||||
typedef struct r_markdown_t {
|
||||
RMarkdownTheme theme;
|
||||
bool initialized;
|
||||
} RMarkdown;
|
||||
|
||||
/* Default built-in theme */
|
||||
R_API RMarkdownTheme r2ai_markdown_theme_default(void);
|
||||
|
||||
/* Initialize a markdown rendering context with the default theme */
|
||||
R_API void r2ai_markdown_init(RMarkdown *md);
|
||||
|
||||
/* Set a custom theme for markdown rendering */
|
||||
R_API void r2ai_markdown_set_theme(const RMarkdownTheme *theme);
|
||||
R_API void r2ai_markdown_set_theme(RMarkdown *md, const RMarkdownTheme *theme);
|
||||
|
||||
/* Get the current theme */
|
||||
R_API const RMarkdownTheme *r2ai_markdown_get_theme(void);
|
||||
R_API const RMarkdownTheme *r2ai_markdown_get_theme(RMarkdown *md);
|
||||
|
||||
/* Render markdown text for terminal display with ANSI color codes
|
||||
* @param markdown: Input markdown string
|
||||
* @return: Dynamically allocated string with rendered markdown (must be freed by caller)
|
||||
*/
|
||||
R_API char *r2ai_markdown(const char *markdown);
|
||||
R_API char *r2ai_markdown(RMarkdown *md, const char *markdown);
|
||||
|
||||
#endif /* R2_MARKDOWN_H */
|
||||
#endif /* R2_MARKDOWN_H */
|
||||
+67
-190
@@ -1,219 +1,96 @@
|
||||
project('r2ai', 'c',
|
||||
version : '1.2.4',
|
||||
version : '1.5.4',
|
||||
default_options : ['warning_level=2', 'c_std=c11']
|
||||
)
|
||||
|
||||
cc = meson.get_compiler('c')
|
||||
r2_prefix = get_option('r2_prefix')
|
||||
|
||||
# radare2 core dependency: use pkg-config when available. Do not fall back
|
||||
# to CMake-based dependency discovery; instead, if pkg-config fails, try
|
||||
# locating the library under user-provided prefixes (see -Dr2_prefix).
|
||||
r_core_dep = dependency('r_core', method: 'pkg-config', required: false)
|
||||
|
||||
# Centralized resolution results: when we locate `r_core` manually below we
|
||||
# record the library and include search directories in `r2_libdirs` and
|
||||
# `r2_incdirs`. Later lookups for other `r_*` libraries will assume they
|
||||
# live in the same locations, avoiding repeating complex prefix search
|
||||
# logic.
|
||||
r2_libdirs = []
|
||||
r2_incdirs = []
|
||||
|
||||
# If pkg-config didn't yield an r_core dependency, fall back to manual
|
||||
# discovery logic below (search common prefixes / extracted zips).
|
||||
# If pkg-config fails, set up fallback search paths from r2_prefix
|
||||
r_core_dep = dependency('r_core', method: 'pkg-config', required: false)
|
||||
if not r_core_dep.found()
|
||||
# On Windows prefer explicit discovery like other third_party projects do:
|
||||
# 1) ask an installed `radare2` binary for its lib/include/plugin dirs
|
||||
# (via `radare2 -H R2_LIBDIR` / `R2_INCDIR`),
|
||||
# 2) fall back to -Dr2_prefix provided by CI, and
|
||||
# 3) try common locations such as C:\\radare2\\lib and <prefix>/bin.
|
||||
# This avoids Meson's CMake-based dependency fallback which doesn't work
|
||||
# reliably with radare2 release zips on Windows.
|
||||
is_windows_like = host_machine.system() == 'windows' or cc.get_id() == 'msvc' or cc.get_id() == 'clang-cl'
|
||||
r2_libdirs = [
|
||||
join_paths(r2_prefix, 'lib'),
|
||||
join_paths(r2_prefix, 'lib64'),
|
||||
join_paths(r2_prefix, 'radare2', 'lib'),
|
||||
join_paths(r2_prefix, 'radare2', 'lib64'),
|
||||
join_paths(r2_prefix, 'mingw64', 'lib'),
|
||||
join_paths(r2_prefix, 'bin'),
|
||||
]
|
||||
r2_incdirs = [
|
||||
join_paths(r2_prefix, 'include'),
|
||||
join_paths(r2_prefix, 'include', 'libr'),
|
||||
join_paths(r2_prefix, 'include', 'radare2'),
|
||||
join_paths(r2_prefix, 'radare2', 'include'),
|
||||
]
|
||||
endif
|
||||
|
||||
# Simpler, robust Windows behavior mirroring third_party projects:
|
||||
# Try to find r_core by checking a small list of likely library dirs.
|
||||
if is_windows_like
|
||||
r2libdirs = []
|
||||
# prefer explicit prefix if given
|
||||
if get_option('r2_prefix') != ''
|
||||
p = get_option('r2_prefix')
|
||||
r2libdirs += [ join_paths(p, 'lib'), join_paths(p, 'radare2', 'lib') ]
|
||||
endif
|
||||
# repository-local radare2 (used by some subprojects) and conventional path
|
||||
r2libdirs += [ meson.current_source_dir() + '/radare2/lib', 'C:/radare2/lib']
|
||||
# Filter include dirs to only those that exist
|
||||
fs = import('fs')
|
||||
r2_valid_incdirs = []
|
||||
foreach d : r2_incdirs
|
||||
if fs.is_dir(d)
|
||||
r2_valid_incdirs += [d]
|
||||
endif
|
||||
endforeach
|
||||
|
||||
# try to find r_core in the candidate dirs (MSVC import libs or mingw import libs)
|
||||
rcore_lib = cc.find_library('r_core', dirs: r2libdirs, required: false)
|
||||
if not rcore_lib.found()
|
||||
rcore_lib = cc.find_library('libr_core', dirs: r2libdirs, required: false)
|
||||
endif
|
||||
|
||||
if rcore_lib.found()
|
||||
# basic include candidates
|
||||
incdirs = []
|
||||
if get_option('r2_prefix') != ''
|
||||
p = get_option('r2_prefix')
|
||||
incdirs += [ join_paths(p, 'include', 'libr'), join_paths(p, 'include') ]
|
||||
endif
|
||||
rcore_inc = include_directories(incdirs)
|
||||
r_core_dep = declare_dependency(dependencies: [rcore_lib], include_directories: rcore_inc)
|
||||
# Remember resolved dirs for other r_* lookups
|
||||
r2_libdirs = r2libdirs
|
||||
r2_incdirs = incdirs
|
||||
else
|
||||
error('Windows dependency lookup for r_core failed: ensure radare2 is installed and pass -Dr2_prefix=<prefix> (e.g. C:/radare2)')
|
||||
endif
|
||||
else
|
||||
# Non-Windows: try locating radare2 from environment-provided prefixes. This
|
||||
# helps CI on Unix where pkg-config may not be installed.
|
||||
r2_prefixes = []
|
||||
if get_option('r2_prefix') != ''
|
||||
r2_prefixes += [get_option('r2_prefix')]
|
||||
endif
|
||||
|
||||
found = false
|
||||
foreach p : r2_prefixes
|
||||
# Common library/include locations under a provided prefix.
|
||||
libdirs = [
|
||||
join_paths(p, 'lib'),
|
||||
join_paths(p, 'lib64'),
|
||||
join_paths(p, 'radare2', 'lib'),
|
||||
join_paths(p, 'radare2', 'lib64'),
|
||||
join_paths(p, 'mingw64', 'lib'),
|
||||
]
|
||||
incdirs = [
|
||||
join_paths(p, 'include'),
|
||||
join_paths(p, 'radare2', 'include'),
|
||||
join_paths(p, 'include', 'radare2'),
|
||||
]
|
||||
extra_libdirs = libdirs + [ join_paths(p, 'bin') ]
|
||||
candidate_names = [ 'r_core', 'r_core-6', 'r_core6', 'r_core64', 'libr_core' ]
|
||||
foreach name : candidate_names
|
||||
rcore_lib = cc.find_library(name, dirs: extra_libdirs, required: false)
|
||||
if rcore_lib.found()
|
||||
rcore_inc = include_directories(incdirs + [ join_paths(p, 'include', 'libr') ])
|
||||
r_core_dep = declare_dependency(dependencies: [rcore_lib], include_directories: rcore_inc)
|
||||
# Record the successful prefix and candidate lib/include dirs so
|
||||
# other r_* dependencies can be looked up in the same locations.
|
||||
r2_libdirs = extra_libdirs
|
||||
r2_incdirs = incdirs + [ join_paths(p, 'include', 'libr') ]
|
||||
found = true
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
if found
|
||||
# Resolve all radare2 dependencies: pkg-config first, then manual search
|
||||
r2_deps = []
|
||||
foreach name : ['r_core', 'r_util', 'r_cons', 'r_config', 'r_socket']
|
||||
dep = dependency(name, method: 'pkg-config', required: false)
|
||||
if not dep.found() and r2_libdirs.length() > 0
|
||||
foreach libname : [name, 'lib' + name]
|
||||
lib = cc.find_library(libname, dirs: r2_libdirs, required: false)
|
||||
if lib.found()
|
||||
dep = declare_dependency(
|
||||
dependencies: [lib],
|
||||
include_directories: include_directories(r2_valid_incdirs),
|
||||
)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
|
||||
if not found
|
||||
error('Dependency lookup for r_core failed: install radare2 development files or provide pkg-config paths or set the meson option -Dr2_prefix=<prefix>')
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
if name == 'r_core' and not dep.found()
|
||||
error('r_core not found: install radare2 dev files, set PKG_CONFIG_PATH, or use -Dr2_prefix=<prefix>')
|
||||
endif
|
||||
r2_deps += [dep]
|
||||
endforeach
|
||||
|
||||
# Resolve other r_* dependencies assuming same paths as r_core
|
||||
r_util_dep = dependency('r_util', method: 'pkg-config', required: false)
|
||||
if not r_util_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_util', 'libr_util']
|
||||
ulib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if ulib.found()
|
||||
uinc = include_directories(r2_incdirs)
|
||||
r_util_dep = declare_dependency(dependencies: [ulib], include_directories: uinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_util_dep.found()
|
||||
warning('r_util dependency not found via pkg-config or same paths as r_core; build may fail on systems without radare2 dev files')
|
||||
endif
|
||||
|
||||
r_cons_dep = dependency('r_cons', method: 'pkg-config', required: false)
|
||||
if not r_cons_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_cons', 'libr_cons']
|
||||
clib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if clib.found()
|
||||
cinc = include_directories(r2_incdirs)
|
||||
r_cons_dep = declare_dependency(dependencies: [clib], include_directories: cinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_cons_dep.found()
|
||||
warning('r_cons dependency not found via pkg-config or same paths as r_core; console helpers may be unavailable')
|
||||
endif
|
||||
|
||||
r_config_dep = dependency('r_config', method: 'pkg-config', required: false)
|
||||
if not r_config_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_config', 'libr_config']
|
||||
clib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if clib.found()
|
||||
cinc = include_directories(r2_incdirs)
|
||||
r_config_dep = declare_dependency(dependencies: [clib], include_directories: cinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_config_dep.found()
|
||||
warning('r_config dependency not found via pkg-config or same paths as r_core; console helpers may be unavailable')
|
||||
endif
|
||||
|
||||
r_socket_dep = dependency('r_socket', method: 'pkg-config', required: false)
|
||||
if not r_socket_dep.found() and r2_libdirs.length() > 0
|
||||
foreach name : ['r_socket', 'libr_socket']
|
||||
slib = cc.find_library(name, dirs: r2_libdirs, required: false)
|
||||
if slib.found()
|
||||
sinc = include_directories(r2_incdirs)
|
||||
r_socket_dep = declare_dependency(dependencies: [slib], include_directories: sinc)
|
||||
break
|
||||
endif
|
||||
endforeach
|
||||
endif
|
||||
if not r_socket_dep.found()
|
||||
warning('r_socket dependency not found via pkg-config or same paths as r_core; socket helpers may be unavailable')
|
||||
endif
|
||||
|
||||
# Math library dependency (needed for log, sqrt, etc.)
|
||||
m_dep = cc.find_library('m', required: false)
|
||||
|
||||
# Common compile flags similar to the Makefile
|
||||
c_args = []
|
||||
if cc.get_id() == 'gcc' or cc.get_id() == 'clang'
|
||||
c_args += ['-Wall', '-g', '-fPIC']
|
||||
elif cc.get_id() == 'msvc'
|
||||
c_args += ['/Zi']
|
||||
endif
|
||||
common_deps = r2_deps + [cc.find_library('m', required: false)]
|
||||
|
||||
srcs = files(
|
||||
'r2ai.c',
|
||||
'auto.c',
|
||||
'vdb.c',
|
||||
'llm.c',
|
||||
'tools.c',
|
||||
'query.c',
|
||||
'rawtools.c',
|
||||
'messages.c',
|
||||
'anthropic.c',
|
||||
'async.c',
|
||||
'auto.c',
|
||||
'apikeys.c',
|
||||
'claw.c',
|
||||
'gemini.c',
|
||||
'vertex.c',
|
||||
'http.c',
|
||||
'openai.c',
|
||||
'json.c',
|
||||
'wizard.c',
|
||||
'llm.c',
|
||||
'markdown.c',
|
||||
'messages.c',
|
||||
'openai.c',
|
||||
'query.c',
|
||||
'r2ai.c',
|
||||
'rawtools.c',
|
||||
'tools.c',
|
||||
'vdb.c',
|
||||
)
|
||||
|
||||
# Shared library (plugin)
|
||||
shared_lib = shared_library(
|
||||
'r2ai',
|
||||
srcs,
|
||||
r2ai_lib = library('r2ai', srcs,
|
||||
install: true,
|
||||
c_args: c_args,
|
||||
dependencies: [r_core_dep, r_util_dep, r_cons_dep, r_config_dep, r_socket_dep, m_dep]
|
||||
dependencies: common_deps,
|
||||
pic: true,
|
||||
)
|
||||
|
||||
# Command-line executable (links the same objects plus main.c)
|
||||
exe = executable(
|
||||
'r2ai',
|
||||
[ 'main.c' ],
|
||||
link_with: shared_lib,
|
||||
executable('r2ai', 'main.c',
|
||||
link_with: r2ai_lib,
|
||||
install: true,
|
||||
c_args: c_args,
|
||||
dependencies: [r_core_dep, r_util_dep, r_cons_dep, r_config_dep, r_socket_dep, m_dep]
|
||||
dependencies: common_deps,
|
||||
)
|
||||
+149
-526
@@ -1,354 +1,133 @@
|
||||
/* r2ai - Copyright 2023-2025 dnakov, pancake */
|
||||
/* r2ai - Copyright 2023-2026 dnakov, pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
#define INITIAL_CAPACITY 8
|
||||
#define GROWTH_FACTOR 1.5
|
||||
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc) {
|
||||
if (tc) {
|
||||
free ((void *)tc->id);
|
||||
free ((void *)tc->name);
|
||||
free ((void *)tc->arguments);
|
||||
free (tc);
|
||||
}
|
||||
}
|
||||
|
||||
R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
static void block_free(R2AI_ContentBlock *b) {
|
||||
free (b->type);
|
||||
free (b->id);
|
||||
free (b->name);
|
||||
free (b->input);
|
||||
free (b->data);
|
||||
free (b->thinking);
|
||||
free (b->signature);
|
||||
free (b->text);
|
||||
free (b);
|
||||
}
|
||||
|
||||
static R2AI_ContentBlock *block_dup(const R2AI_ContentBlock *b) {
|
||||
R2AI_ContentBlock *d = R_NEW0 (R2AI_ContentBlock);
|
||||
d->type = r_str_new (b->type);
|
||||
d->id = r_str_new (b->id);
|
||||
d->name = r_str_new (b->name);
|
||||
d->input = r_str_new (b->input);
|
||||
d->data = r_str_new (b->data);
|
||||
d->thinking = r_str_new (b->thinking);
|
||||
d->signature = r_str_new (b->signature);
|
||||
d->text = r_str_new (b->text);
|
||||
return d;
|
||||
}
|
||||
|
||||
static R2AI_ToolCall *tc_dup(const R2AI_ToolCall *tc) {
|
||||
R2AI_ToolCall *d = R_NEW0 (R2AI_ToolCall);
|
||||
d->id = r_str_new (tc->id);
|
||||
d->name = r_str_new (tc->name);
|
||||
d->arguments = r_str_new (tc->arguments);
|
||||
return d;
|
||||
}
|
||||
|
||||
R_API RList *r2ai_content_blocks_new(void) {
|
||||
return r_list_newf ((RListFree)block_free);
|
||||
}
|
||||
|
||||
R_API void r2ai_message_fini(R2AI_Message *msg) {
|
||||
if (!msg) {
|
||||
return;
|
||||
}
|
||||
|
||||
free (msg->role);
|
||||
free ((void *)msg->content); // maybe double free
|
||||
free ((void *)msg->content);
|
||||
free (msg->reasoning_content);
|
||||
free (msg->tool_call_id);
|
||||
|
||||
// Free tool calls
|
||||
if (msg->tool_calls) {
|
||||
for (int i = 0; i < msg->n_tool_calls; i++) {
|
||||
free ((void *)msg->tool_calls[i].id);
|
||||
free ((void *)msg->tool_calls[i].name);
|
||||
free ((void *)msg->tool_calls[i].arguments);
|
||||
}
|
||||
free ((void *)msg->tool_calls);
|
||||
}
|
||||
|
||||
if (msg->content_blocks) {
|
||||
for (int i = 0; i < msg->content_blocks->n_blocks; i++) {
|
||||
R2AI_ContentBlock *block = &msg->content_blocks->blocks[i];
|
||||
free (block->type);
|
||||
free (block->data);
|
||||
free (block->thinking);
|
||||
free (block->signature);
|
||||
free (block->text);
|
||||
free (block->id);
|
||||
free (block->name);
|
||||
free (block->input);
|
||||
}
|
||||
free (msg->content_blocks->blocks);
|
||||
free (msg->content_blocks);
|
||||
}
|
||||
r_list_free (msg->tool_calls);
|
||||
r_list_free (msg->content_blocks);
|
||||
memset (msg, 0, sizeof (*msg));
|
||||
}
|
||||
|
||||
// Conversation is now stored in R2AI_State
|
||||
R_API void r2ai_message_free(R2AI_Message *msg) {
|
||||
r2ai_message_fini (msg);
|
||||
free (msg);
|
||||
}
|
||||
|
||||
R_API void r2ai_conversation_init(R2AI_State *state) {
|
||||
if (!state || state->conversation) {
|
||||
// Already initialized or invalid state
|
||||
R_IPI void r2ai_chat_response_free(R2AI_ChatResponse *res) {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
r2ai_message_free ((R2AI_Message *)res->message);
|
||||
free ((void *)res->usage);
|
||||
free (res);
|
||||
}
|
||||
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state) {
|
||||
R_API void r2ai_conversation_init(R2AI_State *state) {
|
||||
if (state && !state->conversation) {
|
||||
state->conversation = r2ai_msgs_new ();
|
||||
}
|
||||
}
|
||||
|
||||
R_API RList *r2ai_conversation_get(R2AI_State *state) {
|
||||
return state? state->conversation: NULL;
|
||||
}
|
||||
|
||||
// Create a new temporary messages container
|
||||
R_API R2AI_Messages *r2ai_msgs_new(void) {
|
||||
R2AI_Messages *msgs = R_NEW0 (R2AI_Messages);
|
||||
if (!msgs) {
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages = r_list_new ();
|
||||
if (!msgs->messages) {
|
||||
R_FREE (msgs);
|
||||
return NULL;
|
||||
}
|
||||
msgs->messages->free = (RListFree)r2ai_message_free;
|
||||
return msgs;
|
||||
R_API RList *r2ai_msgs_new(void) {
|
||||
return r_list_newf ((RListFree)r2ai_message_free);
|
||||
}
|
||||
|
||||
R_API void r2ai_msgs_free(R2AI_Messages *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal free for non-static instances
|
||||
if (msgs->messages) {
|
||||
r_list_free (msgs->messages);
|
||||
}
|
||||
|
||||
R_FREE (msgs);
|
||||
R_API void r2ai_msgs_free(RList *msgs) {
|
||||
r_list_free (msgs);
|
||||
}
|
||||
|
||||
// Free the conversation when plugin is unloaded
|
||||
R_API void r2ai_conversation_free(R2AI_State *state) {
|
||||
if (state && state->conversation) {
|
||||
r2ai_msgs_free (state->conversation);
|
||||
if (state) {
|
||||
r_list_free (state->conversation);
|
||||
state->conversation = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear messages in a container without freeing the container itself
|
||||
R_API void r2ai_msgs_clear(R2AI_Messages *msgs) {
|
||||
if (!msgs) {
|
||||
return;
|
||||
}
|
||||
|
||||
r_list_purge (msgs->messages);
|
||||
R_API void r2ai_msgs_clear(RList *msgs) {
|
||||
r_list_purge (msgs);
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg) {
|
||||
// append a deep copy of the message
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg) {
|
||||
if (!msgs || !msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *new_msg = R_NEW0 (R2AI_Message);
|
||||
if (!new_msg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
new_msg->role = msg->role? strdup (msg->role): NULL;
|
||||
new_msg->content = msg->content? strdup (msg->content): NULL;
|
||||
new_msg->reasoning_content = msg->reasoning_content? strdup (msg->reasoning_content): NULL;
|
||||
|
||||
R2AI_Message *m = R_NEW0 (R2AI_Message);
|
||||
m->role = r_str_new (msg->role);
|
||||
m->content = r_str_new (msg->content);
|
||||
m->reasoning_content = r_str_new (msg->reasoning_content);
|
||||
m->tool_call_id = r_str_new (msg->tool_call_id);
|
||||
if (msg->content_blocks) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
if (!cb) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = msg->content_blocks->n_blocks;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
R2AI_ContentBlock *src = &msg->content_blocks->blocks[i];
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
dst->type = src->type? strdup (src->type): NULL;
|
||||
dst->data = src->data? strdup (src->data): NULL;
|
||||
dst->thinking = src->thinking? strdup (src->thinking): NULL;
|
||||
dst->signature = src->signature? strdup (src->signature): NULL;
|
||||
dst->text = src->text? strdup (src->text): NULL;
|
||||
dst->id = src->id? strdup (src->id): NULL;
|
||||
dst->name = src->name? strdup (src->name): NULL;
|
||||
dst->input = src->input? strdup (src->input): NULL;
|
||||
}
|
||||
new_msg->content_blocks = cb;
|
||||
m->content_blocks = r_list_clone (msg->content_blocks, (RListClone)block_dup);
|
||||
m->content_blocks->free = (RListFree)block_free;
|
||||
}
|
||||
|
||||
new_msg->tool_call_id = msg->tool_call_id? strdup (msg->tool_call_id): NULL;
|
||||
new_msg->tool_calls = NULL;
|
||||
new_msg->n_tool_calls = 0;
|
||||
|
||||
// Copy tool calls if any
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
new_msg->tool_calls = R_NEWS0 (R2AI_ToolCall, msg->n_tool_calls);
|
||||
if (!new_msg->tool_calls) {
|
||||
r2ai_message_free (new_msg);
|
||||
free (new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
new_msg->n_tool_calls = msg->n_tool_calls;
|
||||
for (int i = 0; i < msg->n_tool_calls; i++) {
|
||||
const R2AI_ToolCall *src_tc = &msg->tool_calls[i];
|
||||
R2AI_ToolCall *dst_tc = (R2AI_ToolCall *)&new_msg->tool_calls[i];
|
||||
|
||||
dst_tc->name = src_tc->name? strdup (src_tc->name): NULL;
|
||||
dst_tc->arguments = src_tc->arguments? strdup (src_tc->arguments): NULL;
|
||||
dst_tc->id = src_tc->id? strdup (src_tc->id): NULL;
|
||||
}
|
||||
}
|
||||
|
||||
r_list_append (msgs->messages, new_msg);
|
||||
m->tool_calls = msg->tool_calls
|
||||
? r_list_clone (msg->tool_calls, (RListClone)tc_dup)
|
||||
: r_list_new ();
|
||||
m->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
r_list_append (msgs, m);
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc) {
|
||||
if (!msgs || !tc || r_list_length (msgs->messages) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
R2AI_Message *msg = r_list_get_n (msgs->messages, r_list_length (msgs->messages) - 1);
|
||||
|
||||
// Allocate or resize the tool_calls array
|
||||
if (msg->n_tool_calls == 0) {
|
||||
msg->tool_calls = R_NEWS0 (R2AI_ToolCall, 1);
|
||||
if (!msg->tool_calls) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
R2AI_ToolCall *new_tool_calls = realloc (
|
||||
(void *)msg->tool_calls,
|
||||
sizeof (R2AI_ToolCall) *(msg->n_tool_calls + 1));
|
||||
if (!new_tool_calls) {
|
||||
return false;
|
||||
}
|
||||
msg->tool_calls = new_tool_calls;
|
||||
// Zero the new element
|
||||
memset ((void *)&msg->tool_calls[msg->n_tool_calls], 0, sizeof (R2AI_ToolCall));
|
||||
}
|
||||
|
||||
// Copy the tool call
|
||||
R2AI_ToolCall *dst_tc = (R2AI_ToolCall *)&msg->tool_calls[msg->n_tool_calls];
|
||||
dst_tc->name = tc->name? strdup (tc->name): NULL;
|
||||
dst_tc->arguments = tc->arguments? strdup (tc->arguments): NULL;
|
||||
dst_tc->id = tc->id? strdup (tc->id): NULL;
|
||||
|
||||
msg->n_tool_calls++;
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_response(R2AI_Messages *msgs, const char *json_str) {
|
||||
if (!msgs || !json_str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// r_json_parse expects non-const char*, so we need to cast it
|
||||
RJson *json = r_json_parse ((char *)json_str);
|
||||
if (!json) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = r2ai_msgs_from_json (msgs, json);
|
||||
r_json_free (json);
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json) {
|
||||
if (!msgs || !json) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *choices = r_json_get (json, "choices");
|
||||
if (!choices || choices->type != R_JSON_ARRAY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *choice = r_json_item (choices, 0);
|
||||
if (!choice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *message = r_json_get (choice, "message");
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RJson *role = r_json_get (message, "role");
|
||||
const RJson *content = r_json_get (message, "content");
|
||||
const RJson *content_blocks = r_json_get (message, "content_blocks");
|
||||
|
||||
// Create a new message to add
|
||||
R2AI_Message new_msg = { 0 };
|
||||
new_msg.role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
|
||||
new_msg.content = (content && content->type == R_JSON_STRING)? strdup (content->str_value): NULL;
|
||||
new_msg.tool_call_id = NULL;
|
||||
new_msg.tool_calls = NULL;
|
||||
new_msg.n_tool_calls = 0;
|
||||
|
||||
if (content_blocks && content_blocks->type == R_JSON_ARRAY && content_blocks->children.count > 0) {
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
if (!cb) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
cb->n_blocks = content_blocks->children.count;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < cb->n_blocks; i++) {
|
||||
const RJson *block = r_json_item (content_blocks, i);
|
||||
if (!block) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ContentBlock *dst = &cb->blocks[i];
|
||||
const RJson *type = r_json_get (block, "type");
|
||||
const RJson *data = r_json_get (block, "data");
|
||||
const RJson *thinking = r_json_get (block, "thinking");
|
||||
const RJson *signature = r_json_get (block, "signature");
|
||||
const RJson *text = r_json_get (block, "text");
|
||||
const RJson *id = r_json_get (block, "id");
|
||||
const RJson *name = r_json_get (block, "name");
|
||||
const RJson *input = r_json_get (block, "input");
|
||||
|
||||
dst->type = (type && type->type == R_JSON_STRING)? strdup (type->str_value): NULL;
|
||||
dst->data = (data && data->type == R_JSON_STRING)? strdup (data->str_value): NULL;
|
||||
dst->thinking = (thinking && thinking->type == R_JSON_STRING)? strdup (thinking->str_value): NULL;
|
||||
dst->signature = (signature && signature->type == R_JSON_STRING)? strdup (signature->str_value): NULL;
|
||||
dst->text = (text && text->type == R_JSON_STRING)? strdup (text->str_value): NULL;
|
||||
dst->id = (id && id->type == R_JSON_STRING)? strdup (id->str_value): NULL;
|
||||
dst->name = (name && name->type == R_JSON_STRING)? strdup (name->str_value): NULL;
|
||||
dst->input = (input && input->type == R_JSON_STRING)? strdup (input->str_value): NULL;
|
||||
}
|
||||
new_msg.content_blocks = cb;
|
||||
}
|
||||
|
||||
// Add the message without tool calls first
|
||||
if (!r2ai_msgs_add (msgs, &new_msg)) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now add tool calls if present
|
||||
const RJson *tool_calls = r_json_get (message, "tool_calls");
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY) {
|
||||
// Iterate through array elements
|
||||
for (size_t i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const RJson *id = r_json_get (tool_call, "id");
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
if (!function) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const RJson *name = r_json_get (function, "name");
|
||||
const RJson *arguments = r_json_get (function, "arguments");
|
||||
|
||||
R2AI_ToolCall tc = { 0 };
|
||||
tc.name = (name && name->type == R_JSON_STRING)? name->str_value: NULL;
|
||||
tc.arguments = (arguments && arguments->type == R_JSON_STRING)? arguments->str_value: NULL;
|
||||
tc.id = (id && id->type == R_JSON_STRING)? id->str_value: NULL;
|
||||
|
||||
if (!r2ai_msgs_add_tool_call (msgs, &tc)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the parsed message to the messages array
|
||||
if (!r2ai_msgs_add (msgs, &new_msg)) {
|
||||
r2ai_message_free (&new_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Free the temporary message struct (strings are duplicated in add)
|
||||
r2ai_message_free (&new_msg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
R_API char *r2ai_msgs_to_json(const RList *msgs, bool raw_tool_args) {
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -359,16 +138,22 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
|
||||
pj_o (pj); // Start message object
|
||||
|
||||
// Add role
|
||||
pj_ks (pj, "role", msg->role? msg->role: "user");
|
||||
|
||||
if (msg->content) {
|
||||
// Content is required for OpenAI API
|
||||
if (msg->content && *msg->content) {
|
||||
pj_ks (pj, "content", msg->content);
|
||||
} else if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_knull (pj, "content");
|
||||
} else {
|
||||
pj_ks (pj, "content", msg->content? msg->content: "");
|
||||
}
|
||||
|
||||
if (msg->reasoning_content) {
|
||||
@@ -381,13 +166,13 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
// Add tool_calls if present
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
pj_k (pj, "tool_calls");
|
||||
pj_a (pj); // Start tool_calls array
|
||||
|
||||
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); // Start tool call object
|
||||
|
||||
// Add id if present
|
||||
@@ -405,9 +190,13 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
// Add name
|
||||
pj_ks (pj, "name", tc->name? tc->name: "");
|
||||
|
||||
// Add arguments if present
|
||||
if (tc->arguments) {
|
||||
pj_ks (pj, "arguments", tc->arguments);
|
||||
// OpenAI wants arguments as a JSON-encoded string; Ollama wants a raw object.
|
||||
const char *a = R_STR_ISEMPTY (tc->arguments)? "{}": tc->arguments;
|
||||
pj_k (pj, "arguments");
|
||||
if (raw_tool_args) {
|
||||
pj_raw (pj, a);
|
||||
} else {
|
||||
pj_s (pj, a);
|
||||
}
|
||||
|
||||
pj_end (pj); // End function object
|
||||
@@ -426,8 +215,8 @@ R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs) {
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
if (!msgs || r_list_length (msgs->messages) == 0) {
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs) {
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -438,19 +227,24 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < r_list_length (msgs->messages); i++) {
|
||||
const R2AI_Message *msg = r_list_get_n (msgs->messages, i);
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
pj_o (pj); // Start message object
|
||||
|
||||
// Add role
|
||||
// Circumvent Anthropic's allergy to system role messages
|
||||
const char *role = msg->role? msg->role: "user";
|
||||
bool is_system_message = !strcmp (role, "system");
|
||||
if (is_system_message) {
|
||||
role = "user";
|
||||
}
|
||||
pj_ks (pj, "role", strcmp (role, "tool") == 0? "user": role);
|
||||
|
||||
if (msg->content_blocks) {
|
||||
pj_ka (pj, "content"); // Start content array
|
||||
for (int j = 0; j < msg->content_blocks->n_blocks; j++) {
|
||||
const R2AI_ContentBlock *block = &msg->content_blocks->blocks[j];
|
||||
RListIter *iter;
|
||||
R2AI_ContentBlock *block;
|
||||
r_list_foreach (msg->content_blocks, iter, block) {
|
||||
pj_o (pj); // Start content block object
|
||||
if (R_STR_ISNOTEMPTY (block->type)) {
|
||||
pj_ks (pj, "type", block->type);
|
||||
@@ -474,22 +268,8 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
pj_ks (pj, "name", block->name);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (block->input)) {
|
||||
// Try to parse the input as JSON first
|
||||
char *input_str = strdup (block->input);
|
||||
RJson *input_json = r_json_parse (input_str);
|
||||
if (input_json) {
|
||||
// If it's valid JSON, add it directly
|
||||
pj_ko (pj, "input");
|
||||
r_json_to_pj (input_json, pj);
|
||||
pj_end (pj);
|
||||
r_json_free (input_json);
|
||||
} else {
|
||||
// If not valid JSON, create a basic object with command
|
||||
pj_ko (pj, "input");
|
||||
pj_ks (pj, "command", block->input);
|
||||
pj_end (pj);
|
||||
}
|
||||
free (input_str);
|
||||
pj_k (pj, "input");
|
||||
pj_raw (pj, block->input);
|
||||
}
|
||||
pj_end (pj); // End content block object
|
||||
}
|
||||
@@ -505,43 +285,35 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
pj_ks (pj, "content", msg->content);
|
||||
} else {
|
||||
pj_ks (pj, "type", "text");
|
||||
pj_ks (pj, "text", msg->content);
|
||||
if (is_system_message) {
|
||||
char *prefixed = r_str_newf ("SYSTEM INSTRUCTIONS: %s", msg->content);
|
||||
pj_ks (pj, "text", prefixed);
|
||||
free (prefixed);
|
||||
} else {
|
||||
pj_ks (pj, "text", msg->content);
|
||||
}
|
||||
}
|
||||
pj_end (pj); // End content block object
|
||||
}
|
||||
|
||||
if (msg->tool_calls && msg->n_tool_calls > 0) {
|
||||
pj_k (pj, "tool_calls");
|
||||
pj_a (pj); // Start tool_calls array
|
||||
|
||||
for (int j = 0; j < msg->n_tool_calls; j++) {
|
||||
const R2AI_ToolCall *tc = &msg->tool_calls[j];
|
||||
pj_o (pj); // Start tool_calls object
|
||||
if (msg->tool_calls && r_list_length (msg->tool_calls) > 0) {
|
||||
RListIter *iter;
|
||||
R2AI_ToolCall *tc;
|
||||
r_list_foreach (msg->tool_calls, iter, tc) {
|
||||
pj_o (pj); // Start tool_use content block
|
||||
pj_ks (pj, "type", "tool_use");
|
||||
pj_ks (pj, "id", tc->id? tc->id: "");
|
||||
pj_ks (pj, "name", tc->name? tc->name: "");
|
||||
|
||||
// Create a non-const copy for r_json_parse
|
||||
char *arguments_copy = tc->arguments? strdup (tc->arguments): NULL;
|
||||
RJson *arguments = arguments_copy? r_json_parse (arguments_copy): NULL;
|
||||
|
||||
pj_ko (pj, "input"); // Start input object
|
||||
if (arguments) {
|
||||
for (size_t k = 0; k < arguments->children.count; k++) {
|
||||
const RJson *arg = r_json_item (arguments, k);
|
||||
if (arg && arg->type == R_JSON_STRING) {
|
||||
pj_ks (pj, arg->key, arg->str_value);
|
||||
}
|
||||
}
|
||||
r_json_free (arguments);
|
||||
// Insert the arguments directly as the input object
|
||||
pj_k (pj, "input");
|
||||
if (tc->arguments) {
|
||||
pj_raw (pj, tc->arguments);
|
||||
} else {
|
||||
pj_raw (pj, "{}");
|
||||
}
|
||||
free (arguments_copy);
|
||||
|
||||
pj_end (pj); // End input object
|
||||
pj_end (pj); // End tool_calls object
|
||||
pj_end (pj); // End tool_use content block
|
||||
}
|
||||
|
||||
pj_end (pj); // End tool_calls array
|
||||
}
|
||||
pj_end (pj); // End content array
|
||||
}
|
||||
@@ -555,8 +327,8 @@ R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs) {
|
||||
}
|
||||
|
||||
// Function to delete the last N messages from conversation history
|
||||
R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
R_API void r2ai_delete_last_messages(RList *messages, int n) {
|
||||
if (!messages || r_list_length (messages) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -566,162 +338,13 @@ R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n) {
|
||||
}
|
||||
|
||||
// Make sure we don't try to delete more messages than exist
|
||||
int len = r_list_length (messages->messages);
|
||||
int len = r_list_length (messages);
|
||||
if (n > len) {
|
||||
n = len;
|
||||
}
|
||||
|
||||
// Pop the last n messages
|
||||
for (int i = 0; i < n; i++) {
|
||||
r_list_pop (messages->messages);
|
||||
r_list_pop (messages);
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
+378
-359
@@ -1,420 +1,439 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake, dnakov */
|
||||
/* r2ai - Copyright 2023-2026 pancake, dnakov */
|
||||
|
||||
#include <r_core.h>
|
||||
#include <r_util/r_json.h>
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
static const char *json_string(const RJson *json) {
|
||||
return (json && json->type == R_JSON_STRING)? json->str_value: NULL;
|
||||
}
|
||||
|
||||
static uint64_t json_integer(const RJson *json) {
|
||||
return (json && json->type == R_JSON_INTEGER)? json->num.u_value: 0;
|
||||
}
|
||||
|
||||
// Bit flags for different types of model errors/incompatibilities
|
||||
typedef enum {
|
||||
MODEL_ERROR_NONE = 0,
|
||||
MODEL_ERROR_TEMPERATURE = 1 << 0,
|
||||
// Can add more error types here as needed
|
||||
// MODEL_ERROR_TOP_P = 1 << 1,
|
||||
// MODEL_ERROR_MAX_TOKENS = 1 << 2,
|
||||
// etc.
|
||||
} ModelErrorFlags;
|
||||
R2AI_CHAT_OPENAI,
|
||||
R2AI_CHAT_OLLAMA,
|
||||
R2AI_GENERATE_OLLAMA
|
||||
} R2AIChatAPI;
|
||||
|
||||
// Structure to store model compatibility info
|
||||
typedef struct {
|
||||
char *model_id; // Provider:model string
|
||||
int error_flags; // Bitfield of ModelErrorFlags
|
||||
} ModelCompat;
|
||||
static bool is_generate_api(RCore *core) {
|
||||
const char *apitype = r_config_get (core->config, "r2ai.apitype");
|
||||
if (R_STR_ISEMPTY (apitype)) {
|
||||
return false;
|
||||
}
|
||||
return !strcmp (apitype, "generate");
|
||||
}
|
||||
|
||||
// Function to add an error flag to a model
|
||||
static void model_add_error(R2AI_State *state, const char *provider, const char *model, ModelErrorFlags flag) {
|
||||
if (!state) {
|
||||
static char *ollama_generate_prompt_from_messages(const RList *msgs, char **system_prompt) {
|
||||
if (system_prompt) {
|
||||
*system_prompt = NULL;
|
||||
}
|
||||
if (!msgs || r_list_empty (msgs)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
if (!sb) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RListIter *iter;
|
||||
const R2AI_Message *msg;
|
||||
r_list_foreach (msgs, iter, msg) {
|
||||
const char *role = msg->role? msg->role: "user";
|
||||
const char *content = msg->content;
|
||||
if (R_STR_ISEMPTY (content)) {
|
||||
continue;
|
||||
}
|
||||
if (!strcmp (role, "system") || !strcmp (role, "developer")) {
|
||||
if (system_prompt && !*system_prompt) {
|
||||
*system_prompt = strdup (content);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (sb, "%s: %s\n\n", role, content);
|
||||
}
|
||||
|
||||
char *prompt = r_strbuf_drain (sb);
|
||||
if (R_STR_ISEMPTY (prompt) && system_prompt && *system_prompt) {
|
||||
free (prompt);
|
||||
prompt = strdup (*system_prompt);
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
|
||||
static void cache_part(RStrBuf *sb, const char *s) {
|
||||
const char *v = r_str_get (s);
|
||||
r_strbuf_appendf (sb, "%zu:%s\n", strlen (v), v);
|
||||
}
|
||||
|
||||
static char *cache_prefix(const char *provider, const char *model, const char *tools_json, const char *messages_json) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
cache_part (sb, provider);
|
||||
cache_part (sb, model);
|
||||
cache_part (sb, tools_json);
|
||||
const char *messages = r_str_get (messages_json);
|
||||
size_t len = strlen (messages);
|
||||
if (len > 0 && messages[len - 1] == ']') {
|
||||
len--;
|
||||
}
|
||||
r_strbuf_appendf (sb, "%zu:", len);
|
||||
r_strbuf_append_n (sb, messages, len);
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
static size_t prefix_len(const char *a, const char *b) {
|
||||
size_t i = 0;
|
||||
while (a[i] && b[i] && a[i] == b[i]) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static void check_chat_cache(RCorePluginSession *cps, const char *provider, const char *model, const char *tools_json, const char *messages_json) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!state || !r_config_get_b (core->config, "r2ai.cacheck")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state->model_compat_db) {
|
||||
state->model_compat_db = ht_pp_new0 ();
|
||||
char *prefix = cache_prefix (provider, model, tools_json, messages_json);
|
||||
if (state->cache_prefix) {
|
||||
const size_t old_len = strlen (state->cache_prefix);
|
||||
if (!r_str_startswith (prefix, state->cache_prefix)) {
|
||||
const size_t kept = prefix_len (state->cache_prefix, prefix);
|
||||
R_LOG_WARN ("Chat cache prefix changed: preserved %zu/%zu bytes from previous request. Keep system prompt, tool catalog, and previous messages append-only to maximize provider cache hits.", kept, old_len);
|
||||
} else {
|
||||
R_LOG_DEBUG ("Chat cache prefix preserved: %zu bytes", old_len);
|
||||
}
|
||||
R_FREE (state->cache_prefix);
|
||||
}
|
||||
state->cache_prefix = prefix;
|
||||
}
|
||||
|
||||
char *key = r_str_newf ("%s:%s", provider, model? model: "default");
|
||||
bool found_flag = false;
|
||||
ModelCompat *compat = ht_pp_find (state->model_compat_db, key, &found_flag);
|
||||
|
||||
if (found_flag && compat) {
|
||||
// Update existing entry
|
||||
compat->error_flags |= flag;
|
||||
static char *chat_request_json(RCorePluginSession *cps, const R2AIArgs *args, const char *provider, const char *model, const RList *messages, R2AIChatAPI api, bool check_cache) {
|
||||
const bool use_generate = api == R2AI_GENERATE_OLLAMA;
|
||||
const bool use_ollama = api != R2AI_CHAT_OPENAI;
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (provider);
|
||||
const bool is_ollama_provider = provider_info && provider_info->api_type == R2AI_API_OLLAMA;
|
||||
char *messages_json = NULL;
|
||||
char *generate_prompt = NULL;
|
||||
char *generate_system = NULL;
|
||||
if (use_generate) {
|
||||
generate_prompt = ollama_generate_prompt_from_messages (messages, &generate_system);
|
||||
} else {
|
||||
// Create new entry
|
||||
compat = R_NEW0 (ModelCompat);
|
||||
compat->model_id = strdup (key);
|
||||
compat->error_flags = flag;
|
||||
ht_pp_insert (state->model_compat_db, key, compat);
|
||||
messages_json = r2ai_msgs_to_json (messages, use_ollama);
|
||||
}
|
||||
free (key);
|
||||
if (!messages_json && !generate_prompt) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *tools_json = NULL;
|
||||
if (args->tools && !r_list_empty (args->tools)) {
|
||||
if (use_generate) {
|
||||
R_LOG_DEBUG ("Skipping native tool definitions for /api/generate payload");
|
||||
} else {
|
||||
tools_json = r2ai_tools_to_openai_json (args->tools);
|
||||
}
|
||||
}
|
||||
|
||||
PJ *pj = pj_new ();
|
||||
if (!pj) {
|
||||
free (messages_json);
|
||||
free (generate_prompt);
|
||||
free (generate_system);
|
||||
free (tools_json);
|
||||
return NULL;
|
||||
}
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", model);
|
||||
pj_kb (pj, "stream", false);
|
||||
if (use_ollama) {
|
||||
pj_kb (pj, "think", args->thinking_tokens > 0);
|
||||
pj_ko (pj, "options");
|
||||
if (args->max_tokens) {
|
||||
pj_kn (pj, "num_predict", args->max_tokens);
|
||||
}
|
||||
if (args->deterministic) {
|
||||
pj_kn (pj, "temperature", 0);
|
||||
pj_kn (pj, "top_p", 1);
|
||||
pj_kn (pj, "top_k", 1);
|
||||
pj_kn (pj, "repeat_last_n", 0);
|
||||
pj_kn (pj, "repeat_penalty", 1);
|
||||
pj_kn (pj, "seed", 123);
|
||||
} else if (args->temperature > 0) {
|
||||
pj_kd (pj, "temperature", args->temperature);
|
||||
}
|
||||
pj_end (pj);
|
||||
} else if (is_ollama_provider || !strcmp (provider, "mistral")) {
|
||||
pj_kn (pj, "max_tokens", args->max_tokens? args->max_tokens: 5128);
|
||||
} else {
|
||||
pj_kn (pj, "max_completion_tokens", args->max_tokens? args->max_tokens: 5128);
|
||||
}
|
||||
if (use_generate) {
|
||||
pj_ks (pj, "prompt", generate_prompt);
|
||||
if (R_STR_ISNOTEMPTY (generate_system)) {
|
||||
pj_ks (pj, "system", generate_system);
|
||||
}
|
||||
} else {
|
||||
pj_k (pj, "messages");
|
||||
pj_raw (pj, messages_json);
|
||||
if (tools_json) {
|
||||
pj_k (pj, "tools");
|
||||
pj_raw (pj, tools_json);
|
||||
}
|
||||
}
|
||||
pj_end (pj);
|
||||
|
||||
if (!use_generate && check_cache) {
|
||||
check_chat_cache (cps, provider, model, tools_json, messages_json);
|
||||
}
|
||||
free (messages_json);
|
||||
free (generate_prompt);
|
||||
free (generate_system);
|
||||
free (tools_json);
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
// Free a ModelCompat item (for hash table)
|
||||
static bool model_compat_free_cb(void *user, const void *k, const void *v) {
|
||||
(void)user;
|
||||
(void)k;
|
||||
ModelCompat *compat = (ModelCompat *)v;
|
||||
if (compat) {
|
||||
free (compat->model_id);
|
||||
free (compat);
|
||||
}
|
||||
return true;
|
||||
static char *chat_api_url(const char *base_url, R2AIChatAPI api) {
|
||||
const char *path = api == R2AI_CHAT_OPENAI
|
||||
? "/chat/completions"
|
||||
: (api == R2AI_GENERATE_OLLAMA? "/generate": "/chat");
|
||||
return r_str_newf ("%s%s", base_url, path);
|
||||
}
|
||||
|
||||
// Function to free the model_compat_db hash table
|
||||
R_IPI void r2ai_openai_fini(R2AI_State *state) {
|
||||
if (state && state->model_compat_db) {
|
||||
ht_pp_foreach (state->model_compat_db, model_compat_free_cb, NULL);
|
||||
ht_pp_free (state->model_compat_db);
|
||||
state->model_compat_db = NULL;
|
||||
static char *ollama_native_base(const char *base_url) {
|
||||
if (r_str_endswith (base_url, "/api")) {
|
||||
return strdup (base_url);
|
||||
}
|
||||
if (r_str_endswith (base_url, "/v1")) {
|
||||
char *url = strdup (base_url);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
url[strlen (url) - 3] = 0;
|
||||
char *res = r_str_newf ("%s/api", url);
|
||||
free (url);
|
||||
return res;
|
||||
}
|
||||
return r_str_newf ("%s/api", base_url);
|
||||
}
|
||||
|
||||
static bool allow_ollama_fallback(RCore *core, const char *provider) {
|
||||
if (strcmp (provider, "ollama")) {
|
||||
return false;
|
||||
}
|
||||
const char *base_url = r_config_get (core->config, "r2ai.baseurl");
|
||||
if (R_STR_ISEMPTY (base_url)) {
|
||||
return true;
|
||||
}
|
||||
char *url = strdup (base_url);
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
r_str_trim (url);
|
||||
size_t len = strlen (url);
|
||||
while (len > 0 && url[len - 1] == '/') {
|
||||
url[--len] = 0;
|
||||
}
|
||||
const bool explicit_root = r_str_endswith (url, "/v1") || r_str_endswith (url, "/api");
|
||||
free (url);
|
||||
return !explicit_root;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
// Initialize compatibility database if needed
|
||||
if (!state->model_compat_db) {
|
||||
state->model_compat_db = ht_pp_new0 ();
|
||||
}
|
||||
args.provider = r_config_get (core->config, "r2ai.api");
|
||||
args.model = r_config_get (core->config, "r2ai.model");
|
||||
const char *provider_name = args.provider;
|
||||
const char *model_name = R_STR_ISNOTEMPTY (args.model)? args.model: "gpt-4o-mini";
|
||||
|
||||
const char *base_url = r2ai_get_provider_url (core, args.provider);
|
||||
// TODO: default model name should depend on api
|
||||
const char *model_name = args.model? args.model: "gpt-4o-mini";
|
||||
char **error = args.error;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
// create a temp conversation to include the system prompt and the rest of the messages
|
||||
R2AI_Messages *temp_msgs = r2ai_msgs_new ();
|
||||
if (!temp_msgs) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create temporary messages array");
|
||||
}
|
||||
const R2AIProvider *provider_info = r2ai_get_provider (provider_name);
|
||||
const bool is_ollama_provider = provider_info && provider_info->api_type == R2AI_API_OLLAMA;
|
||||
const bool use_generate = is_ollama_provider && is_generate_api (core);
|
||||
char *base_url = r2ai_get_provider_url (core, provider_name);
|
||||
if (!base_url) {
|
||||
*args.error = strdup ("Failed to resolve provider URL");
|
||||
return NULL;
|
||||
}
|
||||
R2AI_Message system_msg = {
|
||||
.role = "system",
|
||||
.content = (char *)args.system_prompt
|
||||
};
|
||||
// Add system message if available from args.system_prompt
|
||||
R2AIChatAPI chat_api = use_generate
|
||||
? R2AI_GENERATE_OLLAMA
|
||||
: (is_ollama_provider && r_str_endswith (base_url, "/api")? R2AI_CHAT_OLLAMA: R2AI_CHAT_OPENAI);
|
||||
char **error = args.error;
|
||||
// the system prompt goes first, o1 and o3 models call it "developer"
|
||||
RList *temp_msgs = r2ai_msgs_new ();
|
||||
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
|
||||
R_LOG_DEBUG ("Using system prompt: %s", args.system_prompt);
|
||||
// if the model name contains "o1" or "o3", it's "developer" role
|
||||
if (strstr (model_name, "o1") || strstr (model_name, "o3")) {
|
||||
system_msg.role = "developer";
|
||||
system_msg.content = (char *)args.system_prompt;
|
||||
} else {
|
||||
system_msg.role = "system";
|
||||
system_msg.content = (char *)args.system_prompt;
|
||||
}
|
||||
const bool dev = strstr (model_name, "o1") || strstr (model_name, "o3");
|
||||
R2AI_Message system_msg = { .role = dev? "developer": "system", .content = (char *)args.system_prompt };
|
||||
r2ai_msgs_add (temp_msgs, &system_msg);
|
||||
} else {
|
||||
// Fallback to config if args.system_prompt is not set
|
||||
const char *sysprompt = r_config_get (core->config, "r2ai.system");
|
||||
if (R_STR_ISNOTEMPTY (sysprompt)) {
|
||||
R_LOG_DEBUG ("Using system prompt from config: %s", sysprompt);
|
||||
if (strstr (model_name, "o1") || strstr (model_name, "o3")) {
|
||||
system_msg.role = "developer";
|
||||
} else {
|
||||
system_msg.role = "system";
|
||||
}
|
||||
system_msg.content = (char *)sysprompt;
|
||||
r2ai_msgs_add (temp_msgs, &system_msg);
|
||||
}
|
||||
}
|
||||
if (args.messages) {
|
||||
for (int i = 0; i < r_list_length (args.messages->messages); i++) {
|
||||
r2ai_msgs_add (temp_msgs, r_list_get_n (args.messages->messages, i));
|
||||
}
|
||||
} else {
|
||||
R_LOG_WARN ("No messages");
|
||||
RListIter *iter;
|
||||
R2AI_Message *msg;
|
||||
r_list_foreach (args.messages, iter, msg) {
|
||||
r2ai_msgs_add (temp_msgs, msg);
|
||||
}
|
||||
// Safely print debug info about first message
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0 && ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role) {
|
||||
R_LOG_DEBUG ("First message role: %s", ((R2AI_Message *)r_list_get_n (temp_msgs->messages, 0))->role);
|
||||
if (r_list_empty (args.messages)) {
|
||||
*error = strdup ("No messages provided");
|
||||
free (base_url);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
}
|
||||
|
||||
const char **headers = NULL;
|
||||
char *auth_header = NULL;
|
||||
const char *headers[] = { "Content-Type: application/json", NULL, NULL };
|
||||
if (R_STR_ISNOTEMPTY (args.api_key)) {
|
||||
auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
|
||||
R_LOG_DEBUG ("Auth header: %s", auth_header);
|
||||
static const char *static_headers[] = { NULL, NULL, NULL };
|
||||
headers = static_headers;
|
||||
headers[0] = "Content-Type: application/json";
|
||||
headers[1] = auth_header;
|
||||
}
|
||||
const char *urlfmt = strcmp (args.provider, "ollama")
|
||||
? "%s/chat/completions"
|
||||
: "%s/chat";
|
||||
char *openai_url = r_str_newf (urlfmt, base_url);
|
||||
|
||||
// Create a messages JSON object, either from input messages or from content
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (temp_msgs && r_list_length (temp_msgs->messages) > 0) {
|
||||
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs->messages));
|
||||
messages_json = r2ai_msgs_to_json (temp_msgs);
|
||||
if (!messages_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to convert messages to JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
if (error) {
|
||||
*error = strdup ("No messages provided");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Convert tools to OpenAI format if available
|
||||
char *openai_tools_json = NULL;
|
||||
if (tools && tools->n_tools > 0) {
|
||||
openai_tools_json = r2ai_tools_to_openai_json (tools);
|
||||
}
|
||||
|
||||
// Create the model settings part
|
||||
PJ *pj = pj_new ();
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "model", model_name);
|
||||
pj_kb (pj, "stream", false);
|
||||
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
// Ollama uses "options" object for parameters
|
||||
pj_ko (pj, "options");
|
||||
if (args.max_tokens) {
|
||||
pj_kn (pj, "num_predict", args.max_tokens);
|
||||
}
|
||||
if (args.temperature > 0) {
|
||||
pj_kd (pj, "temperature", args.temperature);
|
||||
}
|
||||
pj_end (pj); // end options
|
||||
} else {
|
||||
#if 0
|
||||
// gpt-5-mini-chat is the only gpt-5 model that supports temperature
|
||||
// gpt-5 gpt-5-mini and gpt-5-nano just throw an error
|
||||
// Only add temperature if this provider/model doesn't have the temperature error flag
|
||||
if (!model_has_error (args.provider, model_name, MODEL_ERROR_TEMPERATURE)) {
|
||||
pj_kd (pj, "temperature", args.temperature? args.temperature: 0.01);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (strcmp (args.provider, "mistral") == 0) {
|
||||
pj_kn (pj, "max_tokens", args.max_tokens? args.max_tokens: 5128);
|
||||
} else {
|
||||
pj_kn (pj, "max_completion_tokens", args.max_tokens? args.max_tokens: 5128);
|
||||
}
|
||||
}
|
||||
|
||||
pj_end (pj);
|
||||
|
||||
// Get the JSON for model settings
|
||||
char *model_json = pj_drain (pj);
|
||||
if (!model_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create model settings JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
free (messages_json);
|
||||
if (openai_tools_json) {
|
||||
free (openai_tools_json);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Manually create the final JSON by combining parts
|
||||
// Remove the closing brace from model_json
|
||||
size_t model_len = strlen (model_json);
|
||||
if (model_len > 0 && model_json[model_len - 1] == '}') {
|
||||
model_json[model_len - 1] = '\0';
|
||||
}
|
||||
|
||||
// Create the full JSON with proper structure
|
||||
char *complete_json;
|
||||
if (openai_tools_json) {
|
||||
complete_json = r_str_newf ("%s, \"messages\": %s, \"tools\": %s}",
|
||||
model_json, messages_json, openai_tools_json);
|
||||
free (openai_tools_json);
|
||||
} else {
|
||||
complete_json = r_str_newf ("%s, \"messages\": %s}",
|
||||
model_json, messages_json);
|
||||
}
|
||||
|
||||
// Free intermediate strings
|
||||
free (model_json);
|
||||
free (messages_json);
|
||||
|
||||
if (!complete_json) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to create complete request JSON");
|
||||
}
|
||||
free (auth_header);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the full JSON to a file for inspection
|
||||
// XXX: only create request/response files when r2ai.debug is set
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_request.json", tmpdir);
|
||||
r_file_dump (req_path, (const ut8 *)complete_json, strlen (complete_json), 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API request data: %s", complete_json);
|
||||
|
||||
// Make the API call
|
||||
char *res = NULL;
|
||||
int code = 0;
|
||||
res = r2ai_http_post (core, openai_url, headers, complete_json, &code, NULL);
|
||||
free (complete_json);
|
||||
free (openai_url);
|
||||
|
||||
if (code != 200) {
|
||||
R_LOG_ERROR ("OpenAI API error %d", code);
|
||||
if (res) {
|
||||
R_LOG_ERROR ("OpenAI API error response: %s", res);
|
||||
// Check for specific error types in the response
|
||||
ModelErrorFlags error_flag = MODEL_ERROR_NONE;
|
||||
|
||||
// Check for temperature errors
|
||||
if (strstr (res, "temperature")) {
|
||||
R_LOG_DEBUG ("Detected temperature error for %s model %s", args.provider, args.model);
|
||||
error_flag |= MODEL_ERROR_TEMPERATURE;
|
||||
}
|
||||
|
||||
if (error_flag != MODEL_ERROR_NONE) {
|
||||
// Record the error flags for this provider/model
|
||||
model_add_error (state, args.provider, args.model, error_flag);
|
||||
|
||||
// Clean up
|
||||
free (auth_header);
|
||||
free (res);
|
||||
|
||||
// Retry the call (it will skip problematic parameters this time)
|
||||
R_LOG_INFO ("Retrying request with adjusted parameters for %s/%s", args.provider, args.model);
|
||||
RCorePluginSession retry_cps = { .core = core, .data = state };
|
||||
return r2ai_openai (&retry_cps, args);
|
||||
}
|
||||
char *fallback_base = NULL;
|
||||
const char *request_base = base_url;
|
||||
bool first_request = true;
|
||||
for (;;) {
|
||||
char *request_json = chat_request_json (cps, &args, provider_name, model_name, temp_msgs, chat_api, first_request);
|
||||
if (!request_json) {
|
||||
*error = strdup ("Failed to create request JSON");
|
||||
free (fallback_base);
|
||||
free (auth_header);
|
||||
free (base_url);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
free (auth_header);
|
||||
free (res);
|
||||
char *api_url = chat_api_url (request_base, chat_api);
|
||||
res = r2ai_post (core, "openai", api_url, headers, request_json, &code, error);
|
||||
free (request_json);
|
||||
free (api_url);
|
||||
if (chat_api != R2AI_CHAT_OPENAI || (code != 404 && code != 405) || !allow_ollama_fallback (core, provider_name)) {
|
||||
break;
|
||||
}
|
||||
fallback_base = ollama_native_base (base_url);
|
||||
if (!fallback_base) {
|
||||
break;
|
||||
}
|
||||
R_LOG_INFO ("OpenAI-compatible Ollama endpoint unavailable; falling back to /api/chat");
|
||||
R_FREE (*error);
|
||||
request_base = fallback_base;
|
||||
chat_api = R2AI_CHAT_OLLAMA;
|
||||
first_request = false;
|
||||
}
|
||||
free (fallback_base);
|
||||
free (base_url);
|
||||
|
||||
free (auth_header);
|
||||
if (!res) {
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_openai_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("OpenAI API response saved to %s", res_path);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
|
||||
R_LOG_DEBUG ("OpenAI API response: %s", res);
|
||||
|
||||
// Parse the response into our messages structure
|
||||
|
||||
char *res_copy = strdup (res);
|
||||
RJson *jres = r_json_parse (res_copy);
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
eprintf ("OpenAI API response: %s\n", res);
|
||||
}
|
||||
RJson *jres = r_json_parse (res);
|
||||
if (jres) {
|
||||
// Create a new message structure
|
||||
const bool is_native_ollama = chat_api != R2AI_CHAT_OPENAI;
|
||||
R2AI_Message *message = R_NEW0 (R2AI_Message);
|
||||
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
|
||||
if (message) {
|
||||
// Process the response using our r2ai_msgs_from_json logic
|
||||
const RJson *usage_json = NULL;
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
// Ollama has usage info at top level
|
||||
usage_json = jres;
|
||||
} else {
|
||||
usage_json = r_json_get (jres, "usage");
|
||||
const RJson *usage_json = is_native_ollama? jres: r_json_get (jres, "usage");
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
usage->prompt_tokens = json_integer (r_json_get (usage_json, is_native_ollama? "prompt_eval_count": "prompt_tokens"));
|
||||
usage->completion_tokens = json_integer (r_json_get (usage_json, is_native_ollama? "eval_count": "completion_tokens"));
|
||||
usage->total_tokens = is_native_ollama
|
||||
? usage->prompt_tokens + usage->completion_tokens
|
||||
: json_integer (r_json_get (usage_json, "total_tokens"));
|
||||
}
|
||||
|
||||
const RJson *message_json = NULL;
|
||||
if (use_generate) {
|
||||
const char *content = json_string (r_json_get (jres, "response"));
|
||||
const char *thinking = json_string (r_json_get (jres, "thinking"));
|
||||
message->role = strdup ("assistant");
|
||||
if (content) {
|
||||
message->content = strdup (content);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (thinking)) {
|
||||
message->reasoning_content = strdup (thinking);
|
||||
}
|
||||
} else if (is_native_ollama) {
|
||||
message_json = r_json_get (jres, "message");
|
||||
} else {
|
||||
const RJson *choices = r_json_get (jres, "choices");
|
||||
if (choices && choices->type == R_JSON_ARRAY) {
|
||||
const RJson *choice = r_json_item (choices, 0);
|
||||
message_json = choice? r_json_get (choice, "message"): NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (message_json) {
|
||||
const char *role = json_string (r_json_get (message_json, "role"));
|
||||
const char *content = json_string (r_json_get (message_json, "content"));
|
||||
const char *reasoning_content = json_string (r_json_get (message_json, "reasoning_content"));
|
||||
const char *thinking = json_string (r_json_get (message_json, "thinking"));
|
||||
const char *tool_call_id = json_string (r_json_get (message_json, "tool_call_id"));
|
||||
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
|
||||
|
||||
message->role = strdup (role? role: "assistant");
|
||||
if (content) {
|
||||
message->content = strdup (content);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (reasoning_content)) {
|
||||
message->reasoning_content = strdup (reasoning_content);
|
||||
} else if (R_STR_ISNOTEMPTY (thinking)) {
|
||||
message->reasoning_content = strdup (thinking);
|
||||
}
|
||||
if (R_STR_ISNOTEMPTY (tool_call_id)) {
|
||||
message->tool_call_id = strdup (tool_call_id);
|
||||
}
|
||||
|
||||
if (usage_json && usage_json->type == R_JSON_OBJECT) {
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
// Ollama field names
|
||||
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_eval_count");
|
||||
const RJson *completion_tokens = r_json_get (usage_json, "eval_count");
|
||||
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = prompt_tokens->num.u_value;
|
||||
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
|
||||
message->tool_calls = r_list_newf ((RListFree)r2ai_tool_call_free);
|
||||
if (message->tool_calls) {
|
||||
size_t i;
|
||||
for (i = 0; i < tool_calls->children.count; i++) {
|
||||
const RJson *tool_call = r_json_item (tool_calls, i);
|
||||
if (!tool_call || tool_call->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
}
|
||||
const RJson *function = r_json_get (tool_call, "function");
|
||||
const RJson *source = (function && function->type == R_JSON_OBJECT)? function: tool_call;
|
||||
const char *name = json_string (r_json_get (source, "name"));
|
||||
const RJson *arguments = r_json_get (source, "arguments");
|
||||
if (R_STR_ISEMPTY (name) || !arguments) {
|
||||
continue;
|
||||
}
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
const char *tc_id = json_string (r_json_get (tool_call, "id"));
|
||||
if (R_STR_ISNOTEMPTY (tc_id)) {
|
||||
tc->id = strdup (tc_id);
|
||||
}
|
||||
if (!tc->id) {
|
||||
tc->id = r_str_newf ("call_%zu", i);
|
||||
}
|
||||
tc->name = strdup (name);
|
||||
if (arguments->type == R_JSON_STRING && R_STR_ISNOTEMPTY (arguments->str_value)) {
|
||||
tc->arguments = strdup (arguments->str_value);
|
||||
} else {
|
||||
tc->arguments = r_json_to_string (arguments);
|
||||
}
|
||||
if (!tc->name || !tc->arguments) {
|
||||
r2ai_tool_call_free (tc);
|
||||
continue;
|
||||
}
|
||||
r_list_append (message->tool_calls, tc);
|
||||
}
|
||||
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = completion_tokens->num.u_value;
|
||||
}
|
||||
// Calculate total_tokens for ollama
|
||||
usage->total_tokens = usage->prompt_tokens + usage->completion_tokens;
|
||||
} else {
|
||||
// OpenAI field names
|
||||
const RJson *prompt_tokens = r_json_get (usage_json, "prompt_tokens");
|
||||
const RJson *completion_tokens = r_json_get (usage_json, "completion_tokens");
|
||||
const RJson *total_tokens = r_json_get (usage_json, "total_tokens");
|
||||
if (prompt_tokens && prompt_tokens->type == R_JSON_INTEGER) {
|
||||
usage->prompt_tokens = prompt_tokens->num.u_value;
|
||||
}
|
||||
if (completion_tokens && completion_tokens->type == R_JSON_INTEGER) {
|
||||
usage->completion_tokens = completion_tokens->num.u_value;
|
||||
}
|
||||
if (total_tokens && total_tokens->type == R_JSON_INTEGER) {
|
||||
usage->total_tokens = total_tokens->num.u_value;
|
||||
if (r_list_empty (message->tool_calls)) {
|
||||
r_list_free (message->tool_calls);
|
||||
message->tool_calls = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
const RJson *message_json = NULL;
|
||||
if (strcmp (args.provider, "ollama") == 0) {
|
||||
// Ollama returns message directly
|
||||
message_json = r_json_get (jres, "message");
|
||||
} else {
|
||||
// OpenAI-style response with choices array
|
||||
const RJson *choices = r_json_get (jres, "choices");
|
||||
if (choices && choices->type == R_JSON_ARRAY) {
|
||||
const RJson *choice = r_json_item (choices, 0);
|
||||
if (choice) {
|
||||
message_json = r_json_get (choice, "message");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (message_json) {
|
||||
const RJson *role = r_json_get (message_json, "role");
|
||||
const RJson *content = r_json_get (message_json, "content");
|
||||
const RJson *reasoning_content = r_json_get (message_json, "reasoning_content");
|
||||
|
||||
// Set the basic message properties
|
||||
message->role = (role && role->type == R_JSON_STRING)? strdup (role->str_value): strdup ("assistant");
|
||||
|
||||
if (content && content->type == R_JSON_STRING) {
|
||||
message->content = strdup (content->str_value);
|
||||
}
|
||||
|
||||
if (reasoning_content && reasoning_content->type == R_JSON_STRING) {
|
||||
message->reasoning_content = strdup (reasoning_content->str_value);
|
||||
}
|
||||
// TODO: Handle tool calls if present?
|
||||
}
|
||||
}
|
||||
r_json_free (jres);
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
free (res_copy);
|
||||
free (auth_header);
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
free (auth_header);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R_IPI char *r2ai_openai_stream(RCore *core, R2AIArgs args) {
|
||||
(void)core;
|
||||
(void)args;
|
||||
r2ai_msgs_free (temp_msgs);
|
||||
return NULL;
|
||||
}
|
||||
+237
-111
@@ -1,4 +1,4 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake */
|
||||
/* r2ai - Copyright 2023-2026 pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
@@ -19,7 +19,40 @@ R_API void r2aiprompt_free(R2AIPrompt *prompt) {
|
||||
free (prompt);
|
||||
}
|
||||
|
||||
static R2AIPrompt *parse_prompt_file(const char *filepath) {
|
||||
typedef struct {
|
||||
const char *key;
|
||||
size_t off;
|
||||
} PromptField;
|
||||
|
||||
#define PROMPT_FIELD(k, f) \
|
||||
{ k, r_offsetof (R2AIPrompt, f) }
|
||||
static const PromptField prompt_fields[] = {
|
||||
PROMPT_FIELD ("title", title),
|
||||
PROMPT_FIELD ("author", author),
|
||||
PROMPT_FIELD ("description", desc),
|
||||
PROMPT_FIELD ("command", command),
|
||||
PROMPT_FIELD ("requires", requires),
|
||||
PROMPT_FIELD ("if-empty", if_empty),
|
||||
PROMPT_FIELD ("if-command", if_command),
|
||||
PROMPT_FIELD ("model", model),
|
||||
PROMPT_FIELD ("provider", provider),
|
||||
};
|
||||
#undef PROMPT_FIELD
|
||||
|
||||
static bool prompt_set_field(R2AIPrompt *prompt, const char *key, const char *value) {
|
||||
size_t i;
|
||||
for (i = 0; i < R_ARRAY_SIZE (prompt_fields); i++) {
|
||||
if (!strcmp (key, prompt_fields[i].key)) {
|
||||
char **field = (char **) ((ut8 *)prompt + prompt_fields[i].off);
|
||||
free (*field);
|
||||
*field = strdup (value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
R_API R2AIPrompt *parse_prompt_file(const char *filepath) {
|
||||
R2AIPrompt *prompt = R_NEW0 (R2AIPrompt);
|
||||
char *content = r_file_slurp (filepath, NULL);
|
||||
if (!content) {
|
||||
@@ -27,53 +60,43 @@ static R2AIPrompt *parse_prompt_file(const char *filepath) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check if it's a markdown file with YAML frontmatter
|
||||
if (r_str_startswith (content, "---\n")) {
|
||||
// Parse YAML frontmatter
|
||||
char *frontmatter_end = strstr (content + 4, "\n---\n");
|
||||
if (frontmatter_end) {
|
||||
char *frontmatter = r_str_ndup (content + 4, frontmatter_end - (content + 4));
|
||||
RList *lines = r_str_split_list (frontmatter, "\n", -1);
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
r_str_trim (line);
|
||||
if (*line == 0) {
|
||||
continue;
|
||||
}
|
||||
char *colon = strchr (line, ':');
|
||||
if (!colon) {
|
||||
continue;
|
||||
}
|
||||
*colon = 0;
|
||||
char *key = line;
|
||||
char *value = colon + 1;
|
||||
r_str_trim (key);
|
||||
r_str_trim (value);
|
||||
if (!strcmp (key, "title")) {
|
||||
prompt->desc = strdup (value);
|
||||
} else if (!strcmp (key, "author")) {
|
||||
prompt->author = strdup (value);
|
||||
} else if (!strcmp (key, "description")) {
|
||||
prompt->title = strdup (value);
|
||||
} else if (!strcmp (key, "command")) {
|
||||
prompt->command = strdup (value);
|
||||
} else if (!strcmp (key, "model")) {
|
||||
prompt->model = strdup (value);
|
||||
} else if (!strcmp (key, "provider")) {
|
||||
prompt->provider = strdup (value);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (frontmatter);
|
||||
// The prompt content starts after the second ---
|
||||
char *prompt_start = frontmatter_end + 5; // Skip "\n---\n"
|
||||
prompt->prompt = strdup (prompt_start);
|
||||
}
|
||||
} else {
|
||||
if (!r_str_startswith (content, "---\n")) {
|
||||
R_LOG_ERROR ("Invalid prompt file format: %s", filepath);
|
||||
r2aiprompt_free (prompt);
|
||||
free (content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *frontmatter_end = strstr (content + 4, "\n---\n");
|
||||
if (!frontmatter_end) {
|
||||
R_LOG_ERROR ("Invalid prompt file format: %s", filepath);
|
||||
r2aiprompt_free (prompt);
|
||||
free (content);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char *prompt_start = frontmatter_end + 5;
|
||||
*frontmatter_end = 0;
|
||||
RList *lines = r_str_split_list (content + 4, "\n", -1);
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
r_str_trim (line);
|
||||
if (!*line || *line == '#') {
|
||||
continue;
|
||||
}
|
||||
char *value = strchr (line, ':');
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
*value++ = 0;
|
||||
r_str_trim (line);
|
||||
r_str_trim (value);
|
||||
prompt_set_field (prompt, line, value);
|
||||
}
|
||||
r_list_free (lines);
|
||||
prompt->prompt = strdup (prompt_start);
|
||||
|
||||
free (content);
|
||||
return prompt;
|
||||
}
|
||||
@@ -120,7 +143,6 @@ static char *replace_vars(RCore *core, const char *text) {
|
||||
p = end + 1;
|
||||
} else {
|
||||
r_strbuf_append (sb, "${");
|
||||
p += 2;
|
||||
}
|
||||
} else if (*p == '$' && *(p + 1) == '(') {
|
||||
p += 2;
|
||||
@@ -135,7 +157,6 @@ static char *replace_vars(RCore *core, const char *text) {
|
||||
p = end + 1;
|
||||
} else {
|
||||
r_strbuf_append (sb, "$(");
|
||||
p += 2;
|
||||
}
|
||||
} else {
|
||||
r_strbuf_append_n (sb, p, 1);
|
||||
@@ -145,8 +166,33 @@ static char *replace_vars(RCore *core, const char *text) {
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
R_API char *find_prompt_file(RList *search_dirs, const char *name) {
|
||||
char *filepath = NULL;
|
||||
RListIter *dir_iter;
|
||||
char *dir;
|
||||
r_list_foreach (search_dirs, dir_iter, dir) {
|
||||
filepath = r_str_newf ("%s/%s.r2ai.md", dir, name);
|
||||
if (r_file_exists (filepath)) {
|
||||
break;
|
||||
}
|
||||
free (filepath);
|
||||
filepath = NULL;
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
|
||||
static bool prompt_name_seen(RList *seen, const char *name) {
|
||||
RListIter *iter;
|
||||
char *it;
|
||||
r_list_foreach (seen, iter, it) {
|
||||
if (!strcmp (it, name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static RList *prompt_search_dirs(RCore *core) {
|
||||
const char *promptdir = r_config_get (core->config, "r2ai.promptdir");
|
||||
if (!promptdir || !*promptdir) {
|
||||
promptdir = "~/.config/r2ai/prompts";
|
||||
@@ -158,55 +204,135 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
r_list_append (search_dirs, expanded_dir);
|
||||
} else {
|
||||
free (expanded_dir);
|
||||
expanded_dir = NULL;
|
||||
}
|
||||
if (r_file_is_directory (local_prompts)) {
|
||||
r_list_append (search_dirs, local_prompts);
|
||||
} else {
|
||||
free (local_prompts);
|
||||
local_prompts = NULL;
|
||||
}
|
||||
if (r_list_empty (search_dirs)) {
|
||||
R_LOG_ERROR ("No prompt directories found");
|
||||
r_list_free (search_dirs);
|
||||
return search_dirs;
|
||||
}
|
||||
|
||||
static RList *prompt_search_dirs_or_warn(RCore *core) {
|
||||
RList *search_dirs = prompt_search_dirs (core);
|
||||
if (!r_list_empty (search_dirs)) {
|
||||
return search_dirs;
|
||||
}
|
||||
R_LOG_ERROR ("No prompt directories found");
|
||||
r_list_free (search_dirs);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void prompt_print_summary(RCore *core, const char *name, R2AIPrompt *prompt) {
|
||||
const char *title = r_str_get (prompt->title);
|
||||
const char *desc = r_str_get (prompt->desc);
|
||||
if (*title && *desc) {
|
||||
r_cons_printf (core->cons, "%s: %s - %s\n", name, title, desc);
|
||||
} else if (*title) {
|
||||
r_cons_printf (core->cons, "%s: %s\n", name, title);
|
||||
} else if (*desc) {
|
||||
r_cons_printf (core->cons, "%s: %s\n", name, desc);
|
||||
} else {
|
||||
r_cons_println (core->cons, name);
|
||||
}
|
||||
}
|
||||
|
||||
static char *prompt_name_from_file(const char *file) {
|
||||
return r_str_ndup (file, strlen (file) - strlen (".r2ai.md"));
|
||||
}
|
||||
|
||||
static void prompt_json_ks(PJ *pj, const char *key, const char *value) {
|
||||
pj_ks (pj, key, r_str_get (value));
|
||||
}
|
||||
|
||||
static void prompt_print_json(PJ *pj, const char *name, const char *filepath, R2AIPrompt *prompt) {
|
||||
pj_o (pj);
|
||||
pj_ks (pj, "name", name);
|
||||
pj_ks (pj, "path", filepath);
|
||||
pj_kb (pj, "valid", prompt != NULL);
|
||||
prompt_json_ks (pj, "title", prompt? prompt->title: NULL);
|
||||
prompt_json_ks (pj, "description", prompt? prompt->desc: NULL);
|
||||
prompt_json_ks (pj, "author", prompt? prompt->author: NULL);
|
||||
prompt_json_ks (pj, "command", prompt? prompt->command: NULL);
|
||||
prompt_json_ks (pj, "prompt", prompt? prompt->prompt: NULL);
|
||||
prompt_json_ks (pj, "requires", prompt? prompt->requires: NULL);
|
||||
prompt_json_ks (pj, "if_empty", prompt? prompt->if_empty: NULL);
|
||||
prompt_json_ks (pj, "if_command", prompt? prompt->if_command: NULL);
|
||||
prompt_json_ks (pj, "model", prompt? prompt->model: NULL);
|
||||
prompt_json_ks (pj, "provider", prompt? prompt->provider: NULL);
|
||||
pj_end (pj);
|
||||
}
|
||||
|
||||
static void prompt_list(RCore *core, RList *search_dirs, bool json) {
|
||||
RList *seen = r_list_newf (free);
|
||||
PJ *pj = NULL;
|
||||
if (json) {
|
||||
pj = r_core_pj_new (core);
|
||||
pj_a (pj);
|
||||
}
|
||||
RListIter *dir_iter;
|
||||
char *dir;
|
||||
r_list_foreach (search_dirs, dir_iter, dir) {
|
||||
RList *files = r_sys_dir (dir);
|
||||
RListIter *iter;
|
||||
char *file;
|
||||
r_list_foreach (files, iter, file) {
|
||||
if (!r_str_endswith (file, ".r2ai.md")) {
|
||||
continue;
|
||||
}
|
||||
char *name = prompt_name_from_file (file);
|
||||
if (prompt_name_seen (seen, name)) {
|
||||
free (name);
|
||||
continue;
|
||||
}
|
||||
r_list_append (seen, strdup (name));
|
||||
char *filepath = find_prompt_file (search_dirs, name);
|
||||
if (!filepath) {
|
||||
free (name);
|
||||
continue;
|
||||
}
|
||||
R2AIPrompt *prompt = parse_prompt_file (filepath);
|
||||
if (json) {
|
||||
prompt_print_json (pj, name, filepath, prompt);
|
||||
} else if (prompt) {
|
||||
prompt_print_summary (core, name, prompt);
|
||||
} else {
|
||||
r_cons_println (core->cons, name);
|
||||
}
|
||||
r2aiprompt_free (prompt);
|
||||
free (filepath);
|
||||
free (name);
|
||||
}
|
||||
r_list_free (files);
|
||||
}
|
||||
if (json) {
|
||||
pj_end (pj);
|
||||
char *s = pj_drain (pj);
|
||||
r_cons_println (core->cons, s);
|
||||
free (s);
|
||||
}
|
||||
r_list_free (seen);
|
||||
}
|
||||
|
||||
R_API void r2ai_cmd_qj(RCorePluginSession *cps, const char *input) {
|
||||
(void)input;
|
||||
RCore *core = cps->core;
|
||||
RList *search_dirs = prompt_search_dirs_or_warn (core);
|
||||
if (!search_dirs) {
|
||||
return;
|
||||
}
|
||||
prompt_list (core, search_dirs, true);
|
||||
r_list_free (search_dirs);
|
||||
}
|
||||
|
||||
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
RList *search_dirs = prompt_search_dirs_or_warn (core);
|
||||
if (!search_dirs) {
|
||||
return;
|
||||
}
|
||||
if (!input || !*input) {
|
||||
// list prompts
|
||||
RListIter *dir_iter;
|
||||
char *dir;
|
||||
r_list_foreach (search_dirs, dir_iter, dir) {
|
||||
RList *files = r_sys_dir (dir);
|
||||
RListIter *iter;
|
||||
char *file;
|
||||
r_list_foreach (files, iter, file) {
|
||||
if (r_str_endswith (file, ".r2ai.txt") || r_str_endswith (file, ".r2ai.md")) {
|
||||
char *dot = strchr (file, '.');
|
||||
char *name = dot? r_str_ndup (file, dot - file): strdup (file);
|
||||
char *filepath_txt = r_str_newf ("%s/%s.r2ai.txt", dir, name);
|
||||
char *filepath_md = r_str_newf ("%s/%s.r2ai.md", dir, name);
|
||||
char *filepath = NULL;
|
||||
if (r_file_exists (filepath_txt)) {
|
||||
filepath = filepath_txt;
|
||||
} else if (r_file_exists (filepath_md)) {
|
||||
filepath = filepath_md;
|
||||
}
|
||||
if (filepath) {
|
||||
R2AIPrompt *prompt = parse_prompt_file (filepath);
|
||||
if (prompt) {
|
||||
r_cons_printf (core->cons, "%s: %s - %s\n", name, prompt->title? prompt->title: "", prompt->desc? prompt->desc: "");
|
||||
r2aiprompt_free (prompt);
|
||||
} else {
|
||||
r_cons_println (core->cons, name);
|
||||
}
|
||||
}
|
||||
free (name);
|
||||
free (filepath_txt);
|
||||
free (filepath_md);
|
||||
}
|
||||
}
|
||||
r_list_free (files);
|
||||
}
|
||||
prompt_list (core, search_dirs, false);
|
||||
} else {
|
||||
// run prompt
|
||||
char *name = strdup (input);
|
||||
@@ -216,26 +342,9 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
r_str_trim (extra);
|
||||
}
|
||||
r_str_trim (name);
|
||||
char *filepath = NULL;
|
||||
RListIter *dir_iter;
|
||||
char *dir;
|
||||
r_list_foreach (search_dirs, dir_iter, dir) {
|
||||
char *filepath_txt = r_str_newf ("%s/%s.r2ai.txt", dir, name);
|
||||
char *filepath_md = r_str_newf ("%s/%s.r2ai.md", dir, name);
|
||||
if (r_file_exists (filepath_txt)) {
|
||||
filepath = filepath_txt;
|
||||
free (filepath_md);
|
||||
break;
|
||||
} else if (r_file_exists (filepath_md)) {
|
||||
filepath = filepath_md;
|
||||
free (filepath_txt);
|
||||
break;
|
||||
}
|
||||
free (filepath_txt);
|
||||
free (filepath_md);
|
||||
}
|
||||
char *filepath = find_prompt_file (search_dirs, name);
|
||||
if (!filepath) {
|
||||
R_LOG_ERROR ("Cannot find prompt file: %s.r2ai.txt or %s.r2ai.md", name, name);
|
||||
R_LOG_ERROR ("Cannot find prompt file: %s.r2ai.md", name);
|
||||
r_list_free (search_dirs);
|
||||
free (name);
|
||||
return;
|
||||
@@ -248,8 +357,8 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
free (name);
|
||||
return;
|
||||
}
|
||||
if (!prompt->title || !prompt->command) {
|
||||
R_LOG_WARN ("Prompt %s is missing required Title or Command directive", name);
|
||||
if (!prompt->command) {
|
||||
R_LOG_WARN ("Prompt %s is missing required Command directive", name);
|
||||
}
|
||||
free (filepath);
|
||||
// run commands
|
||||
@@ -306,3 +415,20 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
r_list_free (search_dirs);
|
||||
}
|
||||
|
||||
R_API char *r2ai_load_prompt_text(RCore *core, const char *name) {
|
||||
RList *search_dirs = prompt_search_dirs (core);
|
||||
char *filepath = find_prompt_file (search_dirs, name);
|
||||
char *prompt_text = NULL;
|
||||
if (filepath) {
|
||||
R2AIPrompt *prompt = parse_prompt_file (filepath);
|
||||
if (prompt && prompt->prompt) {
|
||||
prompt_text = strdup (prompt->prompt);
|
||||
}
|
||||
r2aiprompt_free (prompt);
|
||||
free (filepath);
|
||||
}
|
||||
|
||||
r_list_free (search_dirs);
|
||||
return prompt_text;
|
||||
}
|
||||
+53
-5
@@ -14,6 +14,9 @@ Decompile the current function.
|
||||
.B \-d \fIquery\fR
|
||||
Ask a question about the current function.
|
||||
.TP
|
||||
.B \-do
|
||||
Decompile the current function with source offsets in the left column.
|
||||
.TP
|
||||
.B \-dr
|
||||
Decompile the current function with one level of recursivity.
|
||||
.TP
|
||||
@@ -26,6 +29,15 @@ Set configuration option (same as 'r2ai.' prefix).
|
||||
.B \-h
|
||||
Show help message.
|
||||
.TP
|
||||
.B \-f \fIfile\fR
|
||||
Load a file with the radare2 API before running scripts, commands, or queries.
|
||||
.TP
|
||||
.B \-s \fIaddr\fR
|
||||
Seek to an address after loading the file specified with \fB\-f\fR.
|
||||
.TP
|
||||
.B \-c \fIcommand\fR
|
||||
Execute a radare2 command after loading the file. Can be used multiple times.
|
||||
.TP
|
||||
.B \-i \fIfile\fR \fIquery\fR
|
||||
Read a file and ask the LLM with the given query.
|
||||
.TP
|
||||
@@ -38,14 +50,20 @@ Set LLM provider (openai, anthropic, gemini, etc.).
|
||||
.B \-q
|
||||
List available query prompts.
|
||||
.TP
|
||||
.B \-qj
|
||||
List available query prompts in JSON.
|
||||
.TP
|
||||
.B \-q \fIname\fR [\fIinstructions\fR]
|
||||
Run predefined prompt with optional custom instructions.
|
||||
.TP
|
||||
.B \-r
|
||||
Enter the chat REPL.
|
||||
.TP
|
||||
.B \-w
|
||||
Launch the interactive setup wizard.
|
||||
.TP
|
||||
.B \-L
|
||||
Show chat logs (use -Lj for JSON). Only for auto mode.
|
||||
Show chat logs (use -Lj for JSON).
|
||||
.TP
|
||||
.B \-L-\fIN\fR
|
||||
Delete the last N messages from the chat history.
|
||||
@@ -68,7 +86,13 @@ Name of the provider (e.g., openai). List possibilities with \fBr2ai -e r2ai.api
|
||||
Model name. List possibilities with \fBr2ai -e r2ai.model=?\fR.
|
||||
.TP
|
||||
.B r2ai.baseurl
|
||||
Remote LLM base URL (e.g., http://127.0.0.1:11434).
|
||||
Provider API root. A /v1 suffix selects OpenAI-compatible chat completions; /api selects native Ollama.
|
||||
.TP
|
||||
.B r2ai.apitype
|
||||
Use chat completions (default) or the native Ollama generate operation.
|
||||
.TP
|
||||
.B r2ai.cacheck
|
||||
Warn when chat requests are not append-only and may miss provider prompt-cache hits.
|
||||
.TP
|
||||
.B r2ai.max_tokens
|
||||
Maximum output tokens or total tokens.
|
||||
@@ -88,17 +112,29 @@ Language for AI responses.
|
||||
.B r2ai.prompt
|
||||
User prompt for \fBr2ai -d\fR.
|
||||
.TP
|
||||
.B r2ai.clippy
|
||||
Display chat replies using r2clippy instead of plain text.
|
||||
.TP
|
||||
.B r2ai.markdown
|
||||
Render LLM responses with the radare2 markdown colorizer (default: true).
|
||||
.TP
|
||||
.B r2ai.wizard
|
||||
Auto-start the setup wizard on the first interactive chat session.
|
||||
.TP
|
||||
.B r2ai.auto.max_runs
|
||||
Maximum loops in auto mode (\fBr2ai -a\fR).
|
||||
.TP
|
||||
.B r2ai.auto.hide_tool_output
|
||||
Hide tool output in auto mode.
|
||||
.B r2ai.auto.verbose
|
||||
Show tool output in auto mode.
|
||||
.TP
|
||||
.B r2ai.auto.yolo
|
||||
Allow AI to run commands without approval (dangerous, default false).
|
||||
.TP
|
||||
.B r2ai.http.use_files
|
||||
Use files for arguments if CURL argument too long.
|
||||
.TP
|
||||
.B r2ai.http.headers
|
||||
Extra HTTP headers added to every provider request, one per line ('\\n' separated). Defaults to the R2AI_HEADERS environment variable.
|
||||
.SH QUERY PROMPTS
|
||||
Query prompts are predefined templates for common tasks. Available prompts:
|
||||
.TP
|
||||
@@ -133,6 +169,9 @@ Find vulnerabilities in the current function.
|
||||
Decompile current function:
|
||||
.B r2ai -d
|
||||
.TP
|
||||
Decompile current function with source offsets:
|
||||
.B r2ai -do
|
||||
.TP
|
||||
Ask about current function:
|
||||
.B r2ai -d "what is the input buffer size?"
|
||||
.TP
|
||||
@@ -142,12 +181,21 @@ Use auto mode:
|
||||
List prompts:
|
||||
.B r2ai -q
|
||||
.TP
|
||||
List prompts as JSON:
|
||||
.B r2ai -qj
|
||||
.TP
|
||||
Run a prompt:
|
||||
.B r2ai -q explain
|
||||
.TP
|
||||
Set provider:
|
||||
.B r2ai -p openai
|
||||
.TP
|
||||
Decompile a function from the shell:
|
||||
.B r2ai -f ./program -s main -d
|
||||
.TP
|
||||
Ask about a function from the shell:
|
||||
.B r2ai -f ./program -s main -d "what does this function do?"
|
||||
.TP
|
||||
Enter REPL:
|
||||
.B r2ai -r
|
||||
.SH SEE ALSO
|
||||
@@ -155,4 +203,4 @@ Enter REPL:
|
||||
.BR r2pm (1),
|
||||
.BR r2agent (1)
|
||||
.SH AUTHOR
|
||||
radare2 community
|
||||
radare2 community
|
||||
+478
-219
@@ -1,36 +1,75 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake */
|
||||
/* r2ai - Copyright 2023-2026 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai"
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
// clang-format off
|
||||
static RCoreHelpMessage help_msg_r2ai = {
|
||||
"Usage:",
|
||||
"r2ai", " [-args] [...]",
|
||||
"Usage:", "r2ai", " [-args] [...]",
|
||||
"r2ai", " -w", "Launch the interactive setup wizard",
|
||||
"r2ai", " -d", "Decompile current function",
|
||||
"r2ai", " -d [query]", "Ask a question on the current function",
|
||||
"r2ai", " -do", "Decompile current function with source offsets",
|
||||
"r2ai", " -dr", "Decompile current function (+ 1 level of recursivity)",
|
||||
"r2ai", " -a [query]", "Resolve question using auto mode",
|
||||
"r2ai", " -b [url]", "set/show base URL for provider API",
|
||||
"r2ai", " -e (k(=v))", "Same as '-e r2ai.'",
|
||||
"r2ai", " -E", "Edit the r2ai rc file",
|
||||
"r2ai", " -K", "Edit the API keys file",
|
||||
"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", " -id [hint]", "generate SOUL.md and IDENTITY.md personality files (optional hint)",
|
||||
"r2ai", " -ide", "edit SOUL.md and IDENTITY.md personality files",
|
||||
"r2ai", " -id-", "delete SOUL.md and IDENTITY.md personality files",
|
||||
"r2ai", " -m [model]", "show selected model, list suggested ones, choose one",
|
||||
"r2ai", " -p [provider]", "set LLM provider (openai, anthropic, gemini, etc.)",
|
||||
"r2ai", " -q", "list available query prompts",
|
||||
"r2ai", " -qj", "list available query prompts in JSON",
|
||||
"r2ai", " -q [name] (inst)", "run predefined prompt with optional instructions",
|
||||
"r2ai", " -r", "enter the chat repl",
|
||||
"r2ai", " -L", "show chat logs (See -Lj for json). Only for auto mode.",
|
||||
"r2ai", " -s[?ijynak*]", "async task queue: list, show, interact, approve, decline, kill (see r2ai -s?)",
|
||||
"r2ai", " -L", "show chat logs (See -Lj for json)",
|
||||
"r2ai", " -C", "compact conversation history",
|
||||
"r2ai", " -LR", "create a log report",
|
||||
"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", " [query]", "query the selected model+provider with the given query",
|
||||
NULL
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static char *r2ai_default_auto_prompt(void) {
|
||||
return strdup (
|
||||
"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");
|
||||
}
|
||||
|
||||
R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
if (R_STR_ISEMPTY (args.input) && !args.messages) {
|
||||
if (args.error) {
|
||||
*args.error = r_str_newf ("Usage: r2ai [-h] [prompt]");
|
||||
@@ -38,77 +77,195 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (R_STR_ISEMPTY (args.system_prompt)) {
|
||||
args.system_prompt = r_config_get (core->config, "r2ai.system");
|
||||
RList *msgs = args.messages;
|
||||
bool own_msgs = false;
|
||||
if (args.input || !msgs) {
|
||||
msgs = r2ai_msgs_new ();
|
||||
own_msgs = true;
|
||||
if (args.messages) {
|
||||
RListIter *iter;
|
||||
R2AI_Message *m;
|
||||
r_list_foreach (args.messages, iter, m) {
|
||||
r2ai_msgs_add (msgs, m);
|
||||
}
|
||||
}
|
||||
if (args.input) {
|
||||
R2AI_Message m = { .role = "user", .content = (char *)args.input };
|
||||
r2ai_msgs_add (msgs, &m);
|
||||
}
|
||||
args.messages = msgs;
|
||||
}
|
||||
|
||||
R2AI_Messages *msgs = r2ai_msgs_new ();
|
||||
|
||||
if (args.input) {
|
||||
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
|
||||
r2ai_msgs_add (msgs, &msg);
|
||||
}
|
||||
|
||||
args.messages = msgs;
|
||||
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (cps, args);
|
||||
if (!res) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Extract content from the response
|
||||
char *content = NULL;
|
||||
|
||||
// If content is present in the result message, use it
|
||||
if (res->message) {
|
||||
if (res->message->content) {
|
||||
R2AI_ChatResponse *res = r2ai_llmcall (cps, args);
|
||||
if (res) {
|
||||
if (res->message && res->message->content) {
|
||||
content = strdup (res->message->content);
|
||||
}
|
||||
r2ai_chat_response_free (res);
|
||||
}
|
||||
|
||||
// Free the message properly using r2ai_message_free
|
||||
if (res->message) {
|
||||
r2ai_message_free ((R2AI_Message *)res->message);
|
||||
if (own_msgs) {
|
||||
r2ai_msgs_free (msgs);
|
||||
}
|
||||
|
||||
// Free the response struct itself
|
||||
free (res);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool recursive) {
|
||||
RCore *core = cps->core;
|
||||
const char *prompt = r_config_get (core->config, "r2ai.prompt");
|
||||
const char *lang = r_config_get (core->config, "r2ai.lang");
|
||||
char *full_prompt;
|
||||
if (!R_STR_ISEMPTY (input)) {
|
||||
R_LOG_DEBUG ("User question: %s", input);
|
||||
full_prompt = strdup (input);
|
||||
} else {
|
||||
if (!R_STR_ISEMPTY (lang)) {
|
||||
full_prompt = r_str_newf ("%s. Translate the code into %s programming language.", prompt, lang);
|
||||
} else {
|
||||
full_prompt = strdup (prompt);
|
||||
static bool r2ai_cmd_is_word(const char *cmd, const char *word) {
|
||||
const size_t len = strlen (word);
|
||||
return !strncmp (cmd, word, len) && (!cmd[len] || IS_WHITESPACE (cmd[len]));
|
||||
}
|
||||
|
||||
static char *r2ai_offset_cmd(const char *cmd) {
|
||||
char *trimmed = strdup (cmd);
|
||||
r_str_trim (trimmed);
|
||||
const char *replacement = NULL;
|
||||
if (r2ai_cmd_is_word (trimmed, "pdc")) {
|
||||
replacement = "pdco";
|
||||
} else if (r2ai_cmd_is_word (trimmed, "pdg")) {
|
||||
replacement = "pdgo";
|
||||
} else if (r2ai_cmd_is_word (trimmed, "pdd")) {
|
||||
replacement = "pddo";
|
||||
}
|
||||
if (replacement) {
|
||||
char *res = r_str_newf ("%s%s", replacement, trimmed + 3);
|
||||
free (trimmed);
|
||||
return res;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
static void r2ai_collect_offsets(RList *offsets, const char *text, int *width) {
|
||||
const char *p = text;
|
||||
while (p && *p) {
|
||||
const char *line = r_str_trim_head_ro (p);
|
||||
if (r_str_startswith (line, "0x")) {
|
||||
const char *q = line + 2;
|
||||
while (IS_HEXCHAR (*q)) {
|
||||
q++;
|
||||
}
|
||||
if (q > line + 2) {
|
||||
int len = q - line;
|
||||
r_list_append (offsets, r_str_ndup (line, len));
|
||||
if (len > *width) {
|
||||
*width = len;
|
||||
}
|
||||
}
|
||||
}
|
||||
p = strchr (p, '\n');
|
||||
if (p) {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char *r2ai_next_offset(RListIter **iter) {
|
||||
if (!iter || !*iter) {
|
||||
return NULL;
|
||||
}
|
||||
const char *addr = (*iter)->data;
|
||||
*iter = (*iter)->n;
|
||||
return addr;
|
||||
}
|
||||
|
||||
static bool r2ai_line_has_statement(const char *line) {
|
||||
const char *trimmed = r_str_trim_head_ro (line);
|
||||
if (R_STR_ISEMPTY (trimmed)) {
|
||||
return false;
|
||||
}
|
||||
if (!strcmp (trimmed, "{") || !strcmp (trimmed, "}") || !strcmp (trimmed, "};")) {
|
||||
return false;
|
||||
}
|
||||
if (*trimmed == '#') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static char *r2ai_apply_offsets(const char *text, RList *offsets, int width) {
|
||||
if (R_STR_ISEMPTY (text) || !offsets || r_list_empty (offsets)) {
|
||||
return text? strdup (text): NULL;
|
||||
}
|
||||
if (width < 10) {
|
||||
width = 10;
|
||||
}
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
RListIter *offiter = offsets->head;
|
||||
const char *p = text;
|
||||
while (p && *p) {
|
||||
const char *nl = strchr (p, '\n');
|
||||
int len = nl? (int)(nl - p): (int)strlen (p);
|
||||
char *line = r_str_ndup (p, len);
|
||||
const char *addr = r2ai_line_has_statement (line)? r2ai_next_offset (&offiter): NULL;
|
||||
r_strbuf_appendf (sb, "%*s | %s\n", width, addr? addr: "", line);
|
||||
free (line);
|
||||
p = nl? nl + 1: NULL;
|
||||
}
|
||||
return r_strbuf_drain (sb);
|
||||
}
|
||||
|
||||
// query the llm with rag and log the error if any, returns NULL on failure
|
||||
// when r2ai.history is set the exchange is kept in the conversation (see -L, -R)
|
||||
static char *ask(RCorePluginSession *cps, const char *input, bool chat) {
|
||||
R2AI_State *state = cps->data;
|
||||
RList *history = (chat && r_config_get_b (cps->core->config, "r2ai.history"))
|
||||
? r2ai_conversation_get (state)
|
||||
: NULL;
|
||||
char *err = NULL;
|
||||
char *res = r2ai (cps, (R2AIArgs){ .input = input, .messages = history, .error = &err, .dorag = true });
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
free (err);
|
||||
R_FREE (res);
|
||||
}
|
||||
if (history && res) {
|
||||
R2AI_Message user = { .role = "user", .content = (char *)input };
|
||||
R2AI_Message assistant = { .role = "assistant", .content = res };
|
||||
r2ai_msgs_add (history, &user);
|
||||
r2ai_msgs_add (history, &assistant);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void add_block(RStrBuf *sb, char *dec, RList *offsets, int *width) {
|
||||
if (offsets) {
|
||||
r2ai_collect_offsets (offsets, dec, width);
|
||||
}
|
||||
r_strbuf_appendf (sb, "\n[BEGIN]\n%s[END]\n", dec);
|
||||
free (dec);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool recursive, const bool offsets) {
|
||||
RCore *core = cps->core;
|
||||
const char *lang = r_config_get (core->config, "r2ai.lang");
|
||||
RStrBuf *sb = r_strbuf_new (R_STR_ISNOTEMPTY (input)? input: r_config_get (core->config, "r2ai.prompt"));
|
||||
if (R_STR_ISEMPTY (input) && R_STR_ISNOTEMPTY (lang)) {
|
||||
r_strbuf_appendf (sb, ". Translate the code into %s programming language.", lang);
|
||||
}
|
||||
char *cmds = strdup (r_config_get (core->config, "r2ai.cmds"));
|
||||
RStrBuf *sb = r_strbuf_new (full_prompt);
|
||||
RList *cmdslist = r_str_split_list (cmds, ",", -1);
|
||||
RListIter *iter;
|
||||
const char *cmd;
|
||||
RList *refslist = NULL;
|
||||
char *refs = NULL;
|
||||
RList *offsetslist = offsets? r_list_newf (free): NULL;
|
||||
char *offset_fallback = NULL;
|
||||
int offset_width = 0;
|
||||
if (recursive) {
|
||||
char *refs = r_core_cmd_str (core, "axff~^C[2]~$$");
|
||||
refs = r_core_cmd_str (core, "axff~^C[2]~$$");
|
||||
refslist = r_str_split_list (refs, ",", 0);
|
||||
free (refs);
|
||||
}
|
||||
RConfigHold *hold = r_config_hold_new (core->config);
|
||||
r_config_hold (hold, "scr.color", "scr.utf8", NULL);
|
||||
r_config_set_i (core->config, "scr.color", 0);
|
||||
r_config_set_b (core->config, "scr.utf8", false);
|
||||
r_list_foreach (cmdslist, iter, cmd) {
|
||||
char *dec = r_core_cmd_str (core, cmd);
|
||||
r_strbuf_append (sb, "\n[BEGIN]\n");
|
||||
r_strbuf_append (sb, dec);
|
||||
r_strbuf_append (sb, "[END]\n");
|
||||
free (dec);
|
||||
char *ocmd = offsets? r2ai_offset_cmd (cmd): NULL;
|
||||
const char *dcmd = ocmd? ocmd: cmd;
|
||||
char *dec = r_core_cmd_str (core, dcmd);
|
||||
if (offsets && !offset_fallback) {
|
||||
offset_fallback = strdup (dec);
|
||||
}
|
||||
add_block (sb, dec, offsetslist, &offset_width);
|
||||
if (recursive) {
|
||||
RListIter *iter2;
|
||||
char *at;
|
||||
@@ -117,35 +274,72 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
|
||||
if (core->num->nc.errors) {
|
||||
continue;
|
||||
}
|
||||
char *dec = r_core_cmd_str_at (core, n, cmd);
|
||||
r_strbuf_append (sb, "\n[BEGIN]\n");
|
||||
r_strbuf_append (sb, dec);
|
||||
r_strbuf_append (sb, "[END]\n");
|
||||
free (dec);
|
||||
add_block (sb, r_core_cmd_str_at (core, n, dcmd), offsetslist, &offset_width);
|
||||
}
|
||||
}
|
||||
free (ocmd);
|
||||
}
|
||||
r_config_hold_restore (hold);
|
||||
r_config_hold_free (hold);
|
||||
r_list_free (refslist);
|
||||
free (refs);
|
||||
char *s = r_strbuf_drain (sb);
|
||||
char *error = NULL;
|
||||
R2AIArgs d_args = { .input = s, .error = &error, .dorag = true };
|
||||
char *res = r2ai (cps, d_args);
|
||||
free (s);
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
free (error);
|
||||
if (r_config_get_b (core->config, "r2ai.async")) {
|
||||
const char *q = r_str_get (input);
|
||||
char *title = r_str_newf ("-d%s%s%s%s", recursive? "r": "", offsets? "o": "", *q? " ": "", q);
|
||||
r2ai_async_query (cps, title, s, NULL);
|
||||
free (title);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
char *res = ask (cps, s, true);
|
||||
if (res && offsets) {
|
||||
char *ores = R_STR_ISNOTEMPTY (res)
|
||||
? r2ai_apply_offsets (res, offsetslist, offset_width)
|
||||
: (offset_fallback? strdup (offset_fallback): NULL);
|
||||
if (ores) {
|
||||
r_cons_printf (core->cons, "%s\n", ores);
|
||||
free (ores);
|
||||
}
|
||||
} else if (res) {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
}
|
||||
free (res);
|
||||
}
|
||||
free (res);
|
||||
free (s);
|
||||
free (offset_fallback);
|
||||
r_list_free (offsetslist);
|
||||
r_list_free (cmdslist);
|
||||
free (cmds);
|
||||
}
|
||||
|
||||
R_IPI void r2ai_print_response(RCore *core, const char *text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
#if R2_VERSION_NUMBER >= 60106
|
||||
if (r_config_get_b (core->config, "r2ai.markdown")) {
|
||||
char *md = r_core_md2txt (core, text, false);
|
||||
if (md) {
|
||||
r_cons_print (core->cons, md);
|
||||
if (!r_str_endswith (md, "\n")) {
|
||||
r_cons_newline (core->cons);
|
||||
}
|
||||
free (md);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
r_cons_println (core->cons, text);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
RConfigHold *hold = r_config_hold_new (core->config);
|
||||
r_config_hold (hold, "r2ai.clippy", NULL);
|
||||
r_config_set_b (core->config, "r2ai.clippy", true);
|
||||
r2ai_wizard_autorun (core);
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
while (true) {
|
||||
r_line_set_prompt (core->cons->line, ">>> ");
|
||||
r_line_set_prompt (core->cons->line, "[r2ai]> ");
|
||||
const char *ptr = r_line_readline (core->cons);
|
||||
if (R_STR_ISEMPTY (ptr)) {
|
||||
break;
|
||||
@@ -164,58 +358,49 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
|
||||
}
|
||||
}
|
||||
r_strbuf_appendf (sb, "User: %s\n", ptr);
|
||||
const char *a = r_strbuf_tostring (sb);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = a, .error = &error, .dorag = true });
|
||||
if (error) {
|
||||
R_LOG_ERROR ("%s", error);
|
||||
free (error);
|
||||
} else if (res) {
|
||||
char *res = ask (cps, r_strbuf_tostring (sb), false);
|
||||
if (res) {
|
||||
r_strbuf_appendf (sb, "Assistant: %s\n", res);
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
if (r_config_get_b (core->config, "r2ai.clippy")) {
|
||||
char *cmd = r_str_newf ("?E %s", res);
|
||||
r_core_call (core, cmd);
|
||||
free (cmd);
|
||||
} else {
|
||||
r2ai_print_response (core, res);
|
||||
}
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
free (res);
|
||||
}
|
||||
r_strbuf_free (sb);
|
||||
r_config_hold_restore (hold);
|
||||
r_config_hold_free (hold);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_R(RCorePluginSession *cps, const char *q) {
|
||||
static void cmd_r2ai_R(RCorePluginSession *cps, const char *q, bool refresh) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!r_config_get_b (core->config, "r2ai.data")) {
|
||||
if (refresh && !r_config_get_b (core->config, "r2ai.data")) {
|
||||
R_LOG_ERROR ("r2ai -e r2ai.data=true");
|
||||
return;
|
||||
}
|
||||
if (R_STR_ISEMPTY (q)) {
|
||||
if (state->db) {
|
||||
r_vdb_free (state->db);
|
||||
state->db = NULL;
|
||||
}
|
||||
if (refresh || !state->db) {
|
||||
r2ai_refresh_embeddings (cps);
|
||||
} else {
|
||||
r2ai_refresh_embeddings (cps);
|
||||
const int K = r_config_get_i (core->config, "r2ai.data.nth");
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, q, K);
|
||||
|
||||
if (rs) {
|
||||
R_LOG_DEBUG ("Query: \"%s\"", q);
|
||||
R_LOG_DEBUG ("Found up to %d neighbors (actual found: %d)", K, rs->size);
|
||||
int i;
|
||||
for (i = 0; i < rs->size; i++) {
|
||||
RVdbResult *r = &rs->results[i];
|
||||
KDNode *n = r->node;
|
||||
float dist_sq = r->dist_sq;
|
||||
float cos_sim = 1.0f - (dist_sq * 0.5f); // for normalized vectors
|
||||
printf ("%2d) dist_sq=%.4f cos_sim=%.4f text=\"%s\"\n", i + 1, dist_sq,
|
||||
cos_sim, (n->text? n->text: "(null)"));
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
} else {
|
||||
R_LOG_ERROR ("No vdb results found");
|
||||
}
|
||||
}
|
||||
if (R_STR_ISEMPTY (q)) {
|
||||
return;
|
||||
}
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, q, r_config_get_i (core->config, "r2ai.data.nth"));
|
||||
if (!rs) {
|
||||
R_LOG_ERROR ("No vdb results found");
|
||||
return;
|
||||
}
|
||||
int i;
|
||||
for (i = 0; i < rs->size; i++) {
|
||||
RVdbResult *r = &rs->results[i];
|
||||
r_cons_printf (core->cons, "%2d) %.4f %s\n", i + 1, r->dist_sq, r->node->text);
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
@@ -226,25 +411,19 @@ static void cmd_r2ai_i(RCorePluginSession *cps, const char *arg) {
|
||||
*query++ = 0;
|
||||
}
|
||||
char *s = r_file_slurp (fname, NULL);
|
||||
if (R_STR_ISNOTEMPTY (s)) {
|
||||
if (!s) {
|
||||
R_LOG_ERROR ("Cannot open %s", fname);
|
||||
free (fname);
|
||||
return;
|
||||
}
|
||||
char *q = r_str_newf ("%s\n```\n%s\n```\n", query, s);
|
||||
char *error = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
|
||||
const char *prompt = R_STR_ISNOTEMPTY (query)? query: "Analyze the contents of the following file:";
|
||||
char *q = r_str_newf ("%s\n```\n%s\n```\n", prompt, s);
|
||||
free (s);
|
||||
if (error) {
|
||||
R_LOG_ERROR (error);
|
||||
free (error);
|
||||
} else {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
}
|
||||
char *res = ask (cps, q, true);
|
||||
free (q);
|
||||
r2ai_print_response (core, res);
|
||||
free (fname);
|
||||
free (res);
|
||||
free (q);
|
||||
}
|
||||
|
||||
static void cmd_r2ai_m(RCorePluginSession *cps, const char *input) {
|
||||
@@ -256,50 +435,63 @@ static void cmd_r2ai_m(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
}
|
||||
|
||||
static void load_embeddings(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
RListIter *iter, *iter2;
|
||||
char *line;
|
||||
char *file;
|
||||
// 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) {
|
||||
RList *lines = r_str_split_list (text, "\n", -1);
|
||||
r_list_foreach (lines, iter2, line) {
|
||||
if (r_str_trim_head_ro (line)[0] == 0) {
|
||||
static bool load_r2airc(RCorePluginSession *cps) {
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
if (rc_path && r_file_exists (rc_path)) {
|
||||
char *content = r_file_slurp (rc_path, NULL);
|
||||
if (content) {
|
||||
RList *lines = r_str_split_list (content, "\n", -1);
|
||||
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;
|
||||
}
|
||||
r_vdb_insert (state->db, line);
|
||||
R_LOG_DEBUG ("Insert %s", line);
|
||||
if (r_str_startswith (trimmed, "r2ai -")) {
|
||||
trimmed += strlen ("r2ai ");
|
||||
}
|
||||
if (*trimmed == '-') {
|
||||
cmd_r2ai (cps, trimmed);
|
||||
} else {
|
||||
R_LOG_ERROR ("Invalid line in r2ai rc: %s", trimmed);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
free (content);
|
||||
}
|
||||
free (filepath);
|
||||
}
|
||||
r_list_free (files);
|
||||
free (rc_path);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
static bool cmd_r2ai_id(RCorePluginSession *cps, const char *input) {
|
||||
if (r_str_startswith (input, "-id-")) {
|
||||
r2ai_claw_delete (cps);
|
||||
} else if (r_str_startswith (input, "-ide")) {
|
||||
r2ai_claw_edit (cps);
|
||||
} else if (r_str_startswith (input, "-id")) {
|
||||
r2ai_claw_create (cps, r_str_trim_head_ro (input + 3));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (*input == '?' || r_str_startswith (input, "-h")) {
|
||||
r_core_cmd_help (core, help_msg_r2ai);
|
||||
r2ai_cmd_help (core, help_msg_r2ai);
|
||||
} else if (r_str_startswith (input, "-w")) {
|
||||
r2ai_wizard (core);
|
||||
} else if (r_str_startswith (input, "-K")) {
|
||||
r2ai_apikeys_edit (cps);
|
||||
} else if (r_str_startswith (input, "-E")) {
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
r_cons_editor (core->cons, rc_path, NULL);
|
||||
r2ai_cons_editor (core->cons, rc_path, NULL);
|
||||
free (rc_path);
|
||||
load_r2airc (cps);
|
||||
} else if (r_str_startswith (input, "-e")) {
|
||||
const char *arg = r_str_trim_head_ro (input + 2);
|
||||
if (r_str_startswith (arg, "r2ai")) {
|
||||
@@ -308,56 +500,63 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
r_core_cmdf (core, "-e r2ai.%s", arg);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-a")) {
|
||||
cmd_r2ai_a (cps, r_str_trim_head_ro (input + 2));
|
||||
const char *q = r_str_trim_head_ro (input + 2);
|
||||
if (r_config_get_b (core->config, "r2ai.async")) {
|
||||
char *title = r_str_newf ("-a %s", q);
|
||||
r2ai_async_auto (cps, title, q, NULL);
|
||||
free (title);
|
||||
} else {
|
||||
cmd_r2ai_a (cps, q);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-b")) {
|
||||
const char *baseurl = r_str_trim_head_ro (input + 2);
|
||||
if (R_STR_ISEMPTY (baseurl)) {
|
||||
r_cons_printf (core->cons, "%s\n", r_config_get (core->config, "r2ai.baseurl"));
|
||||
} else {
|
||||
r_config_set (core->config, "r2ai.baseurl", baseurl);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-L-")) {
|
||||
const char *arg = r_str_trim_head_ro (input + 3);
|
||||
const int N = atoi (arg);
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history available\n");
|
||||
} else {
|
||||
r2ai_delete_last_messages (messages, N);
|
||||
r_cons_printf (core->cons, "Deleted %d message%s from chat history\n", N > 0? N: 1,
|
||||
(N > 0 && N != 1)? "s": "");
|
||||
r_cons_printf (core->cons, "Deleted %d message%s from chat history\n", N > 0? N: 1, (N > 0 && N != 1)? "s": "");
|
||||
}
|
||||
} else if (r_str_startswith (input, "-C")) {
|
||||
cmd_r2ai_c (cps);
|
||||
} else if (r_str_startswith (input, "-s")) {
|
||||
r2ai_async_cmd (cps, input + 2);
|
||||
} else if (r_str_startswith (input, "-LR")) {
|
||||
cmd_r2ai_lr (cps);
|
||||
} else if (r_str_startswith (input, "-L")) {
|
||||
cmd_r2ai_logs (cps);
|
||||
} else if (r_str_startswith (input, "-d")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 2), false);
|
||||
cmd_r2ai_logs (cps, input + 2);
|
||||
} else if (r_str_startswith (input, "-do")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 3), false, true);
|
||||
} else if (r_str_startswith (input, "-dr")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 2), true);
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 3), true, false);
|
||||
} else if (r_str_startswith (input, "-d")) {
|
||||
cmd_r2ai_d (cps, r_str_trim_head_ro (input + 2), false, false);
|
||||
} else if (r_str_startswith (input, "-S")) {
|
||||
if (state->db == NULL) {
|
||||
state->db = r_vdb_new (R2AI_DEFAULT_VECTORS);
|
||||
load_embeddings (cps);
|
||||
}
|
||||
const char *arg = r_str_trim_head_ro (input + 2);
|
||||
const int K = 10;
|
||||
RVdbResultSet *rs = r_vdb_query (state->db, arg, K);
|
||||
if (rs) {
|
||||
int i;
|
||||
eprintf ("Found up to %d neighbors (actual found: %d).\n", K, rs->size);
|
||||
for (i = 0; i < rs->size; i++) {
|
||||
RVdbResult *r = &rs->results[i];
|
||||
KDNode *n = r->node;
|
||||
r_cons_printf (core->cons, "- (%.4f) %s\n", r->dist_sq, n->text);
|
||||
}
|
||||
r_vdb_result_free (rs);
|
||||
}
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 2), false);
|
||||
} else if (cmd_r2ai_id (cps, input)) {
|
||||
} else if (r_str_startswith (input, "-i")) {
|
||||
cmd_r2ai_i (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-r")) {
|
||||
cmd_r2ai_repl (cps);
|
||||
} else if (r_str_startswith (input, "-Rq")) {
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 3), true);
|
||||
} else if (r_str_startswith (input, "-R")) {
|
||||
R2AI_Messages *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_length (messages->messages) == 0) {
|
||||
RList *messages = r2ai_conversation_get (state);
|
||||
if (!messages || r_list_empty (messages)) {
|
||||
r_cons_printf (core->cons, "No conversation history to reset\n");
|
||||
} else {
|
||||
r2ai_msgs_clear (messages);
|
||||
R_FREE (state->cache_prefix);
|
||||
r_cons_printf (core->cons, "Chat conversation context has been reset\n");
|
||||
}
|
||||
} else if (r_str_startswith (input, "-Rq")) {
|
||||
cmd_r2ai_R (cps, r_str_trim_head_ro (input + 3));
|
||||
} else if (r_str_startswith (input, "-m")) {
|
||||
cmd_r2ai_m (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-p")) {
|
||||
@@ -367,20 +566,18 @@ static void cmd_r2ai(RCorePluginSession *cps, const char *input) {
|
||||
} else {
|
||||
r_config_set (core->config, "r2ai.api", provider);
|
||||
}
|
||||
} else if (r_str_startswith (input, "-qj")) {
|
||||
r2ai_cmd_qj (cps, r_str_trim_head_ro (input + 3));
|
||||
} else if (r_str_startswith (input, "-q")) {
|
||||
r2ai_cmd_q (cps, r_str_trim_head_ro (input + 2));
|
||||
} else if (r_str_startswith (input, "-")) {
|
||||
r_core_cmd_help (core, help_msg_r2ai);
|
||||
r2ai_cmd_help (core, help_msg_r2ai);
|
||||
} else {
|
||||
char *err = NULL;
|
||||
char *res =
|
||||
r2ai (cps, (R2AIArgs){ .input = input, .error = &err, .dorag = true });
|
||||
if (err) {
|
||||
R_LOG_ERROR ("%s", err);
|
||||
R_FREE (err);
|
||||
}
|
||||
if (res) {
|
||||
r_cons_printf (core->cons, "%s\n", res);
|
||||
if (r_config_get_b (core->config, "r2ai.async")) {
|
||||
r2ai_async_query (cps, input, input, NULL);
|
||||
} else {
|
||||
char *res = ask (cps, input, true);
|
||||
r2ai_print_response (core, res);
|
||||
free (res);
|
||||
}
|
||||
}
|
||||
@@ -406,6 +603,9 @@ static bool cb_r2ai_model(void *user, void *data) {
|
||||
RConfigNode *node = (RConfigNode *)data;
|
||||
RCore *core = (RCore *)user;
|
||||
const char *api = r_config_get (core->config, "r2ai.api");
|
||||
if (R_STR_ISEMPTY (api)) {
|
||||
api = R2AI_DEFAULT_PROVIDER;
|
||||
}
|
||||
if (*node->value == '?') {
|
||||
// Try to fetch models dynamically first
|
||||
RList *models = r2ai_fetch_available_models (core, api);
|
||||
@@ -420,7 +620,7 @@ static bool cb_r2ai_model(void *user, void *data) {
|
||||
// Fallback to static lists if dynamic fetching fails
|
||||
const R2AIProvider *p = r2ai_get_provider (api);
|
||||
if (p) {
|
||||
if (p->uses_system_ls) {
|
||||
if (p->api_type == R2AI_API_OLLAMA) {
|
||||
char *s = r_sys_cmd_str ("ollama ls", NULL, NULL);
|
||||
if (s) {
|
||||
RList *items = r_str_split_list (s, "\n", 0);
|
||||
@@ -481,21 +681,32 @@ static bool cb_r2ai_http_backend(void *user, void *data) {
|
||||
// return false; // always return false to test
|
||||
}
|
||||
|
||||
static bool cb_r2ai_apitype(void *user, void *data) {
|
||||
RCore *core = (RCore *)user;
|
||||
RConfigNode *node = (RConfigNode *)data;
|
||||
if (*node->value == '?') {
|
||||
r_cons_println (core->cons, "chat");
|
||||
r_cons_println (core->cons, "generate");
|
||||
return false;
|
||||
}
|
||||
if (!strcmp (node->value, "chat") || !strcmp (node->value, "generate")) {
|
||||
return true;
|
||||
}
|
||||
R_LOG_ERROR ("Invalid apitype '%s'. Use chat or generate.", node->value);
|
||||
return false;
|
||||
}
|
||||
|
||||
R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
// Initialize state
|
||||
R2AI_State *state = R_NEW0 (R2AI_State);
|
||||
cps->data = state;
|
||||
state->prompt_auto = r2ai_default_auto_prompt ();
|
||||
r2ai_markdown_init (&state->markdown);
|
||||
|
||||
// Initialize conversation container
|
||||
r2ai_conversation_init (state);
|
||||
|
||||
char *rc_path = r_file_home (".config/r2ai/rc");
|
||||
if (rc_path && r_file_exists (rc_path)) {
|
||||
r_core_cmdf (core, ". %s", rc_path);
|
||||
}
|
||||
free (rc_path);
|
||||
|
||||
r_config_lock (core->config, false);
|
||||
r_config_set_cb (core->config, "r2ai.api", R2AI_DEFAULT_PROVIDER, &cb_r2ai_api);
|
||||
{
|
||||
@@ -511,13 +722,19 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_set_cb (core->config, "r2ai.model", R2AI_DEFAULT_MODEL, &cb_r2ai_model);
|
||||
r_config_desc (core->config, "r2ai.model", "Model identifier for the selected provider (e.g. gpt-5-mini)");
|
||||
r_config_set (core->config, "r2ai.baseurl", "");
|
||||
r_config_desc (core->config, "r2ai.baseurl", "Base URL for provider API (overrides default endpoints)");
|
||||
r_config_desc (core->config, "r2ai.baseurl", "Provider API root (/v1 for OpenAI-compatible chat or /api for native Ollama)");
|
||||
r_config_set_cb (core->config, "r2ai.apitype", "chat", &cb_r2ai_apitype);
|
||||
r_config_desc (core->config, "r2ai.apitype", "Chat completions (default) or native Ollama generate operation");
|
||||
r_config_set_b (core->config, "r2ai.cacheck", false);
|
||||
r_config_desc (core->config, "r2ai.cacheck", "Warn when chat requests are not append-only and may miss provider prompt-cache hits");
|
||||
r_config_set_i (core->config, "r2ai.max_tokens", 4096); // max output tokens, or max total tokens
|
||||
r_config_desc (core->config, "r2ai.max_tokens", "Maximum tokens for LLM responses (output/total depending on provider)");
|
||||
r_config_set_i (core->config, "r2ai.thinking_tokens", 0);
|
||||
r_config_desc (core->config, "r2ai.thinking_tokens", "Number of tokens reserved for internal thinking/context messages");
|
||||
r_config_set (core->config, "r2ai.temperature", "0.01");
|
||||
r_config_desc (core->config, "r2ai.temperature", "Sampling temperature for LLM output (0 = deterministic)");
|
||||
r_config_set_b (core->config, "r2ai.deterministic", false);
|
||||
r_config_desc (core->config, "r2ai.deterministic", "Remove randomness from responses by forcing greedy sampling parameters (overrides r2ai.temperature)");
|
||||
r_config_set (core->config, "r2ai.cmds", "pdc");
|
||||
r_config_desc (core->config, "r2ai.cmds", "Default command sequence used by automation (e.g. 'pdc')");
|
||||
r_config_set (core->config, "r2ai.lang", "C");
|
||||
@@ -533,74 +750,115 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
|
||||
r_config_set (core->config, "r2ai.hlang", "english");
|
||||
r_config_desc (core->config, "r2ai.hlang", "Human language for prompts/messages (e.g. english)");
|
||||
r_config_set (
|
||||
core->config, "r2ai.system",
|
||||
"You are a reverse engineer. The user is reversing a binary, using "
|
||||
"radare2. The user will ask questions about the binary and you will "
|
||||
"respond with the answer to the best of your ability.");
|
||||
core->config, "r2ai.system", "You are a reverse engineer. The user is reversing a binary, using "
|
||||
"radare2. The user will ask questions about the binary and you will "
|
||||
"respond with the answer to the best of your ability. "
|
||||
"If none of the available tools fit the request, answer directly "
|
||||
"from your own knowledge instead of inventing tool names. Only the "
|
||||
"tools explicitly listed in this conversation exist.");
|
||||
r_config_set (
|
||||
core->config, "r2ai.prompt",
|
||||
"Rewrite this function and respond ONLY with code, replace goto/labels "
|
||||
"with if/else/for, use NO explanations, NO markdown, Simplify as much as "
|
||||
"possible, use better variable names, take function arguments and "
|
||||
"strings from comments like 'string:'");
|
||||
core->config, "r2ai.prompt", "Rewrite this function and respond ONLY with code, replace goto/labels "
|
||||
"with if/else/for, use NO explanations, NO markdown, Simplify as much as "
|
||||
"possible, use better variable names, take function arguments and "
|
||||
"strings from comments like 'string:'");
|
||||
r_config_set (core->config, "r2ai.promptdir", "~/.config/r2ai/prompts");
|
||||
r_config_desc (core->config, "r2ai.promptdir", "Directory containing .r2ai prompt files");
|
||||
r_config_set_b (core->config, "r2ai.clippy", false);
|
||||
r_config_desc (core->config, "r2ai.clippy", "Responses from the llm will be displayed by clippy");
|
||||
r_config_set_b (core->config, "r2ai.wizard", true);
|
||||
r_config_desc (core->config, "r2ai.wizard", "Run the setup wizard automatically on the first interactive chat session");
|
||||
r_config_set_b (core->config, "r2ai.markdown", true);
|
||||
r_config_desc (core->config, "r2ai.markdown", "Render LLM responses using the radare2 markdown colorizer");
|
||||
r_config_set_b (core->config, "r2ai.stream", false);
|
||||
r_config_desc (core->config, "r2ai.stream", "Enable streaming responses from the LLM (true/false)");
|
||||
r_config_set_i (core->config, "r2ai.auto.max_runs", 50);
|
||||
r_config_desc (core->config, "r2ai.auto.max_runs", "Maximum number of automated steps/runs in auto mode");
|
||||
r_config_set_b (core->config, "r2ai.auto.hide_tool_output", false);
|
||||
r_config_desc (core->config, "r2ai.auto.hide_tool_output", "Hide tool output when running automated actions");
|
||||
r_config_set (core->config, "r2ai.auto.init_commands", "iI"); // "aaa;iI;afl~[3]"
|
||||
r_config_set_b (core->config, "r2ai.auto.verbose", true);
|
||||
r_config_desc (core->config, "r2ai.auto.verbose", "Show tool output when running automated actions");
|
||||
r_config_set (core->config, "r2ai.auto.init_commands", "i"); // "aaa;iI;afl~[3]"
|
||||
r_config_desc (core->config, "r2ai.auto.init_commands", "Initial commands executed when auto mode starts (semicolon separated)");
|
||||
r_config_set_b (core->config, "r2ai.auto.yolo", false);
|
||||
r_config_desc (core->config, "r2ai.auto.yolo", "Execute potentially dangerous commands in auto mode without asking");
|
||||
r_config_set_b (core->config, "r2ai.history", false);
|
||||
r_config_desc (core->config, "r2ai.history", "Keep direct queries (r2ai [query], -d, -i) in the conversation context (see -L, -R)");
|
||||
r_config_set_b (core->config, "r2ai.auto.reset_on_query", false);
|
||||
r_config_desc (core->config, "r2ai.auto.reset_on_query", "Reset auto-mode conversation state on new user queries");
|
||||
r_config_set_b (core->config, "r2ai.chat.show_cost", true);
|
||||
r_config_desc (core->config, "r2ai.chat.show_cost", "Display estimated API cost for chat interactions");
|
||||
r_config_set_b (core->config, "r2ai.auto.think", true);
|
||||
r_config_desc (core->config, "r2ai.auto.think", "Enable thinking/reasoning in auto mode prompts (true/false)");
|
||||
|
||||
r_config_set_i (core->config, "r2ai.http.timeout", 120);
|
||||
r_config_set_i (core->config, "r2ai.http.timeout", 240);
|
||||
r_config_desc (core->config, "r2ai.http.timeout", "HTTP client timeout (seconds) for provider API calls");
|
||||
r_config_set_i (core->config, "r2ai.http.max_retries", 10);
|
||||
r_config_set_i (core->config, "r2ai.http.max_retries", 3);
|
||||
r_config_desc (core->config, "r2ai.http.max_retries", "Maximum number of HTTP retries for failed requests");
|
||||
r_config_set_i (core->config, "r2ai.http.max_backoff", 30);
|
||||
r_config_desc (core->config, "r2ai.http.max_backoff", "Maximum backoff time (seconds) between HTTP retries");
|
||||
r_config_set_b (core->config, "r2ai.http.use_files", false);
|
||||
r_config_desc (core->config, "r2ai.http.use_files", "Use temporary files to pass HTTP request/response payloads (true/false)");
|
||||
char *http_headers = r_sys_getenv ("R2AI_HEADERS");
|
||||
r_config_set (core->config, "r2ai.http.headers", r_str_get (http_headers));
|
||||
free (http_headers);
|
||||
r_config_desc (core->config, "r2ai.http.headers", "Extra HTTP headers ('\\n' separated 'Name: value'; defaults to R2AI_HEADERS env)");
|
||||
r_config_set_b (core->config, "r2ai.auto.raw", false);
|
||||
r_config_desc (core->config, "r2ai.auto.raw", "Use prompt engineering for tool calling instead of native API support (true/false)");
|
||||
r_config_set_b (core->config, "r2ai.auto.usejs", true);
|
||||
r_config_desc (core->config, "r2ai.auto.usejs", "Enable/disable the execute_js tool for auto mode (true/false)");
|
||||
r_config_set_b (core->config, "r2ai.auto.slim", true);
|
||||
r_config_desc (core->config, "r2ai.auto.slim", "Use slim mode for auto commands (temporarily disable asm.lines.fcn and scr.utf8)");
|
||||
r_config_set_b (core->config, "r2ai.debug", false);
|
||||
r_config_desc (core->config, "r2ai.debug", "Enable debug output including API request/response details and curl commands");
|
||||
r_config_set_b (core->config, "r2ai.async", false);
|
||||
r_config_desc (core->config, "r2ai.async", "Run LLM calls in background threads (see r2ai -s, -si)");
|
||||
r_config_set_b (core->config, "r2ai.async.purge", true);
|
||||
r_config_desc (core->config, "r2ai.async.purge", "Drop completed/errored/cancelled async tasks after status listings");
|
||||
r_config_lock (core->config, true);
|
||||
return true;
|
||||
|
||||
r2ai_async_init (state);
|
||||
|
||||
return load_r2airc (cps);
|
||||
}
|
||||
|
||||
R_API bool r2ai_fini(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
r2ai_async_fini (state);
|
||||
r_config_lock (core->config, false);
|
||||
r_config_rm (core->config, "r2ai.api");
|
||||
r_config_rm (core->config, "r2ai.cmds");
|
||||
r_config_rm (core->config, "r2ai.model");
|
||||
r_config_rm (core->config, "r2ai.apitype");
|
||||
r_config_rm (core->config, "r2ai.cacheck");
|
||||
r_config_rm (core->config, "r2ai.prompt");
|
||||
r_config_rm (core->config, "r2ai.markdown");
|
||||
r_config_rm (core->config, "r2ai.stream");
|
||||
r_config_rm (core->config, "r2ai.system");
|
||||
r_config_rm (core->config, "r2ai.data");
|
||||
r_config_rm (core->config, "r2ai.data.path");
|
||||
r_config_rm (core->config, "r2ai.data.nth");
|
||||
r_config_rm (core->config, "r2ai.history");
|
||||
r_config_rm (core->config, "r2ai.auto.reset_on_query");
|
||||
r_config_rm (core->config, "r2ai.http.timeout");
|
||||
r_config_rm (core->config, "r2ai.http.max_retries");
|
||||
r_config_rm (core->config, "r2ai.http.max_backoff");
|
||||
r_config_rm (core->config, "r2ai.http.backend");
|
||||
r_config_rm (core->config, "r2ai.http.use_files");
|
||||
r_config_rm (core->config, "r2ai.auto.slim");
|
||||
r_config_rm (core->config, "r2ai.debug");
|
||||
r_config_rm (core->config, "r2ai.async");
|
||||
r_config_rm (core->config, "r2ai.async.purge");
|
||||
r_config_lock (core->config, true);
|
||||
|
||||
R2AI_State *state = cps->data;
|
||||
r2ai_conversation_free (state);
|
||||
r2ai_openai_fini (state);
|
||||
|
||||
if (state) {
|
||||
r2ai_tools_free (state->tools);
|
||||
state->tools = NULL;
|
||||
r_vdb_free (state->db);
|
||||
state->db = NULL;
|
||||
free (state->prompt_auto);
|
||||
state->prompt_auto = NULL;
|
||||
free (state->vertex_token);
|
||||
state->vertex_token = NULL;
|
||||
R_FREE (state->cache_prefix);
|
||||
free (state);
|
||||
cps->data = NULL;
|
||||
}
|
||||
@@ -616,7 +874,7 @@ static bool r_cmd_r2ai_client(RCorePluginSession *cps, const char *input) {
|
||||
}
|
||||
|
||||
// PLUGIN Definition Info
|
||||
RCorePlugin r_core_plugin_r2ai_client = {
|
||||
RCorePlugin r_core_plugin_r2ai = {
|
||||
.meta = {
|
||||
.name = "r2ai",
|
||||
.desc = "r2ai plugin in plain C",
|
||||
@@ -632,8 +890,9 @@ RCorePlugin r_core_plugin_r2ai_client = {
|
||||
#ifndef R2_PLUGIN_INCORE
|
||||
R_API RLibStruct radare_plugin = {
|
||||
.type = R_LIB_TYPE_CORE,
|
||||
.data = &r_core_plugin_r2ai_client,
|
||||
.data = &r_core_plugin_r2ai,
|
||||
.abiversion = R2_ABIVERSION,
|
||||
.pkgname = "r2ai",
|
||||
.version = R2_VERSION
|
||||
};
|
||||
#endif
|
||||
+100
-164
@@ -1,6 +1,8 @@
|
||||
#ifndef R2AI_H
|
||||
#define R2AI_H
|
||||
|
||||
#define _GNU_SOURCE
|
||||
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
@@ -8,21 +10,28 @@
|
||||
#include <stdlib.h>
|
||||
#include <r_core.h>
|
||||
#include <r_util/r_json.h>
|
||||
#include <r_types_base.h>
|
||||
#include "r_vdb.h"
|
||||
#include "markdown.h"
|
||||
|
||||
#define R2AI_VERSION "1.2.4"
|
||||
|
||||
#if 0
|
||||
#define R2AI_DEFAULT_MODEL "gpt-5-mini"
|
||||
#define R2AI_DEFAULT_PROVIDER "openai"
|
||||
#else
|
||||
// #define R2AI_DEFAULT_MODEL "gpt-oss:20b"
|
||||
// #define R2AI_DEFAULT_MODEL "gemma3:12b"
|
||||
#define R2AI_DEFAULT_MODEL "mannix/jan-nano"
|
||||
#define R2AI_DEFAULT_PROVIDER "ollama"
|
||||
#ifndef R_LIB_ABIVERSION
|
||||
#define R_LIB_ABIVERSION R2_ABIVERSION
|
||||
#endif
|
||||
|
||||
#if R_LIB_ABIVERSION >= 125
|
||||
#define r2ai_cmd_help(core,msg) r_cons_cmd_help ((core)->cons, msg)
|
||||
#else
|
||||
#define r2ai_cmd_help(core,msg) r_core_cmd_help (core, msg)
|
||||
#endif
|
||||
|
||||
#if R2_ABIVERSION < 83
|
||||
#define r_core_call(x,y) r_core_cmd_call(x,y)
|
||||
#endif
|
||||
|
||||
#define R2AI_VERSION "1.5.4"
|
||||
#define R2AI_DEFAULT_MODEL "gpt-oss:20b"
|
||||
#define R2AI_DEFAULT_PROVIDER "ollama"
|
||||
|
||||
#define R2AI_DEFAULT_VECTORS 16
|
||||
|
||||
// HTTP configuration structure
|
||||
@@ -37,7 +46,7 @@ typedef struct {
|
||||
R2AI_HttpConfig config;
|
||||
const char *url;
|
||||
const char *data;
|
||||
const char * const *headers;
|
||||
const char **headers;
|
||||
} HTTPRequest;
|
||||
|
||||
// HTTP response structure
|
||||
@@ -51,8 +60,6 @@ typedef struct {
|
||||
#error Your radare2 is too old
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
// Tool definition structure
|
||||
typedef struct {
|
||||
char *name;
|
||||
@@ -60,11 +67,13 @@ typedef struct {
|
||||
char *parameters; // JSON string of parameters/input_schema
|
||||
} R2AI_Tool;
|
||||
|
||||
// Tools array management
|
||||
typedef struct {
|
||||
R2AI_Tool *tools;
|
||||
int n_tools;
|
||||
} R2AI_Tools;
|
||||
char *output;
|
||||
char *edited_command;
|
||||
char *comment;
|
||||
} R2AI_ToolResult;
|
||||
|
||||
// Tools array management (now just RList<R2AI_Tool *>)
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
@@ -72,6 +81,7 @@ typedef struct {
|
||||
const char *id;
|
||||
} R2AI_ToolCall;
|
||||
|
||||
|
||||
typedef struct {
|
||||
char *type;
|
||||
char *id;
|
||||
@@ -83,19 +93,20 @@ typedef struct {
|
||||
char *text;
|
||||
} R2AI_ContentBlock;
|
||||
|
||||
typedef struct {
|
||||
R2AI_ContentBlock *blocks;
|
||||
int n_blocks;
|
||||
} R2AI_ContentBlocks;
|
||||
// Content blocks (now just RList<R2AI_ContentBlock *>)
|
||||
|
||||
/**
|
||||
* Create a new content blocks RList
|
||||
*/
|
||||
R_API RList *r2ai_content_blocks_new(void);
|
||||
|
||||
typedef struct {
|
||||
char *role;
|
||||
char *content;
|
||||
char *reasoning_content;
|
||||
R2AI_ContentBlocks *content_blocks;
|
||||
RList *content_blocks; // RList<R2AI_ContentBlock *>
|
||||
char *tool_call_id;
|
||||
R2AI_ToolCall *tool_calls;
|
||||
int n_tool_calls;
|
||||
RList *tool_calls; // RList<R2AI_ToolCall *>
|
||||
} R2AI_Message;
|
||||
|
||||
typedef struct {
|
||||
@@ -122,30 +133,26 @@ typedef struct {
|
||||
const R2AI_Usage *usage;
|
||||
} R2AI_ChatResponse;
|
||||
|
||||
// Messages array management
|
||||
typedef struct {
|
||||
RList *messages; // RList<R2AI_Message *>
|
||||
} R2AI_Messages;
|
||||
// Messages array management (now just RList<R2AI_Message *>)
|
||||
|
||||
typedef struct {
|
||||
const char *input;
|
||||
const char *model;
|
||||
const char *system_prompt; // System prompt to use
|
||||
const R2AI_Tools *tools; // Tools structure (replacing tools_json)
|
||||
R2AI_Messages *messages; // Array of message objects
|
||||
RList *tools; // Tools RList (replacing tools_json)
|
||||
RList *messages; // Array of message objects
|
||||
const char *provider;
|
||||
const char *api_key;
|
||||
int max_tokens;
|
||||
int thinking_tokens;
|
||||
float temperature;
|
||||
bool deterministic; // greedy sampling to remove randomness from responses
|
||||
bool dorag;
|
||||
char **error;
|
||||
} R2AIArgs;
|
||||
|
||||
// Stats structure from auto.c
|
||||
typedef struct {
|
||||
double total_cost;
|
||||
double run_cost;
|
||||
int total_tokens;
|
||||
int run_tokens;
|
||||
int total_prompt_tokens;
|
||||
@@ -156,137 +163,56 @@ typedef struct {
|
||||
time_t total_start_time;
|
||||
} R2AIStats;
|
||||
|
||||
// Forward declaration for async task queue (defined in r2ai_priv.h)
|
||||
struct r2ai_task_queue_t;
|
||||
|
||||
// Main state structure to hold all global state
|
||||
typedef struct r2ai_state_t {
|
||||
R2AI_Messages *conversation; // Global conversation messages (from messages.c)
|
||||
RList *conversation; // Global conversation messages (from messages.c)
|
||||
R2AIStats stats; // Global stats (from auto.c)
|
||||
R2AI_Tools *tools; // Global tools instance (from tools.c)
|
||||
RMarkdownTheme current_theme; // Global theme (from markdown.c)
|
||||
bool theme_initialized; // Global theme flag (from markdown.c)
|
||||
RList *tools; // Global tools instance (from tools.c)
|
||||
RMarkdown markdown; // Markdown rendering context
|
||||
void *help_msg; // Global help message (from r2ai.c)
|
||||
RVdb *db; // Vector database for embeddings
|
||||
HtPP *model_compat_db; // Model compatibility database (from openai.c)
|
||||
struct r2ai_task_queue_t *async; // Async task queue (from async.c)
|
||||
char *prompt_auto; // Auto-mode system prompt
|
||||
char *vertex_token; // Cached Vertex AI OAuth2 token
|
||||
ut64 vertex_token_expiry; // Monotonic microseconds when the cached token expires
|
||||
char *cache_prefix; // Last chat request prefix used by r2ai.cacheck
|
||||
} R2AI_State;
|
||||
|
||||
/**
|
||||
* Initialize a new empty messages array
|
||||
*/
|
||||
R_API R2AI_Messages *r2ai_msgs_new(void);
|
||||
|
||||
/**
|
||||
* Initialize the conversation container (call during plugin init)
|
||||
*/
|
||||
// conversation
|
||||
R_API void r2ai_conversation_init(R2AI_State *state);
|
||||
|
||||
/**
|
||||
* Get the conversation instance (returns NULL if not initialized)
|
||||
*/
|
||||
R_API R2AI_Messages *r2ai_conversation_get(R2AI_State *state);
|
||||
|
||||
/**
|
||||
* Clear all messages in a container without freeing the container
|
||||
*/
|
||||
R_API void r2ai_msgs_clear(R2AI_Messages *msgs);
|
||||
|
||||
/**
|
||||
* Add a message to the array
|
||||
* All strings are duplicated, so caller can free their copies
|
||||
*/
|
||||
R_API bool r2ai_msgs_add(R2AI_Messages *msgs, const R2AI_Message *msg);
|
||||
|
||||
/**
|
||||
* Add a tool call to the last message in the array
|
||||
* All strings are duplicated, so caller can free their copies
|
||||
*/
|
||||
R_API bool r2ai_msgs_add_tool_call(R2AI_Messages *msgs, const R2AI_ToolCall *tc);
|
||||
|
||||
/**
|
||||
* Parse a JSON response string and add the messages to the array
|
||||
* Returns true on success, false on failure
|
||||
*/
|
||||
R_API bool r2ai_msgs_from_response(R2AI_Messages *msgs, const char *json_str);
|
||||
|
||||
/**
|
||||
* Parse a RJson object directly and add the messages to the array
|
||||
* Returns true on success, false on failure
|
||||
*/
|
||||
R_API bool r2ai_msgs_from_json(R2AI_Messages *msgs, const RJson *json);
|
||||
|
||||
/**
|
||||
* Convert messages array to JSON string
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_msgs_to_json(const R2AI_Messages *msgs);
|
||||
|
||||
/**
|
||||
* Convert messages array to Anthropic format JSON string
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const R2AI_Messages *msgs);
|
||||
|
||||
/**
|
||||
* Free a messages array and all associated data
|
||||
*/
|
||||
R_API void r2ai_msgs_free(R2AI_Messages *msgs);
|
||||
|
||||
/**
|
||||
* Free the conversation (call during plugin unload)
|
||||
*/
|
||||
R_API RList *r2ai_conversation_get(R2AI_State *state);
|
||||
R_API void r2ai_conversation_free(R2AI_State *state);
|
||||
|
||||
/**
|
||||
* Free a R2AI_Message structure
|
||||
*/
|
||||
// messages
|
||||
R_API RList *r2ai_msgs_new(void);
|
||||
R_API void r2ai_msgs_clear(RList *msgs);
|
||||
R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg);
|
||||
R_API char *r2ai_msgs_to_json(const RList *msgs, bool raw_tool_args);
|
||||
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs);
|
||||
R_API void r2ai_msgs_free(RList *msgs);
|
||||
R_API void r2ai_message_free(R2AI_Message *msg);
|
||||
R_IPI void r2ai_chat_response_free(R2AI_ChatResponse *res);
|
||||
R_API void r2ai_message_fini(R2AI_Message *msg);
|
||||
R_API void r2ai_delete_last_messages(RList *messages, int n);
|
||||
|
||||
// tools
|
||||
R_API RList *r2ai_get_tools(RCore *core, R2AI_State *state);
|
||||
R_API RList *r2ai_tools_parse(const char *tools_json);
|
||||
R_API char *r2ai_tools_to_openai_json(const RList *tools);
|
||||
R_API char *r2ai_tools_to_anthropic_json(const RList *tools);
|
||||
R_API void r2ai_tools_free(RList *tools);
|
||||
R_API void r2ai_tool_result_fini(R2AI_ToolResult *result);
|
||||
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc);
|
||||
R_API R2AI_ToolResult execute_tool(RCorePluginSession *cps, const char *tool_name, const char *args);
|
||||
|
||||
/**
|
||||
* Free a R2AIPrompt structure
|
||||
* Execute r_core_cmd_str with slim mode if enabled
|
||||
* Temporarily sets asm.lines.fcn=false and scr.utf8=false if r2ai.auto.slim is true
|
||||
*/
|
||||
R_API void r2aiprompt_free(R2AIPrompt *prompt);
|
||||
|
||||
/**
|
||||
* Delete the last N messages from the message array
|
||||
* If n <= 0, defaults to deleting just the last message
|
||||
*/
|
||||
R_API void r2ai_delete_last_messages(R2AI_Messages *messages, int n);
|
||||
|
||||
/**
|
||||
* Get the global tools instance
|
||||
* Returns a pointer to the global tools structure
|
||||
*/
|
||||
R_API const R2AI_Tools *r2ai_get_tools(void);
|
||||
|
||||
/**
|
||||
* Parse OpenAI format tools JSON into internal tools structure
|
||||
* Caller must free the result with r2ai_tools_free
|
||||
*/
|
||||
R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json);
|
||||
|
||||
/**
|
||||
* Convert tools structure to OpenAI format JSON
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_tools_to_openai_json(const R2AI_Tools *tools);
|
||||
|
||||
/**
|
||||
* Convert tools structure to Anthropic format JSON
|
||||
* Caller must free the returned string
|
||||
*/
|
||||
R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools);
|
||||
|
||||
/**
|
||||
* Free a tools structure and all associated data
|
||||
*/
|
||||
R_API void r2ai_tools_free(R2AI_Tools *tools);
|
||||
|
||||
/**
|
||||
* Execute a tool and return the output
|
||||
* The command to execute is within args. The user may interactively
|
||||
* modify this command, hence edited_command contains the real
|
||||
* command which was run
|
||||
* Caller must free edited_command
|
||||
*/
|
||||
R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, char **edited_command);
|
||||
R_API char *r2ai_cmdstr(RCore *core, const char *cmd);
|
||||
|
||||
/**
|
||||
* Send an HTTP POST request
|
||||
@@ -300,7 +226,7 @@ R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, c
|
||||
* @param use_files Use files instead of arguments
|
||||
* @return Response body as string (must be freed by caller) or NULL on error
|
||||
*/
|
||||
R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[], const char *data, int *code, int *rlen);
|
||||
R_API char *r2ai_http_post(RCore *core, const char *url, const char **headers, const char *data, int *code, int *rlen);
|
||||
|
||||
/**
|
||||
* Send an HTTP GET request
|
||||
@@ -311,26 +237,31 @@ R_API char *r2ai_http_post(RCore *core, const char *url, const char *headers[],
|
||||
* @param rlen Pointer to store the response length
|
||||
* @return Response body as string (must be freed by caller) or NULL on error
|
||||
*/
|
||||
R_API char *r2ai_http_get(RCore *core, const char *url, const char *headers[], int *code, int *rlen);
|
||||
R_API char *r2ai_http_get(RCore *core, const char *url, const char **headers, int *code, int *rlen);
|
||||
|
||||
/**
|
||||
* Get the base URL for a given provider
|
||||
*
|
||||
* @param core RCore instance for configuration
|
||||
* @param provider The provider name (e.g., "openai", "anthropic", etc.)
|
||||
* @return Base URL for the provider, or NULL if unknown
|
||||
* @return Newly allocated base URL for the provider, or NULL if unknown
|
||||
*/
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
R_IPI char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
|
||||
// anthropic
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(char *json, char **error);
|
||||
|
||||
// openai
|
||||
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI void r2ai_openai_fini(R2AI_State *state);
|
||||
|
||||
// gemini
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
// auto mode
|
||||
R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query);
|
||||
R_IPI char *r2ai_auto_system_prompt(RCorePluginSession *cps);
|
||||
R_IPI void r2ai_print_response(RCore *core, const char *text);
|
||||
// R_API char *r2ai(RCore *core, R2AI_State *state, R2AIArgs args);
|
||||
R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_API bool r2ai_init(RCorePluginSession *cps);
|
||||
@@ -338,24 +269,29 @@ R_API bool r2ai_fini(RCorePluginSession *cps);
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps);
|
||||
R_IPI void cmd_r2ai_c(RCorePluginSession *cps);
|
||||
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps, const char *flags);
|
||||
R_IPI void cmd_r2ai_lr(RCorePluginSession *cps);
|
||||
|
||||
/**
|
||||
* Process messages through LLM and handle tool calls recursively
|
||||
*/
|
||||
R_API void process_messages(RCorePluginSession *cps, 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);
|
||||
|
||||
/**
|
||||
* Helper function to convert RJson to string
|
||||
*/
|
||||
// json helpers that must be moved into r2
|
||||
R_API char *r_json_to_string(const RJson *json);
|
||||
|
||||
/**
|
||||
* Helper function to convert RJson to PJ
|
||||
*/
|
||||
R_API PJ *r_json_to_pj(const RJson *json, PJ *existing_pj);
|
||||
// commands
|
||||
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input);
|
||||
R_API void r2ai_cmd_qj(RCorePluginSession *cps, const char *input);
|
||||
R_API char *find_prompt_file(RList *search_dirs, const char *name);
|
||||
R_API R2AIPrompt *parse_prompt_file(const char *filepath);
|
||||
R_API void r2aiprompt_free(R2AIPrompt *prompt);
|
||||
R_API char *r2ai_load_prompt_text(RCore *core, const char *name);
|
||||
R_API char *strip_command_comment(const char *input, char **comment_out);
|
||||
R_API bool r2ai_wizard(RCore *core);
|
||||
R_API bool r2ai_wizard_autorun(RCore *core);
|
||||
R_API bool r2ai_wizard_isfirsttime(void);
|
||||
|
||||
#endif
|
||||
+107
-5
@@ -2,21 +2,123 @@
|
||||
#define INCLUDE_R2AI_PRIV__H
|
||||
|
||||
#include "r2ai.h"
|
||||
#include <r_th.h>
|
||||
|
||||
typedef enum {
|
||||
R2AI_API_OPENAI_COMPATIBLE,
|
||||
R2AI_API_ANTHROPIC,
|
||||
R2AI_API_GEMINI,
|
||||
R2AI_API_OLLAMA,
|
||||
R2AI_API_VERTEX_GEMINI,
|
||||
R2AI_API_VERTEX_ANTHROPIC
|
||||
} R2AI_API_Type;
|
||||
|
||||
/* Async task subsystem (see ASYNC.md) */
|
||||
typedef enum {
|
||||
R2AI_TASK_PENDING = 0,
|
||||
R2AI_TASK_RUNNING,
|
||||
R2AI_TASK_WAIT_APPROVE,
|
||||
R2AI_TASK_WAIT_INPUT,
|
||||
R2AI_TASK_COMPLETE,
|
||||
R2AI_TASK_ERROR,
|
||||
R2AI_TASK_CANCELLED,
|
||||
} R2AITaskState;
|
||||
|
||||
typedef enum {
|
||||
R2AI_TASK_QUERY = 0,
|
||||
R2AI_TASK_AUTO,
|
||||
} R2AITaskKind;
|
||||
|
||||
typedef struct r2ai_task_t {
|
||||
int id;
|
||||
R2AITaskKind kind;
|
||||
R2AITaskState state;
|
||||
char *title;
|
||||
char *query;
|
||||
char *system_prompt;
|
||||
char *model;
|
||||
char *provider;
|
||||
RList *messages; /* R2AI_Message * - owned conversation */
|
||||
RList *tools; /* R2AI_Tool * - snapshot taken on the main thread, items not owned */
|
||||
int max_runs;
|
||||
RStrBuf *output; /* flushed by -si */
|
||||
char *error;
|
||||
char *pending_tool_name;
|
||||
char *pending_tool_args;
|
||||
char *pending_tool_call_id;
|
||||
char *tool_result; /* main -> worker handoff */
|
||||
bool cancel_req;
|
||||
RThread *thread;
|
||||
RThreadLock *lock;
|
||||
RThreadSemaphore *gate;
|
||||
RCorePluginSession *cps;
|
||||
RCore *llm_core; /* shallow core view with an owned request config snapshot */
|
||||
time_t created;
|
||||
time_t started;
|
||||
time_t finished;
|
||||
int steps;
|
||||
} R2AITask;
|
||||
|
||||
typedef struct r2ai_task_queue_t {
|
||||
RList *tasks;
|
||||
RThreadLock *lock;
|
||||
int next_id;
|
||||
} R2AITaskQueue;
|
||||
|
||||
R_IPI void r2ai_async_init(R2AI_State *state);
|
||||
R_IPI void r2ai_async_fini(R2AI_State *state);
|
||||
R_IPI int r2ai_async_query(RCorePluginSession *cps,
|
||||
const char *title, const char *query, const char *sysp);
|
||||
R_IPI int r2ai_async_auto(RCorePluginSession *cps,
|
||||
const char *title, const char *query, const char *sysp);
|
||||
R_IPI void r2ai_async_cmd(RCorePluginSession *cps, const char *input);
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *url;
|
||||
R2AI_API_Type api_type;
|
||||
bool requires_api_key;
|
||||
bool uses_anthropic_header;
|
||||
bool uses_tags_endpoint;
|
||||
bool uses_system_ls;
|
||||
bool supports_custom_baseurl;
|
||||
} R2AIProvider;
|
||||
|
||||
R_IPI char *r2ai_get_api_key(RCore *core, const char *provider);
|
||||
R_API void cmd_r2ai(RCorePluginSession *cps, const char *input);
|
||||
static inline char *r2ai_cons_editor(RCons *cons, const char *file, const char *str) {
|
||||
#if R2_ABIVERSION >= 160
|
||||
return r_cons_editor (cons, file, str, 0, NULL);
|
||||
#elif R2_ABIVERSION >= 120
|
||||
return r_cons_editor (cons, file, str, NULL);
|
||||
#else
|
||||
return r_cons_editor (cons, file, str);
|
||||
#endif
|
||||
}
|
||||
R_IPI const R2AIProvider *r2ai_get_provider(const char *name);
|
||||
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
R_IPI const char *r2ai_system_prompt(RCore *core, const char *sysp);
|
||||
R_IPI R2AI_ChatResponse *r2ai_send(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args);
|
||||
R_IPI char *r2ai_post(RCore *core, const char *name, const char *url, const char **headers, const char *data, int *code, char **error);
|
||||
R_IPI char *r2ai_gemini_request(const R2AIArgs *args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_gemini_parse(char *json, char **error);
|
||||
R_IPI char *r2ai_anthropic_request(const R2AIArgs *args, bool vertex);
|
||||
R_IPI R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args);
|
||||
R_IPI char *r2ai_get_provider_url(RCore *core, const char *provider);
|
||||
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider);
|
||||
R_IPI void r2ai_list_providers(RCore *core, RStrBuf *sb);
|
||||
R_IPI void r2ai_refresh_embeddings(RCorePluginSession *cps);
|
||||
R_API char *r2ai_apikeys_path(bool *exists);
|
||||
R_API void r2ai_apikeys_edit(RCorePluginSession *cps);
|
||||
R_API char *r2ai_apikeys_get(const char *provider);
|
||||
|
||||
/* vertex.c */
|
||||
R_IPI const char *r2ai_vertex_get_token(R2AI_State *state);
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_gemini(RCorePluginSession *cps, R2AIArgs args);
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_anthropic(RCorePluginSession *cps, R2AIArgs args);
|
||||
|
||||
/* claw.c - SOUL.md / IDENTITY.md personality files */
|
||||
#define R2AI_CLAW_HINT "use 'r2ai -ide' to edit or 'r2ai -id-' to delete"
|
||||
R_API bool r2ai_claw_exists(void);
|
||||
R_API char *r2ai_claw_system_prompt(const char *base);
|
||||
R_API void r2ai_claw_create(RCorePluginSession *cps, const char *user_extra);
|
||||
R_API void r2ai_claw_show(RCorePluginSession *cps);
|
||||
R_API void r2ai_claw_edit(RCorePluginSession *cps);
|
||||
R_API void r2ai_claw_delete(RCorePluginSession *cps);
|
||||
|
||||
#endif
|
||||
+62
-138
@@ -1,4 +1,4 @@
|
||||
/* r2ai - Copyright 2023-2025 pancake */
|
||||
/* r2ai - Copyright 2023-2026 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "rawtools"
|
||||
#include "r2ai.h"
|
||||
@@ -72,41 +72,20 @@ static bool parse_raw_tool_call(const char *response, char **tool_name, char **t
|
||||
line_start += strlen (tool_prefix);
|
||||
}
|
||||
|
||||
// If no TOOL: format found, try to parse JSON response
|
||||
// Look for JSON that looks like tool arguments
|
||||
const char *json_start = strstr (response, "{\"command\"");
|
||||
if (json_start) {
|
||||
*tool_name = strdup ("r2cmd");
|
||||
*tool_args = strdup (json_start);
|
||||
// Trim trailing whitespace and newlines
|
||||
char *end = *tool_args + strlen (*tool_args) - 1;
|
||||
while (end > *tool_args && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
|
||||
*end = '\0';
|
||||
end--;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Function to handle rawtools mode in LLM call
|
||||
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args) {
|
||||
if (!cps) {
|
||||
return NULL;
|
||||
R_IPI R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, const R2AIProvider *p, R2AIArgs args) {
|
||||
const char *user_system = args.system_prompt;
|
||||
if (R_STR_ISEMPTY (user_system)) {
|
||||
user_system = "You are a reverse engineer using radare2. The binary is loaded. Use r2cmd tool for analysis.";
|
||||
}
|
||||
RCore *core = cps->core;
|
||||
|
||||
// Modify the system prompt to include rawtools instructions only if no tool messages
|
||||
const char *original_system_prompt = args.system_prompt;
|
||||
|
||||
// For rawtools, use a shorter system prompt to avoid token limits
|
||||
const char *short_system_prompt = "You are a reverse engineer using radare2. The binary is loaded. Use r2cmd tool for analysis.";
|
||||
|
||||
// Check for initial command output in messages
|
||||
char *init_output = NULL;
|
||||
if (args.messages && args.messages->messages) {
|
||||
RList *msgs = args.messages->messages;
|
||||
if (args.messages) {
|
||||
RList *msgs = args.messages;
|
||||
int n_msgs = r_list_length (msgs);
|
||||
for (int i = 0; i < n_msgs; i++) {
|
||||
R2AI_Message *msg = r_list_get_n (msgs, i);
|
||||
@@ -118,33 +97,18 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
}
|
||||
|
||||
char *enhanced_system_prompt = NULL;
|
||||
if (init_output) {
|
||||
enhanced_system_prompt = r_str_newf ("%s\n\n%s\n\n%s", short_system_prompt, init_output, RAWTOOLS_PROMPT);
|
||||
} else {
|
||||
enhanced_system_prompt = r_str_newf ("%s\n\n%s", short_system_prompt, RAWTOOLS_PROMPT);
|
||||
}
|
||||
char *base_prompt = init_output
|
||||
? r_str_newf ("%s\n\n%s\n\n%s", user_system, init_output, RAWTOOLS_PROMPT)
|
||||
: r_str_newf ("%s\n\n%s", user_system, RAWTOOLS_PROMPT);
|
||||
free (init_output);
|
||||
// Temporarily modify args to use enhanced prompt and no tools (since we're using prompt engineering)
|
||||
char *enhanced_system_prompt = r2ai_claw_system_prompt (base_prompt);
|
||||
free (base_prompt);
|
||||
|
||||
R2AIArgs rawtools_args = args;
|
||||
rawtools_args.system_prompt = enhanced_system_prompt;
|
||||
rawtools_args.tools = NULL; // Disable native tool calling
|
||||
|
||||
// Make the LLM call directly to provider
|
||||
const char *provider = rawtools_args.provider? rawtools_args.provider: r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
R_LOG_ERROR ("No provider defined");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *response = NULL;
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (p && p->uses_anthropic_header) {
|
||||
response = r2ai_anthropic (cps, rawtools_args);
|
||||
} else {
|
||||
response = r2ai_openai (cps, rawtools_args);
|
||||
}
|
||||
rawtools_args.tools = NULL;
|
||||
|
||||
R2AI_ChatResponse *response = r2ai_send (cps, p, rawtools_args);
|
||||
free (enhanced_system_prompt);
|
||||
|
||||
if (!response || !response->message || !response->message->content) {
|
||||
@@ -153,87 +117,75 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
R_LOG_DEBUG ("[RAWTOOLS] Raw model response: %s", response->message->content);
|
||||
|
||||
// Check if the response contains a raw tool call
|
||||
char *tool_name = NULL;
|
||||
char *tool_args = NULL;
|
||||
|
||||
if (parse_raw_tool_call (response->message->content, &tool_name, &tool_args)) {
|
||||
R_LOG_DEBUG ("Raw tool call detected: %s with args: %s", tool_name, tool_args);
|
||||
|
||||
// Find the tool in our tools list
|
||||
const R2AI_Tools *tools = r2ai_get_tools ();
|
||||
RList *tools = r2ai_get_tools (cps->core, cps->data);
|
||||
bool tool_found = false;
|
||||
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
if (tools->tools[i].name && !strcmp (tools->tools[i].name, tool_name)) {
|
||||
tool_found = true;
|
||||
break;
|
||||
if (tools) {
|
||||
RListIter *iter;
|
||||
R2AI_Tool *tool;
|
||||
r_list_foreach (tools, iter, tool) {
|
||||
if (tool->name && !strcmp (tool->name, tool_name)) {
|
||||
tool_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
if (!modified_message) {
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
return response;
|
||||
}
|
||||
modified_message->role = strdup ("assistant");
|
||||
|
||||
if (tool_found) {
|
||||
// Modify the existing response to include tool call information
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
if (modified_message) {
|
||||
modified_message->role = strdup ("assistant");
|
||||
// Remove the TOOL line from content, or set to empty if that's all there is
|
||||
char *content = strdup (response->message->content? response->message->content: "");
|
||||
char *content = strdup (response->message->content? response->message->content: "");
|
||||
if (content) {
|
||||
char *tool_line = strstr (content, "TOOL: ");
|
||||
if (tool_line) {
|
||||
// Find the end of the line
|
||||
char *line_end = strchr (tool_line, '\n');
|
||||
if (line_end) {
|
||||
// Remove the TOOL line
|
||||
memmove (tool_line, line_end + 1, strlen (line_end + 1) + 1);
|
||||
} else {
|
||||
// The TOOL line is the entire content
|
||||
*tool_line = '\0';
|
||||
}
|
||||
}
|
||||
modified_message->content = content;
|
||||
}
|
||||
|
||||
// Set up proper tool call structure
|
||||
modified_message->tool_calls = R_NEWS0 (R2AI_ToolCall, 1);
|
||||
if (modified_message->tool_calls) {
|
||||
modified_message->tool_calls[0].name = tool_name;
|
||||
modified_message->tool_calls[0].arguments = tool_args;
|
||||
modified_message->tool_calls = r_list_new ();
|
||||
if (modified_message->tool_calls) {
|
||||
modified_message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (tc) {
|
||||
char id_buf[32];
|
||||
snprintf (id_buf, sizeof (id_buf), "rawtool_%d", (int)time (NULL));
|
||||
modified_message->tool_calls[0].id = strdup (id_buf);
|
||||
modified_message->n_tool_calls = 1;
|
||||
tc->name = tool_name;
|
||||
tc->arguments = tool_args;
|
||||
tc->id = strdup (id_buf);
|
||||
r_list_append (modified_message->tool_calls, tc);
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
}
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
}
|
||||
} else {
|
||||
// Unknown tool, modify content to indicate error
|
||||
R2AI_Message *modified_message = R_NEW0 (R2AI_Message);
|
||||
if (modified_message) {
|
||||
modified_message->role = strdup ("assistant");
|
||||
size_t result_len = strlen ("Unknown tool: ") + strlen (tool_name) + 1;
|
||||
modified_message->content = malloc (result_len);
|
||||
if (modified_message->content) {
|
||||
snprintf (modified_message->content, result_len, "Unknown tool: %s", tool_name);
|
||||
}
|
||||
|
||||
// Replace the response message
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
}
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
tool_name = NULL;
|
||||
tool_args = NULL;
|
||||
char *errmsg = r_str_newf ("Unknown tool: %s", tool_name);
|
||||
modified_message->content = errmsg;
|
||||
}
|
||||
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
*(R2AI_Message **)&response->message = modified_message;
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -241,54 +193,26 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (tool_name);
|
||||
free (tool_args);
|
||||
|
||||
// If the response has no content, try again with normal mode (without rawtools prompt)
|
||||
if (!response->message || !response->message->content || !*response->message->content) {
|
||||
R_LOG_DEBUG ("No tool call found and no content, falling back to normal mode");
|
||||
|
||||
// Free the current response
|
||||
if (response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)response->message);
|
||||
}
|
||||
free (response);
|
||||
r2ai_chat_response_free (response);
|
||||
|
||||
// Modify args to use original system prompt without rawtools enhancement
|
||||
R2AIArgs fallback_args = args;
|
||||
fallback_args.system_prompt = original_system_prompt;
|
||||
fallback_args.tools = args.tools; // Keep tools for fallback
|
||||
|
||||
// Call provider directly
|
||||
const char *provider = fallback_args.provider? fallback_args.provider: r_config_get (core->config, "r2ai.api");
|
||||
if (!provider) {
|
||||
R_LOG_ERROR ("No provider defined");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_ChatResponse *fallback_response = NULL;
|
||||
const R2AIProvider *p = r2ai_get_provider (provider);
|
||||
if (p && p->uses_anthropic_header) {
|
||||
fallback_response = r2ai_anthropic (cps, fallback_args);
|
||||
} else {
|
||||
fallback_response = r2ai_openai (cps, fallback_args);
|
||||
}
|
||||
R2AI_ChatResponse *fallback_response = r2ai_send (cps, p, args);
|
||||
|
||||
if (fallback_response && fallback_response->message && fallback_response->message->content && *fallback_response->message->content) {
|
||||
return fallback_response;
|
||||
}
|
||||
|
||||
// If still no content, create a response with warning
|
||||
if (fallback_response) {
|
||||
if (fallback_response->message) {
|
||||
r2ai_message_free ((R2AI_Message *)fallback_response->message);
|
||||
}
|
||||
free (fallback_response);
|
||||
}
|
||||
r2ai_chat_response_free (fallback_response);
|
||||
|
||||
// Create a warning response
|
||||
R2AI_ChatResponse *warning_response = R_NEW0 (R2AI_ChatResponse);
|
||||
R2AI_Message *msg = R_NEW0 (R2AI_Message);
|
||||
msg->role = strdup ("assistant");
|
||||
msg->content = strdup ("Warning: LLM provided no response content");
|
||||
*((R2AI_Message **)&warning_response->message) = msg;
|
||||
if (msg) {
|
||||
msg->role = strdup ("assistant");
|
||||
msg->content = strdup ("Warning: LLM provided no response content");
|
||||
*((R2AI_Message **)&warning_response->message) = msg;
|
||||
}
|
||||
R_LOG_WARN ("LLM provided no response content");
|
||||
return warning_response;
|
||||
}
|
||||
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
# Offline smoke tests for the Vertex AI provider (no network required)
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# Use empty rc to avoid user config interference
|
||||
R2ENV="R2_RCFILE=/dev/null"
|
||||
|
||||
check() {
|
||||
desc="$1"
|
||||
expected="$2"
|
||||
actual="$3"
|
||||
if echo "$actual" | grep -q "$expected"; then
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
FAIL=$((FAIL+1))
|
||||
printf "FAIL: %s\n expected: %s\n got: %s\n" "$desc" "$expected" "$actual"
|
||||
fi
|
||||
}
|
||||
|
||||
# Provider enumeration
|
||||
providers=$(env $R2ENV r2 -qc 'r2ai -e r2ai.api=?' -- 2>/dev/null)
|
||||
check "vertex in provider list" "vertex" "$providers"
|
||||
check "vertex-anthropic in provider list" "vertex-anthropic" "$providers"
|
||||
|
||||
# Config defaults
|
||||
region=$(env $R2ENV r2 -qc 'r2ai -e r2ai.vertex.region' -- 2>/dev/null)
|
||||
check "vertex.region default is us-central1" "us-central1" "$region"
|
||||
|
||||
# Config roundtrip
|
||||
rt=$(env $R2ENV r2 -qc 'e r2ai.vertex.project=test-proj; e r2ai.vertex.region=europe-west4; r2ai -e r2ai.vertex.project; r2ai -e r2ai.vertex.region' -- 2>/dev/null)
|
||||
check "vertex config roundtrip project" "test-proj" "$rt"
|
||||
check "vertex config roundtrip region" "europe-west4" "$rt"
|
||||
|
||||
# Error when project not set
|
||||
err=$(env $R2ENV r2 -qc 'e r2ai.api=vertex; e r2ai.vertex.project=; r2ai hello' -- 2>&1)
|
||||
check "error when project not set" "vertex.project" "$err"
|
||||
|
||||
printf "\n%d passed, %d failed\n" "$PASS" "$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
+383
-358
@@ -1,57 +1,87 @@
|
||||
#include <string.h>
|
||||
#include "r2ai.h"
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
R_API char *strip_command_comment(const char *input, char **comment_out) {
|
||||
const char *hash_pos = strchr (input, '#');
|
||||
char *stripped;
|
||||
if (hash_pos && (hash_pos == input || *(hash_pos - 1) != '\\')) {
|
||||
size_t cmd_len = hash_pos - input;
|
||||
stripped = r_str_ndup (input, cmd_len);
|
||||
r_str_trim (stripped);
|
||||
if (comment_out) {
|
||||
char *comment = strdup (hash_pos + 1); // skip #
|
||||
if (comment) {
|
||||
r_str_trim (comment);
|
||||
if (!*comment) {
|
||||
free (comment);
|
||||
comment = NULL;
|
||||
}
|
||||
}
|
||||
*comment_out = comment;
|
||||
}
|
||||
} else {
|
||||
stripped = strdup (input);
|
||||
if (comment_out) {
|
||||
*comment_out = NULL;
|
||||
}
|
||||
}
|
||||
return stripped;
|
||||
}
|
||||
|
||||
// Define the radare2 command tool
|
||||
static R2AI_Tool r2cmd_tool = {
|
||||
.name = "r2cmd",
|
||||
.description = "Run a radare2 command",
|
||||
.parameters = "{\
|
||||
\"type\": \"object\",\
|
||||
\"properties\": {\
|
||||
\"command\": {\
|
||||
\"type\": \"string\",\
|
||||
\"description\": \"The radare2 command to run\"\
|
||||
}\
|
||||
},\
|
||||
\"required\": [\"command\"]\
|
||||
}"
|
||||
.parameters = "{"
|
||||
"\"type\": \"object\","
|
||||
"\"properties\": {"
|
||||
"\"command\": {"
|
||||
"\"type\": \"string\","
|
||||
"\"description\": \"The radare2 command to run\""
|
||||
"}"
|
||||
"},"
|
||||
"\"required\": [\"command\"]"
|
||||
"}"
|
||||
};
|
||||
|
||||
static R2AI_Tool qjs_tool = {
|
||||
.name = "execute_js",
|
||||
.description = "Execute a JavaScript script inside the radare2 environment. Use `var RESULT=r2.cmd(COMMAND)` to execute radare2 commands and `r2.log(MESSAGE)` instead of console.log`",
|
||||
.parameters = "{\
|
||||
\"type\": \"object\",\
|
||||
\"properties\": {\
|
||||
\"script\": {\
|
||||
\"type\": \"string\",\
|
||||
\"description\": \"The JavaScript script to execute\"\
|
||||
}\
|
||||
},\
|
||||
\"required\": [\"script\"]\
|
||||
}"
|
||||
.parameters = "{"
|
||||
"\"type\": \"object\","
|
||||
"\"properties\": {"
|
||||
"\"script\": {"
|
||||
"\"type\": \"string\","
|
||||
"\"description\": \"The JavaScript script to execute\""
|
||||
"}"
|
||||
"},"
|
||||
"\"required\": [\"script\"]"
|
||||
"}"
|
||||
};
|
||||
|
||||
// Create a global tools structure with our tools
|
||||
static R2AI_Tools r2ai_tools_instance = {
|
||||
.tools = NULL, // Will initialize below
|
||||
.n_tools = 2
|
||||
};
|
||||
|
||||
// Function to get the global tools instance
|
||||
R_API const R2AI_Tools *r2ai_get_tools(void) {
|
||||
// Initialize tools array if not done yet
|
||||
if (!r2ai_tools_instance.tools) {
|
||||
static R2AI_Tool tools_array[2];
|
||||
tools_array[0] = r2cmd_tool;
|
||||
tools_array[1] = qjs_tool;
|
||||
r2ai_tools_instance.tools = tools_array;
|
||||
// Function to get the tools instance from state
|
||||
R_API RList *r2ai_get_tools(RCore *core, R2AI_State *state) {
|
||||
if (!state) {
|
||||
return NULL;
|
||||
}
|
||||
return &r2ai_tools_instance;
|
||||
if (!state->tools) {
|
||||
state->tools = r_list_newf (NULL);
|
||||
}
|
||||
if (!state->tools) {
|
||||
return NULL;
|
||||
}
|
||||
r_list_purge (state->tools);
|
||||
r_list_append (state->tools, (void *)&r2cmd_tool);
|
||||
// Reflect the current config instead of caching the first-seen value.
|
||||
if (r_config_get_b (core->config, "r2ai.auto.usejs")) {
|
||||
r_list_append (state->tools, (void *)&qjs_tool);
|
||||
}
|
||||
return state->tools;
|
||||
}
|
||||
|
||||
// Function to parse input tools_json (OpenAI format) into R2AI_Tool array
|
||||
R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
// Function to parse input tools_json (OpenAI format) into R2AI_Tool list
|
||||
R_API RList *r2ai_tools_parse(const char *tools_json) {
|
||||
if (!tools_json) {
|
||||
return NULL;
|
||||
}
|
||||
@@ -63,19 +93,14 @@ R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_Tools *tools = R_NEW0 (R2AI_Tools);
|
||||
|
||||
int n_tools = json->children.count;
|
||||
tools->tools = R_NEWS0 (R2AI_Tool, n_tools);
|
||||
if (!tools->tools) {
|
||||
free (tools);
|
||||
RList *tools = r_list_new ();
|
||||
if (!tools) {
|
||||
r_json_free (json);
|
||||
return NULL;
|
||||
}
|
||||
tools->n_tools = n_tools;
|
||||
tools->free = (RListFree)free; // Tools are dynamically allocated
|
||||
|
||||
int valid_tools = 0;
|
||||
for (int i = 0; i < n_tools; i++) {
|
||||
for (size_t i = 0; i < json->children.count; i++) {
|
||||
const RJson *tool_json = r_json_item (json, i);
|
||||
if (!tool_json || tool_json->type != R_JSON_OBJECT) {
|
||||
continue;
|
||||
@@ -99,7 +124,7 @@ R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
continue;
|
||||
}
|
||||
|
||||
R2AI_Tool *tool = &tools->tools[valid_tools++];
|
||||
R2AI_Tool *tool = R_NEW0 (R2AI_Tool);
|
||||
tool->name = strdup (name->str_value);
|
||||
|
||||
if (description && description->type == R_JSON_STRING) {
|
||||
@@ -107,34 +132,25 @@ R_API R2AI_Tools *r2ai_tools_parse(const char *tools_json) {
|
||||
}
|
||||
|
||||
if (parameters) {
|
||||
// Just pass through the JSON as a string
|
||||
if (parameters->type == R_JSON_STRING) {
|
||||
tool->parameters = strdup (parameters->str_value);
|
||||
// Convert parameters to JSON string
|
||||
char *params_str = r_json_to_string (parameters);
|
||||
if (params_str) {
|
||||
tool->parameters = params_str; // r_json_to_string returns allocated string
|
||||
} else {
|
||||
// Use pj_raw to pass through any other raw JSON
|
||||
PJ *pj = pj_new ();
|
||||
if (pj) {
|
||||
pj_raw (pj, "{}");
|
||||
char *params_str = pj_drain (pj);
|
||||
if (params_str) {
|
||||
tool->parameters = strdup (params_str);
|
||||
free (params_str);
|
||||
}
|
||||
}
|
||||
tool->parameters = strdup ("{}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update count of valid tools
|
||||
tools->n_tools = valid_tools;
|
||||
r_list_append (tools, tool);
|
||||
}
|
||||
|
||||
r_json_free (json);
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Function to convert R2AI_Tools to OpenAI format JSON
|
||||
R_API char *r2ai_tools_to_openai_json(const R2AI_Tools *tools) {
|
||||
if (!tools || tools->n_tools <= 0) {
|
||||
R_API char *r2ai_tools_to_openai_json(const RList *tools) {
|
||||
if (!tools || r_list_length (tools) <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -143,48 +159,51 @@ R_API char *r2ai_tools_to_openai_json(const R2AI_Tools *tools) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pj_a (pj); // Start array
|
||||
pj_a (pj);
|
||||
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
const R2AI_Tool *tool = &tools->tools[i];
|
||||
RListIter *iter;
|
||||
R2AI_Tool *tool;
|
||||
int a = 0;
|
||||
r_list_foreach (tools, iter, tool) {
|
||||
if (a > 0) {
|
||||
#if R2_VERSION_NUMBER < 60005
|
||||
// XXX this is a bug
|
||||
// pj_raw (pj, ",");
|
||||
#endif
|
||||
}
|
||||
a++;
|
||||
if (!tool->name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pj_o (pj); // Start tool object
|
||||
pj_ks (pj, "type", "function");
|
||||
|
||||
pj_k (pj, "function");
|
||||
pj_o (pj); // Start function object
|
||||
|
||||
pj_ko (pj, "function");
|
||||
pj_ks (pj, "name", tool->name);
|
||||
|
||||
if (tool->description) {
|
||||
pj_ks (pj, "description", tool->description);
|
||||
}
|
||||
|
||||
if (tool->parameters) {
|
||||
pj_k (pj, "parameters");
|
||||
pj_raw (pj, tool->parameters);
|
||||
}
|
||||
|
||||
pj_end (pj); // End function object
|
||||
pj_end (pj); // End tool object
|
||||
if (i < tools->n_tools - 1) {
|
||||
pj_raw (pj, ",");
|
||||
}
|
||||
#if R2_VERSION_NUMBER < 60005
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
|
||||
pj_end (pj); // End array
|
||||
pj_end (pj);
|
||||
|
||||
char *result = pj_drain (pj);
|
||||
// eprintf ("((((((((((((((((((\n\n%s\n\n)))))))))))))\n", result);
|
||||
R_LOG_DEBUG ("OpenAI tools JSON: %s", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Function to convert R2AI_Tools to Anthropic format JSON
|
||||
R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools) {
|
||||
if (!tools || tools->n_tools <= 0) {
|
||||
R_API char *r2ai_tools_to_anthropic_json(const RList *tools) {
|
||||
if (!tools || r_list_length (tools) <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -195,29 +214,22 @@ R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools) {
|
||||
|
||||
pj_a (pj); // Start array
|
||||
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
const R2AI_Tool *tool = &tools->tools[i];
|
||||
RListIter *iter;
|
||||
R2AI_Tool *tool;
|
||||
r_list_foreach (tools, iter, tool) {
|
||||
if (!tool->name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
pj_o (pj); // Start tool object
|
||||
|
||||
pj_ks (pj, "name", tool->name);
|
||||
|
||||
if (tool->description) {
|
||||
pj_ks (pj, "description", tool->description);
|
||||
}
|
||||
|
||||
if (tool->parameters) {
|
||||
pj_k (pj, "input_schema");
|
||||
pj_raw (pj, tool->parameters);
|
||||
}
|
||||
|
||||
pj_end (pj); // End tool object
|
||||
if (i < tools->n_tools - 1) {
|
||||
pj_raw (pj, ",");
|
||||
}
|
||||
}
|
||||
|
||||
pj_end (pj); // End array
|
||||
@@ -227,22 +239,12 @@ R_API char *r2ai_tools_to_anthropic_json(const R2AI_Tools *tools) {
|
||||
}
|
||||
|
||||
// Function to free a tools structure
|
||||
R_API void r2ai_tools_free(R2AI_Tools *tools) {
|
||||
R_API void r2ai_tools_free(RList *tools) {
|
||||
if (!tools) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tools->tools) {
|
||||
for (int i = 0; i < tools->n_tools; i++) {
|
||||
R2AI_Tool *tool = &tools->tools[i];
|
||||
R_FREE (tool->name);
|
||||
R_FREE (tool->description);
|
||||
R_FREE (tool->parameters);
|
||||
}
|
||||
R_FREE (tools->tools);
|
||||
}
|
||||
|
||||
R_FREE (tools);
|
||||
r_list_free (tools);
|
||||
}
|
||||
|
||||
static char *to_cmd(const char *command) {
|
||||
@@ -258,163 +260,216 @@ static char *to_cmd(const char *command) {
|
||||
return pj_drain (pj);
|
||||
}
|
||||
|
||||
R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool hide_tool_output, char **edited_command) {
|
||||
static char *compose_command_with_comment(const char *command, const char *comment) {
|
||||
if (!command) {
|
||||
return NULL;
|
||||
}
|
||||
if (!comment || !*comment) {
|
||||
return strdup (command);
|
||||
}
|
||||
return r_str_newf ("%s # %s", command, comment);
|
||||
}
|
||||
|
||||
R_API void r2ai_tool_result_fini(R2AI_ToolResult *result) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
free (result->output);
|
||||
free (result->edited_command);
|
||||
free (result->comment);
|
||||
memset (result, 0, sizeof (*result));
|
||||
}
|
||||
|
||||
// let the user review or edit the text before running it, returns NULL when cancelled
|
||||
static char *confirm(RCore *core, const char *text) {
|
||||
RCons *cons = core->cons;
|
||||
if (strchr (text, '\n')) {
|
||||
char *edited = r2ai_cons_editor (cons, NULL, text);
|
||||
return edited? edited: strdup (text);
|
||||
}
|
||||
r_cons_newline (cons);
|
||||
r_cons_readpush (cons, text, strlen (text));
|
||||
r_cons_readpush (cons, "\x05", 1); // ctrl+e moves the cursor to the end
|
||||
r_line_set_prompt (cons->line, "[r2ai]> ");
|
||||
const char *line = r_line_readline (cons);
|
||||
if (r_cons_is_breaked (cons) || R_STR_ISEMPTY (line)) {
|
||||
return NULL;
|
||||
}
|
||||
return strdup (line);
|
||||
}
|
||||
|
||||
static R2AI_ToolResult r2ai_r2cmd(RCore *core, RJson *args, bool verbose) {
|
||||
R2AI_ToolResult result = { 0 };
|
||||
if (!args) {
|
||||
return strdup ("{ \"res\":\"Command is NULL\" }");
|
||||
result.output = strdup ("{ \"res\":\"Command is NULL\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
const RJson *command_json = r_json_get (args, "command");
|
||||
if (!command_json || !command_json->str_value) {
|
||||
return strdup ("{ \"res\":\"No command in tool call arguments\" }");
|
||||
result.output = strdup ("{ \"res\":\"No command in tool call arguments\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
const char *command = command_json->str_value;
|
||||
char *comment = NULL;
|
||||
char *command = strip_command_comment (command_json->str_value, &comment);
|
||||
if (!command) {
|
||||
free (comment);
|
||||
result.output = strdup ("{ \"res\":\"Command is NULL\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (r_str_startswith (command, "r2 ")) {
|
||||
return strdup ("{ \"res\":\"You are already in r2!\" }");
|
||||
free (command);
|
||||
free (comment);
|
||||
result.output = strdup ("{ \"res\":\"You are already in r2!\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ask_to_execute = r_config_get_b (core->config, "r2ai.auto.yolo") != true;
|
||||
*edited_command = NULL; // keeps track of the command that was really executed in case user modifies it
|
||||
|
||||
if (ask_to_execute) {
|
||||
// Check if command contains newlines to determine if it's multi-line
|
||||
bool is_multiline = strchr (command, '\n') != NULL;
|
||||
|
||||
if (is_multiline) {
|
||||
// Use editor for multi-line commands
|
||||
*edited_command = strdup (command);
|
||||
r_cons_editor (core->cons, NULL, *edited_command);
|
||||
command = *edited_command;
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.yolo")) {
|
||||
char *input = compose_command_with_comment (command, comment);
|
||||
char *edited = confirm (core, input);
|
||||
free (input);
|
||||
if (!edited) {
|
||||
free (command);
|
||||
free (comment);
|
||||
result.output = strdup ("R2AI_SIGINT");
|
||||
return result;
|
||||
}
|
||||
char *new_comment = NULL;
|
||||
char *new_command = strip_command_comment (edited, &new_comment);
|
||||
free (edited);
|
||||
if (new_command) {
|
||||
free (command);
|
||||
command = new_command;
|
||||
free (comment);
|
||||
comment = new_comment;
|
||||
} else {
|
||||
// For single-line commands, push the command to input buffer
|
||||
r_cons_newline (core->cons);
|
||||
// Push the command to the input buffer
|
||||
|
||||
// Get user input with command pre-filled
|
||||
r_cons_readpush (core->cons, command, strlen (command));
|
||||
r_cons_readpush (core->cons, "\x05", 1); // Ctrl+E - move to end
|
||||
const char *readline_result = r_line_readline (core->cons);
|
||||
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
|
||||
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (readline_result)) {
|
||||
R_LOG_INFO ("Command execution cancelled %s", readline_result);
|
||||
return strdup ("R2AI_SIGINT");
|
||||
}
|
||||
|
||||
// Process the result
|
||||
if (R_STR_ISNOTEMPTY (readline_result)) {
|
||||
*edited_command = strdup (readline_result);
|
||||
command = *edited_command;
|
||||
} else {
|
||||
// If user just pressed enter, keep the original command
|
||||
*edited_command = strdup (command);
|
||||
command = *edited_command;
|
||||
}
|
||||
free (new_comment);
|
||||
}
|
||||
R_LOG_DEBUG ("Edited command: %s", *edited_command);
|
||||
} else {
|
||||
*edited_command = strdup (command);
|
||||
}
|
||||
|
||||
if (!hide_tool_output) {
|
||||
char *red_command = r_str_newf ("\x1b[31m%s\x1b[0m\n", *edited_command);
|
||||
r_cons_printf (core->cons, "%s", red_command);
|
||||
r_cons_flush (core->cons);
|
||||
free (red_command);
|
||||
result.edited_command = strdup (command);
|
||||
R_LOG_DEBUG ("Edited command: %s", command);
|
||||
|
||||
if (verbose) {
|
||||
char *display_command = compose_command_with_comment (command, comment);
|
||||
if (display_command) {
|
||||
char *red_command = r_str_newf (Color_RED "%s" Color_RESET "\n", display_command);
|
||||
r_cons_printf (core->cons, "%s", red_command);
|
||||
r_cons_flush (core->cons);
|
||||
free (red_command);
|
||||
free (display_command);
|
||||
}
|
||||
}
|
||||
|
||||
char *json_cmd = to_cmd (*edited_command);
|
||||
char *json_cmd = to_cmd (command);
|
||||
if (!json_cmd) {
|
||||
// caller should free edited_command
|
||||
return strdup ("{ \"res\":\"Failed to create JSON command\" }");
|
||||
free (command);
|
||||
free (comment);
|
||||
r2ai_tool_result_fini (&result);
|
||||
result.output = strdup ("{ \"res\":\"Failed to create JSON command\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
char *cmd_output = r_core_cmd_str (core, json_cmd);
|
||||
char *cmd_output = r2ai_cmdstr (core, json_cmd);
|
||||
free (json_cmd);
|
||||
free (command);
|
||||
|
||||
if (!cmd_output) {
|
||||
return strdup ("{ \"res\":\"Command returned no output or failed\" }");
|
||||
free (comment);
|
||||
r2ai_tool_result_fini (&result);
|
||||
result.output = strdup ("{ \"res\":\"Command returned no output or failed\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
return cmd_output;
|
||||
result.output = cmd_output;
|
||||
result.comment = comment;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute r_core_cmd_str with slim mode if enabled
|
||||
* Temporarily sets slim r2 commands output
|
||||
*/
|
||||
R_API char *r2ai_cmdstr(RCore *core, const char *cmd) {
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.slim")) {
|
||||
return r_core_cmd_str (core, cmd);
|
||||
}
|
||||
|
||||
RConfigHold *hold = r_config_hold_new (core->config);
|
||||
r_config_hold (hold, "asm.lines", "asm.lines.fcn", "scr.utf8", "asm.bytes", "emu.str", NULL);
|
||||
r_config_set_b (core->config, "asm.lines", false);
|
||||
r_config_set_b (core->config, "asm.lines.fcn", false);
|
||||
r_config_set_b (core->config, "scr.utf8", false);
|
||||
r_config_set_b (core->config, "asm.bytes", false);
|
||||
r_config_set_b (core->config, "emu.str", true);
|
||||
|
||||
char *result = r_core_cmd_str (core, cmd);
|
||||
|
||||
r_config_hold_restore (hold);
|
||||
r_config_hold_free (hold);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// qjs function implementation
|
||||
R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
|
||||
static R2AI_ToolResult r2ai_qjs(RCore *core, R2AI_State *state, RJson *args, bool verbose) {
|
||||
R2AI_ToolResult result = { 0 };
|
||||
if (!args) {
|
||||
return strdup ("{ \"res\":\"Script is NULL\" }");
|
||||
result.output = strdup ("{ \"res\":\"Script is NULL\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
const RJson *script_json = r_json_get (args, "script");
|
||||
if (!script_json) {
|
||||
return strdup ("{ \"res\":\"No script field found in arguments\" }");
|
||||
result.output = strdup ("{ \"res\":\"No script field found in arguments\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!script_json->str_value) {
|
||||
return strdup ("{ \"res\":\"Script value is NULL or empty\" }");
|
||||
result.output = strdup ("{ \"res\":\"Script value is NULL or empty\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ask_to_execute = r_config_get_b (core->config, "r2ai.auto.yolo") != true;
|
||||
const char *script = script_json->str_value;
|
||||
char *edited_script = NULL;
|
||||
|
||||
if (ask_to_execute) {
|
||||
// Check if script contains newlines to determine if it's multi-line
|
||||
bool is_multiline = strchr (script, '\n') != NULL;
|
||||
|
||||
if (is_multiline) {
|
||||
// Use editor for multi-line scripts
|
||||
edited_script = strdup (script);
|
||||
r_cons_editor (core->cons, NULL, edited_script);
|
||||
script = edited_script;
|
||||
} else {
|
||||
// For single-line scripts, push the script to input buffer
|
||||
|
||||
r_cons_readpush (core->cons, script, strlen (script));
|
||||
r_cons_readpush (core->cons, "\x05", 1); // Ctrl+E - move to end
|
||||
const char *readline_result = r_line_readline (core->cons);
|
||||
|
||||
// Check if interrupted or ESC pressed (readline_result is NULL or empty)
|
||||
if (r_cons_is_breaked (core->cons) || R_STR_ISEMPTY (readline_result)) {
|
||||
free (edited_script); // Free if already allocated
|
||||
return strdup ("R2AI_SIGINT");
|
||||
}
|
||||
|
||||
// Process the result
|
||||
if (readline_result && *readline_result) {
|
||||
edited_script = strdup (readline_result);
|
||||
script = edited_script;
|
||||
} else {
|
||||
// If user just pressed enter, keep the original script
|
||||
edited_script = strdup (script);
|
||||
script = edited_script;
|
||||
}
|
||||
char *script = strdup (script_json->str_value);
|
||||
if (!r_config_get_b (core->config, "r2ai.auto.yolo")) {
|
||||
char *edited = confirm (core, script);
|
||||
free (script);
|
||||
if (!edited) {
|
||||
result.output = strdup ("R2AI_SIGINT");
|
||||
return result;
|
||||
}
|
||||
script = edited;
|
||||
}
|
||||
|
||||
if (!hide_tool_output) {
|
||||
result.edited_command = strdup (script);
|
||||
|
||||
if (verbose) {
|
||||
char *print_script = r_str_newf ("\n```js\n%s\n```", script);
|
||||
char *print_script_rendered = r2ai_markdown (print_script);
|
||||
char *print_script_rendered = r2ai_markdown (state? &state->markdown: NULL, print_script);
|
||||
r_cons_printf (core->cons, "%s\n", print_script_rendered);
|
||||
r_cons_flush (core->cons);
|
||||
free (print_script);
|
||||
free (print_script_rendered);
|
||||
}
|
||||
char *payload = r_str_newf ("var console = { log:r2log, warn:r2log, info:r2log, error:r2log, debug:r2log };%s", script);
|
||||
|
||||
// Free edited_script after we're done using it
|
||||
free (edited_script);
|
||||
|
||||
R_LOG_DEBUG ("Payload length: %d", (int)strlen (payload));
|
||||
free (script);
|
||||
|
||||
if (!payload) {
|
||||
return strdup ("{ \"res\":\"Failed to create script payload\" }");
|
||||
r2ai_tool_result_fini (&result);
|
||||
result.output = strdup ("{ \"res\":\"Failed to create script payload\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
R_LOG_DEBUG ("Payload length: %d", (int)strlen (payload));
|
||||
|
||||
char *payload_base64 = r_base64_encode_dyn ((const ut8 *)payload, strlen (payload));
|
||||
if (!payload_base64) {
|
||||
free (payload);
|
||||
return strdup ("{ \"res\":\"Failed to encode script\" }");
|
||||
r2ai_tool_result_fini (&result);
|
||||
result.output = strdup ("{ \"res\":\"Failed to encode script\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
char *cmd = r_str_newf ("#!qjs -e base64:%s", payload_base64);
|
||||
@@ -425,182 +480,152 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool hide_tool_output) {
|
||||
char *json_cmd = to_cmd (cmd);
|
||||
if (!json_cmd) {
|
||||
free (cmd);
|
||||
return strdup ("{ \"res\":\"Failed to execute qjs\" }");
|
||||
r2ai_tool_result_fini (&result);
|
||||
result.output = strdup ("{ \"res\":\"Failed to execute qjs\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
char *cmd_output = r_core_cmd_str (core, json_cmd);
|
||||
free (json_cmd);
|
||||
free (cmd);
|
||||
|
||||
return cmd_output;
|
||||
result.output = cmd_output;
|
||||
return result;
|
||||
}
|
||||
|
||||
R_API char *execute_tool(RCore *core, const char *tool_name, const char *args, char **edited_command) {
|
||||
static char *tool_logs_to_string(const RJson *logs_json) {
|
||||
if (!logs_json || logs_json->type != R_JSON_ARRAY) {
|
||||
return NULL;
|
||||
}
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
if (!sb) {
|
||||
return NULL;
|
||||
}
|
||||
for (size_t i = 0; i < logs_json->children.count; i++) {
|
||||
const RJson *log = r_json_item (logs_json, i);
|
||||
if (log && log->type == R_JSON_OBJECT) {
|
||||
const RJson *type = r_json_get (log, "type");
|
||||
const RJson *message = r_json_get (log, "message");
|
||||
if (message && message->type == R_JSON_STRING) {
|
||||
if (type && type->type == R_JSON_STRING && R_STR_ISNOTEMPTY (type->str_value)) {
|
||||
r_strbuf_appendf (sb, "%s: ", type->str_value);
|
||||
}
|
||||
r_strbuf_appendf (sb, "%s\n", message->str_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
char *s = r_strbuf_drain (sb);
|
||||
if (R_STR_ISEMPTY (s)) {
|
||||
free (s);
|
||||
return NULL;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static void normalize_tool_output(RCore *core, bool verbose, const char *tool_name, const char *args, R2AI_ToolResult *result) {
|
||||
if (R_STR_ISEMPTY (result->output)) {
|
||||
free (result->output);
|
||||
result->output = strdup ("<no output>");
|
||||
return;
|
||||
}
|
||||
if (!strcmp (result->output, "R2AI_SIGINT")) {
|
||||
return;
|
||||
}
|
||||
if (!r_str_startswith (result->output, "{") || !strchr (result->output, '}')) {
|
||||
return;
|
||||
}
|
||||
|
||||
RJson *json = r_json_parse (result->output);
|
||||
if (!json) {
|
||||
R_LOG_WARN ("Failed to parse JSON response from tool execution");
|
||||
return;
|
||||
}
|
||||
|
||||
char *normalized = NULL;
|
||||
const RJson *logs_json = r_json_get (json, "logs");
|
||||
const RJson *error_json = r_json_get (json, "error");
|
||||
if (error_json && (error_json->type == R_JSON_BOOLEAN || error_json->type == R_JSON_STRING) &&
|
||||
((error_json->type == R_JSON_BOOLEAN && error_json->num.u_value) ||
|
||||
(error_json->type == R_JSON_STRING && error_json->str_value[0]))) {
|
||||
normalized = tool_logs_to_string (logs_json);
|
||||
if (!normalized && error_json->type == R_JSON_STRING) {
|
||||
normalized = strdup (error_json->str_value);
|
||||
}
|
||||
if (!normalized) {
|
||||
normalized = strdup ("Error occurred (no details available)");
|
||||
}
|
||||
} else {
|
||||
const RJson *res_json = r_json_get (json, "res");
|
||||
if (res_json && res_json->type == R_JSON_STRING) {
|
||||
normalized = strdup (res_json->str_value);
|
||||
} else {
|
||||
normalized = strdup (result->output);
|
||||
}
|
||||
if (R_STR_ISEMPTY (normalized)) {
|
||||
free (normalized);
|
||||
normalized = tool_logs_to_string (logs_json);
|
||||
}
|
||||
if (!normalized && !strcmp (tool_name, "r2cmd")) {
|
||||
normalized = strdup ("<no output>");
|
||||
}
|
||||
if (!normalized) {
|
||||
normalized = strdup ("<no output>");
|
||||
}
|
||||
}
|
||||
r_json_free (json);
|
||||
|
||||
if (!normalized) {
|
||||
normalized = r_str_newf ("Error running tool: Unknown error\nCommand: %s", args);
|
||||
}
|
||||
free (result->output);
|
||||
result->output = normalized;
|
||||
|
||||
if (verbose) {
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_printf (core->cons, "%s\n", result->output);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
r_str_ansi_strip (result->output);
|
||||
}
|
||||
|
||||
R_API R2AI_ToolResult execute_tool(RCorePluginSession *cps, const char *tool_name, const char *args) {
|
||||
R2AI_ToolResult result = { 0 };
|
||||
if (!cps || !cps->core) {
|
||||
result.output = strdup ("{ \"res\":\"Missing r2ai session\" }");
|
||||
return result;
|
||||
}
|
||||
RCore *core = cps->core;
|
||||
R2AI_State *state = cps->data;
|
||||
if (!tool_name || !args) {
|
||||
return strdup ("{ \"res\":\"Tool name or arguments are NULL\" }");
|
||||
result.output = strdup ("{ \"res\":\"Tool name or arguments are NULL\" }");
|
||||
return result;
|
||||
}
|
||||
|
||||
R_LOG_DEBUG ("Args: %s", args);
|
||||
|
||||
// Check if args is valid JSON before parsing
|
||||
if (!r_str_startswith (args, "{") || !strchr (args, '}')) {
|
||||
return r_str_newf ("Invalid JSON arguments: %s", args);
|
||||
result.output = r_str_newf ("Invalid JSON arguments: %s", args);
|
||||
return result;
|
||||
}
|
||||
|
||||
RJson *args_json = r_json_parse ((char *)args);
|
||||
if (!args_json) {
|
||||
return r_str_newf ("Failed to parse arguments: %s", args);
|
||||
result.output = r_str_newf ("Failed to parse arguments: %s", args);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hide_tool_output = r_config_get_b (core->config, "r2ai.auto.hide_tool_output");
|
||||
char *tool_result = NULL;
|
||||
bool verbose = r_config_get_b (core->config, "r2ai.auto.verbose");
|
||||
|
||||
if (strcmp (tool_name, "r2cmd") == 0) {
|
||||
tool_result = r2ai_r2cmd (core, args_json, hide_tool_output, edited_command);
|
||||
result = r2ai_r2cmd (core, args_json, verbose);
|
||||
} else if (strcmp (tool_name, "execute_js") == 0) {
|
||||
tool_result = r2ai_qjs (core, args_json, hide_tool_output);
|
||||
result = r2ai_qjs (core, state, args_json, verbose);
|
||||
} else {
|
||||
tool_result = strdup ("{ \"res\":\"Unknown tool\" }");
|
||||
result.output = strdup ("{ \"res\":\"Unknown tool\" }");
|
||||
}
|
||||
|
||||
// Check for interruption after executing the tool
|
||||
if (tool_result && strcmp (tool_result, "R2AI_SIGINT") == 0) {
|
||||
free (tool_result);
|
||||
return strdup ("R2AI_SIGINT");
|
||||
}
|
||||
|
||||
// Try to parse as JSON (equivalent to json.loads in Python)
|
||||
char *result = NULL;
|
||||
|
||||
// Check for empty or invalid response that could cause a crash
|
||||
if (R_STR_ISEMPTY (tool_result)) {
|
||||
free (tool_result);
|
||||
return strdup ("{ \"res\":\"Error: Empty or invalid response from QJS execution\" }");
|
||||
}
|
||||
|
||||
// Validate that the tool_result looks like valid JSON before parsing
|
||||
if (!r_str_startswith (tool_result, "{") || !strchr (tool_result, '}')) {
|
||||
// Not a JSON object, return as plain text
|
||||
result = strdup (tool_result);
|
||||
free (tool_result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try to parse the JSON safely
|
||||
RJson *json = r_json_parse (tool_result);
|
||||
|
||||
if (!json) {
|
||||
// JSON parsing failed, return original content
|
||||
R_LOG_WARN ("Failed to parse JSON response from tool execution");
|
||||
result = strdup (tool_result);
|
||||
free (tool_result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (json) {
|
||||
// Check for error field
|
||||
const RJson *error_json = r_json_get (json, "error");
|
||||
if (error_json && (error_json->type == R_JSON_BOOLEAN || error_json->type == R_JSON_STRING) &&
|
||||
((error_json->type == R_JSON_BOOLEAN && error_json->num.u_value) ||
|
||||
(error_json->type == R_JSON_STRING && error_json->str_value[0]))) {
|
||||
|
||||
// Build error message from logs if available
|
||||
const RJson *logs_json = r_json_get (json, "logs");
|
||||
if (logs_json && logs_json->type == R_JSON_ARRAY) {
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
|
||||
for (size_t i = 0; i < logs_json->children.count; i++) {
|
||||
const RJson *log = r_json_item (logs_json, i);
|
||||
if (log && log->type == R_JSON_OBJECT) {
|
||||
const RJson *message = r_json_get (log, "message");
|
||||
if (message && message->type == R_JSON_STRING) {
|
||||
r_strbuf_appendf (sb, "%s\n", message->str_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result = r_strbuf_drain (sb);
|
||||
} else {
|
||||
// If no logs, return the error message directly
|
||||
if (error_json->type == R_JSON_STRING) {
|
||||
result = strdup (error_json->str_value);
|
||||
} else {
|
||||
result = strdup ("Error occurred (no details available)");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Extract res field
|
||||
const RJson *res_json = r_json_get (json, "res");
|
||||
if (res_json && res_json->type == R_JSON_STRING) {
|
||||
result = strdup (res_json->str_value);
|
||||
} else {
|
||||
// Just return the entire command output if res field not found
|
||||
result = strdup (tool_result);
|
||||
}
|
||||
}
|
||||
|
||||
r_json_free (json);
|
||||
} else {
|
||||
// JSON parse failed, handle like JSONDecodeError in Python
|
||||
char *trimmed = r_str_trim_dup (tool_result);
|
||||
if (trimmed) {
|
||||
// Manual line splitting to avoid r_str_split issues
|
||||
RList *lines = r_str_split_list (trimmed, "\n", 0);
|
||||
if (lines) {
|
||||
int line_count = r_list_length (lines);
|
||||
if (line_count > 0) {
|
||||
char *last_line = r_list_get_n (lines, line_count - 1);
|
||||
if (last_line && r_str_startswith (last_line, "{\"res\":\"\"")) {
|
||||
// Remove the last line by joining all lines except the last
|
||||
RStrBuf *sb = r_strbuf_new ("");
|
||||
int i = 0;
|
||||
|
||||
// Use manual iteration instead of r_list_foreach_n
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
// Skip the last line
|
||||
if (line != last_line) {
|
||||
if (i > 0) {
|
||||
r_strbuf_append (sb, "\n");
|
||||
}
|
||||
r_strbuf_append (sb, line);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
result = r_strbuf_drain (sb);
|
||||
} else {
|
||||
// Keep the output as is
|
||||
result = strdup (tool_result);
|
||||
}
|
||||
} else {
|
||||
result = strdup (tool_result);
|
||||
}
|
||||
r_list_free (lines);
|
||||
} else {
|
||||
result = strdup (tool_result);
|
||||
}
|
||||
free (trimmed);
|
||||
} else {
|
||||
result = strdup (tool_result);
|
||||
}
|
||||
}
|
||||
|
||||
free (tool_result);
|
||||
|
||||
if (!result) {
|
||||
return r_str_newf ("Error running tool: Unknown error\nCommand: %s", args);
|
||||
}
|
||||
if (!hide_tool_output) {
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_printf (core->cons, "%s\n", result);
|
||||
r_cons_newline (core->cons);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
|
||||
r_str_ansi_strip (result);
|
||||
|
||||
r_json_free (args_json);
|
||||
normalize_tool_output (core, verbose, tool_name, args, &result);
|
||||
return result;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/* r2ai - MIT - Copyright 2024-2025 pancake */
|
||||
/* r2ai - MIT - Copyright 2024-2026 pancake */
|
||||
|
||||
#include <r_util.h>
|
||||
#include "r_vdb.h"
|
||||
#include "r2ai.h"
|
||||
|
||||
static Vector vector_new(int dim) {
|
||||
Vector v;
|
||||
@@ -36,10 +35,16 @@ static float squared_distance(const Vector *a, const Vector *b) {
|
||||
/*-------------------------------
|
||||
KDNode Utility Functions
|
||||
-------------------------------*/
|
||||
static KDNode *create_kdnode(const Vector *v, const char *text, int split_dim) {
|
||||
KDNode *node = (KDNode *)malloc (sizeof (KDNode));
|
||||
static KDNode *create_kdnode(const Vector *v, const char *text, int split_dim) { KDNode *node = (KDNode *)calloc (1, sizeof (KDNode));
|
||||
if (!node) {
|
||||
return NULL;
|
||||
}
|
||||
node->point.dim = v->dim;
|
||||
node->point.data = (float *)malloc (sizeof (float) * v->dim);
|
||||
if (!node->point.data) {
|
||||
free (node);
|
||||
return NULL;
|
||||
}
|
||||
memcpy (node->point.data, v->data, sizeof (float) * v->dim);
|
||||
node->text = text? strdup (text): NULL;
|
||||
node->split_dim = split_dim;
|
||||
@@ -78,16 +83,21 @@ void r_vdb_free(RVdb *db) {
|
||||
}
|
||||
|
||||
/* Recursive KD-tree insertion */
|
||||
static KDNode *kd_insert_recursive(KDNode *node, const Vector *v, const char *text, int depth, int dimension) {
|
||||
if (node == NULL) {
|
||||
static KDNode *kd_insert_recursive(KDNode *node, const Vector *v, const char *text, int depth, int dimension) { if (node == NULL) {
|
||||
int split_dim = depth % dimension;
|
||||
return create_kdnode (v, text, split_dim);
|
||||
}
|
||||
int axis = node->split_dim;
|
||||
KDNode *child = kd_insert_recursive (
|
||||
(v->data[axis] < node->point.data[axis]) ? node->left : node->right,
|
||||
v, text, depth + 1, dimension);
|
||||
if (!child) {
|
||||
return node; // allocation failed, keep existing tree
|
||||
}
|
||||
if (v->data[axis] < node->point.data[axis]) {
|
||||
node->left = kd_insert_recursive (node->left, v, text, depth + 1, dimension);
|
||||
node->left = child;
|
||||
} else {
|
||||
node->right = kd_insert_recursive (node->right, v, text, depth + 1, dimension);
|
||||
node->right = child;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
@@ -99,8 +109,7 @@ void r_vdb_insert(RVdb *db, const char *text) {
|
||||
return;
|
||||
}
|
||||
float *embedding = (float *)calloc (db->dimension, sizeof (float));
|
||||
// New call: pass the db pointer so TF-IDF stats are updated.
|
||||
compute_embedding (db, text, embedding, db->dimension);
|
||||
compute_embedding (db, text, embedding, db->dimension, true);
|
||||
Vector v;
|
||||
v.dim = db->dimension;
|
||||
v.data = embedding;
|
||||
@@ -110,9 +119,15 @@ void r_vdb_insert(RVdb *db, const char *text) {
|
||||
}
|
||||
|
||||
// K-NN Search Data Structures and Helpers
|
||||
static RVdbResultSet *create_knn_result_set(int capacity) {
|
||||
RVdbResultSet *rs = (RVdbResultSet *)malloc (sizeof (RVdbResultSet));
|
||||
rs->results = (RVdbResult *)malloc (sizeof (RVdbResult) * capacity);
|
||||
static RVdbResultSet *create_knn_result_set(int capacity) { RVdbResultSet *rs = (RVdbResultSet *)calloc (1, sizeof (RVdbResultSet));
|
||||
if (!rs) {
|
||||
return NULL;
|
||||
}
|
||||
rs->results = (RVdbResult *)calloc (capacity, sizeof (RVdbResult));
|
||||
if (!rs->results) {
|
||||
free (rs);
|
||||
return NULL;
|
||||
}
|
||||
rs->capacity = capacity;
|
||||
rs->size = 0;
|
||||
return rs;
|
||||
@@ -228,16 +243,22 @@ void kd_search_knn_recursive(KDNode *node, const Vector *query, RVdbResultSet *r
|
||||
* Find the k nearest neighbors to the embedding computed from `query_data`.
|
||||
* Returns a RVdbResultSet that must be freed by the caller.
|
||||
*/
|
||||
RVdbResultSet *r_vdb_query_embedding(RVdb *db, const float *query_data, int k) {
|
||||
if (!db || db->size == 0 || k <= 0) {
|
||||
RVdbResultSet *r_vdb_query_embedding(RVdb *db, const float *query_data, int k) { if (!db || db->size == 0 || k <= 0) {
|
||||
return NULL;
|
||||
}
|
||||
Vector query_vec = vector_new (db->dimension);
|
||||
if (!query_vec.data) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < db->dimension; i++) {
|
||||
query_vec.data[i] = query_data[i];
|
||||
}
|
||||
// query_vec is already normalized by compute_embedding ()
|
||||
RVdbResultSet *rs = create_knn_result_set (k);
|
||||
if (!rs) {
|
||||
vector_free (&query_vec);
|
||||
return NULL;
|
||||
}
|
||||
kd_search_knn_recursive (db->root, &query_vec, rs, 0, db->dimension);
|
||||
/* Optional: sort the result set in ascending order of distance */
|
||||
qsort (rs->results, rs->size, sizeof (RVdbResult), compare_knn_result);
|
||||
@@ -247,7 +268,7 @@ RVdbResultSet *r_vdb_query_embedding(RVdb *db, const float *query_data, int k) {
|
||||
|
||||
RVdbResultSet *r_vdb_query(RVdb *db, const char *text, int k) {
|
||||
float *query_embedding = (float *)calloc (db->dimension, sizeof (float));
|
||||
compute_embedding (db, text, query_embedding, db->dimension);
|
||||
compute_embedding (db, text, query_embedding, db->dimension, false);
|
||||
// No extra normalization is needed.
|
||||
RVdbResultSet *res = r_vdb_query_embedding (db, query_embedding, k);
|
||||
free (query_embedding);
|
||||
|
||||
+17
-16
@@ -1,4 +1,4 @@
|
||||
/* r2ai - MIT - Copyright 2024-2025 pancake */
|
||||
/* r2ai - MIT - Copyright 2024-2026 pancake */
|
||||
|
||||
#include <r_util.h>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
// Create a new global DF entry.
|
||||
static void gtfidf_add(RList *db_tokens, const char *token) {
|
||||
RVdbToken *t = R_NEW (RVdbToken);
|
||||
RVdbToken *t = R_NEW0 (RVdbToken);
|
||||
t->token = r_str_trim_dup (token);
|
||||
t->count = 1;
|
||||
t->df = 1.0f;
|
||||
r_list_append (db_tokens, t);
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ static bool valid_token(const char *a) {
|
||||
|
||||
#if USE_OLLAMA_EMBED
|
||||
// experimental ollama
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim) {
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim, bool update_stats) {
|
||||
// curl http://localhost:11434/api/embed -d '{ "model": "llama3:latest", "input": "text" }' |jq -r '.embeddings[0]'
|
||||
char *json_text = r_str_escape_utf8_for_json (text, -1);
|
||||
const char *model = "llama3:latest";
|
||||
@@ -74,7 +75,7 @@ static void compute_embedding(RVdb *db, const char *text, float *embedding, unsi
|
||||
free (s);
|
||||
}
|
||||
#else
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim) {
|
||||
static void compute_embedding(RVdb *db, const char *text, float *embedding, unsigned int dim, bool update_stats) {
|
||||
// gtfidf_list (db);
|
||||
|
||||
// Zero the embedding vector.
|
||||
@@ -111,24 +112,24 @@ static void compute_embedding(RVdb *db, const char *text, float *embedding, unsi
|
||||
token = strtok_r (NULL, " \t\r\n", &saveptr);
|
||||
}
|
||||
free (buffer);
|
||||
db->total_docs++;
|
||||
|
||||
/* --- Step 2. Update Global Document Frequencies --- */
|
||||
// Here we use the global definition of token_df (do not re-declare it locally).
|
||||
/* --- Step 2. Update Global Document Frequencies (only when indexing, never for queries) --- */
|
||||
RListIter *iter;
|
||||
RVdbToken *dt_token;
|
||||
r_list_foreach (doc_tokens, iter, dt_token) {
|
||||
RVdbToken *t = gtfidf_find (db->tokens, dt_token->token);
|
||||
if (t) {
|
||||
if (valid_token (dt_token->token)) {
|
||||
t->count++;
|
||||
t->df += 1.0f;
|
||||
if (update_stats) {
|
||||
db->total_docs++;
|
||||
r_list_foreach (doc_tokens, iter, dt_token) {
|
||||
RVdbToken *t = gtfidf_find (db->tokens, dt_token->token);
|
||||
if (t) {
|
||||
if (valid_token (dt_token->token)) {
|
||||
t->count++;
|
||||
t->df += 1.0f;
|
||||
}
|
||||
} else {
|
||||
gtfidf_add (db->tokens, dt_token->token);
|
||||
}
|
||||
} else {
|
||||
gtfidf_add (db->tokens, dt_token->token);
|
||||
}
|
||||
}
|
||||
// Increment the total number of documents.
|
||||
|
||||
/* --- Step 3. Compute TF-IDF for Each Token and Update the Embedding --- */
|
||||
RVdbToken *dt;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
#define R2AI_VERTEX_TOKEN_TTL_US (3300ULL * R_USEC_PER_SEC) /* 55 minutes, tokens are valid for ~60 */
|
||||
|
||||
static char *vertex_fetch_token(void) {
|
||||
char *version = r_sys_cmd_str ("gcloud version", NULL, NULL);
|
||||
if (R_STR_ISEMPTY (version)) {
|
||||
free (version);
|
||||
R_LOG_ERROR ("gcloud CLI not found. Install it from https://cloud.google.com/sdk/docs/install");
|
||||
return NULL;
|
||||
}
|
||||
free (version);
|
||||
char *token = r_sys_cmd_str ("gcloud auth application-default print-access-token", NULL, NULL);
|
||||
if (R_STR_ISEMPTY (token)) {
|
||||
free (token);
|
||||
R_LOG_ERROR ("gcloud is not authenticated. Run 'gcloud auth application-default login'");
|
||||
return NULL;
|
||||
}
|
||||
r_str_trim (token);
|
||||
if (R_STR_ISEMPTY (token)) {
|
||||
free (token);
|
||||
return NULL;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
/* The returned pointer is owned by state and must NOT be freed by the caller */
|
||||
R_IPI const char *r2ai_vertex_get_token(R2AI_State *state) {
|
||||
ut64 now = r_time_now_mono ();
|
||||
if (state->vertex_token && now < state->vertex_token_expiry) {
|
||||
return state->vertex_token;
|
||||
}
|
||||
free (state->vertex_token);
|
||||
state->vertex_token = vertex_fetch_token ();
|
||||
state->vertex_token_expiry = state->vertex_token? now + R2AI_VERTEX_TOKEN_TTL_US: 0;
|
||||
return state->vertex_token;
|
||||
}
|
||||
|
||||
// returns the publisher endpoint url or NULL when the cloud project or region are not set
|
||||
static char *vertex_url(const char *publisher, const char *model, const char *method, char **error) {
|
||||
char *project = r_sys_getenv ("GOOGLE_CLOUD_PROJECT");
|
||||
char *region = r_sys_getenv ("GOOGLE_CLOUD_REGION");
|
||||
char *url = NULL;
|
||||
if (R_STR_ISNOTEMPTY (project) && R_STR_ISNOTEMPTY (region)) {
|
||||
url = r_str_newf ("https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/%s/models/%s:%s",
|
||||
region, project, region, publisher, model, method);
|
||||
} else {
|
||||
*error = strdup ("Set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION env vars first");
|
||||
}
|
||||
free (project);
|
||||
free (region);
|
||||
return url;
|
||||
}
|
||||
|
||||
static char *vertex_post(RCorePluginSession *cps, R2AIArgs *args, const char *name, const char *publisher, const char *method, const char *data) {
|
||||
char *url = vertex_url (publisher, args->model, method, args->error);
|
||||
if (!url) {
|
||||
return NULL;
|
||||
}
|
||||
char *auth = r_str_newf ("Authorization: Bearer %s", args->api_key);
|
||||
const char *headers[] = { "Content-Type: application/json", auth, NULL };
|
||||
int code = 0;
|
||||
char *res = r2ai_post (cps->core, name, url, headers, data, &code, args->error);
|
||||
free (auth);
|
||||
free (url);
|
||||
return res;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_gemini(RCorePluginSession *cps, R2AIArgs args) {
|
||||
char *data = r2ai_gemini_request (&args);
|
||||
char *res = vertex_post (cps, &args, "vertex_gemini", "google", "generateContent", data);
|
||||
free (data);
|
||||
R2AI_ChatResponse *result = res? r2ai_gemini_parse (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_vertex_anthropic(RCorePluginSession *cps, R2AIArgs args) {
|
||||
char *data = r2ai_anthropic_request (&args, true);
|
||||
if (!data) {
|
||||
*args.error = strdup ("No input or messages provided");
|
||||
return NULL;
|
||||
}
|
||||
char *res = vertex_post (cps, &args, "vertex_anthropic", "anthropic", "rawPredict", data);
|
||||
free (data);
|
||||
R2AI_ChatResponse *result = res? r2ai_anthropic_parse_response (res, args.error): NULL;
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
/* r2ai wizard - Copyright 2025-2026 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai_wizard"
|
||||
|
||||
#include <time.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
static const char *clippy_messages[] = {
|
||||
"🪄 Abracadabra! Let's summon some AI magic!",
|
||||
"🔮 Crystal ball says... you need r2ai setup!",
|
||||
"⚡ Powering up the reverse-engineering spells...",
|
||||
"🎩 Pulling rabbits out of the binary hat!",
|
||||
"🌟 Twinkling with AI-powered insights!",
|
||||
"🚀 Launching into the AI dimension!",
|
||||
"💫 Casting binary analysis enchantments!",
|
||||
"🎯 Targeting the perfect AI setup!",
|
||||
"🔓 Unlocking the secrets of the binaries!",
|
||||
"🌈 Painting rainbows on your disassembly!",
|
||||
"⭐ You're not just reversing binaries — you're reverse-engineering reality!",
|
||||
"🎪 Welcome to the greatest AI show in radare2!",
|
||||
"🦄 Riding unicorns through the code matrix!",
|
||||
"🎨 Painting masterpieces with binary brushes!",
|
||||
"🌊 Surfing the waves of artificial intelligence!",
|
||||
"🎭 Masking the complexity, revealing the simplicity!",
|
||||
"🔥 Burning through the binary fog!",
|
||||
"⚗️ Alchemizing bytes into understanding!",
|
||||
"🎪 Step right up! The AI circus is in town!",
|
||||
"🌟 Making binary magic happen!"
|
||||
};
|
||||
|
||||
static const char *wizard_tips[] = {
|
||||
"Only those who understand the bytes will master the bits.",
|
||||
"With great disassembly comes great responsibility.",
|
||||
"AI can read disassembly faster than elves read runes — but don't trust it blindly!",
|
||||
"A function without comments is a spell without meaning.",
|
||||
"Remember, young hacker — local models don't leak secrets!",
|
||||
"The force is strong with this binary analyzer.",
|
||||
"In AI we trust, but verify we must!",
|
||||
"Every binary tells a story, let AI be your translator.",
|
||||
"Debugging is like detective work, AI is your Watson.",
|
||||
"May the source be with you, always!"
|
||||
};
|
||||
|
||||
static char *wizard_ready_file(void) {
|
||||
return r_file_home (".config/r2ai/.ready");
|
||||
}
|
||||
|
||||
static bool wizard_mark_ready(void) {
|
||||
char *config_dir = r_file_home (".config/r2ai");
|
||||
r_sys_mkdirp (config_dir);
|
||||
free (config_dir);
|
||||
|
||||
char *ready_file = wizard_ready_file ();
|
||||
int fd = r_sandbox_open (ready_file, O_CREAT | O_WRONLY, 0644);
|
||||
if (fd != -1) {
|
||||
close (fd);
|
||||
}
|
||||
free (ready_file);
|
||||
return fd != -1;
|
||||
}
|
||||
|
||||
static void show_clippy_message(RCore *core, const char *message) {
|
||||
r_cons_clear00 (core->cons);
|
||||
char *cmd = r_str_newf ("?E %s", message);
|
||||
r_core_call (core, cmd);
|
||||
free (cmd);
|
||||
r_cons_flush (core->cons);
|
||||
}
|
||||
|
||||
static void show_progress_bar(RCore *core, const char *message, int current, int total) {
|
||||
int percent = (current * 100) / total;
|
||||
r_print_progressbar (core->print, percent, 80, message);
|
||||
}
|
||||
|
||||
static const char *get_random_clippy_message(void) {
|
||||
return clippy_messages[rand () % (sizeof (clippy_messages) / sizeof (clippy_messages[0]))];
|
||||
}
|
||||
|
||||
static const char *get_random_wizard_tip(void) {
|
||||
return wizard_tips[rand () % (sizeof (wizard_tips) / sizeof (wizard_tips[0]))];
|
||||
}
|
||||
|
||||
static bool wizard_step_intro(RCore *core) {
|
||||
show_clippy_message (core, get_random_clippy_message ());
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🪄 Hi, I'm r2clippy, your reverse-engineering wizard assistant!\n"
|
||||
"🎯 I'll help you summon the power of AI inside radare2 (and even outside it).\n"
|
||||
"🚀 Let's start with a few spells... ehm, setup steps!\n\n"
|
||||
"%s\n\n",
|
||||
get_random_wizard_tip ());
|
||||
|
||||
return r_cons_yesno (core->cons, 'y', "Ready to begin your AI journey? (Y/n)");
|
||||
}
|
||||
|
||||
static bool wizard_step_setup(RCore *core) {
|
||||
show_clippy_message (core, "⚙️ Configuration time! Let's tune those AI settings!");
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🔧 First things first - let's make sure you have the latest r2ai!\n"
|
||||
"💡 It's good practice to update r2 and r2ai frequently for the latest fixes.\n\n"
|
||||
"Run this command to update: r2pm -Uci r2ai\n\n");
|
||||
|
||||
if (r_cons_yesno (core->cons, 'y', "Have you updated r2ai recently? (Y/n)")) {
|
||||
show_progress_bar (core, "Checking updates...", 1, 3);
|
||||
r_sys_sleep (1);
|
||||
show_progress_bar (core, "Checking updates...", 2, 3);
|
||||
r_sys_sleep (1);
|
||||
show_progress_bar (core, "Checking updates...", 3, 3);
|
||||
r_sys_sleep (1);
|
||||
r_cons_printf (core->cons, "\n");
|
||||
}
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🤖 What's a Provider? It's the AI service you'll use (OpenAI, Anthropic, etc.)\n"
|
||||
"🧠 What's a Model? It's the specific AI brain (GPT-4, Claude, Gemma, etc.)\n\n"
|
||||
"📋 Try these commands to explore:\n"
|
||||
" r2ai -p? # List available providers\n"
|
||||
" r2ai -m? # List available models\n\n"
|
||||
"💬 Type 'shell' to enter r2ai shell mode, 'back' to return to wizard\n\n");
|
||||
|
||||
if (r_cons_yesno (core->cons, 'y', "Want to skip API key setup for now? (Y/n)")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_clippy_message (core, "🔑 Time to configure those API keys!");
|
||||
r_cons_printf (core->cons, "\nOpening API key configuration...\n");
|
||||
r_core_call (core, "r2ai -K");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void wizard_claw_invoke(RCore *core, const char *extra) {
|
||||
char *cmd = R_STR_ISNOTEMPTY (extra)
|
||||
? r_str_newf ("r2ai -id %s", extra)
|
||||
: strdup ("r2ai -id");
|
||||
r_core_call (core, cmd);
|
||||
free (cmd);
|
||||
}
|
||||
|
||||
static bool wizard_step_personality(RCore *core) {
|
||||
if (r2ai_claw_exists ()) {
|
||||
return true;
|
||||
}
|
||||
show_clippy_message (core, "🎭 Want to give r2ai a custom personality?");
|
||||
r_cons_printf (core->cons,
|
||||
"\nr2ai defaults to 'r2clippy'. Later you can %s.\n\n",
|
||||
R2AI_CLAW_HINT);
|
||||
if (!r_cons_yesno (core->cons, 'n', "Generate a custom personality now? (y/N)")) {
|
||||
return true;
|
||||
}
|
||||
r_cons_printf (core->cons,
|
||||
"\nDescribe it in a few words (e.g. 'grumpy pirate hacker'),\n"
|
||||
"or just press Enter to let r2ai pick something at random:\n");
|
||||
const char *desc = r_line_readline (core->cons);
|
||||
char *trimmed = desc? r_str_trim_dup (desc): NULL;
|
||||
wizard_claw_invoke (core, trimmed);
|
||||
free (trimmed);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wizard_step_basic_usage(RCore *core) {
|
||||
show_clippy_message (core, "🧩 Let's learn the basics of AI-powered reversing!");
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"💬 Basic Chat Mode:\n"
|
||||
" r2ai what's your name\n"
|
||||
" r2ai explain this function\n\n"
|
||||
"🤖 Auto Mode - Your AI assistant can interact with r2 automatically!\n"
|
||||
" r2ai -a tell me the architecture of this binary\n"
|
||||
" r2ai -a find all strings in this program\n\n"
|
||||
"✨ Auto Mode features:\n"
|
||||
" • AI can run r2 commands for you\n"
|
||||
" • Perfect for learning r2 commands\n"
|
||||
" • Great for complex analysis tasks\n"
|
||||
" • You'll be asked to confirm actions\n\n");
|
||||
|
||||
if (r_cons_yesno (core->cons, 'y', "Want to try a basic chat test? (Y/n)")) {
|
||||
show_clippy_message (core, "🧪 Testing the AI connection...");
|
||||
r_cons_printf (core->cons, "\nTry: r2ai what's your name\n");
|
||||
r_cons_printf (core->cons, "Type 'shell' to enter r2ai mode, then 'back' to continue\n");
|
||||
|
||||
while (true) {
|
||||
r_cons_printf (core->cons, "r2ai-wizard> ");
|
||||
const char *line = r_line_readline (core->cons);
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (r_str_startswith (line, "shell")) {
|
||||
r_cons_printf (core->cons, "Entering r2ai shell mode (type 'exit' to return)...\n");
|
||||
r_core_call (core, "r2ai -r");
|
||||
} else if (r_str_startswith (line, "back")) {
|
||||
break;
|
||||
} else if (r_str_startswith (line, "skip")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wizard_step_decompilation(RCore *core) {
|
||||
show_clippy_message (core, "🧱 Master the art of AI-powered decompilation!");
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🔍 Decompilation with AI:\n"
|
||||
" r2ai -d # Decompile current function\n"
|
||||
" r2ai -do # Decompile with source offsets\n"
|
||||
" r2ai -d explain this # Ask about current function\n"
|
||||
" r2ai -dr # Recursive decompilation\n\n"
|
||||
"🎛️ Advanced decompilation:\n"
|
||||
" r2ai -e cmds=pdc,pdg,pdd # Use multiple decompilers as input\n\n"
|
||||
"🏠 Recommended local models for decompilation:\n"
|
||||
" • gpt-oss:20b # Open source, good balance\n"
|
||||
" • gemma3:12b # Google's model\n"
|
||||
" • granite:8b # IBM's model\n\n"
|
||||
"☁️ Online services (better quality):\n"
|
||||
" • OpenAI (GPT-4, GPT-3.5)\n"
|
||||
" • Anthropic (Claude)\n"
|
||||
" • Google (Gemini)\n"
|
||||
" • Mistral, Groq, and more!\n\n");
|
||||
|
||||
return r_cons_yesno (core->cons, 'y', "Ready to decompile like a wizard? (Y/n)");
|
||||
}
|
||||
|
||||
static bool wizard_step_plugins(RCore *core) {
|
||||
show_clippy_message (core, "💥 Discover more AI-powered tools!");
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🔮 Other amazing plugins:\n\n"
|
||||
"📜 decai:\n"
|
||||
" • Pure JavaScript decompilation plugin\n"
|
||||
" • Focused specifically on decompilation tasks\n"
|
||||
" • Lightweight and fast\n\n"
|
||||
"🔧 r2mcp:\n"
|
||||
" • Model Context Protocol integration\n"
|
||||
" • Lets AI agents use r2 as a tool\n"
|
||||
" • Advanced automation possibilities\n\n"
|
||||
"🌟 These plugins work great alongside r2ai!\n\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wizard_step_tasks(RCore *core) {
|
||||
show_clippy_message (core, "🧰 Level up your reversing game!");
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🎯 Challenge yourself with these tasks:\n\n"
|
||||
"🔓 Binary Analysis:\n"
|
||||
" r2ai -a find the xor key\n"
|
||||
" r2ai -a patch the binary to skip password check\n"
|
||||
" r2ai -a identify the encryption algorithm\n\n"
|
||||
"📚 Learning Path:\n"
|
||||
" Level 1: r2ai -a name all imported functions\n"
|
||||
" Level 2: r2ai -a find the main decryption routine\n"
|
||||
" Level 3: r2ai -a summarize the binary in one sentence\n"
|
||||
" Level 4: r2ai -a write an r2 script from AI output\n"
|
||||
" Level 5: Build your own AI plugin that uses r2pipe!\n\n"
|
||||
"🎮 Game Modes:\n"
|
||||
" • Mystery Mode: Get cryptic clues instead of answers\n"
|
||||
" • Mentor Mode: Explain the disassembly yourself\n"
|
||||
" • Challenge Mode: Complete tasks and get scored!\n\n");
|
||||
|
||||
return r_cons_yesno (core->cons, 'y', "Ready to tackle these challenges? (Y/n)");
|
||||
}
|
||||
|
||||
static bool wizard_step_followup(RCore *core) {
|
||||
show_clippy_message (core, "🔮 The journey continues...");
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"🌟 Keep the magic going:\n\n"
|
||||
"💬 Join the community:\n"
|
||||
" • r2con videos - Watch talks from reverse engineers\n"
|
||||
" • radare2 Discord - Chat with other wizards\n"
|
||||
" • GitHub discussions - Share your discoveries\n\n"
|
||||
"📚 Continue learning:\n"
|
||||
" • Try different models and providers\n"
|
||||
" • Experiment with custom prompts\n"
|
||||
" • Create your own AI workflows\n\n"
|
||||
"✨ Remember: %s\n\n",
|
||||
get_random_wizard_tip ());
|
||||
|
||||
r_cons_printf (core->cons,
|
||||
"🎉 Congratulations! You've completed the r2ai wizard!\n"
|
||||
"🪄 You're now ready to reverse-engineer with AI magic!\n\n");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool wizard_should_autorun(RCore *core) {
|
||||
if (!core || !core->config || !core->cons) {
|
||||
return false;
|
||||
}
|
||||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO)) {
|
||||
return false;
|
||||
}
|
||||
if (!r_config_get_b (core->config, "r2ai.wizard")) {
|
||||
return false;
|
||||
}
|
||||
return r2ai_wizard_isfirsttime ();
|
||||
}
|
||||
|
||||
R_API bool r2ai_wizard_autorun(RCore *core) {
|
||||
if (!wizard_should_autorun (core)) {
|
||||
return false;
|
||||
}
|
||||
show_clippy_message (core, "🪄 Want help setting up r2ai?");
|
||||
r_cons_printf (core->cons,
|
||||
"\n"
|
||||
"This looks like your first interactive r2ai session.\n"
|
||||
"Run the setup wizard now to configure providers, API keys and the chat workflow.\n\n");
|
||||
wizard_mark_ready ();
|
||||
if (!r_cons_yesno (core->cons, 'y', "Start the setup wizard now? (Y/n)")) {
|
||||
r_cons_printf (core->cons, "\nRun 'r2ai -w' any time to launch it later.\n\n");
|
||||
r_cons_flush (core->cons);
|
||||
return false;
|
||||
}
|
||||
return r2ai_wizard (core);
|
||||
}
|
||||
|
||||
R_API bool r2ai_wizard(RCore *core) {
|
||||
if (!core) {
|
||||
return false;
|
||||
}
|
||||
|
||||
r_cons_printf (core->cons, "\n");
|
||||
show_progress_bar (core, "Summoning r2clippy...", 1, 5);
|
||||
r_sys_sleep (1);
|
||||
show_progress_bar (core, "Summoning r2clippy...", 2, 5);
|
||||
r_sys_sleep (1);
|
||||
show_progress_bar (core, "Summoning r2clippy...", 3, 5);
|
||||
r_sys_sleep (1);
|
||||
show_progress_bar (core, "Summoning r2clippy...", 4, 5);
|
||||
r_sys_sleep (1);
|
||||
show_progress_bar (core, "Summoning r2clippy...", 5, 5);
|
||||
r_sys_sleep (1);
|
||||
r_cons_printf (core->cons, "\n\n");
|
||||
|
||||
if (!wizard_step_intro (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wizard_step_setup (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wizard_step_personality (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wizard_step_basic_usage (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wizard_step_decompilation (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wizard_step_plugins (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!wizard_step_tasks (core)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wizard_step_followup (core);
|
||||
|
||||
show_clippy_message (core, "🎊 Wizard complete! Go forth and reverse!");
|
||||
r_cons_printf (core->cons, "\nPress Enter to exit the wizard...\n");
|
||||
r_line_readline (core->cons);
|
||||
wizard_mark_ready ();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
R_API bool r2ai_wizard_isfirsttime(void) {
|
||||
char *ready_file = wizard_ready_file ();
|
||||
bool first_time = !r_file_exists (ready_file);
|
||||
free (ready_file);
|
||||
return first_time;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
all:
|
||||
r2r -u db/extras
|
||||
|
||||
.PHONY: all
|
||||
@@ -0,0 +1,33 @@
|
||||
NAME=OpenAI-compatible base URL
|
||||
FILE=-
|
||||
TIMEOUT=10
|
||||
CMDS=<<EOF
|
||||
!HOME=/tmp/r2ai-r2r-no-home R2_RCFILE=/dev/null ../src/r2ai -p ollama -m fixture-model -b http://127.0.0.1:1/v1/ -e debug=true -e http.backend=socket -e http.max_retries=0 -e http.timeout=1 'fixture prompt' 2>&1 | sed -n '/^Curl command:/{h;s/.*\(Content-Type: application\/json\).*/\1/p;g;s/.*"\(http[^\"]*\)".*/\1/p;}'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
Content-Type: application/json
|
||||
http://127.0.0.1:1/v1/chat/completions
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=Native Ollama API root
|
||||
FILE=-
|
||||
TIMEOUT=10
|
||||
CMDS=<<EOF
|
||||
!HOME=/tmp/r2ai-r2r-no-home R2_RCFILE=/dev/null ../src/r2ai -p ollama -m fixture-model -b http://127.0.0.1:1/api -e debug=true -e http.backend=socket -e http.max_retries=0 -e http.timeout=1 'fixture prompt' 2>&1 | sed -n '/^Curl command:/s/.*"\(http[^\"]*\)".*/\1/p'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
http://127.0.0.1:1/api/chat
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=Native Ollama generate API
|
||||
FILE=-
|
||||
TIMEOUT=10
|
||||
CMDS=<<EOF
|
||||
!HOME=/tmp/r2ai-r2r-no-home R2_RCFILE=/dev/null ../src/r2ai -p ollama -m fixture-model -b http://127.0.0.1:1/v1 -e apitype=generate -e debug=true -e http.backend=socket -e http.max_retries=0 -e http.timeout=1 'fixture prompt' 2>&1 | sed -n '/^Curl command:/s/.*"\(http[^\"]*\)".*/\1/p'
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
http://127.0.0.1:1/api/generate
|
||||
EOF
|
||||
RUN
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
# version.sh - print current version or bump to a new one
|
||||
# usage: ./version.sh [new-version]
|
||||
# Updates VERSION, decai/src/constants.ts, src/r2ai.h, and src/meson.build
|
||||
|
||||
VERSION_FILE="VERSION"
|
||||
TS_FILE="decai/src/constants.ts"
|
||||
R2AI_H="src/r2ai.h"
|
||||
MESON_FILE="src/meson.build"
|
||||
|
||||
NEW_VERSION="${1:-}"
|
||||
|
||||
get_version() {
|
||||
if [ -f "$VERSION_FILE" ]; then
|
||||
tr -d '[:space:]' < "$VERSION_FILE"
|
||||
else
|
||||
cat <<EOF >&2
|
||||
error: $VERSION_FILE not found
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
CURRENT=$(get_version)
|
||||
|
||||
if [ -z "$NEW_VERSION" ]; then
|
||||
printf '%s\n' "$CURRENT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# basic semver-ish validation: digits.digits.digits with optional pre-release/build
|
||||
if ! printf '%s' "$NEW_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-+.]?[A-Za-z0-9.-]*)?$'; then
|
||||
cat <<EOF >&2
|
||||
error: invalid version '$NEW_VERSION'
|
||||
expected format: MAJOR.MINOR.PATCH[-prerelease]
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "$NEW_VERSION" > "$VERSION_FILE"
|
||||
|
||||
if [ -f "$TS_FILE" ]; then
|
||||
sed -i -E "s/export const VERSION = \"[^\"]+\";/export const VERSION = \"$NEW_VERSION\";/" "$TS_FILE"
|
||||
fi
|
||||
|
||||
if [ -f "$R2AI_H" ]; then
|
||||
sed -i -E "s/#define R2AI_VERSION \"[^\"]+\"/#define R2AI_VERSION \"$NEW_VERSION\"/" "$R2AI_H"
|
||||
fi
|
||||
|
||||
if [ -f "$MESON_FILE" ]; then
|
||||
sed -i -E "s/version : '[^']+'/version : '$NEW_VERSION'/" "$MESON_FILE"
|
||||
fi
|
||||
|
||||
printf '%s -> %s\n' "$CURRENT" "$NEW_VERSION"
|
||||
Reference in new issue
Block a user