Compare commits

...
76 Commits
Author SHA1 Message Date
pancake d0a1f292f5 Release 1.4.2 2026-08-16 21:26:19 +02:00
pancake dad02ac243 Fix OpenAI-compatible endpoint selection for Ollama 2026-08-05 00:42:46 +02:00
dependabot[bot] 02c561c415 Bump radareorg/github-actions from 2 to 3
Bumps [radareorg/github-actions](https://github.com/radareorg/github-actions) from 2 to 3.
- [Release notes](https://github.com/radareorg/github-actions/releases)
- [Commits](https://github.com/radareorg/github-actions/compare/v2...v3)

---
updated-dependencies:
- dependency-name: radareorg/github-actions
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-08-04 18:21:02 +02:00
pancake 9414cdf72b Fix build for latest r2 (abiversion 125) 2026-07-22 18:04:38 +02:00
pancake 7cc61c4cc5 Generic core editor helper 2026-07-16 16:13:17 +02:00
pancake e4875eeb37 Fix build with r2 from git 2026-07-16 13:38:30 +02:00
pancake 888251a2ea Improve kvcache misses and improve token usage 2026-06-28 19:14:33 +02:00
pancake af6153ad64 Support r2ai.apitype (also in decai) to specify generate/chat 2026-06-28 19:14:29 +02:00
dependabot[bot] 87e4e7b232 Bump actions/checkout from 6 to 7 2026-06-22 16:18:17 +02:00
pancake cc3a003a64 Cleanup the openai implementation 2026-06-17 18:07:49 +02:00
pancake f2be5cc3c2 Release 1.4.0 2026-06-03 09:53:26 +02:00
dependabot[bot] e360148931 Bump radareorg/github-actions from 1 to 2 2026-05-18 09:43:20 +03:00
pancake d06a9ee361 Implement -d in the r2ai shell to decompile from the cli 2026-05-14 21:41:20 +02:00
pancake 2e7b97b065 Add r2ai -s to seek on start 2026-05-14 21:34:28 +02:00
orbisai0security cd5d9a1640 Fix shell injection in the abandoned python implementation 2026-05-14 08:18:20 +02:00
Ole André Vadla Ravnås e606392aaf Emit libr2ai.a for embedders
meson uses library() so -Ddefault_library picks the kind.
Makefile also runs ar rcs and installs libr2ai.a + r2ai.h.
2026-05-09 13:11:07 +02:00
pancake 8bf1169844 Implement r2ai -do to decompile with offsets 2026-05-06 13:15:32 +02:00
pancake 90e3bbafc5 Format code 2026-05-05 20:05:56 +02:00
pancake e5a0639b88 Remvoe support for legacy query prompts 2026-05-05 20:02:23 +02:00
pancake 80f27cd4d9 Add -qj to list query prompts in json format 2026-05-05 19:56:40 +02:00
pancake 455da7bdba Improve query prompts syntax and parser 2026-05-05 19:49:18 +02:00
pancake 623e98ed3d Show tool command to be executed when prompting 2026-05-05 19:16:50 +02:00
pancake 17bf56b5e3 Add explicit aprove/reject actions for async tasks 2026-05-05 18:27:58 +02:00
pancake f50a7836eb Initial support for r2plugin xps 2026-05-05 14:03:40 +02:00
pancake 2d9b69c624 Reduce tool calling parameters using the new ToolResult struct 2026-05-05 13:54:48 +02:00
pancake de9c7c904c Auto prompt is no longer a global 2026-05-05 11:32:57 +02:00
pancake 6b3ea982cd Better structure for r2ai-sj 2026-05-05 11:13:50 +02:00
pancake 209d9257ee Use RCoreHelpMessage in async commands 2026-05-05 11:11:46 +02:00
pancake b840d002ee Fix #226 - R2AI_HEADERS and r2ai.http.headers 2026-05-05 10:56:44 +02:00
pancake 9d900b2a6b Fix autopurge in -sj/-s and rename -s*/-sp 2026-05-05 06:45:23 +02:00
AsherDLL 227b15e793 Add Vertex AI provider for Gemini and Anthropic models 2026-04-24 13:49:40 +02:00
pancake 4b9dbf7686 Fully go for RCore.call 2026-04-21 19:48:49 +02:00
pancake 8ca32e258f Add async.purge option and fix some thread locks 2026-04-20 20:56:16 +02:00
pancake 449c41eae6 Format the code again 2026-04-20 20:28:40 +02:00
pancake 5210fe6892 Initial support for async tasks 2026-04-20 20:26:36 +02:00
pancake 6222791075 Add SOUL.md/IDENTITY.md personality files via -id command 2026-04-14 20:48:50 +02:00
pancake 05ce908e3a Fix ollamacloud api arguments object error 2026-04-14 19:07:50 +02:00
pancake b8457c0bfa Release 1.3.8 2026-04-13 22:46:10 +02:00
pancake 6a256b9210 Refresh auto tool list when r2ai.auto.usejs changes 2026-04-13 22:45:30 +02:00
pancake 8cc748ab4c Honor per-call model/provider overrides in LLM backends 2026-04-13 22:35:18 +02:00
pancake 17df397a3d Fix argument parsing to make -Lj actually work 2026-04-13 19:54:42 +02:00
pancake fcd464f1cc Fix OpenAI debug crash with keyless backends 2026-04-13 19:48:08 +02:00
pancake 1f41b6d2d7 Fix getopt parsing for -i, -dr and -Rq 2026-04-13 19:43:37 +02:00
pancake adf8189541 Fix UB in r2ai_message_free error paths 2026-04-13 18:01:24 +02:00
dependabot[bot] cb8f3548ff Bump softprops/action-gh-release from 2 to 3 2026-04-13 18:00:24 +02:00
pancake b99034e5d8 Fix user message not included when the message list was empty 2026-04-12 21:17:53 +02:00
pancake fa1f6c1d3c Fix few logic mistakes swapping name/desc and such 2026-04-12 21:16:01 +02:00
pancake d9fb7b18df Fix duplicated user message then r2ai.data=true 2026-04-12 21:14:01 +02:00
pancake e0b9ff235c Fix invalid free crashes on tool call block 2026-04-12 21:00:38 +02:00
pancake e1bf58d4e4 Some better variable names for decai 2026-04-12 18:40:34 +02:00
pancake 51a63ac7ef Remove unused and duplicated logic using helpers where needed 2026-04-12 18:35:07 +02:00
pancake c3a3eff046 Single helper to check if function is available in decai 2026-04-12 18:11:18 +02:00
pancake 37b5c2b6d0 Fix thinking level issue causing slow queries in r2ai 2026-04-12 11:53:35 +02:00
pancake 0e6a8cc2e2 In decai fix thinking options for glm models making a clear time improvements 2026-04-12 11:42:55 +02:00
pancake 8dcc6f0418 Release 1.3.6 2026-04-11 18:06:11 +02:00
pancake b68cdd1171 Fix empty query bug in r2ai -i 2026-04-10 16:00:58 +02:00
pancake 3f935a46be Do not silently discards caller-provided message history 2026-04-10 15:53:19 +02:00
pancake fd46390d41 Break auto mode when receiving unknown actions 2026-04-10 15:45:42 +02:00
pancake 63106508dc In decai: Restore model state when decopipe execution fails 2026-04-10 15:45:00 +02:00
pancake 46f42d3dd9 Ignore comment lines in decai.txt (decai -E) 2026-04-09 21:39:08 +02:00
pancake 836fae3d91 Use github actions to install r2 in the ci 2026-04-09 11:02:00 +02:00
pancake 66dd3dced7 Oops missing file 2026-04-09 10:44:16 +02:00
pancake c016fc72ed Release 1.3.4 2026-03-31 18:32:01 +02:00
Abhi 779b787f2d Fix URL formatting for OpenAI compatible API 2026-03-31 16:49:22 +02:00
pancake ca210f149b Add zen, ollamacloud and lmstudio providers to r2ai, sync with decai 2026-03-30 17:46:10 +02:00
pancake 99e99b1797 Reduce some LOCs in decai 2026-03-30 16:51:16 +02:00
pancake fb5427238b Add 'decai -E' for the defaults 2026-03-30 16:45:46 +02:00
pancake d7c2dacd7a Add decai timeout, zen provider, improve prompt, custom headers and more 2026-03-30 16:38:10 +02:00
pancake d6c32f4b72 decai: tighten types and simplify provider/config handling 2026-03-30 15:26:23 +02:00
dependabot[bot] 0a083e7231 Bump actions/download-artifact from 4 to 8 2026-03-30 15:20:45 +02:00
dependabot[bot] 1a067fbbfc Bump softprops/action-gh-release from 1 to 2 2026-03-30 15:18:54 +02:00
dependabot[bot] 8a724d3682 Bump actions/upload-artifact from 4 to 7 2026-03-30 15:18:30 +02:00
dependabot[bot] 8f7d152aa3 Bump actions/checkout from 5 to 6 2026-03-30 15:17:48 +02:00
pancake 7120c325da Add dependabot 2026-03-30 13:47:53 +02:00
pancake 14c991cd10 Update ci actions 2026-03-30 13:47:16 +02:00
pancake d1ef1e065e Welcome to the initial r2clippy wizard 2026-03-30 13:45:06 +02:00
66 changed files with 5180 additions and 1968 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- buildsystem
+10 -42
View File
@@ -1,8 +1,5 @@
name: CI and Release
env:
R2V: 6.0.4
on:
push:
branches:
@@ -26,7 +23,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Setup Meson/Ninja
if: matrix.build_system == 'meson'
@@ -34,38 +31,9 @@ jobs:
python -m pip install --upgrade pip
pip install meson ninja
- name: Install radare2 on Linux
if: matrix.os == 'ubuntu-24.04'
run: |
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2_${R2V}_amd64.deb"
wget "https://github.com/radareorg/radare2/releases/download/${R2V}/radare2-dev_${R2V}_amd64.deb"
sudo dpkg -i "radare2_${R2V}_amd64.deb"
sudo dpkg -i "radare2-dev_${R2V}_amd64.deb"
- name: Install radare2 on macOS
if: matrix.os == 'macos-latest'
run: |
git clone --depth=1 --branch "${{env.R2V}}" https://github.com/radareorg/radare2
cd radare2
sys/install.sh
- name: Install radare2 on Windows
if: matrix.os == 'windows-latest'
shell: powershell
run: |
$url = "https://github.com/radareorg/radare2/releases/download/${{ env.R2V }}/radare2-${{ env.R2V }}-w64.zip"
$dest = "$env:TEMP\radare2.zip"
Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
$prefix = 'C:\radare2'
if (Test-Path $prefix) { Remove-Item -Path $prefix -Recurse -Force }
Expand-Archive -Path $dest -DestinationPath $prefix -Force
# Move contents from subdirectory if present
$subdirs = Get-ChildItem -Path $prefix -Directory
if ($subdirs.Count -eq 1) {
$subdir = $subdirs[0]
Get-ChildItem -Path $subdir.FullName | Move-Item -Destination $prefix -Force
Remove-Item -Path $subdir.FullName -Recurse -Force
}
- name: Install radare2
id: r2
uses: radareorg/github-actions@v3
- name: Build with Make
if: matrix.build_system == 'make'
@@ -75,7 +43,7 @@ jobs:
if: matrix.build_system == 'meson' && matrix.os != 'windows-latest'
run: |
cd src
meson setup build -Dr2_prefix=${{ secrets.R2_PREFIX }} || meson configure build
meson setup build || meson configure build
meson compile -C build
- name: Build with Meson (Windows)
@@ -83,7 +51,7 @@ jobs:
shell: powershell
run: |
cd src
$prefix = 'C:\radare2'
$prefix = "${{ steps.r2.outputs.prefix }}"
meson setup build --backend ninja --prefix="$prefix" -Dr2_prefix="$prefix"
if ($LASTEXITCODE -ne 0) { meson configure build }
meson compile -C build
@@ -107,7 +75,7 @@ jobs:
- name: Upload artifacts (tags only)
if: startsWith(github.ref, 'refs/tags/')
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
@@ -127,7 +95,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Verify tag is on master
run: |
@@ -138,7 +106,7 @@ jobs:
fi
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: release-artifacts
@@ -146,7 +114,7 @@ jobs:
run: cp decai/decai.r2.js release-artifacts/
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
files: |
release-artifacts/**/*.zip
+14 -3
View File
@@ -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":
+4 -1
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
1.3.2
1.4.2
+13
View File
@@ -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
+15
View File
@@ -37,6 +37,7 @@ Usage: decai (-h) ...
decai -dd [..] - same as above, but ignoring cache
decai -dD [query]- decompile current function with given extra query
decai -e - display and change eval config vars
decai -E - edit decai.txt
decai -h - show this help
decai -i [f] [q] - include given file and query
decai -n - suggest better function name
@@ -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,10 @@ decai -e ctxfile=
...
```
Persist user defaults in `~/.config/r2ai/decai.txt` with one `key=value` entry
per line, then edit it with `decai -E`. That file is loaded on the first
`decai` command in a session and reloaded after `decai -E` exits.
List possible APIs to discuss with AI: `decai -e api=?`:
```
@@ -106,6 +113,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
+46 -35
View File
@@ -1,14 +1,14 @@
📦
23443 /main.js
27073 /main.js
✄
var F="1.3.2",b="decai",I="Transform this pseudocode and respond ONLY with plain code (NO explanations, comments or 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:', Reduce lines of code and fit everything in a single function, Remove all dead code",_={decai:`# Using Decai
var ee="1.4.2",y="decai",A="~/.config/r2ai",M=A+"/apikeys.txt",R=A+"/decai.txt",N="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.",F={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 gemini hf mistral ollama openapi openai vllm xai
r2ai deepseek anthropic claude gemini groq hf lmstudio mistral ollama ollamacloud openapi openai openrouter vllm xai zen
## Using Ollama
@@ -23,6 +23,10 @@ These are the most recommended models for decompiling in local:
## 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 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
@@ -35,11 +39,13 @@ These are the most recommended models for decompiling in local:
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:
@@ -111,48 +117,53 @@ Use radare2 to resolve user requests.
### Special cases
* On Swift binaries run...
`};var r={decopipe:{use:!1},host:"http://localhost",port:"11434",baseurl:"",api:"ollama",pipeline:"",commands:"pdc",yolo:!1,tts:!1,language:"C",humanLanguage:"English",deterministic:!0,debug:!1,think:-1,useFiles:!1,contextFile:"",model:"",cache:!1,maxInputTokens:-1,prompt:I,lastOutput:""};function j(e){return(r2.cmd("-e dir.tmp").trim()??".")+"/"+e}function R(e){return r2.cmd2("test -h").logs[0].message.indexOf("-fdx")!==-1?!0:r2.cmd("'test -vf "+e).startsWith("found")}function S(e,t){return e+" ".repeat(Math.max(0,t-e.length))}function Y(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}function J(e){let t=e.match(/```json\s*([\s\S]*?)```/);return t&&t[1]?t[1].trim():e}function G(e){let t=e,o=t.indexOf("{");o!==-1&&(t=t.slice(o));let i=t.indexOf("}");return i!==-1&&(t=t.slice(0,i+1)),t}function q(e){return btoa(e)}function $(e,t){let o=q(t);r2.cmd("p6ds "+o+" > "+e)}function k(e){let t=e;return r.think!==2&&(t=t.replace(/<think>[\s\S]*?<\/think>/gi,"")),t.split(`
`).filter(o=>!o.trim().startsWith("```")).join(`
`)}function N(e){r.debug&&console.log(e)}function H(e){let t={};for(let o of e.split(/\r?\n/)){let i=o.trim();if(!i||i.startsWith("#"))continue;let[s,...a]=i.split("=");if(!s||a.length===0)continue;let c=a.join("=").trim(),n=s.toLowerCase().replace(/_api_key$/i,"");t[n]=c}return t}var se={mistral:"MISTRAL_API_KEY",anthropic:"ANTHROPIC_API_KEY",huggingface:"HUGGINGFACE_API_KEY",openai:"OPENAI_API_KEY",gemini:"GEMINI_API_KEY",deepseek:"DEEPSEEK_API_KEY",xai:"XAI_API_KEY",ollama:"OLLAMA_API_KEY",ollamacloud:"OLLAMA_API_KEY"};function A(e,t){let o=r2.cmd("'%"+t).trim();if(o.indexOf("=")===-1&&o!=="")return[o.trim(),null,"env"];let i=e.toLowerCase(),s="~/.config/r2ai/apikeys.txt";if(R(s)){let c=r2.cmd("'cat "+s),n=H(c);if(Object.keys(n).indexOf(i)!==-1)return[n[i],null,"txt"]}let a="~/.r2ai."+i+"-key";if(R(a)){let c=r2.cmd("'cat "+a);return c===""?[null,"Cannot read "+a,"no"]:[c.trim(),null,"file"]}return[null,"Not available","nope"]}function W(){r2.cmd("'ed ~/.config/r2ai/apikeys.txt")}function B(){Object.entries(se).forEach(([e,t])=>{let o=A(e,t)[2];console.log(o," ",e)})}function E(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function ie(e){let{method:t,url:o,headers:i,payload:s}=e,a=["curl","-s",E(o)];if(i.forEach(c=>a.push("-H",E(c))),a.push("-H",E("Content-Type: application/json")),t==="POST"){if(!s)throw new Error("Payload required for POST requests");let c=r2.fdump(s);a.push("-d",E(`@${c}`));let n=a.join(" ")+" && rm "+E(c);return N(n),r2.syscmds(n)}else{let c=a.join(" ");return N(c),r2.syscmds(c)}}function ae(e){if(e==="")throw new Error("empty response");try{return JSON.parse(e)}catch(t){let o=t;throw console.error("output:",e),console.error(o,o.stack),new Error(o.message||"JSON parse error")}}function V(e){try{let t=ie(e);try{return ae(t)}catch(o){return{error:o.message}}}catch(t){return{error:t.message}}}function v(e,t){return V({method:"GET",url:e,headers:t})}function z(e,t,o){return V({method:"POST",url:e,headers:t,payload:o})}function T(e,t,o){let i=v(e,t);return i.error?(console.error(i.error),"error invalid response"):i.data?.map(o).join(`
`)||""}var le=e=>{let t=r.baseurl||r.host+":"+r.port;return T(`${t}/api/tags`,[],o=>o.name)},K=e=>{let t;if(e.requiresAuth&&e.authKey&&(t=A(e.authKey.split("_")[0].toLowerCase(),e.authKey),t&&t[1]))throw new Error(t[1]);let i=(r.baseurl||e.defaultBaseurl)+"/v1/models",s=t?["Authorization: Bearer "+t[0]]:[];return T(i,s,a=>a.id)},Q=e=>{let t=A("anthropic","ANTHROPIC_API_KEY");if(t&&t[1])throw new Error(t[1]);let i=(r.baseurl||e.defaultBaseurl)+"/v1/models",s=["x-api-key: "+t[0],"anthropic-version: 2023-06-01"];return T(i,s,a=>a.id)},ce=e=>{let t=A("mistral","MISTRAL_API_KEY");if(t&&t[1])throw new Error(t[1]);let i=(r.baseurl||e.defaultBaseurl)+"/v1/models",s=["Authorization: Bearer "+t[0]],a=v(i,s);return a.data?(n=>n.filter((l,u,p)=>p.findIndex(d=>d.name===l.name)===u))(a.data).map(n=>[S(n.name||n.id,30),S(""+(n.max_context_length||""),10),n.description||""].join(" ")).join(`
`):""},X={anthropic:{defaultModel:"claude-3-7-sonnet-20250219",defaultBaseurl:"https://api.anthropic.com",requiresAuth:!0,authKey:"ANTHROPIC_API_KEY",apiStyle:"anthropic",listModelsCallback:Q},claude:{defaultModel:"claude-3-7-sonnet-20250219",defaultBaseurl:"https://api.anthropic.com",requiresAuth:!0,authKey:"ANTHROPIC_API_KEY",apiStyle:"anthropic",listModelsCallback:Q},openai:{defaultModel:"gpt-4o-mini",defaultBaseurl:"https://api.openai.com",requiresAuth:!0,authKey:"OPENAI_API_KEY",apiStyle:"openai",listModelsCallback:K},ollama:{defaultModel:"qwen2.5-coder:latest",defaultBaseurl:"http://localhost:11434",requiresAuth:!1,apiStyle:"ollama",listModelsCallback:le},ollamacloud:{defaultModel:"gpt-oss:120b",defaultBaseurl:"https://ollama.com",requiresAuth:!0,authKey:"OLLAMA_API_KEY",apiStyle:"openai",listModelsCallback:K},gemini:{defaultModel:"gemini-2.5-flash",defaultBaseurl:"https://generativelanguage.googleapis.com",requiresAuth:!0,authKey:"GEMINI_API_KEY",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",requiresAuth:!0,authKey:"MISTRAL_API_KEY",apiStyle:"openai",listModelsCallback:ce},xai:{defaultModel:"grok-beta",defaultBaseurl:"https://api.x.ai",requiresAuth:!0,authKey:"XAI_API_KEY",apiStyle:"openai",hardcodedModels:["grok-2","grok-beta"]},lmstudio:{defaultModel:"local-model",defaultBaseurl:"http://127.0.0.1:1234",requiresAuth:!1,apiStyle:"openai",listModelsCallback:K},deepseek:{defaultModel:"deepseek-coder",defaultBaseurl:"https://api.deepseek.com",requiresAuth:!0,authKey:"DEEPSEEK_API_KEY",apiStyle:"openai",hardcodedModels:["deepseek-coder","deepseek-chat"]}};function L(e){return X[e]}function U(){return Object.keys(X)}function ue(e,t){let o=e;return r.think>=0&&(r.think===0?(o+=' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".',o+=" /no_think"):r.think>0&&(o="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."+o)),t?o:r.prompt+de()+o}function de(){return`
.Translate the code into `+r.language+` programming language
`}function M(e,t,o,i,s,a,c){let n=r.model||e.defaultModel,l=ue(t,o),u;if(e.requiresAuth&&e.authKey&&(u=A(e.authKey.split("_")[0].toLowerCase(),e.authKey),u[1]))return`Cannot read ~/.r2ai.${e.authKey.split("_")[0].toLowerCase()}-key`;let p=i(n,l,e),d=r.baseurl||e.defaultBaseurl,m=a(d,n,u&&u[0]?u[0]:void 0),w=c(u?u[0]:null,e);try{let h=z(m,w,JSON.stringify(p));return s(h)}catch(h){return"ERROR: "+h.message}}function pe(e,t,o){return M(e,t,o,(n,l)=>({stream:!1,model:n,messages:[{role:"user",content:l}]}),n=>{if(n.error&&typeof n.error=="object"&&n.error.message)throw new Error(n.error.message);if(n.choices&&n.choices[0]?.message?.content)return k(n.choices[0].message.content);throw new Error("Invalid response format")},(n,l)=>n+"/v1/chat/completions",n=>n?["Authorization: Bearer "+n]:[])}function me(e,t,o){return e.authKey?M(e,t,o,(n,l)=>{let u={model:n,max_tokens:5128,messages:[{role:"user",content:l}]};return r.deterministic&&Object.assign(u,{temperature:0,top_p:0,top_k:1}),u},n=>{if(n.content&&n.content[0]?.text)return k(n.content[0].text);if(n.error){let l=typeof n.error=="object"?n.error.message:n.error;throw new Error(l||"Unknown error")}throw new Error("Invalid response format")},(n,l)=>n+"/v1/messages",n=>["anthropic-version: 2023-06-01","x-api-key: "+n]):"ERROR: No auth key configured"}function fe(e,t,o){return M(e,t,o,(n,l)=>{let u={stream:!1,model:n,messages:[{role:"user",content:l}]};return r.deterministic&&(u.options={repeat_last_n:0,top_p:0,top_k:1,temperature:0,repeat_penalty:1,seed:123}),u},n=>{if(n&&n.error){let l=typeof n.error=="string"?n.error:JSON.stringify(n.error);throw new Error(l)}if(n.message&&n.message.content)return k(n.message.content);throw new Error(JSON.stringify(n))},(n,l)=>n+"/api/chat",()=>[])}function he(e,t,o){return e.authKey?M(e,t,o,(n,l)=>{let u={contents:[{parts:[{text:l}]}]};return r.deterministic&&(u.generationConfig={temperature:0,topP:1,topK:1}),u},n=>{let l=n;if(l.candidates&&l.candidates[0]?.content?.parts?.[0]?.text)return k(l.candidates[0].content.parts[0].text);throw l.error?new Error(typeof l.error=="string"?l.error:JSON.stringify(l.error)):(console.log(JSON.stringify(l)),new Error("Invalid response format"))},(n,l,u)=>`${n}/v1beta/models/${l}:generateContent?key=${u}`,()=>[]):"ERROR: No auth key configured"}function Z(e,t){let o=L(r.api);if(!o)return`Unknown value for 'decai -e api'. Available: ${U().join(", ")}`;switch(o.apiStyle){case"openai":return pe(o,e,t);case"anthropic":return me(o,e,t);case"ollama":return fe(o,e,t);case"gemini":return he(o,e,t);default:return`Unsupported API style: ${o.apiStyle}`}}function ee(e){let t=L(e);if(!t){console.error(`Unknown provider: ${e}`);return}try{let o="";t.listModelsCallback&&(o=t.listModelsCallback(t)),o&&console.log(o),t.hardcodedModels&&t.hardcodedModels.forEach(i=>console.log(i)),e==="mistral"&&console.log("ministral-8b-latest"),!o&&!t.hardcodedModels&&console.log(t.defaultModel)}catch(o){let i=o;console.error(`Error listing models for ${e}:`,i.message),console.log(t.defaultModel)}}var P={pipeline:{get:()=>r.pipeline,set:e=>{r.pipeline=e;try{r.decopipe=JSON.parse(r2.cmd("cat "+e))}catch(t){console.error(t)}}},model:{get:()=>r.model,set:e=>{e==="?"?ee(r.api):r.model=e.trim()}},deterministic:{get:()=>r.deterministic,set:e=>{r.deterministic=e==="true"||e==="1"}},files:{get:()=>r.useFiles,set:e=>{r.useFiles=e==="true"}},think:{get:()=>r.think,set:e=>{r.think=e==="true"?1:e==="false"?0:+e}},debug:{get:()=>r.debug,set:e=>{r.debug=e==="true"||e==="1"}},api:{get:()=>r.api,set:e=>{if(e==="?"){let t=U().join(`
`);console.error(t)}else r.api=e}},lang:{get:()=>r.language,set:e=>{r.language=e}},hlang:{get:()=>r.humanLanguage,set:e=>{r.humanLanguage=e}},cache:{get:()=>r.cache,set:e=>{r.cache=e==="true"||e==="1"}},cmds:{get:()=>r.commands,set:e=>{r.commands=e}},tts:{get:()=>r.tts,set:e=>{r.tts=e==="true"||e==="1"}},yolo:{get:()=>r.yolo,set:e=>{r.yolo=e==="true"||e==="1"}},prompt:{get:()=>r.prompt,set:e=>{r.prompt=e}},ctxfile:{get:()=>r.contextFile,set:e=>{r.contextFile=e}},baseurl:{get:()=>r.baseurl,set:e=>{r.baseurl=e}},maxtokens:{get:()=>r.maxInputTokens,set:e=>{r.maxInputTokens=parseInt(e,10)||-1}}};function x(e){let t=e.indexOf("="),o=t===-1?e:e.slice(0,t),i=t===-1?void 0:e.slice(t+1);if(!P[o]){console.error("Unknown config key");return}typeof i<"u"?P[o].set(i):console.log(P[o].get())}function te(){Object.keys(P).forEach(e=>{let t=P[e].get();console.log("decai -e "+e+"="+t)})}function f(e,t,o=!1){let i=(t||"").replace(/`/g,""),s=e.replace(/'/g,"");if(r.api==="r2"||r.api==="r2ai"){let c=j(".pdc.txt");$(c,i);let n=s.startsWith("-")?s:["-i",c,s].join(" "),u=(r.baseurl?r.baseurl+"/cmd":r.host+":"+r.port+"/cmd")+"/"+n.replace(/ /g,"%20").replace(/'/g,"\\'"),p=v(u,[]);return p.error?`Error: ${p.error}`:p.result||JSON.stringify(p)||"Cannot curl, use r2ai-server or r2ai -w"}if(s.startsWith("-"))return"";let a=s+`:
`+i;return r.maxInputTokens>0&&a.length>r.maxInputTokens&&(a=a.slice(0,r.maxInputTokens)),Z(a,o)}function oe(){let e="",t=o=>e+=" "+b+" "+o+`
`;e+="Usage: "+b+` (-h) ...
`,e+="Version: "+F+`
`,t("-a [query] - solve query with auto mode"),t("-b [url] - set base URL (alias for decai -e baseurl)"),t("-d [f1 ..] - decompile given functions"),t("-dd [..] - same as above, but ignoring cache"),t("-dD [query] - decompile current function with given extra query"),t("-dr - decompile function and its called ones (recursive)"),t("-e - display and change eval config vars"),t("-h - show this help"),t("-H - help setting up r2ai"),t("-i [f] [q] - include given file and query"),t("-k - list API key status"),t("-K - edit apikeys.txt"),t("-m [model] - use -m? or -e model=? to list the available models"),t("-n - suggest better function name"),t("-p [provider] - same as decai -e api (will be provider)"),t("-q [text] - query language model with given text"),t("-Q [text] - query on top of the last output"),t("-r [prompt] - change role prompt (same as: decai -e prompt)"),t("-R - reset role prompt to default prompt"),t("-s - function signature"),t("-v - show local variables"),t("-V - find vulnerabilities"),t("-x[*] - eXplain current function (-x* for r2 script)"),r2.log(e.trim())}function C(e,t,o,i){if(o){let n=r2.cmd("anos").trim();if(n.length>0)return n}let s="";if(i){let n=r2.cmd("s");s+=`## Context functions:
`;let l=r2.cmdAt("axff~^C[2]~$$",n);for(let u of l.split(/\n/g))s+=r2.cmd("pdc@"+u);r2.cmd("s "+n)}let a=t?" "+e:"",c=r2.cmd("e scr.color");try{let n=e.slice(2).trim(),l=0,u="";r.contextFile!==""&&r2.cmd2("test -f "+r.contextFile).value===0&&(u+=`## Context:
`};var v={decopipe:{use:!1},host:"http://localhost",port:"11434",baseurl:"",extraHeaders:[],api:"ollama",pipeline:"",commands:"pdc",yolo:!1,tts:!1,language:"C",humanLanguage:"English",deterministic:!0,debug:!1,timeout:180,think:"",apitype:"chat",useFiles:!1,contextFile:"",model:"",cache:!1,maxInputTokens:-1,prompt:N,lastOutput:""},o={...v,decopipe:{...v.decopipe},extraHeaders:[...v.extraHeaders]};function te(e){return(r2.cmd("-e dir.tmp").trim()??".")+"/"+e}function T(e,t){r2.cmd("'mkdir -p "+e),r2.cmd("'touch "+t)}function _(e){return(r2.cmd2("test -h").logs?.[0]?.message??"").includes("-fdx")?!0:r2.cmd("'test -vf "+e).startsWith("found")}function L(e,t){return e+" ".repeat(Math.max(0,t-e.length))}function ne(e){return e.replace(/\x1b\[[0-9;]*m/g,"")}function oe(e){let t=e.match(/```json\s*([\s\S]*?)```/);return t&&t[1]?t[1].trim():e}function re(e){let t=e,n=t.indexOf("{");n!==-1&&(t=t.slice(n));let r=t.lastIndexOf("}");return r!==-1&&(t=t.slice(0,r+1)),t}function G(e){return btoa(e)}function ie(e,t){let n=G(t);r2.cmd("p6ds "+n+" > "+e)}function b(e){let t=e;return t=t.replace(/<think>[\s\S]*?<\/think>/gi,""),t.split(`
`).filter(n=>!n.trim().startsWith("```")).join(`
`)}function $(e){o.debug&&console.log(e)}function ae(e){let t={};for(let n of e.split(/\r?\n/)){let r=n.trim();if(!r||r.startsWith("#"))continue;let[i,...a]=r.split("=");if(!i||a.length===0)continue;let s=a.join("=").trim(),l=i.toLowerCase().replace(/_api_key$/i,"");t[l]=s}return t}var Ie=["DECAI_HEADERS","R2AI_HEADERS"];function Re(e){let t=r2.cmd("'%"+e).trim();return t!==""&&!t.includes("=")?t:""}function _e(e){return e.trim().toLowerCase()}function Ue(e){let t=e.indexOf(":"),n=e.indexOf("="),r=t!==-1&&(n===-1||t<n)?t:n;if(r===-1)return null;let i=e.slice(0,r).trim();return i===""?null:{name:i,value:e.slice(r+1).trim()}}function Me(e){return e.value===""?`${e.name}:`:`${e.name}: ${e.value}`}function se(e,t){for(let n of e){let r=Ue(n.trim());r&&t.set(_e(r.name),Me(r))}}function J(e){let t=new Map,n=e.replace(/\\n/g,`
`).split(/\r?\n/g).filter(r=>{let i=r.trim();return i&&!i.startsWith("#")});return se(n,t),Array.from(t.values())}function le(e){return e.join("\\n")}function w(...e){let t=new Map;for(let n of e)se(n,t);return Array.from(t.values())}function Ne(){for(let e of Ie){let t=Re(e);if(t)return J(t)}return[]}function ce(){return w(Ne(),o.extraHeaders)}function U(e){return`'${e.replace(/'/g,`'"'"'`)}'`}function Te(e){let{method:t,url:n,headers:r,payload:i}=e,a=["curl","-s"];if(o.timeout>0&&a.push("--max-time",String(o.timeout)),w(["Content-Type: application/json"],r).forEach(l=>a.push("-H",U(l))),t==="POST"){if(!i)throw new Error("Payload required for POST requests");let l=r2.fdump(i);a.push("--data-binary","@"+U(l),U(n));let u=a.join(" ")+"; rm "+U(l);return $(u),r2.syscmds(u)}else{a.push(U(n));let l=a.join(" ");return $(l),r2.syscmds(l)}}function Ke(e){let t=e.split(`
`).filter(i=>i.trim()!=="");if(t.length<=1)return null;let n="",r=null;for(let i of t)try{let a=JSON.parse(i);if(a.error)return a;a.message?.content!==void 0&&(n+=a.message.content),r=a}catch{return null}return r?(r.message={role:"assistant",content:n},r):null}function ue(e){try{let t=Te(e).trim();if(t==="")return{error:"empty response"};try{return JSON.parse(t)}catch{let n=Ke(t);return n||{error:t,rawOutput:t}}}catch(t){return{error:t.message}}}function E(e,t){return ue({method:"GET",url:e,headers:t})}function de(e,t,n){return ue({method:"POST",url:e,headers:t,payload:n})}function W(e){return e.split("_")[0].toLowerCase()}function pe(e){if(!e.authKey)return;let t=Array.from(new Set([e.keyName,W(e.authKey)].filter(Boolean))),n;for(let r of t){let i=K(r,e.authKey);if(i[0])return i;!n&&i[2]!=="nope"&&(n=i)}return n||K(W(e.authKey),e.authKey)}function O(e){if(e)return typeof e=="string"?e:e.message||JSON.stringify(e)}function fe(e){return o.baseurl||e.defaultBaseUrl}function me(e,t=ce()){return w(e,t)}function qe(e){return e?["Authorization: Bearer "+e]:[]}function He(e){let t=["anthropic-version: 2023-06-01"];return e?w(t,["x-api-key: "+e]):t}function he(e,t){switch(e.authStyle||"none"){case"bearer":return qe(t);case"anthropic":return He(t);case"none":default:return[]}}function De(e,t,n){let r=E(e,t),i=O(r.error);return i?(console.error(i),"error invalid response"):r.data?.map(n).join(`
`)||""}function je(e,t){let n=new Set;return e.filter(r=>{let i=t(r);return n.has(i)?!1:(n.add(i),!0)})}function z(e){let t=pe(e)?.[0]||null;return{baseUrl:fe(e),headers:me(he(e,t))}}function Be(e){let{baseUrl:t,headers:n}=z(e);return De(t+"/v1/models",n,r=>r.id)}function Fe(e){let{baseUrl:t,headers:n}=z(e),r=E(t+"/api/tags",n),i=O(r.error);return i?(console.error(i),"error invalid response"):r.models?.map(a=>a.name).join(`
`)||""}function Le(e){let{baseUrl:t,headers:n}=z(e),r=E(t+"/v1/models",n);return r.data?je(r.data,i=>i.name||i.id).map(i=>[L(i.name||i.id,30),L(""+(i.max_context_length||""),10),i.description||""].join(" ")).join(`
`):""}var Ge={openai:{buildPayload:(e,t)=>({stream:!1,model:e,messages:[{role:"user",content:t}]}),parseResponse:e=>{if(e.error)throw new Error(O(e.error)||"Unknown error");if(e.choices&&e.choices[0]?.message?.content)return b(e.choices[0].message.content);throw new Error("Invalid response format")},buildUrl:e=>e+"/v1/chat/completions"},anthropic:{buildPayload:(e,t)=>{let n={model:e,max_tokens:5128,messages:[{role:"user",content:t}]};return Y()&&(n.thinking={type:"enabled",budget_tokens:4096},n.max_tokens=16e3),o.deterministic&&!Y()&&Object.assign(n,{temperature:0,top_p:0,top_k:1}),n},parseResponse:e=>{if(e.content&&Array.isArray(e.content)){let t=[];for(let n of e.content)n.text&&t.push(n.text);if(t.length>0)return b(t.join(`
`))}throw e.error?new Error(O(e.error)||"Unknown error"):new Error("Invalid response format")},buildUrl:e=>e+"/v1/messages"},ollama:{buildPayload:(e,t)=>{let n={stream:!1,model:e};return o.apitype==="generate"?n.prompt=t:n.messages=[{role:"user",content:t}],Y()?o.think==="true"||o.think==="1"?n.think=!0:n.think=o.think:n.think=!1,o.deterministic&&(n.options={repeat_last_n:0,top_p:1,top_k:1,temperature:0,repeat_penalty:1,seed:123}),n},parseResponse:e=>{if(e.error)throw new Error(O(e.error));if(typeof e.response=="string")return b(e.response);if(e.message?.content)return b(e.message.content);throw new Error(JSON.stringify(e))},buildUrl:e=>e+"/api/"+o.apitype},gemini:{buildPayload:(e,t)=>{let n={contents:[{parts:[{text:t}]}]},r={};return o.deterministic&&Object.assign(r,{temperature:0,topP:1,topK:1}),o.think!==""&&(Q()?r.thinkingConfig={thinkingBudget:0}:r.thinkingConfig={thinkingBudget:8192}),Object.keys(r).length>0&&(n.generationConfig=r),n},parseResponse:e=>{if(e.candidates&&e.candidates[0]?.content?.parts){let n=e.candidates[0].content.parts.filter(r=>!r.thought&&r.text).map(r=>r.text);if(n.length>0)return b(n.join(`
`))}throw e.error?new Error(O(e.error)):(console.log(JSON.stringify(e)),new Error("Invalid response format"))},buildUrl:(e,t,n)=>`${e}/v1beta/models/${t}:generateContent?key=${n}`,requiresUrlApiKey:!0}},q={anthropic:{defaultModel:"claude-3-7-sonnet-20250219",defaultBaseUrl:"https://api.anthropic.com",authKey:"ANTHROPIC_API_KEY",keyName:"anthropic",authStyle:"anthropic",apiStyle:"anthropic"},claude:{defaultModel:"claude-3-7-sonnet-20250219",defaultBaseUrl:"https://api.anthropic.com",authKey:"ANTHROPIC_API_KEY",keyName:"claude",authStyle:"anthropic",apiStyle:"anthropic"},openai:{defaultModel:"gpt-4o-mini",defaultBaseUrl:"https://api.openai.com",authKey:"OPENAI_API_KEY",authStyle:"bearer",apiStyle:"openai"},ollama:{defaultModel:"qwen2.5-coder:latest",defaultBaseUrl:"http://localhost:11434",authStyle:"none",apiStyle:"ollama"},ollamacloud:{defaultModel:"gpt-oss:120b",defaultBaseUrl:"https://ollama.com",authKey:"OLLAMA_API_KEY",keyName:"ollamacloud",authStyle:"bearer",apiStyle:"ollama"},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"}};function H(e){return q[e]}function V(){return Object.keys(q)}function ge(e){let t=H(e);if(!t)throw new Error(`Unknown provider: ${e}`);if(e==="mistral")return Le(t);switch(t.apiStyle){case"openai":case"anthropic":return Be(t);case"ollama":return Fe(t);case"gemini":return"";default:return""}}function Q(){return o.think==="false"||o.think==="0"}function Y(){return o.think!==""&&!Q()}function $e(e,t,n){let r=e;return o.think!==""&&n==="openai"&&(Q()?(r+=' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".',r+=" /no_think"):r="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."+r),t?r:o.prompt+Je()+r}function Je(){return`
.Translate the code into `+o.language+` programming language
`}function Ye(e,t,n,r){let i=o.model||e.defaultModel,a=$e(n,r,e.apiStyle),s=pe(e);if(t.requiresUrlApiKey&&s?.[1]&&e.authKey)return`Cannot read ~/.r2ai.${W(e.authKey)}-key`;let l=t.buildPayload(i,a),u=he(e,s?.[0]||null);e.apiStyle==="ollama"&&u.push("Accept: application/x-ndjson");let x=me(u),c=t.buildUrl(fe(e),i,s?.[0]||void 0);try{return t.parseResponse(de(c,x,JSON.stringify(l)))}catch(d){return"ERROR: "+d.message}}function ye(e,t){let n=H(o.api);return n?Ye(n,Ge[n.apiStyle],e,t):`Unknown value for 'decai -e api'. Available: ${V().join(", ")}`}function We(){return Object.fromEntries(Object.entries(q).filter(([,e])=>e.authKey).map(([e,t])=>[e,t.authKey]))}function K(e,t){let n=r2.cmd("'%"+t).trim();if(!n.includes("=")&&n!=="")return[n.trim(),null,"env"];let r=e.toLowerCase(),i=M;if(_(i)){let s=r2.cmd("'cat "+i),l=ae(s);if(r in l)return[l[r],null,"txt"]}let a="~/.r2ai."+r+"-key";if(_(a)){let s=r2.cmd("'cat "+a);return s===""?[null,"Cannot read "+a,"no"]:[s.trim(),null,"file"]}return[null,"Not available","nope"]}function be(){T(A,M),r2.cmd("'ed "+M)}function X(){Object.entries(We()).forEach(([e,t])=>{let n=K(e,t)[2];console.log(n," ",e)})}function ke(e){let t=H(e);if(!t){console.error(`Unknown provider: ${e}`);return}try{let n=ge(e);n&&console.log(n),t.hardcodedModels&&t.hardcodedModels.forEach(r=>console.log(r)),e==="mistral"&&console.log("ministral-8b-latest"),!n&&!t.hardcodedModels&&console.log(t.defaultModel)}catch(n){let r=n;console.error(`Error listing models for ${e}:`,r.message),console.log(t.defaultModel)}}var Z=!1;function ze(e){return e==="true"||e==="1"}function S(e){let t=o;return{get:()=>t[e],set:n=>{t[e]=ze(n)}}}function P(e){let t=o;return{get:()=>t[e],set:n=>{t[e]=n}}}function Ve(e){let t=e.trim();return t==="chat"?"chat":t==="generate"?"generate":null}var xe={pipeline:{get:()=>o.pipeline,set:e=>{o.pipeline=e;try{o.decopipe=JSON.parse(r2.cmd("cat "+e))}catch(t){console.error(t)}}},model:{get:()=>o.model,set:e=>{e==="?"?ke(o.api):o.model=e.trim()}},deterministic:S("deterministic"),files:S("useFiles"),think:{get:()=>o.think||"false",set:e=>{o.think=e}},apitype:{get:()=>o.apitype,set:e=>{let t=Ve(e);t?o.apitype=t:console.error("Invalid apitype. Use chat or generate.")}},debug:S("debug"),timeout:{get:()=>o.timeout,set:e=>{o.timeout=Math.max(0,parseInt(e,10)||0)}},api:{get:()=>o.api,set:e=>{e==="?"?console.error(V().join(`
`)):o.api=e}},lang:P("language"),hlang:P("humanLanguage"),cache:S("cache"),cmds:P("commands"),tts:S("tts"),yolo:S("yolo"),prompt:P("prompt"),ctxfile:P("contextFile"),baseurl:P("baseurl"),headers:{get:()=>le(o.extraHeaders),set:e=>{o.extraHeaders=J(e)}},maxtokens:{get:()=>o.maxInputTokens,set:e=>{o.maxInputTokens=parseInt(e,10)||-1}}};function Qe(){let e={host:o.host,port:o.port,lastOutput:o.lastOutput};Object.assign(o,v,e),o.decopipe={...v.decopipe},o.extraHeaders=[...v.extraHeaders]}function Xe(e){let t=e.trim();return!t||t.startsWith("#")?null:t.startsWith("decai -e ")?t.slice(9).trim():t}function k(e){let t=e.indexOf("="),n=t===-1?e:e.slice(0,t),r=t===-1?void 0:e.slice(t+1),i=xe[n];if(!i){console.error("Unknown config key");return}typeof r<"u"?i.set(r):console.log(i.get())}function ve(){Object.entries(xe).forEach(([e,t])=>{let n=t.get();console.log("decai -e "+e+"="+n)})}function Ae(){if(Qe(),!_(R))return;let e=r2.call("cat "+R);for(let t of e.split(/\r?\n/)){let n=Xe(t);n&&n[0]!="#"&&k(n)}}function we(){Z||(Ae(),Z=!0)}function Ee(){T(A,R),r2.cmd("'ed "+R),Ae(),Z=!0}function p(e,t,n=!1){let r=(t||"").replace(/`/g,""),i=e.replace(/'/g,"");if(o.api==="r2"||o.api==="r2ai"){let s=te(".pdc.txt");ie(s,r);let l=i.startsWith("-")?i:["-i",s,i].join(" "),x=(o.baseurl?o.baseurl+"/cmd":o.host+":"+o.port+"/cmd")+"/"+encodeURIComponent(l),c=E(x,[]);return c.error?`Error: ${c.error}`:c.result||JSON.stringify(c)||"Cannot curl, use r2ai-server or r2ai -w"}if(i.startsWith("-"))return"";let a=i+`:
`+r;return o.maxInputTokens>0&&a.length>o.maxInputTokens&&(a=a.slice(0,o.maxInputTokens)),ye(a,n)}function Se(e){let t=r2.cmd("e scr.color");r2.cmd("e scr.color=0");try{return e()}finally{r2.cmd("e scr.color="+t)}}function Oe(){let e="",t=n=>e+=" "+y+" "+n+`
`;e+="Usage: "+y+` (-h) ...
`,e+="Version: "+ee+`
`,t("-a [query] - solve query with auto mode"),t("-b [url] - set base URL (alias for decai -e baseurl)"),t("-d [f1 ..] - decompile given functions"),t("-dd [..] - same as above, but ignoring cache"),t("-dD [query] - decompile current function with given extra query"),t("-dr - decompile function and its called ones (recursive)"),t("-e - display and change eval config vars"),t("-E - edit and run decai.txt"),t("-h - show this help"),t("-H - help setting up r2ai"),t("-i [f] [q] - include given file and query"),t("-k - list API key status"),t("-K - edit apikeys.txt"),t("-m [model] - use -m? or -e model=? to list the available models"),t("-n - suggest better function name"),t("-p [provider] - same as decai -e api (will be provider)"),t("-q [text] - query language model with given text"),t("-Q [text] - query on top of the last output"),t("-r [prompt] - change role prompt (same as: decai -e prompt)"),t("-R - reset role prompt to default prompt"),t("-s - function signature"),t("-v - show local variables"),t("-V - find vulnerabilities"),t("-x[*] - eXplain current function (-x* for r2 script)"),r2.log(e.trim())}function D(e,t,n,r){if(n){let s=r2.cmd("anos").trim();if(s.length>0)return s}let i="";if(r){let s=r2.cmd("s");i+=`## Context functions:
`;let l=r2.cmdAt("axff~^C[2]~$$",s);for(let u of l.split(/\n/g))i+=r2.cmd("pdc@"+u);r2.cmd("s "+s)}let a=t?" "+e:"";try{let s=e.slice(2).trim(),l="";o.contextFile!==""&&r2.cmd2("test -f "+o.contextFile).value===0&&(l+=`## Context:
[START]
`+r2.cmd("cat "+r.contextFile)+`
`+r2.cmd("cat "+o.contextFile)+`
[END]
`),r2.cmd("e scr.color=0");let p=`## Before:
`;for(let m of r.commands.split(",")){if(m.trim()==="")continue;let w=t||n.trim().length===0?m:m+"@@= "+n,h=r2.cmd(w);h.length>5&&(p+="Output of "+m+`:
`);let{body:u,count:x}=Se(()=>{let d=`## Before:
`,g=0;for(let h of o.commands.split(",")){if(h.trim()==="")continue;let C=t||s.trim().length===0?h:h+"@@= "+s,I=r2.cmd(C);I.length>5&&(d+="Output of "+h+`:
[START]
`+h+`
`+I+`
[END]
`,l++)}if(p+=`## After:
`,r2.cmd("e scr.color="+c),l===0){console.error("Nothing to do.");return}let d="";if(r.decopipe.use){let m=r.decopipe[r.decopipe.default],w=r.model,h=u+p;for(let O of m.pipeline){O.model&&(r.model=O.model);let D=O.query+". "+m.globalQuery;d=f(D,h,!0),r.debug&&console.log(`QUERY
`,D,`
`,g++)}return d+=`## After:
`,{body:d,count:g}});if(x===0){console.error("Nothing to do.");return}let c="";if(o.decopipe.use){let d=o.model;try{let g=o.decopipe[o.decopipe.default],h=l+u;for(let C of g.pipeline){C.model&&(o.model=C.model);let I=C.query+". "+g.globalQuery;c=p(I,h,!0),o.debug&&console.log(`QUERY
`,I,`
INPUT
`,h,`
OUTPUT
`,d),h=d}d=h,r.model=w}else{let m=a;u+=p+s,d=f(m,u,!1),r.lastOutput=d}return o&&d.length>1&&r2.call("ano=base64:"+q(d)),d.startsWith("```")&&(d=d.replace(/```.*\n/,"").replace(/```$/,"")),d.trim()}catch(n){r2.cmd("e scr.color="+c);let l=n;console.error(l,l.stack);return}}function ge(){let e=r2.cmd("e scr.color");r2.cmd("e scr.color=0");let t="[START]"+r.commands.split(",").map(s=>r2.cmd(s)).join(`
`)+"[END]";r2.cmd("e scr.color="+e);let i=f("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 "+r.humanLanguage,t,!0).trim().split(/\n/g);return i[i.length-1].trim()}function ye(){let e=r.language,t=r2.cmd("afv;pdc");r.language="C";let o="'afs "+f("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",t,!1),i=o.indexOf("{");return i!==-1&&(o=o.substring(0,i)),r.language=e,o}function be(e,t){let o=[];for(;;){let i=_.auto;if(o.length>0){i+=`## Command Results
`,c),h=c}c=h}finally{o.model=d}}else{let d=a;l+=u+i,c=p(d,l,!1),o.lastOutput=c}return n&&c.length>1&&r2.call("ano=base64:"+G(c)),c.startsWith("```")&&(c=c.replace(/```.*\n/,"").replace(/```$/,"")),c.trim()}catch(s){let l=s;console.error(l,l.stack);return}}function Ze(){let e=Se(()=>"[START]"+o.commands.split(",").map(r=>r2.cmd(r)).join(`
`)+"[END]"),n=p("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 "+o.humanLanguage,e,!0).trim().split(/\n/g);return n[n.length-1].trim()}function et(){let e=o.language,t=r2.cmd("afv;pdc");o.language="C";let n="'afs "+p("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",t,!1),r=n.indexOf("{");return r!==-1&&(n=n.substring(0,r)),o.language=e,n}function tt(e,t){let n=[];for(;;){let r=F.auto;if(n.length>0){r+=`## Command Results
`;for(let a of o){let c=JSON.parse(a);i+="### "+c.command+"\n\n```\n"+c.response+"\n```\n"}}i+=`
`;for(let a of n){let s=JSON.parse(a);r+="### "+s.command+"\n\n```\n"+s.response+"\n```\n"}}r+=`
## User Prompt
`+e,r.debug&&console.log(`#### input
`+e,o.debug&&console.log(`#### input
`,r,`
#### /input`),console.log("Thinking...");let i=p("",r,!0);o.debug&&console.log(`#### output
`,i,`
#### /input`),console.log("Thinking...");let s=f("",i,!0);r.debug&&console.log(`#### output
`,s,`
#### /output`);try{let a=JSON.parse(G(J(k(s))));if(a.action==="r2cmd"||a.action==="response"||a.action===a.command){let c=a.command||"";a.reason&&(console.log("[r2cmd] Reasoning: "+a.reason),r.tts&&(r2.syscmd("pkill say"),r2.syscmd("say -v Alex -r 250 '"+a.reason.replace(/'/g,"")+"' &"))),console.log("[r2cmd] Action: "+a.description),console.log("[r2cmd] Command: "+c);let n=c;r.yolo||(n=ke(c,t)),console.log("[r2cmd] Running: "+n);let l=r2.cmd2(n),u=l.logs?l.logs.map(m=>m.type+": "+m.message).join(`
`):"",p=(l.res+u).trim();console.log(p);let d=Y(p);r.debug&&console.log(`<r2output>
#### /output`);try{let a=JSON.parse(re(oe(b(i))));if(a.action==="r2cmd"||a.action==="response"||a.action===a.command){let s=a.command||"";a.reason&&(console.log("[r2cmd] Reasoning: "+a.reason),o.tts&&(r2.syscmd("pkill say"),r2.syscmd("say -v Alex -r 250 '"+a.reason.replace(/'/g,"")+"' &"))),console.log("[r2cmd] Action: "+a.description),console.log("[r2cmd] Command: "+s);let l=s;o.yolo||(l=nt(s,t)),console.log("[r2cmd] Running: "+l);let u=r2.cmd2(l),x=u.logs?u.logs.map(g=>g.type+": "+g.message).join(`
`):"",c=(u.res+x).trim();console.log(c);let d=ne(c);o.debug&&console.log(`<r2output>
`,d,`
<(r2output>`),o.push(JSON.stringify({action:"response",command:n,description:a.description,response:d}))}else if(a.action==="reply"){console.log(`Done
`,a.response);break}else console.log(`Unknown response
`,JSON.stringify(s))}catch(a){let c=s.indexOf('response": "');if(c!==-1){let n=s.slice(c+12).replace(/\\n/g,`
`).replace(/\\/g,"");console.log(n)}else console.log(s),console.error(a);break}}}function ke(e,t){for(;;){let o=r2.cmd("'?ie Tweak command? ('?' for help)").trim();if(o==="q!"){console.error("Break!");break}if(o==="?"){xe();continue}else if(o.startsWith(":")){console.log(r2.cmd(o.slice(1)));continue}else if(o.startsWith("-e")){t(o);continue}else{if(o==="!")return"?e do NOT execute '"+e+"' again, continue without it";if(o.startsWith("!")){console.log(r2.syscmd(o.slice(1)));continue}else{if(o==="q")return"?e All data collected!. Do not call more commands, reply the solutions";if(o){let i=o.indexOf("#");return i!==-1?o.slice(0,i).trim():o}else return e}}}return e}function xe(){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 re(e,t){if(e===""||!e.startsWith("-")){oe();return}let o="";switch(e[1]){case"H":console.log(_.decai);break;case"a":be(e.slice(2).trim(),t);break;case"m":{let s=e.slice(2).trim();s==="="?x("model="):s?x("model="+s):x("model");break}case"n":case"f":{o=r2.cmd("axff~$[3]");let s=r2.cmd("fd.").trim().split(/\n/).filter(a=>!a.startsWith("secti")).join(",");o=f("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: "+s,o,!1).trim(),o+=" @ "+r2.cmd("?v $FB").trim();break}case"v":o=r2.cmd("afv;pdc"),o=f("guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",o,!1);break;case"i":{let s=e.slice(2).trim().split(/ /g);if(s.length>=2){let a=s[0],c=s.slice(1).join(" "),n=r2.cmd("cat "+a);console.log(f(c,n,!0))}else console.log("Use: decai -i [file] [query ...]");break}case"p":{let s=e.slice(2).trim();s?x("api="+s):B();break}case"r":{let s=e.slice(2).trim();s?r.prompt=s:console.log(r.prompt);break}case"R":r.prompt=I;break;case"s":o=ye();break;case"V":o=f("find vulnerabilities, dont show the code, only show the response, provide a sample exploit",r.lastOutput,!1);break;case"K":W();break;case"k":B();break;case"b":{let s=e.slice(2).trim();s?x("baseurl="+s):console.log(r.baseurl);break}case"e":{let s=e.slice(2).trim();s?x(s):te();break}case"q":try{o=f(e.slice(2).trim(),null,!0)}catch(s){let a=s;console.error(a,a.stack)}break;case"Q":o=f(e.slice(2).trim(),r.lastOutput,!1);break;case"x":o=ge(),(e[2]==="*"||e[2]==="r")&&(o="'CC "+o);break;case"d":e[2]==="r"?o=C(e.slice(2),!0,r.cache,!0)||"":e[2]==="d"?o=C(e,!1,!1,!1)||"":e[2]==="D"?o=C(e,!0,!1,!1)||"":o=C(e,!1,r.cache,!1)||"";break;default:oe();break}return o||void 0}function ne(e){let t=re(e,ne);return t&&r2.log(t),!0}function Ae(){r2.unload("core",b),r2.plugin("core",function(){function e(t){if(t.startsWith(b)){let o=t.slice(b.length).trim();return ne(o)}return!1}return{name:b,license:"MIT",desc:"r2 decompiler based on r2ai",call:e}})}Ae();
<(r2output>`),n.push(JSON.stringify({action:"response",command:l,description:a.description,response:d}))}else if(a.action==="reply"){console.log(`Done
`,a.response);break}else{console.log(`Unknown response
`,JSON.stringify(i));break}}catch(a){let s=i.indexOf('response": "');if(s!==-1){let l=i.slice(s+12).replace(/\\n/g,`
`).replace(/\\/g,"");console.log(l)}else console.log(i),console.error(a);break}}}function nt(e,t){for(;;){let n=r2.cmd("'?ie Tweak command? ('?' for help)").trim();if(n==="q!"){console.error("Break!");break}if(n==="?"){ot();continue}else if(n.startsWith(":")){console.log(r2.cmd(n.slice(1)));continue}else if(n.startsWith("-e")){t(n);continue}else{if(n==="!")return"?e do NOT execute '"+e+"' again, continue without it";if(n.startsWith("!")){console.log(r2.syscmd(n.slice(1)));continue}else{if(n==="q")return"?e All data collected!. Do not call more commands, reply the solutions";if(n){let r=n.indexOf("#");return r!==-1?n.slice(0,r).trim():n}else return e}}}return e}function ot(){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 rt(e){return e.slice(2).trim()}function j(){return r2.cmd("afi.").trim().length>0}var B="Cannot find function at current offset";function Pe(e,t){if(e===""||!e.startsWith("-")){Oe();return}let n="",r=e[1],i=rt(e);switch(r){case"H":console.log(F.decai);break;case"a":tt(i,t);break;case"m":{i==="="?k("model="):i?k("model="+i):k("model");break}case"n":case"f":{if(!j()){n=B;break}n=r2.cmd("axff~$[3]");let a=r2.cmd("fd.").trim().split(/\n/).filter(s=>!s.startsWith("secti")).join(",");n=p("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: "+a,n,!1).trim(),n+=" @ "+r2.cmd("?v $FB").trim();break}case"v":{if(!j()){n=B;break}n=r2.cmd("afv;pdc"),n=p("guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",n,!1);break}case"i":{let a=i.split(/\s+/).filter(Boolean);if(a.length>=2){let s=a[0],l=a.slice(1).join(" "),u=r2.cmd("cat "+s);console.log(p(l,u,!0))}else console.log("Use: decai -i [file] [query ...]");break}case"p":{i?k("api="+i):X();break}case"r":{i?o.prompt=i:console.log(o.prompt);break}case"R":o.prompt=N;break;case"s":if(!j()){n=B;break}n=et();break;case"V":n=p("find vulnerabilities, dont show the code, only show the response, provide a sample exploit",o.lastOutput,!1);break;case"K":be();break;case"E":Ee();break;case"k":X();break;case"b":{i?k("baseurl="+i):console.log(o.baseurl);break}case"e":{i?k(i):ve();break}case"q":try{n=p(i,null,!0)}catch(a){let s=a;console.error(s,s.stack)}break;case"Q":n=p(i,o.lastOutput,!1);break;case"x":if(!j()){n=B;break}n=Ze(),(e[2]==="*"||e[2]==="r")&&(n="'CC "+n);break;case"d":e[2]==="r"?n=D(e.slice(2),!0,o.cache,!0)||"":e[2]==="d"?n=D(e,!1,!1,!1)||"":e[2]==="D"?n=D(e,!0,!1,!1)||"":n=D(e,!1,o.cache,!1)||"";break;default:Oe();break}return n||void 0}function Ce(e){we();let t=Pe(e,Ce);return t&&r2.log(t),!0}function it(){r2.unload("core",y),r2.plugin("core",function(){function e(t){if(t.startsWith(y)){let n=t.slice(y.length).trim();return Ce(n)}return!1}return{name:y,license:"MIT",desc:"r2 decompiler based on r2ai",call:e}})}it();
+16 -20
View File
@@ -1,31 +1,29 @@
import { API_KEYS_PATH, DECAI_CONFIG_DIR } from "./constants";
import { providerRegistry } from "./providers";
import { ApiKeyResult } from "./types";
import { fileExists, parseEnvLikeString } from "./utils";
import { ensurePath, fileExists, parseEnvLikeString } from "./utils";
const PROVIDER_ENV_MAP: Record<string, string> = {
mistral: "MISTRAL_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
huggingface: "HUGGINGFACE_API_KEY",
openai: "OPENAI_API_KEY",
gemini: "GEMINI_API_KEY",
deepseek: "DEEPSEEK_API_KEY",
xai: "XAI_API_KEY",
ollama: "OLLAMA_API_KEY",
ollamacloud: "OLLAMA_API_KEY",
};
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.indexOf("=") === -1 && keyEnv !== "") {
if (!keyEnv.includes("=") && keyEnv !== "") {
return [keyEnv.trim(), null, "env"];
}
const providerLower = provider.toLowerCase();
const keysPath = "~/.config/r2ai/apikeys.txt";
const keysPath = API_KEYS_PATH;
if (fileExists(keysPath)) {
const keyFile = r2.cmd("'cat " + keysPath);
const kv = parseEnvLikeString(keyFile);
if (Object.keys(kv).indexOf(providerLower) !== -1) {
if (providerLower in kv) {
return [kv[providerLower], null, "txt"];
}
}
@@ -42,16 +40,14 @@ export function getApiKey(provider: string, envvar: string): ApiKeyResult {
}
export function editApiKeys(): void {
r2.cmd("'ed ~/.config/r2ai/apikeys.txt");
ensurePath(DECAI_CONFIG_DIR, API_KEYS_PATH);
r2.cmd("'ed " + API_KEYS_PATH);
}
export function listApiKeys(): void {
Object.entries(PROVIDER_ENV_MAP).forEach(([key, env]) => {
Object.entries(getProviderEnvMap()).forEach(([key, env]) => {
const status = getApiKey(key, env)[2];
console.log(status, "\t", key);
});
}
export function getProviderEnvVar(provider: string): string {
return PROVIDER_ENV_MAP[provider.toLowerCase()] || "";
}
+98 -63
View File
@@ -3,16 +3,25 @@ import { state } from "./state";
import { COMMAND, DEFAULT_PROMPT, HELP_TEXT, VERSION } from "./constants";
import {
b64,
debugLog,
filterResponse,
trimAnsi,
trimDown,
trimJson,
} from "./utils";
import { editApiKeys, listApiKeys } from "./apiKeys";
import { evalConfig, listAllConfig } from "./config";
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");
@@ -26,6 +35,7 @@ export function showHelp(): void {
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");
@@ -71,11 +81,9 @@ export function decompile(
}
const appendQuery = extraQuery ? " " + args : "";
const origColor = r2.cmd("e scr.color");
try {
const parsedArgs = args.slice(2).trim();
let count = 0;
let text = "";
if (
@@ -87,25 +95,23 @@ export function decompile(
"\n[END]\n";
}
r2.cmd("e scr.color=0");
let body = "## Before:\n";
for (const c of state.commands.split(",")) {
if (c.trim() === "") continue;
const oneliner = extraQuery || parsedArgs.trim().length === 0
? c
: c + "@@= " + parsedArgs;
const output = r2.cmd(oneliner);
if (output.length > 5) {
body += "Output of " + c + ":\n[START]\n" + output + "\n[END]\n";
count++;
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";
r2.cmd("e scr.color=" + origColor);
body += "## After:\n";
return { body, count };
});
if (count === 0) {
console.error("Nothing to do.");
@@ -114,24 +120,27 @@ export function decompile(
let out = "";
if (state.decopipe.use) {
const dpipe = state.decopipe[state.decopipe.default as string] as {
pipeline: Array<{ model?: string; query: string }>;
globalQuery: string;
};
const origModel = state.model;
let code = text + body;
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);
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;
}
code = out;
out = code;
} finally {
state.model = origModel;
}
out = code;
state.model = origModel;
} else {
const query = appendQuery;
text += body + context;
@@ -149,7 +158,6 @@ export function decompile(
return out.trim();
} catch (e) {
r2.cmd("e scr.color=" + origColor);
const err = e as Error;
console.error(err, err.stack);
return;
@@ -157,11 +165,10 @@ export function decompile(
}
export function explainFunction(): string {
const origColor = r2.cmd("e scr.color");
r2.cmd("e scr.color=0");
const hints = "[START]" +
state.commands.split(",").map((c) => r2.cmd(c)).join("\n") + "[END]";
r2.cmd("e scr.color=" + origColor);
const 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 " +
@@ -284,6 +291,7 @@ export function autoMode(
break;
} else {
console.log("Unknown response\n", JSON.stringify(out));
break;
}
} catch (e) {
const response = out.indexOf('response": "');
@@ -353,6 +361,16 @@ function autoHelp(): void {
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,
@@ -364,6 +382,7 @@ export function handleCommand(
let output = "";
const flag = args[1];
const argValue = getFlagValue(args);
switch (flag) {
case "H":
@@ -371,15 +390,14 @@ export function handleCommand(
break;
case "a":
autoMode(args.slice(2).trim(), mainHandler);
autoMode(argValue, mainHandler);
break;
case "m": {
const arg0 = args.slice(2).trim();
if (arg0 === "=") {
if (argValue === "=") {
evalConfig("model=");
} else if (arg0) {
evalConfig("model=" + arg0);
} else if (argValue) {
evalConfig("model=" + argValue);
} else {
evalConfig("model");
}
@@ -388,6 +406,10 @@ export function handleCommand(
case "n":
case "f": {
if (!currentFunctionExists()) {
output = noFunctionMessage;
break;
}
output = r2.cmd("axff~$[3]");
const considerations = r2
.cmd("fd.")
@@ -406,7 +428,11 @@ export function handleCommand(
break;
}
case "v":
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",
@@ -414,9 +440,10 @@ export function handleCommand(
false,
);
break;
}
case "i": {
const parts = args.slice(2).trim().split(/ /g);
const parts = argValue.split(/\s+/).filter(Boolean);
if (parts.length >= 2) {
const file = parts[0];
const query = parts.slice(1).join(" ");
@@ -429,9 +456,8 @@ export function handleCommand(
}
case "p": {
const evalArg = args.slice(2).trim();
if (evalArg) {
evalConfig("api=" + evalArg);
if (argValue) {
evalConfig("api=" + argValue);
} else {
listApiKeys();
}
@@ -439,9 +465,8 @@ export function handleCommand(
}
case "r": {
const prompt = args.slice(2).trim();
if (prompt) {
state.prompt = prompt;
if (argValue) {
state.prompt = argValue;
} else {
console.log(state.prompt);
}
@@ -453,6 +478,10 @@ export function handleCommand(
break;
case "s":
if (!currentFunctionExists()) {
output = noFunctionMessage;
break;
}
output = suggestSignature();
break;
@@ -468,14 +497,17 @@ export function handleCommand(
editApiKeys();
break;
case "E":
editRcConfig();
break;
case "k":
listApiKeys();
break;
case "b": {
const baseUrlArg = args.slice(2).trim();
if (baseUrlArg) {
evalConfig("baseurl=" + baseUrlArg);
if (argValue) {
evalConfig("baseurl=" + argValue);
} else {
console.log(state.baseurl);
}
@@ -483,9 +515,8 @@ export function handleCommand(
}
case "e": {
const evalArg = args.slice(2).trim();
if (evalArg) {
evalConfig(evalArg);
if (argValue) {
evalConfig(argValue);
} else {
listAllConfig();
}
@@ -494,7 +525,7 @@ export function handleCommand(
case "q":
try {
output = r2ai(args.slice(2).trim(), null, true);
output = r2ai(argValue, null, true);
} catch (e) {
const err = e as Error;
console.error(err, err.stack);
@@ -502,10 +533,14 @@ export function handleCommand(
break;
case "Q":
output = r2ai(args.slice(2).trim(), state.lastOutput, false);
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;
+125 -84
View File
@@ -1,7 +1,47 @@
import { ConfigHandlers } from "./types";
import { state } from "./state";
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: {
@@ -25,123 +65,124 @@ export const configHandlers: ConfigHandlers = {
}
},
},
deterministic: {
get: () => state.deterministic,
set: (v: string) => {
state.deterministic = v === "true" || v === "1";
},
},
files: {
get: () => state.useFiles,
set: (v: string) => {
state.useFiles = v === "true";
},
},
deterministic: boolHandler("deterministic"),
files: boolHandler("useFiles"),
think: {
get: () => state.think,
get: () => state.think || "false",
set: (v: string) => { state.think = v; },
},
apitype: {
get: () => state.apitype,
set: (v: string) => {
state.think = v === "true" ? 1 : v === "false" ? 0 : +v;
const apiType = normalizeApiType(v);
if (apiType) {
state.apitype = apiType;
} else {
console.error("Invalid apitype. Use chat or generate.");
}
},
},
debug: {
get: () => state.debug,
set: (v: string) => {
state.debug = v === "true" || v === "1";
},
debug: boolHandler("debug"),
timeout: {
get: () => state.timeout,
set: (v: string) => { state.timeout = Math.max(0, parseInt(v, 10) || 0); },
},
api: {
get: () => state.api,
set: (v: string) => {
if (v === "?") {
const providersList = listProviders().join("\n");
console.error(providersList);
console.error(listProviders().join("\n"));
} else {
state.api = v;
}
},
},
lang: {
get: () => state.language,
set: (v: string) => {
state.language = v;
},
},
hlang: {
get: () => state.humanLanguage,
set: (v: string) => {
state.humanLanguage = v;
},
},
cache: {
get: () => state.cache,
set: (v: string) => {
state.cache = v === "true" || v === "1";
},
},
cmds: {
get: () => state.commands,
set: (v: string) => {
state.commands = v;
},
},
tts: {
get: () => state.tts,
set: (v: string) => {
state.tts = v === "true" || v === "1";
},
},
yolo: {
get: () => state.yolo,
set: (v: string) => {
state.yolo = v === "true" || v === "1";
},
},
prompt: {
get: () => state.prompt,
set: (v: string) => {
state.prompt = v;
},
},
ctxfile: {
get: () => state.contextFile,
set: (v: string) => {
state.contextFile = v;
},
},
baseurl: {
get: () => state.baseurl,
set: (v: string) => {
state.baseurl = v;
},
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;
},
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 (!configHandlers[k]) {
if (!handler) {
console.error("Unknown config key");
return;
}
if (typeof v !== "undefined") {
configHandlers[k].set(v);
handler.set(v);
} else {
console.log(configHandlers[k].get());
console.log(handler.get());
}
}
export function listAllConfig(): void {
Object.keys(configHandlers).forEach((key) => {
const value = configHandlers[key].get();
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;
}
+12 -3
View File
@@ -1,8 +1,11 @@
export const VERSION = "1.3.2";
export const VERSION = "1.4.2";
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 =
"Transform this pseudocode and respond ONLY with plain code (NO explanations, comments or 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:', Reduce lines of code and fit everything in a single function, Remove all dead code";
"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
@@ -12,7 +15,7 @@ Decai is the radare2 plugin for decompiling functions with the help of language
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 gemini hf mistral ollama openapi openai vllm xai
r2ai deepseek anthropic claude gemini groq hf lmstudio mistral ollama ollamacloud openapi openai openrouter vllm xai zen
## Using Ollama
@@ -27,6 +30,10 @@ These are the most recommended models for decompiling in local:
## 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 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
@@ -39,11 +46,13 @@ These are the most recommended models for decompiling in local:
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:
+91
View File
@@ -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);
}
+45 -19
View File
@@ -1,5 +1,6 @@
import { HttpResponse } from "./types";
import { state } from "./state";
import { mergeHeaders } from "./headers";
import { debugLog } from "./utils";
interface HttpRequestOptions {
@@ -15,45 +16,70 @@ function shellEscape(arg: string): string {
function executeCurl(options: HttpRequestOptions): string {
const { method, url, headers, payload } = options;
const cmdParts = ["curl", "-s", shellEscape(url)];
headers.forEach((h) => cmdParts.push("-H", shellEscape(h)));
cmdParts.push("-H", shellEscape("Content-Type: application/json"));
const cmdParts = ["curl", "-s"];
if (state.timeout > 0) {
cmdParts.push("--max-time", String(state.timeout));
}
const requestHeaders = mergeHeaders(
["Content-Type: application/json"],
headers,
);
requestHeaders.forEach((h) => cmdParts.push("-H", shellEscape(h)));
if (method === "POST") {
if (!payload) throw new Error("Payload required for POST requests");
const tmpfile = r2.fdump(payload);
cmdParts.push("-d", shellEscape(`@${tmpfile}`));
const cmd = cmdParts.join(" ") + " && rm " + shellEscape(tmpfile);
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 parseJson(output: string): any {
if (output === "") {
throw new Error("empty response");
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;
}
}
try {
return JSON.parse(output);
} catch (e) {
const err = e as Error;
console.error("output:", output);
console.error(err, err.stack);
throw new Error(err.message || "JSON parse error");
if (lastObj) {
lastObj.message = { role: "assistant", content };
return lastObj as HttpResponse;
}
return null;
}
export function httpRequest(options: HttpRequestOptions): HttpResponse {
try {
const output = executeCurl(options);
const output = executeCurl(options).trim();
if (output === "") {
return { error: "empty response" };
}
try {
return parseJson(output);
} catch (e) {
return { error: (e as Error).message };
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;
+10
View File
@@ -0,0 +1,10 @@
### 3. HTTP Request Patterns
**Location**: `http.ts`, used throughout providers and models
**Problems**:
- Two different curl methods (`curlArgs` vs `curlFile`) with similar but
separate logic
- Error handling in HTTP layer vs. application layer
- No centralized request/response abstraction
+2 -2
View File
@@ -1,9 +1,9 @@
import type { R2PipeSync } from "./r2pipe";
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);
+2 -5
View File
@@ -1,4 +1,4 @@
import { getProvider } from "./providers";
import { getProvider, listModels } from "./providers";
export function listModelsFor(api: string): void {
const providerConfig = getProvider(api);
@@ -9,10 +9,7 @@ export function listModelsFor(api: string): void {
}
try {
let models = "";
if (providerConfig.listModelsCallback) {
models = providerConfig.listModelsCallback(providerConfig);
}
const models = listModels(api);
if (models) {
console.log(models);
+405 -315
View File
@@ -1,77 +1,165 @@
import {
AnthropicPayload,
ApiKeyResult,
HeadersBuilder,
PayloadBuilder,
ApiStyle,
AuthStyle,
GeminiPayload,
HttpResponse,
ModelDataEntry,
OllamaModelEntry,
OllamaPayload,
ProviderConfig,
ProviderRegistry,
ResponseParser,
UrlBuilder,
} from "./types";
import { state } from "./state";
import { filterResponse, padRight } from "./utils";
import { getApiKey } from "./apiKeys";
import { getConfiguredHeaders, mergeHeaders } from "./headers";
import { httpGet, httpPost } from "./http";
function listModels(
url: string,
headers: string[],
mapper: (model: any) => string,
): string {
const response = httpGet(url, headers);
if (response.error) {
console.error(response.error);
return "error invalid response";
}
return (response as any).data?.map(mapper).join("\n") || "";
interface ProviderRuntime {
buildPayload: (model: string, query: string) => object;
parseResponse: (response: HttpResponse) => string;
buildUrl: (baseUrl: string, model: string, apiKey?: string) => string;
requiresUrlApiKey?: boolean;
}
const listOllamaModels = (provider: ProviderConfig): string => {
const base = state.baseurl || state.host + ":" + state.port;
return listModels(
`${base}/api/tags`,
[],
(model: { name: string }) => model.name,
);
};
function getApiKeyName(authKey: string): string {
return authKey.split("_")[0].toLowerCase();
}
const listOpenAIModels = (provider: ProviderConfig): string => {
let key: ApiKeyResult | undefined;
if (provider.requiresAuth && provider.authKey) {
key = getApiKey(
provider.authKey.split("_")[0].toLowerCase(),
provider.authKey,
);
if (key && key[1]) throw new Error(key[1]);
function readProviderKey(provider: ProviderConfig): ApiKeyResult | undefined {
if (!provider.authKey) {
return undefined;
}
const base = state.baseurl || provider.defaultBaseurl;
const url = base + "/v1/models";
const headers = key ? ["Authorization: Bearer " + key[0]] : [];
return listModels(url, headers, (model: any) => model.id);
};
const keyNames = Array.from(
new Set(
[provider.keyName, getApiKeyName(provider.authKey)].filter(Boolean),
),
) as string[];
const listAnthropicModels = (provider: ProviderConfig): string => {
const key = getApiKey("anthropic", "ANTHROPIC_API_KEY");
if (key && key[1]) throw new Error(key[1]);
const base = state.baseurl || provider.defaultBaseurl;
const url = base + "/v1/models";
const headers = ["x-api-key: " + key[0], "anthropic-version: 2023-06-01"];
return listModels(url, headers, (model: any) => model.id);
};
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;
}
}
const listMistralModels = (provider: ProviderConfig): string => {
const key = getApiKey("mistral", "MISTRAL_API_KEY");
if (key && key[1]) throw new Error(key[1]);
const base = state.baseurl || provider.defaultBaseurl;
const url = base + "/v1/models";
const headers = ["Authorization: Bearer " + key[0]];
const response = httpGet(url, headers) as any;
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);
return listDataModels(baseUrl + "/v1/models", headers, (model) => model.id);
}
function listOllamaModels(provider: ProviderConfig): string {
const { baseUrl, headers } = getProviderRequest(provider);
const response = httpGet(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(baseUrl + "/v1/models", headers);
if (response.data) {
const uniqByName = (arr: any[]) =>
arr.filter((obj, i, self) =>
self.findIndex((o) => o.name === obj.name) === i
);
return uniqByName(response.data)
.map((model: any) =>
return uniqueBy(response.data, (model) => model.name || model.id)
.map((model) =>
[
padRight(model.name || model.id, 30),
padRight("" + (model.max_context_length || ""), 10),
@@ -81,53 +169,205 @@ const listMistralModels = (provider: ProviderConfig): string => {
.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) => 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) => 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) => 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) =>
`${baseUrl}/v1beta/models/${model}:generateContent?key=${apiKey}`,
requiresUrlApiKey: true,
},
};
export const providerRegistry: ProviderRegistry = {
anthropic: {
defaultModel: "claude-3-7-sonnet-20250219",
defaultBaseurl: "https://api.anthropic.com",
requiresAuth: true,
defaultBaseUrl: "https://api.anthropic.com",
authKey: "ANTHROPIC_API_KEY",
keyName: "anthropic",
authStyle: "anthropic",
apiStyle: "anthropic",
listModelsCallback: listAnthropicModels,
},
claude: {
defaultModel: "claude-3-7-sonnet-20250219",
defaultBaseurl: "https://api.anthropic.com",
requiresAuth: true,
defaultBaseUrl: "https://api.anthropic.com",
authKey: "ANTHROPIC_API_KEY",
keyName: "claude",
authStyle: "anthropic",
apiStyle: "anthropic",
listModelsCallback: listAnthropicModels,
},
openai: {
defaultModel: "gpt-4o-mini",
defaultBaseurl: "https://api.openai.com",
requiresAuth: true,
defaultBaseUrl: "https://api.openai.com",
authKey: "OPENAI_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
listModelsCallback: listOpenAIModels,
},
ollama: {
defaultModel: "qwen2.5-coder:latest",
defaultBaseurl: "http://localhost:11434",
requiresAuth: false,
defaultBaseUrl: "http://localhost:11434",
authStyle: "none",
apiStyle: "ollama",
listModelsCallback: listOllamaModels,
},
ollamacloud: {
defaultModel: "gpt-oss:120b",
defaultBaseurl: "https://ollama.com",
requiresAuth: true,
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",
listModelsCallback: listOpenAIModels,
hardcodedModels: ["big-pickle", "glm-5", "kimi-k2.5"],
},
gemini: {
defaultModel: "gemini-2.5-flash",
defaultBaseurl: "https://generativelanguage.googleapis.com",
requiresAuth: true,
defaultBaseUrl: "https://generativelanguage.googleapis.com",
authKey: "GEMINI_API_KEY",
authStyle: "none",
apiStyle: "gemini",
hardcodedModels: [
"gemini-2.0-flash",
@@ -139,35 +379,47 @@ export const providerRegistry: ProviderRegistry = {
},
mistral: {
defaultModel: "codestral-latest",
defaultBaseurl: "https://api.mistral.ai",
requiresAuth: true,
defaultBaseUrl: "https://api.mistral.ai",
authKey: "MISTRAL_API_KEY",
authStyle: "bearer",
apiStyle: "openai",
listModelsCallback: listMistralModels,
},
xai: {
defaultModel: "grok-beta",
defaultBaseurl: "https://api.x.ai",
requiresAuth: true,
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",
requiresAuth: false,
defaultBaseUrl: "http://127.0.0.1:1234",
authStyle: "none",
apiStyle: "openai",
listModelsCallback: listOpenAIModels,
},
deepseek: {
defaultModel: "deepseek-coder",
defaultBaseurl: "https://api.deepseek.com",
requiresAuth: true,
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 {
@@ -178,15 +430,47 @@ export function listProviders(): string[] {
return Object.keys(providerRegistry);
}
function buildQuery(msg: string, hideprompt: boolean): string {
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;
if (state.think >= 0) {
if (state.think === 0) {
// 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 if (state.think > 0) {
} 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;
@@ -201,247 +485,53 @@ function languagePrompt(): string {
" programming language\n";
}
function handleProvider(
function callRuntime(
provider: ProviderConfig,
runtime: ProviderRuntime,
msg: string,
hideprompt: boolean,
payloadBuilder: PayloadBuilder,
responseParser: ResponseParser,
urlBuilder: UrlBuilder,
headersBuilder: HeadersBuilder,
): string {
const model = state.model || provider.defaultModel;
const query = buildQuery(msg, hideprompt);
const query = buildQuery(msg, hideprompt, provider.apiStyle);
const apiKey = readProviderKey(provider);
let key: ApiKeyResult | undefined;
if (provider.requiresAuth && provider.authKey) {
key = getApiKey(
provider.authKey.split("_")[0].toLowerCase(),
provider.authKey,
);
if (key[1]) {
return `Cannot read ~/.r2ai.${
provider.authKey.split("_")[0].toLowerCase()
}-key`;
}
if (runtime.requiresUrlApiKey && apiKey?.[1] && provider.authKey) {
return `Cannot read ~/.r2ai.${getApiKeyName(provider.authKey)}-key`;
}
const payload = payloadBuilder(model, query, provider);
const base = state.baseurl || provider.defaultBaseurl;
const url = urlBuilder(base, model, key && key[0] ? key[0] : undefined);
const headers = headersBuilder(key ? key[0] : null, provider);
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 {
const res = httpPost(url, headers, JSON.stringify(payload));
return responseParser(res);
} catch (e) {
const err = e as Error;
return "ERROR: " + err.message;
return runtime.parseResponse(
httpPost(url, headers, JSON.stringify(payload)),
);
} catch (error) {
return "ERROR: " + (error as Error).message;
}
}
export function handleOpenAI(
provider: ProviderConfig,
msg: string,
hideprompt: boolean,
): string {
const payloadBuilder: PayloadBuilder = (model, query) => ({
stream: false,
model,
messages: [{ role: "user", content: query }],
});
const responseParser: ResponseParser = (res) => {
if (res.error && typeof res.error === "object" && res.error.message) {
throw new Error(res.error.message);
}
if (res.choices && res.choices[0]?.message?.content) {
return filterResponse(res.choices[0].message.content);
}
throw new Error("Invalid response format");
};
const urlBuilder: UrlBuilder = (base, model) => base + "/v1/chat/completions";
const headersBuilder: HeadersBuilder = (key) =>
key ? ["Authorization: Bearer " + key] : [];
return handleProvider(
provider,
msg,
hideprompt,
payloadBuilder,
responseParser,
urlBuilder,
headersBuilder,
);
}
export function handleAnthropic(
provider: ProviderConfig,
msg: string,
hideprompt: boolean,
): string {
if (!provider.authKey) {
return "ERROR: No auth key configured";
}
const payloadBuilder: PayloadBuilder = (model, query) => {
const payload: Record<string, unknown> = {
model,
max_tokens: 5128,
messages: [{ role: "user", content: query }],
};
if (state.deterministic) {
Object.assign(payload, { temperature: 0, top_p: 0, top_k: 1 });
}
return payload;
};
const responseParser: ResponseParser = (res) => {
if (res.content && res.content[0]?.text) {
return filterResponse(res.content[0].text);
}
if (res.error) {
const errMsg = typeof res.error === "object"
? res.error.message
: res.error;
throw new Error(errMsg || "Unknown error");
}
throw new Error("Invalid response format");
};
const urlBuilder: UrlBuilder = (base, model) => base + "/v1/messages";
const headersBuilder: HeadersBuilder = (
key,
) => ["anthropic-version: 2023-06-01", "x-api-key: " + key];
return handleProvider(
provider,
msg,
hideprompt,
payloadBuilder,
responseParser,
urlBuilder,
headersBuilder,
);
}
export function handleOllama(
provider: ProviderConfig,
msg: string,
hideprompt: boolean,
): string {
const payloadBuilder: PayloadBuilder = (model, query) => {
const payload: Record<string, unknown> = {
stream: false,
model,
messages: [{ role: "user", content: query }],
};
if (state.deterministic) {
payload.options = {
repeat_last_n: 0,
top_p: 0.0,
top_k: 1.0,
temperature: 0.0,
repeat_penalty: 1.0,
seed: 123,
};
}
return payload;
};
const responseParser: ResponseParser = (res) => {
if (res && res.error) {
const errMsg = typeof res.error === "string"
? res.error
: JSON.stringify(res.error);
throw new Error(errMsg);
}
if (res.message && res.message.content) {
return filterResponse(res.message.content);
}
throw new Error(JSON.stringify(res));
};
const urlBuilder: UrlBuilder = (base, model) => base + "/api/chat";
const headersBuilder: HeadersBuilder = () => [];
return handleProvider(
provider,
msg,
hideprompt,
payloadBuilder,
responseParser,
urlBuilder,
headersBuilder,
);
}
export function handleGemini(
provider: ProviderConfig,
msg: string,
hideprompt: boolean,
): string {
if (!provider.authKey) {
return "ERROR: No auth key configured";
}
const payloadBuilder: PayloadBuilder = (model, query) => {
const payload: Record<string, unknown> = {
contents: [{ parts: [{ text: query }] }],
};
if (state.deterministic) {
payload.generationConfig = {
temperature: 0.0,
topP: 1.0,
topK: 1,
};
}
return payload;
};
const responseParser: ResponseParser = (res) => {
const r = res as any;
if (r.candidates && r.candidates[0]?.content?.parts?.[0]?.text) {
return filterResponse(r.candidates[0].content.parts[0].text);
}
if (r.error) {
throw new Error(
typeof r.error === "string" ? r.error : JSON.stringify(r.error),
);
}
console.log(JSON.stringify(r));
throw new Error("Invalid response format");
};
const urlBuilder: UrlBuilder = (base, model, key) =>
`${base}/v1beta/models/${model}:generateContent?key=${key}`;
const headersBuilder: HeadersBuilder = () => [];
return handleProvider(
provider,
msg,
hideprompt,
payloadBuilder,
responseParser,
urlBuilder,
headersBuilder,
);
}
export function callProvider(msg: string, hideprompt: boolean): string {
const providerConfig = getProvider(state.api);
if (!providerConfig) {
const availableApis = listProviders().join(", ");
return `Unknown value for 'decai -e api'. Available: ${availableApis}`;
const provider = getProvider(state.api);
if (!provider) {
return `Unknown value for 'decai -e api'. Available: ${
listProviders().join(", ")
}`;
}
switch (providerConfig.apiStyle) {
case "openai":
return handleOpenAI(providerConfig, msg, hideprompt);
case "anthropic":
return handleAnthropic(providerConfig, msg, hideprompt);
case "ollama":
return handleOllama(providerConfig, msg, hideprompt);
case "gemini":
return handleGemini(providerConfig, msg, hideprompt);
default:
return `Unsupported API style: ${providerConfig.apiStyle}`;
}
return callRuntime(
provider,
providerRuntimes[provider.apiStyle],
msg,
hideprompt,
);
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { state } from "./state";
import { debugLog, fileDump, tmpdir } from "./utils";
import { fileDump, tmpdir } from "./utils";
import { callProvider } from "./providers";
import { httpGet } from "./http";
@@ -24,12 +24,12 @@ export function r2ai(
? state.baseurl + "/cmd"
: state.host + ":" + state.port + "/cmd";
const url = host + "/" + q.replace(/ /g, "%20").replace(/'/g, "\\'");
const url = host + "/" + encodeURIComponent(q);
const response = httpGet(url, []);
if (response.error) {
return `Error: ${response.error}`;
}
return (response as any).result || JSON.stringify(response) ||
return response.result || JSON.stringify(response) ||
"Cannot curl, use r2ai-server or r2ai -w";
}
+64 -22
View File
@@ -1,4 +1,27 @@
export class R2PipeSyncFromSync {
export type R2Address = number | string | unknown;
export interface R2LogEntry {
type: string;
message: string;
}
export interface R2CommandResult {
res?: string;
value?: number;
logs?: R2LogEntry[];
[key: string]: unknown;
}
export interface R2Plugin {
name: string;
license: string;
desc: string;
call: (command: string) => boolean;
}
export type R2PluginFactory = () => R2Plugin;
export class R2PipeAdapter {
r2p: R2Pipe;
constructor(r2p: R2Pipe) {
this.r2p = r2p;
@@ -12,34 +35,44 @@ export class R2PipeSyncFromSync {
cmd(command: string): string {
return this.r2p.cmd(command);
}
cmd2(command: string): any {
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: number | string | any,
address: R2Address,
): string {
return this.r2p.cmdAt(command, address);
}
cmdj(cmd: string): any {
cmdj(cmd: string): unknown {
return this.r2p.cmdj(cmd);
}
call(command: string): string {
return this.r2p.call(command);
}
callj(cmd: string): any {
callj(cmd: string): unknown {
return this.r2p.cmdj(cmd);
}
callAt(
command: string,
address: number | string | any,
address: R2Address,
): string {
return this.r2p.cmdAt(command, address);
}
log(msg: string) {
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: any): boolean {
plugin(type: string, maker: R2PluginFactory): boolean {
return this.r2p.plugin(type, maker);
}
unload(type: string, name: string): boolean {
@@ -47,9 +80,8 @@ export class R2PipeSyncFromSync {
}
}
export function newAsyncR2PipeFromSync(r2p: R2Pipe): R2PipeSync {
const asyncR2Pipe = new R2PipeSyncFromSync(r2p);
return asyncR2Pipe as R2PipeSync;
export function newR2PipeSync(r2p: R2Pipe): R2PipeSync {
return new R2PipeAdapter(r2p) as R2PipeSync;
}
/**
@@ -74,7 +106,8 @@ export interface R2Pipe {
* @param {number|string|NativePointer} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmdAt(cmd: string, address: number | string | any): string;
cmd2(cmd: string): R2CommandResult;
cmdAt(cmd: string, address: R2Address): string;
/**
* Run a radare2 command expecting the output to be JSON
@@ -82,7 +115,7 @@ export interface R2Pipe {
* @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): any;
cmdj(cmd: string): unknown;
/**
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
@@ -102,7 +135,11 @@ export interface R2Pipe {
* @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: string | number | any): string;
callAt(cmd: string, address: R2Address): string;
syscmd(cmd: string): string;
syscmds(cmd: string): string;
fdump(data: string): string;
/**
* Same as cmdj but using .call which avoids command injection problems
@@ -110,7 +147,7 @@ export interface R2Pipe {
* @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): any;
callj(cmd: string): unknown;
/**
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
@@ -126,7 +163,7 @@ export interface R2Pipe {
* @param {string} function that returns the plugin definition
* @returns {boolean} true if successful
*/
plugin(type: string, maker: any): boolean;
plugin(type: string, maker: R2PluginFactory): boolean;
/**
* Unload the plugin associated with a `type` and a `name`.
@@ -160,7 +197,8 @@ export interface R2PipeSync {
* @param {number|string|NativePointer} command to be executed inside radare2.
* @returns {string} The output of the command execution
*/
cmdAt(cmd: string, address: number | string | any): string;
cmd2(cmd: string): R2CommandResult;
cmdAt(cmd: string, address: R2Address): string;
/**
* Run a radare2 command expecting the output to be JSON
@@ -168,7 +206,7 @@ export interface R2PipeSync {
* @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): any;
cmdj(cmd: string): unknown;
/**
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
@@ -188,7 +226,11 @@ export interface R2PipeSync {
* @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: string | number | any): string;
callAt(cmd: string, address: R2Address): string;
syscmd(cmd: string): string;
syscmds(cmd: string): string;
fdump(data: string): string;
/**
* Same as cmdj but using .call which avoids command injection problems
@@ -196,7 +238,7 @@ export interface R2PipeSync {
* @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): any;
callj(cmd: string): unknown;
/**
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
@@ -212,7 +254,7 @@ export interface R2PipeSync {
* @param {string} function that returns the plugin definition
* @returns {boolean} true if successful
*/
plugin(type: string, maker: any): boolean;
plugin(type: string, maker: R2PluginFactory): boolean;
/**
* Unload the plugin associated with a `type` and a `name`.
+11 -2
View File
@@ -1,11 +1,12 @@
import { AppState } from "./types";
import { DEFAULT_PROMPT } from "./constants";
export const state: AppState = {
export const defaultState: AppState = {
decopipe: { use: false },
host: "http://localhost",
port: "11434",
baseurl: "",
extraHeaders: [],
api: "ollama",
pipeline: "",
commands: "pdc",
@@ -15,7 +16,9 @@ export const state: AppState = {
humanLanguage: "English",
deterministic: true,
debug: false,
think: -1,
timeout: 180,
think: "",
apitype: "chat",
useFiles: false,
contextFile: "",
model: "",
@@ -24,3 +27,9 @@ export const state: AppState = {
prompt: DEFAULT_PROMPT,
lastOutput: "",
};
export const state: AppState = {
...defaultState,
decopipe: { ...defaultState.decopipe },
extraHeaders: [...defaultState.extraHeaders],
};
+99 -32
View File
@@ -1,20 +1,28 @@
import type { R2PipeSync } from "r2papi";
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;
requiresAuth: boolean;
defaultBaseUrl: string;
authKey?: string;
authStyle?: AuthStyle;
keyName?: string;
apiStyle: ApiStyle;
hardcodedModels?: string[];
listModelsCallback?: (provider: ProviderConfig) => string;
}
export interface ProviderRegistry {
@@ -32,6 +40,7 @@ export interface AppState {
host: string;
port: string;
baseurl: string;
extraHeaders: string[];
api: string;
pipeline: string;
commands: string;
@@ -41,7 +50,9 @@ export interface AppState {
humanLanguage: string;
deterministic: boolean;
debug: boolean;
think: number;
timeout: number;
think: string;
apitype: OllamaApiType;
useFiles: boolean;
contextFile: string;
model: string;
@@ -52,7 +63,7 @@ export interface AppState {
}
export interface ConfigHandler {
get: () => unknown;
get: () => string | number | boolean;
set: (v: string) => void;
}
@@ -62,35 +73,91 @@ export interface ConfigHandlers {
export type ApiKeyResult = [string | null, string | null, string];
export interface HttpResponse {
error?: { message?: string } | string;
choices?: Array<{ message: { content: string } }>;
content?: Array<{ text: string }>;
message?: { content: string };
candidates?: Array<{ content: { parts: Array<{ text: string }> } }>;
data?: Array<
{
id: string;
name?: string;
max_context_length?: number;
description?: string;
}
>;
models?: Array<{ name: string }>;
export interface ApiError {
message?: string;
[key: string]: unknown;
}
export type PayloadBuilder = (
model: string,
query: string,
provider: ProviderConfig,
) => Record<string, unknown>;
export type ResponseParser = (res: HttpResponse) => string;
export type UrlBuilder = (base: string, model: string, key?: string) => string;
export type HeadersBuilder = (
key: string | null,
provider: ProviderConfig,
) => string[];
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;
+12 -8
View File
@@ -5,8 +5,14 @@ export function tmpdir(path: string): string {
return dir + "/" + path;
}
export function ensurePath(dir: string, file: string): void {
r2.cmd("'mkdir -p " + dir);
r2.cmd("'touch " + file);
}
export function fileExists(path: string): boolean {
if (r2.cmd2("test -h").logs[0].message.indexOf("-fdx") !== -1) {
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");
@@ -27,10 +33,10 @@ export function trimDown(out: string): string {
export function trimJson(out: string): string {
let result = out;
const bob = result.indexOf("{");
if (bob !== -1) result = result.slice(bob);
const eob = result.indexOf("}");
if (eob !== -1) result = result.slice(0, eob + 1);
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;
}
@@ -45,9 +51,7 @@ export function fileDump(fileName: string, fileData: string): void {
export function filterResponse(msg: string): string {
let result = msg;
if (state.think !== 2) {
result = result.replace(/<think>[\s\S]*?<\/think>/gi, "");
}
result = result.replace(/<think>[\s\S]*?<\/think>/gi, "");
return result
.split("\n")
.filter((line) => !line.trim().startsWith("```"))
+28 -14
View File
@@ -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
+18
View File
@@ -0,0 +1,18 @@
# 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')
```
+9
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#ifdef R2_CORE_H
extern RCorePlugin r_core_plugin_r2ai;
#endif
+23
View File
@@ -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)
+27
View File
@@ -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,
)
+1
View File
@@ -0,0 +1 @@
include p/r2ai/r2plugin/core/deps.mk
+30
View File
@@ -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',
)
+3
View File
@@ -0,0 +1,3 @@
#ifdef R2_CORE_H
#include "core/deps.h"
#endif
+1
View File
@@ -0,0 +1 @@
core.r2ai
+16 -1
View File
@@ -5,6 +5,8 @@ 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)
ifeq ($(R2MANDIR),)
R2MANDIR=$(shell r2 -NNH R2_PREFIX)/share/man
@@ -23,6 +25,8 @@ CFLAGS+=-I. -fPIC
CFLAGS+=-Wall
CFLAGS+=-g
AR?=ar
USE_R2_CURL=1
USE_LIBCURL=0
@@ -43,6 +47,8 @@ endif
OBJS=r2ai.o
OBJS+=apikeys.o
OBJS+=async.o
OBJS+=claw.o
OBJS+=llm.o
OBJS+=auto.o
OBJS+=json.o
@@ -51,14 +57,17 @@ 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
try: all
@@ -92,14 +101,20 @@ 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 --
+12 -3
View File
@@ -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,8 +42,10 @@ 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 -w launch the interactive setup wizard
| r2ai -L show chat logs (See -Lj for json). Only for auto mode.
| r2ai -L-[N] delete the last (or N last messages from the chat history)
| r2ai -R reset the chat conversation context
@@ -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,22 @@ 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.wizard | Auto-start the setup wizard on the first interactive chat session |
| r2ai.auto.max_runs | Maximum loops when using auto mode `r2ai -a` |
| r2ai.auto.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
@@ -114,7 +123,7 @@ $ r2 ./substitute
[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
+13 -24
View File
@@ -1,3 +1,5 @@
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
static const char *modelname(const char *model_name) {
@@ -148,17 +150,21 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
R_LOG_DEBUG ("Anthropic API response: %s", res);
}
// Parse the response
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
R2AI_ChatResponse *result = r2ai_anthropic_parse_response (res, error);
free (res);
return result;
}
/* Shared Anthropic response parser -- used by both anthropic.c and vertex.c */
R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **error) {
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
R2AI_Message *message = NULL;
R2AI_Usage *usage = NULL;
char *response_copy = strdup (res);
char *response_copy = strdup (json);
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");
@@ -182,14 +188,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
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");
@@ -201,7 +204,6 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
}
}
// Allocate tool_calls list if needed
if (has_tool_use && n_tool_calls > 0) {
message->tool_calls = r_list_new ();
if (!message->tool_calls) {
@@ -211,13 +213,11 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
r_json_free (jres);
free (response_copy);
r2ai_message_free (message);
free (res);
return NULL;
}
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
}
// Process each content item
int tool_idx = 0;
if (content_array && content_array->type == R_JSON_ARRAY) {
RList *cb = r2ai_content_blocks_new ();
@@ -225,7 +225,6 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
r_json_free (jres);
free (response_copy);
r2ai_message_free (message);
free (res);
return NULL;
}
const RJson *content_item = content_array->children.first;
@@ -234,7 +233,6 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
if (type && type->type == R_JSON_STRING) {
R2AI_ContentBlock *block = R_NEW0 (R2AI_ContentBlock);
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);
@@ -242,7 +240,6 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
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");
@@ -260,7 +257,6 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
}
if (input && input->type == R_JSON_OBJECT) {
#if 1
char *input_str = r_json_to_string (input);
if (input_str) {
R_LOG_DEBUG ("Input string: %s", input_str);
@@ -268,17 +264,16 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
tc->arguments = strdup (input_str);
free (input_str);
}
#endif
}
r_list_append (message->tool_calls, tc);
tool_idx++;
} else if (!strcmp (type->str_value, "thinking")) {
const RJson *data = r_json_get (content_item, "data");
const RJson *tdata = 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 (tdata && tdata->type == R_JSON_STRING) {
block->data = strdup (tdata->str_value);
}
if (thinking && thinking->type == R_JSON_STRING) {
block->thinking = strdup (thinking->str_value);
@@ -299,10 +294,8 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
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->tool_calls || r_list_empty (message->tool_calls))) {
r2ai_message_free (message);
message = NULL;
@@ -313,12 +306,8 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
free (response_copy);
}
// Assign message and usage to result
result->message = message;
result->usage = usage;
// Free the HTTP response body
free (res);
return result;
}
+9 -6
View File
@@ -1,4 +1,4 @@
/* r2ai - Copyright 2023-2025 pancake */
/* r2ai - Copyright 2023-2026 pancake */
#define R_LOG_ORIGIN "r2ai"
@@ -19,15 +19,18 @@ 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";
"\n"
"# Specify your API keys in the format: PROVIDER=KEY\n"
"# OpenAI=sk-your-openai-api-key-here\n"
"# MISTRAL=your-mistral-api-key-here\n";
r_file_dump (keys_path, (const ut8 *)template, strlen (template), 0);
}
r_cons_editor (core->cons, keys_path, NULL);
r2ai_cons_editor (core->cons, keys_path, NULL);
free (keys_path);
}
+954
View File
@@ -0,0 +1,954 @@
/* 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;
}
/* 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 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);
}
free (t->title);
free (t->query);
free (t->system_prompt);
free (t->model);
free (t->provider);
r_strbuf_free (t->output);
free (t->error);
free (t->pending_tool_name);
free (t->pending_tool_args);
free (t->pending_tool_call_id);
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);
}
/* Build the LLM args for the worker, without any live RCore work. */
static void fill_args_from_task(R2AITask *t, R2AIArgs *args, char **error) {
memset (args, 0, sizeof (*args));
args->messages = t->messages;
args->system_prompt = t->system_prompt;
args->model = t->model;
args->provider = t->provider;
args->error = error;
args->dorag = false;
}
/* Worker: execute one LLM turn, appending any text output.
* Returns the R2AI_ChatResponse (caller owns). */
static R2AI_ChatResponse *run_llm_once(R2AITask *t) {
char *error = NULL;
R2AIArgs args;
fill_args_from_task (t, &args, &error);
/* For AUTO tasks we must send tools. */
if (t->kind == R2AI_TASK_AUTO) {
args.tools = r2ai_get_tools (t->cps->core, t->cps->data);
}
R2AI_ChatResponse *res = r2ai_llmcall (t->cps, args);
if (error) {
task_lock (t);
free (t->error);
t->error = error;
task_unlock (t);
}
return res;
}
/* Append human-readable chunks of the assistant message to task output. */
static void dump_message_to_output(R2AITask *t, const R2AI_Message *m) {
if (!m) {
return;
}
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);
}
}
/* Thread entry-point for QUERY tasks. */
static RThreadFunctionRet worker_query(RThread *th) {
R2AITask *t = (R2AITask *)th->user;
task_lock (t);
t->state = R2AI_TASK_RUNNING;
t->started = time (NULL);
task_unlock (t);
R2AI_ChatResponse *res = run_llm_once (t);
task_lock (t);
if (t->cancel_req) {
t->state = R2AI_TASK_CANCELLED;
} else if (!res || !res->message) {
if (!t->error) {
t->error = strdup ("llm call returned no response");
}
t->state = R2AI_TASK_ERROR;
} else {
const R2AI_Message *m = res->message;
task_unlock (t);
dump_message_to_output (t, m);
task_lock (t);
t->state = R2AI_TASK_COMPLETE;
}
t->finished = time (NULL);
task_unlock (t);
if (res) {
r2ai_message_free ((R2AI_Message *)res->message);
free (res);
}
return R_TH_STOP;
}
/* Thread entry-point for AUTO tasks. */
static RThreadFunctionRet worker_auto(RThread *th) {
R2AITask *t = (R2AITask *)th->user;
task_lock (t);
t->state = R2AI_TASK_RUNNING;
t->started = time (NULL);
int max_runs = r_config_get_i (t->cps->core->config, "r2ai.auto.max_runs");
task_unlock (t);
while (true) {
task_lock (t);
if (t->cancel_req) {
t->state = R2AI_TASK_CANCELLED;
t->finished = time (NULL);
task_unlock (t);
return R_TH_STOP;
}
if (t->steps >= max_runs) {
t->state = R2AI_TASK_ERROR;
free (t->error);
t->error = r_str_newf ("max runs (%d) reached", max_runs);
t->finished = time (NULL);
task_unlock (t);
return R_TH_STOP;
}
t->steps++;
t->state = R2AI_TASK_RUNNING;
task_unlock (t);
R2AI_ChatResponse *res = run_llm_once (t);
if (!res || !res->message) {
task_lock (t);
if (!t->error) {
t->error = strdup ("llm call returned no response");
}
t->state = R2AI_TASK_ERROR;
t->finished = time (NULL);
task_unlock (t);
if (res) {
free (res);
}
return R_TH_STOP;
}
const R2AI_Message *m = res->message;
dump_message_to_output (t, m);
r2ai_msgs_add (t->messages, m);
bool has_tool = m->tool_calls && r_list_length (m->tool_calls) > 0;
if (!has_tool) {
task_lock (t);
t->state = R2AI_TASK_COMPLETE;
t->finished = time (NULL);
task_unlock (t);
r2ai_message_free ((R2AI_Message *)m);
free (res);
return R_TH_STOP;
}
/* Take the first valid tool call. */
RListIter *iter;
R2AI_ToolCall *tc, *valid_tc = NULL;
r_list_foreach (m->tool_calls, iter, tc) {
if (tc->name && tc->arguments && tc->id) {
valid_tc = tc;
break;
}
}
if (!valid_tc) {
task_lock (t);
t->state = R2AI_TASK_ERROR;
free (t->error);
t->error = strdup ("llm returned an invalid tool call");
t->finished = time (NULL);
task_unlock (t);
r2ai_message_free ((R2AI_Message *)m);
free (res);
return R_TH_STOP;
}
tc = valid_tc;
char *name = tc->name? strdup (tc->name): NULL;
char *argsjson = tc->arguments? strdup (tc->arguments): NULL;
char *callid = tc->id? strdup (tc->id): NULL;
r2ai_message_free ((R2AI_Message *)m);
free (res);
task_lock (t);
free (t->pending_tool_name);
free (t->pending_tool_args);
free (t->pending_tool_call_id);
free (t->tool_result);
t->pending_tool_name = name;
t->pending_tool_args = argsjson;
t->pending_tool_call_id = callid;
t->tool_result = NULL;
t->state = R2AI_TASK_WAIT_APPROVE;
task_unlock (t);
/* Wait for main thread to provide tool_result via gate. */
r_th_sem_wait (t->gate);
task_lock (t);
if (t->cancel_req) {
t->state = R2AI_TASK_CANCELLED;
t->finished = time (NULL);
task_unlock (t);
return R_TH_STOP;
}
char *tool_out = t->tool_result? t->tool_result: strdup ("<no output>");
t->tool_result = NULL;
char *tool_id = t->pending_tool_call_id? strdup (t->pending_tool_call_id): NULL;
free (t->pending_tool_name);
free (t->pending_tool_args);
free (t->pending_tool_call_id);
t->pending_tool_name = NULL;
t->pending_tool_args = NULL;
t->pending_tool_call_id = NULL;
task_unlock (t);
R2AI_Message tool_msg = {
.role = "tool",
.tool_call_id = tool_id,
.content = tool_out,
};
r2ai_msgs_add (t->messages, &tool_msg);
free (tool_id);
free (tool_out);
}
return R_TH_STOP;
}
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;
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;
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;
t->kind = kind;
t->state = R2AI_TASK_PENDING;
t->title = strdup (title? title: "");
t->query = strdup (query? query: "");
if (R_STR_ISEMPTY (system_prompt) && kind == R2AI_TASK_AUTO) {
t->system_prompt = r2ai_auto_system_prompt (cps);
} else {
t->system_prompt = system_prompt? strdup (system_prompt): NULL;
}
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;
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) {
R2AI_State *state = cps->data;
if (!state || !state->async) {
return -1;
}
R2AITask *t = task_new (cps, kind, title, query, sysp);
int id = queue_register (state->async, t);
t->thread = r_th_new (fn, t, 0);
if (t->thread) {
r_th_start (t->thread);
} else {
task_lock (t);
t->state = R2AI_TASK_ERROR;
free (t->error);
t->error = strdup ("failed to spawn worker");
task_unlock (t);
}
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) {
R2AI_State *state = cps->data;
RCore *core = cps->core;
if (!state || !state->async) {
r_cons_printf (core->cons, "async queue not initialised\n");
return;
}
R2AITaskQueue *q = state->async;
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);
const char *out = r_strbuf_get (t->output);
if (R_STR_ISNOTEMPTY (out)) {
r_cons_printf (core->cons, "output:\n%s", out);
if (!r_str_endswith (out, "\n")) {
r_cons_newline (core->cons);
}
}
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;
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
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));
if (!R_STR_ISEMPTY (output)) {
r_cons_printf (core->cons, "%s", output);
if (!r_str_endswith (output, "\n")) {
r_cons_newline (core->cons);
}
}
free (output);
if (err) {
r_cons_printf (core->cons, Color_RED "error: %s" Color_RESET "\n", err);
free (err);
}
if (st == R2AI_TASK_COMPLETE || st == R2AI_TASK_ERROR || st == R2AI_TASK_CANCELLED) {
/* 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");
char *question = r_str_newf ("Run tool %s? (Y/n)", tool_name? tool_name: "?");
bool approve = yolo? true: r_cons_yesno (core->cons, 'y', "%s", question? question: "Run tool? (Y/n)");
free (question);
answer_wait_approve (cps, t, approve, true);
}
free (tool_name);
free (tool_args);
}
static R2AITask *find_by_id(R2AITaskQueue *q, int id) {
RListIter *it;
R2AITask *t;
r_list_foreach (q->tasks, it, t) {
if (t->id == id) {
return t;
}
}
return NULL;
}
static void answer_by_id(RCorePluginSession *cps, int id, bool approve) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
if (id <= 0) {
r_cons_printf (core->cons, "Missing task id\n");
return;
}
R2AITaskQueue *q = state->async;
queue_lock (q);
R2AITask *t = find_by_id (q, id);
if (!t) {
queue_unlock (q);
r_cons_printf (core->cons, "No task with id %d\n", id);
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;
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
queue_lock (q);
R2AITask *t = find_by_id (q, id);
if (!t) {
queue_unlock (q);
r_cons_printf (core->cons, "No task with id %d\n", id);
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;
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
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;
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
queue_lock (q);
R2AITask *t = find_by_id (q, id);
if (!t) {
queue_unlock (q);
r_cons_printf (core->cons, "No task with id %d\n", id);
return;
}
task_lock (t);
r_cons_printf (core->cons, "id: %d\n", t->id);
r_cons_printf (core->cons, "kind: %s\n", kind_name (t->kind));
r_cons_printf (core->cons, "state: %s\n", state_name (t->state));
r_cons_printf (core->cons, "title: %s\n", t->title? t->title: "");
r_cons_printf (core->cons, "model: %s\n", t->model? t->model: "");
r_cons_printf (core->cons, "prov: %s\n", t->provider? t->provider: "");
r_cons_printf (core->cons, "steps: %d\n", t->steps);
r_cons_printf (core->cons, "age: %ds\n", (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);
}
const char *out = r_strbuf_get (t->output);
if (!R_STR_ISEMPTY (out)) {
r_cons_printf (core->cons, "output:\n%s", out);
if (!r_str_endswith (out, "\n")) {
r_cons_newline (core->cons);
}
}
task_unlock (t);
queue_unlock (q);
}
static void show_last_task(RCorePluginSession *cps) {
RCore *core = cps->core;
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
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) {
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
queue_lock (q);
RListIter *it, *tmp;
R2AITask *t;
r_list_foreach_safe (q->tasks, it, tmp, t) {
task_lock (t);
bool drop = t->state == R2AI_TASK_COMPLETE || t->state == R2AI_TASK_ERROR || t->state == R2AI_TASK_CANCELLED;
task_unlock (t);
if (drop) {
drop_task_locked (q, t);
}
}
queue_unlock (q);
}
static void wait_all(RCorePluginSession *cps) {
R2AI_State *state = cps->data;
if (!state || !state->async) {
return;
}
R2AITaskQueue *q = state->async;
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;
}
}
+48 -82
View File
@@ -1,4 +1,4 @@
/* Copyright r2ai - 2023-2025 - pancake */
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
@@ -56,16 +56,7 @@ static void r2ai_print_run_end(RCorePluginSession *cps, const R2AI_Usage *usage,
char *total_time_str = format_time_duration (total_time);
// Print detailed stats
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n",
r_config_get (core->config, "r2ai.model"),
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, "\x1b[1" Color_BLUE "%s | total: %d in: %d out: %d | run: %d in: %d out: %d | %s / %s" Color_RESET "\n", r_config_get (core->config, "r2ai.model"), 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);
@@ -148,44 +139,22 @@ static void update_tool_call_argument(R2AI_Message *msg, const char *tool_call_i
}
}
#if 0
static const char *Gprompt_auto =
"You are a reverse engineer using radare2.\n"
"Answer questions about the loaded binary using r2cmd tool calls.\n"
"\n"
"Rules:\n"
"- Use r2cmd tool to run radare2 commands when you need information\n"
"- Make tool calls immediately when you need data\n"
"- Provide final answer only after gathering needed information\n"
"- Keep responses short and direct\n"
;
#else
const char *Gprompt_auto =
"You are a reverse engineer and you are using radare2 to analyze a binary.\n"
"The user will ask questions about the binary and you will respond with the answer to the best of your ability.\n"
"\n"
"# Guidelines\n"
"- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output.\n"
"- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached.\n"
"- Assume the user is always asking you about the binary, unless they're specifically asking you for radare2 help.\n"
"- The binary has already been loaded. You can interact with the binary using the r2cmd tool.\n"
"- `this` or `here` might refer to the current address in the binary or the binary itself.\n"
"- If you need more information, try to use the r2cmd tool to run commands before answering.\n"
"- You can use the r2cmd tool multiple times if you need or you can pass a command with pipes if you need to chain commands.\n"
"- IMPORTANT: You must make only ONE tool call per response. Do not make multiple tool calls in a single response.\n"
"- Keep your reasoning concise and avoid repeating the same thoughts. If you have a clear next step, execute it immediately with a tool call.\n"
"- If you're asked to decompile a function, make sure to return the code in the language you think it was originally written "
"and rewrite it to be as easy as possible to be understood. Make sure you use descriptive variable and function names and add comments.\n"
"- Don't just regurgitate the same code, figure out what it's doing and rewrite it to be more understandable.\n"
"- If you need to run a command in r2 before answering, you can use the r2cmd tool\n"
"- Do not repeat commands if you already know the answer.\n"
"- Formulate a plan. Think step by step. Analyze the binary as much as possible before answering.\n"
"- You must keep going until you have a final answer.\n"
"- Double check that final answer. Make sure you didn't miss anything.\n"
"- Make sure you call tools and functions correctly.\n"
"- When calling tools, use the exact format: {\"tool_calls\": [{\"id\": \"call_123\", \"type\": \"function\", \"function\": {\"name\": \"r2cmd\", \"arguments\": \"{\\\"command\\\":\\\"aa\\\"}\"}}]}\n"
"- Arguments must be a JSON string, not an object.\n";
#endif
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, RList *messages, const char *system_prompt, int n_run) {
@@ -201,18 +170,18 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
return;
}
const char *effective_prompt = system_prompt? system_prompt: Gprompt_auto;
const char *effective_prompt = system_prompt? system_prompt: auto_prompt_get (state);
if (!system_prompt) {
const char *init_commands = r_config_get (core->config, "r2ai.auto.init_commands");
if (R_STR_ISNOTEMPTY (init_commands)) {
char *edited_command = NULL;
char *comment = NULL;
char *cmd_output = execute_tool (core, "r2cmd", r_str_newf ("{\"command\":\"%s\"}", init_commands), &edited_command, &comment);
if (R_STR_ISNOTEMPTY (cmd_output)) {
char *tool_args = r_str_newf ("{\"command\":\"%s\"}", init_commands);
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, cmd_output);
if (comment && *comment) {
char *new_content = r_str_newf ("%s\nHINT: %s", content, comment);
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;
}
@@ -222,10 +191,8 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
};
r2ai_msgs_add (messages, &init_msg);
free (display_command);
free (cmd_output);
}
free (edited_command);
free (comment);
r2ai_tool_result_fini (&tool_result);
}
}
@@ -329,24 +296,30 @@ R_API void process_messages(RCorePluginSession *cps, RList *messages, const char
if (interrupted) {
cmd_output = strdup ("<user interrupted>");
} else {
char *edited_command = NULL;
cmd_output = execute_tool (core, tool_name, tool_args, &edited_command, &comment);
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, r_list_length (messages) - 1);
if (last_msg) {
if (!strcmp (tool_name, "r2cmd")) {
update_tool_call_argument (last_msg, tool_call->id, "command", edited_command);
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", edited_command);
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" Color_RED "[r2ai] Processing interrupted after tool execution" Color_RESET "\n\n");
r_cons_flush (core->cons);
@@ -415,6 +388,7 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
// If this is the first message in a new conversation, clear previous history
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
@@ -424,14 +398,9 @@ R_IPI void cmd_r2ai_a(RCorePluginSession *cps, const char *user_query) {
};
r2ai_msgs_add (messages, &user_msg);
const char *system_prompt = NULL;
if (!r_config_get_b (core->config, "r2ai.auto.think")) {
system_prompt = r_str_newf ("/no_think\nReasoning: Low\n%s", Gprompt_auto);
}
char *system_prompt = r2ai_auto_system_prompt (cps);
process_messages (cps, messages, system_prompt, 1);
if (system_prompt) {
free ((char *)system_prompt);
}
free (system_prompt);
}
// Helper function to display content with length indication for long content
@@ -444,7 +413,7 @@ static void print_content_with_length(RCore *core, const char *content, const ch
}
// 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
@@ -454,9 +423,7 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
return;
}
const char *input = r_core_cmd_str (core, "r2ai");
bool json_mode = input && strstr (input, "-Lj");
free ((char *)input);
bool json_mode = flags && strchr (flags, 'j');
if (json_mode) {
PJ *pj = pj_new ();
@@ -500,8 +467,7 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
return;
}
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n",
r_list_length (messages));
r_cons_printf (core->cons, "\x1b[1" Color_BLUE "[r2ai] Chat Logs (%d messages)" Color_RESET "\n", r_list_length (messages));
r_cons_printf (core->cons, "\x1b[1" Color_YELLOW "Note: System prompt is applied automatically but not stored in history" Color_RESET "\n\n");
@@ -523,8 +489,7 @@ R_IPI void cmd_r2ai_logs(RCorePluginSession *cps) {
RListIter *iter;
R2AI_ToolCall *tc;
r_list_foreach (msg->tool_calls, iter, tc) {
r_cons_printf (core->cons, " \x1b[1" Color_MAGENTA "[tool call]:" Color_RESET " %s\n",
tc->name? tc->name: "<unnamed>");
r_cons_printf (core->cons, " \x1b[1" Color_MAGENTA "[tool call]:" Color_RESET " %s\n", tc->name? tc->name: "<unnamed>");
if (tc->arguments) {
r_cons_printf (core->cons, " %s\n", tc->arguments);
@@ -602,7 +567,7 @@ static void process_conversation_with_llm(RCorePluginSession *cps, bool compact)
// 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."
"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");
}
@@ -626,6 +591,7 @@ static void process_conversation_with_llm(RCorePluginSession *cps, bool compact)
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
+167
View File
@@ -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);
}
}
+12 -7
View File
@@ -1,4 +1,4 @@
/* r2ai - Copyright 2023-2025 pancake, dnakov */
/* r2ai - Copyright 2023-2026 pancake, dnakov */
#include "r2ai.h"
@@ -6,10 +6,11 @@
R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
args.model = r_config_get (core->config, "r2ai.model");
const char *base_url = r2ai_get_provider_url (core, args.provider);
const char *model_name = args.model && strstr (args.model, "gemini")? args.model: "gemini-2.0-flash-exp";
const char *model = R_STR_ISNOTEMPTY (args.model)
? args.model
: r_config_get (core->config, "r2ai.model");
char *base_url = r2ai_get_provider_url (core, args.provider);
const char *model_name = model && strstr (model, "gemini")? model: "gemini-2.0-flash-exp";
char **error = args.error;
// Create a temp conversation to include the system prompt and the rest of the messages
@@ -18,6 +19,7 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
if (error) {
*error = strdup ("Failed to create temporary messages array");
}
free (base_url);
return NULL;
}
@@ -65,7 +67,10 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
// Build Gemini API URL
char *gemini_url = r_str_newf ("%s/models/%s:generateContent?key=%s",
base_url, model_name, args.api_key);
base_url,
model_name,
args.api_key);
free (base_url);
// Create Gemini-style request JSON
PJ *pj = pj_new ();
@@ -213,4 +218,4 @@ R_IPI R2AI_ChatResponse *r2ai_gemini(RCorePluginSession *cps, R2AIArgs args) {
}
return result;
}
}
+49 -9
View File
@@ -1,4 +1,4 @@
/* Copyright r2ai - 2023-2025 - pancake */
/* Copyright r2ai - 2023-2026 - pancake */
#define _GNU_SOURCE
// #define _POSIX_C_SOURCE 200809L
@@ -180,28 +180,68 @@ 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) {
free (response.body);
@@ -219,7 +259,7 @@ 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) {
+3 -3
View File
@@ -33,7 +33,7 @@ static char *escape_single_quotes(const char *str) {
}
// Helper to append headers to PowerShell command
static void append_headers_to_cmd(RStrBuf *cmd, const char *const *headers) {
static void append_headers_to_cmd(RStrBuf *cmd, const char **headers) {
r_strbuf_appendf (cmd, "$headers=@{");
if (headers) {
for (int i = 0; headers[i]; i++) {
@@ -80,7 +80,7 @@ static HttpResponse parse_powershell_response(char *full_response) {
*/
HttpResponse windows_http_post(const HTTPRequest *request) {
const char *url = request->url;
const char *const *headers = request->headers;
const char **headers = request->headers;
const char *data = request->data;
int timeout = request->config.timeout;
char *escaped_url = escape_single_quotes (url);
@@ -102,7 +102,7 @@ HttpResponse windows_http_post(const HTTPRequest *request) {
*/
HttpResponse windows_http_get(const HTTPRequest *request) {
const char *url = request->url;
const char *const *headers = request->headers;
const char **headers = request->headers;
int timeout = request->config.timeout;
char *escaped_url = escape_single_quotes (url);
RStrBuf *cmd = r_strbuf_new ("powershell -Command \"");
+2 -2
View File
@@ -12,7 +12,7 @@ static HttpResponse socket_http_post_with_interrupt(const HTTPRequest *request)
#endif
int code = 0;
int rlen = 0;
char *result = r_socket_http_post (request->url, (const char **)request->headers, request->data, &code, &rlen);
char *result = r_socket_http_post (request->url, request->headers, request->data, &code, &rlen);
#if R2__UNIX__
alarm (0);
#endif
@@ -36,7 +36,7 @@ static HttpResponse socket_http_get_with_interrupt(const HTTPRequest *request) {
// 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);
char *result = r_socket_http_get (request->url, request->headers, &code, &rlen);
#if R2__UNIX__
alarm (0);
#endif
+2
View File
@@ -1,3 +1,5 @@
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
// TODO: move into r2/libr/util/json.c
//
+127 -48
View File
@@ -1,3 +1,5 @@
/* Copyright r2ai - 2023-2026 - pancake */
#define R_LOG_ORIGIN "llm"
#include "r2ai.h"
@@ -8,13 +10,20 @@ static const R2AIProvider r2ai_providers[] = {
{ "openai", "https://api.openai.com/v1", R2AI_API_OPENAI_COMPATIBLE, true, true },
{ "gemini", "https://generativelanguage.googleapis.com/v1beta", R2AI_API_GEMINI, true, false },
{ "anthropic", "https://api.anthropic.com/v1", R2AI_API_ANTHROPIC, true, false },
{ "ollama", "http://localhost:11434/api", R2AI_API_OLLAMA, false, true },
{ "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
};
@@ -30,27 +39,43 @@ R_IPI const R2AIProvider *r2ai_get_provider(const char *name) {
return NULL;
}
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);
}
// Forward declaration for rawtools
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 && r_list_length (args.tools) > 0) {
return r2ai_rawtools_llmcall (cps, args);
}
R2AI_State *state = cps->data;
R2AI_ChatResponse *res = NULL;
if (!args.model) {
const char *config_model = r_config_get (core->config, "r2ai.model");
args.model = strdup (config_model? config_model: "");
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) {
@@ -64,11 +89,17 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
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) {
bool is_vertex = (prov->api_type == R2AI_API_VERTEX_GEMINI || prov->api_type == R2AI_API_VERTEX_ANTHROPIC);
if (is_vertex) {
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;
@@ -81,18 +112,28 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
r_str_case (Provider, true);
R_LOG_ERROR ("No API key found for the %s provider. Use r2ai -K", provider);
free (Provider);
return NULL;
goto cleanup;
}
}
// Set system_prompt from config if it's not already set
int context_pullback = -1;
if (use_rawtools (core, prov, &args)) {
res = r2ai_rawtools_llmcall (cps, args);
goto finish;
}
if (!args.system_prompt) {
args.system_prompt = r_config_get (core->config, "r2ai.system");
}
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");
@@ -113,25 +154,25 @@ 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);
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) {
return NULL;
goto cleanup;
}
switch (p->api_type) {
@@ -141,12 +182,19 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args) {
case R2AI_API_GEMINI:
res = r2ai_gemini (cps, args);
break;
case R2AI_API_VERTEX_GEMINI:
res = r2ai_vertex_gemini (cps, args);
break;
case R2AI_API_VERTEX_ANTHROPIC:
res = r2ai_vertex_anthropic (cps, args);
break;
case R2AI_API_OPENAI_COMPATIBLE:
case R2AI_API_OLLAMA:
default:
res = r2ai_openai (cps, args);
break;
}
finish:
if (context_pullback != -1) {
R2AI_Message *msg = r_list_get_n (args.messages, context_pullback);
free ((char *)msg->content);
@@ -158,39 +206,72 @@ 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;
}
R_IPI const char *r2ai_get_provider_url(RCore *core, const char *provider) {
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;
}
r_str_trim (url);
size_t len = strlen (url);
while (len > 0 && url[len - 1] == '/') {
url[--len] = 0;
}
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;
}
if (has_root) {
url[len - 3] = 0;
}
char *res = r_str_newf ("%s%s", url, api_root);
free (url);
return res;
}
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 (p->api_type == R2AI_API_VERTEX_GEMINI || p->api_type == R2AI_API_VERTEX_ANTHROPIC) {
return NULL;
}
const bool is_ollama = p->api_type == R2AI_API_OLLAMA;
const bool use_generate = is_ollama && is_generate_api (core);
if (p->supports_custom_baseurl) {
const char *host = r_config_get (core->config, "r2ai.baseurl");
if (R_STR_ISNOTEMPTY (host)) {
if (r_str_startswith (host, "http")) {
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
return r_str_newf ("%s/v1", host);
}
return r_str_newf ("%s/api", host);
}
if (p->api_type == R2AI_API_OPENAI_COMPATIBLE) {
return r_str_newf ("http://%s/v1", host);
}
return r_str_newf ("http://%s/api", host);
const char *api_root = use_generate? "/api": "/v1";
return provider_api_url (host, api_root, is_ollama && !use_generate);
}
}
if (use_generate) {
return provider_api_url (p->url, "/api", false);
}
return p->url;
return p->url? strdup (p->url): NULL;
}
R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
if (!provider) {
return NULL;
}
const char *purl = r2ai_get_provider_url (core, provider);
const R2AIProvider *pcheck = r2ai_get_provider (provider);
if (pcheck && (pcheck->api_type == R2AI_API_VERTEX_GEMINI || pcheck->api_type == R2AI_API_VERTEX_ANTHROPIC)) {
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;
}
@@ -206,10 +287,12 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
char *models_url = NULL;
int code = 0;
char *response = NULL;
const bool usetags = p && p->api_type == R2AI_API_OLLAMA && r_str_endswith (purl, "/api");
// Special handling for Gemini
if (!strcmp (provider, "gemini")) {
if (!api_key) {
free (purl);
return NULL;
}
models_url = r_str_newf ("%s/models?key=%s", purl, api_key);
@@ -218,8 +301,6 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
response = r2ai_http_get (core, models_url, headers, &code, NULL);
} else {
// Create models endpoint URL
const R2AIProvider *prov = r2ai_get_provider (provider);
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
models_url = r_str_newf ("%s/%s", purl, usetags? "tags": "models");
if (api_key) {
@@ -254,6 +335,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
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);
@@ -276,8 +358,6 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
// Gemini has "models" array directly
data = r_json_get (json, "models");
} else {
const R2AIProvider *prov = r2ai_get_provider (provider);
const bool usetags = (prov && prov->api_type == R2AI_API_OLLAMA);
data = r_json_get (json, usetags? "models": "data");
}
@@ -307,8 +387,7 @@ R_IPI RList *r2ai_fetch_available_models(RCore *core, const char *provider) {
free (s);
}
} else {
const R2AIProvider *prov = r2ai_get_provider (provider);
if (prov && prov->api_type == R2AI_API_OLLAMA) {
if (usetags) {
id = r_json_get (model_item, "model");
} else {
id = r_json_get (model_item, "id");
+103 -13
View File
@@ -1,4 +1,4 @@
/* Copyright r2ai - 2023-2025 - pancake */
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai_priv.h"
@@ -7,25 +7,49 @@
static void show_help() {
printf (
"Usage: r2ai [-vhp:m:q:Eb:Kc:f:ie] <prompt>\n"
"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\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");
" -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;
@@ -38,6 +62,7 @@ static char *build_conversation(RList *conversation) {
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);
@@ -71,12 +96,12 @@ static void r2ai_repl(RCorePluginSession *cps, const char *provider, const char
.model = model,
.error = &err,
.dorag = false,
};
};
char *res = r2ai (cps, args);
if (res) {
if (r_config_get_b (core->config, "r2ai.clippy")) {
char *cmd = r_str_newf (CLIPPY " %s", res);
r_core_cmd_call (core, cmd);
r_core_call (core, cmd);
free (cmd);
} else {
r_cons_println (core->cons, res);
@@ -100,7 +125,12 @@ int main(int argc, const char **argv) {
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);
@@ -111,9 +141,26 @@ int main(int argc, const char **argv) {
r2ai_init (&cps);
RGetopt opt;
r_getopt_init (&opt, argc, argv, "vhp:m:q:Eb:Kc:f:ie:");
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 '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;
@@ -121,7 +168,11 @@ 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;
@@ -132,6 +183,9 @@ int main(int argc, const char **argv) {
case 'f':
filename = opt.arg;
break;
case 's':
seekaddr = opt.arg;
break;
case 'i':
scriptfile = opt.arg;
break;
@@ -143,7 +197,11 @@ int main(int argc, const char **argv) {
*equals = '\0';
const char *var = var_value;
const char *value = equals + 1;
r_config_set (core->config, var, value);
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.");
}
@@ -153,7 +211,7 @@ int main(int argc, const char **argv) {
case '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);
goto beach;
}
@@ -164,6 +222,12 @@ int main(int argc, const char **argv) {
goto beach;
}
break;
case 'w':
{
r2ai_wizard (core);
goto beach;
}
break;
case 'v':
show_version ();
goto beach;
@@ -176,21 +240,37 @@ int main(int argc, const char **argv) {
}
}
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_cmd_call (core, cmd);
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_cmd_call (core, cmd);
r_core_call (core, cmd);
free (cmd);
}
if (!r_list_empty (commands)) {
@@ -212,6 +292,16 @@ int main(int argc, const char **argv) {
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)) {
@@ -233,7 +323,7 @@ int main(int argc, const char **argv) {
.model = model,
.error = &err,
.dorag = false,
};
};
char *res = r2ai (&cps, args);
if (res) {
r_cons_println (core->cons, res);
+63 -54
View File
@@ -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 = {
@@ -46,52 +44,59 @@ R_API RMarkdownTheme r2ai_markdown_theme_default(void) {
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 &current_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
View File
@@ -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 */
+55 -181
View File
@@ -1,196 +1,77 @@
project('r2ai', 'c',
version : '1.3.2',
version : '1.4.2',
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(
'anthropic.c',
'async.c',
'auto.c',
'apikeys.c',
'claw.c',
'gemini.c',
'vertex.c',
'http.c',
'json.c',
'wizard.c',
'llm.c',
'markdown.c',
'messages.c',
@@ -202,21 +83,14 @@ srcs = files(
'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,
)
+24 -15
View File
@@ -1,4 +1,4 @@
/* r2ai - Copyright 2023-2025 dnakov, pancake */
/* r2ai - Copyright 2023-2026 dnakov, pancake */
#include "r2ai.h"
@@ -27,23 +27,28 @@ R_API void r2ai_content_blocks_free(RList *cb) {
r_list_free (cb);
}
R_API void r2ai_message_free(R2AI_Message *msg) {
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) {
r_list_free (msg->tool_calls);
}
if (msg->content_blocks) {
r2ai_content_blocks_free (msg->content_blocks);
}
memset (msg, 0, sizeof (*msg));
}
R_API void r2ai_message_free(R2AI_Message *msg) {
if (!msg) {
return;
}
r2ai_message_fini (msg);
free (msg);
}
@@ -108,7 +113,6 @@ R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg) {
RList *cb = r2ai_content_blocks_new ();
if (!cb) {
r2ai_message_free (new_msg);
free (new_msg);
return false;
}
RListIter *iter;
@@ -132,7 +136,6 @@ R_API bool r2ai_msgs_add(RList *msgs, const R2AI_Message *msg) {
new_msg->tool_calls = r_list_new ();
if (!new_msg->tool_calls) {
r2ai_message_free (new_msg);
free (new_msg);
return false;
}
new_msg->tool_calls->free = (RListFree)r2ai_tool_call_free;
@@ -229,7 +232,7 @@ R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
if (content_blocks && content_blocks->type == R_JSON_ARRAY && content_blocks->children.count > 0) {
RList *cb = r2ai_content_blocks_new ();
if (!cb) {
r2ai_message_free (&new_msg);
r2ai_message_fini (&new_msg);
return false;
}
for (size_t i = 0; i < content_blocks->children.count; i++) {
@@ -240,7 +243,7 @@ R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
R2AI_ContentBlock *dst = R_NEW0 (R2AI_ContentBlock);
if (!dst) {
r2ai_content_blocks_free (cb);
r2ai_message_free (&new_msg);
r2ai_message_fini (&new_msg);
return false;
}
const RJson *type = r_json_get (block, "type");
@@ -267,7 +270,7 @@ R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
// Add the message without tool calls first
if (!r2ai_msgs_add (msgs, &new_msg)) {
r2ai_message_free (&new_msg);
r2ai_message_fini (&new_msg);
return false;
}
@@ -296,7 +299,7 @@ R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
tc.id = (id && id->type == R_JSON_STRING)? id->str_value: NULL;
if (!r2ai_msgs_add_tool_call (msgs, &tc)) {
r2ai_message_free (&new_msg);
r2ai_message_fini (&new_msg);
return false;
}
}
@@ -305,7 +308,7 @@ R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json) {
return true;
}
R_API char *r2ai_msgs_to_json(const RList *msgs) {
R_API char *r2ai_msgs_to_json(const RList *msgs, bool raw_tool_args) {
if (!msgs || r_list_empty (msgs)) {
return NULL;
}
@@ -369,8 +372,14 @@ R_API char *r2ai_msgs_to_json(const RList *msgs) {
// Add name
pj_ks (pj, "name", tc->name? tc->name: "");
// Add arguments (required by OpenAI API)
pj_ks (pj, "arguments", tc->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
pj_end (pj); // End tool call object
+404 -260
View File
@@ -1,25 +1,271 @@
/* r2ai - Copyright 2023-2025 pancake, dnakov */
/* r2ai - Copyright 2023-2026 pancake, dnakov */
#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;
}
typedef enum {
R2AI_CHAT_OPENAI,
R2AI_CHAT_OLLAMA,
R2AI_GENERATE_OLLAMA
} R2AIChatAPI;
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");
}
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;
}
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;
}
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 {
messages_json = r2ai_msgs_to_json (messages, use_ollama);
}
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->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);
}
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);
}
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;
args.provider = r_config_get (core->config, "r2ai.api");
args.model = r_config_get (core->config, "r2ai.model");
const char *provider_name = R_STR_ISNOTEMPTY (args.provider)
? args.provider
: r_config_get (core->config, "r2ai.api");
const char *model_name = R_STR_ISNOTEMPTY (args.model)
? args.model
: r_config_get (core->config, "r2ai.model");
const R2AIProvider *provider_info = r2ai_get_provider (args.provider);
const char *base_url = r2ai_get_provider_url (core, args.provider);
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) {
if (args.error) {
*args.error = strdup ("Failed to resolve provider URL");
}
return NULL;
}
R2AIChatAPI chat_api = use_generate
? R2AI_GENERATE_OLLAMA
: (is_ollama_provider && r_str_endswith (base_url, "/api")? R2AI_CHAT_OLLAMA: R2AI_CHAT_OPENAI);
// TODO: default model name should depend on api
const char *model_name = args.model? args.model: "gpt-4o-mini";
model_name = model_name? model_name: "gpt-4o-mini";
char **error = args.error;
RList *tools = args.tools;
// create a temp conversation to include the system prompt and the rest of the messages
RList *temp_msgs = r2ai_msgs_new ();
if (!temp_msgs) {
if (error) {
*error = strdup ("Failed to create temporary messages array");
}
free (base_url);
return NULL;
}
R2AI_Message system_msg = {
@@ -69,157 +315,93 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
*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 = (provider_info && provider_info->api_type == R2AI_API_OLLAMA)
? "%s/chat"
: "%s/chat/completions";
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_empty (temp_msgs)) {
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
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 (r_list_empty (temp_msgs)) {
if (error) {
*error = strdup ("No messages provided");
}
free (auth_header);
free (base_url);
r2ai_msgs_free (temp_msgs);
return NULL;
}
R_LOG_DEBUG ("Using input messages: %d messages", r_list_length (temp_msgs));
// Convert tools to OpenAI format if available
char *openai_tools_json = NULL;
if (tools && !r_list_empty (tools)) {
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 (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
// 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);
// 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);
if (r_config_get_b (core->config, "r2ai.debug")) {
// Generate curl command for debugging
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
for (int i = 0; headers[i]; i++) {
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
}
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", complete_json, openai_url);
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
r_strbuf_free (curl_cmd);
}
// 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);
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) {
if (error) {
*error = strdup ("Failed to create request JSON");
}
free (fallback_base);
free (auth_header);
free (base_url);
r2ai_msgs_free (temp_msgs);
return NULL;
}
char *api_url = chat_api_url (request_base, chat_api);
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 *)request_json, strlen (request_json), 0);
R_LOG_DEBUG ("Full request saved to %s", req_path);
R_LOG_DEBUG ("LLM API request data: %s", request_json);
free (req_path);
free (tmpdir);
if (r_config_get_b (core->config, "r2ai.debug")) {
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
for (int i = 0; headers[i]; i++) {
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
}
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", request_json, api_url);
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
r_strbuf_free (curl_cmd);
}
res = r2ai_http_post (core, api_url, headers, request_json, &code, NULL);
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");
free (res);
res = NULL;
request_base = fallback_base;
chat_api = R2AI_CHAT_OLLAMA;
first_request = false;
}
free (fallback_base);
free (base_url);
if (code != 200) {
R_LOG_ERROR ("OpenAI API error %d", code);
R_LOG_ERROR ("LLM API error %d", code);
if (res) {
R_LOG_ERROR ("OpenAI API error response: %s", res);
R_LOG_ERROR ("LLM API error response: %s", res);
}
free (auth_header);
free (res);
r2ai_msgs_free (temp_msgs);
return NULL;
}
// Save the response for inspection
tmpdir = r_file_tmpdir ();
char *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);
if (r_config_get_b (core->config, "r2ai.debug")) {
@@ -233,141 +415,101 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
char *res_copy = strdup (res);
RJson *jres = r_json_parse (res_copy);
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 (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
// 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 (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
// 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 (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;
}
}
}
const RJson *message_json = NULL;
if (provider_info && provider_info->api_type == R2AI_API_OLLAMA) {
// 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");
const RJson *thinking = r_json_get (message_json, "thinking");
const RJson *tool_call_id = r_json_get (message_json, "tool_call_id");
const RJson *tool_calls = r_json_get (message_json, "tool_calls");
// 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 && R_STR_ISNOTEMPTY (reasoning_content->str_value)) {
message->reasoning_content = strdup (reasoning_content->str_value);
} else if (thinking && thinking->type == R_JSON_STRING && R_STR_ISNOTEMPTY (thinking->str_value)) {
message->reasoning_content = strdup (thinking->str_value);
}
if (tool_call_id && tool_call_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tool_call_id->str_value)) {
message->tool_call_id = strdup (tool_call_id->str_value);
}
if (tool_calls && tool_calls->type == R_JSON_ARRAY && tool_calls->children.count > 0) {
message->tool_calls = r_list_new ();
if (message->tool_calls) {
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
for (size_t 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;
}
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
if (!tc) {
continue;
}
const RJson *tc_id = r_json_get (tool_call, "id");
if (tc_id && tc_id->type == R_JSON_STRING && R_STR_ISNOTEMPTY (tc_id->str_value)) {
tc->id = strdup (tc_id->str_value);
}
const RJson *function = r_json_get (tool_call, "function");
const RJson *name_json = NULL;
const RJson *arguments = NULL;
if (function && function->type == R_JSON_OBJECT) {
name_json = r_json_get (function, "name");
arguments = r_json_get (function, "arguments");
} else {
name_json = r_json_get (tool_call, "name");
arguments = r_json_get (tool_call, "arguments");
}
if (name_json && name_json->type == R_JSON_STRING && R_STR_ISNOTEMPTY (name_json->str_value)) {
tc->name = strdup (name_json->str_value);
}
if (arguments) {
if (arguments->type == R_JSON_STRING && R_STR_ISNOTEMPTY (arguments->str_value)) {
tc->arguments = strdup (arguments->str_value);
} else {
char *arguments_json = r_json_to_string (arguments);
if (arguments_json) {
tc->arguments = arguments_json;
}
}
}
if (!tc->name && !tc->arguments && !tc->id) {
r2ai_tool_call_free (tc);
continue;
}
r_list_append (message->tool_calls, tc);
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;
}
if (r_list_empty (message->tool_calls)) {
r_list_free (message->tool_calls);
message->tool_calls = NULL;
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 (r_list_empty (message->tool_calls)) {
r_list_free (message->tool_calls);
message->tool_calls = NULL;
}
}
// TODO: Handle tool calls if present?
}
}
r_json_free (jres);
@@ -381,8 +523,10 @@ R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args) {
return result;
}
free (res_copy);
free (auth_header);
free (res);
r2ai_msgs_free (temp_msgs);
return NULL;
}
+208 -122
View File
@@ -1,4 +1,4 @@
/* r2ai - Copyright 2023-2025 pancake */
/* r2ai - Copyright 2023-2026 pancake */
#include "r2ai.h"
@@ -19,6 +19,39 @@ R_API void r2aiprompt_free(R2AIPrompt *prompt) {
free (prompt);
}
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);
@@ -27,53 +60,43 @@ R_API 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;
}
@@ -145,31 +168,33 @@ static char *replace_vars(RCore *core, const char *text) {
return r_strbuf_drain (sb);
}
// Helper function to find prompt file in search directories
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) {
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);
filepath = r_str_newf ("%s/%s.r2ai.md", dir, name);
if (r_file_exists (filepath)) {
break;
}
free (filepath_txt);
free (filepath_md);
free (filepath);
filepath = NULL;
}
return filepath;
}
R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
RCore *core = cps->core;
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";
@@ -181,55 +206,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);
@@ -241,7 +346,7 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
r_str_trim (name);
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;
@@ -254,8 +359,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
@@ -314,26 +419,7 @@ R_API void r2ai_cmd_q(RCorePluginSession *cps, const char *input) {
}
R_API char *r2ai_load_prompt_text(RCore *core, const char *name) {
const char *promptdir = r_config_get (core->config, "r2ai.promptdir");
if (!promptdir || !*promptdir) {
promptdir = "~/.config/r2ai/prompts";
}
char *expanded_dir = r_file_abspath (promptdir);
char *local_prompts = r_file_abspath ("../prompts");
RList *search_dirs = r_list_newf (free);
if (r_file_is_directory (expanded_dir)) {
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;
}
RList *search_dirs = prompt_search_dirs (core);
char *filepath = find_prompt_file (search_dirs, name);
char *prompt_text = NULL;
if (filepath) {
+47 -2
View File
@@ -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,12 +50,18 @@ 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.
.TP
@@ -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,6 +112,12 @@ 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.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
@@ -99,6 +129,9 @@ 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 +166,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 +178,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 +200,4 @@ Enter REPL:
.BR r2pm (1),
.BR r2agent (1)
.SH AUTHOR
radare2 community
radare2 community
+349 -68
View File
@@ -1,15 +1,17 @@
/* 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",
@@ -18,11 +20,16 @@ static RCoreHelpMessage help_msg_r2ai = {
"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", " -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", " -s[?ijynak*]", "async task queue: list, show, interact, approve, decline, kill (see r2ai -s?)",
"r2ai", " -L", "show chat logs (See -Lj for json). Only for auto mode.",
"r2ai", " -C", "compact conversation history",
"r2ai", " -LR", "create a log report",
@@ -32,6 +39,35 @@ static RCoreHelpMessage help_msg_r2ai = {
"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;
@@ -46,15 +82,25 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
args.system_prompt = r_config_get (core->config, "r2ai.system");
}
RList *msgs = r2ai_msgs_new ();
if (args.input) {
R2AI_Message msg = { .role = "user", .content = (char *)args.input };
r2ai_msgs_add (msgs, &msg);
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;
}
args.messages = msgs;
char *content = NULL;
R2AI_ChatResponse *res = r2ai_llmcall (cps, args);
if (res) {
@@ -64,10 +110,106 @@ R_API char *r2ai(RCorePluginSession *cps, R2AIArgs args) {
r2ai_message_free ((R2AI_Message *)res->message);
free (res);
}
if (own_msgs) {
r2ai_msgs_free (msgs);
}
return content;
}
static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool recursive) {
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);
}
static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool recursive, const bool offsets) {
RCore *core = cps->core;
const char *prompt = r_config_get (core->config, "r2ai.prompt");
const char *lang = r_config_get (core->config, "r2ai.lang");
@@ -88,13 +230,28 @@ static void cmd_r2ai_d(RCorePluginSession *cps, const char *input, const bool re
RListIter *iter;
const char *cmd;
RList *refslist = 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]~$$");
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);
char *ocmd = offsets? r2ai_offset_cmd (cmd): NULL;
const char *dcmd = ocmd? ocmd: cmd;
char *dec = r_core_cmd_str (core, dcmd);
if (offsets) {
if (!offset_fallback) {
offset_fallback = strdup (dec);
}
r2ai_collect_offsets (offsetslist, dec, &offset_width);
}
r_strbuf_append (sb, "\n[BEGIN]\n");
r_strbuf_append (sb, dec);
r_strbuf_append (sb, "[END]\n");
@@ -107,32 +264,65 @@ 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);
char *dec = r_core_cmd_str_at (core, n, dcmd);
if (offsets) {
r2ai_collect_offsets (offsetslist, dec, &offset_width);
}
r_strbuf_append (sb, "\n[BEGIN]\n");
r_strbuf_append (sb, dec);
r_strbuf_append (sb, "[END]\n");
free (dec);
}
}
free (ocmd);
}
r_config_hold_restore (hold);
r_config_hold_free (hold);
r_list_free (refslist);
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 *sys = r_config_get (core->config, "r2ai.system");
char *title = r_str_newf ("-d%s%s%s", recursive? "r": "", offsets? "o": "", R_STR_ISNOTEMPTY (input)? " ": "");
if (R_STR_ISNOTEMPTY (input)) {
char *n = r_str_newf ("%s%s", title, input);
free (title);
title = n;
}
int id = r2ai_async_query (cps, title, s, sys);
r_cons_printf (core->cons, "[async] task %d queued (%s)\n", id, title);
free (title);
} else {
r_cons_printf (core->cons, "%s\n", res);
char *error = NULL;
R2AIArgs d_args = { .input = s, .error = &error, .dorag = true };
char *res = r2ai (cps, d_args);
if (error) {
R_LOG_ERROR ("%s", error);
free (error);
} else if (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);
}
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, "[r2ai]> ");
@@ -163,12 +353,20 @@ static void cmd_r2ai_repl(RCorePluginSession *cps) {
free (error);
} else 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 {
r_cons_printf (core->cons, "%s\n", 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) {
@@ -198,8 +396,7 @@ static void cmd_r2ai_R(RCorePluginSession *cps, const char *q) {
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)"));
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 {
@@ -216,25 +413,25 @@ 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);
char *error = NULL;
char *res = r2ai (cps, (R2AIArgs){ .input = q, .error = &error, .dorag = true });
free (q);
if (error) {
R_LOG_ERROR (error);
R_LOG_ERROR ("%s", error);
free (error);
} else {
r_cons_printf (core->cons, "%s\n", res);
}
free (fname);
free (res);
free (q);
}
static void cmd_r2ai_m(RCorePluginSession *cps, const char *input) {
@@ -312,16 +509,31 @@ static bool load_r2airc(RCorePluginSession *cps) {
return true;
}
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")) {
@@ -332,7 +544,17 @@ R_API 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 *system_prompt = r2ai_auto_system_prompt (cps);
char *title = r_str_newf ("-a %s", q);
int id = r2ai_async_auto (cps, title, q, system_prompt);
r_cons_printf (core->cons, "[async] task %d queued (%s)\n", id, title);
free (title);
free (system_prompt);
} 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)) {
@@ -348,19 +570,22 @@ R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
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);
@@ -379,20 +604,22 @@ R_API void cmd_r2ai(RCorePluginSession *cps, const char *input) {
}
r_vdb_result_free (rs);
}
} 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));
} else if (r_str_startswith (input, "-R")) {
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")) {
@@ -402,21 +629,29 @@ R_API 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);
free (res);
if (r_config_get_b (core->config, "r2ai.async")) {
const char *sys = r_config_get (core->config, "r2ai.system");
int id = r2ai_async_query (cps, input, input, sys);
r_cons_printf (core->cons, "[async] task %d queued\n", id);
} 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);
free (res);
}
}
}
}
@@ -519,11 +754,28 @@ 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);
@@ -543,7 +795,11 @@ 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);
@@ -565,20 +821,23 @@ 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.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);
@@ -596,12 +855,16 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
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", 5);
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);
@@ -610,17 +873,27 @@ R_IPI bool r2ai_init(RCorePluginSession *cps) {
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);
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.stream");
r_config_rm (core->config, "r2ai.system");
@@ -635,14 +908,22 @@ R_API bool r2ai_fini(RCorePluginSession *cps) {
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);
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;
}
@@ -658,7 +939,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",
@@ -674,7 +955,7 @@ 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
+62 -133
View File
@@ -15,18 +15,24 @@
#include "r_vdb.h"
#include "markdown.h"
#define R2AI_VERSION "1.3.2"
#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.4.2"
#define R2AI_DEFAULT_MODEL "gpt-oss:20b"
#define R2AI_DEFAULT_PROVIDER "ollama"
#define R2AI_DEFAULT_VECTORS 16
// HTTP configuration structure
@@ -41,7 +47,7 @@ typedef struct {
R2AI_HttpConfig config;
const char *url;
const char *data;
const char * const *headers;
const char **headers;
} HTTPRequest;
// HTTP response structure
@@ -55,8 +61,6 @@ typedef struct {
#error Your radare2 is too old
#endif
// Tool definition structure
typedef struct {
char *name;
@@ -64,6 +68,12 @@ typedef struct {
char *parameters; // JSON string of parameters/input_schema
} R2AI_Tool;
typedef struct {
char *output;
char *edited_command;
char *comment;
} R2AI_ToolResult;
// Tools array management (now just RList<R2AI_Tool *>)
typedef struct {
@@ -72,10 +82,6 @@ typedef struct {
const char *id;
} R2AI_ToolCall;
/**
* Free an R2AI_ToolCall instance
*/
R_API void r2ai_tool_call_free(R2AI_ToolCall *tc);
typedef struct {
char *type;
@@ -162,131 +168,52 @@ 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 {
RList *conversation; // Global conversation messages (from messages.c)
R2AIStats stats; // Global stats (from auto.c)
RList *tools; // Global tools instance (from tools.c)
RMarkdownTheme current_theme; // Global theme (from markdown.c)
bool theme_initialized; // Global theme flag (from markdown.c)
RMarkdown markdown; // Markdown rendering context
void *help_msg; // Global help message (from r2ai.c)
RVdb *db; // Vector database for embeddings
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 RList *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 RList *r2ai_conversation_get(R2AI_State *state);
/**
* Clear all messages in a container without freeing the container
*/
R_API void r2ai_msgs_clear(RList *msgs);
/**
* Add a message to the array
* All strings are duplicated, so caller can free their copies
*/
R_API bool r2ai_msgs_add(RList *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(RList *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(RList *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(RList *msgs, const RJson *json);
/**
* Convert messages array to JSON string
* Caller must free the returned string
*/
R_API char *r2ai_msgs_to_json(const RList *msgs);
/**
* Convert messages array to Anthropic format JSON string
* Caller must free the returned string
*/
R_API char *r2ai_msgs_to_anthropic_json(const RList *msgs);
/**
* Free a messages array and all associated data
*/
R_API void r2ai_msgs_free(RList *msgs);
/**
* Free the conversation (call during plugin unload)
*/
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 bool r2ai_msgs_add_tool_call(RList *msgs, const R2AI_ToolCall *tc);
R_API bool r2ai_msgs_from_response(RList *msgs, const char *json_str);
R_API bool r2ai_msgs_from_json(RList *msgs, const RJson *json);
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);
/**
* Delete the last N messages from the message array
* If n <= 0, defaults to deleting just the last message
*/
R_API void r2ai_message_fini(R2AI_Message *msg);
R_API void r2ai_delete_last_messages(RList *messages, int n);
/**
* Get the tools instance from state
* Returns a pointer to the tools RList
*/
// tools
R_API RList *r2ai_get_tools(RCore *core, R2AI_State *state);
/**
* Parse OpenAI format tools JSON into internal tools RList
* Caller must free the result with r2ai_tools_free
*/
R_API RList *r2ai_tools_parse(const char *tools_json);
/**
* Convert tools RList to OpenAI format JSON
* Caller must free the returned string
*/
R_API char *r2ai_tools_to_openai_json(const RList *tools);
/**
* Convert tools RList to Anthropic format JSON
* Caller must free the returned string
*/
R_API char *r2ai_tools_to_anthropic_json(const RList *tools);
/**
* Free a tools RList and all associated data
*/
R_API void r2ai_tools_free(RList *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, char **comment_out);
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);
/**
* Execute r_core_cmd_str with slim mode if enabled
@@ -306,7 +233,7 @@ R_API char *r2ai_cmdstr(RCore *core, const char *cmd);
* @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
@@ -317,19 +244,20 @@ 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(const char *json, char **error);
// openai
R_IPI R2AI_ChatResponse *r2ai_openai(RCorePluginSession *cps, R2AIArgs args);
@@ -339,6 +267,7 @@ 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_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);
@@ -349,7 +278,7 @@ R_IPI R2AI_ChatResponse *r2ai_llmcall(RCorePluginSession *cps, R2AIArgs args);
R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args);
R_IPI void cmd_r2ai_c(RCorePluginSession *cps);
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps);
R_IPI void cmd_r2ai_logs(RCorePluginSession *cps, const char *flags);
R_IPI void cmd_r2ai_lr(RCorePluginSession *cps);
/**
@@ -357,20 +286,20 @@ R_IPI void cmd_r2ai_lr(RCorePluginSession *cps);
*/
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
+83 -2
View File
@@ -2,14 +2,74 @@
#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_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 */
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;
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;
@@ -19,8 +79,15 @@ typedef struct {
} R2AIProvider;
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 >= 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 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);
@@ -28,4 +95,18 @@ 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
+12 -13
View File
@@ -1,4 +1,4 @@
/* r2ai - Copyright 2023-2025 pancake */
/* r2ai - Copyright 2023-2026 pancake */
#define R_LOG_ORIGIN "rawtools"
#include "r2ai.h"
@@ -82,11 +82,12 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
}
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.";
const char *user_system = R_STR_ISNOTEMPTY (args.system_prompt)
? args.system_prompt
: r_config_get (core->config, "r2ai.system");
if (R_STR_ISEMPTY (user_system)) {
user_system = "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;
@@ -103,13 +104,12 @@ 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);
char *enhanced_system_prompt = r2ai_claw_system_prompt (base_prompt);
free (base_prompt);
// Temporarily modify args to use enhanced prompt and no tools (since we're using prompt engineering)
R2AIArgs rawtools_args = args;
rawtools_args.system_prompt = enhanced_system_prompt;
@@ -240,7 +240,6 @@ R2AI_ChatResponse *r2ai_rawtools_llmcall(RCorePluginSession *cps, R2AIArgs args)
// 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
+40
View File
@@ -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 ]
+187 -215
View File
@@ -1,6 +1,6 @@
/* Copyright r2ai - 2023-2025 - pancake */
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.h"
#include "r2ai_priv.h"
R_API char *strip_command_comment(const char *input, char **comment_out) {
const char *hash_pos = strchr (input, '#');
@@ -67,11 +67,15 @@ R_API RList *r2ai_get_tools(RCore *core, R2AI_State *state) {
}
if (!state->tools) {
state->tools = r_list_newf (NULL);
r_list_append (state->tools, (void *)&r2cmd_tool);
// Only add execute_js tool if enabled in config
if (r_config_get_b (core->config, "r2ai.auto.usejs")) {
r_list_append (state->tools, (void *)&qjs_tool);
}
}
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;
}
@@ -266,34 +270,42 @@ static char *compose_command_with_comment(const char *command, const char *comme
return r_str_newf ("%s # %s", command, comment);
}
R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool verbose, char **edited_command, char **comment_out) {
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));
}
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\" }");
}
if (edited_command) {
*edited_command = NULL;
result.output = strdup ("{ \"res\":\"No command in tool call arguments\" }");
return result;
}
char *comment = NULL;
char *command = strip_command_comment (command_json->str_value, &comment);
if (!command) {
free (comment);
return strdup ("{ \"res\":\"Command is NULL\" }");
result.output = strdup ("{ \"res\":\"Command is NULL\" }");
return result;
}
if (r_str_startswith (command, "r2 ")) {
free (command);
free (comment);
if (comment_out) {
*comment_out = NULL;
}
return strdup ("{ \"res\":\"You are already in r2!\" }");
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;
@@ -302,12 +314,13 @@ R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool verbose, char **edited_com
if (!input_command) {
free (command);
free (comment);
return strdup ("{ \"res\":\"Failed to prepare command\" }");
result.output = strdup ("{ \"res\":\"Failed to prepare command\" }");
return result;
}
bool is_multiline = strchr (input_command, '\n') != NULL;
if (is_multiline) {
r_cons_editor (core->cons, NULL, input_command);
r2ai_cons_editor (core->cons, NULL, input_command);
} else {
r_cons_newline (core->cons);
r_cons_readpush (core->cons, input_command, strlen (input_command));
@@ -319,10 +332,8 @@ R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool verbose, char **edited_com
free (input_command);
free (command);
free (comment);
if (comment_out) {
*comment_out = NULL;
}
return strdup ("R2AI_SIGINT");
result.output = strdup ("R2AI_SIGINT");
return result;
}
if (R_STR_ISNOTEMPTY (readline_result)) {
free (input_command);
@@ -343,9 +354,7 @@ R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool verbose, char **edited_com
free (input_command);
}
if (edited_command) {
*edited_command = strdup (command);
}
result.edited_command = strdup (command);
R_LOG_DEBUG ("Edited command: %s", command);
if (verbose) {
@@ -363,10 +372,9 @@ R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool verbose, char **edited_com
if (!json_cmd) {
free (command);
free (comment);
if (comment_out) {
*comment_out = NULL;
}
return strdup ("{ \"res\":\"Failed to create JSON command\" }");
r2ai_tool_result_fini (&result);
result.output = strdup ("{ \"res\":\"Failed to create JSON command\" }");
return result;
}
char *cmd_output = r2ai_cmdstr (core, json_cmd);
@@ -375,19 +383,14 @@ R_API char *r2ai_r2cmd(RCore *core, RJson *args, bool verbose, char **edited_com
if (!cmd_output) {
free (comment);
if (comment_out) {
*comment_out = NULL;
}
return strdup ("{ \"res\":\"Command returned no output or failed\" }");
r2ai_tool_result_fini (&result);
result.output = strdup ("{ \"res\":\"Command returned no output or failed\" }");
return result;
}
if (comment_out) {
*comment_out = comment;
} else {
free (comment);
}
return cmd_output;
result.output = cmd_output;
result.comment = comment;
return result;
}
/**
@@ -416,22 +419,22 @@ R_API char *r2ai_cmdstr(RCore *core, const char *cmd) {
}
// qjs function implementation
R_API char *r2ai_qjs(RCore *core, RJson *args, bool verbose, char **edited_script_out) {
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\" }");
}
if (edited_script_out) {
*edited_script_out = 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;
@@ -445,7 +448,7 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool verbose, char **edited_scrip
if (is_multiline) {
// Use editor for multi-line scripts
edited_script = strdup (script);
r_cons_editor (core->cons, NULL, edited_script);
r2ai_cons_editor (core->cons, NULL, edited_script);
script = edited_script;
} else {
// For single-line scripts, push the script to input buffer
@@ -458,7 +461,8 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool verbose, char **edited_scrip
// 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");
result.output = strdup ("R2AI_SIGINT");
return result;
}
// Process the result
@@ -473,16 +477,11 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool verbose, char **edited_scrip
}
}
if (edited_script_out) {
char *dup_script = strdup (script);
if (dup_script) {
*edited_script_out = dup_script;
}
}
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);
@@ -493,16 +492,20 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool verbose, char **edited_scrip
// Free edited_script after we're done using it
free (edited_script);
R_LOG_DEBUG ("Payload length: %d", (int)strlen (payload));
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);
@@ -513,183 +516,152 @@ R_API char *r2ai_qjs(RCore *core, RJson *args, bool verbose, char **edited_scrip
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, char **comment_out) {
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 verbose = r_config_get_b (core->config, "r2ai.auto.verbose");
char *tool_result = NULL;
if (strcmp (tool_name, "r2cmd") == 0) {
tool_result = r2ai_r2cmd (core, args_json, verbose, edited_command, comment_out);
result = r2ai_r2cmd (core, args_json, verbose);
} else if (strcmp (tool_name, "execute_js") == 0) {
result = r2ai_qjs (core, state, args_json, verbose);
} else {
if (comment_out) {
*comment_out = NULL;
}
if (strcmp (tool_name, "execute_js") == 0) {
tool_result = r2ai_qjs (core, args_json, verbose, edited_command);
} 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
return tool_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");
return tool_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 (verbose) {
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 -1
View File
@@ -1,4 +1,4 @@
/* r2ai - MIT - Copyright 2024-2025 pancake */
/* r2ai - MIT - Copyright 2024-2026 pancake */
#include "r2ai.h"
+1 -1
View File
@@ -1,4 +1,4 @@
/* r2ai - MIT - Copyright 2024-2025 pancake */
/* r2ai - MIT - Copyright 2024-2026 pancake */
#include <r_util.h>
+409
View File
@@ -0,0 +1,409 @@
/* Copyright r2ai - 2023-2026 - pancake */
#include "r2ai.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;
}
R_IPI R2AI_ChatResponse *r2ai_vertex_gemini(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
char **error = args.error;
char *project = r_sys_getenv ("GOOGLE_CLOUD_PROJECT");
char *region = r_sys_getenv ("GOOGLE_CLOUD_REGION");
if (R_STR_ISEMPTY (project) || R_STR_ISEMPTY (region)) {
if (error) {
*error = strdup ("Set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION env vars first");
}
free (project);
free (region);
return NULL;
}
const char *model_name = R_STR_ISNOTEMPTY (args.model)
? args.model
: r_config_get (core->config, "r2ai.model");
RList *temp_msgs = r2ai_msgs_new ();
if (!temp_msgs) {
if (error) {
*error = strdup ("Failed to create temporary messages array");
}
free (project);
free (region);
return NULL;
}
const char *system_prompt = NULL;
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
system_prompt = args.system_prompt;
} else {
system_prompt = r_config_get (core->config, "r2ai.system");
}
RListIter *iter;
R2AI_Message *msg;
if (args.messages) {
r_list_foreach (args.messages, iter, msg) {
r2ai_msgs_add (temp_msgs, msg);
}
} else {
R_LOG_WARN ("No messages");
}
if (error) {
*error = NULL;
}
char *auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
const char *headers[] = {
"Content-Type: application/json",
auth_header,
NULL
};
char *vertex_url = r_str_newf (
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s"
"/publishers/google/models/%s:generateContent",
region,
project,
region,
model_name);
PJ *pj = pj_new ();
pj_o (pj);
if (R_STR_ISNOTEMPTY (system_prompt)) {
pj_ko (pj, "systemInstruction");
pj_ka (pj, "parts");
pj_o (pj);
pj_ks (pj, "text", system_prompt);
pj_end (pj);
pj_end (pj);
pj_end (pj);
}
pj_ka (pj, "contents");
r_list_foreach (temp_msgs, iter, msg) {
pj_o (pj);
const char *role = "user";
if (!strcmp (msg->role, "assistant")) {
role = "model";
}
pj_ks (pj, "role", role);
pj_ka (pj, "parts");
pj_o (pj);
pj_ks (pj, "text", msg->content? msg->content: "");
pj_end (pj);
pj_end (pj);
pj_end (pj);
}
pj_end (pj);
pj_ko (pj, "generationConfig");
if (args.max_tokens > 0) {
pj_kn (pj, "maxOutputTokens", args.max_tokens);
}
if (args.temperature > 0) {
pj_kd (pj, "temperature", args.temperature);
}
pj_end (pj);
pj_end (pj);
char *request_json = pj_drain (pj);
char *tmpdir = r_file_tmpdir ();
char *req_path = r_file_new (tmpdir, "r2ai_vertex_gemini_request.json", NULL);
r_file_dump (req_path, (const ut8 *)request_json, -1, 0);
R_LOG_DEBUG ("Full request saved to %s", req_path);
free (req_path);
free (tmpdir);
R_LOG_DEBUG ("Vertex Gemini API request data: %s", request_json);
int code = 0;
char *response = r2ai_http_post (core, vertex_url, headers, request_json, &code, NULL);
free (request_json);
free (vertex_url);
free (auth_header);
free (project);
free (region);
if (code != 200) {
R_LOG_ERROR ("Vertex Gemini API error %d", code);
if (response) {
R_LOG_ERROR ("Vertex Gemini API error response: %s", response);
}
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
tmpdir = r_file_tmpdir ();
char *res_path = r_file_new (tmpdir, "r2ai_vertex_gemini_response.json", NULL);
r_file_dump (res_path, (const ut8 *)response, -1, 0);
R_LOG_DEBUG ("Vertex Gemini API response saved to %s", res_path);
free (res_path);
free (tmpdir);
RJson *jres = r_json_parse (response);
if (!jres) {
if (error) {
*error = strdup ("Failed to parse Vertex Gemini response JSON");
}
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
const RJson *candidates = r_json_get (jres, "candidates");
if (!candidates || candidates->type != R_JSON_ARRAY || candidates->children.count == 0) {
r_json_free (jres);
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
const RJson *candidate = r_json_item (candidates, 0);
const RJson *content = r_json_get (candidate, "content");
const RJson *parts = r_json_get (content, "parts");
if (!parts || parts->type != R_JSON_ARRAY || parts->children.count == 0) {
r_json_free (jres);
free (response);
r2ai_msgs_free (temp_msgs);
return NULL;
}
const RJson *part = r_json_item (parts, 0);
const RJson *text = r_json_get (part, "text");
char *response_text = NULL;
if (text && text->type == R_JSON_STRING) {
response_text = strdup (text->str_value);
}
R2AI_Usage *usage = R_NEW0 (R2AI_Usage);
const RJson *usage_meta = r_json_get (jres, "usageMetadata");
if (usage_meta && usage_meta->type == R_JSON_OBJECT) {
const RJson *pt = r_json_get (usage_meta, "promptTokenCount");
const RJson *ct = r_json_get (usage_meta, "candidatesTokenCount");
const RJson *tt = r_json_get (usage_meta, "totalTokenCount");
if (pt && pt->type == R_JSON_INTEGER) {
usage->prompt_tokens = pt->num.u_value;
}
if (ct && ct->type == R_JSON_INTEGER) {
usage->completion_tokens = ct->num.u_value;
}
if (tt && tt->type == R_JSON_INTEGER) {
usage->total_tokens = tt->num.u_value;
}
}
r_json_free (jres);
free (response);
r2ai_msgs_free (temp_msgs);
if (!response_text) {
free (usage);
return NULL;
}
R2AI_Message *message = R_NEW0 (R2AI_Message);
message->role = strdup ("assistant");
message->content = response_text;
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
result->message = message;
result->usage = usage;
return result;
}
/* Vertex Anthropic uses anthropic_version in body instead of header,
* and omits the model field from the request body */
R_IPI R2AI_ChatResponse *r2ai_vertex_anthropic(RCorePluginSession *cps, R2AIArgs args) {
RCore *core = cps->core;
const char *model = args.model;
char **error = args.error;
RList *tools = args.tools;
RList *messages_input = args.messages;
char *project = r_sys_getenv ("GOOGLE_CLOUD_PROJECT");
char *region = r_sys_getenv ("GOOGLE_CLOUD_REGION");
if (R_STR_ISEMPTY (project) || R_STR_ISEMPTY (region)) {
if (error) {
*error = strdup ("Set GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_REGION env vars first");
}
free (project);
free (region);
return NULL;
}
const char *model_name = R_STR_ISNOTEMPTY (model)
? model
: r_config_get (core->config, "r2ai.model");
if (error) {
*error = NULL;
}
char *auth_header = r_str_newf ("Authorization: Bearer %s", args.api_key);
const char *headers[] = {
"Content-Type: application/json",
auth_header,
NULL
};
char *vertex_url = r_str_newf (
"https://%s-aiplatform.googleapis.com/v1/projects/%s/locations/%s"
"/publishers/anthropic/models/%s:rawPredict",
region,
project,
region,
model_name);
const char *system_message = NULL;
if (R_STR_ISNOTEMPTY (args.system_prompt)) {
system_message = args.system_prompt;
} else {
system_message = r_config_get (core->config, "r2ai.system");
}
char *messages_json = NULL;
if (messages_input && !r_list_empty (messages_input)) {
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);
free (vertex_url);
free (project);
free (region);
return NULL;
}
} else {
if (error) {
*error = strdup ("No input or messages provided");
}
free (auth_header);
free (vertex_url);
free (project);
free (region);
return NULL;
}
char *anthropic_tools_json = NULL;
if (tools && !r_list_empty (tools)) {
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
}
PJ *pj = pj_new ();
pj_o (pj);
pj_ks (pj, "anthropic_version", "vertex-2023-10-16");
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);
}
if (system_message) {
pj_ks (pj, "system", system_message);
}
pj_k (pj, "messages");
pj_raw (pj, messages_json);
if (anthropic_tools_json) {
pj_k (pj, "tools");
pj_raw (pj, anthropic_tools_json);
free (anthropic_tools_json);
}
pj_kb (pj, "stream", false);
pj_end (pj);
char *data = pj_drain (pj);
free (messages_json);
char *tmpdir = r_file_tmpdir ();
char *req_path = r_file_new (tmpdir, "r2ai_vertex_anthropic_request.json", NULL);
r_file_dump (req_path, (const ut8 *)data, -1, 0);
R_LOG_DEBUG ("Full request saved to %s", req_path);
free (req_path);
free (tmpdir);
R_LOG_DEBUG ("Vertex Anthropic API request data: %s", data);
int code = 0;
char *res = r2ai_http_post (core, vertex_url, headers, data, &code, NULL);
free (data);
free (auth_header);
free (vertex_url);
free (project);
free (region);
if (!res || code != 200) {
R_LOG_ERROR ("Vertex Anthropic API error %d", code);
if (error && res) {
*error = strdup (res);
} else if (error) {
*error = strdup ("Failed to get response from Vertex Anthropic API");
}
free (res);
return NULL;
}
tmpdir = r_file_tmpdir ();
char *res_path = r_file_new (tmpdir, "r2ai_vertex_anthropic_response.json", NULL);
r_file_dump (res_path, (const ut8 *)res, -1, 0);
R_LOG_DEBUG ("Vertex Anthropic API response saved to %s", res_path);
free (res_path);
free (tmpdir);
R_LOG_DEBUG ("Vertex Anthropic API response: %s", res);
R2AI_ChatResponse *result = r2ai_anthropic_parse_response (res, error);
free (res);
return result;
}
+394
View File
@@ -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;
}
+4
View File
@@ -0,0 +1,4 @@
all:
r2r -u db/extras
.PHONY: all
+33
View File
@@ -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