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 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
-
+
+- **`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
-
+
+### 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
claake code
+
v0.1.16
+
+
+
+
+
+
+
+
+ Open source · MIT
+ v0.1.16
+ Database settings
+ macOS · linux · windows
+
+
+
+
The open AI IDEyou can actuallycontrol .
+
+
+
+ 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
+
+
+ 17 editable tools Including database list, schema and query.
+ ∞ unlimited skills Markdown playbooks the agent can load.
+ DB database settings Connect sources the agent can inspect and query.
+
+
+
+
+
+
+
+ § 01
+ Patch notes
+
+
+
+
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.
+
+
+
+
+
+
+
+
+
+ § 02
+ Live product · see the IDE before you download
+
+
+
+
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
+
+
+
+
+
+
+
+
+ § 04
+ How the agent works
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+ § 05
+ Shape the harness
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+ § 06
+ Built for people who don't trust black boxes
+
+
+ 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.
+
+
+
+
+
+
+
+
+
+
+ § 07
+ Open source
+
+
+
+
+
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.
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+ § 08
+ How it compares
+
+
+ Everyone says "agentic". Few let you see and rewrite the prompt.
+
+
+
+
+ Capability
+ Claake Code
+ Cursor
+ Claude Code
+ Aider
+ Zed AI
+
+
+
+ Native desktop app ✓ ✓ CLI CLI ✓
+ Open source ✓ — — ✓ ✓
+ Multi-provider ✓ ✓ — ✓ ✓
+ Editable tool descriptions ✓ — — — —
+ Toggle individual tools ✓ partial — — —
+ Database sources in Settings ✓ — — — —
+ MCP server CRUD UI ✓ partial partial — —
+ Skills CRUD UI ✓ — — — —
+ 1M context beta toggle (Sonnet) ✓ — partial — —
+ Agent swarm + task board ✓ — — — —
+ Embedded terminal ✓ ✓ n/a n/a ✓
+
+
+
+
+
+
+
+
+ § 09
+ Download
+
+
+
+
+
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