mirror of
https://github.com/radareorg/r2ai.git
synced 2026-11-10 04:01:27 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83b96f7a3e | ||
|
|
e2ee8cdbb6 | ||
|
|
8630e95d83 | ||
|
|
e07293e40f | ||
|
|
415c546f92 |
No files matched your search
@@ -1,8 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- buildsystem
|
||||
+44
-15
@@ -1,5 +1,8 @@
|
||||
name: CI and Release
|
||||
|
||||
env:
|
||||
R2V: 6.0.4
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -23,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Meson/Ninja
|
||||
if: matrix.build_system == 'meson'
|
||||
@@ -31,9 +34,38 @@ jobs:
|
||||
python -m pip install --upgrade pip
|
||||
pip install meson ninja
|
||||
|
||||
- name: Install radare2
|
||||
id: r2
|
||||
uses: radareorg/github-actions@v2
|
||||
- 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: Build with Make
|
||||
if: matrix.build_system == 'make'
|
||||
@@ -43,7 +75,7 @@ jobs:
|
||||
if: matrix.build_system == 'meson' && matrix.os != 'windows-latest'
|
||||
run: |
|
||||
cd src
|
||||
meson setup build || meson configure build
|
||||
meson setup build -Dr2_prefix=${{ secrets.R2_PREFIX }} || meson configure build
|
||||
meson compile -C build
|
||||
|
||||
- name: Build with Meson (Windows)
|
||||
@@ -51,7 +83,7 @@ jobs:
|
||||
shell: powershell
|
||||
run: |
|
||||
cd src
|
||||
$prefix = "${{ steps.r2.outputs.prefix }}"
|
||||
$prefix = 'C:\radare2'
|
||||
meson setup build --backend ninja --prefix="$prefix" -Dr2_prefix="$prefix"
|
||||
if ($LASTEXITCODE -ne 0) { meson configure build }
|
||||
meson compile -C build
|
||||
@@ -61,11 +93,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p release
|
||||
# Collect common binary artefact names produced by Make/Meson
|
||||
# Include both "r2ai.*" and "libr2ai.*" variants for DLL/dylib/so on all platforms.
|
||||
find src -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
|
||||
# Also check top-level build output directories
|
||||
find . -maxdepth 3 -type f \( -iname "r2ai.dll" -o -iname "libr2ai.dll" -o -iname "r2ai.dylib" -o -iname "libr2ai.dylib" -o -iname "r2ai.so" -o -iname "libr2ai.so" -o -iname "r2ai.exe" \) -exec cp -f {} release/ \; || true
|
||||
cp -rf src/r2ai.dll src/r2ai.exe src/r2ai src/r2ai.dylib src/r2ai.so release/ || true
|
||||
cp -rf src/build/r2ai src/build/r2ai.dylib src/build/r2ai.so release/ || true
|
||||
cp -f decai/decai.r2.js release/ || true
|
||||
if command -v zip >/dev/null 2>&1; then
|
||||
zip -j "release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip" release/*
|
||||
@@ -75,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Upload artifacts (tags only)
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.os }}-${{ matrix.build_system }}-binary
|
||||
path: release/r2ai-${{ github.ref_name }}-${{ matrix.os }}-${{ matrix.build_system }}.zip
|
||||
@@ -95,7 +124,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Verify tag is on master
|
||||
run: |
|
||||
@@ -106,7 +135,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-artifacts
|
||||
|
||||
@@ -114,7 +143,7 @@ jobs:
|
||||
run: cp decai/decai.r2.js release-artifacts/
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
release-artifacts/**/*.zip
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
MODELS=gpt-oss:20b gemma3:4b gemma3:12b
|
||||
MODELS+=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
|
||||
MODELS=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
|
||||
LANGS=Dart Perl Ruby Javascript PHP C
|
||||
|
||||
all:
|
||||
for a in ${MODELS} ; do \
|
||||
declare -a ARGS; \
|
||||
for l in ${LANGS} ; do \
|
||||
ARGS+=("echo \"decai -e model=$$a;decai -e lang=$$l;s 0x100000924;af;decai -d\" | r2 -q /bin/ls"); \
|
||||
done ; \
|
||||
./run.sh "$${ARGS[@]}" > ${a//\//_}.html ; done
|
||||
+3
-14
@@ -50,16 +50,8 @@ def stt(seconds, lang):
|
||||
return
|
||||
tts("(r2ai) listening for 5s... ", "digues?", lang)
|
||||
print(f"DEVICE IS {device}")
|
||||
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
|
||||
os.system("rm -f .audiomsg.wav")
|
||||
rc = os.system(f"ffmpeg -f avfoundation -t 5 -i '{device}' .audiomsg.wav > /dev/null 2>&1")
|
||||
if rc != 0:
|
||||
tts("(r2ai)", "cannot record from microphone. missing permissions in terminal?", lang)
|
||||
return
|
||||
@@ -68,10 +60,7 @@ def stt(seconds, lang):
|
||||
result = model.transcribe(".audiomsg.wav")
|
||||
else:
|
||||
result = model.transcribe(".audiomsg.wav", language=lang)
|
||||
try:
|
||||
os.remove(".audiomsg.wav")
|
||||
except OSError:
|
||||
pass
|
||||
os.system("rm -f .audiomsg.wav")
|
||||
tts("(r2ai)", "ok", lang)
|
||||
text = result["text"].strip()
|
||||
if text == "you":
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
recursive-include r2ai/ui *.tcss
|
||||
@@ -1,6 +1,4 @@
|
||||
TARGETS=all clean mrproper user-install install user-uninstall uninstall fmt indent
|
||||
|
||||
TARGETS=all clean mrproper user-install install user-uninstall uninstall
|
||||
$(TARGETS):
|
||||
$(MAKE) -C src $@
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
@@ -19,17 +19,14 @@
|
||||
|
||||
This repository contains two plugins for radare2:
|
||||
|
||||
* **r2ai** - native [AI plugin](./src/README.md) for radare2
|
||||
* **decai** - r2js plugin with special [focus on decompilation](./decai/README.md)
|
||||
* **r2ai** - native plugin for radare2
|
||||
* **decai** - r2js plugin with special focus on decompilation
|
||||
|
||||
If you are looking to use radare2 with other agents via MCP:
|
||||
|
||||
* **r2mcp** - the [official radare2 mcp](https://github.com/radare2/radare2-mcp)
|
||||
* **r2copilot** - the mcp with focus on CTF [r2copilot](https://github.com/darallium/r2-copilot)
|
||||
|
||||
If you are looking for a radare2 focused autonomous agent:
|
||||
* **r2agent** - automate radare2 workflows through autonomous agents. [r2agent](https://github.com/nitanmarcel/r2agent)
|
||||
|
||||
## Features
|
||||
|
||||
* Configure different roles and customize prompts
|
||||
@@ -40,21 +37,12 @@ If you are looking for a radare2 focused autonomous agent:
|
||||
* RAG markdown, code or textfiles using its native vector database
|
||||
* Embed the output of an r2 command and resolve questions on the given data
|
||||
|
||||
## User defined Prompts
|
||||
## Practical Examples
|
||||
|
||||
```console
|
||||
[0x00000000]> r2ai -q
|
||||
explain: Explain the current function -
|
||||
devices: Find and explain devices used -
|
||||
libs: Group imports by Libraries -
|
||||
varnames: Better variable names -
|
||||
autoname: Automatically suggest a better name for this function -
|
||||
vulns: Find vulnerabilities or bugs in the current function -
|
||||
signature: Suggest an improved function signature -
|
||||
dlopen: List libraries loaded with dlopen - Some libraries are loaded
|
||||
decompile: Augmented decompilation based on LLM -
|
||||
[0x00000000]>
|
||||
```
|
||||
* Enhanced decompilation with `r2ai -d`
|
||||
* Autoname functions with `r2ai -n`
|
||||
* Explain function with `r2ai -x`
|
||||
* Find vulnerabilities with `r2ai -V`
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -70,46 +58,39 @@ $ r2pm -Uci decai
|
||||
- Adds the **r2ai** command to the radare2 shell: `r2 -qc r2ai`
|
||||
- You can also run the wrapper in $PATH: `r2pm -r r2ai`
|
||||
|
||||
Drop your API keys in environment variables or use the configuration file:
|
||||
Drop your API keys in environment variables or files in your home:
|
||||
|
||||
```console
|
||||
$ export ANTHROPIC_API_KEY=sk-ant-api03-CENSORED
|
||||
$ cat ~/.r2ai.anthropic-key
|
||||
sk-ant-api03-CENSORED
|
||||
$ export OPENAI_API_KEY=sk-proj-6rlSPS-zN1v...
|
||||
```
|
||||
|
||||
Or edit the api keys file `~/.config/r2ai/apikeys.txt` run:
|
||||
| AI | API key |
|
||||
| --------- | -------------------------- |
|
||||
| OpenAI | `$HOME/.r2ai.openai-key` |
|
||||
| Gemini | `$HOME/.r2ai.gemini-key` |
|
||||
| Anthropic | `$HOME/.r2ai.anthropic-key` |
|
||||
| Mistral | `$HOME/.r2ai.mistral-key` |
|
||||
|
||||
```console
|
||||
$ r2ai -K
|
||||
```
|
||||
|
||||
## Saving settings
|
||||
|
||||
You may customize and save your configuration settings using your OS's default settings file (e.g `~/.radare2rc` on Linux).
|
||||
For example, the following configuration sets Claude 3.7 by default, with max output tokens to 64000.
|
||||
|
||||
|
||||
```console
|
||||
$ r2ai -E
|
||||
```
|
||||
|
||||
then you can type the commands you want to run when the r2ai plugin is loaded:
|
||||
|
||||
```console
|
||||
r2ai -e api=anthropic
|
||||
r2ai -e model=claude-3-7-sonnet-20250219
|
||||
r2ai -e max_tokens=64000
|
||||
```
|
||||
|
||||
## Further Reading
|
||||
## Documentation
|
||||
|
||||
* There's [a chapter](https://book.rada.re/plugins/r2ai.html) in the official r2book
|
||||
* Cryptax on [lmstudio+gptoss](https://cryptax.medium.com/r2ai-with-lmstudio-and-gpt-oss-08efa5ea2476) blog post
|
||||
* Malware analysis [with r2ai](https://arxiv.org/pdf/2504.07574) by Cryptax and Daniel Nakov
|
||||
* Analysis of [Linux/Trigona ransomware](https://cryptax.medium.com/linux-trigona-analysis-with-r2ai-3e2bd1815e52), [Linux/Prometei botnet](https://cryptax.medium.com/reversing-a-prometei-botnet-binary-with-r2-and-ai-part-one-3cdb3dc6ffab) and [W32/SkyAI](https://cryptax.medium.com/w32-skyai-uses-ai-so-do-i-d33f04d63534with) with r2ai
|
||||
* Malware analysis [with r2ai](https://github.com/cryptax/talks/blob/master/BSidesKristiansand-2025/r2ai.pdf) by Cryptax
|
||||
|
||||
## Videos
|
||||
|
||||
- [Solving a crackme](https://infosec.exchange/@radareorg/111946255058894583)
|
||||
- [De-obfuscation of malware Linux/Ladvix](https://asciinema.org/a/724126)
|
||||
- [Analysis of the /fast option inside Linux/Trigona ransomware](https://asciinema.org/a/pBPEaJhp6cunWSKFpBUDTgPt4)
|
||||
- https://infosec.exchange/@radareorg/111946255058894583
|
||||
+1
-4
@@ -1,6 +1,3 @@
|
||||
#!/bin/sh
|
||||
vim decai/src/constants.ts
|
||||
make -C decai/src
|
||||
vim src/r2ai.h
|
||||
vim src/meson.build
|
||||
vim src/r2ai.c
|
||||
vim VERSION
|
||||
@@ -1,13 +0,0 @@
|
||||
# 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
|
||||
+6
-5
@@ -1,16 +1,17 @@
|
||||
PD=$(shell r2 -H R2_USER_PLUGINS)
|
||||
SPD=$(shell r2 -H R2_LIBR_PLUGINS)
|
||||
BF=/bin/dd
|
||||
#BF=/bin/dd
|
||||
BF=a.out
|
||||
|
||||
all:
|
||||
@echo "Run: 'make user-install' to install it in your home"
|
||||
@echo "Run: 'make -C src' to recompile the r2.js"
|
||||
@echo "Run: make user-install"
|
||||
|
||||
test:
|
||||
@echo 'You can now use 'decai' command'
|
||||
r2 -i decai.r2.js -c 'decai -e deterministic=true' -caf $(BF)
|
||||
|
||||
fmt indent:
|
||||
$(MAKE) -C src fmt
|
||||
deno fmt
|
||||
|
||||
user-install:
|
||||
mkdir -p "$(PD)"
|
||||
@@ -26,4 +27,4 @@ uninstall:
|
||||
make user-uninstall PD=$(SPD)
|
||||
|
||||
vs open:
|
||||
open -a "Visual Studio Code" src
|
||||
open -a "Visual Studio Code" .
|
||||
@@ -37,7 +37,6 @@ 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
|
||||
@@ -60,7 +59,6 @@ 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
|
||||
@@ -98,10 +96,6 @@ 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=?`:
|
||||
|
||||
```
|
||||
@@ -112,14 +106,6 @@ 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
|
||||
|
||||
+1241
-58
File diff suppressed because it is too large.
Load diff
@@ -1,18 +0,0 @@
|
||||
PLUGIN_NAME=decai
|
||||
R2F_C_FLAGS=-c -S
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
r2pm -r r2frida-compile $(R2F_C_FLAGS) -o ../$(PLUGIN_NAME).r2.js main.ts
|
||||
|
||||
test: build
|
||||
r2 -i ../$(PLUGIN_NAME).r2.js -q -c 'af;decai -d' /bin/ls
|
||||
|
||||
fmt:
|
||||
deno fmt
|
||||
|
||||
clean:
|
||||
rm -rf out node_modules ../$(PLUGIN_NAME).r2.js
|
||||
|
||||
.PHONY: all build test clean
|
||||
@@ -1,53 +0,0 @@
|
||||
import { API_KEYS_PATH, DECAI_CONFIG_DIR } from "./constants";
|
||||
import { providerRegistry } from "./providers";
|
||||
import { ApiKeyResult } from "./types";
|
||||
import { ensurePath, fileExists, parseEnvLikeString } from "./utils";
|
||||
|
||||
function getProviderEnvMap(): Record<string, string> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(providerRegistry)
|
||||
.filter(([, v]) => v.authKey)
|
||||
.map(([k, v]) => [k, v.authKey!]),
|
||||
);
|
||||
}
|
||||
|
||||
export function getApiKey(provider: string, envvar: string): ApiKeyResult {
|
||||
const keyEnv = r2.cmd("'%" + envvar).trim();
|
||||
if (!keyEnv.includes("=") && keyEnv !== "") {
|
||||
return [keyEnv.trim(), null, "env"];
|
||||
}
|
||||
|
||||
const providerLower = provider.toLowerCase();
|
||||
const keysPath = API_KEYS_PATH;
|
||||
|
||||
if (fileExists(keysPath)) {
|
||||
const keyFile = r2.cmd("'cat " + keysPath);
|
||||
const kv = parseEnvLikeString(keyFile);
|
||||
if (providerLower in kv) {
|
||||
return [kv[providerLower], null, "txt"];
|
||||
}
|
||||
}
|
||||
|
||||
const keyPath = "~/.r2ai." + providerLower + "-key";
|
||||
if (fileExists(keyPath)) {
|
||||
const keyFile = r2.cmd("'cat " + keyPath);
|
||||
return keyFile === ""
|
||||
? [null, "Cannot read " + keyPath, "no"]
|
||||
: [keyFile.trim(), null, "file"];
|
||||
}
|
||||
|
||||
return [null, "Not available", "nope"];
|
||||
}
|
||||
|
||||
export function editApiKeys(): void {
|
||||
ensurePath(DECAI_CONFIG_DIR, API_KEYS_PATH);
|
||||
r2.cmd("'ed " + API_KEYS_PATH);
|
||||
}
|
||||
|
||||
export function listApiKeys(): void {
|
||||
Object.entries(getProviderEnvMap()).forEach(([key, env]) => {
|
||||
const status = getApiKey(key, env)[2];
|
||||
console.log(status, "\t", key);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,568 +0,0 @@
|
||||
import { AutoReply } from "./types";
|
||||
import { state } from "./state";
|
||||
import { COMMAND, DEFAULT_PROMPT, HELP_TEXT, VERSION } from "./constants";
|
||||
import {
|
||||
b64,
|
||||
filterResponse,
|
||||
trimAnsi,
|
||||
trimDown,
|
||||
trimJson,
|
||||
} from "./utils";
|
||||
import { editApiKeys, listApiKeys } from "./apiKeys";
|
||||
import { editRcConfig, evalConfig, listAllConfig } from "./config";
|
||||
import { r2ai } from "./r2ai";
|
||||
|
||||
function withColorDisabled<T>(fn: () => T): T {
|
||||
const origColor = r2.cmd("e scr.color");
|
||||
r2.cmd("e scr.color=0");
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
r2.cmd("e scr.color=" + origColor);
|
||||
}
|
||||
}
|
||||
|
||||
export function showHelp(): void {
|
||||
let helpmsg = "";
|
||||
const msg = (m: string) => (helpmsg += " " + COMMAND + " " + m + "\n");
|
||||
|
||||
helpmsg += "Usage: " + COMMAND + " (-h) ...\n";
|
||||
helpmsg += "Version: " + VERSION + "\n";
|
||||
msg("-a [query] - solve query with auto mode");
|
||||
msg("-b [url] - set base URL (alias for decai -e baseurl)");
|
||||
msg("-d [f1 ..] - decompile given functions");
|
||||
msg("-dd [..] - same as above, but ignoring cache");
|
||||
msg("-dD [query] - decompile current function with given extra query");
|
||||
msg("-dr - decompile function and its called ones (recursive)");
|
||||
msg("-e - display and change eval config vars");
|
||||
msg("-E - edit and run decai.txt");
|
||||
msg("-h - show this help");
|
||||
msg("-H - help setting up r2ai");
|
||||
msg("-i [f] [q] - include given file and query");
|
||||
msg("-k - list API key status");
|
||||
msg("-K - edit apikeys.txt");
|
||||
msg("-m [model] - use -m? or -e model=? to list the available models");
|
||||
msg("-n - suggest better function name");
|
||||
msg("-p [provider] - same as decai -e api (will be provider)");
|
||||
msg("-q [text] - query language model with given text");
|
||||
msg("-Q [text] - query on top of the last output");
|
||||
msg("-r [prompt] - change role prompt (same as: decai -e prompt)");
|
||||
msg("-R - reset role prompt to default prompt");
|
||||
msg("-s - function signature");
|
||||
msg("-v - show local variables");
|
||||
msg("-V - find vulnerabilities");
|
||||
msg("-x[*] - eXplain current function (-x* for r2 script)");
|
||||
|
||||
r2.log(helpmsg.trim());
|
||||
}
|
||||
|
||||
export function decompile(
|
||||
args: string,
|
||||
extraQuery: boolean,
|
||||
useCache: boolean,
|
||||
recursiveCalls: boolean,
|
||||
): string | undefined {
|
||||
if (useCache) {
|
||||
const cachedAnnotation = r2.cmd("anos").trim();
|
||||
if (cachedAnnotation.length > 0) {
|
||||
return cachedAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
let context = "";
|
||||
if (recursiveCalls) {
|
||||
const at = r2.cmd("s");
|
||||
context += "## Context functions:\n";
|
||||
const funcs = r2.cmdAt("axff~^C[2]~$$", at);
|
||||
for (const addr of funcs.split(/\n/g)) {
|
||||
context += r2.cmd("pdc@" + addr);
|
||||
}
|
||||
r2.cmd("s " + at);
|
||||
}
|
||||
|
||||
const appendQuery = extraQuery ? " " + args : "";
|
||||
|
||||
try {
|
||||
const parsedArgs = args.slice(2).trim();
|
||||
let text = "";
|
||||
|
||||
if (
|
||||
state.contextFile !== "" &&
|
||||
r2.cmd2("test -f " + state.contextFile).value === 0
|
||||
) {
|
||||
text += "## Context:\n[START]\n" +
|
||||
r2.cmd("cat " + state.contextFile) +
|
||||
"\n[END]\n";
|
||||
}
|
||||
|
||||
const { body, count } = withColorDisabled(() => {
|
||||
let body = "## Before:\n";
|
||||
let count = 0;
|
||||
for (const c of state.commands.split(",")) {
|
||||
if (c.trim() === "") continue;
|
||||
const oneliner = extraQuery || parsedArgs.trim().length === 0
|
||||
? c
|
||||
: c + "@@= " + parsedArgs;
|
||||
const output = r2.cmd(oneliner);
|
||||
if (output.length > 5) {
|
||||
body += "Output of " + c + ":\n[START]\n" + output + "\n[END]\n";
|
||||
count++;
|
||||
}
|
||||
}
|
||||
body += "## After:\n";
|
||||
return { body, count };
|
||||
});
|
||||
|
||||
if (count === 0) {
|
||||
console.error("Nothing to do.");
|
||||
return;
|
||||
}
|
||||
|
||||
let out = "";
|
||||
if (state.decopipe.use) {
|
||||
const origModel = state.model;
|
||||
try {
|
||||
const dpipe = state.decopipe[state.decopipe.default as string] as {
|
||||
pipeline: Array<{ model?: string; query: string }>;
|
||||
globalQuery: string;
|
||||
};
|
||||
let code = text + body;
|
||||
|
||||
for (const dp of dpipe.pipeline) {
|
||||
if (dp.model) state.model = dp.model;
|
||||
const query = dp.query + ". " + dpipe.globalQuery;
|
||||
out = r2ai(query, code, true);
|
||||
if (state.debug) {
|
||||
console.log("QUERY\n", query, "\nINPUT\n", code, "\nOUTPUT\n", out);
|
||||
}
|
||||
code = out;
|
||||
}
|
||||
out = code;
|
||||
} finally {
|
||||
state.model = origModel;
|
||||
}
|
||||
} else {
|
||||
const query = appendQuery;
|
||||
text += body + context;
|
||||
out = r2ai(query, text, false);
|
||||
state.lastOutput = out;
|
||||
}
|
||||
|
||||
if (useCache && out.length > 1) {
|
||||
r2.call("ano=base64:" + b64(out));
|
||||
}
|
||||
|
||||
if (out.startsWith("```")) {
|
||||
out = out.replace(/```.*\n/, "").replace(/```$/, "");
|
||||
}
|
||||
|
||||
return out.trim();
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error(err, err.stack);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function explainFunction(): string {
|
||||
const hints = withColorDisabled(() =>
|
||||
"[START]" +
|
||||
state.commands.split(",").map((c) => r2.cmd(c)).join("\n") + "[END]"
|
||||
);
|
||||
|
||||
const res = r2ai(
|
||||
"Analyze function calls, references, comments and strings, loops and ignore registers and memory accesses. Explain the purpose of this function in a single short sentence. /no_think Do not introduce or argue the response, translation of the explanation in " +
|
||||
state.humanLanguage,
|
||||
hints,
|
||||
true,
|
||||
);
|
||||
|
||||
const lines = res.trim().split(/\n/g);
|
||||
return lines[lines.length - 1].trim();
|
||||
}
|
||||
|
||||
export function suggestSignature(): string {
|
||||
const tmp = state.language;
|
||||
const code = r2.cmd("afv;pdc");
|
||||
state.language = "C";
|
||||
|
||||
let out = "'afs " +
|
||||
r2ai(
|
||||
"analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the return. Do NOT print the function body, ONLY output the function signature, like if it was going to be used in a C header",
|
||||
code,
|
||||
false,
|
||||
);
|
||||
|
||||
const brace = out.indexOf("{");
|
||||
if (brace !== -1) {
|
||||
out = out.substring(0, brace);
|
||||
}
|
||||
|
||||
state.language = tmp;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function autoMode(
|
||||
queryText: string,
|
||||
mainHandler: (args: string) => boolean,
|
||||
): void {
|
||||
const replies: string[] = [];
|
||||
|
||||
while (true) {
|
||||
let q = HELP_TEXT.auto;
|
||||
|
||||
if (replies.length > 0) {
|
||||
q += "## Command Results\n\n";
|
||||
for (const rep of replies) {
|
||||
const rp = JSON.parse(rep);
|
||||
q += "### " + rp.command + "\n\n```\n" + rp.response + "\n```\n";
|
||||
}
|
||||
}
|
||||
|
||||
q += "\n\n## User Prompt\n\n" + queryText;
|
||||
|
||||
if (state.debug) {
|
||||
console.log("#### input\n", q, "\n#### /input");
|
||||
}
|
||||
|
||||
console.log("Thinking...");
|
||||
const out = r2ai("", q, true);
|
||||
|
||||
if (state.debug) {
|
||||
console.log("#### output\n", out, "\n#### /output");
|
||||
}
|
||||
|
||||
try {
|
||||
const o: AutoReply = JSON.parse(
|
||||
trimJson(trimDown(filterResponse(out))),
|
||||
);
|
||||
|
||||
if (
|
||||
o.action === "r2cmd" ||
|
||||
o.action === "response" ||
|
||||
o.action === o.command
|
||||
) {
|
||||
const ocmd = o.command || "";
|
||||
|
||||
if (o.reason) {
|
||||
console.log("[r2cmd] Reasoning: " + o.reason);
|
||||
if (state.tts) {
|
||||
r2.syscmd("pkill say");
|
||||
r2.syscmd(
|
||||
"say -v Alex -r 250 '" + o.reason.replace(/'/g, "") + "' &",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[r2cmd] Action: " + o.description);
|
||||
console.log("[r2cmd] Command: " + ocmd);
|
||||
|
||||
let cmd = ocmd;
|
||||
if (!state.yolo) {
|
||||
cmd = autoRepl(ocmd, mainHandler);
|
||||
}
|
||||
|
||||
console.log("[r2cmd] Running: " + cmd);
|
||||
const obj = r2.cmd2(cmd);
|
||||
const logs = obj.logs
|
||||
? obj.logs.map((x: { type: string; message: string }) =>
|
||||
x.type + ": " + x.message
|
||||
).join("\n")
|
||||
: "";
|
||||
const res = (obj.res + logs).trim();
|
||||
|
||||
console.log(res);
|
||||
const cleanRes = trimAnsi(res);
|
||||
|
||||
if (state.debug) {
|
||||
console.log("<r2output>\n", cleanRes, "\n<(r2output>");
|
||||
}
|
||||
|
||||
replies.push(
|
||||
JSON.stringify({
|
||||
action: "response",
|
||||
command: cmd,
|
||||
description: o.description,
|
||||
response: cleanRes,
|
||||
}),
|
||||
);
|
||||
} else if (o.action === "reply") {
|
||||
console.log("Done\n", o.response);
|
||||
break;
|
||||
} else {
|
||||
console.log("Unknown response\n", JSON.stringify(out));
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
const response = out.indexOf('response": "');
|
||||
if (response !== -1) {
|
||||
const res = out
|
||||
.slice(response + 12)
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\/g, "");
|
||||
console.log(res);
|
||||
} else {
|
||||
console.log(out);
|
||||
console.error(e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function autoRepl(
|
||||
ocmd: string,
|
||||
mainHandler: (args: string) => boolean,
|
||||
): string {
|
||||
while (true) {
|
||||
const cmd = r2.cmd("'?ie Tweak command? ('?' for help)").trim();
|
||||
|
||||
if (cmd === "q!") {
|
||||
console.error("Break!");
|
||||
break;
|
||||
}
|
||||
if (cmd === "?") {
|
||||
autoHelp();
|
||||
continue;
|
||||
} else if (cmd.startsWith(":")) {
|
||||
console.log(r2.cmd(cmd.slice(1)));
|
||||
continue;
|
||||
} else if (cmd.startsWith("-e")) {
|
||||
mainHandler(cmd);
|
||||
continue;
|
||||
} else if (cmd === "!") {
|
||||
return "?e do NOT execute '" + ocmd + "' again, continue without it";
|
||||
} else if (cmd.startsWith("!")) {
|
||||
console.log(r2.syscmd(cmd.slice(1)));
|
||||
continue;
|
||||
} else if (cmd === "q") {
|
||||
return "?e All data collected!. Do not call more commands, reply the solutions";
|
||||
} else if (!cmd) {
|
||||
return ocmd;
|
||||
} else {
|
||||
const comment = cmd.indexOf("#");
|
||||
if (comment !== -1) {
|
||||
const command = cmd.slice(0, comment).trim();
|
||||
return command;
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
return ocmd;
|
||||
}
|
||||
|
||||
function autoHelp(): void {
|
||||
console.log(" '!' do not run this command");
|
||||
console.log(" '!c' run system command");
|
||||
console.log(" 'q' to quit auto and try to solve");
|
||||
console.log(" 'q!' quit auto without solving");
|
||||
console.log(" 'c # C' use given command with comment");
|
||||
console.log(" ':c' run r2 command without feeding auto");
|
||||
console.log(" '-e' set decai configuration variables");
|
||||
}
|
||||
|
||||
function getFlagValue(args: string): string {
|
||||
return args.slice(2).trim();
|
||||
}
|
||||
|
||||
function currentFunctionExists(): boolean {
|
||||
return r2.cmd("afi.").trim().length > 0;
|
||||
}
|
||||
|
||||
const noFunctionMessage = "Cannot find function at current offset";
|
||||
|
||||
export function handleCommand(
|
||||
args: string,
|
||||
mainHandler: (args: string) => boolean,
|
||||
): string | undefined {
|
||||
if (args === "" || !args.startsWith("-")) {
|
||||
showHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
let output = "";
|
||||
const flag = args[1];
|
||||
const argValue = getFlagValue(args);
|
||||
|
||||
switch (flag) {
|
||||
case "H":
|
||||
console.log(HELP_TEXT.decai);
|
||||
break;
|
||||
|
||||
case "a":
|
||||
autoMode(argValue, mainHandler);
|
||||
break;
|
||||
|
||||
case "m": {
|
||||
if (argValue === "=") {
|
||||
evalConfig("model=");
|
||||
} else if (argValue) {
|
||||
evalConfig("model=" + argValue);
|
||||
} else {
|
||||
evalConfig("model");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "n":
|
||||
case "f": {
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
output = r2.cmd("axff~$[3]");
|
||||
const considerations = r2
|
||||
.cmd("fd.")
|
||||
.trim()
|
||||
.split(/\n/)
|
||||
.filter((x: string) => !x.startsWith("secti"))
|
||||
.join(",");
|
||||
|
||||
output = r2ai(
|
||||
"give me a better name for this function. the output must be: 'afn NEWNAME'. do not include the function code, only the afn line. consider: " +
|
||||
considerations,
|
||||
output,
|
||||
false,
|
||||
).trim();
|
||||
output += " @ " + r2.cmd("?v $FB").trim();
|
||||
break;
|
||||
}
|
||||
|
||||
case "v": {
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
output = r2.cmd("afv;pdc");
|
||||
output = r2ai(
|
||||
"guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands",
|
||||
output,
|
||||
false,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case "i": {
|
||||
const parts = argValue.split(/\s+/).filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
const file = parts[0];
|
||||
const query = parts.slice(1).join(" ");
|
||||
const fileData = r2.cmd("cat " + file);
|
||||
console.log(r2ai(query, fileData, true));
|
||||
} else {
|
||||
console.log("Use: decai -i [file] [query ...]");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "p": {
|
||||
if (argValue) {
|
||||
evalConfig("api=" + argValue);
|
||||
} else {
|
||||
listApiKeys();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "r": {
|
||||
if (argValue) {
|
||||
state.prompt = argValue;
|
||||
} else {
|
||||
console.log(state.prompt);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "R":
|
||||
state.prompt = DEFAULT_PROMPT;
|
||||
break;
|
||||
|
||||
case "s":
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
output = suggestSignature();
|
||||
break;
|
||||
|
||||
case "V":
|
||||
output = r2ai(
|
||||
"find vulnerabilities, dont show the code, only show the response, provide a sample exploit",
|
||||
state.lastOutput,
|
||||
false,
|
||||
);
|
||||
break;
|
||||
|
||||
case "K":
|
||||
editApiKeys();
|
||||
break;
|
||||
|
||||
case "E":
|
||||
editRcConfig();
|
||||
break;
|
||||
|
||||
case "k":
|
||||
listApiKeys();
|
||||
break;
|
||||
|
||||
case "b": {
|
||||
if (argValue) {
|
||||
evalConfig("baseurl=" + argValue);
|
||||
} else {
|
||||
console.log(state.baseurl);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "e": {
|
||||
if (argValue) {
|
||||
evalConfig(argValue);
|
||||
} else {
|
||||
listAllConfig();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "q":
|
||||
try {
|
||||
output = r2ai(argValue, null, true);
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error(err, err.stack);
|
||||
}
|
||||
break;
|
||||
|
||||
case "Q":
|
||||
output = r2ai(argValue, state.lastOutput, false);
|
||||
break;
|
||||
|
||||
case "x":
|
||||
if (!currentFunctionExists()) {
|
||||
output = noFunctionMessage;
|
||||
break;
|
||||
}
|
||||
output = explainFunction();
|
||||
if (args[2] === "*" || args[2] === "r") {
|
||||
output = "'CC " + output;
|
||||
}
|
||||
break;
|
||||
|
||||
case "d":
|
||||
if (args[2] === "r") {
|
||||
output = decompile(args.slice(2), true, state.cache, true) || "";
|
||||
} else if (args[2] === "d") {
|
||||
output = decompile(args, false, false, false) || "";
|
||||
} else if (args[2] === "D") {
|
||||
output = decompile(args, true, false, false) || "";
|
||||
} else {
|
||||
output = decompile(args, false, state.cache, false) || "";
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
showHelp();
|
||||
break;
|
||||
}
|
||||
|
||||
return output || undefined;
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
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; },
|
||||
};
|
||||
}
|
||||
|
||||
export const configHandlers: ConfigHandlers = {
|
||||
pipeline: {
|
||||
get: () => state.pipeline,
|
||||
set: (v: string) => {
|
||||
state.pipeline = v;
|
||||
try {
|
||||
state.decopipe = JSON.parse(r2.cmd("cat " + v));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
},
|
||||
model: {
|
||||
get: () => state.model,
|
||||
set: (v: string) => {
|
||||
if (v === "?") {
|
||||
listModelsFor(state.api);
|
||||
} else {
|
||||
state.model = v.trim();
|
||||
}
|
||||
},
|
||||
},
|
||||
deterministic: boolHandler("deterministic"),
|
||||
files: boolHandler("useFiles"),
|
||||
think: {
|
||||
get: () => state.think || "false",
|
||||
set: (v: string) => { state.think = v; },
|
||||
},
|
||||
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 === "?") {
|
||||
console.error(listProviders().join("\n"));
|
||||
} else {
|
||||
state.api = v;
|
||||
}
|
||||
},
|
||||
},
|
||||
lang: stringHandler("language"),
|
||||
hlang: stringHandler("humanLanguage"),
|
||||
cache: boolHandler("cache"),
|
||||
cmds: stringHandler("commands"),
|
||||
tts: boolHandler("tts"),
|
||||
yolo: boolHandler("yolo"),
|
||||
prompt: stringHandler("prompt"),
|
||||
ctxfile: stringHandler("contextFile"),
|
||||
baseurl: stringHandler("baseurl"),
|
||||
headers: {
|
||||
get: () => formatHeaders(state.extraHeaders),
|
||||
set: (v: string) => { state.extraHeaders = parseHeaders(v); },
|
||||
},
|
||||
maxtokens: {
|
||||
get: () => state.maxInputTokens,
|
||||
set: (v: string) => { state.maxInputTokens = parseInt(v, 10) || -1; },
|
||||
},
|
||||
};
|
||||
|
||||
function resetRcConfigState(): void {
|
||||
const keep = { host: state.host, port: state.port, lastOutput: state.lastOutput };
|
||||
Object.assign(state, defaultState, keep);
|
||||
state.decopipe = { ...defaultState.decopipe };
|
||||
state.extraHeaders = [...defaultState.extraHeaders];
|
||||
}
|
||||
|
||||
function normalizeRcLine(line: string): string | null {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
return null;
|
||||
}
|
||||
return trimmed.startsWith("decai -e ")
|
||||
? trimmed.slice("decai -e ".length).trim()
|
||||
: trimmed;
|
||||
}
|
||||
|
||||
export function evalConfig(arg: string): void {
|
||||
const eqIndex = arg.indexOf("=");
|
||||
const k = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
|
||||
const v = eqIndex === -1 ? undefined : arg.slice(eqIndex + 1);
|
||||
const handler = configHandlers[k];
|
||||
|
||||
if (!handler) {
|
||||
console.error("Unknown config key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof v !== "undefined") {
|
||||
handler.set(v);
|
||||
} else {
|
||||
console.log(handler.get());
|
||||
}
|
||||
}
|
||||
|
||||
export function listAllConfig(): void {
|
||||
Object.entries(configHandlers).forEach(([key, handler]) => {
|
||||
const value = handler.get();
|
||||
console.log("decai -e " + key + "=" + value);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadRcConfig(): void {
|
||||
resetRcConfigState();
|
||||
if (!fileExists(DECAI_CONFIG_PATH)) {
|
||||
return;
|
||||
}
|
||||
const rcFile = r2.call("cat " + DECAI_CONFIG_PATH);
|
||||
for (const line of rcFile.split(/\r?\n/)) {
|
||||
const rcLine = normalizeRcLine(line);
|
||||
if (rcLine && rcLine[0] != '#') {
|
||||
evalConfig(rcLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureRcConfigLoaded(): void {
|
||||
if (rcConfigLoaded) {
|
||||
return;
|
||||
}
|
||||
loadRcConfig();
|
||||
rcConfigLoaded = true;
|
||||
}
|
||||
|
||||
export function editRcConfig(): void {
|
||||
ensurePath(DECAI_CONFIG_DIR, DECAI_CONFIG_PATH);
|
||||
r2.cmd("'ed " + DECAI_CONFIG_PATH);
|
||||
loadRcConfig();
|
||||
rcConfigLoaded = true;
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
export const VERSION = "1.4.0";
|
||||
export const COMMAND = "decai";
|
||||
export const DECAI_CONFIG_DIR = "~/.config/r2ai";
|
||||
export const API_KEYS_PATH = DECAI_CONFIG_DIR + "/apikeys.txt";
|
||||
export const DECAI_CONFIG_PATH = DECAI_CONFIG_DIR + "/decai.txt";
|
||||
|
||||
export const DEFAULT_PROMPT =
|
||||
"Rewrite this pseudocode into concise and clean code. Output only the provided function. Do not add wrappers, helper examples, test code, or main-like functions. Replace goto with structured control flow, simplify as much as possible, infer types and use better names for variables and parameters, some strings may be appearing as comments, preserve only what is implied by the input, and remove dead code.";
|
||||
|
||||
export const HELP_TEXT = {
|
||||
decai: `# Using Decai
|
||||
|
||||
Decai is the radare2 plugin for decompiling functions with the help of language models.
|
||||
|
||||
By default uses a local ollama server, but can you can pick any other service by using 'decai -e api=?'.
|
||||
|
||||
[0x00000000]> decai -e api=?
|
||||
r2ai deepseek anthropic claude gemini groq hf lmstudio mistral ollama ollamacloud openapi openai openrouter vllm xai zen
|
||||
|
||||
## Using Ollama
|
||||
|
||||
* Visit https://ollama.com to install it.
|
||||
* Download the model of choice: 'ollama run llama3.3'
|
||||
* Configure decai to use the given model with: 'decai -e model=?'
|
||||
|
||||
These are the most recommended models for decompiling in local:
|
||||
|
||||
* hhao/qwen2.5-coder-tools:latest (18GB of ram)
|
||||
* hhao/qwen2.5-coder-tools:32b (24GB of ram required)
|
||||
|
||||
## Common Options
|
||||
* 'decai -e baseurl=<url>' override default host and port for API endpoint (e.g., 'http://localhost:11434')
|
||||
* 'decai -e headers=Authorization: Bearer ...\\nUser-Agent: curl/8.7.1' add or override HTTP headers
|
||||
* "export DECAI_HEADERS='Authorization: Bearer ...\\nUser-Agent: curl/8.7.1'" set extra headers from the environment
|
||||
* 'decai -e timeout=0' disable curl timeouts, or set a positive number of seconds
|
||||
|
||||
* 'decai -e deterministic=true' to remove randomness from decompilation responses
|
||||
* 'decai -e lang=Python' to output the decompilation in Python instead of C
|
||||
* 'decai -e hlang=Catalan' to add comments or explanations in that language (instead of English)
|
||||
* 'decai -e cmds=pdd,pdg' use r2dec and r2ghidra instead of r2's pdc as input for decompiling
|
||||
* 'decai -e prompt=..' default prompt must be fine for most models and binaries, feel free to tweak it
|
||||
|
||||
## API Keys
|
||||
|
||||
Remove services like OpenAI, Mistral, Anthropic, Grok, Gemini, .. require API keys to work.
|
||||
|
||||
See 'decai -k' to list the status of available APIkeys
|
||||
Use 'decai -K' to edit apikeys.txt and 'decai -E' to edit decai.txt.
|
||||
|
||||
Decai will pick them from the environment or the config files in your home:
|
||||
|
||||
* echo KEY > ~/.r2ai.openai-key
|
||||
* export OPENAI_API_KEY=...
|
||||
* put one 'key=value' pair per line in ~/.config/r2ai/decai.txt
|
||||
|
||||
## Using the R2AI Server:
|
||||
|
||||
Install r2ai or r2ai-server with r2pm:
|
||||
|
||||
[0x0000000]> decai -e api=r2ai
|
||||
[0x0000000]> r2pm -ci r2ai
|
||||
|
||||
Choose one of the recommended models (after r2pm -r r2ai):
|
||||
|
||||
* -m ibm-granite/granite-20b-code-instruct-8k-GGUF
|
||||
* -m QuantFactory/granite-8b-code-instruct-4k-GGUF
|
||||
* -m TheBloke/Mistral-7B-Instruct-v0.2-GGUF
|
||||
|
||||
Start the webserver:
|
||||
|
||||
$ r2pm -r r2ai-server -l r2ai -m granite-8...
|
||||
`,
|
||||
auto: `# Radare2 Auto Mode
|
||||
|
||||
Use function calling to execute radare2 commands in order to resolve the user request defined in the "User Prompt" section, analyze the responses attached in the "Command Results" section.
|
||||
|
||||
## Function Calling
|
||||
|
||||
Respond ONLY using plain JSON. Process user query and decide which function calls are necessary to solve the task.
|
||||
|
||||
1. Analyze the user request to determine if we need to run commands to extend the knowledge and context of the problem.
|
||||
2. If function call is needed, construct the JSON like this:
|
||||
- Fill the "action" key with the "r2cmd" value.
|
||||
- Specify the "command" as a string.
|
||||
- Optionally, provide a "reason" and "description"
|
||||
3. If the answer can be provided and no more function calls are required:
|
||||
- Use the key "action": "reply".
|
||||
- Include "response" with the direct answer to the user query.
|
||||
|
||||
Return the result as a JSON object.
|
||||
|
||||
### Sample Function Calling Communication
|
||||
|
||||
Command Results: already performed actions with their responses
|
||||
User Prompt: "Count how many functions we have here."
|
||||
Response:
|
||||
{
|
||||
"action": "r2cmd",
|
||||
"command": "aflc",
|
||||
"description": "Count functions"
|
||||
"reason": "Evaluate if the program is analyzed before running aaa"
|
||||
}
|
||||
|
||||
## Rules
|
||||
|
||||
Use radare2 to resolve user requests.
|
||||
|
||||
* Explain each step in the "reason" field of the JSON.
|
||||
* Follow the initial analysis instructions.
|
||||
* Output only valid JSON as specified.
|
||||
* Decompile and inspect functions, starting from main.
|
||||
* Run only the needed commands to gather info.
|
||||
* Use "@ (address|symbol)" to seek temporarily.
|
||||
* Output should be a verbose markdown report.
|
||||
* Use "sym." or "fcn." prefixes if "pdc" is empty.
|
||||
* If a seek fails, use "f~name" to find the symbol's address.
|
||||
|
||||
### Initial Analysis
|
||||
|
||||
1. Run "aflc" to count the number of functions
|
||||
2. If the output of "aflc" is "0" run "aaa" once, then "aflc" again
|
||||
3. Run only one command at a time (do not use ";")
|
||||
|
||||
### Special cases
|
||||
|
||||
* On Swift binaries run...
|
||||
`,
|
||||
};
|
||||
@@ -1,91 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { HttpResponse } from "./types";
|
||||
import { state } from "./state";
|
||||
import { mergeHeaders } from "./headers";
|
||||
import { debugLog } from "./utils";
|
||||
|
||||
interface HttpRequestOptions {
|
||||
method: "GET" | "POST";
|
||||
url: string;
|
||||
headers: string[];
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
function shellEscape(arg: string): string {
|
||||
return `'${arg.replace(/'/g, `'"'"'`)}'`;
|
||||
}
|
||||
|
||||
function executeCurl(options: HttpRequestOptions): string {
|
||||
const { method, url, headers, payload } = options;
|
||||
const cmdParts = ["curl", "-s"];
|
||||
if (state.timeout > 0) {
|
||||
cmdParts.push("--max-time", String(state.timeout));
|
||||
}
|
||||
const requestHeaders = mergeHeaders(
|
||||
["Content-Type: application/json"],
|
||||
headers,
|
||||
);
|
||||
requestHeaders.forEach((h) => cmdParts.push("-H", shellEscape(h)));
|
||||
|
||||
if (method === "POST") {
|
||||
if (!payload) throw new Error("Payload required for POST requests");
|
||||
const tmpfile = r2.fdump(payload);
|
||||
cmdParts.push("--data-binary", "@" + shellEscape(tmpfile), shellEscape(url));
|
||||
const cmd = cmdParts.join(" ") + "; rm " + shellEscape(tmpfile);
|
||||
debugLog(cmd);
|
||||
return r2.syscmds(cmd);
|
||||
} else {
|
||||
cmdParts.push(shellEscape(url));
|
||||
const cmd = cmdParts.join(" ");
|
||||
debugLog(cmd);
|
||||
return r2.syscmds(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
function parseNdjson(output: string): HttpResponse | null {
|
||||
const lines = output.split("\n").filter((l) => l.trim() !== "");
|
||||
if (lines.length <= 1) return null;
|
||||
|
||||
let content = "";
|
||||
let lastObj: Record<string, unknown> | null = null;
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.error) return obj as HttpResponse;
|
||||
if (obj.message?.content !== undefined) {
|
||||
content += obj.message.content;
|
||||
}
|
||||
lastObj = obj;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastObj) {
|
||||
lastObj.message = { role: "assistant", content };
|
||||
return lastObj as HttpResponse;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function httpRequest(options: HttpRequestOptions): HttpResponse {
|
||||
try {
|
||||
const output = executeCurl(options).trim();
|
||||
if (output === "") {
|
||||
return { error: "empty response" };
|
||||
}
|
||||
try {
|
||||
return JSON.parse(output) as HttpResponse;
|
||||
} catch {
|
||||
const ndjson = parseNdjson(output);
|
||||
if (ndjson) return ndjson;
|
||||
return { error: output, rawOutput: output };
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
return { error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
export function httpGet(url: string, headers: string[]): HttpResponse {
|
||||
return httpRequest({ method: "GET", url, headers });
|
||||
}
|
||||
|
||||
export function httpPost(
|
||||
url: string,
|
||||
headers: string[],
|
||||
payload: string,
|
||||
): HttpResponse {
|
||||
return httpRequest({ method: "POST", url, headers, payload });
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
### 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
|
||||
@@ -1,34 +0,0 @@
|
||||
import { COMMAND } from "./constants";
|
||||
import { handleCommand } from "./commands";
|
||||
import { ensureRcConfigLoaded } from "./config";
|
||||
|
||||
function main(args: string): boolean {
|
||||
ensureRcConfigLoaded();
|
||||
const output = handleCommand(args, main);
|
||||
if (output) {
|
||||
r2.log(output);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function installPlugin(): void {
|
||||
r2.unload("core", COMMAND);
|
||||
r2.plugin("core", function () {
|
||||
function coreCall(cmd: string): boolean {
|
||||
if (cmd.startsWith(COMMAND)) {
|
||||
const args = cmd.slice(COMMAND.length).trim();
|
||||
return main(args);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
name: COMMAND,
|
||||
license: "MIT",
|
||||
desc: "r2 decompiler based on r2ai",
|
||||
call: coreCall,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
installPlugin();
|
||||
@@ -1,34 +0,0 @@
|
||||
import { getProvider, listModels } from "./providers";
|
||||
|
||||
export function listModelsFor(api: string): void {
|
||||
const providerConfig = getProvider(api);
|
||||
|
||||
if (!providerConfig) {
|
||||
console.error(`Unknown provider: ${api}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const models = listModels(api);
|
||||
|
||||
if (models) {
|
||||
console.log(models);
|
||||
}
|
||||
|
||||
if (providerConfig.hardcodedModels) {
|
||||
providerConfig.hardcodedModels.forEach((model) => console.log(model));
|
||||
}
|
||||
|
||||
if (api === "mistral") {
|
||||
console.log("ministral-8b-latest");
|
||||
}
|
||||
|
||||
if (!models && !providerConfig.hardcodedModels) {
|
||||
console.log(providerConfig.defaultModel);
|
||||
}
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
console.error(`Error listing models for ${api}:`, err.message);
|
||||
console.log(providerConfig.defaultModel);
|
||||
}
|
||||
}
|
||||
@@ -1,530 +0,0 @@
|
||||
import {
|
||||
AnthropicPayload,
|
||||
ApiKeyResult,
|
||||
ApiStyle,
|
||||
AuthStyle,
|
||||
GeminiPayload,
|
||||
HttpResponse,
|
||||
ModelDataEntry,
|
||||
OllamaModelEntry,
|
||||
OllamaPayload,
|
||||
ProviderConfig,
|
||||
ProviderRegistry,
|
||||
} from "./types";
|
||||
import { state } from "./state";
|
||||
import { filterResponse, padRight } from "./utils";
|
||||
import { getApiKey } from "./apiKeys";
|
||||
import { getConfiguredHeaders, mergeHeaders } from "./headers";
|
||||
import { httpGet, httpPost } from "./http";
|
||||
|
||||
interface ProviderRuntime {
|
||||
buildPayload: (model: string, query: string) => object;
|
||||
parseResponse: (response: HttpResponse) => string;
|
||||
buildUrl: (baseUrl: string, model: string, apiKey?: string) => string;
|
||||
requiresUrlApiKey?: boolean;
|
||||
}
|
||||
|
||||
function getApiKeyName(authKey: string): string {
|
||||
return authKey.split("_")[0].toLowerCase();
|
||||
}
|
||||
|
||||
function readProviderKey(provider: ProviderConfig): ApiKeyResult | undefined {
|
||||
if (!provider.authKey) {
|
||||
return undefined;
|
||||
}
|
||||
const keyNames = Array.from(
|
||||
new Set(
|
||||
[provider.keyName, getApiKeyName(provider.authKey)].filter(Boolean),
|
||||
),
|
||||
) as string[];
|
||||
|
||||
let fallback: ApiKeyResult | undefined;
|
||||
for (const keyName of keyNames) {
|
||||
const result = getApiKey(keyName, provider.authKey);
|
||||
if (result[0]) {
|
||||
return result;
|
||||
}
|
||||
if (!fallback && result[2] !== "nope") {
|
||||
fallback = result;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback ||
|
||||
getApiKey(getApiKeyName(provider.authKey), provider.authKey);
|
||||
}
|
||||
|
||||
function getErrorMessage(error: HttpResponse["error"]): string | undefined {
|
||||
if (!error) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
return error.message || JSON.stringify(error);
|
||||
}
|
||||
|
||||
function getProviderBaseUrl(provider: ProviderConfig): string {
|
||||
return state.baseurl || provider.defaultBaseUrl;
|
||||
}
|
||||
|
||||
function getProviderHeaders(
|
||||
providerHeaders: string[],
|
||||
extraHeaders: string[] = getConfiguredHeaders(),
|
||||
): string[] {
|
||||
return mergeHeaders(providerHeaders, extraHeaders);
|
||||
}
|
||||
|
||||
function buildBearerHeaders(apiKey: string | null): string[] {
|
||||
return apiKey ? ["Authorization: Bearer " + apiKey] : [];
|
||||
}
|
||||
|
||||
function buildAnthropicHeaders(apiKey: string | null): string[] {
|
||||
const headers = ["anthropic-version: 2023-06-01"];
|
||||
return apiKey ? mergeHeaders(headers, ["x-api-key: " + apiKey]) : headers;
|
||||
}
|
||||
|
||||
function buildAuthHeaders(
|
||||
provider: ProviderConfig,
|
||||
apiKey: string | null,
|
||||
): string[] {
|
||||
const authStyle: AuthStyle = provider.authStyle || "none";
|
||||
switch (authStyle) {
|
||||
case "bearer":
|
||||
return buildBearerHeaders(apiKey);
|
||||
case "anthropic":
|
||||
return buildAnthropicHeaders(apiKey);
|
||||
case "none":
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function listDataModels(
|
||||
url: string,
|
||||
headers: string[],
|
||||
mapper: (model: ModelDataEntry) => string,
|
||||
): string {
|
||||
const response = httpGet(url, headers);
|
||||
const error = getErrorMessage(response.error);
|
||||
if (error) {
|
||||
console.error(error);
|
||||
return "error invalid response";
|
||||
}
|
||||
return response.data?.map(mapper).join("\n") || "";
|
||||
}
|
||||
|
||||
function uniqueBy<T>(items: T[], getKey: (item: T) => string): T[] {
|
||||
const seen = new Set<string>();
|
||||
return items.filter((item) => {
|
||||
const key = getKey(item);
|
||||
if (seen.has(key)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function getProviderRequest(provider: ProviderConfig): {
|
||||
baseUrl: string;
|
||||
headers: string[];
|
||||
} {
|
||||
const apiKey = readProviderKey(provider)?.[0] || null;
|
||||
return {
|
||||
baseUrl: getProviderBaseUrl(provider),
|
||||
headers: getProviderHeaders(buildAuthHeaders(provider, apiKey)),
|
||||
};
|
||||
}
|
||||
|
||||
function listV1Models(provider: ProviderConfig): string {
|
||||
const { baseUrl, headers } = getProviderRequest(provider);
|
||||
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) {
|
||||
return uniqueBy(response.data, (model) => model.name || model.id)
|
||||
.map((model) =>
|
||||
[
|
||||
padRight(model.name || model.id, 30),
|
||||
padRight("" + (model.max_context_length || ""), 10),
|
||||
model.description || "",
|
||||
].join(" ")
|
||||
)
|
||||
.join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const providerRuntimes: Record<ApiStyle, ProviderRuntime> = {
|
||||
openai: {
|
||||
buildPayload: (model, query) => ({
|
||||
stream: false,
|
||||
model,
|
||||
messages: [{ role: "user", content: query }],
|
||||
}),
|
||||
parseResponse: (response) => {
|
||||
if (response.error) {
|
||||
throw new Error(getErrorMessage(response.error) || "Unknown error");
|
||||
}
|
||||
if (response.choices && response.choices[0]?.message?.content) {
|
||||
return filterResponse(response.choices[0].message.content);
|
||||
}
|
||||
throw new Error("Invalid response format");
|
||||
},
|
||||
buildUrl: (baseUrl) => 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,
|
||||
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 (response.message?.content) {
|
||||
return filterResponse(response.message.content);
|
||||
}
|
||||
throw new Error(JSON.stringify(response));
|
||||
},
|
||||
buildUrl: (baseUrl) => baseUrl + "/api/chat",
|
||||
},
|
||||
gemini: {
|
||||
buildPayload: (_model, query) => {
|
||||
const payload: 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",
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
export function getProvider(api: string): ProviderConfig | undefined {
|
||||
return providerRegistry[api];
|
||||
}
|
||||
|
||||
export function listProviders(): string[] {
|
||||
return Object.keys(providerRegistry);
|
||||
}
|
||||
|
||||
export function listModels(providerName: string): string {
|
||||
const provider = getProvider(providerName);
|
||||
if (!provider) {
|
||||
throw new Error(`Unknown provider: ${providerName}`);
|
||||
}
|
||||
|
||||
if (providerName === "mistral") {
|
||||
return listMistralModels(provider);
|
||||
}
|
||||
|
||||
switch (provider.apiStyle) {
|
||||
case "openai":
|
||||
case "anthropic":
|
||||
return listV1Models(provider);
|
||||
case "ollama":
|
||||
return listOllamaModels(provider);
|
||||
case "gemini":
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function isThinkDisabled(): boolean {
|
||||
return state.think === "false" || state.think === "0";
|
||||
}
|
||||
|
||||
function isThinkEnabled(): boolean {
|
||||
return state.think !== "" && !isThinkDisabled();
|
||||
}
|
||||
|
||||
function buildQuery(msg: string, hideprompt: boolean, apiStyle: ApiStyle): string {
|
||||
let query = msg;
|
||||
|
||||
// Only add prompt-based thinking hints for APIs without native think support
|
||||
if (state.think !== "" && apiStyle === "openai") {
|
||||
if (isThinkDisabled()) {
|
||||
query +=
|
||||
' Answers directly and concisely, without showing any thinking steps or internal reasoning. Never include phrases like "Let me think".';
|
||||
query += " /no_think";
|
||||
} else {
|
||||
query =
|
||||
"Think step by step and explain the reasoning process, When answering, first output your reasoning inside <think> and </think> tags, then give the final answer." +
|
||||
query;
|
||||
}
|
||||
}
|
||||
|
||||
return hideprompt ? query : state.prompt + languagePrompt() + query;
|
||||
}
|
||||
|
||||
function languagePrompt(): string {
|
||||
return "\n.Translate the code into " + state.language +
|
||||
" programming language\n";
|
||||
}
|
||||
|
||||
function callRuntime(
|
||||
provider: ProviderConfig,
|
||||
runtime: ProviderRuntime,
|
||||
msg: string,
|
||||
hideprompt: boolean,
|
||||
): string {
|
||||
const model = state.model || provider.defaultModel;
|
||||
const query = buildQuery(msg, hideprompt, provider.apiStyle);
|
||||
const apiKey = readProviderKey(provider);
|
||||
|
||||
if (runtime.requiresUrlApiKey && apiKey?.[1] && provider.authKey) {
|
||||
return `Cannot read ~/.r2ai.${getApiKeyName(provider.authKey)}-key`;
|
||||
}
|
||||
|
||||
const payload = runtime.buildPayload(model, query);
|
||||
const authHeaders = buildAuthHeaders(provider, apiKey?.[0] || null);
|
||||
if (provider.apiStyle === "ollama") {
|
||||
authHeaders.push("Accept: application/x-ndjson");
|
||||
}
|
||||
const headers = getProviderHeaders(authHeaders);
|
||||
const url = runtime.buildUrl(
|
||||
getProviderBaseUrl(provider),
|
||||
model,
|
||||
apiKey?.[0] || undefined,
|
||||
);
|
||||
|
||||
try {
|
||||
return runtime.parseResponse(
|
||||
httpPost(url, headers, JSON.stringify(payload)),
|
||||
);
|
||||
} catch (error) {
|
||||
return "ERROR: " + (error as Error).message;
|
||||
}
|
||||
}
|
||||
|
||||
export function callProvider(msg: string, hideprompt: boolean): string {
|
||||
const provider = getProvider(state.api);
|
||||
if (!provider) {
|
||||
return `Unknown value for 'decai -e api'. Available: ${
|
||||
listProviders().join(", ")
|
||||
}`;
|
||||
}
|
||||
|
||||
return callRuntime(
|
||||
provider,
|
||||
providerRuntimes[provider.apiStyle],
|
||||
msg,
|
||||
hideprompt,
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import { state } from "./state";
|
||||
import { fileDump, tmpdir } from "./utils";
|
||||
import { callProvider } from "./providers";
|
||||
import { httpGet } from "./http";
|
||||
|
||||
export function r2ai(
|
||||
queryText: string,
|
||||
fileData: string | null,
|
||||
hideprompt: boolean = false,
|
||||
): string {
|
||||
const cleanFileData = (fileData || "").replace(/`/g, "");
|
||||
const cleanQuery = queryText.replace(/'/g, "");
|
||||
|
||||
// Handle r2ai server mode
|
||||
if (state.api === "r2" || state.api === "r2ai") {
|
||||
const fileName = tmpdir(".pdc.txt");
|
||||
fileDump(fileName, cleanFileData);
|
||||
|
||||
const q = cleanQuery.startsWith("-")
|
||||
? cleanQuery
|
||||
: ["-i", fileName, cleanQuery].join(" ");
|
||||
|
||||
const host = state.baseurl
|
||||
? state.baseurl + "/cmd"
|
||||
: state.host + ":" + state.port + "/cmd";
|
||||
|
||||
const url = host + "/" + encodeURIComponent(q);
|
||||
const response = httpGet(url, []);
|
||||
if (response.error) {
|
||||
return `Error: ${response.error}`;
|
||||
}
|
||||
return response.result || JSON.stringify(response) ||
|
||||
"Cannot curl, use r2ai-server or r2ai -w";
|
||||
}
|
||||
|
||||
if (cleanQuery.startsWith("-")) return "";
|
||||
|
||||
let q = cleanQuery + ":\n" + cleanFileData;
|
||||
if (state.maxInputTokens > 0 && q.length > state.maxInputTokens) {
|
||||
q = q.slice(0, state.maxInputTokens);
|
||||
}
|
||||
|
||||
return callProvider(q, hideprompt);
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* Run a command in the associated instance of radare2 and return the output as a string
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd(command: string): string {
|
||||
return this.r2p.cmd(command);
|
||||
}
|
||||
cmd2(command: string): R2CommandResult {
|
||||
const output = this.r2p.cmd(command);
|
||||
return typeof output === "string" ? { res: output } : output;
|
||||
}
|
||||
cmdAt(
|
||||
command: string,
|
||||
address: R2Address,
|
||||
): string {
|
||||
return this.r2p.cmdAt(command, address);
|
||||
}
|
||||
cmdj(cmd: string): unknown {
|
||||
return this.r2p.cmdj(cmd);
|
||||
}
|
||||
call(command: string): string {
|
||||
return this.r2p.call(command);
|
||||
}
|
||||
callj(cmd: string): unknown {
|
||||
return this.r2p.cmdj(cmd);
|
||||
}
|
||||
callAt(
|
||||
command: string,
|
||||
address: R2Address,
|
||||
): string {
|
||||
return this.r2p.cmdAt(command, address);
|
||||
}
|
||||
syscmd(command: string): string {
|
||||
return this.r2p.syscmd(command);
|
||||
}
|
||||
syscmds(command: string): string {
|
||||
return this.r2p.syscmds(command);
|
||||
}
|
||||
fdump(data: string): string {
|
||||
return this.r2p.fdump(data);
|
||||
}
|
||||
log(msg: string): void {
|
||||
return this.r2p.log(msg);
|
||||
}
|
||||
plugin(type: string, maker: R2PluginFactory): boolean {
|
||||
return this.r2p.plugin(type, maker);
|
||||
}
|
||||
unload(type: string, name: string): boolean {
|
||||
return this.r2p.unload(type, name);
|
||||
}
|
||||
}
|
||||
|
||||
export function newR2PipeSync(r2p: R2Pipe): R2PipeSync {
|
||||
return new R2PipeAdapter(r2p) as R2PipeSync;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic interface to interact with radare2, abstracts the access to the associated
|
||||
* instance of the tool, which could be native via rlang or remote via pipes or tcp/http.
|
||||
*
|
||||
* @typedef R2Pipe
|
||||
*/
|
||||
export interface R2Pipe {
|
||||
/**
|
||||
* Run a command in the associated instance of radare2 and return the output as a string
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command in a different address. Same as `.cmd(x + '@ ' + a)`
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @param {number|string|NativePointer} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd2(cmd: string): R2CommandResult;
|
||||
cmdAt(cmd: string, address: R2Address): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command expecting the output to be JSON
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
cmdj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
|
||||
* which is safer and faster but you cannot use any special modifier like `@`, `~`, ...
|
||||
*
|
||||
* See R2Pipe.callAt() to call a command on a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
call(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Call a radare2 command in a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @param {NativePointer|string|number} where to seek to execute this command (previous offset is restored after executing it)
|
||||
* @returns {string} the string containing the output of the command
|
||||
*/
|
||||
callAt(cmd: string, address: R2Address): string;
|
||||
|
||||
syscmd(cmd: string): string;
|
||||
syscmds(cmd: string): string;
|
||||
fdump(data: string): string;
|
||||
|
||||
/**
|
||||
* Same as cmdj but using .call which avoids command injection problems
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the command output
|
||||
*/
|
||||
callj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
|
||||
*
|
||||
* @param {string} text to be displayed
|
||||
*/
|
||||
log(msg: string): void;
|
||||
|
||||
/**
|
||||
* Instantiate a new radare2 plugin with the given type and constructor method.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} function that returns the plugin definition
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
plugin(type: string, maker: R2PluginFactory): boolean;
|
||||
|
||||
/**
|
||||
* Unload the plugin associated with a `type` and a `name`.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} name of the plugin
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
unload(type: string, name: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic interface to interact with radare2, abstracts the access to the associated
|
||||
* instance of the tool, which could be native via rlang or remote via pipes or tcp/http.
|
||||
*
|
||||
* @typedef R2PipeSync
|
||||
*/
|
||||
export interface R2PipeSync {
|
||||
/**
|
||||
* Run a command in the associated instance of radare2
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command in a different address. Same as `.cmd(x + '@ ' + a)`
|
||||
*
|
||||
* @param {string} command to be executed inside radare2.
|
||||
* @param {number|string|NativePointer} command to be executed inside radare2.
|
||||
* @returns {string} The output of the command execution
|
||||
*/
|
||||
cmd2(cmd: string): R2CommandResult;
|
||||
cmdAt(cmd: string, address: R2Address): string;
|
||||
|
||||
/**
|
||||
* Run a radare2 command expecting the output to be JSON
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
cmdj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Call a radare2 command. This is similar to `R2Pipe.cmd`, but skips all the command parsing rules,
|
||||
* which is safer and faster but you cannot use any special modifier like `@`, `~`, ...
|
||||
*
|
||||
* See R2Pipe.callAt() to call a command on a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the output of the command
|
||||
*/
|
||||
call(cmd: string): string;
|
||||
|
||||
/**
|
||||
* Call a radare2 command in a different address
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @param {NativePointer|string|number} where to seek to execute this command (previous offset is restored after executing it)
|
||||
* @returns {string} the string containing the output of the command
|
||||
*/
|
||||
callAt(cmd: string, address: R2Address): string;
|
||||
|
||||
syscmd(cmd: string): string;
|
||||
syscmds(cmd: string): string;
|
||||
fdump(data: string): string;
|
||||
|
||||
/**
|
||||
* Same as cmdj but using .call which avoids command injection problems
|
||||
*
|
||||
* @param {string} command to be executed inside radare2. The given command should end with `j`
|
||||
* @returns {object} the JSON decoded object from the command output
|
||||
*/
|
||||
callj(cmd: string): unknown;
|
||||
|
||||
/**
|
||||
* Log a string to the associated console. This is used internally by `console.log` in some implementations.
|
||||
*
|
||||
* @param {string} text to be displayed
|
||||
*/
|
||||
log(msg: string): void;
|
||||
|
||||
/**
|
||||
* Instantiate a new radare2 plugin with the given type and constructor method.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} function that returns the plugin definition
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
plugin(type: string, maker: R2PluginFactory): boolean;
|
||||
|
||||
/**
|
||||
* Unload the plugin associated with a `type` and a `name`.
|
||||
*
|
||||
* @param {string} type of plugin ("core", "io", "arch", ...)
|
||||
* @param {string} name of the plugin
|
||||
* @returns {boolean} true if successful
|
||||
*/
|
||||
unload(type: string, name: string): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A global instance of R2Pipe associated with the current instance of radare2
|
||||
*
|
||||
* @type {R2PipeSync}
|
||||
*/
|
||||
export declare const r2: R2Pipe;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { AppState } from "./types";
|
||||
import { DEFAULT_PROMPT } from "./constants";
|
||||
|
||||
export const defaultState: AppState = {
|
||||
decopipe: { use: false },
|
||||
host: "http://localhost",
|
||||
port: "11434",
|
||||
baseurl: "",
|
||||
extraHeaders: [],
|
||||
api: "ollama",
|
||||
pipeline: "",
|
||||
commands: "pdc",
|
||||
yolo: false,
|
||||
tts: false,
|
||||
language: "C",
|
||||
humanLanguage: "English",
|
||||
deterministic: true,
|
||||
debug: false,
|
||||
timeout: 180,
|
||||
think: "",
|
||||
useFiles: false,
|
||||
contextFile: "",
|
||||
model: "",
|
||||
cache: false,
|
||||
maxInputTokens: -1,
|
||||
prompt: DEFAULT_PROMPT,
|
||||
lastOutput: "",
|
||||
};
|
||||
|
||||
export const state: AppState = {
|
||||
...defaultState,
|
||||
decopipe: { ...defaultState.decopipe },
|
||||
extraHeaders: [...defaultState.extraHeaders],
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"lib": ["esnext"],
|
||||
"module": "commonjs",
|
||||
"outDir": "out",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import type { R2PipeSync } from "./r2pipe";
|
||||
|
||||
interface ConsoleLike {
|
||||
log: (...args: unknown[]) => void;
|
||||
error: (...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
const r2: R2PipeSync;
|
||||
const console: ConsoleLike;
|
||||
function btoa(str: string): string;
|
||||
}
|
||||
|
||||
export type ApiStyle = "openai" | "anthropic" | "ollama" | "gemini";
|
||||
export type AuthStyle = "none" | "bearer" | "anthropic";
|
||||
|
||||
export interface ProviderConfig {
|
||||
defaultModel: string;
|
||||
defaultBaseUrl: string;
|
||||
authKey?: string;
|
||||
authStyle?: AuthStyle;
|
||||
keyName?: string;
|
||||
apiStyle: ApiStyle;
|
||||
hardcodedModels?: string[];
|
||||
}
|
||||
|
||||
export interface ProviderRegistry {
|
||||
[key: string]: ProviderConfig;
|
||||
}
|
||||
|
||||
export interface DecoPipeConfig {
|
||||
use: boolean;
|
||||
default?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AppState {
|
||||
decopipe: DecoPipeConfig;
|
||||
host: string;
|
||||
port: string;
|
||||
baseurl: string;
|
||||
extraHeaders: string[];
|
||||
api: string;
|
||||
pipeline: string;
|
||||
commands: string;
|
||||
yolo: boolean;
|
||||
tts: boolean;
|
||||
language: string;
|
||||
humanLanguage: string;
|
||||
deterministic: boolean;
|
||||
debug: boolean;
|
||||
timeout: number;
|
||||
think: string;
|
||||
useFiles: boolean;
|
||||
contextFile: string;
|
||||
model: string;
|
||||
cache: boolean;
|
||||
maxInputTokens: number;
|
||||
prompt: string;
|
||||
lastOutput: string;
|
||||
}
|
||||
|
||||
export interface ConfigHandler {
|
||||
get: () => string | number | boolean;
|
||||
set: (v: string) => void;
|
||||
}
|
||||
|
||||
export interface ConfigHandlers {
|
||||
[key: string]: ConfigHandler;
|
||||
}
|
||||
|
||||
export type ApiKeyResult = [string | null, string | null, string];
|
||||
|
||||
export interface ApiError {
|
||||
message?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface OpenAIChoice {
|
||||
message?: {
|
||||
content?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AnthropicContentBlock {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface OllamaMessage {
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface GeminiPart {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface GeminiCandidate {
|
||||
content?: {
|
||||
parts?: GeminiPart[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModelDataEntry {
|
||||
id: string;
|
||||
name?: string;
|
||||
max_context_length?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface OllamaModelEntry {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface HttpResponse {
|
||||
error?: ApiError | string;
|
||||
rawOutput?: string;
|
||||
choices?: OpenAIChoice[];
|
||||
content?: AnthropicContentBlock[];
|
||||
message?: OllamaMessage;
|
||||
candidates?: GeminiCandidate[];
|
||||
data?: ModelDataEntry[];
|
||||
models?: OllamaModelEntry[];
|
||||
result?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type JsonObject = Record<string, unknown>;
|
||||
|
||||
export interface AnthropicPayload {
|
||||
model: string;
|
||||
max_tokens: number;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
thinking?: { type: string; budget_tokens?: number };
|
||||
temperature?: number;
|
||||
top_p?: number;
|
||||
top_k?: number;
|
||||
}
|
||||
|
||||
export interface OllamaPayload {
|
||||
stream: boolean;
|
||||
model: string;
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
think?: boolean | string;
|
||||
options?: {
|
||||
repeat_last_n: number;
|
||||
top_p: number;
|
||||
top_k: number;
|
||||
temperature: number;
|
||||
repeat_penalty: number;
|
||||
seed: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GeminiPayload {
|
||||
contents: Array<{ parts: Array<{ text: string }> }>;
|
||||
generationConfig?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AutoReply {
|
||||
action: string;
|
||||
command?: string;
|
||||
description?: string;
|
||||
reason?: string;
|
||||
response?: string;
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { state } from "./state";
|
||||
|
||||
export function tmpdir(path: string): string {
|
||||
const dir = r2.cmd("-e dir.tmp").trim() ?? ".";
|
||||
return dir + "/" + path;
|
||||
}
|
||||
|
||||
export function ensurePath(dir: string, file: string): void {
|
||||
r2.cmd("'mkdir -p " + dir);
|
||||
r2.cmd("'touch " + file);
|
||||
}
|
||||
|
||||
export function fileExists(path: string): boolean {
|
||||
const helpMessage = r2.cmd2("test -h").logs?.[0]?.message ?? "";
|
||||
if (helpMessage.includes("-fdx")) {
|
||||
return true; // r2 is old
|
||||
}
|
||||
return r2.cmd("'test -vf " + path).startsWith("found");
|
||||
}
|
||||
|
||||
export function padRight(str: string, length: number): string {
|
||||
return str + " ".repeat(Math.max(0, length - str.length));
|
||||
}
|
||||
|
||||
export function trimAnsi(str: string): string {
|
||||
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
|
||||
export function trimDown(out: string): string {
|
||||
const jsonMatch = out.match(/```json\s*([\s\S]*?)```/);
|
||||
return jsonMatch && jsonMatch[1] ? jsonMatch[1].trim() : out;
|
||||
}
|
||||
|
||||
export function trimJson(out: string): string {
|
||||
let result = out;
|
||||
const openBrace = result.indexOf("{");
|
||||
if (openBrace !== -1) result = result.slice(openBrace);
|
||||
const closeBrace = result.lastIndexOf("}");
|
||||
if (closeBrace !== -1) result = result.slice(0, closeBrace + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function b64(str: string): string {
|
||||
return btoa(str);
|
||||
}
|
||||
|
||||
export function fileDump(fileName: string, fileData: string): void {
|
||||
const d = b64(fileData);
|
||||
r2.cmd("p6ds " + d + " > " + fileName);
|
||||
}
|
||||
|
||||
export function filterResponse(msg: string): string {
|
||||
let result = msg;
|
||||
result = result.replace(/<think>[\s\S]*?<\/think>/gi, "");
|
||||
return result
|
||||
.split("\n")
|
||||
.filter((line) => !line.trim().startsWith("```"))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function debugLog(msg: string): void {
|
||||
if (state.debug) {
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseEnvLikeString(input: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const line of input.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
const [key, ...rest] = trimmed.split("=");
|
||||
if (!key || rest.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const value = rest.join("=").trim();
|
||||
// Normalize key: strip trailing _API_KEY (case-insensitive)
|
||||
const normKey = key.toLowerCase().replace(/_api_key$/i, "");
|
||||
result[normKey] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="210mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 210 297"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
|
||||
sodipodi:docname="decai.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:zoom="1.0482005"
|
||||
inkscape:cx="318.64133"
|
||||
inkscape:cy="426.92214"
|
||||
inkscape:window-width="1512"
|
||||
inkscape:window-height="960"
|
||||
inkscape:window-x="222"
|
||||
inkscape:window-y="44"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="rect2"
|
||||
width="186.28354"
|
||||
height="176.18687"
|
||||
x="1.5145004"
|
||||
y="23.979588"
|
||||
ry="0.047831953" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path1"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="71.751205"
|
||||
sodipodi:cy="102.09375"
|
||||
sodipodi:r1="53.804771"
|
||||
sodipodi:r2="49.845222"
|
||||
sodipodi:arg1="0.65927727"
|
||||
sodipodi:arg2="0.97343657"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 114.28039,135.05163 -14.49317,8.25532 -13.001341,10.44831 -16.577574,-1.84017 -16.659669,0.81086 -12.329911,-11.23279 -13.95457,-9.1363 -3.37264,-16.33486 -5.919299,-15.59371 6.872864,-15.197565 4.376943,-16.094859 14.493168,-8.255319 13.001341,-10.448311 16.577574,1.840177 16.659669,-0.810865 12.329915,11.232788 13.95457,9.136304 3.37264,16.334856 5.91929,15.593714 -6.87286,15.19757 z"
|
||||
transform="matrix(1.2188073,0,0,1.2188073,1.600691,-15.424646)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#37662e;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path1-5"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="71.751205"
|
||||
sodipodi:cy="102.09375"
|
||||
sodipodi:r1="35.902484"
|
||||
sodipodi:r2="49.845222"
|
||||
sodipodi:arg1="0.32368816"
|
||||
sodipodi:arg2="0.63784749"
|
||||
inkscape:rounded="0"
|
||||
inkscape:randomized="0"
|
||||
d="m 105.78923,113.51309 6.0066,18.26187 -19.219401,-0.43572 -5.874621,18.30475 L 71.4091,137.9946 55.897164,149.35045 50.372443,130.93707 31.148226,131.00645 37.501747,112.86236 21.908246,101.61878 37.71318,90.674414 31.706582,72.412538 l 19.2194,0.435725 5.874621,-18.304757 15.292708,11.649389 15.511935,-11.355844 5.524722,18.413382 19.224212,-0.06938 -6.35352,18.144089 15.5935,11.243582 z"
|
||||
transform="matrix(1.2188073,0,0,1.2188073,0.97362416,-14.941195)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#800000;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round"
|
||||
id="path2"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="69.252403"
|
||||
sodipodi:cy="183.84013"
|
||||
sodipodi:r1="76.18972"
|
||||
sodipodi:r2="39.770834"
|
||||
sodipodi:arg1="0.94510027"
|
||||
sodipodi:arg2="1.2659605"
|
||||
inkscape:rounded="0.029345222"
|
||||
inkscape:randomized="0"
|
||||
d="m 113.8738,245.59614 c -0.96661,0.6886 -31.555039,-24.18254 -32.684714,-23.81876 -1.120974,0.36099 -10.958876,38.2632 -12.136488,38.25221 -1.186753,-0.0111 -11.314419,-38.11167 -12.442172,-38.48137 -1.119068,-0.36685 -31.356457,24.51411 -32.302709,23.81304 -0.953595,-0.70651 13.247923,-37.48344 12.552854,-38.44541 -0.689714,-0.95456 -39.7769375,1.40147 -40.1303916,0.27809 -0.3561976,-1.13209 32.7500076,-22.5378 32.7531166,-23.7246 0.0031,-1.17766 -33.0039804,-22.24649 -32.6296302,-23.36307 0.3772559,-1.12525 39.7427032,1.0165 40.4428022,0.0582 0.694707,-0.95093 -13.624624,-37.39704 -12.665459,-38.08034 0.966611,-0.68859 31.555037,24.18255 32.684712,23.81876 1.120974,-0.36098 10.958875,-38.26319 12.136488,-38.2522 1.186752,0.0111 11.314419,38.11167 12.442172,38.48137 1.119067,0.36685 31.356459,-24.51411 32.302709,-23.81304 0.95359,0.70651 -13.24792,37.48344 -12.55285,38.44541 0.68971,0.95456 39.77693,-1.40147 40.13039,-0.2781 0.35619,1.13209 -32.75001,22.53781 -32.75312,23.72461 -0.003,1.17766 33.00398,22.24649 32.62963,23.36307 -0.37726,1.12524 -39.7427,-1.01651 -40.4428,-0.0582 -0.69471,0.95093 13.62462,37.39704 12.66546,38.08033 z"
|
||||
transform="translate(19.276442,-75.320913)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#d45500;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path2-0"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="69.252403"
|
||||
sodipodi:cy="183.84013"
|
||||
sodipodi:r1="3.2911112"
|
||||
sodipodi:r2="71.359137"
|
||||
sodipodi:arg1="1.3521275"
|
||||
sodipodi:arg2="1.5760036"
|
||||
inkscape:rounded="0.029345222"
|
||||
inkscape:randomized="0"
|
||||
d="m 69.966345,187.05287 c -1.9053,0.60813 0.905346,68.33625 -1.085527,68.14543 -1.996473,-0.19135 0.964985,-67.70967 -0.939225,-68.33936 -1.898868,-0.62792 -39.434599,55.81734 -40.933089,54.49275 -1.502705,-1.3283 40.579437,-54.21107 39.409022,-55.83977 -1.167132,-1.62412 -64.7118681,21.9781 -65.1456023,20.0257 -0.43495449,-1.95789 64.6939243,-20.00568 64.7043623,-22.01128 0.01041,-1.99997 -65.27140303,-20.25603 -64.4747102,-22.09049 0.7989341,-1.83963 64.0975312,21.84119 65.2848352,20.22477 1.183973,-1.61189 -40.89948,-54.75304 -39.17667,-55.76887 1.727657,-1.01869 39.01806,55.34548 40.92872,54.73564 1.905301,-0.60812 -0.905346,-68.33625 1.085527,-68.14543 1.996474,0.19136 -0.964984,67.70968 0.939225,68.33937 1.898868,0.62792 39.434597,-55.81734 40.933087,-54.49276 1.50271,1.32831 -40.579435,54.21108 -39.409019,55.83977 1.167131,1.62413 64.711869,-21.97809 65.145599,-20.0257 0.43496,1.9579 -64.693921,20.00569 -64.704359,22.01129 -0.01041,1.99997 65.271399,20.25602 64.474709,22.09049 -0.79893,1.83963 -64.09753,-21.8412 -65.284834,-20.22477 -1.183973,1.61189 40.899484,54.75304 39.176674,55.76887 -1.72766,1.01869 -39.018064,-55.34548 -40.928725,-54.73565 z"
|
||||
transform="translate(19.25264,-74.484719)" />
|
||||
<path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ffb380;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
|
||||
id="path2-0-7"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="10"
|
||||
sodipodi:cx="69.252403"
|
||||
sodipodi:cy="183.84013"
|
||||
sodipodi:r1="2.0329688"
|
||||
sodipodi:r2="36.653175"
|
||||
sodipodi:arg1="1.3051581"
|
||||
sodipodi:arg2="1.5885803"
|
||||
inkscape:rounded="0.029345222"
|
||||
inkscape:randomized="0"
|
||||
d="m 69.786109,185.8018 c -0.974051,0.29744 -0.168255,34.73512 -1.18551,34.68571 -1.01845,-0.0495 0.894533,-34.41437 -0.06946,-34.74665 -0.962858,-0.3319 -20.552916,28.00241 -21.346851,27.36451 -0.794867,-0.63865 20.951954,-27.31602 20.367382,-28.15147 -0.583886,-0.83446 -33.087061,10.57373 -33.354419,9.59099 -0.267673,-0.98389 33.006439,-9.78387 33.024571,-10.80336 0.01811,-1.01829 -32.983073,-10.89376 -32.621734,-11.84596 0.361764,-0.95332 32.453587,11.48538 33.067497,10.67125 0.61319,-0.81317 -20.280673,-28.2002 -19.428655,-28.75816 0.853019,-0.55861 19.504568,28.36761 20.479763,28.06981 0.974051,-0.29744 0.168255,-34.73512 1.18551,-34.68572 1.01845,0.0495 -0.894534,34.41438 0.06946,34.74666 0.962858,0.3319 20.552915,-28.00241 21.34685,-27.36451 0.794868,0.63865 -20.951953,27.31602 -20.367382,28.15146 0.583886,0.83446 33.087059,-10.57372 33.354419,-9.59099 0.26767,0.98389 -33.006439,9.78388 -33.024571,10.80337 -0.01811,1.01829 32.983071,10.89376 32.621731,11.84596 -0.36176,0.95332 -32.453584,-11.48538 -33.067494,-10.67125 -0.613189,0.81317 20.280674,28.2002 19.428655,28.75816 C 89.412852,214.43022 70.761304,185.504 69.786109,185.8018 Z"
|
||||
transform="rotate(-17.984043,-157.18948,86.992365)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 8.3 KiB |
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,41 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
PROVIDER=ollama
|
||||
MODELS=granite3-dense:8b mannix/jan-nano:latest yi-coder:latest qwen3:latest deepseek-coder-v2:latest hhao/qwen2.5-coder-tools:latest qwen2.5-coder:latest
|
||||
MODELS=gemma3:1b
|
||||
LANGS=Dart Perl Ruby Javascript PHP C
|
||||
BINARIES=files/complex1 files/complex2 files/complex3
|
||||
MODELS=gpt-4o
|
||||
PROVIDER=openai
|
||||
|
||||
all:
|
||||
mkdir -p tmp
|
||||
for b in $(BINARIES) ; do \
|
||||
for a in $(MODELS) ; do \
|
||||
echo "$$b $$a"; \
|
||||
./run.sh $$b $$a ${PROVIDER} ${LANGS} > tmp/$${b##*/}_$${a//\//_}.html ; \
|
||||
done ; \
|
||||
done
|
||||
|
||||
.PHONY: run index evaluate
|
||||
|
||||
compile: $(BINARIES)
|
||||
|
||||
files/complex1: files/complex1.c
|
||||
gcc -o $@ $<
|
||||
|
||||
files/complex2: files/complex2.c
|
||||
gcc -o $@ $<
|
||||
|
||||
files/complex3: files/complex3.c
|
||||
gcc -o $@ $<
|
||||
|
||||
lmstudio:
|
||||
for b in $(BINARIES) ; do \
|
||||
for a in $(MODELS) ; do \
|
||||
./run.sh $$b $$a lmstudio ${LANGS} > $${b##*/}_$${a//\//_}.html ; \
|
||||
done ; \
|
||||
done
|
||||
|
||||
clean:
|
||||
rm -rf tmp
|
||||
File renamed without changes.
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Evaluate decompiled outputs using mai and determine best model+lang
|
||||
|
||||
echo "Evaluating decompiled outputs..."
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
MAI="mai -q -p openai -m gpt-4o"
|
||||
# MAI="mai -q"
|
||||
|
||||
for orig in files/complex1.c files/complex2.c files/complex3.c; do
|
||||
base=$(basename $orig .c)
|
||||
original_code=$(cat $orig)
|
||||
for f in tmp/${base}_*.txt; do
|
||||
if [ -f "$f" ]; then
|
||||
decompiled_code=$(cat $f)
|
||||
#echo "Comparing $orig $f"
|
||||
result=`echo n | ${MAI} -q -r "/template files/CMPROMPT.txt original=@${orig} decompiled=@${f}"`
|
||||
score=`echo "$result" | head -n 1 | grep -oE '[0-9]+'` # |head -n 1`
|
||||
perfect=`echo "$result" | grep 'No sign' && echo yes`
|
||||
[ -z "$score" ] && score=0
|
||||
if [ "$perfect" = yes ]; then
|
||||
score=100
|
||||
else
|
||||
score="$((0+${score}))"
|
||||
fi
|
||||
echo "$score\t$f"
|
||||
if [ -n "$score" ]; then
|
||||
key="${f%.*}" # remove .txt
|
||||
# Check if key exists in tmpfile
|
||||
existing=$(grep "^$key " $tmpfile | awk '{print $2}')
|
||||
if [ -n "$existing" ]; then
|
||||
new_score=$(( existing + score ))
|
||||
sed -i "s/^$key .*/$key $new_score/" $tmpfile
|
||||
else
|
||||
echo "$key $score" >> $tmpfile
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Find the best (highest total score)
|
||||
best_key=""
|
||||
best_score=0
|
||||
while read key score; do
|
||||
if [ "$score" -gt "$best_score" ]; then
|
||||
best_score=$score
|
||||
best_key=$key
|
||||
fi
|
||||
done < $tmpfile
|
||||
|
||||
rm $tmpfile
|
||||
|
||||
if [ -n "$best_key" ]; then
|
||||
echo "Best model+language combination: $best_key with total score $best_score"
|
||||
else
|
||||
echo "No scores found"
|
||||
fi
|
||||
@@ -0,0 +1,17 @@
|
||||
Compare the two provided code in C with the decompiled counterpart. Ignore function or variable names, syntax differences, language-specific constructs, and formatting. Focus solely on what the code does: the operations performed, control flow, logic, and functional behavior. Determine if they are functionally equivalent in terms of their effects and computations.
|
||||
|
||||
Output only:
|
||||
- A numeric score from 1 to 100 (100 being identical logic and functionality)
|
||||
- Followed by a single sentence describing the most relevant semantic differences (or "No significant semantic differences" if score is 95+)
|
||||
|
||||
**Original Source Code:**
|
||||
|
||||
```C
|
||||
{original}
|
||||
```
|
||||
|
||||
**Decompiled Code:**
|
||||
|
||||
```
|
||||
{decompiled}
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main() {
|
||||
int n = 5;
|
||||
int *arr = (int *)malloc(n * sizeof(int));
|
||||
if (arr == NULL) return 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
arr[i] = i * i;
|
||||
}
|
||||
int sum = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
sum += arr[i];
|
||||
for (int j = 0; j < i; j++) {
|
||||
sum -= j;
|
||||
}
|
||||
}
|
||||
printf("Sum: %d\n", sum);
|
||||
free(arr);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
struct Node {
|
||||
int data;
|
||||
struct Node *next;
|
||||
};
|
||||
|
||||
int main() {
|
||||
struct Node *head = NULL;
|
||||
struct Node *tail = NULL;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
|
||||
if (newNode == NULL) return 1;
|
||||
newNode->data = i * 2;
|
||||
newNode->next = NULL;
|
||||
if (head == NULL) {
|
||||
head = newNode;
|
||||
tail = newNode;
|
||||
} else {
|
||||
tail->next = newNode;
|
||||
tail = newNode;
|
||||
}
|
||||
}
|
||||
struct Node *current = head;
|
||||
int sum = 0;
|
||||
while (current != NULL) {
|
||||
sum += current->data;
|
||||
current = current->next;
|
||||
}
|
||||
printf("Sum: %d\n", sum);
|
||||
current = head;
|
||||
while (current != NULL) {
|
||||
struct Node *temp = current;
|
||||
current = current->next;
|
||||
free(temp);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
int x = 42;
|
||||
int y = 0;
|
||||
switch (x & 15) {
|
||||
case 0:
|
||||
y = x << 1;
|
||||
break;
|
||||
case 1:
|
||||
y = x >> 1;
|
||||
break;
|
||||
case 2:
|
||||
y = x ^ 255;
|
||||
break;
|
||||
case 3:
|
||||
y = ~x;
|
||||
break;
|
||||
default:
|
||||
y = x | 128;
|
||||
break;
|
||||
}
|
||||
if ((y & 1) == 0) {
|
||||
y += 10;
|
||||
} else {
|
||||
y -= 5;
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
y = (y << 2) | (y >> 30);
|
||||
}
|
||||
printf("Result: %d\n", y);
|
||||
return 0;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
cp decai/BENCH.txt tmp/BENCH.txt
|
||||
# Find all directories containing BENCH.txt
|
||||
dirs=$(find . -name "BENCH.txt" -type f | xargs dirname | sort | uniq)
|
||||
|
||||
@@ -46,4 +47,4 @@ cat <<EOF >> benchmark_index.html
|
||||
<br />
|
||||
<br />
|
||||
</html>
|
||||
EOF
|
||||
EOF
|
||||
@@ -1,12 +1,55 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Generate HTML output for command executions with collapsible sections and width control
|
||||
# If PLAIN=1, output plain text instead
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 command1 [command2 ...]"
|
||||
if [ $# -lt 4 ]; then
|
||||
echo "Usage: $0 <binary> <model> <provider> <lang1> [lang2 ...]"
|
||||
echo "Or: $0 command1 [command2 ...] (legacy)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if first arg is a file (binary), assume new format
|
||||
if [ -f "$1" ]; then
|
||||
binary="$1"
|
||||
model="$2"
|
||||
provider="$3"
|
||||
shift 3
|
||||
langs=("$@")
|
||||
commands=()
|
||||
for lang in "${langs[@]}"; do
|
||||
# commands+=("# $lang echo \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" | r2 -q $binary 2> /dev/null")
|
||||
commands+=("# $lang r2 -e scr.color=0 -qcq -qc \"decai -e model=$model;decai -e lang=$lang;decai -e api=$provider;af;decai -d\" $binary")
|
||||
done
|
||||
set -- "${commands[@]}"
|
||||
else
|
||||
# Legacy mode
|
||||
commands=("$@")
|
||||
fi
|
||||
|
||||
if [ "$PLAIN" = "1" ]; then
|
||||
# In plain mode, execute each command and print raw output.
|
||||
# Also, if TXTFILE is set, save combined raw output there.
|
||||
tmp_plain=$(mktemp)
|
||||
for cmd in "$@"; do
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
|
||||
exec_cmd="${BASH_REMATCH[1]}"
|
||||
else
|
||||
exec_cmd="$cmd"
|
||||
fi
|
||||
tmpout=$(mktemp)
|
||||
eval "$exec_cmd" > "$tmpout" 2>&1 || true
|
||||
mv "$tmpout" "$tmp_plain"
|
||||
rm -f "$tmpout"
|
||||
done
|
||||
if [ -n "$TXTFILE" ]; then
|
||||
mv "$tmp_plain" "$TXTFILE"
|
||||
else
|
||||
rm -f "$tmp_plain"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Timeout in seconds for each command (configurable via TIMEOUT environment variable)
|
||||
TIMEOUT=${TIMEOUT:-30}
|
||||
|
||||
@@ -109,24 +152,27 @@ EOF
|
||||
|
||||
# Process each command
|
||||
for cmd in "$@"; do
|
||||
# Extract title: if command starts with #, use the rest as title, else use the command
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*(.*) ]]; then
|
||||
# Extract title: if command starts with #, parse title and command, else use the command
|
||||
if [[ "$cmd" =~ ^#[[:space:]]*([^[:space:]]+)[[:space:]]*(.*) ]]; then
|
||||
title="${BASH_REMATCH[1]}"
|
||||
exec_cmd="${cmd#*#}"
|
||||
exec_cmd="${exec_cmd#"${exec_cmd%%[![:space:]]*}"}" # trim leading spaces
|
||||
exec_cmd="${BASH_REMATCH[2]}"
|
||||
else
|
||||
title="$cmd"
|
||||
exec_cmd="$cmd"
|
||||
fi
|
||||
|
||||
echo "RUN $exec_cmd" > /dev/stderr
|
||||
# Execute command and time it
|
||||
start_time=$(date +%s.%N)
|
||||
tmpfile=$(mktemp)
|
||||
eval "$exec_cmd" > "$tmpfile" 2>&1 &
|
||||
# echo "eval $exec_cmd > $tmpfile" > /dev/stder
|
||||
eval "$exec_cmd" > "$tmpfile" 2> /dev/null &
|
||||
pid=$!
|
||||
(sleep $TIMEOUT && kill $pid 2>/dev/null) &
|
||||
wait $pid 2>/dev/null
|
||||
output=$(cat "$tmpfile")
|
||||
#echo "$output" > /dev/stderr
|
||||
|
||||
rm "$tmpfile"
|
||||
exit_code=$?
|
||||
end_time=$(date +%s.%N)
|
||||
@@ -139,8 +185,16 @@ for cmd in "$@"; do
|
||||
execution_time=$(printf "%.3f" "$execution_time")
|
||||
fi
|
||||
|
||||
# Save plain output to .txt file if in new format
|
||||
if [ -n "$binary" ]; then
|
||||
txtfile="tmp/${binary##*/}_${model//\//_}_${title}.txt"
|
||||
mkdir -p "$(dirname "$txtfile")" 2>/dev/null || true
|
||||
echo "$output" > "$txtfile"
|
||||
fi
|
||||
|
||||
# Escape HTML in output
|
||||
escaped_output=$(echo "$output" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||
#escaped_output=$(echo "$output" | sed 's/&/\&/g; s/</\</g; s/>/\>/g')
|
||||
escaped_output="$output"
|
||||
|
||||
cat << EOF
|
||||
<div class="command-section expanded">
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,57 +0,0 @@
|
||||
# R2AI Prompts
|
||||
|
||||
This directory contains predefined prompt files for the r2ai plugin.
|
||||
|
||||
## Format
|
||||
|
||||
Each prompt is stored in a `.r2ai.md` file with YAML-style frontmatter and a markdown body.
|
||||
|
||||
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
|
||||
- `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 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`
|
||||
|
||||
## 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
|
||||
|
||||
## Future Features
|
||||
|
||||
- Support for r2js scripts
|
||||
- More advanced conditionals
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Automatically suggest a better name for this function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: afv;r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Suggest a better name for this function. Output only the suggested function name (must be one alphanumeric word) prefixed with `'afn '. Check the code below:
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Compact conversation history into a summary'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: r2ai -C
|
||||
if-empty: exit
|
||||
---
|
||||
Create a compact summary of this conversation that preserves all key information, insights, and context for future reference. Focus on essential details about the binary analysis, tools used, findings, and any important conclusions.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Augmented decompilation based on LLM'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdc
|
||||
if-empty: exit
|
||||
---
|
||||
$(-e r2ai.prompt). Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Find and explain devices used'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: izqq~^/dev
|
||||
if-empty: exit
|
||||
---
|
||||
explain in one line the purpose of each of these devices
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'List libraries loaded with dlopen'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: iiq~dlopen
|
||||
if-empty: This binary is not using dlopen
|
||||
---
|
||||
Some libraries are loaded
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Explain the current function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: r2ai -d
|
||||
if-empty: exit
|
||||
---
|
||||
Analyze function calls, comments and strings, ignore registers and memory accesses. Considering the references and involved loops; explain the purpose of this function in one or two short sentences. Output must be only the translation of the explanation in $(-e r2ai.hlang)
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Group imports by Libraries'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: iiq;il
|
||||
if-empty: exit
|
||||
---
|
||||
Group the imports that belong to each library and describe its purpose and usage scope
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Suggest an improved function signature'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: ?e variables;afv;?e function name:;afd;?e current signature:?e afs
|
||||
if-empty: exit
|
||||
---
|
||||
Analyze the uses of the arguments and return value to infer the signature, identify which is the correct type for the resturn. Do NOT print the function body. Output must be *ONLY* the function signature prefixed with the `'afs ` text
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
description: 'Decompile this code into PHP'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdg
|
||||
if-empty: exit
|
||||
---
|
||||
Your goal is to $(-e r2ai.prompt).
|
||||
|
||||
Translate the code into $(-e r2ai.lang) programming language.
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Better variable names'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: afv
|
||||
if-empty: exit
|
||||
---
|
||||
Output only the radare2 command without markdown, guess a better name and type for each local variable and function argument taking using. output an r2 script using afvn and afvt commands
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
description: 'Find vulnerabilities or bugs in the current function'
|
||||
version: 0.1
|
||||
author: pancake
|
||||
command: pdc
|
||||
if-empty: exit
|
||||
---
|
||||
Find vulnerabilities, do not show the code, only a short summary of the resulting analysis report including suggested good practices to address the problem and a practical exploit example.
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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')
|
||||
```
|
||||
@@ -1,9 +0,0 @@
|
||||
EXTERNAL_PLUGINS+=r2ai
|
||||
# EXTERNAL_PLUGINS+=hi
|
||||
|
||||
.PHONY: r2ai
|
||||
|
||||
r2ai: p/r2ai
|
||||
|
||||
p/r2ai:
|
||||
cd p && git clone https://github.com/radareorg/r2ai
|
||||
@@ -1,3 +0,0 @@
|
||||
#ifdef R2_CORE_H
|
||||
extern RCorePlugin r_core_plugin_r2ai;
|
||||
#endif
|
||||
@@ -1,23 +0,0 @@
|
||||
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)
|
||||
@@ -1,27 +0,0 @@
|
||||
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 +0,0 @@
|
||||
include p/r2ai/r2plugin/core/deps.mk
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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',
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
#ifdef R2_CORE_H
|
||||
#include "core/deps.h"
|
||||
#endif
|
||||
@@ -1 +0,0 @@
|
||||
core.r2ai
|
||||
@@ -0,0 +1,40 @@
|
||||
BasedOnStyle: LLVM
|
||||
Language: Cpp
|
||||
PointerAlignment: Right
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
BinPackParameters: true
|
||||
MaxEmptyLinesToKeep: 1
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpaceBeforeParens: Custom
|
||||
SpaceBeforeParensOptions:
|
||||
AfterIfMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterForeachMacros: true
|
||||
AfterControlStatements: true
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpacesInParentheses: false
|
||||
InsertBraces: true
|
||||
ContinuationIndentWidth: 8
|
||||
IndentCaseLabels: false
|
||||
IndentFunctionDeclarationAfterType: false
|
||||
IndentWidth: 8
|
||||
UseTab: ForContinuationAndIndentation
|
||||
ColumnLimit: 0
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeTernaryOperators: true
|
||||
SpaceBeforeSquareBrackets: false
|
||||
SpaceInEmptyBlock: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AlignConsecutiveMacros: true
|
||||
AlignTrailingComments: false
|
||||
AlignOperands: false
|
||||
Cpp11BracedListStyle: false
|
||||
ForEachMacros: ['r_list_foreach', 'ls_foreach', 'fcn_tree_foreach_intersect', 'r_skiplist_foreach', 'graph_foreach_anode', 'r_list_foreach_safe', 'r_pvector_foreach', 'r_rbtree_foreach', 'r_interval_ tree_foreach']
|
||||
SortIncludes: false
|
||||
@@ -1,33 +0,0 @@
|
||||
# R2AI Plugin for radare2
|
||||
|
||||
## Locations
|
||||
|
||||
- All the source code of the plugin is in the current directory
|
||||
- Radare2 headers use to be in /usr/local/include/libr
|
||||
|
||||
## Coding Style
|
||||
|
||||
- `R_NEW` and `R_NEW0` calls never return NULL, so there's no need to check after calling them
|
||||
- Use the radare2 coding style rules
|
||||
- Always add a whitespace before `(` in function calls
|
||||
- Indent the code with tabs
|
||||
- Indent comments with spaces
|
||||
- Do not define variables inside the `for` parenthesis
|
||||
- Do not check for null before calling free
|
||||
|
||||
## Test
|
||||
|
||||
Run `r2ai` cli tool or radare2 oneliners running the `r2ai` command.
|
||||
|
||||
* `r2pm -r r2ai -h` -> help message for the r2ai tool
|
||||
* `r2 -qc 'r2ai -h' /bin/ls -> help message of the r2ai plugin
|
||||
* `r2 -qc 'r2ai -a what arch is used here' -> testing auto mode
|
||||
* `r2 -e r2ai.model=gemma3:12b -e r2ai.auto.raw=true -qc 'r2ai -a what arch is used here' -> testing auto mode
|
||||
|
||||
## Actions
|
||||
|
||||
Run the following commands to perform the action described
|
||||
|
||||
- FORMAT: `make fmt`
|
||||
- COMPILE: `make` (with no extra arguments)
|
||||
- INSTALL: `make user-install`
|
||||
+12
-57
@@ -1,32 +1,17 @@
|
||||
DOTEXE=
|
||||
# DOTEXE=.exe
|
||||
DOTLIB=.$(shell r2 -NNH R2_LIBEXT)
|
||||
DOTLIB=.$(shell r2 -H R2_LIBEXT)
|
||||
|
||||
R2SYSPLUGDIR=$(shell r2 -NNH R2_LIBR_PLUGINS)
|
||||
R2USRPLUGDIR=$(shell r2 -NNH R2_USER_PLUGINS)
|
||||
CFLAGS+=$(shell pkg-config --cflags r_core)
|
||||
LDFLAGS+=$(shell pkg-config --libs r_core)
|
||||
R2SYSPLUGDIR=$(shell r2 -H R2_LIBR_PLUGINS)
|
||||
R2USRPLUGDIR=$(shell r2 -H R2_USER_PLUGINS)
|
||||
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
|
||||
endif
|
||||
|
||||
R2_CFLAGS=$(shell pkg-config --cflags r_core)
|
||||
R2_LDFLAGS=$(shell pkg-config --libs r_core)
|
||||
ifeq ($(R2_CFLAGS),)
|
||||
R2_CFLAGS=$(shell r2 -NNH R2_CFLAGS)
|
||||
R2_LDFLAGS=$(shell r2 -NNH R2_LDFLAGS)
|
||||
endif
|
||||
|
||||
CFLAGS+=$(R2_CFLAGS)
|
||||
LDFLAGS+=$(R2_LDFLAGS)
|
||||
CFLAGS+=-I. -fPIC
|
||||
CFLAGS+=-Wall
|
||||
CFLAGS+=-g
|
||||
|
||||
AR?=ar
|
||||
|
||||
USE_R2_CURL=1
|
||||
USE_LIBCURL=0
|
||||
|
||||
@@ -46,28 +31,17 @@ ifeq ($(USE_LIBCURL),1)
|
||||
endif
|
||||
|
||||
OBJS=r2ai.o
|
||||
OBJS+=apikeys.o
|
||||
OBJS+=async.o
|
||||
OBJS+=claw.o
|
||||
OBJS+=llm.o
|
||||
OBJS+=auto.o
|
||||
OBJS+=json.o
|
||||
OBJS+=vdb.o
|
||||
OBJS+=tools.o
|
||||
OBJS+=messages.o
|
||||
OBJS+=anthropic.o
|
||||
OBJS+=gemini.o
|
||||
OBJS+=vertex.o
|
||||
OBJS+=http.o
|
||||
OBJS+=r2ai_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
|
||||
@@ -80,43 +54,24 @@ doc-install:
|
||||
|
||||
vdb.o: vdb_embed.inc.c
|
||||
|
||||
fmt format indent:
|
||||
clang-format-radare2 *.c http/*.c
|
||||
indent:
|
||||
for a in *.c ; do python indent.py $$a ; done
|
||||
|
||||
user-install: user-uninstall
|
||||
mkdir -p $(R2USRPLUGDIR)
|
||||
mkdir -p $(R2BINDIR)
|
||||
mkdir -p $(R2MANDIR)/man1
|
||||
mkdir -p $(HOME)/.config/r2ai/prompts
|
||||
$(MAKE) user-uninstall
|
||||
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
cp -f r2ai.1 $(R2MANDIR)/man1/r2ai.1
|
||||
cp -r ../prompts/* $(HOME)/.config/r2ai/prompts/
|
||||
cp -f r2ai$(DOTLIB) $(R2USRPLUGDIR)
|
||||
cp -f r2ai$(DOTEXE) $(R2BINDIR)
|
||||
|
||||
user-uninstall:
|
||||
rm -f $(R2USRPLUGDIR)/r2ai$(DOTLIB)
|
||||
rm -f $(R2BINDIR)/r2ai$(DOTEXE)
|
||||
rm -f $(R2MANDIR)/man1/r2ai.1
|
||||
|
||||
install:
|
||||
$(MAKE) uninstall
|
||||
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
|
||||
cp -f r2ai$(DOTLIB) $(R2SYSPLUGDIR)
|
||||
|
||||
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 --
|
||||
@r2 -qcq --
|
||||
clean:
|
||||
rm -f *.o *.d
|
||||
+19
-49
@@ -1,10 +1,12 @@
|
||||
# R2AI
|
||||
# C-R2AI
|
||||
|
||||
The r2ai plugin for radare2.
|
||||
This directory contains the C rewrite of the r2ai plugin for radare2.
|
||||
|
||||
## Installation
|
||||
|
||||
Requires a C compiler and modern version of radare2 if you want to use the stream HTTP APIs. It will fallback to curl or powershell if not available and `R2_CURL` environment is set to `1`.
|
||||
Requires radare2 from git if you want to use the stream HTTP APIs.
|
||||
|
||||
Otherwise the only requirement is a C compiler.
|
||||
|
||||
```console
|
||||
make
|
||||
@@ -33,59 +35,26 @@ 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.'
|
||||
| r2ai -e Same as '-e r2ai.'
|
||||
| r2ai -h Show this help message
|
||||
| r2ai -i [file] [query] read file and ask the llm with the given query
|
||||
| r2ai -m show selected model, list suggested ones, choose one
|
||||
| r2ai -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 -n suggest a better name for the current function
|
||||
| r2ai -r enter the chat repl
|
||||
| r2ai -w launch the interactive setup wizard
|
||||
| r2ai -L show chat logs (See -Lj for json). Only for auto mode.
|
||||
| r2ai -L-[N] delete the last (or N last messages from the chat history)
|
||||
| r2ai -R reset the chat conversation context
|
||||
| r2ai -Rq ([text]) refresh and query embeddings (see r2ai.data)
|
||||
| r2ai [query] query the selected model+provider with the given query
|
||||
| r2ai -s function signature
|
||||
| r2ai -x explain current function
|
||||
| r2ai -v suggest better variables names and types
|
||||
| r2ai -V[r] find vulnerabilities in the decompiled code (-Vr uses -dr)
|
||||
| r2ai [arg] send a post request to talk to r2ai and print the output
|
||||
[0x100003f58]>
|
||||
```
|
||||
|
||||
## Query Prompts
|
||||
|
||||
Query prompts are predefined templates that combine radare2 commands with structured prompts to the LLM for common reverse engineering tasks. They are stored in the `prompts/` directory as `.r2ai.md` files with YAML-style frontmatter. Legacy `.r2ai.txt` prompts are still supported.
|
||||
|
||||
### Available Prompts
|
||||
|
||||
- `autoname`: Automatically rename functions and variables
|
||||
- `decompile`: Decompile the current function
|
||||
- `devices`: Analyze device-related code
|
||||
- `dlopen`: Analyze dynamic library loading
|
||||
- `explain`: Explain the purpose of the current function
|
||||
- `libs`: Analyze library usage
|
||||
- `signature`: Improve function signatures
|
||||
- `varnames`: Rename variables based on context
|
||||
- `vulns`: Find vulnerabilities in the current function
|
||||
|
||||
### Usage
|
||||
|
||||
- `r2ai -q`: List all available query prompts
|
||||
- `r2ai -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
|
||||
|
||||
### Example
|
||||
|
||||
```console
|
||||
[0x00001303]> r2ai -q explain
|
||||
The main function implements a simple substitution cipher program that reads user input, encrypts it using a hardcoded alphabet, and then decrypts it back to demonstrate the algorithm.
|
||||
```
|
||||
|
||||
Query prompts automate repetitive analysis tasks by executing predefined radare2 commands and sending context-aware prompts to the LLM.
|
||||
|
||||
## Configuration
|
||||
|
||||
These can be set with `r2ai -e <keyname>=<value>`
|
||||
@@ -101,13 +70,11 @@ These can be set with `r2ai -e <keyname>=<value>`
|
||||
| r2ai.lang | Tells LLM which programming language to use for decompilation result. Only works for `r2ai -d` |
|
||||
| r2ai.hlang | Tells LLM in which language to speak |
|
||||
| r2ai.prompt | User prompt to send to LLM with `r2ai -d` |
|
||||
| r2ai.clippy | Display chat replies using `r2clippy` instead of plain text |
|
||||
| r2ai.wizard | Auto-start the setup wizard on the first interactive chat session |
|
||||
| r2ai.auto.max_runs | Maximum loops when using auto mode `r2ai -a` |
|
||||
| r2ai.auto.verbose | Only for auto mode `r2ai -a`. Will show the output of the tool which ran locally |
|
||||
| r2ai.auto.hide_tool_output | Only for auto mode `r2ai -a`. Will not 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
|
||||
@@ -121,7 +88,7 @@ $ r2 ./substitute
|
||||
[0x000010a0]> aa
|
||||
```
|
||||
|
||||
- Decompile the main function: `r2ai -d` or `r2ai -do` to keep source offsets in the left column
|
||||
- Decompile the main function: `r2ai -d`
|
||||
|
||||
```
|
||||
[0x000010a0]> s main
|
||||
@@ -248,6 +215,8 @@ Note: System prompt is applied automatically but not stored in history
|
||||
{"command":"pdf @ main"}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
* add "undo" command to drop the last message
|
||||
@@ -256,6 +225,7 @@ Note: System prompt is applied automatically but not stored in history
|
||||
* keep context between direct commands unless explicitly reset
|
||||
* modify context if tool command is user edited
|
||||
|
||||
|
||||
## Messages API
|
||||
|
||||
The r2ai project includes a new API for managing messages and tool calls in conversations with AI models. This API is implemented in `messages.c` and `messages.h`.
|
||||
@@ -298,4 +268,4 @@ In r2ai, as lm-studio uses an OpenAI-like API, configure:
|
||||
|
||||
- `r2ai -e api=openai`
|
||||
- `r2ai -e baseurl=http://LM-STUDIO-IP:PORT`
|
||||
- `r2ai -e model=?` to list available models
|
||||
- `r2ai -e model=?` to list available models
|
||||
+62
-58
@@ -1,17 +1,14 @@
|
||||
/* Copyright r2ai - 2023-2026 - pancake */
|
||||
|
||||
#include "r2ai.h"
|
||||
|
||||
static const char *modelname(const char *model_name) {
|
||||
return model_name? model_name: "claude-3-7-sonnet-20250219";
|
||||
return model_name ? model_name : "claude-3-7-sonnet-20250219";
|
||||
}
|
||||
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args) {
|
||||
RCore *core = cps->core;
|
||||
R_IPI R2AI_ChatResponse *r2ai_anthropic(RCore *core, R2AIArgs args) {
|
||||
const char *model = args.model;
|
||||
char **error = args.error;
|
||||
RList *tools = args.tools;
|
||||
RList *messages_input = args.messages;
|
||||
const R2AI_Tools *tools = args.tools;
|
||||
R2AI_Messages *messages_input = args.messages;
|
||||
|
||||
if (error) {
|
||||
*error = NULL;
|
||||
@@ -43,7 +40,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
// Create messages JSON
|
||||
char *messages_json = NULL;
|
||||
|
||||
if (messages_input && !r_list_empty (messages_input)) {
|
||||
if (messages_input && messages_input->n_messages > 0) {
|
||||
// Convert directly to JSON without filtering
|
||||
messages_json = r2ai_msgs_to_anthropic_json (messages_input);
|
||||
if (!messages_json) {
|
||||
@@ -63,7 +60,7 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
|
||||
// Convert tools to Anthropic format if available
|
||||
char *anthropic_tools_json = NULL;
|
||||
if (tools && !r_list_empty (tools)) {
|
||||
if (tools && tools->n_tools > 0) {
|
||||
anthropic_tools_json = r2ai_tools_to_anthropic_json (tools);
|
||||
}
|
||||
|
||||
@@ -101,26 +98,10 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
free (messages_json);
|
||||
|
||||
// Save the full JSON for debugging
|
||||
char *tmpdir = r_file_tmpdir ();
|
||||
char *req_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_request.json", tmpdir);
|
||||
r_file_dump (req_path, (const ut8 *)data, strlen (data), 0);
|
||||
R_LOG_DEBUG ("Full request saved to %s", req_path);
|
||||
free (req_path);
|
||||
free (tmpdir);
|
||||
|
||||
r_file_dump ("/tmp/r2ai_anthropic_request.json", (const ut8 *)data, strlen (data), 0);
|
||||
R_LOG_DEBUG ("Full request saved to /tmp/r2ai_anthropic_request.json");
|
||||
R_LOG_DEBUG ("Anthropic API request data: %s", data);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
// Generate curl command for debugging
|
||||
RStrBuf *curl_cmd = r_strbuf_new ("curl -X POST");
|
||||
for (int i = 0; headers[i]; i++) {
|
||||
r_strbuf_appendf (curl_cmd, " -H \"%s\"", headers[i]);
|
||||
}
|
||||
r_strbuf_appendf (curl_cmd, " -d '%s' \"%s\"", data, anthropic_url);
|
||||
eprintf ("Curl command: %s\n", r_strbuf_get (curl_cmd));
|
||||
r_strbuf_free (curl_cmd);
|
||||
}
|
||||
|
||||
// Make the API call
|
||||
int code = 0;
|
||||
char *res = r2ai_http_post (core, anthropic_url, headers, data, &code, NULL);
|
||||
@@ -139,32 +120,28 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic(RCorePluginSession *cps, R2AIArgs args)
|
||||
}
|
||||
|
||||
// Save the response for inspection
|
||||
tmpdir = r_file_tmpdir ();
|
||||
char *res_path = r_str_newf ("%s" R_SYS_DIR "r2ai_anthropic_response.json", tmpdir);
|
||||
r_file_dump (res_path, (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("Anthropic API response saved to %s", res_path);
|
||||
free (res_path);
|
||||
free (tmpdir);
|
||||
r_file_dump ("/tmp/r2ai_anthropic_response.json", (const ut8 *)res, strlen (res), 0);
|
||||
R_LOG_DEBUG ("Anthropic API response saved to /tmp/r2ai_anthropic_response.json");
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
|
||||
if (r_config_get_b (core->config, "r2ai.debug")) {
|
||||
R_LOG_DEBUG ("Anthropic API response: %s", res);
|
||||
// Parse the response
|
||||
R2AI_ChatResponse *result = R_NEW0 (R2AI_ChatResponse);
|
||||
if (!result) {
|
||||
free (res);
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for response");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
R2AI_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 (json);
|
||||
char *response_copy = strdup (res);
|
||||
if (response_copy) {
|
||||
RJson *jres = r_json_parse (response_copy);
|
||||
if (jres) {
|
||||
// Create a new message structure
|
||||
message = R_NEW0 (R2AI_Message);
|
||||
|
||||
const RJson *usage_json = r_json_get (jres, "usage");
|
||||
@@ -188,11 +165,14 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
|
||||
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");
|
||||
@@ -204,8 +184,9 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate tool_calls array if needed
|
||||
if (has_tool_use && n_tool_calls > 0) {
|
||||
message->tool_calls = r_list_new ();
|
||||
message->tool_calls = R_NEWS0 (R2AI_ToolCall, n_tool_calls);
|
||||
if (!message->tool_calls) {
|
||||
if (error) {
|
||||
*error = strdup ("Failed to allocate memory for tool calls");
|
||||
@@ -213,26 +194,41 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
message->tool_calls->free = (RListFree)r2ai_tool_call_free;
|
||||
message->n_tool_calls = n_tool_calls;
|
||||
}
|
||||
|
||||
// Process each content item
|
||||
int tool_idx = 0;
|
||||
if (content_array && content_array->type == R_JSON_ARRAY) {
|
||||
RList *cb = r2ai_content_blocks_new ();
|
||||
R2AI_ContentBlocks *cb = R_NEW0 (R2AI_ContentBlocks);
|
||||
if (!cb) {
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
cb->n_blocks = content_array->children.count;
|
||||
cb->blocks = R_NEWS0 (R2AI_ContentBlock, cb->n_blocks);
|
||||
if (!cb->blocks) {
|
||||
free (cb);
|
||||
r_json_free (jres);
|
||||
free (response_copy);
|
||||
r2ai_message_free (message);
|
||||
free (res);
|
||||
return NULL;
|
||||
}
|
||||
int block_idx = 0;
|
||||
const RJson *content_item = content_array->children.first;
|
||||
while (content_item) {
|
||||
while (content_item && block_idx < cb->n_blocks) {
|
||||
const RJson *type = r_json_get (content_item, "type");
|
||||
if (type && type->type == R_JSON_STRING) {
|
||||
R2AI_ContentBlock *block = R_NEW0 (R2AI_ContentBlock);
|
||||
R2AI_ContentBlock *block = &cb->blocks[block_idx];
|
||||
if (!strcmp (type->str_value, "text")) {
|
||||
// Text content
|
||||
const RJson *text = r_json_get (content_item, "text");
|
||||
if (text && text->type == R_JSON_STRING) {
|
||||
r_strbuf_append (content_buf, text->str_value);
|
||||
@@ -240,19 +236,21 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
block->text = strdup (text->str_value);
|
||||
}
|
||||
} else if (!strcmp (type->str_value, "tool_use") && tool_idx < n_tool_calls) {
|
||||
// Tool call - convert from Anthropic format to OpenAI format
|
||||
const RJson *name = r_json_get (content_item, "name");
|
||||
const RJson *id = r_json_get (content_item, "id");
|
||||
const RJson *input = r_json_get (content_item, "input");
|
||||
|
||||
block->type = strdup ("tool_use");
|
||||
R2AI_ToolCall *tc = R_NEW0 (R2AI_ToolCall);
|
||||
if (name && name->type == R_JSON_STRING) {
|
||||
block->name = strdup (name->str_value);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->name = strdup (name->str_value);
|
||||
}
|
||||
|
||||
if (id && id->type == R_JSON_STRING) {
|
||||
block->id = strdup (id->str_value);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->id = strdup (id->str_value);
|
||||
}
|
||||
|
||||
@@ -261,19 +259,19 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
if (input_str) {
|
||||
R_LOG_DEBUG ("Input string: %s", input_str);
|
||||
block->input = strdup (input_str);
|
||||
R2AI_ToolCall *tc = (R2AI_ToolCall *)&message->tool_calls[tool_idx];
|
||||
tc->arguments = strdup (input_str);
|
||||
free (input_str);
|
||||
}
|
||||
}
|
||||
|
||||
r_list_append (message->tool_calls, tc);
|
||||
tool_idx++;
|
||||
} else if (!strcmp (type->str_value, "thinking")) {
|
||||
const RJson *tdata = r_json_get (content_item, "data");
|
||||
const RJson *data = r_json_get (content_item, "data");
|
||||
const RJson *thinking = r_json_get (content_item, "thinking");
|
||||
const RJson *signature = r_json_get (content_item, "signature");
|
||||
if (tdata && tdata->type == R_JSON_STRING) {
|
||||
block->data = strdup (tdata->str_value);
|
||||
if (data && data->type == R_JSON_STRING) {
|
||||
block->data = strdup (data->str_value);
|
||||
}
|
||||
if (thinking && thinking->type == R_JSON_STRING) {
|
||||
block->thinking = strdup (thinking->str_value);
|
||||
@@ -283,20 +281,22 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
}
|
||||
block->type = strdup ("thinking");
|
||||
|
||||
r_strbuf_append (content_buf, "\n" Color_GRAY "<thinking>\n");
|
||||
r_strbuf_append (content_buf, "\n\x1b[90m<thinking>\n");
|
||||
r_strbuf_append (content_buf, block->thinking);
|
||||
r_strbuf_append (content_buf, "\n</thinking>" Color_RESET "\n");
|
||||
r_strbuf_append (content_buf, "\n</thinking>\x1b[0m\n");
|
||||
}
|
||||
r_list_append (cb, block);
|
||||
block_idx++;
|
||||
}
|
||||
content_item = content_item->next;
|
||||
}
|
||||
message->content_blocks = cb;
|
||||
}
|
||||
|
||||
// Store the content
|
||||
message->content = r_strbuf_drain (content_buf);
|
||||
|
||||
if (!message->content && (!message->tool_calls || r_list_empty (message->tool_calls))) {
|
||||
// If there's no content and no tool calls, clean up
|
||||
if (!message->content && !message->n_tool_calls) {
|
||||
r2ai_message_free (message);
|
||||
message = NULL;
|
||||
}
|
||||
@@ -306,8 +306,12 @@ R_IPI R2AI_ChatResponse *r2ai_anthropic_parse_response(const char *json, char **
|
||||
free (response_copy);
|
||||
}
|
||||
|
||||
// Assign message and usage to result
|
||||
result->message = message;
|
||||
result->usage = usage;
|
||||
|
||||
// Free the HTTP response body
|
||||
free (res);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/* r2ai - Copyright 2023-2026 pancake */
|
||||
|
||||
#define R_LOG_ORIGIN "r2ai"
|
||||
|
||||
#include "r2ai.h"
|
||||
#include "r2ai_priv.h"
|
||||
|
||||
/* Return the path to the apikeys.txt file */
|
||||
R_API char *r2ai_apikeys_path(bool *exists) {
|
||||
char *path = r_file_home (".config/r2ai/apikeys.txt");
|
||||
if (exists) {
|
||||
*exists = r_file_exists (path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/* Edit the API keys file */
|
||||
R_API void r2ai_apikeys_edit(RCorePluginSession *cps) {
|
||||
RCore *core = cps->core;
|
||||
bool exists = false;
|
||||
char *keys_path = r2ai_apikeys_path (&exists);
|
||||
char *config_dir = r_file_home (".config/r2ai");
|
||||
r_sys_mkdirp (config_dir);
|
||||
free (config_dir);
|
||||
if (!exists) {
|
||||
const char *template = "# API Keys configuration for r2ai\n"
|
||||
"\n"
|
||||
"# Specify your API keys in the format: PROVIDER=KEY\n"
|
||||
"# OpenAI=sk-your-openai-api-key-here\n"
|
||||
"# MISTRAL=your-mistral-api-key-here\n";
|
||||
r_file_dump (keys_path, (const ut8 *)template, strlen (template), 0);
|
||||
}
|
||||
r_cons_editor (core->cons, keys_path, NULL);
|
||||
free (keys_path);
|
||||
}
|
||||
|
||||
/* Return a malloc'd API key read from ~/.config/r2ai/apikeys.txt
|
||||
* Provider matching is case-insensitive */
|
||||
R_API char *r2ai_apikeys_get(const char *provider) {
|
||||
if (!provider) {
|
||||
return NULL;
|
||||
}
|
||||
bool exists = false;
|
||||
char *config_file = r2ai_apikeys_path (&exists);
|
||||
if (!exists) {
|
||||
free (config_file);
|
||||
return NULL;
|
||||
}
|
||||
char *content = r_file_slurp (config_file, NULL);
|
||||
free (config_file);
|
||||
if (!content) {
|
||||
return NULL;
|
||||
}
|
||||
RList *lines = r_str_split_list (content, "\n", 0);
|
||||
char *key = NULL;
|
||||
if (lines) {
|
||||
RListIter *iter;
|
||||
char *line;
|
||||
r_list_foreach (lines, iter, line) {
|
||||
const char *trimmed = r_str_trim_head_ro (line);
|
||||
if (R_STR_ISEMPTY (trimmed) || *trimmed == '#') {
|
||||
continue;
|
||||
}
|
||||
char *eq = strchr (trimmed, '=');
|
||||
if (eq) {
|
||||
*eq = 0;
|
||||
char *lower = strdup (trimmed);
|
||||
r_str_case (lower, false); /* lowercase */
|
||||
char *tail = strstr (lower, "_api_key");
|
||||
if (tail) {
|
||||
*tail = 0;
|
||||
}
|
||||
if (r_str_casecmp (lower, provider) == 0) {
|
||||
key = strdup (eq + 1);
|
||||
r_str_trim (key);
|
||||
free (lower);
|
||||
break;
|
||||
}
|
||||
free (lower);
|
||||
} else if (*trimmed) {
|
||||
R_LOG_WARN ("Invalid syntax in apikeys.txt: %s", line);
|
||||
}
|
||||
}
|
||||
r_list_free (lines);
|
||||
}
|
||||
free (content);
|
||||
return key;
|
||||
}
|
||||
-954
@@ -1,954 +0,0 @@
|
||||
/* 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;
|
||||
r_core_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 {
|
||||
r_core_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;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user