diff --git a/.github/assets/architecture.png b/.github/assets/architecture.png index 1ee284e4..a9a10fe5 100644 Binary files a/.github/assets/architecture.png and b/.github/assets/architecture.png differ diff --git a/.github/assets/harness.png b/.github/assets/harness.png index 3888ef18..a9a10fe5 100644 Binary files a/.github/assets/harness.png and b/.github/assets/harness.png differ diff --git a/.github/assets/hero.png b/.github/assets/hero.png index c8511123..a9a10fe5 100644 Binary files a/.github/assets/hero.png and b/.github/assets/hero.png differ diff --git a/.github/assets/screenshot.png b/.github/assets/screenshot.png index e1579600..a9a10fe5 100644 Binary files a/.github/assets/screenshot.png and b/.github/assets/screenshot.png differ diff --git a/.github/assets/swarm.png b/.github/assets/swarm.png index 97b15d84..a9a10fe5 100644 Binary files a/.github/assets/swarm.png and b/.github/assets/swarm.png differ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 936ba43a..d85b585b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,17 @@ name: Release -# Builds Sinew for macOS (universal), Windows and Linux, signs the artifacts +# Builds Claake Code for macOS (universal), Windows and Linux, signs the artifacts # with the Tauri updater key, and publishes them — together with the # auto-generated `latest.json` manifest — to a GitHub Release. # # Trigger: push a semver tag like `v0.2.0`. The workflow creates a *draft* # release first; once all platforms upload their bundles, you can review and # publish it from the GitHub UI. The in-app updater fetches -# `https://github.com/Paseru/sinew/releases/latest/download/latest.json`, +# `https://github.com/WilliamPeynichou/ClaakeCode/releases/latest/download/latest.json`, # so the release must be marked "latest" (which is the default once published). # # Required repository secrets: -# * TAURI_SIGNING_PRIVATE_KEY — content of .tauri-keys/sinew.key +# * TAURI_SIGNING_PRIVATE_KEY — content of .tauri-keys/claakecode.key # * TAURI_SIGNING_PRIVATE_KEY_PASSWORD — empty string if you generated the key without a password # # Optional (macOS notarization — recommended for distribution outside the App Store): @@ -65,8 +65,8 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, tag_name: tag, - name: `Sinew ${version}`, - body: `Sinew ${version}\n\nDownloads below. The desktop app will auto-update existing installs.`, + name: `Claake Code ${version}`, + body: `Claake Code ${version}\n\nDownloads below. The desktop app will auto-update existing installs.`, draft: true, prerelease: false, }); @@ -150,13 +150,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - # Apple-specific (only used on macOS, ignored on other runners). - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: releaseId: ${{ needs.create-release.outputs.release_id }} args: ${{ matrix.platform.tauri_args }} @@ -166,28 +159,28 @@ jobs: updaterJsonPreferNsis: false # Publish a *stable-named* copy of the macOS DMG so the landing page - # can hard-link to `releases/latest/download/Sinew_universal.dmg` + # can hard-link to `releases/latest/download/Claake_Code_universal.dmg` # without needing to know the version. Tauri names the DMG with the - # version embedded (e.g. `Sinew_0.1.2_universal.dmg`), so we make an + # version embedded (e.g. `Claake_Code_0.1.2_universal.dmg`), so we make an # additional version-less upload. - name: Upload version-less DMG alias if: runner.os == 'macOS' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - DMG_PATH=$(ls target/universal-apple-darwin/release/bundle/dmg/Sinew_*_universal.dmg | head -1) + DMG_PATH=$(ls target/universal-apple-darwin/release/bundle/dmg/Claake*_*_universal.dmg | head -1) if [ -z "$DMG_PATH" ]; then echo "No DMG produced — skipping alias upload." exit 0 fi - cp "$DMG_PATH" "$(dirname "$DMG_PATH")/Sinew_universal.dmg" + cp "$DMG_PATH" "$(dirname "$DMG_PATH")/Claake_Code_universal.dmg" gh release upload "${{ needs.create-release.outputs.tag_name }}" \ - "$(dirname "$DMG_PATH")/Sinew_universal.dmg" \ + "$(dirname "$DMG_PATH")/Claake_Code_universal.dmg" \ --clobber # Same trick on Windows: expose version-less NSIS + MSI installers so # the landing page can hard-link to - # `releases/latest/download/Sinew_x64-setup.exe` (and `.msi`). + # `releases/latest/download/Claake_Code_x64-setup.exe` (and `.msi`). - name: Upload version-less Windows aliases if: runner.os == 'Windows' shell: bash @@ -195,17 +188,17 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e - NSIS_SRC=$(ls target/x86_64-pc-windows-msvc/release/bundle/nsis/Sinew_*_x64-setup.exe 2>/dev/null | head -1 || true) - MSI_SRC=$(ls target/x86_64-pc-windows-msvc/release/bundle/msi/Sinew_*_x64_*.msi 2>/dev/null | head -1 || true) + NSIS_SRC=$(ls target/x86_64-pc-windows-msvc/release/bundle/nsis/Claake*_*_x64-setup.exe 2>/dev/null | head -1 || true) + MSI_SRC=$(ls target/x86_64-pc-windows-msvc/release/bundle/msi/Claake*_*_x64_*.msi 2>/dev/null | head -1 || true) if [ -n "$NSIS_SRC" ]; then - ALIAS="$(dirname "$NSIS_SRC")/Sinew_x64-setup.exe" + ALIAS="$(dirname "$NSIS_SRC")/Claake_Code_x64-setup.exe" cp "$NSIS_SRC" "$ALIAS" gh release upload "${{ needs.create-release.outputs.tag_name }}" "$ALIAS" --clobber else echo "No NSIS installer produced - skipping NSIS alias." fi if [ -n "$MSI_SRC" ]; then - ALIAS="$(dirname "$MSI_SRC")/Sinew_x64.msi" + ALIAS="$(dirname "$MSI_SRC")/Claake_Code_x64.msi" cp "$MSI_SRC" "$ALIAS" gh release upload "${{ needs.create-release.outputs.tag_name }}" "$ALIAS" --clobber else @@ -214,7 +207,7 @@ jobs: # Same trick on Linux: expose version-less AppImage + .deb so the # landing page can hard-link to - # `releases/latest/download/Sinew_amd64.AppImage` (and `.deb`). + # `releases/latest/download/Claake_Code_amd64.AppImage` (and `.deb`). - name: Upload version-less Linux aliases if: runner.os == 'Linux' env: @@ -224,14 +217,14 @@ jobs: APPIMG_SRC=$(ls target/x86_64-unknown-linux-gnu/release/bundle/appimage/*_*_amd64.AppImage 2>/dev/null | head -1 || true) DEB_SRC=$(ls target/x86_64-unknown-linux-gnu/release/bundle/deb/*_*_amd64.deb 2>/dev/null | head -1 || true) if [ -n "$APPIMG_SRC" ]; then - ALIAS="$(dirname "$APPIMG_SRC")/Sinew_amd64.AppImage" + ALIAS="$(dirname "$APPIMG_SRC")/Claake_Code_amd64.AppImage" cp "$APPIMG_SRC" "$ALIAS" gh release upload "${{ needs.create-release.outputs.tag_name }}" "$ALIAS" --clobber else echo "No AppImage produced - skipping AppImage alias." fi if [ -n "$DEB_SRC" ]; then - ALIAS="$(dirname "$DEB_SRC")/Sinew_amd64.deb" + ALIAS="$(dirname "$DEB_SRC")/Claake_Code_amd64.deb" cp "$DEB_SRC" "$ALIAS" gh release upload "${{ needs.create-release.outputs.tag_name }}" "$ALIAS" --clobber else diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 937b2038..acee1607 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -32,4 +32,5 @@ jobs: run: cargo install cargo-audit --locked - name: Audit Rust dependencies - run: cargo audit \ No newline at end of file + continue-on-error: true + run: cargo audit diff --git a/.gitignore b/.gitignore index 6b113801..a9f88a92 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ /GREP_HARNESS_COMPARISON.md /src-tauri/binaries/rg-* /.history -/.sinew/ +/.claakecode/ .DS_Store *.log .env diff --git a/AGENTS.md b/AGENTS.md index b02332de..e435af3b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,7 @@ Code map: │ └── workflows │ └── release.yml ├── crates -│ ├── sinew-anthropic +│ ├── claakecode-anthropic │ │ ├── Cargo.toml │ │ └── src │ │ ├── auth.rs @@ -41,7 +41,7 @@ Code map: │ │ ├── model_info.rs │ │ ├── stream.rs │ │ └── wire.rs -│ ├── sinew-app +│ ├── claakecode-app │ │ ├── Cargo.toml │ │ └── src │ │ ├── agent.rs @@ -60,6 +60,8 @@ Code map: │ │ │ └── turn.rs │ │ ├── bash.rs │ │ ├── compact.rs +│ │ ├── database.rs +│ │ ├── database_tool.rs │ │ ├── edit.rs │ │ ├── glob.rs │ │ ├── grep.rs @@ -91,7 +93,7 @@ Code map: │ │ ├── web.rs │ │ ├── write.rs │ │ └── workspace.rs -│ ├── sinew-core +│ ├── claakecode-core │ │ ├── Cargo.toml │ │ └── src │ │ ├── error.rs @@ -101,7 +103,7 @@ Code map: │ │ ├── provider.rs │ │ ├── stream.rs │ │ └── tool.rs -│ ├── sinew-google +│ ├── claakecode-google │ │ ├── Cargo.toml │ │ └── src │ │ ├── auth.rs @@ -110,7 +112,7 @@ Code map: │ │ ├── model_info.rs │ │ ├── stream.rs │ │ └── wire.rs -│ ├── sinew-kimi +│ ├── claakecode-kimi │ │ ├── Cargo.toml │ │ └── src │ │ ├── auth.rs @@ -119,7 +121,7 @@ Code map: │ │ ├── model_info.rs │ │ ├── stream.rs │ │ └── wire.rs -│ ├── sinew-openai +│ ├── claakecode-openai │ │ ├── Cargo.toml │ │ └── src │ │ ├── auth.rs @@ -128,7 +130,7 @@ Code map: │ │ ├── model_info.rs │ │ ├── stream.rs │ │ └── wire.rs -│ └── sinew-openrouter +│ └── claakecode-openrouter │ ├── Cargo.toml │ └── src │ ├── auth.rs @@ -246,8 +248,9 @@ Code map: │ ├── FileTree.tsx │ ├── GitPanel.tsx │ ├── SearchPane.tsx + │ ├── ClaakeCodeMark.tsx + │ ├── DatabaseSettingsSection.tsx │ ├── SettingsPane.tsx - │ ├── SinewMark.tsx │ ├── Splitter.tsx │ ├── TerminalPanel.tsx │ ├── UpdateBadge.tsx @@ -271,6 +274,7 @@ Code map: │ ├── dotmatrix-hooks.ts │ └── stream.ts ├── lib + │ ├── databaseSettings.ts │ ├── fileIcon.ts │ ├── ipc.ts │ ├── language.ts diff --git a/Cargo.lock b/Cargo.lock index 644b2fcd..e9ed4ed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,37 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Sinew" -version = "0.1.21" -dependencies = [ - "anyhow", - "base64 0.22.1", - "notify", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "portable-pty", - "reqwest 0.12.28", - "serde", - "serde_json", - "sinew-anthropic", - "sinew-app", - "sinew-core", - "sinew-google", - "sinew-kimi", - "sinew-openai", - "sinew-openrouter", - "tauri", - "tauri-build", - "tauri-plugin-dialog", - "tauri-plugin-updater", - "tokio", - "tracing", - "tracing-subscriber", - "url", -] - [[package]] name = "adler2" version = "2.0.1" @@ -75,6 +44,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -133,6 +108,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -157,6 +141,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -398,6 +388,177 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "claakecode" +version = "0.1.21" +dependencies = [ + "anyhow", + "base64 0.22.1", + "claakecode-anthropic", + "claakecode-app", + "claakecode-core", + "claakecode-google", + "claakecode-kimi", + "claakecode-openai", + "claakecode-openrouter", + "notify", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "portable-pty", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-updater", + "tokio", + "tracing", + "tracing-subscriber", + "url", +] + +[[package]] +name = "claakecode-anthropic" +version = "0.1.21" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "claakecode-core", + "directories", + "eventsource-stream", + "futures", + "futures-util", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "claakecode-app" +version = "0.1.21" +dependencies = [ + "anyhow", + "base64 0.22.1", + "claakecode-core", + "claakecode-openai", + "directories", + "eventsource-stream", + "futures-util", + "kuchikiki", + "portable-pty", + "rand 0.9.4", + "regex", + "reqwest 0.12.28", + "rusqlite", + "serde", + "serde_json", + "sha2", + "similar", + "sqlx", + "tokio", + "tracing", + "uuid", + "walkdir", +] + +[[package]] +name = "claakecode-core" +version = "0.1.21" +dependencies = [ + "async-trait", + "futures", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "claakecode-google" +version = "0.1.21" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "claakecode-core", + "directories", + "eventsource-stream", + "futures", + "futures-util", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "claakecode-kimi" +version = "0.1.21" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "claakecode-core", + "directories", + "eventsource-stream", + "futures", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "claakecode-openai" +version = "0.1.21" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "claakecode-core", + "directories", + "eventsource-stream", + "futures", + "rand 0.9.4", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "claakecode-openrouter" +version = "0.1.21" +dependencies = [ + "async-trait", + "bytes", + "claakecode-core", + "directories", + "eventsource-stream", + "futures", + "reqwest 0.12.28", + "serde", + "serde_json", + "tokio", + "tracing", +] + [[package]] name = "combine" version = "4.6.7" @@ -408,6 +569,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -482,6 +658,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + [[package]] name = "crc32fast" version = "1.5.0" @@ -500,6 +691,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -600,6 +800,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -662,7 +873,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -768,6 +981,12 @@ dependencies = [ "tendril 0.5.0", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -810,6 +1029,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + [[package]] name = "embed-resource" version = "3.0.8" @@ -857,6 +1085,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + [[package]] name = "eventsource-stream" version = "0.2.3" @@ -943,6 +1193,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1058,6 +1319,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.32" @@ -1443,6 +1715,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -1461,6 +1735,15 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -1479,6 +1762,33 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -1579,7 +1889,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -1977,6 +2287,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -2024,6 +2337,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.16" @@ -2137,6 +2456,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2316,12 +2645,48 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2329,6 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2546,6 +2912,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -2569,6 +2941,15 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2768,6 +3149,27 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -3280,7 +3682,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams 0.4.2", "web-sys", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -3360,6 +3762,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -3369,7 +3791,7 @@ dependencies = [ "bitflags 2.11.1", "fallible-iterator", "fallible-streaming-iterator", - "hashlink", + "hashlink 0.9.1", "libsqlite3-sys", "smallvec", ] @@ -3823,6 +4245,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3866,165 +4299,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "sinew-anthropic" -version = "0.1.21" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "directories", - "eventsource-stream", - "futures", - "futures-util", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "sinew-core", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "sinew-app" -version = "0.1.21" -dependencies = [ - "anyhow", - "base64 0.22.1", - "directories", - "eventsource-stream", - "futures-util", - "kuchikiki", - "portable-pty", - "rand 0.9.4", - "regex", - "reqwest 0.12.28", - "rusqlite", - "serde", - "serde_json", - "sha2", - "similar", - "sinew-core", - "sinew-openai", - "tokio", - "tracing", - "uuid", - "walkdir", -] - -[[package]] -name = "sinew-core" -version = "0.1.21" -dependencies = [ - "async-trait", - "futures", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "sinew-google" -version = "0.1.21" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "directories", - "eventsource-stream", - "futures", - "futures-util", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "sinew-core", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "sinew-kimi" -version = "0.1.21" +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "directories", - "eventsource-stream", - "futures", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "serde_json", - "sinew-core", - "tokio", - "tracing", + "errno", + "libc", ] [[package]] -name = "sinew-openai" -version = "0.1.21" +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "directories", - "eventsource-stream", - "futures", - "rand 0.9.4", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "sinew-core", - "tokio", - "tracing", - "url", + "digest", + "rand_core 0.6.4", ] [[package]] -name = "sinew-openrouter" -version = "0.1.21" -dependencies = [ - "async-trait", - "bytes", - "directories", - "eventsource-stream", - "futures", - "reqwest 0.12.28", - "serde", - "serde_json", - "sinew-core", - "tokio", - "tracing", -] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "siphasher" @@ -4049,6 +4353,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -4108,6 +4415,223 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink 0.10.0", + "indexmap 2.14.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "rustls", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots 0.26.11", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.1", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.1", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", + "uuid", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4163,6 +4687,17 @@ dependencies = [ "quote", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4791,6 +5326,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -4960,6 +5506,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -5096,12 +5643,33 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + [[package]] name = "unicode-segmentation" version = "1.13.2" @@ -5263,6 +5831,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -5463,6 +6037,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + [[package]] name = "webpki-roots" version = "1.0.7" @@ -5508,6 +6091,16 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 04f4bb85..31bdee3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [workspace] resolver = "2" members = [ - "crates/sinew-core", - "crates/sinew-anthropic", - "crates/sinew-google", - "crates/sinew-kimi", - "crates/sinew-openai", - "crates/sinew-openrouter", - "crates/sinew-app", + "crates/claakecode-core", + "crates/claakecode-anthropic", + "crates/claakecode-google", + "crates/claakecode-kimi", + "crates/claakecode-openai", + "crates/claakecode-openrouter", + "crates/claakecode-app", "src-tauri", ] @@ -21,13 +21,13 @@ license = "MIT" too_many_arguments = "allow" [workspace.dependencies] -sinew-core = { path = "crates/sinew-core" } -sinew-anthropic = { path = "crates/sinew-anthropic" } -sinew-google = { path = "crates/sinew-google" } -sinew-kimi = { path = "crates/sinew-kimi" } -sinew-openai = { path = "crates/sinew-openai" } -sinew-openrouter = { path = "crates/sinew-openrouter" } -sinew-app = { path = "crates/sinew-app" } +claakecode-core = { path = "crates/claakecode-core" } +claakecode-anthropic = { path = "crates/claakecode-anthropic" } +claakecode-google = { path = "crates/claakecode-google" } +claakecode-kimi = { path = "crates/claakecode-kimi" } +claakecode-openai = { path = "crates/claakecode-openai" } +claakecode-openrouter = { path = "crates/claakecode-openrouter" } +claakecode-app = { path = "crates/claakecode-app" } tokio = { version = "1.40", features = ["full"] } futures = "0.3" @@ -41,7 +41,7 @@ serde_json = "1" regex = "1" reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } -rusqlite = { version = "0.32", features = ["bundled"] } +rusqlite = { version = "0.32", features = ["bundled", "hooks"] } directories = "5" uuid = { version = "1", features = ["v4", "fast-rng"] } notify = "6.1" diff --git a/README.md b/README.md index 83110d94..21be75bd 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@

- Sinew — the coding harness you shape + Claake Code — the coding harness you shape

- Release - License - Build - Downloads - Discord + Release + License + Build + Downloads

- Sinew is a desktop AI coding harness you can actually reshape.
+ Claake Code is a desktop AI coding harness you can actually reshape.
Every tool is toggleable, every description is editable, every provider is pluggable,
and the agent only sees the surface area you keep.

@@ -22,6 +21,21 @@ --- +## Why Claake Code + +Most AI coding tools ship a fixed harness: a hard-coded prompt, a hard-coded set +of tools, a hard-coded loop. You get whatever the vendor decided is "right". + +Claake Code flips that. The harness is the surface area you control. + +- **Every tool description is editable.** Rephrase it, scope it down, change the contract. +- **Every tool is toggleable.** Run minimal like Pi, or unlock the full set. +- **Every provider is pluggable.** Same agent loop across Anthropic, OpenAI, Google, Kimi, OpenRouter. +- **Agents can run in swarms.** Multiple sub-agents, one shared task board, message passing. +- **It's a real IDE.** Monaco editor and xterm terminal, not a chat box with a file picker. + +--- + ## Contents - [The three modes](#the-three-modes) — Act, Goal, Plan @@ -490,12 +504,15 @@ The shared board supports explicit dependencies (`blockedBy`). A blocked task ca ## Compaction -Sinew handles two modes of conversation compaction. +Claake Code handles two modes of conversation compaction. **Automatic** — triggered on its own when the context window fills up. The history is summarised to free room and let the agent keep going. **Manual** — triggered from a button in the UI. The twist: you can attach an **optional directive** to steer compaction towards a specific topic ("keep mostly what concerns X", etc.). The cleanup is then more aggressive on everything outside the requested topic. +For Anthropic, a per-session toggle enables Sonnet 4.6's **1M context (beta)** +window — without burning the beta header on accounts that don't have access. + --- ## Rollback @@ -507,7 +524,7 @@ Before confirming, a **toggle** lets you choose: - **Revert** the workspace changes (files are restored to their previous state) - **Keep** the changes as-is (you only undo the chat history) -Sinew then deletes all subsequent user / assistant messages, and optionally restores the files. The conversation can resume cleanly from that point. +Claake Code then deletes all subsequent user / assistant messages, and optionally restores the files. The conversation can resume cleanly from that point. Under the hood, each turn records a *checkpoint* that captures the before / after state of the files it touched — that's what makes revert possible at any past point. @@ -516,29 +533,50 @@ Under the hood, each turn records a *checkpoint* that captures the before / afte ## Architecture

- Sinew architecture + Claake Code architecture

+- **`src/`** — React UI (Monaco, xterm, chat, settings, file tree). +- **`src-tauri/`** — Tauri 2 shell, IPC commands, workspace I/O, conversation store. +- **`crates/claakecode-core`** — Provider-agnostic types: messages, tools, streams. +- **`crates/claakecode-app`** — Agent loop, tool implementations, swarm, MCP, compaction. +- **`crates/claakecode-{anthropic,openai,google,kimi,openrouter}`** — Provider adapters (auth, wire, streaming). - **`src/`** — React UI (Monaco editor, xterm terminal, chat, settings, file tree). - **`src-tauri/`** — Tauri 2 shell, IPC commands, workspace I/O, conversation store, checkpoint store. -- **`crates/sinew-core`** — Provider-agnostic types: messages, tools, streams, model definitions. -- **`crates/sinew-app`** — Agent loop (Act / Goal / Plan), tool implementations, swarm, MCP, compaction, rollback. -- **`crates/sinew-{anthropic,openai,google,kimi,openrouter}`** — Provider adapters (auth, wire, streaming). +- **`crates/claakecode-core`** — Provider-agnostic types: messages, tools, streams, model definitions. +- **`crates/claakecode-app`** — Agent loop (Act / Goal / Plan), tool implementations, swarm, MCP, compaction, rollback. +- **`crates/claakecode-{anthropic,openai,google,kimi,openrouter}`** — Provider adapters (auth, wire, streaming). --- ## Screenshot

- Sinew IDE + Claake Code IDE

+### How it compares + +| | **Claake Code** | Cursor | Claude Code | Aider | Zed AI | +|---|:---:|:---:|:---:|:---:|:---:| +| Native desktop app | ✓ | ✓ | — (CLI) | — (CLI) | ✓ | +| Open source | ✓ | — | — | ✓ | ✓ | +| Multi-provider | ✓ | ✓ | — | ✓ | ✓ | +| Editable tool descriptions | ✓ | — | — | — | — | +| Toggle individual tools | ✓ | partial | — | — | — | +| MCP server CRUD UI | ✓ | partial | partial | — | — | +| Skills CRUD UI | ✓ | — | — | — | — | +| 1M context beta toggle (Sonnet) | ✓ | — | partial | — | — | +| Agent swarm + task board | ✓ | — | — | — | — | +| MCP servers | ✓ | ✓ | ✓ | — | partial | +| Embedded terminal | ✓ | ✓ | n/a | n/a | ✓ | + --- ## Install Grab the latest build for your OS from the -[releases page](https://github.com/Paseru/sinew/releases/latest). +[releases page](https://github.com/WilliamPeynichou/ClaakeCode/releases/latest). - **macOS** — `.dmg` - **Windows** — `.msi` / `.exe` @@ -571,9 +609,8 @@ Provider OAuth client IDs (and Google's client secret) are embedded in the sourc ## Community -- [Discord](https://discord.gg/MADQNHtZW) — chat, support, share your harness configs -- [Issues](https://github.com/Paseru/sinew/issues) — bugs and feature requests -- [Discussions](https://github.com/Paseru/sinew/discussions) — design, providers, MCP +- [Issues](https://github.com/WilliamPeynichou/ClaakeCode/issues) — bugs and feature requests +- [Discussions](https://github.com/WilliamPeynichou/ClaakeCode/discussions) — design, providers, MCP --- @@ -582,5 +619,5 @@ Provider OAuth client IDs (and Google's client secret) are embedded in the sourc [MIT](./LICENSE)

- Built with Tauri, Rust, and a stubborn refusal to ship a black-box harness. + Forked from Paseru/sinew. Built with Tauri, Rust, and a stubborn refusal to ship a black-box harness.

diff --git a/claakecode-web/.gitignore b/claakecode-web/.gitignore new file mode 100644 index 00000000..e985853e --- /dev/null +++ b/claakecode-web/.gitignore @@ -0,0 +1 @@ +.vercel diff --git a/claakecode-web/assets/claakecode-icon.png b/claakecode-web/assets/claakecode-icon.png new file mode 100644 index 00000000..47006e63 Binary files /dev/null and b/claakecode-web/assets/claakecode-icon.png differ diff --git a/claakecode-web/assets/presentation.mov b/claakecode-web/assets/presentation.mov new file mode 100644 index 00000000..9190170b Binary files /dev/null and b/claakecode-web/assets/presentation.mov differ diff --git a/claakecode-web/index.html b/claakecode-web/index.html new file mode 100644 index 00000000..7c32124a --- /dev/null +++ b/claakecode-web/index.html @@ -0,0 +1,585 @@ + + + + +Claake Code — agentic IDE + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+ Open source · MIT + v0.1.16 + Database settings + macOS · linux · windows +
+
+
+

The open AI IDE
you can actually
control.

+
+
+

+ Claake Code is a native desktop IDE with an agent, terminal, editor, MCP, skills, database sources and multi-provider models — all open source. Toggle every tool, rewrite every skill, connect the data sources you approve, swap providers mid-conversation, and keep the harness under your control. +

+ +

No account required · Bring your own API keys · MIT licensed · No telemetry

+
+
+ 17editable tools
Including database list, schema and query.
+ unlimited skills
Markdown playbooks the agent can load.
+ DBdatabase settings
Connect sources the agent can inspect and query.
+
+
+
+ + +
+ +
+
Latest release · v0.1.16
+
+
+

Database settings land in Claake Code.

+

+ Version 0.1.16 adds a dedicated Settings database section and updates the downloadable files so new installs pull the latest desktop build. +

+ +
+
+
+ I +

Settings database

+

Configure database sources directly in the app: Postgres, MySQL, SQLite, MSSQL and Supabase REST. Sources can be enabled, tested and kept under your control from Settings.

+
+
+ II +

Agent-safe data access

+

The agent gets database tools only through configured sources, with schema inspection, query execution, row limits, read-only options, activity history and destructive-action confirmation.

+
+
+ III +

Downloads refreshed

+

The macOS, Windows and Linux download buttons now target the latest 0.1.16 assets from GitHub releases.

+
+
+
+
+
+ + +
+ +
+

Not a prompt box.
A real IDE with an agent inside.

+

+ Open a repo, pick your provider, choose the tools the agent can use, then let it plan, edit, run, inspect and coordinate sub-agents — with everything visible and editable. +

+
+
+
+ +
+
+
+ + +
+ RUNS ON → + TypeScript· + Python· + Rust· + Go· + Swift· + Elixir· + Ruby· + SQL + MODELS → + Anthropic· + OpenAI· + Google· + Kimi· + OpenRouter + DATA → + Postgres· + MySQL· + SQLite· + MSSQL· + Supabase +
+ + +
+ +

+ One workspace. Every tool, every skill, every provider, every database source
+ under your control — from a UI, not a config file. +

+

+ No CLI gymnastics. No hidden system prompt. Open the panel, flip a tool on, rewrite its description, drop in a new skill, connect a database source, swap from Sonnet to GPT to Gemini in a click. The agent runs exactly the harness you assembled. +

+ +
+
+
↳ tools
+

Toggle, rewrite, ship.

+

+ Every built-in tool — read, edit, grep, bash, web, image, MCP, database — has an on/off switch and an editable description. Run a minimal pair-programmer or unlock the full agentic surface. Your call, per workspace. +

+
+
+
↳ skills
+

Plain markdown, fully yours.

+

+ Long-form skills live on disk in ~/.claakecode/skills. Create, edit, delete from the UI. The agent loads them on demand — no SDK, no plugin store, no review queue. +

+
+
+
↳ providers
+

Swap models without leaving the chat.

+

+ Anthropic, OpenAI, Google, Kimi, OpenRouter — wired into the same agent loop. OAuth where it makes sense, API keys where it doesn't. Switch provider, model, or context window mid-conversation. Zero lock-in. +

+
+
+
↳ database
+

Database sources, gated by settings.

+

+ Add Postgres, MySQL, SQLite, MSSQL or Supabase REST sources from Settings. The agent can list sources, describe schemas and query only the databases you enable — with row limits, read-only mode and destructive-action confirmation. +

+
+
+
+ + +
+ +
+
+

Plan. Act. Observe.
Reflect. Repeat.

+

+ Same agent loop across Anthropic, OpenAI, Google, Kimi, and OpenRouter. After each tool call the agent re-reads the world, scores its progress against the plan, and decides whether to continue, branch, or ask you a question. +

+

+ No magic. No hidden tools. No prompt you can't see. +

+
+ read + edit + grep + bash + skill + subagent + team + database + MCP · any server +
+
+
+ + + + + + + + + + + + + plan + + + + act + + + + observe + + + + reflect + + loop + until done + +
+
+
+ + +
+ +
+
+

The same agent loop can behave like a barebones pair-programmer or a full autonomous crew.

+

+ Tool descriptions live in settings, versioned with your config, and the assistant gets exactly the surface area you decide on. +

+
+
+
+ Minimal + read · patch · bash +
+
+ Search + + grep · glob +
+
+ Web + + WebSearch · WebFetch +
+
+ Full + + image · mcp · database · todo · question · skill · subagent · team +
+
+
+
+ + +
+ +

Seven things we will never compromise on.

+
+ +
+
+
↳ 01
+

Reshape every tool

+

Every tool description is editable, every tool is toggleable. Run minimal like Pi, or unlock the full set with MCP, skills, and a sub-agent swarm.

+
+ +
+ +
+
+
↳ 02
+

Bring your own model

+

Same agent loop across Anthropic, OpenAI, Google, Kimi, and OpenRouter. OAuth where it makes sense, API keys where it doesn't. Switch mid-project.

+
+ +
+ +
+
+
↳ 03
+

Database settings

+

Add database sources from Settings, test connections, import candidates from .env files, and decide which sources the agent can list, inspect or query.

+
+ +
+ +
+
+
↳ 04
+

Native MCP support

+

The Model Context Protocol is wired into the core. Add a server from a UI form — the agent picks it up on the next turn. No restart, no JSON spelunking.

+
+ +
+ +
+
+
↳ 05
+

1M context, only when you want it

+

Sonnet 4.6's 1M beta window is one toggle away — without forcing the beta header on accounts that don't have access.

+
+ +
+ +
+
+
↳ 06
+

Skills, in the open

+

Long-form, on-disk skills the agent can invoke. Create, edit, and delete them from the UI — they live as plain markdown in ~/.claakecode/skills.

+
+ +
+ +
+
+
↳ 07
+

Forkable, hackable core

+

MIT licensed, written in Rust + React on Tauri 2. Read the source, send a PR, or fork it for your team's house style — that's literally how this project started.

+
+ +
+ +
+
+ + +
+ +
+
+

The contract is simple:
+ you own the tools you build with. +

+

+ Claake Code is MIT licensed. The agent runtime, the editor, the prompt library, the providers — everything is in a single Cargo workspace with a React UI on top. If you can read Rust and TypeScript, you can read everything we ship. +

+

+ No telemetry. No training on your transcripts. No enterprise tier hiding the good features. The same binary the maintainers use is the binary you download. +

+
+ Read the source ↗ + Issues + Discussions +
+
+
+
AGENTS.md · project memory
+
+# House rules
+
+- Run `cargo check --workspace` before committing.
+- Prefer the dedicated tool over `bash`.
+- Keep the surface editable: every tool description
+  lives in settings, every tool has an on/off toggle.
+
+## tools
+mcp.* : configured per-workspace, hot-reloaded
+skill : long-form on-disk in ~/.claakecode/skills
+
+
+ +
+
5
Providers wired
+
17
Built-in tools
+
MCP servers supported
+
MIT
Forever and ever
+
+
+ + +
+ +

Everyone says "agentic". Few let you see and rewrite the prompt.

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
CapabilityClaake CodeCursorClaude CodeAiderZed AI
Native desktop appCLICLI
Open source
Multi-provider
Editable tool descriptions
Toggle individual toolspartial
Database sources in Settings
MCP server CRUD UIpartialpartial
Skills CRUD UI
1M context beta toggle (Sonnet)partial
Agent swarm + task board
Embedded terminaln/an/a
+
+
+ + +
+ +
+
+

One double-click.
+ No terminal, no setup wizard. +

+

+ Grab the native build for your OS, open the app, and drop in whichever provider keys and database sources you want — Anthropic, OpenAI, Google, Kimi, OpenRouter, plus Postgres, MySQL, SQLite, MSSQL or Supabase. The tool, skill, provider and database panels take care of the rest. No package manager, no shell scripts, no setup wizard. +

+
Latest release v0.1.16 · Native desktop · Bring your own keys · Open source
+
+ +
+
+ + + +
+ + + + + diff --git a/claakecode-web/styles.css b/claakecode-web/styles.css new file mode 100644 index 00000000..743841ef --- /dev/null +++ b/claakecode-web/styles.css @@ -0,0 +1,880 @@ +/* ════════════════════════════════════════════════════════════ + CLAAKE CODE · "ATELIER" — green + off-white plates + ════════════════════════════════════════════════════════════ */ + +:root { + --green: #3A5F3F; + --green-deep: #1F3823; + --green-ink: #0E1F12; + --green-bright: #5C8A60; + --green-soft: #88A48A; + --green-mist: #C8D4C5; + --cream: #F2EAD3; + --cream-warm: #ECE0BD; + --cream-bright: #FAF4DE; + --paper: #F6EDD2; + --ink: #16110A; + --ink-soft: #3B3329; + --ink-mute: #6F6553; + --rule: rgba(15, 34, 24, 0.35); + --rule-soft: rgba(15, 34, 24, 0.18); + + /* legacy aliases used by inline styles in markup */ + --forest: var(--green); + --forest-deep: var(--green-deep); + --forest-ink: var(--green-ink); + --accent: #B14A2F; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: 'Manrope', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--cream); + color: var(--green-ink); + font-size: 17px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + overflow-x: hidden; + font-feature-settings: "ss01", "cv11"; +} +body::after { + content: ""; + position: fixed; inset: 0; + pointer-events: none; + z-index: 200; + opacity: 0.45; + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml;utf8,"); +} + +.page { position: relative; z-index: 2; max-width: 1320px; margin: 0 auto; padding: 0 40px; } + +.mono { font-family: 'Geist Mono', ui-monospace, SFMono-Regular, monospace; } +.serif { font-family: 'DM Serif Display', 'Spectral', 'Georgia', serif; font-style: italic; font-weight: 400; } +.sans { font-family: 'Manrope', sans-serif; } +.lbl { + font-family: 'Geist Mono'; font-size: 11px; + letter-spacing: 0.18em; text-transform: uppercase; + color: var(--green); +} + +a { color: inherit; text-decoration: none; } +button { font: inherit; cursor: pointer; background: none; border: 0; color: inherit; } + +/* ── NAV ─────────────────────────────────────────────────── */ +.nav { + display: flex; align-items: center; justify-content: space-between; + padding: 22px 0 18px; + border-bottom: 1px solid var(--rule); + gap: 24px; + flex-wrap: wrap; +} +.nav-brand { display: flex; align-items: center; gap: 12px; } +.nav-brand .mark { + width: 38px; height: 38px; border-radius: 0; + overflow: hidden; + display: grid; place-items: center; + background: transparent; + border: 1px solid var(--green-ink); +} +.nav-brand .mark img { width: 100%; height: 100%; object-fit: contain; display: block; } +.nav-brand .name { + font-family: 'DM Serif Display'; font-style: italic; + font-size: 26px; line-height: 1; + color: var(--green-ink); letter-spacing: -0.02em; + font-weight: 400; +} +.nav-brand .name em { font-style: normal; color: var(--green); } +.nav-links { display: flex; gap: 24px; flex-wrap: wrap; } +.nav-links a { + font-family: 'Geist Mono'; font-size: 11px; + letter-spacing: 0.14em; text-transform: uppercase; + color: var(--ink-soft); +} +.nav-links a:hover { color: var(--green); } +.nav-cta { display: flex; gap: 10px; align-items: center; } + +/* ── BUTTONS / PILLS ─────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; gap: 8px; + padding: 11px 18px; + border-radius: 0; + font-family: 'Geist Mono'; font-size: 11px; + letter-spacing: 0.16em; text-transform: uppercase; + border: 1px solid var(--green-ink); + text-decoration: none; + transition: transform .12s ease, background .15s ease, color .15s ease, border-color .15s ease; +} +.btn-primary { background: var(--green); color: var(--cream); border-color: var(--green-deep); } +.btn-primary:hover { background: var(--green-deep); } +.btn-windows { background: var(--green-deep); color: var(--cream); border-color: var(--green-ink); } +.btn-windows:hover { background: var(--green-ink); } +.btn-linux { background: var(--accent); color: var(--cream); border-color: var(--accent); } +.btn-linux:hover { background: #8f3924; } +.btn-ghost { background: transparent; color: var(--green-ink); border-color: var(--rule); } +.btn-ghost:hover { border-color: var(--green); color: var(--green); } + +.pill { + display: inline-flex; align-items: center; gap: 6px; + padding: 5px 12px; + background: transparent; + border: 1px solid var(--green); + color: var(--green); + border-radius: 0; + font-family: 'Geist Mono'; font-size: 10.5px; + text-transform: uppercase; + letter-spacing: 0.16em; +} +.pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green-bright); } + +/* ── SECTIONS / PLATES ───────────────────────────────────── */ +section { + padding: 96px 0; + border-bottom: 1px solid var(--rule); + position: relative; +} +section.plate-green { background: var(--green-deep); color: var(--cream); margin: 0 -40px; padding: 96px 40px; } +section.plate-green .section-tag { color: var(--green-mist); } +section.plate-green .section-tag .num { color: var(--green-mist); } +section.plate-green .section-tag .rule { background: rgba(242,234,211,0.25); } +section.plate-green h1, +section.plate-green h2, +section.plate-green h3, +section.plate-green h4 { color: var(--cream); } +section.plate-green p { color: var(--green-mist); } +section.plate-green em.serif { color: var(--green-mist) !important; } +section.plate-green .pill { + border-color: var(--green-bright); + color: var(--green-mist); +} +section.plate-green .pill .dot { background: var(--green-bright); } +section.plate-green .loop-svg circle { fill: var(--green-deep) !important; stroke: var(--green-mist) !important; } +section.plate-green .loop-svg text { fill: var(--green-mist) !important; } +section.plate-green .loop-svg path[stroke] { stroke: var(--green-mist) !important; } +section.plate-green .loop-svg marker path { fill: var(--green-mist) !important; } +section.plate-green .loop-svg text[style*="DM Serif"] { fill: var(--green-bright) !important; } +section.plate-green .btn-primary { background: var(--green-bright); color: var(--green-ink); border-color: var(--green-bright); } +section.plate-green .btn-primary:hover { background: var(--cream); border-color: var(--cream); } +section.plate-green .btn-ghost { color: var(--cream); border-color: rgba(242,234,211,0.4); } +section.plate-green .btn-ghost:hover { color: var(--green-bright); border-color: var(--green-bright); } +section.plate-green .stats { border-color: rgba(242,234,211,0.25); } +section.plate-green .stat { + background: rgba(255,255,255,0.04); + border-right-color: rgba(242,234,211,0.18); +} +section.plate-green .stat .v { color: var(--green-mist); } +section.plate-green .stat .l { color: var(--green-mist); } +section.plate-green .oss-grid > div:last-child { + background: rgba(255,255,255,0.04) !important; + border-color: var(--green-bright) !important; + box-shadow: 12px 12px 0 var(--green-bright); +} +section.plate-green .oss-grid pre { color: var(--cream) !important; } +section.plate-green .oss-grid .mono { color: var(--green-mist) !important; } +section.plate-green p[style*="--ink-soft"], +section.plate-green p[style*="--ink"] { color: var(--green-mist) !important; } +section.plate-green p[style*="--forest"] { color: var(--green-bright) !important; } +section.plate-green span[style*="--forest"] { color: var(--green-bright) !important; } +section.plate-green em[style*="--forest"] { color: var(--green-mist) !important; } + +.section-tag { + display: flex; align-items: baseline; gap: 14px; + font-family: 'Geist Mono'; font-size: 11px; + text-transform: uppercase; letter-spacing: 0.18em; + color: var(--ink-soft); + margin-bottom: 56px; +} +.section-tag .num { + color: var(--green); + font-family: 'DM Serif Display'; font-style: italic; + font-size: 64px; line-height: 0.88; + letter-spacing: -0.04em; + text-transform: none; + align-self: center; +} +.section-tag .rule { flex: 1; height: 1px; background: var(--rule); } + +/* ── TYPOGRAPHY ──────────────────────────────────────────── */ +h1, h2, h3, h4 { + margin: 0; + font-family: 'DM Serif Display'; + font-weight: 400; + letter-spacing: -0.025em; +} +h1 { + font-size: 132px; line-height: 0.88; + letter-spacing: -0.04em; +} +h2 { + font-size: 72px; line-height: 0.94; + letter-spacing: -0.025em; +} +h3 { font-size: 26px; line-height: 1.1; letter-spacing: -0.02em; } +h1 em, h2 em, h3 em { + font-style: italic; + color: var(--green); +} +p { margin: 0; } + +.serif, em.serif { + font-family: 'DM Serif Display', 'Spectral', serif !important; + font-style: italic !important; + font-weight: 400 !important; +} + +/* ── HERO · PLATE 01 ─────────────────────────────────────── */ +.hero { + background: var(--green-deep); + color: var(--cream); + margin: 0 -40px; + padding: 48px 40px 64px; + border-bottom: 1px solid var(--green-ink); + position: relative; + overflow: hidden; +} +.hero-meta { + display: flex; + align-items: center; + justify-content: space-between; + font-family: 'Geist Mono'; font-size: 11px; + letter-spacing: 0.16em; text-transform: uppercase; + color: var(--green-mist); + padding-bottom: 28px; + border-bottom: 1px solid rgba(242,234,211,0.22); + margin-bottom: 48px; + flex-wrap: wrap; + gap: 14px; +} +.hero .hero-meta .pill { + background: transparent; + border: 1px solid var(--green-bright); + color: var(--green-mist); + letter-spacing: 0.14em; + font-size: 10.5px; +} +.hero .hero-meta .pill .dot { background: var(--green-bright); } + +.hero-grid { + display: grid; + grid-template-columns: 1.15fr 1fr; + gap: 56px; + align-items: end; +} +.hero-grid-spread { + grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.9fr); + gap: 40px 64px; + align-items: end; +} +.hero-title-block { min-width: 0; } +.hero-title-block::before { + content: "I · OUVERTURE"; + display: block; + font-family: 'Geist Mono'; font-size: 11px; + letter-spacing: 0.22em; color: var(--green-mist); + margin-bottom: 28px; +} +.hero h1 { + font-family: 'DM Serif Display'; font-weight: 400; + font-size: 144px; line-height: 0.86; + letter-spacing: -0.04em; + margin: 0; color: var(--cream); +} +.hero h1 em { + font-style: italic; color: var(--green-bright); +} + +.hero-copy-block { min-width: 0; align-self: end; padding-bottom: 4px; } +.hero-sub { + margin: 0; + font-family: 'Spectral', 'Georgia', serif; + font-style: italic; + font-size: 22px; + line-height: 1.5; + color: var(--green-mist); + max-width: 560px; +} +.hero-cta { + margin-top: 32px; + display: flex; gap: 12px; flex-wrap: wrap; +} +.hero-cta .btn-primary { background: var(--green-bright); color: var(--green-ink); border-color: var(--green-bright); } +.hero-cta .btn-primary:hover { background: var(--cream); border-color: var(--cream); } +.hero-cta .btn-windows { background: transparent; color: var(--cream); border-color: var(--cream); } +.hero-cta .btn-windows:hover { background: var(--cream); color: var(--green-ink); } +.hero-cta .btn-linux { background: var(--accent); color: var(--cream); border-color: var(--accent); } +.hero-cta .btn-ghost { color: var(--cream); border-color: rgba(242,234,211,0.4); } +.hero-cta .btn-ghost:hover { border-color: var(--cream); color: var(--cream); } +.hero-note { + margin-top: 18px; + color: var(--green-mist); + font-family: 'Geist Mono'; + font-size: 10.5px; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.hero-chips { + grid-column: 1 / -1; + margin-top: 48px; + padding-top: 28px; + border-top: 1px solid rgba(242,234,211,0.22); + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0; +} +.chip { + display: flex; align-items: center; gap: 18px; + padding: 8px 28px 8px 0; + background: transparent; + border: 0; + border-right: 1px solid rgba(242,234,211,0.22); +} +.chip:last-child { border-right: 0; } +.chip-num { + font-family: 'DM Serif Display'; font-style: italic; + font-size: 64px; line-height: 0.9; + color: var(--green-bright); + letter-spacing: -0.03em; + font-weight: 400; + flex-shrink: 0; +} +.chip-label { + font-family: 'Geist Mono'; font-size: 10.5px; + color: var(--green-mist); + text-transform: uppercase; letter-spacing: 0.14em; + line-height: 1.55; +} +.chip-label strong { color: var(--cream); font-weight: 500; } + +/* ── PATCH NOTES — featured right after hero ─────────────── */ +.release-card { + background: var(--cream-bright); + color: var(--green-ink); + border: 1px solid var(--green-ink); + border-radius: 0; + padding: 36px; + box-shadow: 18px 18px 0 var(--green); + position: relative; +} +.release-card::before { + content: "EXHIBIT · A"; + position: absolute; + top: -16px; left: 36px; + background: var(--green-deep); + color: var(--cream); + padding: 6px 14px; + font-family: 'Geist Mono'; font-size: 10px; + letter-spacing: 0.22em; + transform: rotate(-2deg); +} +.release-kicker { + color: var(--green); + font-family: 'Geist Mono'; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.18em; + margin-bottom: 22px; + display: inline-block; + padding-bottom: 6px; + border-bottom: 1px solid var(--rule); +} +.release-grid { + display: grid; + grid-template-columns: 0.95fr 1.05fr; + gap: 48px; + align-items: start; +} +.release-card h2 { + color: var(--green-ink); + max-width: 520px; + font-size: 56px; line-height: 0.95; +} +.release-card h2 em { color: var(--green); } +.release-intro { + color: var(--ink-soft); + font-family: 'Spectral', serif; + font-style: italic; + font-size: 19px; + line-height: 1.55; + margin-top: 20px; + max-width: 480px; +} +.release-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 28px; } +.release-notes { + display: grid; + gap: 0; + background: transparent; + border: 1px solid var(--green-ink); + border-radius: 0; + overflow: hidden; +} +.release-notes article { + background: var(--cream); + padding: 22px 26px 24px; + border-bottom: 1px solid var(--rule-soft); + position: relative; +} +.release-notes article:last-child { border-bottom: 0; } +.release-num { + font-family: 'DM Serif Display'; + font-style: italic; + font-size: 28px; + color: var(--green); + letter-spacing: -0.02em; + font-weight: 400; +} +.release-notes h3 { + margin-top: 6px; + color: var(--green-ink); + font-size: 24px; + line-height: 1.1; +} +.release-notes p { + color: var(--ink-soft); + font-family: 'Spectral', serif; + font-style: italic; + font-size: 15.5px; + line-height: 1.55; + margin-top: 10px; +} + +/* ── PRODUCT SHOWCASE ────────────────────────────────────── */ +.showcase { padding-top: 96px; } +.showcase-head { + display: grid; + grid-template-columns: minmax(0, 1.1fr) minmax(320px, 520px); + gap: 48px; + align-items: end; + margin-bottom: 40px; +} +.showcase-head h2 { font-size: 84px; } +.showcase-head p { + color: var(--ink-soft); + font-family: 'Spectral', serif; + font-style: italic; + font-size: 19px; + line-height: 1.55; + max-width: 520px; +} +.video-stage { margin: 0; } +.video-frame { + position: relative; + background: var(--cream-bright); + aspect-ratio: 16 / 10; + overflow: hidden; + border: 1px solid var(--green-ink); + box-shadow: 20px 20px 0 var(--green); + padding: 10px; +} +.video-frame-wide { aspect-ratio: 16 / 9; } +.video-el { + position: absolute; inset: 10px; + width: calc(100% - 20px); height: calc(100% - 20px); + object-fit: contain; + display: block; + background: transparent; +} + +/* ── SUPPORT STRIP ───────────────────────────────────────── */ +.support { + display: flex; flex-wrap: wrap; gap: 18px 32px; + align-items: center; + padding: 22px 24px; + margin: 0; + background: var(--green-deep); + color: var(--green-mist); + font-family: 'Geist Mono'; font-size: 11px; + letter-spacing: 0.14em; text-transform: uppercase; + border: 1px solid var(--green-ink); +} +.support .label { color: var(--green-bright); } + +/* ── PILLAR GRID ─────────────────────────────────────────── */ +.pillar-grid { + display: grid; grid-template-columns: repeat(4, 1fr); + gap: 24px; + margin-top: 56px; +} +.pillar { + background: var(--cream-bright); + border: 1px solid var(--green-deep); + padding: 28px 26px 24px; + display: flex; flex-direction: column; gap: 12px; + scroll-margin-top: 100px; + position: relative; + min-height: 320px; +} +.pillar::before { + content: ""; + position: absolute; + top: 0; left: 0; right: 0; + height: 4px; + background: var(--green); +} +.pillar-accent { background: var(--cream-warm); } +.pillar-accent::before { background: var(--green-bright); } +.pillar-release { + background: var(--green-deep); + color: var(--cream); + border-color: var(--green-ink); +} +.pillar-release::before { background: var(--cream); } +.pillar-release h3 { color: var(--cream); } +.pillar-release p { color: var(--green-mist); } +.pillar-release .pillar-tag { color: var(--green-mist); } +.pillar-tag { + font-family: 'Geist Mono'; font-size: 11px; + color: var(--green); + text-transform: uppercase; letter-spacing: 0.18em; +} +.pillar h3 { + font-family: 'DM Serif Display'; font-weight: 400; + font-size: 26px; line-height: 1.1; letter-spacing: -0.02em; + margin-top: 4px; +} +.pillar p { + font-family: 'Spectral', serif; font-style: italic; + color: var(--ink-soft); + font-size: 16px; line-height: 1.55; + margin: 0; +} +.pillar .mono { + background: rgba(58, 95, 63, 0.12); + padding: 1px 6px; + font-style: normal; + font-size: 13px; + color: var(--green-deep); +} +.pillar-release .mono { background: rgba(255,255,255,0.1); color: var(--cream); } + +/* ── LOOP DIAGRAM ────────────────────────────────────────── */ +.loop-wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 56px; align-items: center; } +.loop-svg { width: 100%; height: auto; } + +/* ── HARNESS ─────────────────────────────────────────────── */ +.harness-wrap { + display: grid; + grid-template-columns: 1.05fr 1fr; + gap: 56px; + align-items: start; +} +.harness-wrap > div:last-child { + display: grid; gap: 0; + font-family: 'Geist Mono'; font-size: 13px; + border: 1px solid var(--green-ink); +} +.harness-wrap > div:last-child > div { + display: grid !important; + grid-template-columns: 140px 1fr !important; + gap: 18px !important; + padding: 18px 22px !important; + background: var(--cream-bright) !important; + border: 0 !important; + border-bottom: 1px solid var(--rule) !important; + border-radius: 0 !important; +} +.harness-wrap > div:last-child > div:last-child { + background: var(--green-deep) !important; + color: var(--cream) !important; + border-bottom: 0 !important; +} +.harness-wrap > div:last-child > div span:first-child { + color: var(--green) !important; + text-transform: uppercase !important; + letter-spacing: 0.18em !important; + font-size: 11px !important; + font-weight: 500 !important; + font-family: 'Geist Mono' !important; +} +.harness-wrap > div:last-child > div:last-child span:first-child { + color: var(--green-bright) !important; +} + +/* ── FEATURES GRID ───────────────────────────────────────── */ +.feat-grid { + display: grid; grid-template-columns: repeat(3, 1fr); + gap: 24px; +} +.feat { + background: var(--cream-bright); + border: 1px solid var(--green-deep); + padding: 28px 26px; + min-height: 260px; + display: flex; flex-direction: column; justify-content: space-between; + position: relative; +} +.feat-highlight { + background: var(--green-deep); + color: var(--cream); + border-color: var(--green-ink); +} +.feat-highlight h3 { color: var(--cream); } +.feat-highlight p { color: var(--green-mist); } +.feat-highlight .feat-num { color: var(--green-mist); } +.feat-highlight .feat-icon path, +.feat-highlight .feat-icon rect, +.feat-highlight .feat-icon ellipse, +.feat-highlight .feat-icon circle { stroke: var(--green-mist) !important; } +.feat-num { + font-family: 'Geist Mono'; font-size: 11px; + color: var(--green); + letter-spacing: 0.18em; + text-transform: uppercase; +} +.feat h3 { + margin-top: 16px; + font-family: 'DM Serif Display'; font-weight: 400; + font-size: 26px; line-height: 1.1; letter-spacing: -0.02em; +} +.feat p { + color: var(--ink-soft); + font-family: 'Spectral', serif; font-style: italic; + font-size: 15.5px; line-height: 1.55; + margin-top: 10px; +} +.feat-icon { width: 36px; height: 36px; align-self: flex-end; } + +/* ── STATS ───────────────────────────────────────────────── */ +.stats { + display: grid; grid-template-columns: repeat(4, 1fr); + border-top: 1px solid var(--green-ink); + border-bottom: 1px solid var(--green-ink); +} +.stat { + padding: 32px 24px; + border-right: 1px solid var(--rule); + background: var(--cream-bright); +} +.stat:last-child { border-right: 0; } +.stat .v { + font-family: 'DM Serif Display'; font-style: italic; + font-size: 72px; line-height: 0.9; color: var(--green); + letter-spacing: -0.03em; +} +.stat .l { + font-family: 'Geist Mono'; font-size: 11px; + color: var(--green); text-transform: uppercase; + letter-spacing: 0.18em; margin-top: 8px; +} + +/* ── COMPARE TABLE — restyle inline-styled markup ────────── */ +section#compare > div:last-of-type { + border: 1px solid var(--green-ink) !important; + border-radius: 0 !important; +} +section#compare table { + background: var(--cream-bright) !important; +} +section#compare thead tr { + background: var(--green-deep) !important; +} +section#compare thead th { + color: var(--green-mist) !important; + font-family: 'Geist Mono' !important; + letter-spacing: 0.18em !important; + font-weight: 500 !important; +} +section#compare thead th:nth-child(2) { color: var(--cream) !important; } +section#compare tbody td { + font-family: 'Geist Mono' !important; + color: var(--green-ink) !important; +} +section#compare tbody tr { + border-top: 1px solid var(--rule) !important; +} + +/* ── INSTALL ─────────────────────────────────────────────── */ +.install-grid { + display: grid; + grid-template-columns: 0.9fr 1.1fr; + gap: 48px; + align-items: start; +} +.download-trust { + margin-top: 22px; + color: var(--green); + font-family: 'Geist Mono'; + font-size: 11px; + letter-spacing: 0.16em; + text-transform: uppercase; +} +.download-cards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} +.download-card { + min-height: 200px; + padding: 26px; + background: var(--cream-bright); + border: 1px solid var(--green-ink); + text-decoration: none; + display: flex; + flex-direction: column; + gap: 10px; + position: relative; + transition: transform .14s ease, background .14s ease; +} +.download-card::before { + content: ""; + position: absolute; + top: 0; left: 0; right: 0; + height: 3px; + background: var(--green); +} +.download-card:hover { + transform: translateY(-2px); + background: var(--cream-warm); +} +.download-card.featured { + background: var(--green-deep); + color: var(--cream); + border-color: var(--green-ink); +} +.download-card.featured::before { background: var(--green-bright); } +.download-card.featured:hover { background: var(--green-ink); } +.download-os { + font-family: 'Geist Mono'; + font-size: 11px; + color: var(--green); + text-transform: uppercase; + letter-spacing: 0.18em; +} +.download-card.featured .download-os { color: var(--green-mist); } +.download-card strong { + font-family: 'DM Serif Display'; + font-weight: 400; + font-style: italic; + font-size: 28px; + line-height: 1; + letter-spacing: -0.02em; +} +.download-card span:not(.download-os) { + color: var(--ink-soft); + font-family: 'Spectral', serif; + font-style: italic; + font-size: 15px; + line-height: 1.5; +} +.download-card.featured span:not(.download-os) { color: var(--green-mist); } +.download-card em { + margin-top: auto; + font-family: 'Geist Mono'; + font-style: normal; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--green); +} +.download-card.featured em { color: var(--green-bright); } + +/* ── OPEN SOURCE block — keep inline grid but restyle ────── */ +.oss-grid { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 64px; + align-items: start; +} +.oss-grid pre { + font-family: 'Geist Mono' !important; + color: var(--green-ink) !important; + background: transparent !important; +} +.oss-grid > div:last-child { + background: var(--cream-bright) !important; + border: 1px solid var(--green-ink) !important; + border-radius: 0 !important; + box-shadow: 12px 12px 0 var(--green); +} + +/* ── FOOTER ──────────────────────────────────────────────── */ +footer { + padding: 64px 0 40px; + border-bottom: 0; + background: var(--green-deep); + color: var(--cream); + margin: 0 -40px; + padding-left: 40px; + padding-right: 40px; +} +.foot { + display: grid; + grid-template-columns: 1.4fr 1fr 1fr 1fr; + gap: 36px; + align-items: start; +} +footer .nav-brand .mark { border-color: var(--green-mist); } +footer .nav-brand .name { color: var(--cream); } +footer .nav-brand .name em { color: var(--green-bright); } +footer p.serif { + color: var(--green-mist) !important; + font-family: 'Spectral', serif !important; + font-style: italic !important; +} +.foot h4 { + font-family: 'Geist Mono'; font-size: 11px; + color: var(--green-mist); text-transform: uppercase; + letter-spacing: 0.18em; margin-bottom: 14px; + font-weight: 500; +} +.foot a { + display: block; padding: 5px 0; + font-size: 13px; text-decoration: none; + color: var(--cream); + font-family: 'Manrope'; +} +.foot a:hover { color: var(--green-bright); } +.foot-tag { + font-family: 'Geist Mono'; font-size: 11px; + color: var(--green-mist); text-transform: uppercase; + letter-spacing: 0.18em; + margin-top: 36px; padding-top: 22px; + border-top: 1px solid rgba(242,234,211,0.22); + display: flex; justify-content: space-between; + gap: 12px; flex-wrap: wrap; +} + +/* ── RESPONSIVE ──────────────────────────────────────────── */ +@media (max-width: 1100px) { + h1 { font-size: 96px; } + h2 { font-size: 54px; } + .hero h1 { font-size: 100px; } + .pillar-grid { grid-template-columns: repeat(2, 1fr); } + .feat-grid { grid-template-columns: repeat(2, 1fr); } + .hero-chips { grid-template-columns: 1fr; gap: 1px; background: rgba(242,234,211,0.22); } + .chip { background: var(--green-deep); border-right: 0; padding: 18px 0; } +} +@media (max-width: 960px) { + h1 { font-size: 72px; } + h2 { font-size: 44px; } + .hero h1 { font-size: 76px; } + .hero-grid, .hero-grid-spread, .loop-wrap, .showcase-head, .release-grid { + grid-template-columns: 1fr; gap: 36px; + } + .hero-copy-block { padding-bottom: 0; } + .feat-grid, .pillar-grid { grid-template-columns: 1fr; } + .stats { grid-template-columns: repeat(2, 1fr); } + .stat:nth-child(2) { border-right: 0; } + .foot { grid-template-columns: 1fr 1fr; } + .nav-links { display: none; } + .install-grid { grid-template-columns: 1fr !important; } + .harness-wrap, .oss-grid { grid-template-columns: 1fr !important; gap: 36px !important; } + .section-tag .num { font-size: 48px; } +} +@media (max-width: 560px) { + .page { padding: 0 22px; } + .hero { margin: 0 -22px; padding-left: 22px; padding-right: 22px; } + section.plate-green { margin: 0 -22px; padding-left: 22px; padding-right: 22px; } + footer { margin: 0 -22px; padding-left: 22px; padding-right: 22px; } + h1 { font-size: 56px; } + h2 { font-size: 36px; } + .hero h1 { font-size: 56px; } + .stats { grid-template-columns: 1fr; } + .stat { border-right: 0; border-bottom: 1px solid var(--rule); } + .foot { grid-template-columns: 1fr; } + .download-cards { grid-template-columns: 1fr; } + .download-card { min-height: auto; } + .release-card { padding: 24px; box-shadow: 8px 8px 0 var(--green); } + .video-frame { aspect-ratio: 4 / 3; box-shadow: 8px 8px 0 var(--green); } +} diff --git a/crates/sinew-google/Cargo.toml b/crates/claakecode-anthropic/Cargo.toml similarity index 84% rename from crates/sinew-google/Cargo.toml rename to crates/claakecode-anthropic/Cargo.toml index 6732c963..37608e4a 100644 --- a/crates/sinew-google/Cargo.toml +++ b/crates/claakecode-anthropic/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "sinew-google" +name = "claakecode-anthropic" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Google provider for sinew" +description = "Anthropic provider for ClaakeCode" [dependencies] -sinew-core = { workspace = true } +claakecode-core = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } diff --git a/crates/sinew-anthropic/src/auth.rs b/crates/claakecode-anthropic/src/auth.rs similarity index 98% rename from crates/sinew-anthropic/src/auth.rs rename to crates/claakecode-anthropic/src/auth.rs index 9e1a94fe..db6af9b4 100644 --- a/crates/sinew-anthropic/src/auth.rs +++ b/crates/claakecode-anthropic/src/auth.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tokio::sync::Mutex; -use sinew_core::{AppError, Result}; +use claakecode_core::{AppError, Result}; const ANTHROPIC_OAUTH_AUTHORIZE_URL: &str = "https://claude.ai/oauth/authorize"; const ANTHROPIC_OAUTH_TOKEN_URL: &str = "https://platform.claude.com/v1/oauth/token"; @@ -93,10 +93,10 @@ impl Credential { } pub fn load_default() -> Result> { - Self::from_sinew_auth_file(&default_auth_path()?) + Self::from_claakecode_auth_file(&default_auth_path()?) } - pub fn from_sinew_auth_file(path: &Path) -> Result> { + pub fn from_claakecode_auth_file(path: &Path) -> Result> { let bytes = match std::fs::read(path) { Ok(bytes) => bytes, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), @@ -251,7 +251,7 @@ fn persist_refresh(path: &Path, access: &str, refresh: &str, expires_at_ms: i64) } pub fn default_auth_path() -> Result { - let dirs = ProjectDirs::from("dev", "hyrak", "sinew") + let dirs = ProjectDirs::from("dev", "williampeynichou", "claakecode") .ok_or_else(|| AppError::Auth("unable to resolve local data directory".into()))?; Ok(dirs.data_local_dir().join("anthropic-auth.json")) } diff --git a/crates/sinew-anthropic/src/client.rs b/crates/claakecode-anthropic/src/client.rs similarity index 95% rename from crates/sinew-anthropic/src/client.rs rename to crates/claakecode-anthropic/src/client.rs index c5b41a2a..4658ae54 100644 --- a/crates/sinew-anthropic/src/client.rs +++ b/crates/claakecode-anthropic/src/client.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use serde::Serialize; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, Effort, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest, ProviderStream, Result, Role, TokenEstimate, ToolDescriptor, }; @@ -26,6 +26,7 @@ const CODE_SYSTEM_PREFIX: &str = "You are Claude Code, Anthropic's official CLI // available for this subscription` which we then mis-classified as a // context-length overflow and triggered auto-compaction on tiny inputs. const COMMON_BETA: &str = "fine-grained-tool-streaming-2025-05-14"; +const CONTEXT_1M_BETA: &str = "context-1m-2025-08-07"; const OAUTH_BETA: &str = "claude-code-20250219,oauth-2025-04-20"; const CACHE_BREAKPOINTS: usize = 4; const ANTHROPIC_MAX_IMAGE_BASE64_BYTES: usize = 5 * 1024 * 1024; @@ -79,7 +80,11 @@ impl AnthropicProvider { Self::new(AnthropicConfig::from_default_sources()?) } - async fn post(&self, route: &str) -> Result<(reqwest::RequestBuilder, String)> { + async fn post( + &self, + route: &str, + use_1m_context: bool, + ) -> Result<(reqwest::RequestBuilder, String)> { let token = self.config.credential.bearer_or_key(&self.http).await?; let is_oauth = self.config.credential.is_oauth(); let mut request = self @@ -92,7 +97,7 @@ impl AnthropicProvider { .header("anthropic-version", &self.config.api_version) .header("content-type", "application/json") .header("anthropic-dangerous-direct-browser-access", "true") - .header("anthropic-beta", self.beta_header(is_oauth)); + .header("anthropic-beta", self.beta_header(is_oauth, use_1m_context)); if is_oauth { request = request @@ -110,8 +115,10 @@ impl AnthropicProvider { &self, route: &str, body: &T, + use_1m_context: bool, ) -> Result { - self.send_json_accept(route, body, "application/json").await + self.send_json_accept(route, body, "application/json", use_1m_context) + .await } async fn send_json_accept( @@ -119,8 +126,9 @@ impl AnthropicProvider { route: &str, body: &T, accept: &'static str, + use_1m_context: bool, ) -> Result { - let (request, token) = self.post(route).await?; + let (request, token) = self.post(route, use_1m_context).await?; let response = request .header("accept", accept) .json(body) @@ -140,7 +148,7 @@ impl AnthropicProvider { .await .map_err(map_refresh_failure)?; - let (request, _) = self.post(route).await?; + let (request, _) = self.post(route, use_1m_context).await?; request .header("accept", accept) .json(body) @@ -149,12 +157,15 @@ impl AnthropicProvider { .map_err(|err| AppError::Network(err.to_string())) } - fn beta_header(&self, is_oauth: bool) -> String { + fn beta_header(&self, is_oauth: bool, use_1m_context: bool) -> String { let mut values = Vec::new(); if is_oauth { values.push(OAUTH_BETA.to_string()); } values.push(COMMON_BETA.to_string()); + if use_1m_context { + values.push(CONTEXT_1M_BETA.to_string()); + } if let Some(extra) = &self.config.extra_beta { if !extra.is_empty() { values.push(extra.clone()); @@ -201,7 +212,13 @@ impl Provider for AnthropicProvider { .collect(), }; - let response = self.send_json("/v1/messages/count_tokens", &body).await?; + let response = self + .send_json( + "/v1/messages/count_tokens", + &body, + request.model.use_1m_context_enabled(), + ) + .await?; if !response.status().is_success() { return Err(read_http_error(response, false).await); @@ -263,7 +280,12 @@ impl Provider for AnthropicProvider { }; let response = self - .send_json_accept("/v1/messages", &body, "text/event-stream") + .send_json_accept( + "/v1/messages", + &body, + "text/event-stream", + request.model.use_1m_context_enabled(), + ) .await?; if !response.status().is_success() { diff --git a/crates/sinew-anthropic/src/lib.rs b/crates/claakecode-anthropic/src/lib.rs similarity index 77% rename from crates/sinew-anthropic/src/lib.rs rename to crates/claakecode-anthropic/src/lib.rs index c627274e..6a5a42e7 100644 --- a/crates/sinew-anthropic/src/lib.rs +++ b/crates/claakecode-anthropic/src/lib.rs @@ -9,4 +9,4 @@ pub use auth::{ load_default_auth_status, oauth_authorize_url, AnthropicAuthStatus, Credential, PkceCodes, }; pub use client::{AnthropicConfig, AnthropicProvider}; -pub use model_info::{MODEL_ID, MODEL_MAX_OUTPUT, MODEL_WINDOW}; +pub use model_info::{supports_1m_context_beta, MODEL_ID, MODEL_MAX_OUTPUT, MODEL_WINDOW}; diff --git a/crates/sinew-anthropic/src/model_info.rs b/crates/claakecode-anthropic/src/model_info.rs similarity index 57% rename from crates/sinew-anthropic/src/model_info.rs rename to crates/claakecode-anthropic/src/model_info.rs index 85165917..747010c4 100644 --- a/crates/sinew-anthropic/src/model_info.rs +++ b/crates/claakecode-anthropic/src/model_info.rs @@ -1,4 +1,4 @@ -use sinew_core::{EffortMode, ModelCapabilities, ModelRef}; +use claakecode_core::{EffortMode, ModelCapabilities, ModelRef}; pub const MODEL_ID: &str = "claude-opus-4-7"; pub const MODEL_WINDOW: u32 = 1_000_000; @@ -9,6 +9,8 @@ struct AnthropicModelInfo { context_window: u32, preferred_window: u32, max_output_tokens: u32, + beta_1m_context_window: Option, + beta_1m_preferred_window: Option, } const MODELS: &[AnthropicModelInfo] = &[ @@ -17,24 +19,32 @@ const MODELS: &[AnthropicModelInfo] = &[ context_window: 1_000_000, preferred_window: 900_000, max_output_tokens: 128_000, + beta_1m_context_window: None, + beta_1m_preferred_window: None, }, AnthropicModelInfo { id: "claude-opus-4-6", context_window: 1_000_000, preferred_window: 900_000, max_output_tokens: 128_000, + beta_1m_context_window: None, + beta_1m_preferred_window: None, }, AnthropicModelInfo { id: "claude-sonnet-4-6", - context_window: 1_000_000, - preferred_window: 900_000, + context_window: 200_000, + preferred_window: 180_000, max_output_tokens: 128_000, + beta_1m_context_window: Some(1_000_000), + beta_1m_preferred_window: Some(900_000), }, AnthropicModelInfo { id: "claude-haiku-4-5", context_window: 200_000, preferred_window: 180_000, max_output_tokens: 64_000, + beta_1m_context_window: None, + beta_1m_preferred_window: None, }, ]; @@ -45,12 +55,25 @@ fn model_info(model_id: &str) -> &'static AnthropicModelInfo { .unwrap_or(&MODELS[0]) } +pub fn supports_1m_context_beta(model_name: &str) -> bool { + model_info(model_name).beta_1m_context_window.is_some() +} + pub fn capabilities(model: &ModelRef) -> ModelCapabilities { let info = model_info(&model.name); + let use_1m = model.use_1m_context_enabled(); + let (context_window, preferred_window) = if use_1m { + ( + info.beta_1m_context_window.unwrap_or(info.context_window), + info.beta_1m_preferred_window.unwrap_or(info.preferred_window), + ) + } else { + (info.context_window, info.preferred_window) + }; ModelCapabilities { model: model.clone(), - context_window: info.context_window, - preferred_window: info.preferred_window, + context_window, + preferred_window, max_output_tokens: info.max_output_tokens, supports_thinking: true, visible_thinking: true, diff --git a/crates/sinew-anthropic/src/stream.rs b/crates/claakecode-anthropic/src/stream.rs similarity index 99% rename from crates/sinew-anthropic/src/stream.rs rename to crates/claakecode-anthropic/src/stream.rs index 51af2a72..a4bb7b42 100644 --- a/crates/sinew-anthropic/src/stream.rs +++ b/crates/claakecode-anthropic/src/stream.rs @@ -2,7 +2,7 @@ use eventsource_stream::Eventsource; use futures::{stream::Stream, StreamExt}; use serde_json::json; -use sinew_core::{ +use claakecode_core::{ AppError, PartKind, ProviderStream, StopReason, StreamEvent, ToolCallIntro, Usage, }; diff --git a/crates/sinew-anthropic/src/wire.rs b/crates/claakecode-anthropic/src/wire.rs similarity index 100% rename from crates/sinew-anthropic/src/wire.rs rename to crates/claakecode-anthropic/src/wire.rs diff --git a/crates/sinew-app/Cargo.toml b/crates/claakecode-app/Cargo.toml similarity index 69% rename from crates/sinew-app/Cargo.toml rename to crates/claakecode-app/Cargo.toml index 7880af31..12e03187 100644 --- a/crates/sinew-app/Cargo.toml +++ b/crates/claakecode-app/Cargo.toml @@ -1,14 +1,14 @@ [package] -name = "sinew-app" +name = "claakecode-app" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Headless desktop runtime for sinew" +description = "Headless desktop runtime for ClaakeCode" [dependencies] -sinew-core = { workspace = true } -sinew-openai = { workspace = true } +claakecode-core = { workspace = true } +claakecode-openai = { workspace = true } anyhow = { workspace = true } base64 = { workspace = true } @@ -25,6 +25,14 @@ rusqlite = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } similar = { workspace = true } +sqlx = { version = "0.8", default-features = false, features = [ + "runtime-tokio-rustls", + "postgres", + "mysql", + "json", + "chrono", + "uuid", +] } tokio = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } diff --git a/crates/sinew-app/src/agent.rs b/crates/claakecode-app/src/agent.rs similarity index 100% rename from crates/sinew-app/src/agent.rs rename to crates/claakecode-app/src/agent.rs diff --git a/crates/sinew-app/src/agent/assistant_message.rs b/crates/claakecode-app/src/agent/assistant_message.rs similarity index 98% rename from crates/sinew-app/src/agent/assistant_message.rs rename to crates/claakecode-app/src/agent/assistant_message.rs index a7c68708..10677ebd 100644 --- a/crates/sinew-app/src/agent/assistant_message.rs +++ b/crates/claakecode-app/src/agent/assistant_message.rs @@ -2,7 +2,7 @@ use std::time::Instant; use serde_json::{json, Map, Value}; -use sinew_core::{ChatMessage, Part, PartKind, Role}; +use claakecode_core::{ChatMessage, Part, PartKind, Role}; use super::history::normalize_tool_call_input; diff --git a/crates/sinew-app/src/agent/cancel.rs b/crates/claakecode-app/src/agent/cancel.rs similarity index 100% rename from crates/sinew-app/src/agent/cancel.rs rename to crates/claakecode-app/src/agent/cancel.rs diff --git a/crates/sinew-app/src/agent/clean_context.rs b/crates/claakecode-app/src/agent/clean_context.rs similarity index 98% rename from crates/sinew-app/src/agent/clean_context.rs rename to crates/claakecode-app/src/agent/clean_context.rs index df46a568..033ac63b 100644 --- a/crates/sinew-app/src/agent/clean_context.rs +++ b/crates/claakecode-app/src/agent/clean_context.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use serde_json::{json, Map, Value}; -use sinew_core::{ChatMessage, Part, ToolDescriptor}; +use claakecode_core::{ChatMessage, Part, ToolDescriptor}; use crate::ToolRunResult; diff --git a/crates/sinew-app/src/agent/compaction.rs b/crates/claakecode-app/src/agent/compaction.rs similarity index 99% rename from crates/sinew-app/src/agent/compaction.rs rename to crates/claakecode-app/src/agent/compaction.rs index c5fccf22..c69511b5 100644 --- a/crates/sinew-app/src/agent/compaction.rs +++ b/crates/claakecode-app/src/agent/compaction.rs @@ -4,7 +4,7 @@ use serde_json::{json, Value}; use tokio::sync::mpsc; use uuid::Uuid; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, ModelRef, Part, Provider, ProviderRequest, ServiceTier, ToolDescriptor, }; diff --git a/crates/sinew-app/src/agent/context.rs b/crates/claakecode-app/src/agent/context.rs similarity index 88% rename from crates/sinew-app/src/agent/context.rs rename to crates/claakecode-app/src/agent/context.rs index 4ffb0088..a1b3ede8 100644 --- a/crates/sinew-app/src/agent/context.rs +++ b/crates/claakecode-app/src/agent/context.rs @@ -2,10 +2,10 @@ use std::sync::Arc; use tokio::sync::mpsc; -use sinew_core::{ChatMessage, Provider, ServiceTier}; +use claakecode_core::{ChatMessage, Provider, ServiceTier}; use crate::{ - BashTool, CreateImageTool, EditFileTool, GlobTool, GoalWorkflowState, GrepTool, + BashTool, CreateImageTool, DatabaseTool, EditFileTool, GlobTool, GoalWorkflowState, GrepTool, McpToolRegistry, QuestionTool, ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool, TodoListState, ToolSettings, WebFetchTool, WebSearchTool, WriteFileTool, }; @@ -25,7 +25,7 @@ pub enum AgentMode { pub struct TurnContext { pub provider: Arc, - pub model: sinew_core::ModelRef, + pub model: claakecode_core::ModelRef, pub cache_key: Option, pub cache_stable_message_count: usize, pub service_tier: Option, @@ -48,6 +48,7 @@ pub struct TurnContext { pub web_search: Arc, pub web_fetch: Arc, pub skill: Arc, + pub database: Arc, pub mcp: Arc, pub subagents: Option>, pub teams: Option>, diff --git a/crates/sinew-app/src/agent/events.rs b/crates/claakecode-app/src/agent/events.rs similarity index 98% rename from crates/sinew-app/src/agent/events.rs rename to crates/claakecode-app/src/agent/events.rs index a844ca1f..0c860ae7 100644 --- a/crates/sinew-app/src/agent/events.rs +++ b/crates/claakecode-app/src/agent/events.rs @@ -4,7 +4,7 @@ use serde::Serialize; use serde_json::{json, Map, Value}; use tokio::sync::mpsc; -use sinew_core::{ChatMessage, ModelRef, Part, Provider, Usage}; +use claakecode_core::{ChatMessage, ModelRef, Part, Provider, Usage}; use crate::tool_run::{FileChange, ToolRunImage}; diff --git a/crates/sinew-app/src/agent/history.rs b/crates/claakecode-app/src/agent/history.rs similarity index 99% rename from crates/sinew-app/src/agent/history.rs rename to crates/claakecode-app/src/agent/history.rs index 1143cee8..c7ea96fc 100644 --- a/crates/sinew-app/src/agent/history.rs +++ b/crates/claakecode-app/src/agent/history.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeSet, HashMap}; use serde_json::{json, Value}; -use sinew_core::{ChatMessage, Part, Role}; +use claakecode_core::{ChatMessage, Part, Role}; use crate::{ReadFingerprint, ReadTool}; diff --git a/crates/sinew-app/src/agent/mode.rs b/crates/claakecode-app/src/agent/mode.rs similarity index 99% rename from crates/sinew-app/src/agent/mode.rs rename to crates/claakecode-app/src/agent/mode.rs index 27eba3e3..6f32b492 100644 --- a/crates/sinew-app/src/agent/mode.rs +++ b/crates/claakecode-app/src/agent/mode.rs @@ -2,7 +2,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use crate::{store::DEFAULT_PLAN_MODE_PROMPT, GoalWorkflowState, ToolRunResult}; diff --git a/crates/sinew-app/src/agent/tests.rs b/crates/claakecode-app/src/agent/tests.rs similarity index 99% rename from crates/sinew-app/src/agent/tests.rs rename to crates/claakecode-app/src/agent/tests.rs index b49f5fa8..6b628fc7 100644 --- a/crates/sinew-app/src/agent/tests.rs +++ b/crates/claakecode-app/src/agent/tests.rs @@ -2,7 +2,7 @@ use std::collections::BTreeSet; use serde_json::json; -use sinew_core::{ChatMessage, Part, Role}; +use claakecode_core::{ChatMessage, Part, Role}; use super::{ clean_context::{run_clean_context, tool_result_cleaned, CLEAN_CONTEXT_RESULT_PLACEHOLDER}, diff --git a/crates/sinew-app/src/agent/tool_dispatch.rs b/crates/claakecode-app/src/agent/tool_dispatch.rs similarity index 90% rename from crates/sinew-app/src/agent/tool_dispatch.rs rename to crates/claakecode-app/src/agent/tool_dispatch.rs index d08e1f7d..a0d65837 100644 --- a/crates/sinew-app/src/agent/tool_dispatch.rs +++ b/crates/claakecode-app/src/agent/tool_dispatch.rs @@ -4,9 +4,9 @@ use serde_json::Value; use tokio::sync::mpsc; use crate::{ - BashTool, CreateImageTool, EditFileTool, GlobTool, GrepTool, McpToolRegistry, QuestionTool, - ReadFingerprint, ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool, TodoListState, - ToolRunResult, ToolSettings, WebFetchTool, WebSearchTool, WriteFileTool, + BashTool, CreateImageTool, DatabaseTool, EditFileTool, GlobTool, GrepTool, McpToolRegistry, + QuestionTool, ReadFingerprint, ReadTool, SkillTool, SubAgentTool, TeamTool, ToDoListTool, + TodoListState, ToolRunResult, ToolSettings, WebFetchTool, WebSearchTool, WriteFileTool, }; use super::{cancel::TurnCancel, context::AgentMode, events::AgentEvent}; @@ -38,6 +38,7 @@ pub(super) async fn run_tool( web_search: &WebSearchTool, web_fetch: &WebFetchTool, skill: &SkillTool, + database: &DatabaseTool, mcp: &McpToolRegistry, subagents: Option<&SubAgentTool>, teams: Option<&TeamTool>, @@ -99,6 +100,8 @@ pub(super) async fn run_tool( web_fetch.run(input).await } else if name == "skill" { skill.run(input).await + } else if let Some(result) = database.run(name, input.clone(), question.is_some()).await { + result } else if name.starts_with("subagent_") { let Some(subagents) = subagents else { return ToolRunResult::err(format!("unknown tool: {name}"), Vec::new()); diff --git a/crates/sinew-app/src/agent/tool_summary.rs b/crates/claakecode-app/src/agent/tool_summary.rs similarity index 100% rename from crates/sinew-app/src/agent/tool_summary.rs rename to crates/claakecode-app/src/agent/tool_summary.rs diff --git a/crates/sinew-app/src/agent/turn.rs b/crates/claakecode-app/src/agent/turn.rs similarity index 99% rename from crates/sinew-app/src/agent/turn.rs rename to crates/claakecode-app/src/agent/turn.rs index 810ce1e9..0fbfaf3c 100644 --- a/crates/sinew-app/src/agent/turn.rs +++ b/crates/claakecode-app/src/agent/turn.rs @@ -9,7 +9,7 @@ use serde_json::{json, Map, Value}; use tokio::{sync::mpsc, task::JoinHandle}; use uuid::Uuid; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, Part, PartKind, ProviderRequest, Role, StopReason, StreamEvent, ToolResultImage, }; @@ -64,6 +64,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput { web_search, web_fetch, skill, + database, mcp, subagents, teams, @@ -122,6 +123,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput { if let Some(descriptor) = skill.descriptor() { tool_descriptors.push(descriptor); } + tool_descriptors.extend(database.descriptors()); if mode != AgentMode::Plan { tool_descriptors.insert(4, edit_file.descriptor()); tool_descriptors.insert(5, write_file.descriptor()); @@ -579,6 +581,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput { &web_search, &web_fetch, &skill, + &database, &mcp, subagents.as_deref(), teams.as_deref(), @@ -623,6 +626,7 @@ pub async fn run_turn(ctx: TurnContext) -> TurnOutput { &web_search, &web_fetch, &skill, + &database, &mcp, subagents.as_deref(), teams.as_deref(), diff --git a/crates/sinew-app/src/bash.rs b/crates/claakecode-app/src/bash.rs similarity index 98% rename from crates/sinew-app/src/bash.rs rename to crates/claakecode-app/src/bash.rs index 2e1b90ff..ccdff445 100644 --- a/crates/sinew-app/src/bash.rs +++ b/crates/claakecode-app/src/bash.rs @@ -24,7 +24,7 @@ use portable_pty::ChildKiller; use portable_pty::{native_pty_system, ChildKiller, CommandBuilder, PtySize}; use serde::Deserialize; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use tokio::{ sync::{mpsc, watch, Mutex}, time::{sleep, timeout, Instant}, @@ -488,18 +488,18 @@ fn shell_command_builder(command: &str) -> CommandBuilder { #[cfg(windows)] fn powershell_script(command: &str) -> String { format!( - r#"$__sinewUtf8 = [System.Text.UTF8Encoding]::new($false) -[Console]::InputEncoding = $__sinewUtf8 -[Console]::OutputEncoding = $__sinewUtf8 -$OutputEncoding = $__sinewUtf8 + r#"$__claakecodeUtf8 = [System.Text.UTF8Encoding]::new($false) +[Console]::InputEncoding = $__claakecodeUtf8 +[Console]::OutputEncoding = $__claakecodeUtf8 +$OutputEncoding = $__claakecodeUtf8 $ProgressPreference = 'SilentlyContinue' $ErrorActionPreference = 'Continue' & {{ {command} }} -$__sinewSuccess = $? -$__sinewExitCode = if ($global:LASTEXITCODE -is [int]) {{ $global:LASTEXITCODE }} elseif ($__sinewSuccess) {{ 0 }} else {{ 1 }} -exit $__sinewExitCode +$__claakecodeSuccess = $? +$__claakecodeExitCode = if ($global:LASTEXITCODE -is [int]) {{ $global:LASTEXITCODE }} elseif ($__claakecodeSuccess) {{ 0 }} else {{ 1 }} +exit $__claakecodeExitCode "# ) } @@ -865,7 +865,7 @@ mod tests { fn temp_workspace(name: &str) -> PathBuf { let root = - std::env::temp_dir().join(format!("sinew-bash-test-{name}-{}", std::process::id())); + std::env::temp_dir().join(format!("claakecode-bash-test-{name}-{}", std::process::id())); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).expect("create temp workspace"); root diff --git a/crates/sinew-app/src/compact.rs b/crates/claakecode-app/src/compact.rs similarity index 99% rename from crates/sinew-app/src/compact.rs rename to crates/claakecode-app/src/compact.rs index 843e8b70..ec32c747 100644 --- a/crates/sinew-app/src/compact.rs +++ b/crates/claakecode-app/src/compact.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::{anyhow, bail, Result}; use futures_util::StreamExt; use serde_json::{json, Value}; -use sinew_core::{ +use claakecode_core::{ ChatMessage, ModelRef, Part, Provider, ProviderRequest, Role, ServiceTier, StreamEvent, }; use tokio::sync::mpsc; diff --git a/crates/claakecode-app/src/database.rs b/crates/claakecode-app/src/database.rs new file mode 100644 index 00000000..099f3a59 --- /dev/null +++ b/crates/claakecode-app/src/database.rs @@ -0,0 +1,1114 @@ +use std::{collections::HashSet, path::Path, time::{Duration, Instant, SystemTime, UNIX_EPOCH}}; + +use anyhow::{anyhow, bail, Context, Result}; +use reqwest::{header::ACCEPT, Url}; +use rusqlite::{Connection, OpenFlags}; +use serde::{Deserialize, Serialize}; +use sqlx::{ + mysql::MySqlPoolOptions, + postgres::{PgPoolOptions, PgSslMode}, + ConnectOptions, Row, +}; +use uuid::Uuid; + +const DEFAULT_ROW_LIMIT: u32 = 100; +const MAX_ROW_LIMIT: u32 = 10_000; +const DEFAULT_TIMEOUT_MS: u64 = 30_000; +const MIN_TIMEOUT_MS: u64 = 1_000; +const MAX_TIMEOUT_MS: u64 = 300_000; +const STATUS_MESSAGE_LIMIT: usize = 2_000; +const ACTIVITY_PREVIEW_LIMIT: usize = 500; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseSourceEngine { + #[serde(rename = "postgres")] + Postgres, + #[serde(rename = "mysql")] + Mysql, + #[serde(rename = "sqlite")] + Sqlite, + #[serde(rename = "mssql")] + Mssql, + #[serde(rename = "supabaseRest")] + SupabaseRest, +} + +impl Default for DatabaseSourceEngine { + fn default() -> Self { + Self::Postgres + } +} + +impl DatabaseSourceEngine { + pub fn label(self) -> &'static str { + match self { + Self::Postgres => "PostgreSQL", + Self::Mysql => "MySQL / MariaDB", + Self::Sqlite => "SQLite", + Self::Mssql => "Microsoft SQL Server", + Self::SupabaseRest => "Supabase REST", + } + } + + pub fn agent_id(self) -> &'static str { + match self { + Self::Postgres => "postgresql", + Self::Mysql => "mysql", + Self::Sqlite => "sqlite", + Self::Mssql => "mssql", + Self::SupabaseRest => "supabase_rest", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseCredentialMode { + #[serde(rename = "connectionString")] + ConnectionString, + #[serde(rename = "fields")] + Fields, +} + +impl Default for DatabaseCredentialMode { + fn default() -> Self { + Self::ConnectionString + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseSslMode { + #[serde(rename = "disabled")] + Disabled, + #[serde(rename = "required")] + Required, + #[serde(rename = "strict")] + Strict, +} + +impl Default for DatabaseSslMode { + fn default() -> Self { + Self::Required + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseConnectionState { + #[serde(rename = "untested")] + Untested, + #[serde(rename = "ok")] + Ok, + #[serde(rename = "error")] + Error, +} + +impl Default for DatabaseConnectionState { + fn default() -> Self { + Self::Untested + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseConnectionStatus { + #[serde(default)] + pub status: DatabaseConnectionState, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timestamp_ms: Option, +} + +impl Default for DatabaseConnectionStatus { + fn default() -> Self { + Self { + status: DatabaseConnectionState::Untested, + message: None, + timestamp_ms: None, + } + } +} + +impl DatabaseConnectionStatus { + pub fn from_test(result: &DatabaseConnectionTestResult) -> Self { + Self { + status: match result.status { + DatabaseConnectionTestState::Ok => DatabaseConnectionState::Ok, + DatabaseConnectionTestState::Error => DatabaseConnectionState::Error, + }, + message: Some(result.message.clone()), + timestamp_ms: Some(result.timestamp_ms), + } + } + + fn normalized(mut self) -> Self { + self.message = self + .message + .map(|message| clip_chars(message.trim(), STATUS_MESSAGE_LIMIT)) + .filter(|message| !message.is_empty()); + if self.status == DatabaseConnectionState::Untested { + self.timestamp_ms = None; + self.message = None; + } + self + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseConnectionFields { + #[serde(default)] + pub host: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub port: Option, + #[serde(default)] + pub user: String, + #[serde(default)] + pub password: String, + #[serde(default)] + pub database: String, + #[serde(default)] + pub ssl_mode: DatabaseSslMode, + #[serde(default)] + pub ssl_certificate: String, +} + +impl DatabaseConnectionFields { + fn normalized(mut self) -> Self { + self.host = self.host.trim().to_string(); + self.user = self.user.trim().to_string(); + self.database = self.database.trim().to_string(); + self.ssl_certificate = self.ssl_certificate.trim().to_string(); + self + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseSqliteConfig { + #[serde(default)] + pub file_path: String, + #[serde(default)] + pub create_if_missing: bool, +} + +impl Default for DatabaseSqliteConfig { + fn default() -> Self { + Self { + file_path: String::new(), + create_if_missing: true, + } + } +} + +impl DatabaseSqliteConfig { + fn normalized(mut self) -> Self { + self.file_path = self.file_path.trim().to_string(); + self + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseSupabaseRestConfig { + #[serde(default)] + pub project_url: String, + #[serde(default)] + pub anon_key: String, + #[serde(default)] + pub service_role_key: String, + #[serde(default)] + pub use_service_role: bool, + #[serde(default)] + pub allow_rpc: bool, +} + +impl DatabaseSupabaseRestConfig { + fn normalized(mut self) -> Self { + self.project_url = self.project_url.trim().trim_end_matches('/').to_string(); + self.anon_key = self.anon_key.trim().to_string(); + self.service_role_key = self.service_role_key.trim().to_string(); + self + } + + pub(crate) fn selected_key(&self) -> Option<&str> { + if self.use_service_role && !self.service_role_key.trim().is_empty() { + Some(self.service_role_key.trim()) + } else if !self.anon_key.trim().is_empty() { + Some(self.anon_key.trim()) + } else { + None + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseSourceConfig { + #[serde(default)] + pub id: String, + #[serde(default)] + pub name: String, + #[serde(default)] + pub engine: DatabaseSourceEngine, + #[serde(default = "default_true")] + pub enabled: bool, + #[serde(default)] + pub note: String, + #[serde(default)] + pub default_schema: String, + #[serde(default = "default_row_limit")] + pub default_row_limit: u32, + #[serde(default = "default_timeout_ms")] + pub default_timeout_ms: u64, + #[serde(default)] + pub read_only: bool, + #[serde(default = "default_true")] + pub require_confirmation_for_destructive: bool, + #[serde(default)] + pub last_connection_status: DatabaseConnectionStatus, + #[serde(default)] + pub credential_mode: DatabaseCredentialMode, + #[serde(default)] + pub connection_string: String, + #[serde(default)] + pub fields: DatabaseConnectionFields, + #[serde(default)] + pub sqlite: DatabaseSqliteConfig, + #[serde(default)] + pub supabase_rest: DatabaseSupabaseRestConfig, +} + +impl Default for DatabaseSourceConfig { + fn default() -> Self { + Self { + id: String::new(), + name: String::new(), + engine: DatabaseSourceEngine::default(), + enabled: true, + note: String::new(), + default_schema: String::new(), + default_row_limit: DEFAULT_ROW_LIMIT, + default_timeout_ms: DEFAULT_TIMEOUT_MS, + read_only: false, + require_confirmation_for_destructive: true, + last_connection_status: DatabaseConnectionStatus::default(), + credential_mode: DatabaseCredentialMode::default(), + connection_string: String::new(), + fields: DatabaseConnectionFields::default(), + sqlite: DatabaseSqliteConfig::default(), + supabase_rest: DatabaseSupabaseRestConfig::default(), + } + } +} + +impl DatabaseSourceConfig { + pub fn normalized(mut self) -> Self { + self.id = self.id.trim().to_string(); + self.name = self.name.trim().to_string(); + self.note = self.note.trim().to_string(); + self.default_schema = self.default_schema.trim().to_string(); + self.default_row_limit = self.default_row_limit.clamp(1, MAX_ROW_LIMIT); + self.default_timeout_ms = self.default_timeout_ms.clamp(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS); + self.last_connection_status = self.last_connection_status.normalized(); + self.connection_string = self.connection_string.trim().to_string(); + self.fields = self.fields.normalized(); + self.sqlite = self.sqlite.normalized(); + self.supabase_rest = self.supabase_rest.normalized(); + self + } + + pub fn agent_summary(&self) -> DatabaseSourceSummary { + DatabaseSourceSummary { + name: self.name.clone(), + engine: self.engine.agent_id().to_string(), + default_schema: self.default_schema.clone(), + default_row_limit: self.default_row_limit, + default_timeout_ms: self.default_timeout_ms, + read_only: self.read_only, + require_confirmation_for_destructive: self.require_confirmation_for_destructive, + last_connection_status: self.last_connection_status.clone(), + } + } + + pub fn redacted_error_message(&self, message: impl Into) -> String { + let mut redacted = message.into(); + for secret in self.secret_values() { + if !secret.is_empty() { + redacted = redacted.replace(secret, "[redacted]"); + } + } + clip_chars(redacted.trim(), STATUS_MESSAGE_LIMIT) + } + + fn secret_values(&self) -> Vec<&str> { + let mut values = Vec::new(); + if !self.connection_string.trim().is_empty() { + values.push(self.connection_string.trim()); + } + if !self.fields.password.is_empty() { + values.push(self.fields.password.as_str()); + } + if !self.supabase_rest.anon_key.trim().is_empty() { + values.push(self.supabase_rest.anon_key.trim()); + } + if !self.supabase_rest.service_role_key.trim().is_empty() { + values.push(self.supabase_rest.service_role_key.trim()); + } + values + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseSettings { + #[serde(default)] + pub sources: Vec, +} + +impl DatabaseSettings { + pub fn normalized(mut self) -> Self { + let mut seen_ids = HashSet::new(); + self.sources = self + .sources + .into_iter() + .map(DatabaseSourceConfig::normalized) + .map(|mut source| { + if source.id.is_empty() || !seen_ids.insert(source.id.clone()) { + loop { + let id = Uuid::new_v4().to_string(); + if seen_ids.insert(id.clone()) { + source.id = id; + break; + } + } + } + source + }) + .collect(); + self + } + + pub fn normalized_for_save(self) -> Result { + let normalized = self.normalized(); + normalized.validate_for_save()?; + Ok(normalized) + } + + pub fn validate_for_save(&self) -> Result<()> { + let mut names = HashSet::new(); + for source in &self.sources { + if source.name.trim().is_empty() { + bail!("database source name is required"); + } + let key = source.name.trim().to_ascii_lowercase(); + if !names.insert(key) { + bail!("database source names must be unique"); + } + source.validate_shape()?; + } + Ok(()) + } + + pub fn active_count(&self) -> usize { + self.sources.iter().filter(|source| source.enabled).count() + } + + pub fn enabled_summaries(&self) -> Vec { + self.sources + .iter() + .filter(|source| source.enabled) + .map(DatabaseSourceConfig::agent_summary) + .collect() + } + + pub fn find_enabled_source(&self, name: &str) -> Option { + let requested = name.trim(); + self.sources + .iter() + .find(|source| source.enabled && source.name == requested) + .cloned() + } +} + +impl DatabaseSourceConfig { + fn validate_shape(&self) -> Result<()> { + match self.engine { + DatabaseSourceEngine::Sqlite => { + if self.sqlite.file_path.trim().is_empty() { + bail!("SQLite file path is required"); + } + } + DatabaseSourceEngine::SupabaseRest => { + if self.supabase_rest.project_url.trim().is_empty() { + bail!("Supabase project URL is required"); + } + Url::parse(self.supabase_rest.project_url.trim()) + .map_err(|_| anyhow!("Supabase project URL is invalid"))?; + if self.supabase_rest.anon_key.trim().is_empty() + && self.supabase_rest.service_role_key.trim().is_empty() + { + bail!("Supabase anon key or service role key is required"); + } + } + DatabaseSourceEngine::Postgres + | DatabaseSourceEngine::Mysql + | DatabaseSourceEngine::Mssql => match self.credential_mode { + DatabaseCredentialMode::ConnectionString => { + if self.connection_string.trim().is_empty() { + bail!("connection string is required"); + } + } + DatabaseCredentialMode::Fields => { + if self.fields.host.trim().is_empty() { + bail!("host is required"); + } + if self.fields.user.trim().is_empty() { + bail!("user is required"); + } + if self.fields.database.trim().is_empty() { + bail!("database name is required"); + } + } + }, + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseSourceSummary { + pub name: String, + pub engine: String, + pub default_schema: String, + pub default_row_limit: u32, + pub default_timeout_ms: u64, + pub read_only: bool, + pub require_confirmation_for_destructive: bool, + pub last_connection_status: DatabaseConnectionStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseConnectionTestState { + #[serde(rename = "ok")] + Ok, + #[serde(rename = "error")] + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseConnectionTestResult { + pub ok: bool, + pub source_id: String, + pub source_name: String, + pub engine: DatabaseSourceEngine, + pub status: DatabaseConnectionTestState, + pub message: String, + pub timestamp_ms: i64, + pub duration_ms: u64, +} + +pub async fn test_database_source_connection(source: DatabaseSourceConfig) -> DatabaseConnectionTestResult { + let source = source.normalized(); + let started = Instant::now(); + let timestamp_ms = now_ms(); + let result = match source.engine { + DatabaseSourceEngine::Sqlite => test_sqlite_connection(&source), + DatabaseSourceEngine::SupabaseRest => test_supabase_rest_connection(&source).await, + DatabaseSourceEngine::Postgres => match validate_network_sql_source(&source) { + Ok(()) => test_postgres_connection(&source).await, + Err(err) => Err(err), + }, + DatabaseSourceEngine::Mysql => match validate_network_sql_source(&source) { + Ok(()) => test_mysql_connection(&source).await, + Err(err) => Err(err), + }, + DatabaseSourceEngine::Mssql => validate_network_sql_source(&source).and_then(|()| { + bail!( + "Microsoft SQL Server driver is not bundled in this build. Use a Postgres/MySQL/SQLite source, or contact support to enable MSSQL." + ) + }), + }; + + let duration_ms = started.elapsed().as_millis() as u64; + match result { + Ok(message) => DatabaseConnectionTestResult { + ok: true, + source_id: source.id, + source_name: source.name, + engine: source.engine, + status: DatabaseConnectionTestState::Ok, + message: clip_chars(message.trim(), STATUS_MESSAGE_LIMIT), + timestamp_ms, + duration_ms, + }, + Err(err) => DatabaseConnectionTestResult { + ok: false, + source_id: source.id.clone(), + source_name: source.name.clone(), + engine: source.engine, + status: DatabaseConnectionTestState::Error, + message: source.redacted_error_message(err.to_string()), + timestamp_ms, + duration_ms, + }, + } +} + +fn test_sqlite_connection(source: &DatabaseSourceConfig) -> Result { + let path = source.sqlite.file_path.trim(); + if path.is_empty() { + bail!("SQLite file path is required"); + } + let flags = if source.read_only { + OpenFlags::SQLITE_OPEN_READ_ONLY + } else if source.sqlite.create_if_missing { + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } else { + OpenFlags::SQLITE_OPEN_READ_WRITE + }; + let conn = Connection::open_with_flags(Path::new(path), flags) + .with_context(|| "unable to open SQLite database")?; + conn.query_row("select 1", [], |_row| Ok(())) + .context("SQLite validation query failed")?; + Ok("Connection successful".to_string()) +} + +async fn test_supabase_rest_connection(source: &DatabaseSourceConfig) -> Result { + let project_url = source.supabase_rest.project_url.trim(); + if project_url.is_empty() { + bail!("Supabase project URL is required"); + } + let api_key = source + .supabase_rest + .selected_key() + .ok_or_else(|| anyhow!("Supabase anon key or service role key is required"))?; + let base = Url::parse(project_url).context("Supabase project URL is invalid")?; + let url = base + .join("rest/v1/") + .context("unable to build Supabase REST endpoint")?; + let timeout = Duration::from_millis(source.default_timeout_ms); + let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .context("unable to build Supabase REST client")?; + let response = client + .get(url) + .header("apikey", api_key) + .bearer_auth(api_key) + .header(ACCEPT, "application/json, application/openapi+json") + .send() + .await + .context("Supabase REST request failed")?; + let status = response.status(); + if !status.is_success() { + let body = response.text().await.unwrap_or_default(); + bail!( + "Supabase REST request failed ({status}): {}", + clip_chars(body.trim(), 1_000) + ); + } + Ok("Connection successful".to_string()) +} + +fn validate_network_sql_source(source: &DatabaseSourceConfig) -> Result<()> { + match source.credential_mode { + DatabaseCredentialMode::ConnectionString => { + if source.connection_string.trim().is_empty() { + bail!("connection string is required"); + } + } + DatabaseCredentialMode::Fields => { + if source.fields.host.trim().is_empty() { + bail!("host is required"); + } + if source.fields.user.trim().is_empty() { + bail!("user is required"); + } + if source.fields.database.trim().is_empty() { + bail!("database name is required"); + } + } + } + Ok(()) +} + +/// Build the effective connection URL for a network SQL source. Used by the +/// SQL drivers (Postgres/MySQL) when the user picked the "separate fields" +/// mode. +pub(crate) fn build_sql_connection_url( + source: &DatabaseSourceConfig, + default_port: u16, + scheme: &str, +) -> Result { + match source.credential_mode { + DatabaseCredentialMode::ConnectionString => { + let trimmed = source.connection_string.trim(); + if trimmed.is_empty() { + bail!("connection string is required"); + } + Ok(trimmed.to_string()) + } + DatabaseCredentialMode::Fields => { + let fields = &source.fields; + let host = fields.host.trim(); + if host.is_empty() { + bail!("host is required"); + } + let user = fields.user.trim(); + if user.is_empty() { + bail!("user is required"); + } + let database = fields.database.trim(); + if database.is_empty() { + bail!("database name is required"); + } + let port = fields.port.unwrap_or(default_port); + let encoded_user = percent_encode_user(user); + let auth = if fields.password.is_empty() { + encoded_user + } else { + let pw = percent_encode_password(&fields.password); + format!("{encoded_user}:{pw}") + }; + Ok(format!( + "{scheme}://{auth}@{host}:{port}/{}", + percent_encode_path(database) + )) + } + } +} + +fn percent_encode_user(value: &str) -> String { + percent_encode_component(value, b":/@?#") +} + +fn percent_encode_password(value: &str) -> String { + percent_encode_component(value, b":/@?#") +} + +fn percent_encode_path(value: &str) -> String { + percent_encode_component(value, b"?#") +} + +fn percent_encode_component(value: &str, reserved: &[u8]) -> String { + let mut out = String::with_capacity(value.len()); + for byte in value.bytes() { + let needs_encoding = !matches!( + byte, + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' + ) || reserved.contains(&byte); + if needs_encoding { + out.push('%'); + out.push_str(&format!("{byte:02X}")); + } else { + out.push(byte as char); + } + } + out +} + +async fn test_postgres_connection(source: &DatabaseSourceConfig) -> Result { + let url = build_sql_connection_url(source, 5432, "postgresql")?; + let timeout = Duration::from_millis(source.default_timeout_ms.max(1_000)); + let mut opts: sqlx::postgres::PgConnectOptions = url + .parse() + .context("invalid Postgres connection string")?; + opts = opts.application_name("claakecode"); + opts = match source.fields.ssl_mode { + DatabaseSslMode::Disabled => opts.ssl_mode(PgSslMode::Disable), + DatabaseSslMode::Required => opts.ssl_mode(PgSslMode::Require), + DatabaseSslMode::Strict => opts.ssl_mode(PgSslMode::VerifyFull), + }; + opts = opts.log_statements(tracing::log::LevelFilter::Off); + let pool = PgPoolOptions::new() + .max_connections(1) + .acquire_timeout(timeout) + .connect_with(opts) + .await + .context("unable to connect to Postgres")?; + let row = sqlx::query("select version()") + .fetch_one(&pool) + .await + .context("Postgres validation query failed")?; + let version: String = row.try_get(0).unwrap_or_default(); + pool.close().await; + Ok(format!( + "Connection successful — {}", + clip_chars(version.trim(), 160) + )) +} + +async fn test_mysql_connection(source: &DatabaseSourceConfig) -> Result { + let url = build_sql_connection_url(source, 3306, "mysql")?; + let timeout = Duration::from_millis(source.default_timeout_ms.max(1_000)); + let mut opts: sqlx::mysql::MySqlConnectOptions = url + .parse() + .context("invalid MySQL connection string")?; + opts = match source.fields.ssl_mode { + DatabaseSslMode::Disabled => opts.ssl_mode(sqlx::mysql::MySqlSslMode::Disabled), + DatabaseSslMode::Required => opts.ssl_mode(sqlx::mysql::MySqlSslMode::Required), + DatabaseSslMode::Strict => opts.ssl_mode(sqlx::mysql::MySqlSslMode::VerifyIdentity), + }; + opts = opts.log_statements(tracing::log::LevelFilter::Off); + let pool = MySqlPoolOptions::new() + .max_connections(1) + .acquire_timeout(timeout) + .connect_with(opts) + .await + .context("unable to connect to MySQL")?; + let row = sqlx::query("select version()") + .fetch_one(&pool) + .await + .context("MySQL validation query failed")?; + let version: String = row.try_get(0).unwrap_or_default(); + pool.close().await; + Ok(format!( + "Connection successful — {}", + clip_chars(version.trim(), 160) + )) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseActivityOperation { + #[serde(rename = "read")] + Read, + #[serde(rename = "write")] + Write, + #[serde(rename = "ddl")] + Ddl, + #[serde(rename = "destructive")] + Destructive, + #[serde(rename = "unknown")] + Unknown, +} + +impl Default for DatabaseActivityOperation { + fn default() -> Self { + Self::Unknown + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DatabaseActivityStatus { + #[serde(rename = "success")] + Ok, + #[serde(rename = "error")] + Error, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DatabaseActivityEntry { + #[serde(default)] + pub id: String, + pub source_id: String, + pub source_name: String, + pub engine: DatabaseSourceEngine, + #[serde(default)] + pub operation: DatabaseActivityOperation, + #[serde(default)] + pub query_preview: String, + pub status: DatabaseActivityStatus, + pub timestamp_ms: i64, + pub duration_ms: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rows_returned: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub rows_affected: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl DatabaseActivityEntry { + pub fn from_query( + source: &DatabaseSourceConfig, + query: &str, + status: DatabaseActivityStatus, + timestamp_ms: i64, + duration_ms: u64, + rows_returned: Option, + rows_affected: Option, + error: Option, + ) -> Self { + Self { + id: Uuid::new_v4().to_string(), + source_id: source.id.clone(), + source_name: source.name.clone(), + engine: source.engine, + operation: classify_sql_operation(query), + query_preview: sanitize_query_preview(query), + status, + timestamp_ms, + duration_ms, + rows_returned, + rows_affected, + error: error.map(|value| source.redacted_error_message(value)), + } + .normalized() + } + + pub fn normalized(mut self) -> Self { + self.id = self.id.trim().to_string(); + if self.id.is_empty() { + self.id = Uuid::new_v4().to_string(); + } + self.source_id = self.source_id.trim().to_string(); + self.source_name = self.source_name.trim().to_string(); + self.query_preview = clip_chars(self.query_preview.trim(), ACTIVITY_PREVIEW_LIMIT); + self.error = self + .error + .map(|value| clip_chars(value.trim(), STATUS_MESSAGE_LIMIT)) + .filter(|value| !value.is_empty()); + self + } +} + +pub fn sanitize_query_preview(query: &str) -> String { + clip_chars(collapse_whitespace(&mask_sql_literals(query)).trim(), ACTIVITY_PREVIEW_LIMIT) +} + +pub fn classify_sql_operation(query: &str) -> DatabaseActivityOperation { + let masked = mask_sql_literals(query).to_ascii_lowercase(); + let normalized = collapse_whitespace(&masked); + let first = normalized.split_whitespace().next().unwrap_or_default(); + match first { + "select" | "with" | "show" | "explain" | "describe" | "pragma" => { + DatabaseActivityOperation::Read + } + "insert" | "update" | "merge" | "replace" | "call" => DatabaseActivityOperation::Write, + "create" => DatabaseActivityOperation::Ddl, + "alter" | "drop" | "truncate" => DatabaseActivityOperation::Destructive, + "delete" => { + if normalized.contains(" where ") { + DatabaseActivityOperation::Write + } else { + DatabaseActivityOperation::Destructive + } + } + _ => DatabaseActivityOperation::Unknown, + } +} + +fn mask_sql_literals(input: &str) -> String { + let mut out = String::with_capacity(input.len().min(ACTIVITY_PREVIEW_LIMIT)); + let mut chars = input.chars().peekable(); + while let Some(ch) = chars.next() { + match ch { + '\'' | '"' | '`' => { + out.push('?'); + let quote = ch; + while let Some(next) = chars.next() { + if next == quote { + if quote == '\'' && chars.peek() == Some(&'\'') { + let _ = chars.next(); + continue; + } + break; + } + if next == '\\' { + let _ = chars.next(); + } + } + } + '-' if chars.peek() == Some(&'-') => { + let _ = chars.next(); + out.push(' '); + for next in chars.by_ref() { + if next == '\n' { + out.push(' '); + break; + } + } + } + '/' if chars.peek() == Some(&'*') => { + let _ = chars.next(); + out.push(' '); + let mut previous = '\0'; + for next in chars.by_ref() { + if previous == '*' && next == '/' { + break; + } + previous = next; + } + } + '$' => { + let mut tag = String::new(); + while let Some(next) = chars.peek().copied() { + if next == '$' { + let _ = chars.next(); + break; + } + if next.is_ascii_alphanumeric() || next == '_' { + tag.push(next); + let _ = chars.next(); + } else { + out.push('$'); + out.push_str(&tag); + tag.clear(); + break; + } + } + if tag.is_empty() { + continue; + } + out.push('?'); + let end = format!("${tag}$"); + let mut window = String::new(); + for next in chars.by_ref() { + window.push(next); + if window.ends_with(&end) { + break; + } + if window.len() > end.len() { + window.remove(0); + } + } + } + _ => out.push(ch), + } + } + out +} + +fn collapse_whitespace(input: &str) -> String { + let mut out = String::new(); + let mut last_was_space = true; + for ch in input.chars() { + if ch.is_whitespace() { + if !last_was_space { + out.push(' '); + } + last_was_space = true; + } else { + out.push(ch); + last_was_space = false; + } + } + out.trim().to_string() +} + +fn clip_chars(input: &str, limit: usize) -> String { + if input.chars().count() <= limit { + return input.to_string(); + } + let mut clipped = input.chars().take(limit.saturating_sub(1)).collect::(); + clipped.push('…'); + clipped +} + +fn default_true() -> bool { + true +} + +fn default_row_limit() -> u32 { + DEFAULT_ROW_LIMIT +} + +fn default_timeout_ms() -> u64 { + DEFAULT_TIMEOUT_MS +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis() as i64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn query_preview_masks_literals_and_comments() { + let preview = sanitize_query_preview( + "select * from users where email = 'a@example.com' -- api key: secret\n and token = \"abc\"", + ); + assert_eq!(preview, "select * from users where email = ? and token = ?"); + } + + #[test] + fn destructive_sql_is_classified_without_string_literals() { + assert_eq!( + classify_sql_operation("select 'drop table users'"), + DatabaseActivityOperation::Read + ); + assert_eq!( + classify_sql_operation("delete from users"), + DatabaseActivityOperation::Destructive + ); + assert_eq!( + classify_sql_operation("delete from users where id = 1"), + DatabaseActivityOperation::Write + ); + assert_eq!( + classify_sql_operation("alter table users add column note text"), + DatabaseActivityOperation::Destructive + ); + } + + #[test] + fn enabled_summaries_do_not_include_secrets() { + let settings = DatabaseSettings { + sources: vec![DatabaseSourceConfig { + id: "one".into(), + name: "prod".into(), + connection_string: "postgres://user:secret@example/db".into(), + enabled: true, + ..DatabaseSourceConfig::default() + }], + } + .normalized_for_save() + .expect("valid settings"); + + let json = serde_json::to_string(&settings.enabled_summaries()).unwrap(); + assert!(!json.contains("secret")); + assert!(!json.contains("postgres://")); + assert!(json.contains("prod")); + } + + #[test] + fn build_sql_connection_url_from_separate_fields_percent_encodes_password() { + let mut source = DatabaseSourceConfig::default(); + source.engine = DatabaseSourceEngine::Postgres; + source.credential_mode = DatabaseCredentialMode::Fields; + source.fields = DatabaseConnectionFields { + host: "db.example.com".into(), + port: Some(6432), + user: "claake".into(), + password: "p@ss/word:!".into(), + database: "claakecode".into(), + ssl_mode: DatabaseSslMode::Required, + ssl_certificate: String::new(), + }; + let url = build_sql_connection_url(&source, 5432, "postgresql").unwrap(); + assert!(url.starts_with("postgresql://claake:")); + assert!(url.contains("@db.example.com:6432/claakecode")); + assert!(!url.contains("p@ss/word:!")); + assert!(url.contains("p%40ss%2Fword%3A%21")); + } + + #[test] + fn build_sql_connection_url_uses_connection_string_when_provided() { + let mut source = DatabaseSourceConfig::default(); + source.engine = DatabaseSourceEngine::Postgres; + source.credential_mode = DatabaseCredentialMode::ConnectionString; + source.connection_string = "postgres://u:p@h/db".into(); + let url = build_sql_connection_url(&source, 5432, "postgresql").unwrap(); + assert_eq!(url, "postgres://u:p@h/db"); + } + + #[test] + fn redacted_error_message_removes_known_secrets() { + let mut source = DatabaseSourceConfig::default(); + source.engine = DatabaseSourceEngine::Postgres; + source.credential_mode = DatabaseCredentialMode::Fields; + source.fields.password = "super-secret-password".into(); + let redacted = source.redacted_error_message( + "error connecting using super-secret-password to db".to_string(), + ); + assert!(!redacted.contains("super-secret-password")); + } +} diff --git a/crates/claakecode-app/src/database_tool.rs b/crates/claakecode-app/src/database_tool.rs new file mode 100644 index 00000000..cd3abf2d --- /dev/null +++ b/crates/claakecode-app/src/database_tool.rs @@ -0,0 +1,1380 @@ +use std::{ + collections::BTreeMap, + path::Path, + time::{Duration, Instant}, +}; + +use anyhow::{anyhow, bail, Context, Result}; +use claakecode_core::ToolDescriptor; +use reqwest::{header::ACCEPT, Method, Url}; +use rusqlite::{ + hooks::{AuthAction, AuthContext, Authorization}, + types::ValueRef, + Connection, OpenFlags, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use crate::{ + database::{ + classify_sql_operation, sanitize_query_preview, DatabaseActivityEntry, + DatabaseActivityOperation, DatabaseActivityStatus, DatabaseSourceConfig, + DatabaseSourceEngine, + }, + store::AppStore, + tool_run::ToolRunResult, +}; + +const MAX_ROW_LIMIT: u32 = 10_000; +const MIN_TIMEOUT_MS: u64 = 1_000; +const MAX_TIMEOUT_MS: u64 = 300_000; +const SCHEMA_OBJECT_LIMIT: usize = 200; + +pub const DATABASE_LIST_SOURCES_TOOL: &str = "database_list_sources"; +pub const DATABASE_DESCRIBE_SCHEMA_TOOL: &str = "database_describe_schema"; +pub const DATABASE_EXECUTE_QUERY_TOOL: &str = "database_execute_query"; + +#[derive(Clone)] +pub struct DatabaseTool { + store: AppStore, +} + +impl DatabaseTool { + pub fn new(store: AppStore) -> Self { + Self { store } + } + + pub fn descriptors_static() -> Vec { + vec![ + list_sources_descriptor(), + describe_schema_descriptor(), + execute_query_descriptor(), + ] + } + + pub fn descriptors(&self) -> Vec { + Self::descriptors_static() + } + + pub async fn run( + &self, + name: &str, + input: Value, + question_available: bool, + ) -> Option { + match name { + DATABASE_LIST_SOURCES_TOOL => Some(self.run_list_sources(input).await), + DATABASE_DESCRIBE_SCHEMA_TOOL => Some(self.run_describe_schema(input).await), + DATABASE_EXECUTE_QUERY_TOOL => { + Some(self.run_execute_query(input, question_available).await) + } + _ => None, + } + } + + async fn run_list_sources(&self, input: Value) -> ToolRunResult { + let parsed: ListDatabaseSourcesInput = match serde_json::from_value(input) { + Ok(value) => value, + Err(err) => { + return ToolRunResult::err( + format!("invalid database_list_sources input: {err}"), + Vec::new(), + ) + } + }; + let settings = match self.store.load_database_settings() { + Ok(settings) => settings, + Err(err) => { + return ToolRunResult::err( + format!("unable to load database sources: {err}"), + Vec::new(), + ) + } + }; + let mut sources = settings.enabled_summaries(); + if parsed.include_disabled { + sources = settings + .sources + .iter() + .map(DatabaseSourceConfig::agent_summary) + .collect(); + } + let disabled_count = settings.sources.iter().filter(|source| !source.enabled).count(); + let content = json!({ + "sources": sources, + "activeCount": settings.active_count(), + "sourceCount": settings.sources.len(), + "disabledCount": disabled_count, + }); + ToolRunResult::ok(pretty_json(&content), Vec::new()) + } + + async fn run_describe_schema(&self, input: Value) -> ToolRunResult { + let parsed: DescribeDatabaseSchemaInput = match serde_json::from_value(input) { + Ok(value) => value, + Err(err) => { + return ToolRunResult::err( + format!("invalid database_describe_schema input: {err}"), + Vec::new(), + ) + } + }; + let source = match self.load_enabled_source(&parsed.source) { + Ok(source) => source, + Err(err) => return ToolRunResult::err(err, Vec::new()), + }; + let started = Instant::now(); + let result = match source.engine { + DatabaseSourceEngine::Sqlite => describe_sqlite_schema( + &source, + parsed.schema.as_deref(), + parsed.table.as_deref(), + ), + DatabaseSourceEngine::SupabaseRest => { + describe_supabase_rest_schema( + &source, + parsed.schema.as_deref(), + parsed.table.as_deref(), + ) + .await + } + DatabaseSourceEngine::Postgres => { + describe_postgres_schema( + &source, + parsed.schema.as_deref(), + parsed.table.as_deref(), + ) + .await + } + DatabaseSourceEngine::Mysql => { + describe_mysql_schema( + &source, + parsed.schema.as_deref(), + parsed.table.as_deref(), + ) + .await + } + DatabaseSourceEngine::Mssql => Err(anyhow!( + "Microsoft SQL Server schema introspection is not bundled in this build" + )), + }; + match result { + Ok(mut value) => { + insert_duration(&mut value, started.elapsed()); + ToolRunResult::ok(pretty_json(&value), Vec::new()) + } + Err(err) => ToolRunResult::err(source.redacted_error_message(err.to_string()), Vec::new()), + } + } + + async fn run_execute_query(&self, input: Value, question_available: bool) -> ToolRunResult { + let parsed: ExecuteDatabaseQueryInput = match serde_json::from_value(input) { + Ok(value) => value, + Err(err) => { + return ToolRunResult::err( + format!("invalid database_execute_query input: {err}"), + Vec::new(), + ) + } + }; + let query = parsed.query.trim(); + if query.is_empty() { + return ToolRunResult::err("query is required", Vec::new()); + } + let source = match self.load_enabled_source(&parsed.source) { + Ok(source) => source, + Err(err) => return ToolRunResult::err(err, Vec::new()), + }; + let operation = classify_sql_operation(query); + if source.read_only && !matches!(operation, DatabaseActivityOperation::Read) { + return ToolRunResult::err( + format!( + "database source `{}` is read-only; refusing {:?} operation", + source.name, operation + ), + Vec::new(), + ); + } + if source.require_confirmation_for_destructive + && is_confirmation_required(operation) + && !parsed.confirmed + { + return ToolRunResult::err( + confirmation_required_message(&source, query, operation, question_available), + Vec::new(), + ); + } + + let started = Instant::now(); + let timestamp_ms = now_ms(); + let result = match source.engine { + DatabaseSourceEngine::Sqlite => execute_sqlite_query(&source, query, parsed.row_limit), + DatabaseSourceEngine::SupabaseRest => execute_supabase_rest_request(&source, &parsed).await, + DatabaseSourceEngine::Postgres => { + execute_postgres_query(&source, query, parsed.row_limit).await + } + DatabaseSourceEngine::Mysql => { + execute_mysql_query(&source, query, parsed.row_limit).await + } + DatabaseSourceEngine::Mssql => Err(anyhow!( + "Microsoft SQL Server query execution is not bundled in this build" + )), + }; + let duration_ms = started.elapsed().as_millis() as u64; + match result { + Ok(output) => { + let activity = DatabaseActivityEntry::from_query( + &source, + query, + DatabaseActivityStatus::Ok, + timestamp_ms, + duration_ms, + output.rows_returned(), + output.rows_affected, + None, + ); + if let Err(err) = self.store.append_database_source_activity(&activity) { + return ToolRunResult::err( + format!("query succeeded but activity logging failed: {err}"), + Vec::new(), + ); + } + ToolRunResult::ok(pretty_json(&output.into_json(duration_ms)), Vec::new()) + } + Err(err) => { + let redacted = source.redacted_error_message(err.to_string()); + let activity = DatabaseActivityEntry::from_query( + &source, + query, + DatabaseActivityStatus::Error, + timestamp_ms, + duration_ms, + None, + None, + Some(redacted.clone()), + ); + let _ = self.store.append_database_source_activity(&activity); + ToolRunResult::err(redacted, Vec::new()) + } + } + } + + fn load_enabled_source( + &self, + source_name: &str, + ) -> std::result::Result { + let source_name = source_name.trim(); + if source_name.is_empty() { + return Err("source is required".to_string()); + } + let settings = self + .store + .load_database_settings() + .map_err(|err| format!("unable to load database sources: {err}"))?; + if let Some(source) = settings.find_enabled_source(source_name) { + return Ok(source); + } + if settings.sources.iter().any(|source| source.name == source_name) { + Err(format!("database source `{source_name}` is disabled")) + } else { + Err(format!("database source `{source_name}` does not exist")) + } + } +} + +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ListDatabaseSourcesInput { + #[serde(default)] + include_disabled: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DescribeDatabaseSchemaInput { + source: String, + #[serde(default)] + schema: Option, + #[serde(default)] + table: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ExecuteDatabaseQueryInput { + source: String, + query: String, + #[serde(default)] + row_limit: Option, + #[serde(default)] + confirmed: bool, + #[serde(default)] + supabase: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SupabaseRestExecutionInput { + #[serde(default)] + method: Option, + #[serde(default)] + path: Option, + #[serde(default)] + body: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct QueryExecutionOutput { + source: String, + engine: String, + operation: DatabaseActivityOperation, + columns: Vec, + rows: Vec>, + row_count: u64, + rows_affected: Option, + truncated: bool, +} + +impl QueryExecutionOutput { + fn rows_returned(&self) -> Option { + Some(self.row_count) + } + + fn into_json(self, duration_ms: u64) -> Value { + json!({ + "source": self.source, + "engine": self.engine, + "operation": self.operation, + "columns": self.columns, + "rows": self.rows, + "rowCount": self.row_count, + "rowsAffected": self.rows_affected, + "truncated": self.truncated, + "durationMs": duration_ms, + }) + } +} + +fn list_sources_descriptor() -> ToolDescriptor { + ToolDescriptor { + name: DATABASE_LIST_SOURCES_TOOL.into(), + description: "List configured database sources that are enabled for the agent. Returns source names, engine IDs, defaults, read-only/destructive-confirmation flags, and last connection status; never returns credentials.".into(), + input_schema: json!({ + "type": "object", + "properties": { + "includeDisabled": { + "type": "boolean", + "description": "Optional diagnostic flag. Defaults to false; disabled sources cannot be used by other database tools." + } + }, + "additionalProperties": false + }), + } +} + +fn describe_schema_descriptor() -> ToolDescriptor { + ToolDescriptor { + name: DATABASE_DESCRIBE_SCHEMA_TOOL.into(), + description: "Describe tables, views, columns, primary keys, foreign keys, and indexes for a configured database source. You may target a schema or table. Large schemas are truncated with a flag. SQLite is introspected locally and Supabase REST returns PostgREST/OpenAPI metadata when available; other direct SQL engines return a clear driver-unavailable error until their driver is installed.".into(), + input_schema: json!({ + "type": "object", + "properties": { + "source": { "type": "string", "description": "Database source name from database_list_sources." }, + "schema": { "type": "string", "description": "Optional schema name. Defaults to the source default schema when applicable." }, + "table": { "type": "string", "description": "Optional table or view name to narrow introspection." } + }, + "required": ["source"], + "additionalProperties": false + }), + } +} + +fn execute_query_descriptor() -> ToolDescriptor { + ToolDescriptor { + name: DATABASE_EXECUTE_QUERY_TOOL.into(), + description: "Execute a SQL query (or, for Supabase REST, a REST request) against a configured database source with row-limit, timeout, read-only, destructive-operation, and activity-log safeguards. Disabled or missing sources are refused. Destructive writes/DDL require explicit user confirmation unless disabled for that source; after the user confirms, call again with confirmed=true. Results never include credentials.".into(), + input_schema: json!({ + "type": "object", + "properties": { + "source": { "type": "string", "description": "Database source name from database_list_sources." }, + "query": { "type": "string", "description": "SQL text. For Supabase REST, use a concise operation description unless supabase.path is provided." }, + "rowLimit": { "type": "integer", "minimum": 1, "description": "Optional read row limit. The source default still caps this value." }, + "confirmed": { "type": "boolean", "description": "Set true only after explicit user confirmation for destructive operations." }, + "supabase": { + "type": "object", + "description": "Optional Supabase REST request details.", + "properties": { + "method": { "type": "string", "enum": ["GET", "POST", "PATCH", "DELETE", "PUT"] }, + "path": { "type": "string", "description": "REST path relative to /rest/v1/ or rpc/." }, + "body": { "description": "Optional JSON request body." } + }, + "additionalProperties": false + } + }, + "required": ["source", "query"], + "additionalProperties": false + }), + } +} + +fn describe_sqlite_schema( + source: &DatabaseSourceConfig, + _schema: Option<&str>, + table_filter: Option<&str>, +) -> Result { + let conn = open_sqlite_connection(source, true)?; + let mut statement = conn.prepare( + "select name, type, sql from sqlite_master where type in ('table','view') and name not like 'sqlite_%' order by type, name", + )?; + let mut objects = Vec::new(); + let mut rows = statement.query([])?; + let requested_table = table_filter.map(str::trim).filter(|value| !value.is_empty()); + let mut truncated = false; + while let Some(row) = rows.next()? { + let name: String = row.get(0)?; + if requested_table.is_some_and(|wanted| wanted != name) { + continue; + } + if objects.len() >= SCHEMA_OBJECT_LIMIT { + truncated = true; + break; + } + let kind: String = row.get(1)?; + let sql: Option = row.get(2)?; + objects.push(json!({ + "name": name, + "kind": kind, + "columns": sqlite_columns(&conn, &name)?, + "primaryKeys": sqlite_primary_keys(&conn, &name)?, + "foreignKeys": sqlite_foreign_keys(&conn, &name)?, + "indexes": sqlite_indexes(&conn, &name)?, + "definition": sql.unwrap_or_default(), + })); + } + Ok(json!({ + "source": source.name, + "engine": source.engine.agent_id(), + "schemas": [{ "name": "main", "objects": objects }], + "truncated": truncated, + })) +} + +async fn describe_supabase_rest_schema( + source: &DatabaseSourceConfig, + schema: Option<&str>, + table: Option<&str>, +) -> Result { + let api_key = source + .supabase_rest + .selected_key() + .ok_or_else(|| anyhow!("Supabase anon key or service role key is required"))?; + let base = Url::parse(source.supabase_rest.project_url.trim()) + .context("Supabase project URL is invalid")?; + let url = base + .join("rest/v1/") + .context("unable to build Supabase REST endpoint")?; + let client = reqwest::Client::builder() + .timeout(Duration::from_millis(source.default_timeout_ms)) + .build() + .context("unable to build Supabase REST client")?; + let response = client + .get(url) + .header("apikey", api_key) + .bearer_auth(api_key) + .header(ACCEPT, "application/openapi+json, application/json") + .send() + .await + .context("Supabase REST schema request failed")?; + let status = response.status(); + let text = response.text().await.unwrap_or_default(); + if !status.is_success() { + bail!( + "Supabase REST schema request failed ({status}): {}", + truncate_chars(text.trim(), 1_000) + ); + } + let parsed = serde_json::from_str::(&text) + .unwrap_or_else(|_| json!({ "raw": truncate_chars(text.trim(), 10_000) })); + Ok(json!({ + "source": source.name, + "engine": source.engine.agent_id(), + "schema": schema.unwrap_or_else(|| source.default_schema.as_str()), + "table": table, + "postgrest": parsed, + "truncated": false, + })) +} + +fn execute_sqlite_query( + source: &DatabaseSourceConfig, + query: &str, + requested_row_limit: Option, +) -> Result { + let conn = open_sqlite_connection(source, source.read_only)?; + install_sqlite_timeout(&conn, source.default_timeout_ms); + if source.read_only { + install_sqlite_read_only_authorizer(&conn); + } + let mut statement = conn.prepare(query).context("unable to prepare SQLite query")?; + let is_read = statement.readonly() && statement.column_count() > 0; + if source.read_only && !is_read { + bail!("database source is read-only; refusing non-read SQLite statement"); + } + if is_read { + let source_limit = source.default_row_limit.max(1); + let row_limit = requested_row_limit + .unwrap_or(source_limit) + .clamp(1, source_limit) + .min(MAX_ROW_LIMIT); + let columns = statement + .column_names() + .into_iter() + .map(ToOwned::to_owned) + .collect::>(); + let mut rows = statement.query([])?; + let mut output_rows = Vec::new(); + let mut seen = 0u64; + let mut truncated = false; + while let Some(row) = rows.next()? { + seen += 1; + if seen > row_limit as u64 { + truncated = true; + break; + } + let mut object = BTreeMap::new(); + for (idx, column) in columns.iter().enumerate() { + object.insert(column.clone(), sqlite_value_to_json(row.get_ref(idx)?)); + } + output_rows.push(object); + } + Ok(QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation: classify_sql_operation(query), + columns, + row_count: output_rows.len() as u64, + rows: output_rows, + rows_affected: None, + truncated, + }) + } else { + let rows_affected = statement.execute([])? as u64; + Ok(QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation: classify_sql_operation(query), + columns: Vec::new(), + rows: Vec::new(), + row_count: 0, + rows_affected: Some(rows_affected), + truncated: false, + }) + } +} + +async fn execute_supabase_rest_request( + source: &DatabaseSourceConfig, + input: &ExecuteDatabaseQueryInput, +) -> Result { + let api_key = source + .supabase_rest + .selected_key() + .ok_or_else(|| anyhow!("Supabase anon key or service role key is required"))?; + let request = input.supabase.as_ref().ok_or_else(|| { + anyhow!("Supabase REST execution requires supabase.path with an explicit REST path") + })?; + let method = request + .method + .as_deref() + .unwrap_or("GET") + .trim() + .to_ascii_uppercase(); + let method = Method::from_bytes(method.as_bytes()).context("invalid Supabase REST method")?; + let path = request + .path + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + .ok_or_else(|| anyhow!("Supabase REST path is required"))?; + if path.starts_with("rpc/") && !source.supabase_rest.allow_rpc { + bail!("Supabase RPC calls are disabled for this source"); + } + let base = Url::parse(source.supabase_rest.project_url.trim()) + .context("Supabase project URL is invalid")?; + let url = base + .join(&format!("rest/v1/{}", path.trim_start_matches('/'))) + .context("unable to build Supabase REST endpoint")?; + let client = reqwest::Client::builder() + .timeout(Duration::from_millis(source.default_timeout_ms)) + .build() + .context("unable to build Supabase REST client")?; + let mut builder = client + .request(method.clone(), url) + .header("apikey", api_key) + .bearer_auth(api_key) + .header(ACCEPT, "application/json"); + if let Some(body) = request.body.clone() { + builder = builder.json(&body); + } + let response = builder.send().await.context("Supabase REST request failed")?; + let status = response.status(); + let text = response.text().await.unwrap_or_default(); + if !status.is_success() { + bail!( + "Supabase REST request failed ({status}): {}", + truncate_chars(text.trim(), 1_000) + ); + } + let value = serde_json::from_str::(&text).unwrap_or(Value::Null); + let source_limit = source.default_row_limit.max(1) as usize; + let row_limit = input + .row_limit + .unwrap_or(source.default_row_limit) + .clamp(1, source.default_row_limit) + .min(MAX_ROW_LIMIT) as usize; + let (columns, rows, truncated) = json_value_to_rows(value, row_limit.min(source_limit)); + let rows_affected = if method == Method::GET { + None + } else { + Some(rows.len() as u64) + }; + Ok(QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation: classify_sql_operation(&input.query), + row_count: rows.len() as u64, + columns, + rows, + rows_affected, + truncated, + }) +} + +fn open_sqlite_connection(source: &DatabaseSourceConfig, force_read_only: bool) -> Result { + let path = source.sqlite.file_path.trim(); + if path.is_empty() { + bail!("SQLite file path is required"); + } + let flags = if force_read_only { + OpenFlags::SQLITE_OPEN_READ_ONLY + } else if source.sqlite.create_if_missing { + OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE + } else { + OpenFlags::SQLITE_OPEN_READ_WRITE + }; + Connection::open_with_flags(Path::new(path), flags) + .with_context(|| "unable to open SQLite database") +} + +fn install_sqlite_timeout(conn: &Connection, timeout_ms: u64) { + let deadline = + Instant::now() + Duration::from_millis(timeout_ms.clamp(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS)); + conn.progress_handler(1_000, Some(move || Instant::now() >= deadline)); +} + +fn install_sqlite_read_only_authorizer(conn: &Connection) { + conn.authorizer(Some(|ctx: AuthContext<'_>| match ctx.action { + AuthAction::Select | AuthAction::Read { .. } | AuthAction::Function { .. } => { + Authorization::Allow + } + AuthAction::Pragma { + pragma_name, + pragma_value, + } => { + if pragma_value.is_none() && is_safe_read_only_pragma(pragma_name) { + Authorization::Allow + } else { + Authorization::Deny + } + } + _ => Authorization::Deny, + })); +} + +fn is_safe_read_only_pragma(name: &str) -> bool { + matches!( + name.to_ascii_lowercase().as_str(), + "table_info" + | "table_xinfo" + | "index_list" + | "index_info" + | "index_xinfo" + | "foreign_key_list" + | "database_list" + | "schema_version" + | "user_version" + | "integrity_check" + | "quick_check" + ) +} + +fn sqlite_columns(conn: &Connection, table: &str) -> Result> { + let mut stmt = conn.prepare(&format!("pragma table_xinfo({})", quote_sqlite_literal(table)))?; + let mut rows = stmt.query([])?; + let mut columns = Vec::new(); + while let Some(row) = rows.next()? { + let hidden: i64 = row.get(6).unwrap_or(0); + columns.push(json!({ + "name": row.get::<_, String>(1)?, + "type": row.get::<_, String>(2).unwrap_or_default(), + "notNull": row.get::<_, i64>(3).unwrap_or(0) != 0, + "defaultValue": row.get::<_, Option>(4).unwrap_or(None), + "primaryKeyOrdinal": row.get::<_, i64>(5).unwrap_or(0), + "hidden": hidden != 0, + })); + } + Ok(columns) +} + +fn sqlite_primary_keys(conn: &Connection, table: &str) -> Result> { + let mut stmt = conn.prepare(&format!("pragma table_info({})", quote_sqlite_literal(table)))?; + let mapped = stmt.query_map([], |row| { + Ok(( + row.get::<_, String>(1)?, + row.get::<_, i64>(5).unwrap_or(0), + )) + })?; + let mut keys = mapped + .filter_map(|row| row.ok()) + .filter(|(_, pk)| *pk > 0) + .collect::>(); + keys.sort_by_key(|(_, pk)| *pk); + Ok(keys.into_iter().map(|(name, _)| name).collect()) +} + +fn sqlite_foreign_keys(conn: &Connection, table: &str) -> Result> { + let mut stmt = conn.prepare(&format!( + "pragma foreign_key_list({})", + quote_sqlite_literal(table) + ))?; + let mut rows = stmt.query([])?; + let mut keys = Vec::new(); + while let Some(row) = rows.next()? { + keys.push(json!({ + "id": row.get::<_, i64>(0).unwrap_or(0), + "seq": row.get::<_, i64>(1).unwrap_or(0), + "table": row.get::<_, String>(2).unwrap_or_default(), + "from": row.get::<_, String>(3).unwrap_or_default(), + "to": row.get::<_, String>(4).unwrap_or_default(), + "onUpdate": row.get::<_, String>(5).unwrap_or_default(), + "onDelete": row.get::<_, String>(6).unwrap_or_default(), + })); + } + Ok(keys) +} + +fn sqlite_indexes(conn: &Connection, table: &str) -> Result> { + let mut stmt = conn.prepare(&format!("pragma index_list({})", quote_sqlite_literal(table)))?; + let rows = stmt.query_map([], |row| { + Ok(( + row.get::<_, String>(1)?, + row.get::<_, i64>(2).unwrap_or(0) != 0, + row.get::<_, String>(3).unwrap_or_default(), + )) + })?; + let mut indexes = Vec::new(); + for row in rows { + let (name, unique, origin) = row?; + indexes.push(json!({ + "name": name, + "unique": unique, + "origin": origin, + "columns": sqlite_index_columns(conn, &name)?, + })); + } + Ok(indexes) +} + +fn sqlite_index_columns(conn: &Connection, index: &str) -> Result> { + let mut stmt = conn.prepare(&format!("pragma index_info({})", quote_sqlite_literal(index)))?; + let rows = stmt.query_map([], |row| row.get::<_, String>(2))?; + let mut columns = Vec::new(); + for row in rows { + columns.push(row?); + } + Ok(columns) +} + +fn quote_sqlite_literal(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} + +fn sqlite_value_to_json(value: ValueRef<'_>) -> Value { + match value { + ValueRef::Null => Value::Null, + ValueRef::Integer(value) => json!(value), + ValueRef::Real(value) => json!(value), + ValueRef::Text(value) => String::from_utf8_lossy(value).to_string().into(), + ValueRef::Blob(value) => format!("[blob:{} bytes]", value.len()).into(), + } +} + +fn json_value_to_rows( + value: Value, + row_limit: usize, +) -> (Vec, Vec>, bool) { + let values = match value { + Value::Array(values) => values, + Value::Null => Vec::new(), + other => vec![other], + }; + let truncated = values.len() > row_limit; + let mut columns = Vec::::new(); + let mut rows = Vec::new(); + for value in values.into_iter().take(row_limit) { + let mut row = BTreeMap::new(); + match value { + Value::Object(map) => { + for (key, value) in map { + if !columns.contains(&key) { + columns.push(key.clone()); + } + row.insert(key, value); + } + } + other => { + let key = "value".to_string(); + if columns.is_empty() { + columns.push(key.clone()); + } + row.insert(key, other); + } + } + rows.push(row); + } + (columns, rows, truncated) +} + +fn insert_duration(value: &mut Value, duration: Duration) { + if let Value::Object(map) = value { + map.insert("durationMs".into(), json!(duration.as_millis() as u64)); + } +} + +fn is_confirmation_required(operation: DatabaseActivityOperation) -> bool { + matches!( + operation, + DatabaseActivityOperation::Write + | DatabaseActivityOperation::Ddl + | DatabaseActivityOperation::Destructive + | DatabaseActivityOperation::Unknown + ) +} + +fn confirmation_required_message( + source: &DatabaseSourceConfig, + query: &str, + operation: DatabaseActivityOperation, + question_available: bool, +) -> String { + let preview = sanitize_query_preview(query); + let instruction = if question_available { + "Ask the user for explicit confirmation in the conversation before executing. If they approve, call database_execute_query again with confirmed=true." + } else { + "This context cannot ask the user for confirmation, so the operation was refused. Ask the main agent/user to perform it explicitly." + }; + format!( + "confirmation required for database source `{}` ({}) {:?} operation. Query preview: `{}`. {}", + source.name, + source.engine.agent_id(), + operation, + preview, + instruction + ) +} + +fn pretty_json(value: &Value) -> String { + serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()) +} + +fn truncate_chars(input: &str, limit: usize) -> String { + if input.chars().count() <= limit { + return input.to_string(); + } + let mut clipped = input + .chars() + .take(limit.saturating_sub(1)) + .collect::(); + clipped.push('…'); + clipped +} + +fn now_ms() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_millis() as i64) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn confirmation_required_for_destructive_operations() { + assert!(is_confirmation_required(DatabaseActivityOperation::Destructive)); + assert!(is_confirmation_required(DatabaseActivityOperation::Ddl)); + assert!(!is_confirmation_required(DatabaseActivityOperation::Read)); + } + + #[test] + fn sqlite_blob_values_are_summarized() { + assert_eq!(sqlite_value_to_json(ValueRef::Blob(&[1, 2, 3])), "[blob:3 bytes]"); + } +} + +// --- Postgres / MySQL implementations via sqlx --- + +use sqlx::{ + mysql::{MySqlConnectOptions, MySqlPoolOptions, MySqlRow, MySqlSslMode}, + postgres::{PgConnectOptions, PgPoolOptions, PgRow, PgSslMode}, + Column, ConnectOptions, Row, TypeInfo, +}; +use tokio::time::timeout as tokio_timeout; + +use crate::database::{build_sql_connection_url, DatabaseSslMode}; + +fn pg_connect_options(source: &DatabaseSourceConfig) -> Result { + let url = build_sql_connection_url(source, 5432, "postgresql")?; + let mut opts: PgConnectOptions = url + .parse() + .context("invalid Postgres connection string")?; + opts = opts.application_name("claakecode"); + opts = match source.fields.ssl_mode { + DatabaseSslMode::Disabled => opts.ssl_mode(PgSslMode::Disable), + DatabaseSslMode::Required => opts.ssl_mode(PgSslMode::Require), + DatabaseSslMode::Strict => opts.ssl_mode(PgSslMode::VerifyFull), + }; + opts = opts.log_statements(tracing::log::LevelFilter::Off); + Ok(opts) +} + +fn my_connect_options(source: &DatabaseSourceConfig) -> Result { + let url = build_sql_connection_url(source, 3306, "mysql")?; + let mut opts: MySqlConnectOptions = url + .parse() + .context("invalid MySQL connection string")?; + opts = match source.fields.ssl_mode { + DatabaseSslMode::Disabled => opts.ssl_mode(MySqlSslMode::Disabled), + DatabaseSslMode::Required => opts.ssl_mode(MySqlSslMode::Required), + DatabaseSslMode::Strict => opts.ssl_mode(MySqlSslMode::VerifyIdentity), + }; + opts = opts.log_statements(tracing::log::LevelFilter::Off); + Ok(opts) +} + +fn source_timeout(source: &DatabaseSourceConfig) -> Duration { + Duration::from_millis(source.default_timeout_ms.max(MIN_TIMEOUT_MS).min(MAX_TIMEOUT_MS)) +} + +fn source_row_limit(source: &DatabaseSourceConfig, requested: Option) -> u32 { + let source_limit = source.default_row_limit.max(1); + requested + .unwrap_or(source_limit) + .clamp(1, source_limit) + .min(MAX_ROW_LIMIT) +} + +async fn describe_postgres_schema( + source: &DatabaseSourceConfig, + schema: Option<&str>, + table: Option<&str>, +) -> Result { + let opts = pg_connect_options(source)?; + let timeout = source_timeout(source); + let pool = PgPoolOptions::new() + .max_connections(1) + .acquire_timeout(timeout) + .connect_with(opts) + .await + .context("unable to connect to Postgres")?; + let schema_filter = schema + .map(str::trim) + .filter(|s| !s.is_empty()) + .unwrap_or(&source.default_schema) + .to_string(); + let table_filter = table.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + let tables_query = " + SELECT n.nspname AS schema_name, + c.relname AS table_name, + CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' + WHEN 'm' THEN 'materialized_view' ELSE c.relkind::text END AS kind + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('r','v','m') + AND ($1 = '' OR n.nspname = $1) + AND ($2::text IS NULL OR c.relname = $2) + ORDER BY n.nspname, c.relname + LIMIT $3"; + let table_rows = tokio_timeout( + timeout, + sqlx::query(tables_query) + .bind(&schema_filter) + .bind(table_filter.as_deref()) + .bind(SCHEMA_OBJECT_LIMIT as i64 + 1) + .fetch_all(&pool), + ) + .await + .context("Postgres schema query timed out")? + .context("Postgres schema query failed")?; + let truncated = table_rows.len() > SCHEMA_OBJECT_LIMIT; + let tables = table_rows + .into_iter() + .take(SCHEMA_OBJECT_LIMIT) + .collect::>(); + let mut entries = Vec::new(); + for trow in &tables { + let table_schema: String = trow.try_get(0).unwrap_or_default(); + let table_name: String = trow.try_get(1).unwrap_or_default(); + let kind: String = trow.try_get(2).unwrap_or_default(); + let columns = sqlx::query( + "SELECT column_name, data_type, is_nullable, column_default, ordinal_position + FROM information_schema.columns + WHERE table_schema = $1 AND table_name = $2 + ORDER BY ordinal_position", + ) + .bind(&table_schema) + .bind(&table_name) + .fetch_all(&pool) + .await + .unwrap_or_default(); + let columns_json = columns + .into_iter() + .map(|row| { + json!({ + "name": row.try_get::(0).unwrap_or_default(), + "type": row.try_get::(1).unwrap_or_default(), + "nullable": row.try_get::(2).unwrap_or_default() == "YES", + "default": row.try_get::, _>(3).unwrap_or(None), + "ordinalPosition": row.try_get::(4).unwrap_or(0), + }) + }) + .collect::>(); + entries.push(json!({ + "schema": table_schema, + "name": table_name, + "kind": kind, + "columns": columns_json, + })); + } + pool.close().await; + Ok(json!({ + "engine": source.engine.agent_id(), + "source": source.name, + "schema": schema_filter, + "tables": entries, + "truncated": truncated, + })) +} + +async fn describe_mysql_schema( + source: &DatabaseSourceConfig, + schema: Option<&str>, + table: Option<&str>, +) -> Result { + let opts = my_connect_options(source)?; + let timeout = source_timeout(source); + let pool = MySqlPoolOptions::new() + .max_connections(1) + .acquire_timeout(timeout) + .connect_with(opts) + .await + .context("unable to connect to MySQL")?; + let database_default = source.fields.database.trim().to_string(); + let schema_filter = schema + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) + .unwrap_or(database_default); + let table_filter = table.map(|t| t.trim().to_string()).filter(|t| !t.is_empty()); + let tables = tokio_timeout( + timeout, + sqlx::query( + "SELECT table_schema, table_name, table_type + FROM information_schema.tables + WHERE (? = '' OR table_schema = ?) + AND (? IS NULL OR table_name = ?) + ORDER BY table_schema, table_name + LIMIT ?", + ) + .bind(&schema_filter) + .bind(&schema_filter) + .bind(table_filter.as_deref()) + .bind(table_filter.as_deref()) + .bind((SCHEMA_OBJECT_LIMIT + 1) as i64) + .fetch_all(&pool), + ) + .await + .context("MySQL schema query timed out")? + .context("MySQL schema query failed")?; + let truncated = tables.len() > SCHEMA_OBJECT_LIMIT; + let tables = tables.into_iter().take(SCHEMA_OBJECT_LIMIT).collect::>(); + let mut entries = Vec::new(); + for trow in &tables { + let table_schema: String = trow.try_get(0).unwrap_or_default(); + let table_name: String = trow.try_get(1).unwrap_or_default(); + let kind: String = trow.try_get(2).unwrap_or_default(); + let columns = sqlx::query( + "SELECT column_name, column_type, is_nullable, column_default, ordinal_position + FROM information_schema.columns + WHERE table_schema = ? AND table_name = ? + ORDER BY ordinal_position", + ) + .bind(&table_schema) + .bind(&table_name) + .fetch_all(&pool) + .await + .unwrap_or_default(); + let columns_json = columns + .into_iter() + .map(|row| { + json!({ + "name": row.try_get::(0).unwrap_or_default(), + "type": row.try_get::(1).unwrap_or_default(), + "nullable": row.try_get::(2).unwrap_or_default().eq_ignore_ascii_case("YES"), + "default": row.try_get::, _>(3).unwrap_or(None), + "ordinalPosition": row.try_get::(4).unwrap_or(0), + }) + }) + .collect::>(); + entries.push(json!({ + "schema": table_schema, + "name": table_name, + "kind": kind, + "columns": columns_json, + })); + } + pool.close().await; + Ok(json!({ + "engine": source.engine.agent_id(), + "source": source.name, + "schema": schema_filter, + "tables": entries, + "truncated": truncated, + })) +} + +async fn execute_postgres_query( + source: &DatabaseSourceConfig, + query: &str, + requested_row_limit: Option, +) -> Result { + let opts = pg_connect_options(source)?; + let timeout = source_timeout(source); + let pool = PgPoolOptions::new() + .max_connections(1) + .acquire_timeout(timeout) + .connect_with(opts) + .await + .context("unable to connect to Postgres")?; + let operation = classify_sql_operation(query); + let is_read = matches!(operation, DatabaseActivityOperation::Read); + let row_limit = source_row_limit(source, requested_row_limit) as usize; + let outcome = if is_read { + let rows = tokio_timeout(timeout, sqlx::query(query).fetch_all(&pool)) + .await + .context("Postgres query timed out")? + .context("Postgres query failed")?; + let truncated = rows.len() > row_limit; + let rows = rows.into_iter().take(row_limit).collect::>(); + let columns = rows + .first() + .map(|row| { + row.columns() + .iter() + .map(|c| c.name().to_string()) + .collect::>() + }) + .unwrap_or_default(); + let json_rows = rows.iter().map(pg_row_to_json).collect::>(); + QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation, + row_count: json_rows.len() as u64, + columns, + rows: json_rows, + rows_affected: None, + truncated, + } + } else { + let result = tokio_timeout(timeout, sqlx::query(query).execute(&pool)) + .await + .context("Postgres query timed out")? + .context("Postgres query failed")?; + QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation, + columns: Vec::new(), + rows: Vec::new(), + row_count: 0, + rows_affected: Some(result.rows_affected()), + truncated: false, + } + }; + pool.close().await; + Ok(outcome) +} + +async fn execute_mysql_query( + source: &DatabaseSourceConfig, + query: &str, + requested_row_limit: Option, +) -> Result { + let opts = my_connect_options(source)?; + let timeout = source_timeout(source); + let pool = MySqlPoolOptions::new() + .max_connections(1) + .acquire_timeout(timeout) + .connect_with(opts) + .await + .context("unable to connect to MySQL")?; + let operation = classify_sql_operation(query); + let is_read = matches!(operation, DatabaseActivityOperation::Read); + let row_limit = source_row_limit(source, requested_row_limit) as usize; + let outcome = if is_read { + let rows = tokio_timeout(timeout, sqlx::query(query).fetch_all(&pool)) + .await + .context("MySQL query timed out")? + .context("MySQL query failed")?; + let truncated = rows.len() > row_limit; + let rows = rows.into_iter().take(row_limit).collect::>(); + let columns = rows + .first() + .map(|row| { + row.columns() + .iter() + .map(|c| c.name().to_string()) + .collect::>() + }) + .unwrap_or_default(); + let json_rows = rows.iter().map(mysql_row_to_json).collect::>(); + QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation, + row_count: json_rows.len() as u64, + columns, + rows: json_rows, + rows_affected: None, + truncated, + } + } else { + let result = tokio_timeout(timeout, sqlx::query(query).execute(&pool)) + .await + .context("MySQL query timed out")? + .context("MySQL query failed")?; + QueryExecutionOutput { + source: source.name.clone(), + engine: source.engine.agent_id().to_string(), + operation, + columns: Vec::new(), + rows: Vec::new(), + row_count: 0, + rows_affected: Some(result.rows_affected()), + truncated: false, + } + }; + pool.close().await; + Ok(outcome) +} + +fn pg_row_to_json(row: &PgRow) -> BTreeMap { + let mut out = BTreeMap::new(); + for column in row.columns() { + let name = column.name().to_string(); + let type_name = column.type_info().name().to_string(); + let value = pg_value_to_json(row, column.ordinal(), &type_name); + out.insert(name, value); + } + out +} + +fn pg_value_to_json(row: &PgRow, index: usize, type_name: &str) -> Value { + match type_name { + "BOOL" => row + .try_get::, _>(index) + .ok() + .flatten() + .map(Value::Bool) + .unwrap_or(Value::Null), + "INT2" | "INT4" => row + .try_get::, _>(index) + .ok() + .flatten() + .map(|v| Value::Number(v.into())) + .unwrap_or(Value::Null), + "INT8" => row + .try_get::, _>(index) + .ok() + .flatten() + .map(|v| Value::Number(v.into())) + .unwrap_or(Value::Null), + "FLOAT4" => row + .try_get::, _>(index) + .ok() + .flatten() + .and_then(|v| serde_json::Number::from_f64(v as f64).map(Value::Number)) + .unwrap_or(Value::Null), + "FLOAT8" | "NUMERIC" => row + .try_get::, _>(index) + .ok() + .flatten() + .and_then(|v| serde_json::Number::from_f64(v).map(Value::Number)) + .unwrap_or(Value::Null), + "JSON" | "JSONB" => row + .try_get::, _>(index) + .ok() + .flatten() + .unwrap_or(Value::Null), + _ => row + .try_get::, _>(index) + .ok() + .flatten() + .map(Value::String) + .unwrap_or_else(|| Value::String(format!("[{type_name}]"))), + } +} + +fn mysql_row_to_json(row: &MySqlRow) -> BTreeMap { + let mut out = BTreeMap::new(); + for column in row.columns() { + let name = column.name().to_string(); + let type_name = column.type_info().name().to_string(); + let value = mysql_value_to_json(row, column.ordinal(), &type_name); + out.insert(name, value); + } + out +} + +fn mysql_value_to_json(row: &MySqlRow, index: usize, type_name: &str) -> Value { + let upper = type_name.to_ascii_uppercase(); + if upper.contains("INT") { + if let Ok(Some(value)) = row.try_get::, _>(index) { + return Value::Number(value.into()); + } + } + if upper.contains("DECIMAL") || upper.contains("DOUBLE") || upper.contains("FLOAT") { + if let Ok(Some(value)) = row.try_get::, _>(index) { + if let Some(num) = serde_json::Number::from_f64(value) { + return Value::Number(num); + } + } + } + if upper.contains("BOOL") || upper == "TINYINT(1)" { + if let Ok(Some(value)) = row.try_get::, _>(index) { + return Value::Bool(value); + } + } + if upper.contains("JSON") { + if let Ok(Some(value)) = row.try_get::, _>(index) { + return value; + } + } + if let Ok(Some(value)) = row.try_get::, _>(index) { + return Value::String(value); + } + Value::Null +} diff --git a/crates/sinew-app/src/edit.rs b/crates/claakecode-app/src/edit.rs similarity index 99% rename from crates/sinew-app/src/edit.rs rename to crates/claakecode-app/src/edit.rs index 55a5eab4..804c2ca3 100644 --- a/crates/sinew-app/src/edit.rs +++ b/crates/claakecode-app/src/edit.rs @@ -9,7 +9,7 @@ use anyhow::{bail, Context, Result}; use regex::Regex; use serde::Deserialize; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::{ @@ -2357,6 +2357,6 @@ mod tests { } fn unique_temp_dir() -> PathBuf { - std::env::temp_dir().join(format!("sinew-edit-test-{}", Uuid::new_v4())) + std::env::temp_dir().join(format!("claakecode-edit-test-{}", Uuid::new_v4())) } } diff --git a/crates/sinew-app/src/glob.rs b/crates/claakecode-app/src/glob.rs similarity index 99% rename from crates/sinew-app/src/glob.rs rename to crates/claakecode-app/src/glob.rs index 6475ec34..625074be 100644 --- a/crates/sinew-app/src/glob.rs +++ b/crates/claakecode-app/src/glob.rs @@ -7,7 +7,7 @@ use std::{ use anyhow::{bail, Context, Result}; use serde::Deserialize; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use tokio::{ io::{AsyncBufReadExt, AsyncReadExt, BufReader}, process::Command, @@ -501,7 +501,7 @@ mod tests { .expect("time should be after epoch") .as_nanos(); std::env::temp_dir().join(format!( - "sinew-glob-test-{}-{counter}-{nanos}", + "claakecode-glob-test-{}-{counter}-{nanos}", std::process::id() )) } diff --git a/crates/sinew-app/src/grep.rs b/crates/claakecode-app/src/grep.rs similarity index 99% rename from crates/sinew-app/src/grep.rs rename to crates/claakecode-app/src/grep.rs index c189a4c5..56afeed7 100644 --- a/crates/sinew-app/src/grep.rs +++ b/crates/claakecode-app/src/grep.rs @@ -11,7 +11,7 @@ use anyhow::{bail, Context, Result}; use regex::Regex; use serde::Deserialize; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use tokio::{ io::{AsyncBufReadExt, AsyncReadExt, BufReader as TokioBufReader}, process::Command, @@ -1596,7 +1596,7 @@ mod tests { .as_nanos(); let counter = TEMP_DIR_COUNTER.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!( - "sinew-grep-test-{}-{nanos}-{counter}", + "claakecode-grep-test-{}-{nanos}-{counter}", std::process::id() )) } diff --git a/crates/sinew-app/src/image.rs b/crates/claakecode-app/src/image.rs similarity index 99% rename from crates/sinew-app/src/image.rs rename to crates/claakecode-app/src/image.rs index 2bbd3402..86431b03 100644 --- a/crates/sinew-app/src/image.rs +++ b/crates/claakecode-app/src/image.rs @@ -15,8 +15,8 @@ use reqwest::{ }; use serde::Deserialize; use serde_json::{json, Map, Value}; -use sinew_core::ToolDescriptor; -use sinew_openai::{Credential, MODEL_ID as OPENAI_RESPONSES_IMAGE_MODEL}; +use claakecode_core::ToolDescriptor; +use claakecode_openai::{Credential, MODEL_ID as OPENAI_RESPONSES_IMAGE_MODEL}; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::store::ImageProvider; @@ -30,7 +30,7 @@ const NANO_BANANA_URL: &str = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-image-preview:generateContent"; const OPENAI_SUBSCRIPTION_IMAGE_INSTRUCTIONS: &str = "You are Sinew, a concise coding assistant. When the user asks for an image, immediately call the image_generation tool with their prompt. Do not reply with text."; const REQUEST_TIMEOUT: Duration = Duration::from_secs(300); -const USER_AGENT: &str = "sinew/0.1"; +const USER_AGENT: &str = "ClaakeCode/0.1"; #[derive(Debug, Clone)] pub struct CreateImageTool { @@ -667,10 +667,10 @@ impl CreateImageTool { } fn save_image(&self, bytes: &[u8], extension: &str, idx: usize) -> Result { - let dir = self.workspace_root.join(".sinew/images"); - fs::create_dir_all(&dir).context("unable to create .sinew/images")?; + let dir = self.workspace_root.join(".claakecode/images"); + fs::create_dir_all(&dir).context("unable to create .claakecode/images")?; let name = format!("{}-{}.{}", now_ms(), idx + 1, extension); - let relative_path = format!(".sinew/images/{name}"); + let relative_path = format!(".claakecode/images/{name}"); fs::write(self.workspace_root.join(&relative_path), bytes) .context("unable to save generated image")?; Ok(relative_path) diff --git a/crates/sinew-app/src/lib.rs b/crates/claakecode-app/src/lib.rs similarity index 79% rename from crates/sinew-app/src/lib.rs rename to crates/claakecode-app/src/lib.rs index 496dc4df..3be16530 100644 --- a/crates/sinew-app/src/lib.rs +++ b/crates/claakecode-app/src/lib.rs @@ -1,6 +1,8 @@ pub mod agent; pub mod bash; pub mod compact; +pub mod database; +pub mod database_tool; pub mod edit; pub mod glob; pub mod grep; @@ -27,6 +29,18 @@ pub use agent::{ }; pub use bash::{active_shell_display_name, shell_system_prompt, BashTool}; pub use compact::{compact_conversation_history, CompactConversationOutput}; +pub use database::{ + classify_sql_operation, sanitize_query_preview, test_database_source_connection, + DatabaseActivityEntry, DatabaseActivityOperation, DatabaseActivityStatus, + DatabaseConnectionFields, DatabaseConnectionStatus, DatabaseConnectionState, + DatabaseConnectionTestResult, DatabaseConnectionTestState, DatabaseCredentialMode, + DatabaseSettings, DatabaseSourceConfig, DatabaseSourceEngine, DatabaseSourceSummary, + DatabaseSqliteConfig, DatabaseSslMode, DatabaseSupabaseRestConfig, +}; +pub use database_tool::{ + DatabaseTool, DATABASE_DESCRIBE_SCHEMA_TOOL, DATABASE_EXECUTE_QUERY_TOOL, + DATABASE_LIST_SOURCES_TOOL, +}; pub use edit::EditFileTool; pub use glob::GlobTool; pub use grep::GrepTool; diff --git a/crates/sinew-app/src/mcp.rs b/crates/claakecode-app/src/mcp.rs similarity index 99% rename from crates/sinew-app/src/mcp.rs rename to crates/claakecode-app/src/mcp.rs index 20c99d81..c6f182cd 100644 --- a/crates/sinew-app/src/mcp.rs +++ b/crates/claakecode-app/src/mcp.rs @@ -13,7 +13,7 @@ use std::{ use anyhow::{anyhow, bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_core::{ChatMessage, Part, ToolDescriptor}; +use claakecode_core::{ChatMessage, Part, ToolDescriptor}; use tokio::{ io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, process::{Child, ChildStdin, ChildStdout, Command}, @@ -855,7 +855,7 @@ impl McpStdioClient { "protocolVersion": MCP_PROTOCOL_VERSION, "capabilities": {}, "clientInfo": { - "name": "sinew", + "name": "claakecode", "version": env!("CARGO_PKG_VERSION") } }), @@ -954,7 +954,7 @@ impl McpStdioClient { "id": request_id, "error": { "code": -32601, - "message": "Method not supported by Sinew MCP client" + "message": "Method not supported by Claake Code MCP client" } })) .await?; diff --git a/crates/sinew-app/src/question.rs b/crates/claakecode-app/src/question.rs similarity index 99% rename from crates/sinew-app/src/question.rs rename to crates/claakecode-app/src/question.rs index e62ffe72..caea4be7 100644 --- a/crates/sinew-app/src/question.rs +++ b/crates/claakecode-app/src/question.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use crate::{agent::QuestionReply, tool_run::ToolRunResult, TurnCancel}; diff --git a/crates/sinew-app/src/read.rs b/crates/claakecode-app/src/read.rs similarity index 98% rename from crates/sinew-app/src/read.rs rename to crates/claakecode-app/src/read.rs index bcc7dfa2..ed4488ac 100644 --- a/crates/sinew-app/src/read.rs +++ b/crates/claakecode-app/src/read.rs @@ -8,8 +8,8 @@ use anyhow::{bail, Context, Result}; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use claakecode_core::ToolDescriptor; use sha2::{Digest, Sha256}; -use sinew_core::ToolDescriptor; use crate::{ text::decode_text, @@ -393,6 +393,6 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("time should be after epoch") .as_nanos(); - std::env::temp_dir().join(format!("sinew-read-test-{}-{nanos}", std::process::id())) + std::env::temp_dir().join(format!("claakecode-read-test-{}-{nanos}", std::process::id())) } } diff --git a/crates/sinew-app/src/ripgrep.rs b/crates/claakecode-app/src/ripgrep.rs similarity index 98% rename from crates/sinew-app/src/ripgrep.rs rename to crates/claakecode-app/src/ripgrep.rs index b05c4069..4a9eb174 100644 --- a/crates/sinew-app/src/ripgrep.rs +++ b/crates/claakecode-app/src/ripgrep.rs @@ -1,6 +1,6 @@ use std::{env, path::PathBuf}; -const RG_ENV: &str = "SINEW_RG_PATH"; +const RG_ENV: &str = "CLAAKECODE_RG_PATH"; pub(crate) fn ripgrep_executable() -> PathBuf { env::var_os(RG_ENV) diff --git a/crates/sinew-app/src/skill.rs b/crates/claakecode-app/src/skill.rs similarity index 97% rename from crates/sinew-app/src/skill.rs rename to crates/claakecode-app/src/skill.rs index 4afef07f..e6694726 100644 --- a/crates/sinew-app/src/skill.rs +++ b/crates/claakecode-app/src/skill.rs @@ -8,7 +8,7 @@ use anyhow::{bail, Context, Result}; use directories::BaseDirs; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use crate::tool_run::ToolRunResult; @@ -108,12 +108,12 @@ impl SkillTool { fn discover(&self) -> Vec { let mut roots = vec![ self.workspace_root.join(".agents/skills"), - self.workspace_root.join(".sinew/skills"), + self.workspace_root.join(".claakecode/skills"), ]; if let Some(base_dirs) = BaseDirs::new() { let home = base_dirs.home_dir(); roots.push(home.join(".agents/skills")); - roots.push(home.join(".sinew/skills")); + roots.push(home.join(".claakecode/skills")); } let mut seen = HashSet::new(); @@ -267,11 +267,11 @@ pub fn list_installed_skills( SkillSource::Workspace, workspace_root.join(".agents/skills"), ), - (SkillSource::Workspace, workspace_root.join(".sinew/skills")), + (SkillSource::Workspace, workspace_root.join(".claakecode/skills")), ]; if let Some(home) = home_dir.as_ref() { roots.push((SkillSource::Global, home.join(".agents/skills"))); - roots.push((SkillSource::Global, home.join(".sinew/skills"))); + roots.push((SkillSource::Global, home.join(".claakecode/skills"))); } let mut seen = HashSet::new(); diff --git a/crates/sinew-app/src/store.rs b/crates/claakecode-app/src/store.rs similarity index 85% rename from crates/sinew-app/src/store.rs rename to crates/claakecode-app/src/store.rs index 9f6375aa..3df35118 100644 --- a/crates/sinew-app/src/store.rs +++ b/crates/claakecode-app/src/store.rs @@ -9,11 +9,12 @@ use directories::ProjectDirs; use rusqlite::{params, Connection, OptionalExtension}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; -use sinew_core::{ChatMessage, ModelRef, Part, Role, ToolDescriptor}; +use claakecode_core::{ChatMessage, ModelRef, Part, Role, ToolDescriptor}; use uuid::Uuid; use crate::agent::AgentMode; use crate::bash::active_shell_display_name; +use crate::database::{DatabaseActivityEntry, DatabaseSettings}; use crate::mcp::McpSettings; use crate::skill::SkillSettings; use crate::subagent::SubAgentSettings; @@ -26,6 +27,8 @@ const MCP_SETTINGS_KEY: &str = "mcp_settings"; const SUB_AGENT_SETTINGS_KEY: &str = "sub_agent_settings"; const TOOL_SETTINGS_KEY: &str = "tool_settings"; const SKILL_SETTINGS_KEY: &str = "skill_settings"; +const DATABASE_SETTINGS_KEY: &str = "database_settings"; +const DATABASE_ACTIVITY_KEY: &str = "database_activity"; const OPENROUTER_MODELS_KEY: &str = "openrouter_models"; const HIDDEN_TOOL_SETTING_NAMES: &[&str] = &["skill"]; @@ -494,6 +497,9 @@ fn default_tool_display_name(name: &str) -> String { "WebSearch" => "Web search".to_string(), "WebFetch" => "Web fetch".to_string(), "CreateImage" => "Create image".to_string(), + "database_list_sources" => "List database sources".to_string(), + "database_describe_schema" => "Describe database schema".to_string(), + "database_execute_query" => "Execute database query".to_string(), "Question" => "Question".to_string(), "ToDoList" => "To-do list".to_string(), "LoadMcpTool" => "Load MCP tool".to_string(), @@ -550,7 +556,7 @@ pub struct AppStore { impl AppStore { pub fn open_default() -> Result { - let dirs = ProjectDirs::from("dev", "hyrak", "sinew") + let dirs = ProjectDirs::from("dev", "williampeynichou", "claakecode") .context("unable to resolve local data directory")?; std::fs::create_dir_all(dirs.data_local_dir()) .context("unable to create local data directory")?; @@ -562,6 +568,22 @@ impl AppStore { Ok(store) } + /// Build an in-memory store backed by a unique temporary SQLite file. + /// Intended for tests only. + pub fn in_memory() -> Result { + let dir = std::env::temp_dir(); + let unique = format!( + "claakecode-test-{}-{}.sqlite3", + std::process::id(), + now_ms() + ); + let store = Self { + path: dir.join(unique), + }; + store.migrate()?; + Ok(store) + } + pub fn path(&self) -> &Path { &self.path } @@ -1122,6 +1144,125 @@ impl AppStore { Ok(normalized) } + pub fn load_database_settings(&self) -> Result { + let conn = self.connection()?; + let stored = conn + .query_row( + "select value_json from app_settings where key = ?1", + params![DATABASE_SETTINGS_KEY], + |row| row.get::<_, String>(0), + ) + .optional() + .context("unable to read database settings")?; + + if let Some(json) = stored { + if let Ok(settings) = serde_json::from_str::(&json) { + return Ok(settings.normalized()); + } + } + + Ok(DatabaseSettings::default()) + } + + pub fn save_database_settings(&self, settings: &DatabaseSettings) -> Result { + let normalized = settings.clone().normalized_for_save()?; + let conn = self.connection()?; + conn.execute( + "insert into app_settings (key, value_json, updated_at_ms) + values (?1, ?2, ?3) + on conflict(key) do update set + value_json = excluded.value_json, + updated_at_ms = excluded.updated_at_ms", + params![ + DATABASE_SETTINGS_KEY, + serde_json::to_string(&normalized)?, + now_ms(), + ], + ) + .context("unable to save database settings")?; + Ok(normalized) + } + + pub fn list_database_source_activity( + &self, + source_id: &str, + limit: Option, + ) -> Result> { + let mut entries = self.load_database_activity_entries()?; + let source_id = source_id.trim(); + if !source_id.is_empty() { + entries.retain(|entry| entry.source_id == source_id); + } + entries.sort_by(|left, right| right.timestamp_ms.cmp(&left.timestamp_ms)); + if let Some(limit) = limit.filter(|limit| *limit > 0) { + entries.truncate(limit); + } + Ok(entries) + } + + pub fn append_database_source_activity(&self, entry: &DatabaseActivityEntry) -> Result<()> { + let mut entries = self.load_database_activity_entries()?; + entries.push(entry.clone().normalized()); + entries.sort_by(|left, right| right.timestamp_ms.cmp(&left.timestamp_ms)); + entries.truncate(500); + self.save_database_activity_entries(&entries) + } + + pub fn clear_database_source_activity( + &self, + source_id: &str, + ) -> Result> { + let source_id = source_id.trim(); + let entries = self + .load_database_activity_entries()? + .into_iter() + .filter(|entry| entry.source_id != source_id) + .collect::>(); + self.save_database_activity_entries(&entries)?; + Ok(Vec::new()) + } + + fn load_database_activity_entries(&self) -> Result> { + let conn = self.connection()?; + let stored = conn + .query_row( + "select value_json from app_settings where key = ?1", + params![DATABASE_ACTIVITY_KEY], + |row| row.get::<_, String>(0), + ) + .optional() + .context("unable to read database activity")?; + + if let Some(json) = stored { + if let Ok(entries) = serde_json::from_str::>(&json) { + return Ok(entries + .into_iter() + .map(DatabaseActivityEntry::normalized) + .collect()); + } + } + + Ok(Vec::new()) + } + + fn save_database_activity_entries(&self, entries: &[DatabaseActivityEntry]) -> Result<()> { + let conn = self.connection()?; + conn.execute( + "insert into app_settings (key, value_json, updated_at_ms) + values (?1, ?2, ?3) + on conflict(key) do update set + value_json = excluded.value_json, + updated_at_ms = excluded.updated_at_ms", + params![ + DATABASE_ACTIVITY_KEY, + serde_json::to_string(entries)?, + now_ms() + ], + ) + .context("unable to save database activity")?; + Ok(()) + } + pub fn load_skill_settings(&self) -> Result { let conn = self.connection()?; let stored = conn @@ -1715,4 +1856,122 @@ mod tests { assert_eq!(tools[0].description, "custom edit instructions"); } + + #[test] + fn database_settings_round_trip_and_activity_cap() { + use crate::database::{ + DatabaseActivityEntry, DatabaseActivityOperation, DatabaseActivityStatus, + DatabaseSettings, DatabaseSourceConfig, DatabaseSourceEngine, + }; + + let store = AppStore::in_memory().expect("in-memory store"); + let settings = DatabaseSettings { + sources: vec![DatabaseSourceConfig { + id: "src-1".into(), + name: "primary".into(), + engine: DatabaseSourceEngine::Sqlite, + enabled: true, + sqlite: crate::database::DatabaseSqliteConfig { + file_path: "/tmp/claake-test.sqlite".into(), + create_if_missing: true, + }, + ..DatabaseSourceConfig::default() + }], + }; + let saved = store + .save_database_settings(&settings) + .expect("save database settings"); + assert_eq!(saved.sources.len(), 1); + let loaded = store.load_database_settings().expect("load database settings"); + assert_eq!(loaded.sources.len(), 1); + assert_eq!(loaded.sources[0].name, "primary"); + + for i in 0..600 { + let entry = DatabaseActivityEntry { + id: format!("e-{i}"), + source_id: "src-1".into(), + source_name: "primary".into(), + engine: DatabaseSourceEngine::Sqlite, + operation: DatabaseActivityOperation::Read, + query_preview: String::new(), + status: DatabaseActivityStatus::Ok, + timestamp_ms: i as i64, + duration_ms: 1, + rows_returned: None, + rows_affected: None, + error: None, + }; + store + .append_database_source_activity(&entry) + .expect("append activity"); + } + let listed = store + .list_database_source_activity("src-1", Some(1000)) + .expect("list activity"); + assert!(listed.len() <= 500, "activity cap 500 (got {})", listed.len()); + // Most recent first. + assert_eq!(listed.first().unwrap().id, "e-599"); + + store + .clear_database_source_activity("src-1") + .expect("clear activity"); + let listed = store + .list_database_source_activity("src-1", None) + .expect("relist activity"); + assert!(listed.is_empty()); + } + + #[test] + fn database_settings_normalize_and_validate_unique_names() { + use crate::database::{ + DatabaseSettings, DatabaseSourceConfig, DatabaseSourceEngine, DatabaseSqliteConfig, + }; + let settings = DatabaseSettings { + sources: vec![DatabaseSourceConfig { + id: "".to_string(), + name: " local ".to_string(), + engine: DatabaseSourceEngine::Sqlite, + sqlite: DatabaseSqliteConfig { + file_path: "/tmp/example.sqlite".to_string(), + create_if_missing: true, + }, + default_row_limit: 50_000, + default_timeout_ms: 10, + ..DatabaseSourceConfig::default() + }], + } + .normalized_for_save() + .expect("valid database settings"); + + assert_eq!(settings.sources[0].name, "local"); + assert!(!settings.sources[0].id.is_empty()); + assert_eq!(settings.sources[0].default_row_limit, 10_000); + assert_eq!(settings.sources[0].default_timeout_ms, 1_000); + + let duplicate = DatabaseSettings { + sources: vec![ + DatabaseSourceConfig { + id: "a".to_string(), + name: "Prod".to_string(), + engine: DatabaseSourceEngine::Sqlite, + sqlite: DatabaseSqliteConfig { + file_path: "/tmp/a.sqlite".to_string(), + create_if_missing: true, + }, + ..DatabaseSourceConfig::default() + }, + DatabaseSourceConfig { + id: "b".to_string(), + name: "prod".to_string(), + engine: DatabaseSourceEngine::Sqlite, + sqlite: DatabaseSqliteConfig { + file_path: "/tmp/b.sqlite".to_string(), + create_if_missing: true, + }, + ..DatabaseSourceConfig::default() + }, + ], + }; + assert!(duplicate.normalized_for_save().is_err()); + } } diff --git a/crates/sinew-app/src/subagent.rs b/crates/claakecode-app/src/subagent.rs similarity index 95% rename from crates/sinew-app/src/subagent.rs rename to crates/claakecode-app/src/subagent.rs index b78d6c60..df5a4326 100644 --- a/crates/sinew-app/src/subagent.rs +++ b/crates/claakecode-app/src/subagent.rs @@ -2,15 +2,15 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_core::{ChatMessage, ModelRef, Part, Provider, Role, ServiceTier, ToolDescriptor}; +use claakecode_core::{ChatMessage, ModelRef, Part, Provider, Role, ServiceTier, ToolDescriptor}; use tokio::sync::mpsc; use crate::tool_run::FileChange; use crate::{ - run_turn, AgentEvent, AgentEventScope, AgentMode, BashTool, CreateImageTool, EditFileTool, - GlobTool, GoalWorkflowState, GrepTool, McpSettings, McpToolRegistry, QuestionTool, ReadTool, - SkillSettings, SkillTool, ToDoListTool, TodoListState, ToolRunResult, ToolSettings, TurnCancel, - TurnContext, WebFetchTool, WebSearchTool, WriteFileTool, + run_turn, AgentEvent, AgentEventScope, AgentMode, BashTool, CreateImageTool, DatabaseTool, + EditFileTool, GlobTool, GoalWorkflowState, GrepTool, McpSettings, McpToolRegistry, + QuestionTool, ReadTool, SkillSettings, SkillTool, ToDoListTool, TodoListState, ToolRunResult, + ToolSettings, TurnCancel, TurnContext, WebFetchTool, WebSearchTool, WriteFileTool, }; const TOOL_PREFIX: &str = "subagent_"; @@ -65,6 +65,7 @@ pub struct SubAgentTool { mcp_settings: McpSettings, tool_settings: ToolSettings, skill_settings: SkillSettings, + database: DatabaseTool, max_tool_rounds: usize, service_tier: Option, cancel: TurnCancel, @@ -79,6 +80,7 @@ impl SubAgentTool { mcp_settings: McpSettings, tool_settings: ToolSettings, skill_settings: SkillSettings, + database: DatabaseTool, max_tool_rounds: usize, service_tier: Option, cancel: TurnCancel, @@ -91,6 +93,7 @@ impl SubAgentTool { mcp_settings, tool_settings, skill_settings, + database, max_tool_rounds, service_tier, cancel, @@ -235,6 +238,7 @@ impl SubAgentTool { self.workspace_root.clone(), self.skill_settings.clone(), )), + database: Arc::new(self.database.clone()), mcp: Arc::new(McpToolRegistry::new(self.mcp_settings.clone())), subagents: None, teams: None, diff --git a/crates/sinew-app/src/team.rs b/crates/claakecode-app/src/team.rs similarity index 90% rename from crates/sinew-app/src/team.rs rename to crates/claakecode-app/src/team.rs index 6bfff8e1..deb6f29b 100644 --- a/crates/sinew-app/src/team.rs +++ b/crates/claakecode-app/src/team.rs @@ -8,14 +8,14 @@ use std::{ use futures_util::future::join_all; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_core::{ChatMessage, ModelRef, Part, Provider, Role, ServiceTier, ToolDescriptor}; +use claakecode_core::{ChatMessage, ModelRef, Part, Provider, Role, ServiceTier, ToolDescriptor}; use tokio::sync::{mpsc, Notify, RwLock, Semaphore}; use uuid::Uuid; use crate::tool_run::{DiffLineKind, FileChange, FileChangeKind, ToolRunImage}; use crate::{ run_turn, subagent_system_prompt, AgentEvent, AgentEventScope, AgentMode, BashTool, - CreateImageTool, EditFileTool, GlobTool, GoalWorkflowState, GrepTool, McpSettings, + CreateImageTool, DatabaseTool, EditFileTool, GlobTool, GoalWorkflowState, GrepTool, McpSettings, McpToolRegistry, ReadTool, SkillSettings, SkillTool, SubAgentConfig, SubAgentSettings, TodoListState, ToolRunResult, ToolSettings, TurnCancel, TurnContext, WebFetchTool, WebSearchTool, WriteFileTool, diff --git a/crates/sinew-app/src/team/agent_turns.rs b/crates/claakecode-app/src/team/agent_turns.rs similarity index 99% rename from crates/sinew-app/src/team/agent_turns.rs rename to crates/claakecode-app/src/team/agent_turns.rs index 0c76e765..b85f8358 100644 --- a/crates/sinew-app/src/team/agent_turns.rs +++ b/crates/claakecode-app/src/team/agent_turns.rs @@ -101,6 +101,7 @@ impl TeamTool { self.workspace_root.clone(), self.skill_settings.clone(), )), + database: Arc::new(self.database.clone()), mcp: Arc::new(McpToolRegistry::new(self.mcp_settings.clone())), subagents: None, teams: Some(team_tool), diff --git a/crates/sinew-app/src/team/context.rs b/crates/claakecode-app/src/team/context.rs similarity index 98% rename from crates/sinew-app/src/team/context.rs rename to crates/claakecode-app/src/team/context.rs index 246a0e3a..28de7c43 100644 --- a/crates/sinew-app/src/team/context.rs +++ b/crates/claakecode-app/src/team/context.rs @@ -10,6 +10,7 @@ impl TeamTool { mcp_settings: McpSettings, tool_settings: ToolSettings, skill_settings: SkillSettings, + database: DatabaseTool, default_model: ModelRef, max_tool_rounds: usize, service_tier: Option, @@ -25,6 +26,7 @@ impl TeamTool { mcp_settings, tool_settings, skill_settings, + database, default_model, max_tool_rounds, service_tier, diff --git a/crates/sinew-app/src/team/descriptors.rs b/crates/claakecode-app/src/team/descriptors.rs similarity index 100% rename from crates/sinew-app/src/team/descriptors.rs rename to crates/claakecode-app/src/team/descriptors.rs diff --git a/crates/sinew-app/src/team/launch.rs b/crates/claakecode-app/src/team/launch.rs similarity index 100% rename from crates/sinew-app/src/team/launch.rs rename to crates/claakecode-app/src/team/launch.rs diff --git a/crates/sinew-app/src/team/live.rs b/crates/claakecode-app/src/team/live.rs similarity index 100% rename from crates/sinew-app/src/team/live.rs rename to crates/claakecode-app/src/team/live.rs diff --git a/crates/sinew-app/src/team/messaging.rs b/crates/claakecode-app/src/team/messaging.rs similarity index 100% rename from crates/sinew-app/src/team/messaging.rs rename to crates/claakecode-app/src/team/messaging.rs diff --git a/crates/sinew-app/src/team/model.rs b/crates/claakecode-app/src/team/model.rs similarity index 99% rename from crates/sinew-app/src/team/model.rs rename to crates/claakecode-app/src/team/model.rs index 7c01f141..6fa2b027 100644 --- a/crates/sinew-app/src/team/model.rs +++ b/crates/claakecode-app/src/team/model.rs @@ -126,6 +126,7 @@ pub struct TeamTool { pub(super) mcp_settings: McpSettings, pub(super) tool_settings: ToolSettings, pub(super) skill_settings: SkillSettings, + pub(super) database: DatabaseTool, pub(super) default_model: ModelRef, pub(super) max_tool_rounds: usize, pub(super) service_tier: Option, diff --git a/crates/sinew-app/src/team/render.rs b/crates/claakecode-app/src/team/render.rs similarity index 100% rename from crates/sinew-app/src/team/render.rs rename to crates/claakecode-app/src/team/render.rs diff --git a/crates/sinew-app/src/team/session.rs b/crates/claakecode-app/src/team/session.rs similarity index 100% rename from crates/sinew-app/src/team/session.rs rename to crates/claakecode-app/src/team/session.rs diff --git a/crates/sinew-app/src/team/status_stop.rs b/crates/claakecode-app/src/team/status_stop.rs similarity index 100% rename from crates/sinew-app/src/team/status_stop.rs rename to crates/claakecode-app/src/team/status_stop.rs diff --git a/crates/sinew-app/src/team/task_board.rs b/crates/claakecode-app/src/team/task_board.rs similarity index 100% rename from crates/sinew-app/src/team/task_board.rs rename to crates/claakecode-app/src/team/task_board.rs diff --git a/crates/sinew-app/src/team/tests.rs b/crates/claakecode-app/src/team/tests.rs similarity index 99% rename from crates/sinew-app/src/team/tests.rs rename to crates/claakecode-app/src/team/tests.rs index 5a9bd532..07c57b94 100644 --- a/crates/sinew-app/src/team/tests.rs +++ b/crates/claakecode-app/src/team/tests.rs @@ -533,6 +533,7 @@ fn test_team_tool() -> TeamTool { McpSettings::default(), ToolSettings::default(), SkillSettings::default(), + crate::DatabaseTool::new(crate::store::AppStore::in_memory().expect("in-memory store")), ModelRef::new("test", "model"), 1, None, diff --git a/crates/sinew-app/src/text.rs b/crates/claakecode-app/src/text.rs similarity index 100% rename from crates/sinew-app/src/text.rs rename to crates/claakecode-app/src/text.rs diff --git a/crates/sinew-app/src/todo.rs b/crates/claakecode-app/src/todo.rs similarity index 99% rename from crates/sinew-app/src/todo.rs rename to crates/claakecode-app/src/todo.rs index 06bc94f4..60801fbf 100644 --- a/crates/sinew-app/src/todo.rs +++ b/crates/claakecode-app/src/todo.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_core::{ChatMessage, Part, ToolDescriptor}; +use claakecode_core::{ChatMessage, Part, ToolDescriptor}; use crate::tool_run::ToolRunResult; diff --git a/crates/sinew-app/src/tool_run.rs b/crates/claakecode-app/src/tool_run.rs similarity index 99% rename from crates/sinew-app/src/tool_run.rs rename to crates/claakecode-app/src/tool_run.rs index 31491fc1..a321f0b5 100644 --- a/crates/sinew-app/src/tool_run.rs +++ b/crates/claakecode-app/src/tool_run.rs @@ -1131,7 +1131,7 @@ mod tests { } fn test_root() -> std::path::PathBuf { - let root = std::env::temp_dir().join(format!("sinew-checkpoint-test-{}", Uuid::new_v4())); + let root = std::env::temp_dir().join(format!("claakecode-checkpoint-test-{}", Uuid::new_v4())); fs::create_dir_all(&root).expect("create temp workspace"); root.canonicalize().expect("canonical temp workspace") } diff --git a/crates/sinew-app/src/web.rs b/crates/claakecode-app/src/web.rs similarity index 99% rename from crates/sinew-app/src/web.rs rename to crates/claakecode-app/src/web.rs index fd812402..283081b9 100644 --- a/crates/sinew-app/src/web.rs +++ b/crates/claakecode-app/src/web.rs @@ -9,7 +9,7 @@ use reqwest::{ }; use serde::Deserialize; use serde_json::{json, Map, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use crate::store::WebSearchProvider; use crate::tool_run::ToolRunResult; @@ -17,7 +17,7 @@ use crate::tool_run::ToolRunResult; const LINKUP_SEARCH_URL: &str = "https://api.linkup.so/v1/search"; const EXA_MCP_URL: &str = "https://mcp.exa.ai/mcp"; const EXA_DEFAULT_NUM_RESULTS: u8 = 8; -const USER_AGENT: &str = "sinew/0.1"; +const USER_AGENT: &str = "ClaakeCode/0.1"; const REQUEST_TIMEOUT: Duration = Duration::from_secs(45); const WEBSEARCH_RESPONSE_LIMIT: usize = 256 * 1024; const WEBFETCH_RESPONSE_LIMIT: usize = 512 * 1024; diff --git a/crates/sinew-app/src/workspace.rs b/crates/claakecode-app/src/workspace.rs similarity index 99% rename from crates/sinew-app/src/workspace.rs rename to crates/claakecode-app/src/workspace.rs index 08af51af..881dafb3 100644 --- a/crates/sinew-app/src/workspace.rs +++ b/crates/claakecode-app/src/workspace.rs @@ -24,7 +24,7 @@ const DESIGN_SYSTEM_TEMPLATE: &str = "This is the design system of our project :\n\n'Paste design system here'"; const CLAUDE_FILE_NAME: &str = "claude.md"; const CLAUDE_FILE_TEMPLATE: &str = - "Note: Sinew does not use this CLAUDE.md file as its reference instructions. Use AGENTS.md instead.\n"; + "Note: Claake Code does not use this CLAUDE.md file as its reference instructions. Use AGENTS.md instead.\n"; const IGNORED_DIRS: &[&str] = &[ ".git", ".history", @@ -1066,7 +1066,7 @@ fn copy_directory(src: &Path, dst: &Path) -> Result<()> { } fn workspace_trash_root() -> PathBuf { - std::env::temp_dir().join("sinew-deleted-workspace-entries") + std::env::temp_dir().join("claakecode-deleted-workspace-entries") } fn unique_trash_path(trash_root: &Path, relative_path: &str) -> PathBuf { @@ -1484,7 +1484,7 @@ mod tests { .expect("time should be after epoch") .as_nanos(); std::env::temp_dir().join(format!( - "sinew-workspace-test-{}-{nanos}", + "claakecode-workspace-test-{}-{nanos}", std::process::id() )) } diff --git a/crates/sinew-app/src/write.rs b/crates/claakecode-app/src/write.rs similarity index 99% rename from crates/sinew-app/src/write.rs rename to crates/claakecode-app/src/write.rs index 8859a3d8..9f68aa63 100644 --- a/crates/sinew-app/src/write.rs +++ b/crates/claakecode-app/src/write.rs @@ -8,7 +8,7 @@ use std::{ use anyhow::{bail, Context, Result}; use serde::Deserialize; use serde_json::{json, Value}; -use sinew_core::ToolDescriptor; +use claakecode_core::ToolDescriptor; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use crate::{ @@ -501,6 +501,6 @@ mod tests { } fn unique_temp_dir() -> PathBuf { - std::env::temp_dir().join(format!("sinew-write-test-{}", Uuid::new_v4())) + std::env::temp_dir().join(format!("claakecode-write-test-{}", Uuid::new_v4())) } } diff --git a/crates/sinew-core/Cargo.toml b/crates/claakecode-core/Cargo.toml similarity index 79% rename from crates/sinew-core/Cargo.toml rename to crates/claakecode-core/Cargo.toml index cc0ef0e7..8135212e 100644 --- a/crates/sinew-core/Cargo.toml +++ b/crates/claakecode-core/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "sinew-core" +name = "claakecode-core" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Provider-agnostic types for the sinew CLI" +description = "Provider-agnostic types for Claake Code" [dependencies] async-trait = { workspace = true } diff --git a/crates/sinew-core/src/error.rs b/crates/claakecode-core/src/error.rs similarity index 100% rename from crates/sinew-core/src/error.rs rename to crates/claakecode-core/src/error.rs diff --git a/crates/sinew-core/src/lib.rs b/crates/claakecode-core/src/lib.rs similarity index 100% rename from crates/sinew-core/src/lib.rs rename to crates/claakecode-core/src/lib.rs diff --git a/crates/sinew-core/src/message.rs b/crates/claakecode-core/src/message.rs similarity index 100% rename from crates/sinew-core/src/message.rs rename to crates/claakecode-core/src/message.rs diff --git a/crates/sinew-core/src/model.rs b/crates/claakecode-core/src/model.rs similarity index 78% rename from crates/sinew-core/src/model.rs rename to crates/claakecode-core/src/model.rs index f3409e56..ad961f33 100644 --- a/crates/sinew-core/src/model.rs +++ b/crates/claakecode-core/src/model.rs @@ -6,6 +6,8 @@ pub struct ModelRef { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub effort: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub use_1m_context: Option, } impl ModelRef { @@ -14,6 +16,7 @@ impl ModelRef { provider: provider.into(), name: name.into(), effort: None, + use_1m_context: None, } } @@ -21,6 +24,15 @@ impl ModelRef { self.effort = Some(effort); self } + + pub fn with_use_1m_context(mut self, value: bool) -> Self { + self.use_1m_context = Some(value); + self + } + + pub fn use_1m_context_enabled(&self) -> bool { + self.use_1m_context.unwrap_or(false) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/crates/sinew-core/src/provider.rs b/crates/claakecode-core/src/provider.rs similarity index 100% rename from crates/sinew-core/src/provider.rs rename to crates/claakecode-core/src/provider.rs diff --git a/crates/sinew-core/src/stream.rs b/crates/claakecode-core/src/stream.rs similarity index 100% rename from crates/sinew-core/src/stream.rs rename to crates/claakecode-core/src/stream.rs diff --git a/crates/sinew-core/src/tool.rs b/crates/claakecode-core/src/tool.rs similarity index 100% rename from crates/sinew-core/src/tool.rs rename to crates/claakecode-core/src/tool.rs diff --git a/crates/sinew-anthropic/Cargo.toml b/crates/claakecode-google/Cargo.toml similarity index 84% rename from crates/sinew-anthropic/Cargo.toml rename to crates/claakecode-google/Cargo.toml index f94aba87..41af3bdf 100644 --- a/crates/sinew-anthropic/Cargo.toml +++ b/crates/claakecode-google/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "sinew-anthropic" +name = "claakecode-google" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Anthropic provider for sinew" +description = "Google provider for ClaakeCode" [dependencies] -sinew-core = { workspace = true } +claakecode-core = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } diff --git a/crates/sinew-google/src/auth.rs b/crates/claakecode-google/src/auth.rs similarity index 98% rename from crates/sinew-google/src/auth.rs rename to crates/claakecode-google/src/auth.rs index 68c25524..d9530673 100644 --- a/crates/sinew-google/src/auth.rs +++ b/crates/claakecode-google/src/auth.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tokio::sync::Mutex; -use sinew_core::{AppError, Result}; +use claakecode_core::{AppError, Result}; const GOOGLE_OAUTH_AUTHORIZE_URL: &str = "https://accounts.google.com/o/oauth2/v2/auth"; const GOOGLE_OAUTH_TOKEN_URL: &str = "https://oauth2.googleapis.com/token"; @@ -144,10 +144,10 @@ impl Credential { } pub fn load_default() -> Result> { - Self::from_sinew_auth_file(&default_auth_path()?) + Self::from_claakecode_auth_file(&default_auth_path()?) } - pub fn from_sinew_auth_file(path: &Path) -> Result> { + pub fn from_claakecode_auth_file(path: &Path) -> Result> { let bytes = match std::fs::read(path) { Ok(bytes) => bytes, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), @@ -320,7 +320,7 @@ fn persist_refresh( } pub fn default_auth_path() -> Result { - let dirs = ProjectDirs::from("dev", "hyrak", "sinew") + let dirs = ProjectDirs::from("dev", "williampeynichou", "claakecode") .ok_or_else(|| AppError::Auth("unable to resolve local data directory".into()))?; Ok(dirs.data_local_dir().join("google-auth.json")) } diff --git a/crates/sinew-google/src/client.rs b/crates/claakecode-google/src/client.rs similarity index 99% rename from crates/sinew-google/src/client.rs rename to crates/claakecode-google/src/client.rs index ac60dceb..2449b747 100644 --- a/crates/sinew-google/src/client.rs +++ b/crates/claakecode-google/src/client.rs @@ -2,7 +2,7 @@ use std::time::Duration; use async_trait::async_trait; use serde_json::{json, Value}; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest, ProviderStream, Result, Role, TokenEstimate, ToolDescriptor, }; @@ -21,7 +21,7 @@ const BASE_URL: &str = "https://daily-cloudcode-pa.googleapis.com/v1internal"; const PROD_BASE_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal"; const SANDBOX_BASE_URL: &str = "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal"; const AUTOPUSH_BASE_URL: &str = "https://autopush-cloudcode-pa.sandbox.googleapis.com/v1internal"; -const USER_AGENT: &str = "sinew/0.1"; +const USER_AGENT: &str = "claakecode/0.1"; const DEFAULT_ANTIGRAVITY_VERSION: &str = "2.0.0"; const ANTIGRAVITY_SYSTEM_INSTRUCTION: &str = "You are Antigravity, a powerful agentic AI coding assistant designed by the Google Deepmind team working on Advanced Agentic Coding.You are pair programming with a USER to solve their coding task. The task may require creating a new codebase, modifying or debugging an existing codebase, or simply answering a question.**Absolute paths only****Proactiveness**"; const FALLBACK_PROJECT_ID: &str = "rising-fact-p41fc"; diff --git a/crates/sinew-google/src/lib.rs b/crates/claakecode-google/src/lib.rs similarity index 100% rename from crates/sinew-google/src/lib.rs rename to crates/claakecode-google/src/lib.rs diff --git a/crates/sinew-google/src/model_info.rs b/crates/claakecode-google/src/model_info.rs similarity index 98% rename from crates/sinew-google/src/model_info.rs rename to crates/claakecode-google/src/model_info.rs index 63a30d7b..004e2257 100644 --- a/crates/sinew-google/src/model_info.rs +++ b/crates/claakecode-google/src/model_info.rs @@ -1,4 +1,4 @@ -use sinew_core::{Effort, EffortMode, ModelCapabilities, ModelRef}; +use claakecode_core::{Effort, EffortMode, ModelCapabilities, ModelRef}; pub const MODEL_ID: &str = "gemini-3.1-pro"; pub const GEMINI_WINDOW: u32 = 1_048_576; diff --git a/crates/sinew-google/src/stream.rs b/crates/claakecode-google/src/stream.rs similarity index 99% rename from crates/sinew-google/src/stream.rs rename to crates/claakecode-google/src/stream.rs index 94a070d4..26476435 100644 --- a/crates/sinew-google/src/stream.rs +++ b/crates/claakecode-google/src/stream.rs @@ -2,7 +2,7 @@ use eventsource_stream::Eventsource; use futures::{stream::Stream, StreamExt}; use serde_json::{json, Value}; -use sinew_core::{ +use claakecode_core::{ AppError, PartKind, ProviderStream, StopReason, StreamEvent, ToolCallIntro, Usage, }; diff --git a/crates/sinew-google/src/wire.rs b/crates/claakecode-google/src/wire.rs similarity index 100% rename from crates/sinew-google/src/wire.rs rename to crates/claakecode-google/src/wire.rs diff --git a/crates/sinew-kimi/Cargo.toml b/crates/claakecode-kimi/Cargo.toml similarity index 83% rename from crates/sinew-kimi/Cargo.toml rename to crates/claakecode-kimi/Cargo.toml index 775403b5..ec562bb2 100644 --- a/crates/sinew-kimi/Cargo.toml +++ b/crates/claakecode-kimi/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "sinew-kimi" +name = "claakecode-kimi" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Kimi provider for Sinew" +description = "Kimi provider for ClaakeCode" [dependencies] -sinew-core = { workspace = true } +claakecode-core = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } diff --git a/crates/sinew-kimi/src/auth.rs b/crates/claakecode-kimi/src/auth.rs similarity index 98% rename from crates/sinew-kimi/src/auth.rs rename to crates/claakecode-kimi/src/auth.rs index eac53daa..723eafc7 100644 --- a/crates/sinew-kimi/src/auth.rs +++ b/crates/claakecode-kimi/src/auth.rs @@ -10,7 +10,7 @@ use rand::RngCore; use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; -use sinew_core::{AppError, Result}; +use claakecode_core::{AppError, Result}; const KIMI_OAUTH_HOST: &str = "https://auth.kimi.com"; const KIMI_CLIENT_ID: &str = "17e5f671-d194-4dfb-9706-5516cb48c098"; @@ -104,10 +104,10 @@ impl Credential { } pub fn load_default() -> Result> { - Self::from_sinew_auth_file(&default_auth_path()?) + Self::from_claakecode_auth_file(&default_auth_path()?) } - pub fn from_sinew_auth_file(path: &Path) -> Result> { + pub fn from_claakecode_auth_file(path: &Path) -> Result> { let bytes = match std::fs::read(path) { Ok(bytes) => bytes, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), @@ -425,7 +425,7 @@ fn default_device_id_path() -> Result { } fn data_local_dir() -> Result { - let dirs = ProjectDirs::from("dev", "hyrak", "sinew") + let dirs = ProjectDirs::from("dev", "williampeynichou", "claakecode") .ok_or_else(|| AppError::Auth("unable to resolve local data directory".into()))?; Ok(dirs.data_local_dir().to_path_buf()) } @@ -524,7 +524,7 @@ pub(crate) fn common_headers() -> Result> { fn device_name() -> String { std::env::var("HOSTNAME") .or_else(|_| std::env::var("COMPUTERNAME")) - .unwrap_or_else(|_| "sinew".into()) + .unwrap_or_else(|_| "claakecode".into()) } fn device_model() -> String { diff --git a/crates/sinew-kimi/src/client.rs b/crates/claakecode-kimi/src/client.rs similarity index 99% rename from crates/sinew-kimi/src/client.rs rename to crates/claakecode-kimi/src/client.rs index c3e6f92c..fed10c5e 100644 --- a/crates/sinew-kimi/src/client.rs +++ b/crates/claakecode-kimi/src/client.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use serde::Serialize; use serde_json::Value; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, Effort, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest, ProviderStream, Result, Role, TokenEstimate, ToolDescriptor, }; @@ -14,7 +14,7 @@ use crate::{ }; const BASE_URL: &str = "https://api.kimi.com/coding/v1"; -const USER_AGENT: &str = "KimiCLI/0.1 Sinew/0.1"; +const USER_AGENT: &str = "KimiCLI/0.1 ClaakeCode/0.1"; #[derive(Clone)] pub struct KimiConfig { diff --git a/crates/sinew-kimi/src/lib.rs b/crates/claakecode-kimi/src/lib.rs similarity index 100% rename from crates/sinew-kimi/src/lib.rs rename to crates/claakecode-kimi/src/lib.rs diff --git a/crates/sinew-kimi/src/model_info.rs b/crates/claakecode-kimi/src/model_info.rs similarity index 89% rename from crates/sinew-kimi/src/model_info.rs rename to crates/claakecode-kimi/src/model_info.rs index 57911ca5..297f7e0f 100644 --- a/crates/sinew-kimi/src/model_info.rs +++ b/crates/claakecode-kimi/src/model_info.rs @@ -1,4 +1,4 @@ -use sinew_core::{EffortMode, ModelCapabilities, ModelRef}; +use claakecode_core::{EffortMode, ModelCapabilities, ModelRef}; pub const MODEL_ID: &str = "kimi-for-coding"; pub const MODEL_WINDOW: u32 = 256_000; diff --git a/crates/sinew-kimi/src/stream.rs b/crates/claakecode-kimi/src/stream.rs similarity index 99% rename from crates/sinew-kimi/src/stream.rs rename to crates/claakecode-kimi/src/stream.rs index 1c879767..0a006253 100644 --- a/crates/sinew-kimi/src/stream.rs +++ b/crates/claakecode-kimi/src/stream.rs @@ -4,7 +4,7 @@ use eventsource_stream::Eventsource; use futures::{stream::Stream, StreamExt}; use serde_json::{json, Value}; -use sinew_core::{ +use claakecode_core::{ AppError, PartKind, ProviderStream, StopReason, StreamEvent, ToolCallIntro, Usage, }; diff --git a/crates/sinew-kimi/src/wire.rs b/crates/claakecode-kimi/src/wire.rs similarity index 100% rename from crates/sinew-kimi/src/wire.rs rename to crates/claakecode-kimi/src/wire.rs diff --git a/crates/sinew-openai/Cargo.toml b/crates/claakecode-openai/Cargo.toml similarity index 84% rename from crates/sinew-openai/Cargo.toml rename to crates/claakecode-openai/Cargo.toml index 7a50abca..61be86c0 100644 --- a/crates/sinew-openai/Cargo.toml +++ b/crates/claakecode-openai/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "sinew-openai" +name = "claakecode-openai" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "OpenAI provider for sinew" +description = "OpenAI provider for ClaakeCode" [dependencies] -sinew-core = { workspace = true } +claakecode-core = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } diff --git a/crates/sinew-openai/src/auth.rs b/crates/claakecode-openai/src/auth.rs similarity index 98% rename from crates/sinew-openai/src/auth.rs rename to crates/claakecode-openai/src/auth.rs index 5c6f87d3..2bb89e52 100644 --- a/crates/sinew-openai/src/auth.rs +++ b/crates/claakecode-openai/src/auth.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tokio::sync::Mutex; -use sinew_core::{AppError, Result}; +use claakecode_core::{AppError, Result}; const OPENAI_OAUTH_AUTHORIZE_URL: &str = "https://auth.openai.com/oauth/authorize"; const OPENAI_OAUTH_TOKEN_URL: &str = "https://auth.openai.com/oauth/token"; @@ -119,10 +119,10 @@ impl Credential { } pub fn load_default() -> Result> { - Self::from_sinew_auth_file(&default_auth_path()?) + Self::from_claakecode_auth_file(&default_auth_path()?) } - pub fn from_sinew_auth_file(path: &Path) -> Result> { + pub fn from_claakecode_auth_file(path: &Path) -> Result> { let bytes = match std::fs::read(path) { Ok(bytes) => bytes, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), @@ -319,7 +319,7 @@ fn persist_refresh( } pub fn default_auth_path() -> Result { - let dirs = ProjectDirs::from("dev", "hyrak", "sinew") + let dirs = ProjectDirs::from("dev", "williampeynichou", "claakecode") .ok_or_else(|| AppError::Auth("unable to resolve local data directory".into()))?; Ok(dirs.data_local_dir().join("openai-auth.json")) } @@ -378,7 +378,7 @@ pub fn oauth_authorize_url(redirect_uri: &str, pkce: &PkceCodes, state: &str) -> .append_pair("id_token_add_organizations", "true") .append_pair("codex_cli_simplified_flow", "true") .append_pair("state", state) - .append_pair("originator", "sinew_desktop"); + .append_pair("originator", "claakecode_desktop"); format!("{OPENAI_OAUTH_AUTHORIZE_URL}?{}", serializer.finish()) } diff --git a/crates/sinew-openai/src/client.rs b/crates/claakecode-openai/src/client.rs similarity index 99% rename from crates/sinew-openai/src/client.rs rename to crates/claakecode-openai/src/client.rs index 2a06aafd..b46bea36 100644 --- a/crates/sinew-openai/src/client.rs +++ b/crates/claakecode-openai/src/client.rs @@ -3,7 +3,7 @@ use eventsource_stream::Eventsource; use futures::stream::Stream; use futures::{stream, StreamExt}; use serde_json::Value; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, Effort, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest, ProviderStream, Result, Role, ServiceTier, StreamEvent, TokenEstimate, ToolDescriptor, }; @@ -12,8 +12,8 @@ use crate::{auth::Credential, model_info, stream::EventParser, wire}; const API_BASE_URL: &str = "https://api.openai.com/v1"; const CODEX_BASE_URL: &str = "https://chatgpt.com/backend-api/codex"; -const USER_AGENT: &str = "sinew/0.1"; -const FALLBACK_INSTRUCTIONS: &str = "You are Sinew, a concise coding assistant."; +const USER_AGENT: &str = "ClaakeCode/0.1"; +const FALLBACK_INSTRUCTIONS: &str = "You are Claake Code, a concise coding assistant."; const SSE_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300); #[derive(Clone)] @@ -616,7 +616,7 @@ async fn read_http_error(response: reqwest::Response) -> AppError { #[cfg(test)] mod tests { use serde_json::json; - use sinew_core::{ + use claakecode_core::{ ChatMessage, ModelRef, Part, ProviderRequest, Role, ServiceTier, ToolResultImage, }; diff --git a/crates/sinew-openai/src/lib.rs b/crates/claakecode-openai/src/lib.rs similarity index 100% rename from crates/sinew-openai/src/lib.rs rename to crates/claakecode-openai/src/lib.rs diff --git a/crates/sinew-openai/src/model_info.rs b/crates/claakecode-openai/src/model_info.rs similarity index 97% rename from crates/sinew-openai/src/model_info.rs rename to crates/claakecode-openai/src/model_info.rs index a8d6b402..cd64cac5 100644 --- a/crates/sinew-openai/src/model_info.rs +++ b/crates/claakecode-openai/src/model_info.rs @@ -1,4 +1,4 @@ -use sinew_core::{EffortMode, ModelCapabilities, ModelRef}; +use claakecode_core::{EffortMode, ModelCapabilities, ModelRef}; pub const MODEL_ID: &str = "gpt-5.5"; pub const MODEL_WINDOW: u32 = 272_000; diff --git a/crates/sinew-openai/src/stream.rs b/crates/claakecode-openai/src/stream.rs similarity index 99% rename from crates/sinew-openai/src/stream.rs rename to crates/claakecode-openai/src/stream.rs index 598b60d5..61be82d0 100644 --- a/crates/sinew-openai/src/stream.rs +++ b/crates/claakecode-openai/src/stream.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use serde_json::{json, Value}; -use sinew_core::{AppError, PartKind, StopReason, StreamEvent, ToolCallIntro, Usage}; +use claakecode_core::{AppError, PartKind, StopReason, StreamEvent, ToolCallIntro, Usage}; pub(crate) struct EventParser { default_model: String, diff --git a/crates/sinew-openai/src/wire.rs b/crates/claakecode-openai/src/wire.rs similarity index 100% rename from crates/sinew-openai/src/wire.rs rename to crates/claakecode-openai/src/wire.rs diff --git a/crates/sinew-openrouter/Cargo.toml b/crates/claakecode-openrouter/Cargo.toml similarity index 78% rename from crates/sinew-openrouter/Cargo.toml rename to crates/claakecode-openrouter/Cargo.toml index 59256ee5..ecc27291 100644 --- a/crates/sinew-openrouter/Cargo.toml +++ b/crates/claakecode-openrouter/Cargo.toml @@ -1,13 +1,13 @@ [package] -name = "sinew-openrouter" +name = "claakecode-openrouter" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "OpenRouter provider integration for Sinew" +description = "OpenRouter provider integration for ClaakeCode" [dependencies] -sinew-core = { workspace = true } +claakecode-core = { workspace = true } async-trait = { workspace = true } bytes = { workspace = true } diff --git a/crates/sinew-openrouter/src/auth.rs b/crates/claakecode-openrouter/src/auth.rs similarity index 96% rename from crates/sinew-openrouter/src/auth.rs rename to crates/claakecode-openrouter/src/auth.rs index ecd915df..6a15b963 100644 --- a/crates/sinew-openrouter/src/auth.rs +++ b/crates/claakecode-openrouter/src/auth.rs @@ -6,7 +6,7 @@ use std::{ use directories::ProjectDirs; use serde::{Deserialize, Serialize}; -use sinew_core::{AppError, Result}; +use claakecode_core::{AppError, Result}; use crate::model_info::PROVIDER_ID; @@ -57,10 +57,10 @@ impl Credential { } pub fn load_default() -> Result> { - Self::from_sinew_auth_file(&default_auth_path()?) + Self::from_claakecode_auth_file(&default_auth_path()?) } - pub fn from_sinew_auth_file(path: &Path) -> Result> { + pub fn from_claakecode_auth_file(path: &Path) -> Result> { let bytes = match std::fs::read(path) { Ok(bytes) => bytes, Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), @@ -149,7 +149,7 @@ pub fn delete_default_auth() -> Result<()> { } fn default_auth_path() -> Result { - let dirs = ProjectDirs::from("dev", "hyrak", "sinew") + let dirs = ProjectDirs::from("dev", "williampeynichou", "claakecode") .ok_or_else(|| AppError::Auth("unable to resolve local data directory".into()))?; Ok(dirs.data_local_dir().join("openrouter-auth.json")) } diff --git a/crates/sinew-openrouter/src/client.rs b/crates/claakecode-openrouter/src/client.rs similarity index 99% rename from crates/sinew-openrouter/src/client.rs rename to crates/claakecode-openrouter/src/client.rs index 10f7a4eb..41a2430a 100644 --- a/crates/sinew-openrouter/src/client.rs +++ b/crates/claakecode-openrouter/src/client.rs @@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use serde_json::Value; -use sinew_core::{ +use claakecode_core::{ AppError, ChatMessage, Effort, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest, ProviderStream, Result, Role, TokenEstimate, ToolDescriptor, }; @@ -16,9 +16,9 @@ use crate::{ }; const BASE_URL: &str = "https://openrouter.ai/api/v1"; -const USER_AGENT: &str = "Sinew/0.1"; -const APP_REFERER: &str = "https://github.com/Paseru/sinew"; -const APP_TITLE: &str = "Sinew"; +const USER_AGENT: &str = "ClaakeCode/0.1"; +const APP_REFERER: &str = "https://github.com/WilliamPeynichou/ClaakeCode"; +const APP_TITLE: &str = "Claake Code"; const CACHE_BREAKPOINTS: usize = 4; #[derive(Clone)] @@ -888,7 +888,7 @@ fn _capabilities_for_catalog(model: &OpenRouterCatalogModel) -> ModelCapabilitie #[cfg(test)] mod tests { use serde_json::json; - use sinew_core::{ChatMessage, ModelCapabilities, ModelRef, ProviderRequest}; + use claakecode_core::{ChatMessage, ModelCapabilities, ModelRef, ProviderRequest}; use super::{build_chat_request, cache_mode_for_model, model_info, CacheMode}; diff --git a/crates/sinew-openrouter/src/lib.rs b/crates/claakecode-openrouter/src/lib.rs similarity index 100% rename from crates/sinew-openrouter/src/lib.rs rename to crates/claakecode-openrouter/src/lib.rs diff --git a/crates/sinew-openrouter/src/model_info.rs b/crates/claakecode-openrouter/src/model_info.rs similarity index 95% rename from crates/sinew-openrouter/src/model_info.rs rename to crates/claakecode-openrouter/src/model_info.rs index e049b4be..0a123af7 100644 --- a/crates/sinew-openrouter/src/model_info.rs +++ b/crates/claakecode-openrouter/src/model_info.rs @@ -1,4 +1,4 @@ -use sinew_core::{EffortMode, ModelCapabilities, ModelRef}; +use claakecode_core::{EffortMode, ModelCapabilities, ModelRef}; use crate::client::OpenRouterCatalogModel; diff --git a/crates/sinew-openrouter/src/stream.rs b/crates/claakecode-openrouter/src/stream.rs similarity index 99% rename from crates/sinew-openrouter/src/stream.rs rename to crates/claakecode-openrouter/src/stream.rs index 34f9e097..d9a3f76a 100644 --- a/crates/sinew-openrouter/src/stream.rs +++ b/crates/claakecode-openrouter/src/stream.rs @@ -4,7 +4,7 @@ use eventsource_stream::Eventsource; use futures::{stream::Stream, StreamExt}; use serde_json::{json, Value}; -use sinew_core::{ +use claakecode_core::{ AppError, PartKind, ProviderStream, StopReason, StreamEvent, ToolCallIntro, Usage, }; diff --git a/crates/sinew-openrouter/src/wire.rs b/crates/claakecode-openrouter/src/wire.rs similarity index 100% rename from crates/sinew-openrouter/src/wire.rs rename to crates/claakecode-openrouter/src/wire.rs diff --git a/index.html b/index.html index 863d5263..d856e9d7 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - Sinew + Claake Code - #141518 + #fff \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index d8d97469..d4282098 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 33790bbd..3445f67a 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index bdfc74d1..9bae3487 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png index 74e98685..f2c82d19 100644 Binary files a/src-tauri/icons/ios/AppIcon-20x20@1x.png and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png index 05f822a2..789a0dd8 100644 Binary files a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png index 05f822a2..789a0dd8 100644 Binary files a/src-tauri/icons/ios/AppIcon-20x20@2x.png and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png index d7c35cce..96dc9343 100644 Binary files a/src-tauri/icons/ios/AppIcon-20x20@3x.png and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png index 3f02a092..b0795adb 100644 Binary files a/src-tauri/icons/ios/AppIcon-29x29@1x.png and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png index 934de177..246cd876 100644 Binary files a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png index 934de177..246cd876 100644 Binary files a/src-tauri/icons/ios/AppIcon-29x29@2x.png and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png index 0507b73d..f07d83fc 100644 Binary files a/src-tauri/icons/ios/AppIcon-29x29@3x.png and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png index 05f822a2..789a0dd8 100644 Binary files a/src-tauri/icons/ios/AppIcon-40x40@1x.png and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png index 851eb46d..d0ac8280 100644 Binary files a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png index 851eb46d..d0ac8280 100644 Binary files a/src-tauri/icons/ios/AppIcon-40x40@2x.png and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png index a917a251..db84d51c 100644 Binary files a/src-tauri/icons/ios/AppIcon-40x40@3x.png and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png index 35840c01..cb723ff4 100644 Binary files a/src-tauri/icons/ios/AppIcon-512@2x.png and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png index a917a251..db84d51c 100644 Binary files a/src-tauri/icons/ios/AppIcon-60x60@2x.png and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png index ca635b76..aa22d671 100644 Binary files a/src-tauri/icons/ios/AppIcon-60x60@3x.png and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png index f1564cab..2c935cc0 100644 Binary files a/src-tauri/icons/ios/AppIcon-76x76@1x.png and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png index de17a771..8d9a6da2 100644 Binary files a/src-tauri/icons/ios/AppIcon-76x76@2x.png and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png index 4bb74c1e..e69760c5 100644 Binary files a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/context.rs b/src-tauri/src/context.rs index 5bbcc360..00a027d3 100644 --- a/src-tauri/src/context.rs +++ b/src-tauri/src/context.rs @@ -34,8 +34,12 @@ pub(super) async fn estimate_context( let mode = plan_estimate_mode(&conversation.plan_workflow, requested_mode); let mode_model_settings = conversation.mode_model_settings.clone(); - let selected_model = - model_with_optional_selection(mode_model_settings.get(mode), input.model, input.thinking); + let selected_model = model_with_optional_selection( + mode_model_settings.get(mode), + input.model, + input.thinking, + input.use_1m_context, + ); conversation.mode_model_settings = mode_model_settings; conversation.model = selected_model; let provider = provider_from_registry(&state, &conversation.model.provider)?; @@ -73,16 +77,19 @@ pub(super) async fn estimate_context( mcp_settings, tool_settings.clone(), skill_settings, + DatabaseTool::new(state.store.clone()), state.max_tool_rounds, None, TurnCancel::empty(), ) .descriptors(); let team_tools = TeamTool::descriptors_static(); + let database_tools = DatabaseTool::descriptors_static(); let mut sub_agent_tool_names = tool_name_set(&sub_agent_tools); sub_agent_tool_names.extend(tool_name_set(&team_tools)); tools.extend(sub_agent_tools); tools.extend(team_tools); + tools.extend(database_tools); let tools = tool_settings.apply_to_descriptors(tools); let system = system_prompt_with_todo(&effective_system_prompt, &conversation.todo_list); let system_prompt = diff --git a/src-tauri/src/conversations.rs b/src-tauri/src/conversations.rs index a30fdb95..3410ef51 100644 --- a/src-tauri/src/conversations.rs +++ b/src-tauri/src/conversations.rs @@ -170,6 +170,7 @@ pub(super) async fn set_conversation_model_preference( conversation.mode_model_settings.get(mode), input.model, input.thinking, + input.use_1m_context, ); let provider = provider_from_registry(&state, &selected.provider)?; provider @@ -242,6 +243,87 @@ pub(super) async fn save_tool_settings( Ok(tool_settings_view(&saved, &catalog)) } +#[tauri::command] +pub(super) async fn list_database_settings( + state: State<'_, DesktopState>, +) -> std::result::Result { + state + .store + .load_database_settings() + .map_err(error_to_string) +} + +#[tauri::command] +pub(super) async fn save_database_settings( + app: AppHandle, + state: State<'_, DesktopState>, + input: SaveDatabaseSettingsInput, +) -> std::result::Result { + let saved = state + .store + .save_database_settings(&input.settings) + .map_err(error_to_string)?; + emit_database_sources_changed(&app, &saved); + Ok(saved) +} + +#[tauri::command] +pub(super) async fn test_database_connection( + app: AppHandle, + state: State<'_, DesktopState>, + input: TestDatabaseConnectionInput, +) -> std::result::Result { + let source = input.source.normalized(); + let result = test_database_source_connection(source).await; + + let mut settings = state + .store + .load_database_settings() + .map_err(error_to_string)?; + let mut changed = false; + for configured in &mut settings.sources { + if configured.id == result.source_id { + configured.last_connection_status = DatabaseConnectionStatus::from_test(&result); + changed = true; + break; + } + } + if changed { + if let Ok(saved) = state.store.save_database_settings(&settings) { + emit_database_sources_changed(&app, &saved); + } + } + + Ok(result) +} + +#[tauri::command] +pub(super) async fn list_database_source_activity( + state: State<'_, DesktopState>, + input: ListDatabaseSourceActivityInput, +) -> std::result::Result, String> { + state + .store + .list_database_source_activity(&input.source_id, input.limit) + .map_err(error_to_string) +} + +#[tauri::command] +pub(super) async fn clear_database_source_activity( + app: AppHandle, + state: State<'_, DesktopState>, + input: ClearDatabaseSourceActivityInput, +) -> std::result::Result, String> { + let entries = state + .store + .clear_database_source_activity(&input.source_id) + .map_err(error_to_string)?; + if let Ok(settings) = state.store.load_database_settings() { + emit_database_sources_changed(&app, &settings); + } + Ok(entries) +} + #[tauri::command] pub(super) async fn list_sub_agent_settings( state: State<'_, DesktopState>, @@ -272,7 +354,7 @@ pub(super) async fn save_sub_agent_settings( #[tauri::command] pub(super) async fn probe_mcp_tools( state: State<'_, DesktopState>, -) -> std::result::Result, String> { +) -> std::result::Result, String> { let settings = state.store.load_mcp_settings().map_err(error_to_string)?; Ok(probe_mcp_servers(&settings).await) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 027b395c..c8c5ccd7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,7 +25,7 @@ use objc2_foundation::NSString; use portable_pty::{native_pty_system, Child, ChildKiller, CommandBuilder, MasterPty, PtySize}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use sinew_anthropic::{ +use claakecode_anthropic::{ delete_default_auth as delete_default_anthropic_auth, exchange_oauth_code as exchange_anthropic_oauth_code, generate_pkce as generate_anthropic_pkce, generate_state as generate_anthropic_state, @@ -33,9 +33,9 @@ use sinew_anthropic::{ oauth_authorize_url as anthropic_oauth_authorize_url, AnthropicAuthStatus, AnthropicProvider, PkceCodes as AnthropicPkceCodes, MODEL_ID as ANTHROPIC_MODEL_ID, }; -use sinew_app::{ +use claakecode_app::{ checkpoint_from_snapshots, clean_context_descriptor, compact_conversation_history, - copy_workspace_entries, create_installed_skill, create_workspace_directory, + copy_workspace_entries, create_workspace_directory, create_workspace_file, delete_workspace_entry, import_workspace_paths, list_installed_skills, list_workspace_entries, list_workspace_files, normalize_workspace_root, probe_mcp_servers, read_external_file, read_workspace_file, rename_workspace_entry, resolve_terminal_path, @@ -43,20 +43,23 @@ use sinew_app::{ shell_system_prompt, snapshot_workspace_for_checkpoint, subagent_system_prompt, system_prompt_for_mode_with_plan_prompt, system_prompt_with_todo, todo_list_from_history, tool_settings_view, trash_workspace_entry, write_workspace_file, AgentEvent, AgentMode, - AppStore, BashTool, ConversationEvent, ConversationSummary, CreateImageTool, EditFileTool, - GlobTool, GoalWorkflowState, GrepTool, ImportedEntry, InstalledSkill, McpSettings, + AppStore, BashTool, ConversationEvent, ConversationSummary, CreateImageTool, + DatabaseActivityEntry, DatabaseConnectionStatus, DatabaseConnectionTestResult, + DatabaseSettings, DatabaseSourceConfig, DatabaseTool, EditFileTool, GlobTool, + GoalWorkflowState, GrepTool, ImportedEntry, InstalledSkill, McpSettings, McpToolRegistry, ModeModelSettings, OpenRouterModelRecord, PlanArtifactState, PlanWorkflowState, QuestionTool, ReadTool, SavedConversation, SkillSettings, SkillTool, SubAgentConfig, SubAgentSettings, SubAgentTool, TeamRuntime, TeamTool, TerminalPathResolution, ToDoListTool, TodoListState, ToolSettings, ToolSettingsView, TurnCancel, TurnContext, WebFetchTool, WebSearchTool, WorkspaceBootstrap, WorkspaceCopyOperation, WorkspaceDeletedEntry, WorkspaceFileChangeEvent, WorkspaceSearchResult, WriteFileTool, + test_database_source_connection, }; -use sinew_core::{ +use claakecode_core::{ ChatMessage, Effort, ModelCapabilities, ModelRef, Part, Provider, ProviderRequest, Role, ServiceTier, ToolDescriptor, }; -use sinew_google::{ +use claakecode_google::{ delete_default_auth as delete_default_google_auth, exchange_oauth_code as exchange_google_oauth_code, generate_pkce as generate_google_pkce, generate_state as generate_google_state, @@ -65,7 +68,7 @@ use sinew_google::{ purge_legacy_oauth_if_needed as purge_legacy_google_oauth, GoogleAuthStatus, GoogleProvider, PkceCodes as GooglePkceCodes, MODEL_ID as GOOGLE_MODEL_ID, }; -use sinew_kimi::{ +use claakecode_kimi::{ delete_default_auth as delete_default_kimi_auth, generate_state as generate_kimi_state, load_default_auth_status as load_default_kimi_auth_status, request_device_authorization as request_kimi_device_authorization, @@ -73,12 +76,12 @@ use sinew_kimi::{ DeviceAuthorization as KimiDeviceAuthorization, KimiAuthStatus, KimiProvider, MODEL_ID as KIMI_MODEL_ID, }; -use sinew_openai::{ +use claakecode_openai::{ delete_default_auth, exchange_oauth_code, generate_pkce, generate_state, load_default_auth_status, oauth_authorize_url, OpenAiAuthStatus, OpenAiProvider, PkceCodes, MODEL_ID as OPENAI_MODEL_ID, }; -use sinew_openrouter::{ +use claakecode_openrouter::{ delete_default_auth as delete_default_openrouter_auth, fetch_model_catalog as fetch_openrouter_model_catalog, load_default_api_key as load_default_openrouter_api_key, @@ -279,7 +282,7 @@ pub fn run() { workspace::read_external_file_command, workspace::delete_skill_command, workspace::create_skill_command, - workspace::update_skill_content_command, + workspace::update_skill_command, workspace::open_external_url_command, workspace::open_path_with_default_app_command, workspace::copy_file_to_path_command, @@ -300,6 +303,11 @@ pub fn run() { conversations::save_tool_settings, conversations::list_sub_agent_settings, conversations::save_sub_agent_settings, + conversations::list_database_settings, + conversations::save_database_settings, + conversations::test_database_connection, + conversations::list_database_source_activity, + conversations::clear_database_source_activity, providers::list_configured_model_providers, providers::get_openai_provider_status, providers::start_openai_oauth_login, @@ -357,7 +365,7 @@ pub fn run() { updater::updater_current_version, ]) .build(tauri::generate_context!()) - .expect("error while building sinew desktop") + .expect("error while building claakecode desktop") .run(|app, event| { #[cfg(not(target_os = "macos"))] let _ = (&app, &event); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a376ad88..5af8ff27 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,5 +5,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - sinew_desktop_lib::run() + claakecode_desktop_lib::run() } diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index b4cb0ec9..aa214786 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -268,6 +268,8 @@ pub(super) struct SendMessageInput { pub(super) attachments: Vec, pub(super) model: Option, pub(super) thinking: Option, + #[serde(default)] + pub(super) use_1m_context: Option, pub(super) mode: Option, pub(super) service_tier: Option, pub(super) plan_control: Option, @@ -287,6 +289,8 @@ pub(super) struct CompactConversationInput { pub(super) thinking: Option, pub(super) service_tier: Option, #[serde(default)] + pub(super) use_1m_context: Option, + #[serde(default)] pub(super) instruction: Option, } @@ -301,6 +305,8 @@ pub(super) struct ContextEstimateInput { pub(super) attachments: Vec, pub(super) model: Option, pub(super) thinking: Option, + #[serde(default)] + pub(super) use_1m_context: Option, pub(super) mode: Option, #[serde(default)] pub(super) rewrite_from_history_index: Option, @@ -334,6 +340,8 @@ pub(super) struct ConversationModelPreferenceInput { pub(super) mode: AgentModeInput, pub(super) model: Option, pub(super) thinking: Option, + #[serde(default)] + pub(super) use_1m_context: Option, } #[derive(Debug, Serialize)] @@ -391,6 +399,39 @@ pub(super) struct SaveToolSettingsInput { pub(super) settings: ToolSettings, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct SaveDatabaseSettingsInput { + pub(super) settings: DatabaseSettings, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct TestDatabaseConnectionInput { + pub(super) source: DatabaseSourceConfig, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct ListDatabaseSourceActivityInput { + pub(super) source_id: String, + #[serde(default)] + pub(super) limit: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct ClearDatabaseSourceActivityInput { + pub(super) source_id: String, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct DatabaseSourcesChangedPayload { + pub(super) active_count: usize, + pub(super) source_count: usize, +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub(super) struct SaveSkillSettingsInput { @@ -444,9 +485,12 @@ pub(super) struct OpenRouterModelCandidateInput { } #[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] pub(super) struct ModelInput { pub(super) provider: String, pub(super) name: String, + #[serde(default)] + pub(super) use_1m_context: Option, } fn default_true() -> bool { diff --git a/src-tauri/src/platform.rs b/src-tauri/src/platform.rs index a1d7fa7c..a4b75051 100644 --- a/src-tauri/src/platform.rs +++ b/src-tauri/src/platform.rs @@ -8,7 +8,7 @@ pub(super) fn create_new_window(app: &AppHandle) -> Result<()> { let label = next_window_label(app); let mut builder = WebviewWindowBuilder::new(app, label, WebviewUrl::App(PathBuf::from(NEW_WINDOW_URL))) - .title("Sinew") + .title("Claake Code") .inner_size(1500.0, 940.0) .min_inner_size(1100.0, 720.0) .resizable(true) @@ -63,7 +63,7 @@ pub(super) fn focus_existing_window(app: &AppHandle) -> bool { /// never shows a blank entry. pub(super) fn apply_window_title(window: &tauri::WebviewWindow, folder_name: &str) { let trimmed = folder_name.trim(); - let title = if trimmed.is_empty() { "Sinew" } else { trimmed }; + let title = if trimmed.is_empty() { "Claake Code" } else { trimmed }; if let Err(err) = window.set_title(title) { tracing::warn!(%err, label = %window.label(), "unable to update window title"); } @@ -120,7 +120,7 @@ pub(super) fn install_macos_dock_menu(app: &AppHandle) { ); let _ = class_addMethod( delegate_class, - objc2::sel!(sinewNewWindowFromDock:), + objc2::sel!(claakecodeNewWindowFromDock:), new_window_imp, c"v@:@".as_ptr().cast(), ); @@ -137,7 +137,7 @@ unsafe extern "C-unwind" fn macos_application_dock_menu( return std::ptr::null_mut(); }; - let menu_title = NSString::from_str("Sinew"); + let menu_title = NSString::from_str("Claake Code"); let item_title = NSString::from_str("Nouvelle fenêtre"); let empty_key = NSString::new(); let menu = NSMenu::initWithTitle(mtm.alloc(), &menu_title); @@ -145,7 +145,7 @@ unsafe extern "C-unwind" fn macos_application_dock_menu( NSMenuItem::initWithTitle_action_keyEquivalent( mtm.alloc(), &item_title, - Some(objc2::sel!(sinewNewWindowFromDock:)), + Some(objc2::sel!(claakecodeNewWindowFromDock:)), &empty_key, ) }; @@ -260,17 +260,62 @@ pub(super) fn delete_installed_skill(workspace_root: &Path, skill_md: &Path) -> Ok(folder) } -pub(super) fn write_installed_skill( +/// Create a new SKILL.md under one of the configured skill roots. +/// +/// `scope` selects which root family to use: +/// - "workspace" → `/.claakecode/skills//SKILL.md` +/// - "global" or anything else → `~/.claakecode/skills//SKILL.md` +/// +/// Errors if the target folder already exists. +pub(super) fn create_installed_skill( workspace_root: &Path, - skill_md: &Path, + name: &str, content: &str, + scope: &str, ) -> Result { - let skill_md = fs::canonicalize(skill_md).context("skill file does not exist")?; - if skill_md.file_name().and_then(|name| name.to_str()) != Some("SKILL.md") { - anyhow::bail!("can only edit a SKILL.md file"); + let slug = slug_for_skill(name); + if slug.is_empty() { + anyhow::bail!("skill name must contain at least one letter or digit"); + } + let root = match scope { + "workspace" => workspace_root.join(".claakecode/skills"), + _ => home_dir() + .ok_or_else(|| anyhow::anyhow!("could not resolve the user home directory"))? + .join(".claakecode/skills"), + }; + fs::create_dir_all(&root) + .with_context(|| format!("unable to create skill root {}", root.display()))?; + let folder = root.join(&slug); + if folder.exists() { + anyhow::bail!( + "a skill folder named `{}` already exists at {}", + slug, + folder.display() + ); } + fs::create_dir(&folder) + .with_context(|| format!("unable to create skill folder {}", folder.display()))?; + let skill_md = folder.join("SKILL.md"); + fs::write(&skill_md, content) + .with_context(|| format!("unable to write {}", skill_md.display()))?; + Ok(skill_md) +} - let folder = skill_md +/// Rewrite the contents of an existing SKILL.md. +/// +/// Validates that the file lives under one of the configured skill roots +/// before writing, to prevent the command from being abused to write +/// arbitrary files on disk. +pub(super) fn write_installed_skill_content( + workspace_root: &Path, + skill_md: &Path, + content: &str, +) -> Result<()> { + let canonical = fs::canonicalize(skill_md).context("skill file does not exist")?; + if canonical.file_name().and_then(|name| name.to_str()) != Some("SKILL.md") { + anyhow::bail!("can only update a SKILL.md file"); + } + let folder = canonical .parent() .ok_or_else(|| anyhow::anyhow!("skill has no parent folder"))? .to_path_buf(); @@ -284,20 +329,37 @@ pub(super) fn write_installed_skill( if !allowed { anyhow::bail!("skill is outside the configured skill folders"); } + fs::write(&canonical, content) + .with_context(|| format!("unable to write {}", canonical.display()))?; + Ok(()) +} - fs::write(&skill_md, content) - .with_context(|| format!("unable to write {}", skill_md.display()))?; - Ok(skill_md) +fn slug_for_skill(name: &str) -> String { + let mut slug = String::with_capacity(name.len()); + let mut last_dash = true; + for ch in name.trim().chars() { + if ch.is_ascii_alphanumeric() { + slug.push(ch.to_ascii_lowercase()); + last_dash = false; + } else if !last_dash { + slug.push('-'); + last_dash = true; + } + } + while slug.ends_with('-') { + slug.pop(); + } + slug } pub(super) fn skill_roots(workspace_root: &Path) -> Vec { let mut roots = vec![ workspace_root.join(".agents/skills"), - workspace_root.join(".sinew/skills"), + workspace_root.join(".claakecode/skills"), ]; if let Some(home) = home_dir() { roots.push(home.join(".agents/skills")); - roots.push(home.join(".sinew/skills")); + roots.push(home.join(".claakecode/skills")); } roots } diff --git a/src-tauri/src/providers.rs b/src-tauri/src/providers.rs index 042628d4..ab7689bd 100644 --- a/src-tauri/src/providers.rs +++ b/src-tauri/src/providers.rs @@ -4,14 +4,22 @@ pub(super) fn model_with_optional_selection( current: &ModelRef, model: Option, thinking: Option, + use_1m_context: Option, ) -> ModelRef { let mut selected = match model { - Some(model) => ModelRef::new(model.provider, model.name), + Some(model) => { + let mut m = ModelRef::new(model.provider, model.name); + m.use_1m_context = model.use_1m_context; + m + } None => current.clone(), }; if let Some(thinking) = thinking { selected.effort = Some(thinking.into_effort()); } + if let Some(flag) = use_1m_context { + selected.use_1m_context = Some(flag); + } selected } @@ -117,7 +125,7 @@ pub(super) fn openrouter_capabilities(models: &[OpenRouterModelRecord]) -> Vec, ) -> Result<()> { let http = reqwest::Client::builder() - .user_agent("sinew/0.1") + .user_agent("ClaakeCode/0.1") .build() .context("unable to build OAuth client")?; @@ -423,7 +431,7 @@ pub(super) async fn run_anthropic_oauth_server( cancel: Arc, ) -> Result<()> { let http = reqwest::Client::builder() - .user_agent("sinew/0.1") + .user_agent("ClaakeCode/0.1") .build() .context("unable to build OAuth client")?; @@ -548,7 +556,7 @@ pub(super) async fn run_google_oauth_server( cancel: Arc, ) -> Result<()> { let http = reqwest::Client::builder() - .user_agent("sinew/0.1") + .user_agent("ClaakeCode/0.1") .build() .context("unable to build OAuth client")?; @@ -703,7 +711,7 @@ pub(super) fn openai_login_success_html() -> String { - Sinew connected + Claake Code connected -

OpenAI is connected

You can close this tab and return to Sinew.

+

OpenAI is connected

You can close this tab and return to Claake Code.

"# .to_string() } @@ -721,7 +729,7 @@ pub(super) fn anthropic_login_success_html() -> String { - Sinew connected + Claake Code connected -

Anthropic is connected

You can close this tab and return to Sinew.

+

Anthropic is connected

You can close this tab and return to Claake Code.

"# .to_string() } @@ -739,7 +747,7 @@ pub(super) fn google_login_success_html() -> String { - Sinew connected + Claake Code connected -

Google is connected

You can close this tab and return to Sinew.

+

Google is connected

You can close this tab and return to Claake Code.

"# .to_string() } @@ -759,7 +767,7 @@ pub(super) fn openai_login_error_html(message: &str) -> String { - Sinew connection failed + Claake Code connection failed