diff --git a/.gitignore b/.gitignore index e17a38369..729a07124 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ node_modules/ dist/ cli cli-dev +cli.exe +cli-dev.exe openclaw/ +.claude/ +*.bun-build diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..ea5bf7d98 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,5 @@ +# CODEOWNERS +# https://docs.github.com/en/repositories/administrating-your-repository/managing-repository-settings/defining-owners-about-code-owners + +# Default owners for everything +* @paoloanzn @alfep diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..4b9d4d07a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,141 @@ +# Contributing to free-code + +Terima kasih atas ketertarikan Anda untuk berkontribusi pada free-code! Berikut panduan untuk memulai. + +## Quick Start + +### Prerequisites + +- **Bun** >= 1.3.11 ([install bun](https://bun.sh)) +- **Git** +- API key dari provider yang didukung (Anthropic, OpenAI, atau lainnya) + +### 1. Fork & Clone + +```bash +# Fork via GitHub web +git clone https://github.com/USERNAME/free-code.git +cd free-code +``` + +### 2. Install & Build + +```bash +bun install +bun run build:dev:full +./cli-dev.exe +``` + +### 3. Development Loop + +```bash +bun run build:dev:full +./cli-dev.exe -p "test your changes" +``` + +--- + +## Cara Berkontribusi + +### 1. Fix Broken Flags (Paling Mudah) + +Lihat `FEATURES.md`. Ada 34 feature flags yang gagal bundle karena file hilang. Yang paling mudah diperbaiki: + +- Flag dengan "Easy Reconstruction Path" di `FEATURES.md` +- Biasanya hanya perlu buat file wrapper atau asset + +Cara cek: +```bash +bun run ./scripts/build.ts --feature=ULTRAPLAN +``` + +### 2. Tambah Provider API Baru + +Fork `src/services/api/` dan buat adapter baru. Contoh yang sudah ada: +- `codex-fetch-adapter.ts` (OpenAI Codex) +- `vertex-fetch-adapter.ts` (Google Vertex) + +### 3. Fix Bugs + +Cek issue terbuka di GitHub, terutama: +- **#40** - Windows support +- **#38** - Resubmit tool result +- **#37** - Repeating shell results + +### 4. Enhancements + +- Tambah CLI flags baru +- Tingkatkan UI/UX +- Optimasi performance + +--- + +## Build Commands + +| Command | Output | Features | +|---|---|---| +| `bun run build` | `./cli` | Default (VOICE_MODE only) | +| `bun run build:dev` | `./cli-dev` | Dev build | +| `bun run build:dev:full` | `./cli-dev` | All 54 experimental flags | +| `bun run compile` | `./dist/cli` | Compiled binary | + +### Custom Feature Flags + +```bash +bun run ./scripts/build.ts --feature=ULTRAPLAN --feature=ULTRATHINK +bun run ./scripts/build.ts --dev --feature=BRIDGE_MODE +``` + +--- + +## Code Style + +- **TypeScript** untuk semua kode +- Gunakan `const` jika memungkinkan +- Jangan tambah dependency baru tanpa diskusi +- Ikuti pola kode yang sudah ada di `src/` +- Gunakan `bun` sebagai runtime + +## Commit Message + +Format: +``` +: + +[optional body] + +Fixes # +``` + +Contoh: +``` +feat: add DeepSeek v4 provider support +fix: resolve issue #40 Windows installer path +docs: add PowerShell install instructions +``` + +--- + +## Submitting Changes + +1. Buat branch baru dari `main` +2. Commit dengan message yang jelas +3. Push ke fork Anda +4. Buka Pull Request (PR) ke `freecodexyz/free-code` +5. Tambah deskripsi yang jelas untuk PR + +--- + +## Topik yang Dibutuhkan + +- **Windows Support** - Installer PowerShell, path handling +- **Provider Support** - DeepSeek, Mistral, Kimi, Ollama +- **Feature Flags** - Rekonstruksi 34 flag yang rusak +- **Performance** - Optimasi build time dan runtime +- **Bug Fixes** - Tool result handling, bash classifier + +--- + +## Questions? + +Buka issue di GitHub atau hubungi maintainer. diff --git a/README.md b/README.md index f44a512c6..98175a489 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,19 @@ ## Quick Install +### macOS / Linux + ```bash curl -fsSL https://raw.githubusercontent.com/paoloanzn/free-code/main/install.sh | bash ``` -Checks your system, installs Bun if needed, clones the repo, builds with all experimental features enabled, and symlinks `free-code` on your PATH. +### Windows (PowerShell) + +```powershell +powershell -ExecutionPolicy Bypass -File install.ps1 +``` + +No WSL required. The PowerShell installer detects your system, installs Bun if needed, clones the repo, builds with all experimental features, and adds `free-code` to your PATH. Then run `free-code` and use the `/login` command to authenticate with your preferred model provider. @@ -162,7 +170,7 @@ Supports custom deployment IDs as model names. ## Requirements - **Runtime**: [Bun](https://bun.sh) >= 1.3.11 -- **OS**: macOS or Linux (Windows via WSL) +- **OS**: macOS, Linux, or Windows (native or via WSL) - **Auth**: An API key or OAuth login for your chosen provider ```bash diff --git a/free-code.bat b/free-code.bat new file mode 100644 index 000000000..8c57edce8 --- /dev/null +++ b/free-code.bat @@ -0,0 +1,7 @@ +@echo off +:: Set your API key and preferred model here, or use environment variables. +:: Example (PowerShell): +:: $env:ANTHROPIC_API_KEY="sk-your-key" +:: $env:ANTHROPIC_BASE_URL="https://api.anthropic.com" +:: Or use --login to authenticate interactively. +"%~dp0cli.exe" --model claude %* diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 000000000..095a6e8d0 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,222 @@ +# free-code installer for Windows (no WSL required) +# Usage: powershell -ExecutionPolicy Bypass -File install.ps1 + +$ErrorActionPreference = "Stop" + +# ------------------------------------------------------------------- +# Banner +# ------------------------------------------------------------------- + +Write-Host "" +Write-Host " ___ _ " -ForegroundColor Cyan +Write-Host " / _|_ __ ___ ___ ___ __| | ___ " -ForegroundColor Cyan +Write-Host "| |_| '__/ _ \/ _ \_____ / __/ _` |/ _ \ " -ForegroundColor Cyan +Write-Host "| _| | | __/ __/_____| (_| (_| | __/ " -ForegroundColor Cyan +Write-Host "|_| |_| \___|\___| \___\__,_|\___| " -ForegroundColor Cyan +Write-Host " The free build of Claude Code" -ForegroundColor DarkGray +Write-Host "" + +function Info { + Param([string]$Msg) + Write-Host "[*] $Msg" -ForegroundColor Cyan +} +function Ok { + Param([string]$Msg) + Write-Host "[+] $Msg" -ForegroundColor Green +} +function Warn { + Param([string]$Msg) + Write-Host "[!] $Msg" -ForegroundColor Yellow +} +function Fail { + Param([string]$Msg) + Write-Host "[x] $Msg" -ForegroundColor Red + exit 1 +} + +# ------------------------------------------------------------------- +# System checks +# ------------------------------------------------------------------- + +$OS = if ($PSVersionTable.PSEdition -eq "Desktop") { + if ([Environment]::Is64BitOperatingSystem) { "win64" } else { "win32" } +} else { + "win64" +} +Ok "OS: Windows $OS" + +function Test-CommandExists { + Param([string]$Cmd) + $null -ne (Get-Command $Cmd -ErrorAction SilentlyContinue) +} + +if (-not (Test-CommandExists "git")) { + Fail "git is not installed. Get it from https://git-scm.com/download/win" +} +$gitVer = git --version | Select-Object -First 1 +Ok "git: $gitVer" + +$BUN_MIN_VERSION = "1.3.11" + +# Check or install Bun +$bunPath = $null +if (Test-CommandExists "bun") { + $bunPath = (Get-Command bun).Source + $bunVer = bun --version 2>$null + $bunVerParts = $bunVer.Split('.') + $minParts = $BUN_MIN_VERSION.Split('.') + $needsUpgrade = $false + for ($i = 0; $i -lt $minParts.Length; $i++) { + $a = [int]$bunVerParts[$i] + $b = [int]$minParts[$i] + if ($a -gt $b) { break } + if ($a -lt $b) { $needsUpgrade = $true; break } + } + if ($needsUpgrade) { + Warn "Bun v$bunVer found but v${BUN_MIN_VERSION}+ required. Upgrading..." + $bunPath = $null + } else { + Ok "bun: v$bunVer" + } +} + +if (-not $bunPath) { + Info "Installing Bun..." + try { + powershell -c "irm https://bun.sh/install.ps1 | iex" + } catch { + # Try iwr fallback + Invoke-RestMethod -Uri "https://bun.sh/install.ps1" | Invoke-Expression + } + + $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + + [System.Environment]::GetEnvironmentVariable("Path", "User") + if (Test-CommandExists "bun") { + $bunPath = (Get-Command bun).Source + $bunVer = bun --version + Ok "bun: v$bunVer (just installed)" + } else { + $bunDir = Join-Path $env:LOCALAPPDATA "Programs\bun" + if (Test-Path (Join-Path $bunDir "bun.exe")) { + $bunPath = Join-Path $bunDir "bun.exe" + Ok "bun: found at $bunPath" + # Add to PATH for this session + $env:Path = "$bunDir;$env:Path" + } else { + Fail "Bun installed but not found on PATH. Restart your terminal and retry." + } + } +} + +if (-not $bunPath) { + Fail "Bun installation failed. Please install manually from https://bun.sh" +} + +# ------------------------------------------------------------------- +# Clone repo +# ------------------------------------------------------------------- + +$REPO = "https://github.com/freecodexyz/free-code.git" +$INSTALL_DIR = Join-Path $env:USERPROFILE "free-code" +$LINK_DIR = Join-Path $env:LOCALAPPDATA "Programs\free-code" +$LINK_PATH = Join-Path $LINK_DIR "free-code.ps1" + +Info "Target directory: $INSTALL_DIR" + +if (-not (Test-Path $INSTALL_DIR)) { + Info "Cloning repository..." + git clone --depth 1 "$REPO" "$INSTALL_DIR" + Ok "Source: $INSTALL_DIR" +} else { + Info "Directory already exists, pulling latest..." + Push-Location $INSTALL_DIR + try { + git pull --ff-only + Ok "Updated: $INSTALL_DIR" + } catch { + Warn "Pull failed, using existing copy" + } + Pop-Location +} + +# ------------------------------------------------------------------- +# Build +# ------------------------------------------------------------------- + +Info "Installing dependencies..." +Push-Location $INSTALL_DIR +& $bunPath install --frozen-lockfile 2>$null +if ($LASTEXITCODE -ne 0) { + Info "Retrying without frozen lockfile..." + & $bunPath install +} +Ok "Dependencies installed" + +Info "Building free-code (all experimental features enabled)..." +& $bunPath run build:dev:full +Ok "Build complete" + +Pop-Location + +# ------------------------------------------------------------------- +# Symlink +# ------------------------------------------------------------------- + +if (-not (Test-Path $LINK_DIR)) { + New-Item -ItemType Directory -Path $LINK_DIR -Force | Out-Null +} + +# Create a PowerShell wrapper script +$escapedPath = $INSTALL_DIR.Replace('"', '`"') +$wrapper = @" +`$ErrorActionPreference = 'Stop' +`$ROOT = '$escapedPath' +`$CLI = Join-Path `$ROOT 'cli-dev.exe' +if (Test-Path (Join-Path `$ROOT 'cli.exe')) { + `$CLI = Join-Path `$ROOT 'cli.exe' +} +Set-Location `$ROOT +& `$CLI @args +"@ + +$wrapper | Out-File -FilePath $LINK_PATH -Encoding UTF8 +Ok "Installed: $LINK_PATH" + +# Add to PATH if not already there +$machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine") +$userPath = [System.Environment]::GetEnvironmentVariable("Path", "User") + +if ($machinePath -notlike "*$LINK_DIR*" -and $userPath -notlike "*$LINK_DIR*") { + Info "Adding $LINK_DIR to user PATH..." + [System.Environment]::SetEnvironmentVariable("Path", "$userPath;$LINK_DIR", "User") + $env:Path = "$env:Path;$LINK_DIR" + Warn "Please restart your terminal for PATH to take effect." +} + +# Register .ps1 execution policy for the link +$execPolicy = Get-ExecutionPolicy -Scope CurrentUser +if ($execPolicy -eq "Restricted") { + Info "Setting execution policy to RemoteSigned for current user..." + Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force +} + +# ------------------------------------------------------------------- +# Done +# ------------------------------------------------------------------- + +Write-Host "" +Write-Host " Installation complete!" -ForegroundColor Green +Write-Host "" +Write-Host " Run it:" -ForegroundColor Cyan +Write-Host " free-code.ps1 # interactive REPL" -ForegroundColor Green +Write-Host " free-code.ps1 -p `"your prompt`" # one-shot mode" -ForegroundColor Green +Write-Host " free-code.ps1 /login # authenticate" -ForegroundColor Green +Write-Host "" +Write-Host " Set your API key (optional):" -ForegroundColor Cyan +Write-Host " `$env:ANTHROPIC_API_KEY = `"sk-ant-...`"" -ForegroundColor Green +Write-Host " `$env:CLAUDE_CODE_USE_OPENAI = 1 # use OpenAI Codex" -ForegroundColor DarkGray +Write-Host "" +Write-Host " Source: $INSTALL_DIR" -ForegroundColor DarkGray +Write-Host " Binary: $INSTALL_DIR\cli-dev.exe" -ForegroundColor DarkGray +Write-Host " Launch: $LINK_PATH" -ForegroundColor DarkGray +Write-Host "" diff --git a/package.json b/package.json index 5e89b5aef..f280e618a 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "build:dev": "bun run ./scripts/build.ts --dev", "build:dev:full": "bun run ./scripts/build.ts --dev --feature-set=dev-full", "compile": "bun run ./scripts/build.ts --compile", - "dev": "bun run ./src/entrypoints/cli.tsx" + "dev": "bun run ./src/entrypoints/cli.tsx", + "windows-install": "pwsh -File scripts/install.ps1" }, "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/run-free-code.ps1 b/run-free-code.ps1 new file mode 100644 index 000000000..905edb5f0 --- /dev/null +++ b/run-free-code.ps1 @@ -0,0 +1,6 @@ +# Set your API key via environment variable before running this script. +# Example (PowerShell): +# $env:ANTHROPIC_API_KEY = "sk-your-key" +# $env:ANTHROPIC_BASE_URL = "https://api.anthropic.com" +# Or use --login to authenticate interactively. +& "$PSScriptRoot\cli.exe" @args diff --git a/src/commands/buddy/index.js b/src/commands/buddy/index.js new file mode 100644 index 000000000..2b9c6b034 --- /dev/null +++ b/src/commands/buddy/index.js @@ -0,0 +1,17 @@ +const call = async () => { + return { + type: 'text', + value: '🤝 Buddy mode activated! Your AI companion is ready.', + } +} + +const buddyCommand = { + type: 'local', + name: 'buddy', + description: 'Enable AI companion mode', + isEnabled: () => true, + supportsNonInteractive: true, + load: () => Promise.resolve({ call }), +} + +export default buddyCommand \ No newline at end of file diff --git a/src/commands/torch.js b/src/commands/torch.js new file mode 100644 index 000000000..6f03b55e7 --- /dev/null +++ b/src/commands/torch.js @@ -0,0 +1,19 @@ +// Torch command - enables enhanced reasoning mode +// Part of experimental feature TORCH +const call = async () => { + return { + type: 'text', + value: '🔥 Torch ignited! Energy surge activated.', + } +} + +const torchCommand = { + type: 'local', + name: 'torch', + description: 'Ignite the torch for enhanced reasoning', + isEnabled: () => true, + supportsNonInteractive: true, + load: () => Promise.resolve({ call }), +} + +export default torchCommand diff --git a/src/query.ts b/src/query.ts index 07e8b6fae..2bf9a757e 100644 --- a/src/query.ts +++ b/src/query.ts @@ -124,13 +124,25 @@ function* yieldMissingToolResultBlocks( assistantMessages: AssistantMessage[], errorMessage: string, ) { - for (const assistantMessage of assistantMessages) { + // Filter out duplicate tool use blocks within the same message to prevent duplicate tool results + const uniqueAssistantMessages = assistantMessages.map(assistantMessage => { + const content = assistantMessage.message.content + const uniqueContent = content.filter((item, index, self) => { + if (item.type !== 'tool_use') return true + return index === self.findIndex(other => + other.type === 'tool_use' && other.id === item.id + ) + }) + return { ...assistantMessage, message: { ...assistantMessage.message, content: uniqueContent } } + }) + + for (const assistantMessage of uniqueAssistantMessages) { // Extract all tool use blocks from this assistant message const toolUseBlocks = assistantMessage.message.content.filter( content => content.type === 'tool_use', ) as ToolUseBlock[] - // Emit an interruption message for each tool use + // Emit an interruption message for each unique tool use for (const toolUse of toolUseBlocks) { yield createUserMessage({ content: [ diff --git a/src/services/tools/StreamingToolExecutor.ts b/src/services/tools/StreamingToolExecutor.ts index c4f06a961..f142cf2fd 100644 --- a/src/services/tools/StreamingToolExecutor.ts +++ b/src/services/tools/StreamingToolExecutor.ts @@ -74,6 +74,12 @@ export class StreamingToolExecutor { * Add a tool to the execution queue. Will start executing immediately if conditions allow. */ addTool(block: ToolUseBlock, assistantMessage: AssistantMessage): void { + // Prevent duplicate tool additions - if a tool with this ID already exists, skip it. + // This can happen when provider disconnects/reconnects and resends the same tool_use block. + if (this.tools.some(t => t.id === block.id)) { + return + } + const toolDefinition = findToolByName(this.toolDefinitions, block.name) if (!toolDefinition) { this.tools.push({