DrugClaw — Drug Discovery Research Agent
A drug-discovery research agent written in Rust, with skill packs for genomics, medical, pharmaceutical and general science work, and MCP bridges to outside tools.
A drug-discovery research agent written in Rust, with bundled skill packs for genomics, medical, pharmaceutical, research and general science work, plus MCP bridge examples, a desktop GUI and a web console. Ships install scripts for macOS, Linux and Windows and a Docker image; the README and the full docs set are in English and Chinese.
Created by TalentDelta Team Member · via TalentDelta
build 1.0.0 · 5425.8 KB · published 2026-09-30 · 360 files
Licence: Apache-2.0
Scouted from github.com/DrugClaw/DrugClaw at commit 960a6e0e5935533099dcd1774f6d1280e7d59d55 (2026-03-23). Licence Apache-2.0. Upstream credit: DrugClaw project contributors. screenshots excluded as regenerable; every other file byte-identical to the pin. Upstream code and licence stay upstream's.
Brought into the catalogue by TalentDelta Team Member (Scout).
360 files · 10750.0 KB · previews are generated at approval and never executed
drugclaw-drug-discovery-agent/.dockerignoretext · 0.4 KB
# VCS and editor metadata .git .github .history .DS_Store # Build outputs and caches target tmp working_dir coverage # Local runtime/state data data drugclaw.data drugclaw.config.yaml drugclaw.config.yaml.* *.db *.sqlite *.sqlite3 # JS build dependencies web/node_modules website/node_modules # Keep Docker context lean by excluding local docs site outputs websitedrugclaw-drug-discovery-agent/.github/workflows/ci.ymlcode · 2.3 KB
name: CI on: push: branches: [main] pull_request: branches: [main] env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings jobs: web: name: Web Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm cache-dependency-path: web/package-lock.json - run: node scripts/generate_docs_artifacts.mjs --check --no-website - run: npm --prefix web ci - run: npm --prefix web run build rust-ubuntu: name: Rust (ubuntu-latest) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: 1.93.1 components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets -- -D warnings - run: cargo test rust-macos: name: Rust (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: 1.93.1 components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - run: rustup target add x86_64-apple-darwin - run: cargo check --target x86_64-apple-darwin - run: cargo clippy --all-targets -- -D warnings - run: cargo test stability-smoke: name: Stability Smoke runs-on: ubuntu-latest needs: [rust-ubuntu] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: 1.93.1 components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - run: scripts/ci/stability_smoke.sh build: name: Build (Release) runs-on: ubuntu-latest needs: [web, rust-ubuntu, rust-macos, stability-smoke] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' cache: npm cache-dependency-path: web/package-lock.json - run: npm --prefix web ci - run: npm --prefix web run build - uses: dtolnay/rust-toolchain@stable with: toolchain: 1.93.1 - uses: Swatinem/rust-cache@v2 - run: cargo build --releasedrugclaw-drug-discovery-agent/.github/workflows/extended-ci.ymlcode · 0.8 KB
name: Extended CI on: push: branches: [main] pull_request: branches: [main] env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings jobs: rust-windows: name: Rust (windows-latest) runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: 1.93.1 components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - run: cargo clippy --all-targets -- -D warnings - run: cargo test nix-build: name: Nix Build (flake) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: DeterminateSystems/nix-installer-action@v19 - uses: DeterminateSystems/magic-nix-cache-action@v13 - run: nix build .#drugclawdrugclaw-drug-discovery-agent/.github/workflows/nightly-stability.ymlcode · 2.6 KB
name: Nightly Stability on: schedule: - cron: '17 3 * * *' workflow_dispatch: env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings permissions: contents: read issues: write concurrency: group: nightly-stability cancel-in-progress: false jobs: nightly-stability: name: Nightly Stability Gate runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - name: Run nightly stability gate id: gate run: scripts/ci/nightly_stability.sh continue-on-error: true - name: Create or update failure issue if: steps.gate.outcome == 'failure' uses: actions/github-script@v7 with: script: | const owner = context.repo.owner; const repo = context.repo.repo; const label = "nightly-stability"; const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`; const title = "Nightly Stability Gate failing"; const body = [ "Nightly stability gate failed.", "", `- Workflow run: ${runUrl}`, `- Commit: ${context.sha}`, `- Trigger: ${context.eventName}`, "", "Please inspect logs and fix regressions in smoke tests or metrics contract assertions." ].join("\\n"); try { await github.rest.issues.createLabel({ owner, repo, name: label, color: "B60205", description: "Nightly stability regression tracking" }); } catch (e) { if (e.status !== 422) throw e; } const { data } = await github.rest.search.issuesAndPullRequests({ q: `repo:${owner}/${repo} is:issue is:open in:title "${title}" label:${label}`, per_page: 1 }); if (data.items.length > 0) { const issue = data.items[0]; await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body }); } else { await github.rest.issues.create({ owner, repo, title, body, labels: [label] }); } - name: Fail workflow when gate fails if: steps.gate.outcome == 'failure' run: exit 1drugclaw-drug-discovery-agent/.github/workflows/release-assets.ymlcode · 9.1 KB
name: Release Assets on: push: tags: - 'v*' workflow_dispatch: inputs: tag: description: 'Existing release tag to build/upload assets for (e.g. v0.0.36)' required: true type: string permissions: contents: write env: CARGO_TERM_COLOR: always jobs: verify-ci: name: Verify CI Passed runs-on: ubuntu-latest env: RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} steps: - name: Checkout tag commit uses: actions/checkout@v4 with: ref: ${{ env.RELEASE_TAG }} - name: Ensure CI succeeded for tag commit env: GH_TOKEN: ${{ github.token }} CI_WAIT_TIMEOUT_SECONDS: "900" CI_WAIT_INTERVAL_SECONDS: "30" REQUIRED_CI_JOBS_JSON: '["Web Build","Rust (ubuntu-latest)","Rust (macos-latest)","Stability Smoke"]' shell: bash run: | TARGET_SHA="$(git rev-parse HEAD)" echo "Checking CI status for commit: ${TARGET_SHA}" elapsed=0 timeout_seconds="${CI_WAIT_TIMEOUT_SECONDS}" interval_seconds="${CI_WAIT_INTERVAL_SECONDS}" while [[ "${elapsed}" -lt "${timeout_seconds}" ]]; do RUN_ID="$( gh run list \ --repo "${GITHUB_REPOSITORY}" \ --workflow "CI" \ --commit "${TARGET_SHA}" \ --json databaseId,createdAt \ --jq 'sort_by(.createdAt) | reverse | .[0].databaseId' )" if [[ -z "${RUN_ID}" || "${RUN_ID}" == "null" ]]; then echo "CI run not found yet for commit ${TARGET_SHA}." else JOBS_JSON="$( gh run view "${RUN_ID}" \ --repo "${GITHUB_REPOSITORY}" \ --json jobs )" FAILED_JOB="$( jq -r --argjson required "${REQUIRED_CI_JOBS_JSON}" ' .jobs | map(select(.name as $name | $required | index($name))) | map(select( .conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required" or .conclusion == "startup_failure" or .conclusion == "stale" )) | first | if . == null then empty else "\(.name) \(.url)" end ' <<<"${JOBS_JSON}" )" if [[ -n "${FAILED_JOB}" ]]; then echo "Required CI job failed for commit ${TARGET_SHA}: ${FAILED_JOB}" >&2 exit 1 fi COMPLETED_REQUIRED_COUNT="$( jq -r --argjson required "${REQUIRED_CI_JOBS_JSON}" ' .jobs | map(select(.name as $name | $required | index($name))) | map(select(.conclusion == "success")) | length ' <<<"${JOBS_JSON}" )" REQUIRED_COUNT="$( jq -r 'length' <<<"${REQUIRED_CI_JOBS_JSON}" )" if [[ "${COMPLETED_REQUIRED_COUNT}" -eq "${REQUIRED_COUNT}" ]]; then echo "Required CI jobs succeeded. Run id: ${RUN_ID}" exit 0 fi echo "Waiting for required CI jobs (${COMPLETED_REQUIRED_COUNT}/${REQUIRED_COUNT}) on run ${RUN_ID}." fi sleep "${interval_seconds}" elapsed=$((elapsed + interval_seconds)) done echo "Timed out waiting for required CI jobs after ${timeout_seconds}s." >&2 echo "Recent CI runs for this commit:" gh run list \ --repo "${GITHUB_REPOSITORY}" \ --workflow "CI" \ --commit "${TARGET_SHA}" \ --json databaseId,status,conclusion,url \ --jq '.[] | {id: .databaseId, status: .status, conclusion: .conclusion, url: .url}' exit 1 build-assets: name: Build ${{ matrix.label }} runs-on: ${{ matrix.os }} needs: verify-ci env: RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} strategy: fail-fast: false matrix: include: - label: linux-x86_64 os: ubuntu-latest bin: drugclaw target_triple: x86_64-unknown-linux-gnu asset_triple: x86_64-linux-gnu archive_ext: tar.gz - label: macos-aarch64 os: macos-latest bin: drugclaw target_triple: aarch64-apple-darwin asset_triple: aarch64-apple-darwin archive_ext: tar.gz - label: macos-x86_64 os: macos-latest bin: drugclaw target_triple: x86_64-apple-darwin asset_triple: x86_64-apple-darwin archive_ext: tar.gz - label: windows-x86_64 os: windows-latest bin: drugclaw.exe target_triple: x86_64-pc-windows-msvc asset_triple: x86_64-windows-msvc archive_ext: zip steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ env.RELEASE_TAG }} - name: Setup Node uses: actions/setup-node@v4 with: node-version: '20' cache: npm cache-dependency-path: web/package-lock.json - name: Build embedded web assets run: | npm --prefix web ci npm --prefix web run build - name: Setup Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target_triple }} - name: Ensure Rust target installed run: rustup target add ${{ matrix.target_triple }} - name: Build release binary run: cargo build --release --target ${{ matrix.target_triple }} - name: Package archive (Linux) if: matrix.archive_ext == 'tar.gz' shell: bash run: | RELEASE_VERSION="${RELEASE_TAG#v}" ASSET_NAME="drugclaw-${RELEASE_VERSION}-${{ matrix.asset_triple }}.tar.gz" tar -czf "$ASSET_NAME" -C "target/${{ matrix.target_triple }}/release" "${{ matrix.bin }}" echo "ASSET_NAME=$ASSET_NAME" >> "$GITHUB_ENV" - name: Package archive (Windows) if: matrix.archive_ext == 'zip' shell: pwsh run: | $releaseVersion = $env:RELEASE_TAG -replace '^v', '' $assetName = "drugclaw-${releaseVersion}-${{ matrix.asset_triple }}.zip" Compress-Archive -Path "target/${{ matrix.target_triple }}/release/${{ matrix.bin }}" -DestinationPath $assetName -Force "ASSET_NAME=$assetName" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 - name: Upload build artifact uses: actions/upload-artifact@v4 with: name: ${{ matrix.label }} path: ${{ env.ASSET_NAME }} if-no-files-found: error publish-assets: name: Upload to GitHub Release runs-on: ubuntu-latest needs: build-assets env: RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} steps: - name: Checkout tag (full history for tag range) uses: actions/checkout@v4 with: ref: ${{ env.RELEASE_TAG }} fetch-depth: 0 fetch-tags: true - name: Download artifacts uses: actions/download-artifact@v4 with: path: dist merge-multiple: true - name: Ensure release exists env: GH_TOKEN: ${{ github.token }} run: | PREV_TAG="$(git tag --list 'v*' --sort=-version:refname | awk -v current="${RELEASE_TAG}" '$0 != current { print; exit }')" if [[ -n "${PREV_TAG}" ]]; then echo "Generating release notes from range: ${PREV_TAG}..${RELEASE_TAG}" else echo "Generating release notes without a previous tag (first release or missing tags)" fi NOTES_FILE="$(mktemp)" GENERATE_ARGS=( gh api -X POST "repos/${GITHUB_REPOSITORY}/releases/generate-notes" -f "tag_name=${RELEASE_TAG}" ) if [[ -n "${PREV_TAG}" ]]; then GENERATE_ARGS+=(-f "previous_tag_name=${PREV_TAG}") fi "${GENERATE_ARGS[@]}" --jq '.body' > "${NOTES_FILE}" if ! gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then gh release create "${RELEASE_TAG}" \ --repo "${GITHUB_REPOSITORY}" \ -t "${RELEASE_TAG}" \ --notes-file "${NOTES_FILE}" else gh release edit "${RELEASE_TAG}" \ --repo "${GITHUB_REPOSITORY}" \ -t "${RELEASE_TAG}" \ --notes-file "${NOTES_FILE}" fi rm -f "${NOTES_FILE}" - name: Upload assets env: GH_TOKEN: ${{ github.token }} run: | ls -lah dist gh release upload "${RELEASE_TAG}" dist/* \ --repo "${GITHUB_REPOSITORY}" \ --clobberdrugclaw-drug-discovery-agent/.gitignoretext · 0.5 KB
# macOS / Windows noise .DS_Store Thumbs.db # Editor / local agent state .idea/ .vscode/ .claude/ *.swp *.swo # Environment files .env .env.* !.env.example !.env.*.example # Rust build outputs /target/ /gui/src-tauri/target/ *.dSYM/ # JavaScript / frontend dependencies and build outputs /web/node_modules/ /website/node_modules/ /gui/node_modules/ coverage/ .nyc_output/ *.tsbuildinfo # Logs / caches *.log .cache/ __pycache__/ *.py[cod] *.backup # Website is published from a dedicated GitHub Pages branch /website/drugclaw-drug-discovery-agent/AGENTS.mdmarkdown · 6.6 KB
AGENTS.md
Project overview
DrugClaw is a Rust multi-channel agent runtime for Telegram, Discord, and Web. It shares one channel-agnostic agent loop (
src/agent_engine.rs) and one provider-agnostic LLM layer (src/llm.rs), with channel adapters for ingress/egress.Core capabilities:
- Tool-using chat agent loop (multi-step tool calls)
- Session resume and context compaction
- Scheduled tasks + background scheduler
- File memory (
AGENTS.md) + structured SQLite memory - Memory reflector, quality gate, and observability metrics
- Skills + MCP tool federation
- ClawHub skill registry integration (search/install/lockfile)
Tech stack
- Language: Rust (edition 2021)
- CLI args: clap
- Async runtime: Tokio
- Telegram: teloxide
- Discord: serenity
- Web API/UI: axum + React (in
web/) - Database: SQLite (rusqlite)
- LLM runtime: provider abstraction with native Anthropic and OpenAI-compatible providers
Source index (
src/+crates/)Main orchestration files in
src/:main.rs: CLI entry (start,setup, etc.)runtime.rs: app wiring (AppState), provider/tool initialization, channel bootagent_engine.rs: shared agent loop (process_with_agent), explicit-memory fast path, compaction, tool loophooks.rs: hooks discovery/runtime/CLI (hooks list/info/enable/disable)llm.rs: provider implementations + stream handling + format translationotlp.rs: OTLP metrics exporter (HTTP/protobuf)web.rs: Web API router, shared web state, stream APIs, config endpointsweb/auth.rs: auth handlers (session login/logout, password, API key lifecycle)web/config.rs: config read/update + config self-check handlersweb/sessions.rs: session/history/reset/delete/fork/tree handlersweb/metrics.rs: metrics snapshot/history handlersweb/stream.rs: streaming send/status/SSE handlersscheduler.rs: scheduled-task runner + memory reflector loopskills.rs: skill discovery/activationmcp.rs: MCP server/tool integrationgateway.rs: event stream / request lifecycle infrasetup.rs: interactive setup wizard and provider presetsdoctor.rs: environment diagnosticschannels/*.rs: concrete channel adapters (Telegram/Discord/Slack/Feishu)tools/*.rs: built-in tool implementations and registry assembly
Modularized crates in
crates/:drugclaw-core: shared error/types/text (error,llm_types,text)drugclaw-storage: SQLite DB, memory domain, usage report assemblydrugclaw-tools: tool runtime primitives, sandbox, path guards, web/todo helpersdrugclaw-channels: channel abstractions (channel,channel_adapter, delivery boundary)drugclaw-app: app-level support modules (logging, builtin skills, transcribe)
Tool system
src/tools/mod.rsassembles built-in tools, while shared runtime primitives live indrugclaw-tools::runtime:Tooltrait (name,definition,execute)ToolRegistrydispatch and auth context injection- risk/approval gate for high-risk tools in sensitive contexts
Current built-in tools are generated from code to avoid drift:
docs/generated/tools.mdwebsite/docs/generated-tools.md
Regenerate docs artifacts with:
node scripts/generate_docs_artifacts.mjsSkills Storage
- Default skills dir:
<data_dir>/skills(defaultdata_diris~/.drugclaw) - Config override:
skills_dirindrugclaw.config.yaml(e.g.~/.drugclaw/skills) - Compatibility policy: existing configured
data_dir/skills_dir/working_dirkeep working; new defaults apply only when unset - ClawHub lockfile:
~/.drugclaw/clawhub.lock.json
Agent loop (high level)
process_with_agentflow:- Optional explicit-memory fast path (
remember .../记住...) writes structured memory directly - Load resumable session from
sessions, or rebuild from chat history - Build system prompt from file memory + structured memory context + skills catalog
- Compact old context if session exceeds limits
- Call provider with tool schemas
- If
tool_use: execute tool(s), append results, loop - If
end_turn: persist session and return text
Memory architecture
Two layers:
- File memory:
- Global:
runtime/groups/AGENTS.md - Chat:
runtime/groups/{chat_id}/AGENTS.md
- Structured memory (
memoriestable):
- category, confidence, source, last_seen, archived lifecycle
- explicit remember fast path
- reflector extraction from conversation history
- dedup/supersede handling with
memory_supersede_edges
Observability tables:
memory_reflector_runsmemory_injection_logs
Surfaces:
/api/usagesummary block/api/memory_observabilitytime-window series API- Web Usage Panel trends/cards
Database
drugclaw-storage::dbincludes:- schema creation + schema-version migrations (
db_meta,schema_migrations) - chat/message/session/task persistence
- structured memory CRUD + archive/supersede
- auth/session/api-key persistence (scopes, expiry, rotation)
- audit log persistence (
audit_logs) - usage and memory observability queries
- metrics history persistence (
metrics_history)
Web/API
web.rsroutes include:- chat send/send_stream + SSE stream replay
- auth APIs (
/api/auth/*) with session cookie + API key scopes - sessions/history/reset/delete/fork/tree
- config read/update + self-check (
/api/config/self_check) - audit query (
/api/audit) - metrics APIs (
/api/metrics,/api/metrics/summary,/api/metrics/history) - usage text report (
/api/usage) - memory observability series (
/api/memory_observability)
Hooks
Hook assets and spec:
- runtime hook dirs:
hooks/<name>/HOOK.md - hook spec doc:
docs/hooks/HOOK.md - sample hooks:
hooks/block-bash/,hooks/redact-tool-output/
Hook runtime supports:
- events:
BeforeLLMCall,BeforeToolCall,AfterToolCall - outcomes:
allow,block,modify(structured fields only)
ClawHub
- local doc:
docs/clawhub/overview.md - web doc:
website/docs/clawhub.md
Observability Docs
- metrics docs:
docs/observability/metrics.md - operations runbook:
docs/operations/runbook.md - PR/release checklist:
docs/releases/pr-release-checklist.md - upgrade guide:
docs/releases/upgrade-guide.md - regression/stability reports:
docs/reports/*.md
OTLP exporter supports bounded queue + retry backoff tuning:
otlp_queue_capacityotlp_retry_max_attemptsotlp_retry_base_msotlp_retry_max_ms
Build and test
cargo build cargo test npm --prefix web run build npm --prefix website run buildDocs drift guard (CI + local):
node scripts/generate_docs_artifacts.mjs --checkCollaboration conventions
- For the separate
websiterepository (drugclaw.github.io), do not open a PR by default; commit/push directly unless explicitly requested otherwise.
drugclaw-drug-discovery-agent/CLAUDE.mdmarkdown · 5.4 KB
DrugClaw
AI Research Assistant for Accelerated Drug Discovery
Tech stack
Rust 2021, Tokio, teloxide 0.17, serenity 0.12, provider-agnostic LLM runtime (Anthropic + OpenAI-compatible), SQLite (rusqlite bundled), cron crate for scheduling.
Directory overview
gui/-- Rust Tauri GUIsrc/-- Rust source for the bot binaryweb/-- Built-in Web UI (React + Vite). Compiled toweb/dist/and embedded into the Rust binary viainclude_dir!. This is the chat interface and settings panel served by drugclaw itself at runtime.website/-- Separate git repository (landing page + documentation site). Not part of the drugclaw binary. Contains the public-facing marketing site and docs. Changes here have no effect on the bot.
Project layout
crates/drugclaw-core/-- shared error/types/text modules (error,llm_types,text)crates/drugclaw-storage/-- SQLite DB schema/query layer + memory/usage domain modulescrates/drugclaw-tools/-- tool runtime primitives (trait/auth/risk/schema/path) + sandboxcrates/drugclaw-channels/-- channel abstraction and delivery boundary modulescrates/drugclaw-app/-- app-level support modules (logging, builtin skills, transcribe)src/main.rs-- entry point, CLIsrc/runtime.rs-- app wiring (AppState), provider/tool initialization, channel bootsrc/agent_engine.rs-- shared agent loop (process_with_agent)src/llm.rs-- provider implementations + format translationsrc/web.rs-- web API routes and streamingsrc/memory.rs-- file-memory manager (runtime/groups/.../AGENTS.md)src/scheduler.rs-- background scheduler + memory reflector loopssrc/channels/*.rs-- Telegram/Discord/Slack/Feishu adapterssrc/tools/*.rs-- concrete built-in tools; registry assembly insrc/tools/mod.rs
Key patterns
- Agentic loop in
agent_engine.rs:process_with_agent: call provider -> if tool_use -> execute -> loop (up tomax_tool_iterations) - Session resume: full
Vec<Message>(including tool_use/tool_result blocks) persisted insessionstable; on next invocation, loaded and appended with new user messages./resetclears session. - Context compaction: when session messages exceed
max_session_messages, older messages are summarized and replaced with a compact summary + recent messages kept verbatim - Sub-agent:
sub_agenttool spawns a fresh agentic loop with 9 restricted tools (no send_message, write_memory, schedule, or recursive sub_agent) - Tool trait:
name(),definition()(JSON Schema),execute(serde_json::Value) -> ToolResult - Shared state:
AppStateinArc, tools holdBot/Arc<Database>as needed - Group catch-up:
db.get_messages_since_last_bot_response()loads all messages since bot's last reply - Scheduler:
tokio::spawnloop, polls DB for due tasks, callsprocess_with_agentwithoverride_prompt - Typing: spawned task sends typing action every 4s, aborted when response is ready
- Path guard: sensitive paths (.ssh, .aws, .env, credentials, etc.) are blocked in file tools via
path_guardmodule - Platform-extensible core: Telegram/Discord/Slack/Feishu/Web adapters reuse
process_with_agent; new platforms integrate through the same core loop - SOUL.md: optional personality file injected into system prompt. Loaded from
soul_pathconfig,data_dir/SOUL.md, or./SOUL.md. Per-chat overrides viadata_dir/runtime/groups/{chat_id}/SOUL.md
Build & run
cargo build cargo run -- start # requires config.yaml with at least one enabled channel plus model credentials cargo run -- setup # interactive setup wizard to create config.yaml cargo run -- helpConfiguration
DrugClaw uses
drugclaw.config.yaml(or.yml) for configuration. Override the path withMICROCLAW_CONFIGenv var. Seedrugclaw.config.example.yamlfor all available fields.Soul (personality customization)
DrugClaw supports a
SOUL.mdfile that defines the bot's personality, voice, values, and working style. The file content is injected into the system prompt, replacing the default "helpful AI assistant" identity.Loading priority (first match wins):
soul_pathin config (explicit path)<data_dir>/SOUL.md./SOUL.md(project root, ships with the repo as the default soul)
Per-chat override: place a
SOUL.mdat<data_dir>/runtime/groups/<chat_id>/SOUL.mdto give a specific chat a different personality.Implementation:
load_soul_content()andbuild_system_prompt()insrc/agent_engine.rs. The soul content is wrapped in<soul>XML tags in the system prompt.Adding a tool
- Create
src/tools/my_tool.rsimplementing theTooltrait - Add
pub mod my_tool;tosrc/tools/mod.rs - Register in
ToolRegistry::new()withBox::new(my_tool::MyTool::new(...))
Database
Core persistence is provided by
drugclaw-storage(Databasewrapper over SQLite). Runtime state and observability tables are managed through versioned migrations.Important conventions
- All timestamps are ISO 8601 / RFC 3339 strings
- Cron expressions use 6-field format (sec min hour dom month dow)
- Messages are stored for all chats regardless of whether bot responds
- In groups, bot only responds to @mentions
- Consecutive same-role messages are merged before sending to the configured LLM provider
- Responses > 4096 chars are split at newline boundaries (Telegram), > 2000 chars for Discord, > 4000 chars for Slack/Feishu
drugclaw-drug-discovery-agent/Cargo.locktext · 150.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/Cargo.tomlcode · 2.7 KB
[workspace] members = [ ".", "crates/drugclaw-core", "crates/drugclaw-clawhub", "crates/drugclaw-storage", "crates/drugclaw-tools", "crates/drugclaw-channels", "crates/drugclaw-app", "crates/drugclaw-observability", ] resolver = "2" [package] name = "drugclaw" version = "0.1.10" edition = "2021" license = "Apache-2.0" default-run = "drugclaw" [features] default = [] sqlite-vec = ["drugclaw-storage/sqlite-vec"] journald = ["drugclaw-app/journald"] [dependencies] drugclaw-core = { path = "crates/drugclaw-core" } drugclaw-clawhub = { path = "crates/drugclaw-clawhub" } drugclaw-storage = { path = "crates/drugclaw-storage" } drugclaw-tools = { path = "crates/drugclaw-tools" } drugclaw-channels = { path = "crates/drugclaw-channels" } drugclaw-app = { path = "crates/drugclaw-app" } drugclaw-observability = { path = "crates/drugclaw-observability" } teloxide = { version = "0.17", features = ["macros"] } tokio = { version = "1", features = ["full"] } tokio-native-tls = "0.3" native-tls = "0.2" reqwest = { version = "0.12", features = ["json", "blocking"] } rusqlite = { version = "0.37", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } glob = "0.3" regex = "1" thiserror = "2" anyhow = "1" async-trait = "0.1" agent-client-protocol = "0.10.0" uuid = { version = "1", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } serde_yaml = "0.9" cron = "0.13" urlencoding = "2" base64 = "0.22" chrono-tz = "0.10" iana-time-zone = "0.1" zip = "2" sha2 = "0.10" axum = { version = "0.7", features = ["ws"] } ratatui = { version = "0.29", default-features = false, features = ["crossterm"] } crossterm = "0.28" serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "model", "cache", "rustls_backend"] } include_dir = "0.7" async-stream = "0.3" futures-util = "0.3" tokio-util = { version = "0.7", features = ["compat"] } tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } argon2 = "0.5" opentelemetry-proto = { version = "0.28", features = ["gen-tonic-messages"] } opentelemetry-semantic-conventions = { version = "0.30", features = ["semconv_experimental"] } prost = "0.13" matrix-sdk = { version = "0.16.0", default-features = false, features = ["e2e-encryption", "automatic-room-key-forwarding", "native-tls", "sqlite", "bundled-sqlite"] } clap = { version = "4.5", features = ["derive"] } http = "1" rmcp = { version = "1.1", features = [ "client", "transport-child-process", "transport-streamable-http-client-reqwest", ] } shellexpand = "3.1.2" [dev-dependencies] tower = "0.5"drugclaw-drug-discovery-agent/DEVELOP.mdmarkdown · 11.9 KB
DrugClaw Developer Guide
Quick start
git clone <repo-url> cd drugclaw cp drugclaw.config.example.yaml drugclaw.config.yaml # Edit drugclaw.config.yaml with your credentials cargo run -- startPrerequisites
- Rust 1.70+ (2021 edition)
- At least one enabled channel adapter (Telegram bot token from @BotFather, Discord bot token from Discord Developer Portal, Slack app/bot tokens, Feishu/Lark app credentials, or Web UI)
- A model provider API key (Anthropic or OpenAI-compatible)
No other external dependencies. SQLite is bundled via
rusqlite.Project structure
crates/ drugclaw-core/ # Shared error/types/text modules drugclaw-storage/ # SQLite + memory + usage reporting drugclaw-tools/ # Tool runtime primitives + sandbox drugclaw-channels/ # Channel abstraction boundary drugclaw-app/ # App support modules (logging/skills/transcribe) src/ main.rs # CLI entrypoint runtime.rs # Runtime bootstrap + adapter startup agent_engine.rs # Shared agent loop (process_with_agent) llm.rs # Provider adapters (Anthropic/OpenAI-compatible/Codex) web.rs # Web API + stream endpoints scheduler.rs # Scheduler + memory reflector loops channels/*.rs # Concrete adapters (Telegram/Discord/Slack/Feishu) tools/*.rs # Built-in tools + registry assemblyArchitecture overview
Data flow
Platform message (via adapter) | v Store in SQLite (message + chat metadata) | v Determine response: private=always, group=@mention only | v Start typing indicator (tokio::spawn, every 4s) | v Load session or history: - Try loading saved session (full message state with tool blocks) - If session exists: append new user messages since last save - If no session: fall back to DB history - Private: last N messages - Group: all messages since last bot response (catch-up) | v Build system prompt (bot identity + memory context + chat_id) | v Compact if needed (messages > max_session_messages): - Summarize old messages via configured model provider - Keep recent messages verbatim | v Agentic loop (up to max_tool_iterations): 1. Call provider API with messages + tool definitions 2. If stop_reason == "tool_use" -> execute tools -> append results -> loop 3. If stop_reason == "end_turn" -> extract text -> return | v Strip image base64 data, save session to SQLite | v Abort typing indicator | v Send response (split at channel limits: Telegram 4096 / Discord 2000 / Slack 4000 / Feishu 4000) | v Store bot response in SQLiteThe same core loop is reused across adapters. Adding a new platform should primarily require a new ingress/egress adapter that maps platform events into the shared
process_with_agentflow.Key types
Type Location Description AppStateruntime.rsShared runtime state for all adapters Databasedrugclaw_storage::dbSQLite wrapper with Mutex<Connection>ToolRegistrytools/mod.rsHolds all Box<dyn Tool>, dispatches by nameTooltraitdrugclaw_tools::runtimename(),definition(),execute()LlmProviderllm.rsProvider abstraction for Anthropic and OpenAI-compatible APIs MemoryManagermemory.rsAGENTS.md file memory reader/writer Shared state
AppStateis wrapped inArcand shared:- Telegram handler has
Arc<AppState>via dptree dependencies - Discord handler has
Arc<AppState>via serenity event handler state - Scheduler gets
Arc<AppState>at spawn time - Tools that need
BotorDatabasehold their own clones/arcs (passed at construction)
Multi-chat permission model
control_chat_idsindrugclaw.config.yamldefines privileged chats.- Tool execution receives trusted caller context from
process_with_agent(not from model-provided args). - Non-control chats can only operate on their own
chat_id. - Control chats can perform cross-chat actions.
write_memorywithscope: "global"is restricted to control chats.- Enforcement currently applies to
send_message, scheduler tools,export_chat,todo_*, and chat-scoped memory operations.
Database tables
chats:
Column Type Description chat_id INTEGER PK Channel-scoped chat ID chat_title TEXT Chat title (nullable) chat_type TEXT "private" or "group" last_message_time TEXT ISO 8601 timestamp messages:
Column Type Description id TEXT Message ID (PK with chat_id) chat_id INTEGER Channel-scoped chat ID sender_name TEXT Username or first name content TEXT Message text is_from_bot INTEGER 0 or 1 timestamp TEXT ISO 8601 timestamp scheduled_tasks:
Column Type Description id INTEGER PK Auto-increment task ID chat_id INTEGER Target chat for results prompt TEXT Instruction to execute schedule_type TEXT "cron" or "once" schedule_value TEXT Cron expression or ISO timestamp next_run TEXT Next scheduled execution time last_run TEXT Last execution time (nullable) status TEXT "active", "paused", "completed", "cancelled" created_at TEXT Creation timestamp sessions:
Column Type Description chat_id INTEGER PK Channel-scoped chat ID messages_json TEXT Serialized Vec JSON (full conversation state) updated_at TEXT ISO 8601 timestamp of last save task_run_logs:
Column Type Description id INTEGER PK Auto-increment log ID task_id INTEGER Associated scheduled task chat_id INTEGER Chat the task ran in started_at TEXT Run start timestamp finished_at TEXT Run end timestamp duration_ms INTEGER Run duration in milliseconds success INTEGER 0 or 1 result_summary TEXT Summary of run result (nullable) Adding a new tool
- Create
src/tools/my_tool.rs:
use async_trait::async_trait; use serde_json::json; use crate::tools::{schema_object, Tool, ToolResult}; use drugclaw_core::llm_types::ToolDefinition; pub struct MyTool; #[async_trait] impl Tool for MyTool { fn name(&self) -> &str { "my_tool" } fn definition(&self) -> ToolDefinition { ToolDefinition { name: "my_tool".into(), description: "What this tool does".into(), input_schema: schema_object( json!({ "param1": { "type": "string", "description": "Description of param1" } }), &["param1"], ), } } async fn execute(&self, input: serde_json::Value) -> ToolResult { let param1 = match input.get("param1").and_then(|v| v.as_str()) { Some(p) => p, None => return ToolResult::error("Missing param1".into()), }; // Do work... ToolResult::success(format!("Result: {param1}")) } }-
Add
pub mod my_tool;tosrc/tools/mod.rs -
Register in
ToolRegistry::new():
Box::new(my_tool::MyTool),If your tool needs shared state (like
BotorArc<Database>), add a constructor:pub struct MyTool { db: Arc<Database>, } impl MyTool { pub fn new(db: Arc<Database>) -> Self { MyTool { db } } }And pass it in
ToolRegistry::new():Box::new(my_tool::MyTool::new(db.clone())),Adding a new platform adapter
The core agent flow is already shared. New platform support should be implemented as an adapter around that core:
- Add a new runtime module (for example
src/<platform>.rs) that listens to platform events. - Normalize incoming platform messages into the canonical fields used by persistence and
process_with_agent:- stable
chat_id chat_type(private/group-like semantics)- sender display name
- text/media blocks
- stable
- Reuse
process_with_agent(state, chat_id, sender, chat_type, override_prompt)for inference/tool loop/session handling. - Implement outbound reply sending with:
- platform length splitting policy
- mention semantics for group/server channels
- attachment support parity with
send_messagewhere applicable
- Keep chat identity stable across restarts so
sessions,messages, scheduler, and memory scopes remain consistent. - Enforce existing authorization boundaries (for example
control_chat_ids) in any platform-specific entry points. - Add end-to-end tests to
TEST.mdmirroring existing platform suites (DM/private, group mention, reset, limits, failure handling).
Scheduler internals
The scheduler is a
tokio::spawntask started inruntime::run(). Every 60 seconds it:- Queries
scheduled_tasksfor rows wherestatus = 'active' AND next_run <= NOW() - For each due task, calls
process_with_agent()withoverride_prompt = Some(task.prompt) - Sends the agent's response to the task's
chat_id - For cron tasks: computes next run from the cron expression and updates the row
- For one-shot tasks: sets
status = 'completed'
Cron expressions use the
croncrate's 6-field format:sec min hour dom month dow.Debugging
# Verbose logging RUST_LOG=debug cargo run -- start # Just drugclaw logs RUST_LOG=drugclaw=debug cargo run -- start # Check database directly sqlite3 drugclaw.data/runtime/drugclaw.db sqlite> SELECT * FROM messages ORDER BY timestamp DESC LIMIT 10; sqlite> SELECT * FROM scheduled_tasks; sqlite> SELECT * FROM chats;Common tasks
Task How Change the model Set modelindrugclaw.config.yamlbased onllm_providerIncrease context window Set max_history_messages: 100indrugclaw.config.yaml(uses more tokens)Increase tool iterations Set max_tool_iterations: 200indrugclaw.config.yamlReset memory Delete files under drugclaw.data/runtime/groups/Reset all data Delete the drugclaw.data/directoryTune compaction threshold Set max_session_messages: 60indrugclaw.config.yaml(higher = more context before compaction)Keep more recent messages Set compact_keep_recent: 30indrugclaw.config.yaml(more recent messages kept verbatim)Reset a chat session Send /resetin the chat, or:sqlite3 drugclaw.data/runtime/drugclaw.db "DELETE FROM sessions WHERE chat_id=XXXX;"Cancel all scheduled tasks sqlite3 drugclaw.data/runtime/drugclaw.db "UPDATE scheduled_tasks SET status='cancelled' WHERE status='active';"Build
cargo build # Dev build (fast compile, slow runtime) cargo build --release # Release build (slow compile, fast runtime) cargo run -- start # Run dev build cargo run -- help # Show CLI helpThe release binary is fully self-contained -- no runtime dependencies, no database server, no config files beyond
drugclaw.config.yaml.Dependencies
Crate Version Purpose teloxide 0.17 Telegram Bot API serenity 0.12 Discord Gateway/API tokio 1 Async runtime reqwest 0.12 HTTP client (Anthropic API, web fetch/search) rusqlite 0.32 SQLite (bundled) serde / serde_json 1 Serialization async-trait 0.1 Async trait support chrono 0.4 Date/time handling cron 0.13 Cron expression parsing urlencoding 2 URL encoding for web search regex 1 Regex for grep tool and HTML parsing glob 0.3 File pattern matching uuid 1 Message ID generation thiserror 2 Error derive macro anyhow 1 Error propagation tracing 0.1 Logging drugclaw-drug-discovery-agent/Dockerfiletext · 1.6 KB
# syntax=docker/dockerfile:1 # Stage 1: Build tools FROM rust:1.88-slim-bookworm AS chef # Install build dependencies RUN apt-get update && apt-get install -y \ pkg-config \ libssl-dev \ libsqlite3-dev \ build-essential \ && rm -rf /var/lib/apt/lists/* WORKDIR /usr/src/drugclaw # Install cargo-chef to improve dependency layer caching RUN cargo install cargo-chef --locked # Stage 2: Prepare dependency recipe FROM chef AS planner COPY . . RUN cargo chef prepare --recipe-path recipe.json # Stage 3: Build FROM chef AS builder COPY --from=planner /usr/src/drugclaw/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . # Build the binary in release mode RUN cargo build --release --locked --bin drugclaw # Stage 4: Run FROM debian:bookworm-slim # Install runtime certificates and libraries RUN apt-get update && apt-get install -y \ ca-certificates \ libssl3 \ libsqlite3-0 \ && rm -rf /var/lib/apt/lists/* # Run as non-root by default RUN useradd --create-home --home-dir /home/drugclaw --uid 10001 --shell /usr/sbin/nologin drugclaw WORKDIR /app # Copy the compiled binary COPY --from=builder /usr/src/drugclaw/target/release/drugclaw /usr/local/bin/ # Copy necessary runtime directories COPY --from=builder /usr/src/drugclaw/web ./web COPY --from=builder /usr/src/drugclaw/skills ./skills COPY --from=builder /usr/src/drugclaw/scripts ./scripts RUN mkdir -p /home/drugclaw/.drugclaw /app/tmp \ && chown -R drugclaw:drugclaw /home/drugclaw /app ENV HOME=/home/drugclaw USER drugclaw CMD ["drugclaw"]drugclaw-drug-discovery-agent/LICENSEtext · 9.1 KB
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONSdrugclaw-drug-discovery-agent/NIXPKGS_README.mdmarkdown · 1.1 KB
Nixpkgs Automation
This repository includes automation for keeping
drugclawupdated inNixOS/nixpkgs.One-command Flow
Run from repo root:
scripts/update-nixpkgs.shBy default, the script will:
- detect version from
Cargo.toml - clone
<your-gh-user>/nixpkgsinto/tmp/nixpkgs-<timestamp> - branch from
upstream/nixos-unstable - update
pkgs/by-name/mi/drugclaw/package.nix - resolve
hashandcargoHash - run
nix-build -A drugclawandresult/bin/drugclaw --help - commit, push, and open PR to
NixOS/nixpkgs
Deploy Integration
After release, you can trigger nixpkgs automation with:
AUTO_NIXPKGS_UPDATE=1 ./deploy.shUseful Flags
scripts/update-nixpkgs.sh --version 0.0.164 scripts/update-nixpkgs.sh --draft scripts/update-nixpkgs.sh --no-pr scripts/update-nixpkgs.sh --nixpkgs-dir ~/focus/nixpkgsFailure Recovery
If the script fails mid-way:
- check printed temp dir path, inspect git/logs there
- re-run script (it creates a new timestamped temp repo by default)
- optionally run with
--nixpkgs-dirto reuse a local checkout
- detect version from
drugclaw-drug-discovery-agent/README.mdmarkdown · 22.7 KB
DrugClaw
DrugClaw is an AI Research Assistant for Accelerated Drug Discovery, implemented as a Rust multi-channel agent runtime. One agent core serves chat channels, the local Web UI, hooks, scheduled tasks, and domain skills without splitting the product into separate bots.
TL;DR
- DrugClaw is a multi-channel AI runtime focused on drug-discovery research workflows.
- It combines tool use, memory, skills, scheduler, hooks, and Web UI in one agent core.
- You can run it locally with
drugclaw setup,drugclaw doctor, anddrugclaw start. - Treat docking/QSAR/ADMET outputs as prioritization signals, not experimental proof.
Table of Contents
- What DrugClaw Is
- Current Scope
- Capability Boundary
- Prerequisites
- Demo Examples
- Install
- Quick Start
- Minimal Config
- Core Concepts
- Web UI And Hooks
- Science Skills
- Configuration Model
- CLI
- Shell Scripts
- Development
- Architecture
- Documentation Map
Built with Rust. 🦀
This project is built on top of microclaw.
What DrugClaw Is
DrugClaw combines drug-discovery research workflows with a general-purpose agent runtime:
- a channel-agnostic agent loop with tool use and session resume
- a provider-agnostic LLM layer with Anthropic and OpenAI-compatible backends
- persistent SQLite storage for chats, memory, auth, and observability
- a local Web UI and HTTP hook surface for operations and automation
- a skills system for reproducible workflows, including bioinformatics, chemistry, and docking
Current channel adapters include:
- Telegram
- Discord
- Slack
- Feishu / Lark
- Matrix
- WhatsApp Cloud API
- IRC
- iMessage
- Nostr
- Signal
- DingTalk
- Web
Current Scope
DrugClaw is already useful as:
- a tool-using chat agent for code and file operations
- a multi-chat automation runtime with hooks and scheduled tasks
- a memory-backed assistant with file memory plus structured SQLite memory
- a local operator console through the Web UI
- a research assistant for literature review, public database triage, molecular property analysis, DrugBank retrieval, QSAR, and docking workflows
The runtime is generic enough to automate other workflows, but the product direction is explicitly drug-discovery research acceleration.
Capability Boundary
DrugClaw is strong at:
- literature and public-database lookup
- structured note-taking over biological and chemical artifacts
- reproducible scripting for bioinformatics and computational chemistry
- heuristic prioritization through docking, ADMET triage, QSAR, and structure-aware scoring
- moving from chat intent to saved artifacts, reports, and follow-up analyses
DrugClaw is not:
- a wet-lab automation system
- a substitute for medicinal chemistry or structural biology judgment
- a clinically validated ADMET or affinity oracle
- a regulatory, diagnostic, or treatment decision system
- proof that a compound works in vitro, in vivo, or in humans
When DrugClaw reports docking scores, QSAR predictions, ADMET heuristics, or affinity estimates, those outputs should be treated as prioritization signals only.
Prerequisites
- macOS or Linux
- Docker Desktop
- Anthropic API key
Demo Examples
Below are live demonstrations of DrugClaw handling real tasks via Telegram.
- Protein Structure Rendering
Fetch a PDB structure, render it in rainbow coloring with PyMOL, and send the image.
- PubMed Literature Search
Search PubMed for recent high-impact papers and provide structured summaries.
- Hydrogen Bond Analysis
Visualize hydrogen bonds between a ligand and protein in PDB 3BIK.
- Target Intelligence Dossier
Build a concise target dossier by combining UniProt, OpenTargets, Reactome, STRING, ClinVar, and known-drug evidence into one brief.
- Compound Database Triage
Query PubChem, ChEMBL, and BindingDB for a compound or target, normalize the returned activity records, and send back a ranked summary table.
- Docking Workflow Summary
Generate the search box, run docking, and return the top poses with a compact report.
Install
One-line installer
curl -fsSL https://drugclaw.com/install.sh | bashWhen Docker is installed and the daemon is reachable, the installer also tries to build the default science sandbox image
drugclaw-drug-sandbox:latest.Windows PowerShell installer
iwr https://drugclaw.com/install.ps1 -UseBasicParsing | iexFrom source
git clone https://github.com/DrugClaw/DrugClaw.git cd drugclaw cargo build npm --prefix web install npm --prefix web run buildUninstall
./uninstall.shQuick Start
1. Create config
cp drugclaw.config.example.yaml drugclaw.config.yaml2. Run setup and diagnostics
drugclaw setup drugclaw doctorIf the default sandbox image is already present locally,
drugclaw setupdefaults the bash sandbox to enabled.3. Start runtime
drugclaw start4. Open the local Web UI
By default the UI listens on
http://127.0.0.1:10961.Minimal Config
A smallest practical config is usually Web-first, then add channels as needed.
llm_provider: "anthropic" api_key: "replace-me" model: "" data_dir: "./drugclaw.data" working_dir: "./tmp" working_dir_isolation: "chat" channels: web: enabled: true telegram: enabled: false web_host: "127.0.0.1" web_port: 10961Recommended next steps:
- enable one chat channel under
channels: - set
soul_pathor addSOUL.md - enable sandboxing for code execution when you need stronger isolation
- use
drugclaw web password-generatefor Web operator access
Core Concepts
Agent loop
The runtime does one thing consistently across channels:
- load chat state and memory
- build the system prompt plus skills catalog
- call the selected model with tool schemas
- execute tools when requested
- persist the updated session and artifacts
The shared loop lives in src/agent_engine.rs. Channels are ingress and egress adapters, not separate agent implementations.
Memory
DrugClaw has two memory layers:
- file memory:
AGENTS.mdplus chat-scoped files underruntime/groups/ - structured memory: SQLite-backed facts, confidence, supersession, and observability
This lets the runtime keep durable context without forcing every instruction into a single prompt.
Skills
Bundled skills currently include:
bio-toolsbio-db-toolsbayesian-optimization-toolsomics-toolsgrn-toolstarget-intelligence-toolsvariant-analysis-toolspharma-db-toolschem-toolspharma-ml-toolsliterature-review-toolsmedical-data-toolsclinical-research-toolsmedical-qms-toolsstat-modeling-toolssurvival-analysis-toolsscientific-visualization-toolsscientific-workflow-toolsdocking-tools- document, spreadsheet, PDF, GitHub, weather, and macOS utility skills
Bundled domain skills now cover:
- sequence analysis and general bioinformatics workflows
- public biology database lookup across UniProt, PDB, AlphaFold, ClinVar, dbSNP, gnomAD, Ensembl, GEO, InterPro, KEGG, OpenTargets, Reactome, and STRING
- AnnData, single-cell, BAM or CRAM, and mzML dataset triage
- Arboreto-based gene regulatory network inference with GRNBoost2 or GENIE3
- local VCF, SNV, indel, and SV summarization plus target-intelligence dossiers
- public drug-discovery database lookup across PubChem, ChEMBL, BindingDB, openFDA, ClinicalTrials.gov, and OpenAlex
- datamol, molfeat, PyTDC, and medchem-backed pharma ML preparation
- DeepChem, RDKit, PySCF, assay normalization, QSAR, virtual screening, and DrugBank lookup
- hypothesis tests, statsmodels regression, Kaplan-Meier, Cox modeling, and reusable scientific figures
- citation cleanup, evidence matrices, hypothesis framing, and reproducibility checklists
- Bayesian optimization for bounded experiment suggestion and parameter tuning
- DICOM metadata inspection, biosignal analysis, and cohort-table profiling for medical research datasets
- clinical-research design, reporting-guideline selection, and study-planning support
- Vina-based docking plus downstream chemistry post-processing
See docs/operations/science-runtime.md for runtime requirements.
Hooks
Hooks let you gate or modify LLM and tool traffic at runtime.
Supported events:
BeforeLLMCallBeforeToolCallAfterToolCall
Supported outcomes:
allowblockmodify
See docs/hooks/HOOK.md.
ClawHub
ClawHub is the registry layer for discovering and installing skills.
Use:
drugclaw skill search <query> drugclaw skill install <slug> drugclaw skill listReference: docs/clawhub/overview.md
Web UI And Hooks
The local Web surface is not an afterthought. It exposes:
- session and history browsing across channels
- auth and API key management
- metrics and memory observability
- config self-check and runtime operations
- HTTP hook endpoints for automation ingress
Important endpoints:
POST /hooks/agentPOST /api/hooks/agentPOST /hooks/wakePOST /api/hooks/wake
Reference: docs/operations/http-hook-trigger.md
Science Skills
DrugClaw now ships a non-trivial scientific workflow layer.
bio-toolsUse for:
- FASTA / FASTQ / BAM / BED workflows
- BLAST, alignment, QC, plotting, structure rendering
- literature search and general bioinformatics scripting
bio-db-toolsUse for API-backed lookup of:
- UniProt
- RCSB PDB
- AlphaFold DB
- ClinVar
- dbSNP
- gnomAD
- Ensembl
- GEO
- InterPro
- KEGG
- OpenTargets
- Reactome
- STRING
Bundled template:
skills/science/bio-db-tools/templates/bio_db_lookup.py
omics-toolsUse for:
h5adand AnnData triage before Scanpy or scvi workflows- BAM or CRAM region inspection with pysam
- mzML experiment inventory before pyOpenMS workflows
Bundled templates:
skills/science/omics-tools/templates/single_cell_profile.pyskills/science/omics-tools/templates/pysam_region_profile.pyskills/science/omics-tools/templates/mzml_summary.py
grn-toolsUse for:
- GRNBoost2 or GENIE3 regulatory-edge inference
- transcription factor to target ranking from bulk or single-cell expression matrices
- TF-whitelist constrained GRN runs with Arboreto
Bundled template:
skills/genomics/grn-tools/templates/arboreto_grn.py
variant-analysis-toolsUse for:
- local VCF or BCF summarization
- VAF, depth, PASS, and consequence filtering
- SNV, indel, and SV mutation-class counts before downstream annotation
Bundled template:
skills/genomics/variant-analysis-tools/templates/variant_report.py
target-intelligence-toolsUse for:
- one-file target briefs spanning identifiers, disease evidence, known drugs, pathways, and interaction partners
- compact target-validation snapshots before screening or docking
- integrating UniProt, OpenTargets, STRING, Reactome, ClinVar, and gnomAD signals into one dossier
Bundled template:
skills/research/target-intelligence-tools/templates/target_dossier.py
pharma-db-toolsUse for API-backed lookup of:
- PubChem
- ChEMBL
- BindingDB measured affinities
- openFDA drug labels, events, NDC, recalls, approvals, and shortages
- ClinicalTrials.gov
- OpenAlex
Bundled template:
skills/pharma/pharma-db-tools/templates/pharma_db_lookup.py
chem-toolsUse for:
- DeepChem featurization
- RDKit descriptors
- heuristic ADMET screening
- DrugBank local or online lookup
- assay-table normalization
- QSAR and bioactivity prediction
- ligand-only and structure-aware affinity prediction
- virtual screening reranking
pharma-ml-toolsUse for:
- datamol-backed library profiling and scaffold summaries
- molfeat feature generation for QSAR or ranking workflows
- PyTDC benchmark dataset fetch and split export
- medchem rule and alert screening before prioritization
Bundled templates:
skills/pharma/pharma-ml-tools/templates/datamol_library_profile.pyskills/pharma/pharma-ml-tools/templates/molfeat_featurize.pyskills/pharma/pharma-ml-tools/templates/pytdc_dataset_fetch.pyskills/pharma/pharma-ml-tools/templates/medchem_screen.py
literature-review-toolsUse for:
- citation-table normalization and deduplication
- lightweight BibTeX export from local metadata tables
- evidence-matrix assembly for review writing or gap mapping
Bundled templates:
skills/science/literature-review-tools/templates/citation_table_normalize.pyskills/science/literature-review-tools/templates/evidence_matrix.py
medical-data-toolsUse for:
- DICOM metadata inspection and basic de-identification
- ECG, PPG, EDA, RSP, or EMG analysis with NeuroKit2
- cohort-table profiling for clinical research datasets
Bundled templates:
skills/medical/medical-data-tools/templates/dicom_inspect.pyskills/medical/medical-data-tools/templates/neuro_signal_analyze.pyskills/medical/medical-data-tools/templates/clinical_cohort_profile.py
clinical-research-toolsUse for:
- study design and endpoint planning
- reporting-guideline routing such as CONSORT, STROBE, PRISMA, STARD, TRIPOD, SPIRIT, and ICH E3
- protocol, SAP, and evidence-synthesis support
- bias, confounding, and sample-size review
medical-qms-toolsUse for:
- ISO 13485 or FDA QMSR documentation planning
- gap assessment for QMS procedures and records
- CAPA, complaint, audit, supplier, and design-control documentation review
stat-modeling-toolsUse for:
- statistical hypothesis tests with explicit result export
- OLS, logistic, and Poisson regression with statsmodels
- coefficient tables, confidence intervals, and model-fit summaries
Bundled templates:
skills/science/stat-modeling-tools/templates/stat_test_report.pyskills/science/stat-modeling-tools/templates/statsmodels_regression.py
survival-analysis-toolsUse for:
- Kaplan-Meier summaries and plots
- log-rank comparisons between groups
- Cox proportional hazards baselines with hazard-ratio export
Bundled template:
skills/science/survival-analysis-tools/templates/survival_analysis.py
scientific-visualization-toolsUse for:
- static publication-style figures with seaborn or matplotlib
- interactive Plotly HTML charts for exploratory research review
- reusable plotting scripts parameterized by column names
Bundled templates:
skills/science/scientific-visualization-tools/templates/publication_plot.pyskills/science/scientific-visualization-tools/templates/interactive_plot.py
scientific-workflow-toolsUse for:
- hypothesis framing from observations
- peer-review style critique for rigor and claim scope
- reproducibility checklist generation
- research-method planning and scientific-writing structure
Bundled template:
skills/science/scientific-workflow-tools/templates/reproducibility_checklist.py
bayesian-optimization-toolsUse for:
- proposing the next experiment from bounded numeric parameters
- balancing exploration and exploitation over expensive assay or reaction runs
- tuning conditions or hyperparameters with Gaussian-process surrogates
Bundled template:
skills/research/bayesian-optimization-tools/templates/bayesian_optimize.py
docking-toolsUse for:
- receptor / ligand preparation
- AutoDock Vina workflows
- box generation
- PyMOL rendering
- docking reports and chemistry post-processing
Bundled templates:
skills/pharma/docking-tools/templates/docking_workflow.pyskills/pharma/docking-tools/templates/docking_manifest.example.json
Sandbox images
Build the bundled runtime image when you need the full scientific and docking toolchain:
docker build -f docker/drug-sandbox.Dockerfile -t drugclaw-drug-sandbox:latest .Those Dockerfiles read version-constrained requirements from
docker/requirements-*.txtso the science stack does not drift on every rebuild.Design choice:
drug-sandboxis the canonical shared runtime for bio, omics, chemistry, literature, medical-research, and docking skills- the legacy
drug-sandbox-dockingtag is retained only as a compatibility alias for older configs and scripts - dedicated
med-sandboxorchem-sandboximages are not provided because the current dependency overlap is high and the operator complexity is not worth it
Reference: docs/operations/science-runtime.md
Configuration Model
The main config file is
drugclaw.config.yaml.Important fields:
llm_provider,api_key,modeldata_dir,working_dir,working_dir_isolationchannels.*sandbox.*soul_path,souls_dirplugins.*clawhub_*voice_provider,voice_transcription_commandweb_host,web_port
Useful facts:
channels:is the preferred configuration surface for modern channel setup.SOUL.mdcan be global, channel-level, or account-level.- built-in skills are installed under
<data_dir>/skillsunlessskills_diroverrides that path. - runtime env overrides still use
MICROCLAW_*prefixes for compatibility, includingMICROCLAW_CONFIGandMICROCLAW_SKILLS_DIR.
Start from drugclaw.config.example.yaml.
CLI
Main commands:
drugclaw start drugclaw acp drugclaw setup drugclaw doctor drugclaw gateway drugclaw skill drugclaw hooks drugclaw web drugclaw reembed drugclaw upgrade drugclaw versionExamples:
drugclaw web password-generate drugclaw hooks list drugclaw gateway status drugclaw skill search dockingACP stdio mode (optional)
DrugClaw can run as an Agent Client Protocol (ACP) server over stdio:
drugclaw acpUse this mode when another local tool needs a sessioned DrugClaw runtime over stdio instead of Telegram/Discord/Web ingress.
Shell Scripts
The repository keeps operator scripts aligned with the runtime:
install.sh: install the latest release binary on macOS or Linuxuninstall.sh: remove the installed binarystart.sh: build the Web UI and run the local runtime from sourcecheck.sh: run the standard local validation setdeploy.sh: run release automation and optional nixpkgs follow-upscripts/test_http_hooks.sh: smoke-test Web hook endpointsscripts/matrix-smoke-test.sh: end-to-end Matrix ingestion smoke testscripts/update-nixpkgs.sh: update thedrugclawpackage innixpkgs
These scripts are repository workflow glue, not a second control plane.
Development
Build
cargo build npm --prefix web run build npm --prefix website run buildTest
cargo test ./check.shDocs drift guard
node scripts/generate_docs_artifacts.mjs --checkArchitecture
Key files:
- src/main.rs: CLI entrypoint
- src/runtime.rs: application wiring
- src/agent_engine.rs: shared agent loop
- src/llm.rs: provider abstraction
- src/web.rs: Web router and APIs
- src/scheduler.rs: scheduled tasks and reflector loop
- src/skills.rs: skill discovery and activation
- src/mcp.rs: MCP integration
- src/hooks.rs: runtime hook system
Supporting crates:
drugclaw-coredrugclaw-storagedrugclaw-toolsdrugclaw-channelsdrugclaw-app
Documentation Map
Start here for deeper docs:
- docs/generated/tools.md
- docs/generated/config-defaults.md
- docs/generated/provider-matrix.md
- docs/a2a.md
- docs/operations/acp-stdio.md
- docs/operations/runbook.md
- docs/operations/science-runtime.md
- docs/releases/pr-release-checklist.md
- docs/security/execution-model.md
License
Apache License 2.0. See LICENSE.
Star History
TalentDelta provenance record (added at packaging - upstream tree otherwise unchanged)
- Packaged for: TalentDelta catalogue (draft; gated by review)
- Upstream: https://github.com/DrugClaw/DrugClaw
- Pinned commit:
960a6e0e5935533099dcd1774f6d1280e7d59d55(2026-03-23) - Licence: Apache-2.0 - the upstream
LICENSEfile ships unchanged - Upstream author credit: DrugClaw project contributors
- Runtime: Rust toolchain (cargo workspace), ./install.sh, or the shipped Dockerfile/docker-compose; model provider keys
- Changes made at packaging: this section was appended to
README.md. Upstream paths deliberately NOT shipped in this archive:screenshots(upstream demo imagery, regenerable). Every other file is byte-identical to the pinned commit. - Safety read of the packaged archive (packaging time, 2026-09-30): no live secrets or credentials. Credentials-shaped values are declared sample placeholders; see the pack change note on the catalogue entry for the file-by-file detail.
drugclaw-drug-discovery-agent/README_CN.mdmarkdown · 20.0 KB
DrugClaw
DrugClaw 是一个面向加速药物发现的 AI Research Assistant,以 Rust 多渠道智能体运行时的形式实现。它把聊天渠道、本地 Web UI、HTTP hooks、定时任务和领域技能放进同一套 agent core,而不是拆成多个彼此割裂的 bot。
TL;DR
- DrugClaw 是一个聚焦药物发现研究场景的多渠道 AI 运行时。
- 它把工具调用、长期记忆、技能系统、定时任务、hooks 和 Web UI 集成到同一个 agent core。
- 本地启动最短路径是:
drugclaw setup、drugclaw doctor、drugclaw start。 - docking / QSAR / ADMET 的输出应视为优先级信号,而非实验结论。
目录
- DrugClaw 是什么
- 当前范围
- 能力边界
- Prerequisites
- Demo Examples
- 安装
- 快速开始
- 最小配置示例
- 核心概念
- Web UI 与 Hooks
- 科学技能能力层
- 配置模型
- CLI
- Shell 脚本
- 开发
- 架构入口
- 文档索引
Built with Rust. 🦀
本项目在 microclaw 的基础上构建。
DrugClaw 是什么
DrugClaw 把药物发现研究工作流和通用 agent runtime 组合在一起:
- 渠道无关的 agent loop,支持工具调用与会话恢复
- provider 无关的 LLM 层,原生支持 Anthropic 与 OpenAI-compatible 后端
- 基于 SQLite 的持久化存储,用于聊天、记忆、鉴权与观测
- 面向运维和自动化的本地 Web UI 与 HTTP hook 接口
- 可复用技能系统,覆盖生信、化学与分子对接工作流
当前渠道适配器包括:
- Telegram
- Discord
- Slack
- Feishu / Lark
- Matrix
- WhatsApp Cloud API
- IRC
- iMessage
- Nostr
- Signal
- DingTalk
- Web
当前范围
DrugClaw 现在已经适合作为:
- 一个可调用工具的聊天智能体
- 一个带 hooks 与定时任务的多聊天自动化运行时
- 一个带持久记忆的长期助手
- 一个可用于运营和排障的本地控制台
- 一个面向文献检索、公共数据库检索、分子性质分析、DrugBank、QSAR 与 docking 的研究助手
这套运行时本身足够通用,也能承载别的自动化任务,但产品方向明确聚焦在药物发现研究加速。
能力边界
DrugClaw 擅长:
- 文献与公共数据库检索
- 面向生物和化学产物的结构化整理
- 生信与计算化学的可复现脚本流程
- 通过 docking、ADMET、QSAR、结构型打分做启发式优先级排序
- 把聊天里的研究意图转成可落盘的分析产物、报告与后续任务
DrugClaw 不是:
- 湿实验自动化系统
- 药化或结构生物学专家判断的替代品
- 经过临床或监管验证的 ADMET / affinity 预测系统
- 诊断、治疗或监管决策系统
- 证明某个化合物在体外、体内或人体中有效的证据
当 DrugClaw 给出 docking 分数、QSAR 预测、ADMET 启发式或亲和力估计时,应把它们视为优先级排序信号,而不是实验事实。
Prerequisites
- macOS 或 Linux
- Docker Desktop
- Anthropic API key
Demo Examples
下面是 DrugClaw 通过 Telegram 处理真实任务的演示示例。
- Protein Structure Rendering
获取一个 PDB 结构,用 PyMOL 按 rainbow coloring 渲染,并把图片发送回来。
- PubMed Literature Search
检索近期高影响力的 PubMed 论文,并输出结构化摘要。
- Hydrogen Bond Analysis
可视化 PDB 3BIK 中配体与蛋白之间的氢键。
- Target Intelligence Dossier
整合 UniProt、OpenTargets、Reactome、STRING、ClinVar 和已知药物证据,生成一份紧凑的靶点 dossier。
- Compound Database Triage
针对某个化合物或靶点检索 PubChem、ChEMBL 和 BindingDB,标准化活性记录,并返回排序后的摘要表。
- Docking Workflow Summary
生成 search box,执行 docking,并返回 top poses 和简洁报告。
安装
一键安装
curl -fsSL https://drugclaw.com/install.sh | bash如果本机装了 Docker 且 daemon 可用,安装脚本还会尝试自动构建默认科学 sandbox 镜像
drugclaw-drug-sandbox:latest。Windows PowerShell 安装
iwr https://drugclaw.com/install.ps1 -UseBasicParsing | iex从源码运行
git clone https://github.com/DrugClaw/DrugClaw.git cd drugclaw cargo build npm --prefix web install npm --prefix web run build卸载
./uninstall.sh快速开始
1. 创建配置文件
cp drugclaw.config.example.yaml drugclaw.config.yaml2. 跑 setup 和 doctor
drugclaw setup drugclaw doctor如果本地已经准备好默认 sandbox 镜像,
drugclaw setup会默认把 bash sandbox 设为开启。3. 启动运行时
drugclaw start4. 打开本地 Web UI
默认地址是
http://127.0.0.1:10961。最小配置示例
最实用的起点通常是先开 Web,再逐步加渠道。
llm_provider: "anthropic" api_key: "replace-me" model: "" data_dir: "./drugclaw.data" working_dir: "./tmp" working_dir_isolation: "chat" channels: web: enabled: true telegram: enabled: false web_host: "127.0.0.1" web_port: 10961建议继续补的几项:
- 在
channels:下启用至少一个聊天渠道 - 设置
soul_path或放置SOUL.md - 对代码执行开启 sandbox
- 用
drugclaw web password-generate初始化 Web 侧操作员访问
核心概念
Agent loop
DrugClaw 在所有渠道里遵循同一条主链路:
- 加载聊天状态和记忆
- 组装 system prompt 与技能目录
- 调用模型并暴露工具 schema
- 在模型请求时执行工具
- 持久化新的会话和产物
共享循环位于 src/agent_engine.rs。渠道只是收发适配层,不是独立 agent。
记忆系统
DrugClaw 有两层记忆:
- 文件记忆:
AGENTS.md与runtime/groups/下的聊天级文件 - 结构化记忆:SQLite 中的事实、置信度、覆盖关系与观测日志
这让长期上下文不必全部硬塞进一次 prompt。
技能系统
当前随包技能包括:
bio-toolsbio-db-toolsbayesian-optimization-toolsomics-toolsgrn-toolstarget-intelligence-toolsvariant-analysis-toolspharma-db-toolschem-toolspharma-ml-toolsliterature-review-toolsmedical-data-toolsclinical-research-toolsmedical-qms-toolsstat-modeling-toolssurvival-analysis-toolsscientific-visualization-toolsscientific-workflow-toolsdocking-tools- 文档、表格、PDF、GitHub、天气与若干 macOS 工具技能
其中领域相关技能已经覆盖:
- 序列分析与通用生信流程
- UniProt、PDB、AlphaFold、ClinVar、dbSNP、gnomAD、Ensembl、GEO、InterPro、KEGG、OpenTargets、Reactome、STRING 的数据库检索
- AnnData、single-cell、BAM / CRAM、mzML 数据集的快速体检与预分析
- 基于 Arboreto 的基因调控网络推断,支持 GRNBoost2 和 GENIE3
- 本地 VCF / SNV / indel / SV 总结,以及面向靶点的 intelligence dossier
- PubChem、ChEMBL、BindingDB、openFDA、ClinicalTrials.gov、OpenAlex 的药物发现数据库检索
- datamol、molfeat、PyTDC、medchem 驱动的 pharma ML 预处理
- DeepChem、RDKit、PySCF、DrugBank、ADMET、QSAR、virtual screening
- 假设检验、statsmodels 回归、Kaplan-Meier、Cox 建模与可复用科研绘图
- 文献引用清洗、evidence matrix、假设框架与可复现性清单
- 面向有界参数空间的 Bayesian optimization 实验建议
- 面向医学研究数据的 DICOM 元数据检查、生理信号分析与 cohort 表概览
- 临床研究设计、报告规范选择与统计规划支持
- AutoDock Vina 对接与后续化学重排序
运行要求见 docs/operations/science-runtime.md。
Hooks
Hooks 用于在运行时拦截和修饰 LLM / 工具流量。
支持事件:
BeforeLLMCallBeforeToolCallAfterToolCall
支持动作:
allowblockmodify
ClawHub
ClawHub 是 DrugClaw 的技能发现与安装层。
常用命令:
drugclaw skill search <query> drugclaw skill install <slug> drugclaw skill listWeb UI 与 Hooks
本地 Web 面不是附属功能,而是操作面的一部分。它暴露:
- 跨渠道会话与历史浏览
- 鉴权与 API key 管理
- metrics 与 memory observability
- 配置自检与运行操作
- 供自动化接入的 HTTP hook 入口
重要接口:
POST /hooks/agentPOST /api/hooks/agentPOST /hooks/wakePOST /api/hooks/wake
参考:docs/operations/http-hook-trigger.md
科学技能能力层
DrugClaw 现在已经自带一层较完整的科学工作流能力。
bio-tools适合:
- FASTA / FASTQ / BAM / BED
- BLAST、比对、QC、绘图、结构渲染
- 文献检索与一般生信脚本流程
bio-db-tools适合检索:
- UniProt
- RCSB PDB
- AlphaFold DB
- ClinVar
- dbSNP
- gnomAD
- Ensembl
- GEO
- InterPro
- KEGG
- OpenTargets
- Reactome
- STRING
附带模板:
skills/science/bio-db-tools/templates/bio_db_lookup.py
omics-tools适合:
- 在进入 Scanpy 或 scvi 工作流前检查
h5ad/ AnnData - 用 pysam 做 BAM / CRAM 区域级检查
- 在 pyOpenMS 深入处理前盘点 mzML 实验结构
附带模板:
skills/science/omics-tools/templates/single_cell_profile.pyskills/science/omics-tools/templates/pysam_region_profile.pyskills/science/omics-tools/templates/mzml_summary.py
grn-tools适合:
- 用 GRNBoost2 或 GENIE3 做调控边推断
- 从 bulk 或 single-cell 表达矩阵中做 TF 到 target 的排序
- 基于 Arboreto 做带 TF 白名单约束的 GRN 推断
附带模板:
skills/genomics/grn-tools/templates/arboreto_grn.py
variant-analysis-tools适合:
- 本地 VCF / BCF 总结
- 依据 VAF、深度、PASS、consequence 做过滤
- 在下游注释前先统计 SNV、indel、SV 的数量和分布
附带模板:
skills/genomics/variant-analysis-tools/templates/variant_report.py
target-intelligence-tools适合:
- 生成覆盖 identifiers、disease evidence、known drugs、pathways、interaction partners 的靶点简报
- 在 screening 或 docking 前生成 target validation 快照
- 把 UniProt、OpenTargets、STRING、Reactome、ClinVar、gnomAD 信号整合成一份 dossier
附带模板:
skills/research/target-intelligence-tools/templates/target_dossier.py
pharma-db-tools适合检索:
- PubChem
- ChEMBL
- BindingDB 实测亲和力
- openFDA 的药品标签、不良事件、NDC、召回、审批和短缺数据
- ClinicalTrials.gov
- OpenAlex
附带模板:
skills/pharma/pharma-db-tools/templates/pharma_db_lookup.py
chem-tools适合:
- DeepChem 特征化
- RDKit 描述符
- 启发式 ADMET 筛查
- DrugBank 本地或在线检索
- assay 表标准化
- QSAR / bioactivity 预测
- ligand-only 与 structure-aware 亲和力预测
- virtual screening 重排序
pharma-ml-tools适合:
- 用 datamol 做 compound library 体检和 scaffold 概览
- 用 molfeat 生成 QSAR / ranking 需要的特征
- 用 PyTDC 拉取 benchmark 数据集和切分结果
- 用 medchem 做规则和 alert 级的初筛
附带模板:
skills/pharma/pharma-ml-tools/templates/datamol_library_profile.pyskills/pharma/pharma-ml-tools/templates/molfeat_featurize.pyskills/pharma/pharma-ml-tools/templates/pytdc_dataset_fetch.pyskills/pharma/pharma-ml-tools/templates/medchem_screen.py
literature-review-tools适合:
- 引文表清洗与去重
- 基于本地元数据表导出轻量 BibTeX
- 为综述写作或 gap mapping 生成 evidence matrix
附带模板:
skills/science/literature-review-tools/templates/citation_table_normalize.pyskills/science/literature-review-tools/templates/evidence_matrix.py
medical-data-tools适合:
- DICOM 元数据检查与基础去标识化
- 使用 NeuroKit2 分析 ECG、PPG、EDA、RSP、EMG
- 面向临床研究数据集的 cohort 表概览
附带模板:
skills/medical/medical-data-tools/templates/dicom_inspect.pyskills/medical/medical-data-tools/templates/neuro_signal_analyze.pyskills/medical/medical-data-tools/templates/clinical_cohort_profile.py
clinical-research-tools适合:
- 研究设计与终点规划
- CONSORT、STROBE、PRISMA、STARD、TRIPOD、SPIRIT、ICH E3 等规范路由
- protocol、SAP 与证据综述支持
- 偏倚、混杂与样本量假设复查
medical-qms-tools适合:
- ISO 13485 / FDA QMSR 文档规划
- QMS 程序与记录的缺口分析
- CAPA、投诉、审计、供应商与设计控制文档复查
stat-modeling-tools适合:
- 带结构化输出的统计检验
- 基于 statsmodels 的 OLS、logistic、Poisson 回归
- 系数表、置信区间与模型拟合摘要
附带模板:
skills/science/stat-modeling-tools/templates/stat_test_report.pyskills/science/stat-modeling-tools/templates/statsmodels_regression.py
survival-analysis-tools适合:
- Kaplan-Meier 摘要和曲线
- 组间 log-rank 比较
- 导出 hazard ratio 的 Cox 比例风险基线模型
附带模板:
skills/science/survival-analysis-tools/templates/survival_analysis.py
scientific-visualization-tools适合:
- 用 seaborn / matplotlib 生成静态论文风格图
- 用 Plotly 生成交互式 HTML 图
- 通过列名参数化的可复用绘图脚本
附带模板:
skills/science/scientific-visualization-tools/templates/publication_plot.pyskills/science/scientific-visualization-tools/templates/interactive_plot.py
scientific-workflow-tools适合:
- 基于观察现象组织研究假设
- 用 peer-review 视角检查 rigor 与 claim scope
- 生成 reproducibility checklist
- 研究方法规划与科学写作结构化支持
附带模板:
skills/science/scientific-workflow-tools/templates/reproducibility_checklist.py
bayesian-optimization-tools适合:
- 从有界数值参数空间中推荐下一轮实验
- 在高成本 assay 或 reaction 条件搜索中平衡探索与利用
- 用 Gaussian process surrogate 调参或做闭环实验建议
附带模板:
skills/research/bayesian-optimization-tools/templates/bayesian_optimize.py
docking-tools适合:
- 受体 / 配体预处理
- AutoDock Vina 对接
- 搜索盒生成
- PyMOL 渲染
- docking 报告与化学后处理
附带模板:
skills/pharma/docking-tools/templates/docking_workflow.pyskills/pharma/docking-tools/templates/docking_manifest.example.json
Sandbox 镜像
需要完整 science + docking 工具链时,构建内置统一镜像:
docker build -f docker/drug-sandbox.Dockerfile -t drugclaw-drug-sandbox:latest .这些 Dockerfile / requirements 清单读取
docker/requirements-*.txt里的版本约束,避免科学依赖在每次重建时无控制漂移。设计选择:
drug-sandbox是 bio、omics、chemistry、literature、medical-research、docking 共用的统一运行时- 旧的
drug-sandbox-dockingtag 只保留为兼容别名,方便旧配置和旧脚本继续工作 - 当前不额外提供
med-sandbox或chem-sandbox,因为依赖重叠太高,拆分只会增加运维复杂度
参考:docs/operations/science-runtime.md
配置模型
主配置文件是
drugclaw.config.yaml。关键字段:
llm_provider,api_key,modeldata_dir,working_dir,working_dir_isolationchannels.*sandbox.*soul_path,souls_dirplugins.*clawhub_*voice_provider,voice_transcription_commandweb_host,web_port
几条实用结论:
- 现代渠道配置优先走
channels: SOUL.md可以全局、按渠道、按账号覆盖- 内置技能默认安装到
<data_dir>/skills,也可以用skills_dir改路径 - 运行时环境变量仍然保留
MICROCLAW_*前缀兼容接口,例如MICROCLAW_CONFIG与MICROCLAW_SKILLS_DIR
从 drugclaw.config.example.yaml 开始最稳妥。
CLI
主命令:
drugclaw start drugclaw setup drugclaw doctor drugclaw gateway drugclaw skill drugclaw hooks drugclaw web drugclaw reembed drugclaw upgrade drugclaw version例子:
drugclaw web password-generate drugclaw hooks list drugclaw gateway status drugclaw skill search dockingShell 脚本
仓库内保留了一组与运行时配套的脚本:
install.sh:安装最新 release 二进制uninstall.sh:卸载本地安装的二进制start.sh:从源码构建 Web UI 并启动本地运行时check.sh:运行标准本地检查集deploy.sh:执行 release 自动化与可选 nixpkgs 后续动作scripts/test_http_hooks.sh:HTTP hooks 冒烟测试scripts/matrix-smoke-test.sh:Matrix 端到端冒烟测试scripts/update-nixpkgs.sh:更新nixpkgs中的drugclaw包
这些脚本是仓库工作流的胶水层,不是独立的第二套控制面。
开发
构建
cargo build npm --prefix web run build npm --prefix website run build测试
cargo test ./check.sh文档漂移检查
node scripts/generate_docs_artifacts.mjs --check架构入口
关键文件:
- src/main.rs:CLI 入口
- src/runtime.rs:应用装配
- src/agent_engine.rs:共享 agent loop
- src/llm.rs:provider 抽象层
- src/web.rs:Web 路由与 API
- src/scheduler.rs:定时任务与 reflector loop
- src/skills.rs:技能发现与激活
- src/mcp.rs:MCP 集成
- src/hooks.rs:运行时 hooks 系统
支撑 crate:
drugclaw-coredrugclaw-storagedrugclaw-toolsdrugclaw-channelsdrugclaw-app
文档索引
需要更细的文档时,从这里继续:
- docs/generated/tools.md
- docs/generated/config-defaults.md
- docs/generated/provider-matrix.md
- docs/operations/runbook.md
- docs/operations/science-runtime.md
- docs/releases/pr-release-checklist.md
- docs/security/execution-model.md
许可证
Apache License 2.0,见 LICENSE。
drugclaw-drug-discovery-agent/SOUL.mdmarkdown · 2.0 KB
DrugClaw Soul
I am DrugClaw, an AI Research Assistant for Accelerated Drug Discovery.
Role
- I turn drug-discovery research requests into concrete actions across chat channels, the local runtime, the Web UI, and scientific workflow skills.
- I prefer verified execution over speculation.
- I treat files, commands, APIs, skills, and memory as working surfaces, not abstract concepts.
Voice
- Be direct, calm, and useful.
- Default to concise answers with enough context to make the result auditable.
- Avoid filler, theatrics, and fake certainty.
- When the user needs depth, add it. When they need speed, do not slow them down.
Operating Principles
- Reliability over cleverness.
- Reproducibility over one-off heroics.
- Clear boundaries over hand-wavy promises.
- Progress over ceremony.
Working Rules
- Use tools first when a fact can be checked.
- Show the important result, the key artifact paths, and the next actionable step.
- Distinguish confirmed facts, model inferences, and open unknowns.
- Preserve user intent, but challenge weak assumptions when accuracy matters.
- Do not pretend a command, analysis, or lookup succeeded when it did not.
Domain Rules
- For software work: prefer minimal, reviewable changes with explicit validation.
- For operations work: state prerequisites, risk, and rollback surface.
- For bioinformatics, chemistry, docking, QSAR, and affinity tasks: report method, input scope, and uncertainty.
- Treat docking scores, ADMET heuristics, and ligand-only ML outputs as screening signals, not experimental truth.
- Do not present DrugClaw as a clinical decision-maker, regulatory system, or substitute for experimental validation.
- Do not blur the line between research acceleration and scientific proof.
Memory And Persona
- Respect chat-level context and long-term memory, but do not let stale memory override fresh evidence.
- Use SOUL as a stable operating contract; use skills as situational playbooks.
- If the user provides a stricter local or per-channel SOUL, follow that override.
drugclaw-drug-discovery-agent/TEST.mdmarkdown · 25.5 KB
DrugClaw Testing Guide
This document describes how to test every feature of DrugClaw. It includes both automated tests (unit/integration) and manual black-box functional tests organized by user stories.
Automated Tests
cargo test # Run all unit + integration tests cargo clippy # Lint check cargo fmt --check # Format checkBlack-Box Functional Tests
Since DrugClaw is a multi-platform bot with external dependencies (LLM APIs, Telegram/Discord/WhatsApp APIs, DuckDuckGo), many features require live interaction testing.
Prerequisites
- A working
drugclaw.config.yamlfile with valid credentials cargo buildsucceeds with zero errors- Bot is running:
cargo run -- start - A Telegram account to send messages
- (For group tests) A Telegram group with the bot added as a member
1. Startup & Configuration
# User Story Steps Expected 1.1 First launch without config Run drugclaw startwith no config.yaml presentAuto-launches setup wizard 1.2 Normal startup Fill config.yaml, run drugclaw startLogs: Database initialized / Memory manager initialized / Scheduler started 1.3 Missing required field Remove api_keyfrom config, startError message + launches setup wizard 1.4 Invalid timezone Set timezone: "Mars/Olympus"Startup error with invalid timezone message 1.5 CLI help drugclaw helpFull output: commands, features, config docs 1.6 CLI version drugclaw versionOutput: drugclaw {VERSION}1.7 Unknown command drugclaw foobar"Unknown command: foobar" + help text 1.8 Setup wizard drugclaw setupTUI interactive guide, provider/model selection 1.9 MICROCLAW_CONFIG env var MICROCLAW_CONFIG=/tmp/test.yaml drugclaw startLoads config from specified path
2. Private Chat -- Basic Conversation
# User Story Steps Expected 2.1 Send plain text Private chat: send "Hello" Bot responds; typing indicator visible during processing 2.2 Empty message Send message with no text/image/voice Bot does not respond, no crash 2.3 Very long input Send 10000-character text Bot processes and responds normally 2.4 Special characters Send <script>alert('xss')</script>XML-escaped, processed safely, no injection 2.5 Emoji/Unicode Send various emoji and CJK characters Processed and responded to normally 2.6 Long response splitting Ask for a 5000-character essay Response split at newline boundaries into multiple messages (max 4096 chars each) 2.7 Typing indicator persistence Send request requiring multiple tool calls Typing refreshes every ~4s until response completes 2.8 Rapid consecutive messages Send 5 messages quickly All stored; bot processes each without dropping any
3. Image Handling
# User Story Steps Expected 3.1 JPEG photo Send a JPEG photo Bot describes image content 3.2 PNG screenshot Send a PNG image Correctly identified as PNG, processed 3.3 WebP image Send a WebP format image Correctly identified as WebP, processed 3.4 GIF image Send an animated GIF Correctly identified as GIF, processed 3.5 Image + caption Send photo with caption "What is this?" Bot sees both image and caption, responds accordingly 3.6 Image without caption Send photo with no caption Bot proactively describes image content 3.7 Image session persistence Send image, then ask "What was in that image?" Bot recalls image from session (stored as [image was sent]placeholder)3.8 Large image Send high-resolution image (5MB+) Downloads largest resolution, processes without timeout
4. Voice Messages
# User Story Steps Expected 4.1 Voice transcription (with key) Send voice message (openai_api_key configured) Bot transcribes via Whisper, processes as text 4.2 Voice without key Send voice message (no openai_api_key) "Voice messages not supported (no Whisper API key configured)" 4.3 Transcription failure Send corrupted audio file Embeds [transcription failed: {error}], no crash4.4 Long voice message Send 2-minute voice message Transcribed and processed normally
5. Session Management
# User Story Steps Expected 5.1 Conversation continuity Msg 1: "My name is Alice"; Msg 2: "What's my name?" Bot remembers "Alice" 5.2 Tool context preserved Msg 1: "Create /tmp/test.txt with hello"; Msg 2: "What did you just create?" Bot remembers file operation (session includes tool_use/tool_result blocks) 5.3 /reset clears session Chat several turns, send /reset, ask "What did we talk about?"Bot does not remember previous session context 5.4 /archive session Chat several turns, send /archiveSession archived to <data_dir>/groups/<channel>/<chat_id>/conversations/<timestamp>.md5.5 Context compaction trigger Chat beyond max_session_messages threshold Old messages auto-summarized; recent messages kept verbatim 5.6 Memory after compaction After compaction, ask about an early topic Bot recalls key facts from summary (details may be lost) 5.7 Corrupted session recovery Manually corrupt sessions table JSON, then send message Falls back to DB history, no crash 5.8 Session survives restart Chat several turns → restart bot → continue chatting Session loaded from DB, conversation continues seamlessly
6. Group Chat
# User Story Steps Expected 6.1 No @mention = no reply Send plain message in group Bot does not reply; message is stored in DB 6.2 @mention triggers reply Send @botusername helloBot responds 6.3 Group catch-up Multiple users send messages, then @bot "summarize" Bot's response covers ALL messages since its last reply 6.4 allowed_groups whitelist (allowed) Configure allowed_groups with current group ID @mention works normally 6.5 allowed_groups whitelist (blocked) Configure allowed_groups WITHOUT current group ID @mention gets no response; messages still stored 6.6 allowed_groups empty Set allowed_groups to [] or omit All groups can @mention and get replies 6.7 Multi-group isolation Chat in Group A, then @mention in Group B Group B has independent session/memory, unaffected by Group A
7. Tool -- Bash Execution
# User Story Steps Expected 7.1 Simple command "Run echo hello world"Returns "hello world" 7.2 Non-zero exit code "Run exit 1"Reports exit code 1 7.3 stderr output "Run ls /nonexistent 2>&1"Returns stderr content 7.4 Command timeout "Run sleep 300"Times out after 120s 7.5 Output truncation Command producing >30KB output Output truncated with marker 7.6 Multi-step command "Count the .rs files in this directory" Bot uses bash correctly, returns result
8. Tool -- File Operations
# User Story Steps Expected 8.1 Create file "Create /tmp/test.txt with content hello" File created successfully 8.2 Read file "Read /tmp/test.txt" Returns file content with line numbers 8.3 Edit file "Change hello to world in /tmp/test.txt" Find-replace succeeds 8.4 Read nonexistent file "Read /nonexistent/file.txt" Error: file not found, no crash 8.5 Edit non-unique string File has multiple "hello", request replace Error: string not unique 8.6 Read with offset/limit "Read /tmp/big.txt from line 100, 20 lines" Returns specified range 8.7 Auto-create directories "Create /tmp/a/b/c/test.txt" Intermediate directories auto-created, file written 8.8 Path guard: .ssh "Read ~/.ssh/id_rsa" Blocked by path_guard 8.9 Path guard: .env "Read .env" Blocked by path_guard 8.10 Path guard: .aws "Read ~/.aws/credentials" Blocked by path_guard 8.11 Path guard: /etc/shadow "Read /etc/shadow" Blocked by path_guard
9. Tool -- File Search
# User Story Steps Expected 9.1 Glob search "Find all .rs files in the project" Returns matching file list 9.2 Glob no match "Find *.xyz files" Returns empty result or "no matches" 9.3 Grep content search "Search for 'fn main' in source code" Returns file names, line numbers, matching content 9.4 Grep regex search "Search for async fn.*execute"Regex matching works correctly 9.5 Grep no match "Search for 'xyznotexist123'" Returns no matches 9.6 Search excludes sensitive paths Glob/Grep results path_guard filters sensitive paths from results
10. Tool -- Web Search & Fetch
# User Story Steps Expected 10.1 Web search "Search for Rust programming language" Returns DuckDuckGo results (title + URL + snippet) 10.2 Web search no results Search for extremely obscure keywords Returns empty or "no results" 10.3 Web fetch "Fetch https://example.com" Returns plain text with HTML tags stripped 10.4 Web fetch large page Fetch page >20KB Content truncated to 20KB 10.5 Web fetch invalid URL "Fetch https://thisdomaindoesnotexist12345.com" Returns network error, no crash 10.6 Combined web research "Look up today's news and summarize" Bot combines web_search + web_fetch to complete task
11. Tool -- Mid-Conversation Messaging
# User Story Steps Expected 11.1 Progress update "Send 'Processing...' first, then tell me today's date" Receive TWO messages: progress + final reply 11.2 Only send_message "Use send_message to say 'test' and nothing else" Receive exactly ONE message "test", no empty reply 11.3 Cross-chat send (regular user) "Send a message to chat_id 99999" Permission denied 11.4 Cross-chat send (control chat) From control_chat_id, specify another chat_id Message sent successfully
12. Memory System
# User Story Steps Expected 12.1 Write chat memory "Remember I like Rust" Written to <data_dir>/groups/{chat_id}/AGENTS.md12.2 Read chat memory "What language do I like?" Bot recalls "Rust" from memory 12.3 Memory persists across /reset /reset, then "What do I like?"Still recalls (memory is independent of session) 12.4 Memory persists across restart Restart bot, then ask Still recalls (memory is file-persisted) 12.5 Write global memory (control) From control chat, write global memory Written to <data_dir>/groups/AGENTS.md12.6 Write global memory (regular) From regular chat, attempt global write "Permission denied: chat {id} cannot write global memory" 12.7 Read empty memory New chat, first read "No memory file found (not yet created)." 12.8 Memory overwrite Write twice with different content Second write completely replaces first 12.9 Cross-chat read memory (regular) Read another chat_id's memory Permission denied 12.10 Cross-chat read memory (control) From control chat, read other chat memory Returns content normally
13. Scheduled Tasks
# User Story Steps Expected 13.1 Create cron task "Remind me to drink water every 5 minutes" Returns Task #{id} scheduled, cron: 0 */5 * * * *13.2 Create one-time task "Send 'Happy New Year' at 2099-12-31T23:59:00+00:00" Returns Task #{id} scheduled (once) 13.3 Task actually fires Create per-minute task, wait 60-90s Automatically receive task execution result 13.4 List tasks "List my scheduled tasks" Shows all active/paused tasks with details 13.5 Pause task "Pause task #1" Confirmed paused; task stops firing 13.6 Resume task "Resume task #1" Confirmed resumed; task resumes firing 13.7 Cancel task "Cancel task #1" Confirmed cancelled; permanently stopped 13.8 View execution history "Show task #1 execution history" Shows recent runs (time, duration, success/fail, summary) 13.9 Nonexistent task "Pause task #9999" "Task #9999 not found" 13.10 Invalid cron expression Pass "not a cron" Invalid cron expression error 13.11 Invalid timezone Create task with timezone "invalid" Invalid timezone error 13.12 Cross-chat schedule (regular) Regular chat schedules for other chat_id Permission denied 13.13 Cross-chat schedule (control) Control chat schedules for other chat_id Created successfully 13.14 Empty task list New chat lists tasks "No scheduled tasks found for this chat." 13.15 One-time task completion Create imminent one-time task After firing, status becomes "completed"
14. Todo System
# User Story Steps Expected 14.1 Read empty todo New chat, read todos "No tasks in the todo list." 14.2 Create todo list "Create 3 todo items for me" Bot uses todo_write, returns formatted list 14.3 Update todo status "Mark the first item as completed" Bot reads existing → modifies status → writes back 14.4 Todo full replacement Write, then write completely different list Old list fully replaced 14.5 Cross-chat read todo (regular) Read another chat_id's todos Permission denied 14.6 Cross-chat read todo (control) Control chat reads other chat_id's todos Returns normally 14.7 Todo status icons Create list with pending/in_progress/completed Shows [ ],[~],[x]respectively
15. Chat Export
# User Story Steps Expected 15.1 Export current chat "Export this chat's history" Generates markdown file, returns path and message count 15.2 Export to specific path Specify path parameter File written to specified path 15.3 Export empty chat Export chat_id with no messages "No messages found for chat {id}." 15.4 Export format verification Export, then read the file Each message: **{sender}** ({timestamp})\n\n{content}\n\n---15.5 Cross-chat export (regular) Export another chat_id Permission denied 15.6 Cross-chat export (control) Control chat exports other chat_id Exported successfully
16. Sub-Agent
# User Story Steps Expected 16.1 Basic sub-agent task "Use sub_agent to count .rs files in this project" Sub-agent uses glob/bash, result returned to main conversation 16.2 Sub-agent cannot schedule "Use sub_agent to create a scheduled task" Sub-agent reports no scheduling tools available 16.3 Sub-agent cannot send_message "Use sub_agent to send me a message" Sub-agent reports no send_message tool 16.4 Sub-agent cannot write memory "Use sub_agent to save a memory" Sub-agent has no write_memory tool 16.5 Sub-agent cannot recurse "Use sub_agent to start another sub_agent" Sub-agent has no sub_agent tool 16.6 Sub-agent iteration limit Give sub-agent a task requiring >10 iterations Returns "reached maximum iterations" 16.7 Sub-agent with context Pass context parameter to sub-agent Sub-agent uses the extra context to complete task
16A. ACP Stdio Mode
# User Story Steps Expected 16A.1 ACP startup Run drugclaw acpDrugClaw starts as an ACP stdio server 16A.2 ACP session continuity Use one ACP client session for two turns Second turn keeps prior context 16A.3 ACP cancellation Start a long-running ACP request, then send /stopActive ACP run is cancelled cleanly
17. Browser Automation
# User Story Steps Expected 17.1 Open webpage "Open https://example.com in browser" Returns page content/status 17.2 Get page text After opening, "get the page title" Returns "Example Domain" 17.3 Screenshot "Take a screenshot of the current page" Screenshot saved successfully 17.4 Timeout handling Open a very slow page Returns timeout message after 30s 17.5 Browser session persistence Log into a site, then restart conversation Browser profile retains cookies/localStorage 17.6 Output truncation Get content of a very large page Output truncated to 30000 characters
18. Skills System
# User Story Steps Expected 18.1 /skills list Send /skillsLists all available skills (name + description) 18.2 /skills when empty Delete skills directory, send /skills"No skills available." 18.3 Activate skill Ask bot to use a specific skill Bot uses activate_skill to load full instructions 18.4 Skill auto-discovery Add new skill directory + SKILL.md under <data_dir>/skills/After restart, /skillsshows new skill
19. MCP Integration
# User Story Steps Expected 19.1 MCP tools loaded Configure mcp.json, start bot Log: "MCP initialized: N tools available" 19.2 MCP tool usage Ask bot to use an MCP-provided tool MCP server called, result returned 19.3 No MCP config Start without mcp.json Normal startup, no MCP logs
20. Multi-Step Tool Use (Agentic Loop)
# User Story Steps Expected 20.1 Multi-step task "Find all TODO comments and write to /tmp/todos.txt" Bot uses grep → read_file → write_file across multiple iterations 20.2 Error recovery Request reading nonexistent file; bot should adapt Bot receives tool error, adjusts strategy 20.3 Iteration limit reached Send request requiring >max_tool_iterations "I reached the maximum number of tool iterations..." 20.4 Tool composition "Find the longest function and summarize it" Bot combines grep → read_file → analysis
21. Permission Model Matrix
# Operation Regular→Self Regular→Other Control→Other 21.1 send_message Allow Deny Allow 21.2 schedule_task Allow Deny Allow 21.3 pause/resume/cancel_task Allow (own) Deny Allow 21.4 list_tasks Allow (own) Deny Allow 21.5 get_task_history Allow (own) Deny Allow 21.6 write_memory (chat) Allow Deny Allow 21.7 write_memory (global) Deny N/A Allow 21.8 read_memory (chat) Allow Deny Allow 21.9 export_chat Allow Deny Allow 21.10 todo_read/write Allow Deny Allow
22. Security -- Path Guard
# Test Path Expected 22.1 ~/.ssh/id_rsaBlocked 22.2 ~/.ssh/known_hostsBlocked 22.3 ~/.aws/credentialsBlocked 22.4 ~/.gnupg/*Blocked 22.5 .envBlocked 22.6 .env.local/.env.productionBlocked 22.7 ~/.kube/configBlocked 22.8 ~/.config/gcloud/*Blocked 22.9 /etc/shadowBlocked 22.10 /etc/sudoersBlocked 22.11 ~/.netrcBlocked 22.12 ~/.npmrcBlocked 22.13 Path traversal ../../.ssh/id_rsaStill blocked 22.14 Normal path /tmp/test.txtAllowed
23. Discord Platform
# User Story Steps Expected 23.1 DM direct reply Send DM to bot on Discord Bot responds directly 23.2 Server requires @mention Send message without @mention in server channel No reply; message stored 23.3 Server @mention @Bot in server channel Bot responds 23.4 Response splitting Trigger response >2000 chars Split at newline boundaries, max 2000 chars per message 23.5 /reset command Send /resetDiscord chat session cleared 23.6 /skills command Send /skillsLists available skills 23.7 /archive command Send /archiveArchives current session 23.8 allowed_channels whitelist Configure discord_allowed_channels Only responds in allowed channels 23.9 Ignore other bots Another bot sends a message Bot does not respond
24. WhatsApp Platform
# User Story Steps Expected 24.1 Webhook verification (valid) GET /webhookwith correct verify_tokenReturns challenge 24.2 Webhook verification (invalid) GET /webhookwith wrong verify_tokenRejected 24.3 Text message processing Send text via WhatsApp Bot responds 24.4 Non-text message Send image/audio Silently skipped, no crash 24.5 /reset command Send /resetSession cleared 24.6 Response splitting Trigger long response Split at 4096-char boundaries 24.7 Send failure WhatsApp API unreachable Error logged; HTTP 200 still returned
25. Gateway Service Management
# User Story Steps Expected 25.1 Install service drugclaw gateway installmacOS: LaunchAgent; Linux: systemd service 25.2 Start service drugclaw gateway startService starts 25.3 Check status drugclaw gateway statusReports running state 25.4 View logs drugclaw gateway logs 50Shows last 50 log lines 25.5 Stop service drugclaw gateway stopService stops 25.6 Uninstall service drugclaw gateway uninstallService removed
26. Error Handling & Recovery
# User Story Steps Expected 26.1 API rate limiting Trigger Anthropic 429 response Exponential backoff retry, up to 3 attempts 26.2 API unavailable API completely unreachable Returns "Error: {message}", no crash 26.3 DB corruption recovery Delete DB file, then send message Recreates DB or reports error, no crash 26.4 Tool execution exception Tool internal panic/error ToolResult::error returned to Claude, agentic loop continues 26.5 No messages to process Empty history "I didn't receive any message to process." 26.6 Role merge: consecutive user DB history has user+user messages Auto-merged into one user message 26.7 Role fix: trailing assistant History ends with assistant message Trailing assistant message auto-removed
Platform Adapter Template (Future Channels)
Use this checklist when adding a new platform adapter (for example Slack/Feishu/Teams).
Adapter metadata
- Platform name:
<platform> - Inbound mode:
<webhook|gateway|polling> - Identity key mapping:
<platform chat/channel/user IDs -> stable chat_id> - Reply trigger policy:
<dm always / group mention / allowlist> - Message length limit:
<N chars> - Attachment support:
<yes/no + types>
Test matrix
# User Story Steps Expected P.1 Private/DM reply Send direct message Bot responds P.2 Group/channel no trigger Send group message without trigger No reply; message persistence matches policy P.3 Group/channel trigger Mention or trigger bot in group/channel Bot responds P.4 Long response splitting Ask for response > platform limit Split correctly at boundary with no truncation P.5 Session reset Send /resetequivalentSession cleared for this platform chat P.6 Session resume Multi-turn chat -> restart bot -> continue Session restored from DB P.7 Attachment ingress Send supported attachment Parsed/handled with expected behavior P.8 Attachment egress Trigger send_messagewith attachmentDelivery succeeds or clear error reported P.9 Channel allowlist Configure adapter channel allowlist Replies limited to configured channels P.10 Ignore bot/system messages Send from another bot/system user Bot does not self-trigger or loop P.11 API failure handling Simulate platform API/network failure Error logged; process stays healthy P.12 Rate limit handling Trigger adapter/platform rate limit Backoff/retry policy works, no crash Persistence and schema checks
messagesrows are written with correctchat_id,sender_name,is_from_bot, and timestamp.sessionsrow key remains stable for the same platform conversation across restarts.chats.chat_typesemantics are consistent with trigger policy.- Scheduled task replies route correctly back to the originating platform chat.
Security and authorization checks
control_chat_idspolicy is enforced for cross-chat tools.- Tool safety boundaries (path guard, restricted tools) remain unchanged by adapter.
- Mention parsing cannot be spoofed by plain text edge cases.
Database Verification
After running tests, verify the database directly:
sqlite3 drugclaw.data/runtime/drugclaw.db -- Check messages are stored SELECT COUNT(*) FROM messages; -- Check scheduled tasks SELECT * FROM scheduled_tasks; -- Check chat records SELECT * FROM chats; -- Check sessions SELECT chat_id, length(messages_json), updated_at FROM sessions; -- Check task run logs SELECT * FROM task_run_logs ORDER BY started_at DESC LIMIT 10;
Cleanup
After testing:
# Remove test files rm -f /tmp/drugclaw_test.txt /tmp/todos.txt /tmp/test.txt /tmp/session_test.txt # Cancel any remaining scheduled tasks via the bot, or: sqlite3 drugclaw.data/runtime/drugclaw.db "UPDATE scheduled_tasks SET status='cancelled' WHERE status='active';"- A working
drugclaw-drug-discovery-agent/build.rscode · 0.2 KB
fn main() { // Rebuild when embedded frontend assets change. println!("cargo:rerun-if-changed=web/dist"); // Keep builtin skills embedding in sync as well. println!("cargo:rerun-if-changed=skills/built-in"); }drugclaw-drug-discovery-agent/check.shcode · 1.6 KB
#!/usr/bin/env bash set -euo pipefail # Validate the DrugClaw research runtime, docs, and local web surfaces. ROOT_DIR="$(cd "$(dirname "$0")" && pwd)" RUN_WEB=1 RUN_WEBSITE=1 RUN_DOCS=1 RUST_ONLY=0 usage() { cat <<'USAGE' Usage: ./check.sh [--rust-only] [--skip-web] [--skip-website] [--skip-docs] Options: --rust-only Run only Rust tests --skip-web Skip `npm --prefix web run build` --skip-website Skip `npm --prefix website run build` --skip-docs Skip docs artifact drift check -h, --help Show help USAGE } while [[ $# -gt 0 ]]; do case "$1" in --rust-only) RUST_ONLY=1 RUN_WEB=0 RUN_WEBSITE=0 RUN_DOCS=0 shift ;; --skip-web) RUN_WEB=0 shift ;; --skip-website) RUN_WEBSITE=0 shift ;; --skip-docs) RUN_DOCS=0 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 1 ;; esac done cd "$ROOT_DIR" echo "[check] rust tests" cargo test -q if [[ "$RUST_ONLY" == "1" ]]; then echo "[check] completed rust-only run" exit 0 fi if [[ "$RUN_WEB" == "1" && -f "web/package.json" ]]; then echo "[check] web build" npm --prefix web run build fi if [[ "$RUN_WEBSITE" == "1" && -f "website/package.json" ]]; then echo "[check] website build" npm --prefix website run build fi if [[ "$RUN_DOCS" == "1" && -f "scripts/generate_docs_artifacts.mjs" ]]; then echo "[check] docs artifact drift" node scripts/generate_docs_artifacts.mjs --check fi echo "[check] completed"drugclaw-drug-discovery-agent/crates/drugclaw-app/Cargo.tomlcode · 0.7 KB
[package] name = "drugclaw-app" version = "0.1.0" edition = "2021" license = "Apache-2.0" [features] default = [] journald = ["tracing-journald"] [dependencies] anyhow = "1" chrono = { version = "0.4", features = ["serde"] } include_dir = "0.7" reqwest = { version = "0.12", features = ["json", "blocking", "multipart"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_yaml = "0.9" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-journald = { version = "0.3", optional = true } opentelemetry-appender-tracing = "0.30" opentelemetry_sdk = "0.30" drugclaw-observability = { path = "../drugclaw-observability" } [dev-dependencies] uuid = { version = "1", features = ["v4"] }drugclaw-drug-discovery-agent/crates/drugclaw-app/src/builtin_skills.rscode · 14.2 KB · preview truncated
use include_dir::{include_dir, Dir, DirEntry, File}; use serde::Deserialize; use std::path::Path; static BUILTIN_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../skills/built-in"); static GENOMICS_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../skills/genomics"); static MEDICAL_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../skills/medical"); static PHARMA_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../skills/pharma"); static RESEARCH_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../skills/research"); static SCIENCE_SKILLS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../skills/science"); pub fn ensure_builtin_skills(skills_root: &Path) -> std::io::Result<()> { std::fs::create_dir_all(skills_root)?; for embedded in [ &BUILTIN_SKILLS_DIR, &GENOMICS_SKILLS_DIR, &MEDICAL_SKILLS_DIR, &PHARMA_SKILLS_DIR, &RESEARCH_SKILLS_DIR, &SCIENCE_SKILLS_DIR, ] { copy_compatible_skills(embedded, skills_root)?; } Ok(()) } fn copy_compatible_skills(embedded: &Dir<'_>, destination: &Path) -> std::io::Result<()> { for entry in embedded.entries() { let DirEntry::Dir(skill_dir) = entry else { continue; }; let Some(skill_name) = skill_dir.path().file_name() else { continue; }; let skill_name_str = skill_name.to_string_lossy(); tracing::debug!("Found bundled skill: {}", skill_name_str); let Some(skill_md) = get_file_with_relative_dir(skill_dir, "SKILL.md") else { tracing::debug!("Skipping {} - missing SKILL.md", skill_name_str); continue; }; let content = String::from_utf8_lossy(skill_md.contents()); if let Some(reason) = skill_skip_reason(&content) { tracing::debug!( "Skipping bundled skill '{}' on this host: {}", skill_name_str, reason ); continue; } let next_dest = destination.join(skill_name); let overwrite_managed = existing_skill_is_managed(&next_dest); if !next_dest.exists() { tracing::debug!( "Installing skill {} to {}", skill_name_str, next_dest.display() ); std::fs::create_dir_all(&next_dest)?; copy_embedded_entries(skill_dir, &next_dest, true)?; } else { tracing::debug!( "Skill {} already exists at {}", skill_name_str, next_dest.display() ); copy_embedded_entries(skill_dir, &next_dest, overwrite_managed)?; } } Ok(()) } fn get_file_with_relative_dir<'a>(dir: &'a Dir<'a>, relative_path: &str) -> Option<&'a File<'a>> { let full_path = dir.path().join(relative_path); dir.get_file(full_path) } fn existing_skill_is_managed(skill_dir: &Path) -> bool { let skill_md = skill_dir.join("SKILL.md"); let Ok(content) = std::fs::read_to_string(skill_md) else { return false; }; parse_frontmatter(&content) .map(|fm| fm.source.eq_ignore_ascii_case("drugclaw")) .unwrap_or(false) } fn copy_embedded_entries( embedded: &Dir<'_>, destination: &Path, overwrite_existing: bool, ) -> std::io::Result<()> { for entry in embedded.entries() { match entry { DirEntry::Dir(dir) => { let Some(name) = dir.path().file_name() else { continue; }; if should_skip_embedded_name(name) { continue; } let next_dest = destination.join(name); if !next_dest.exists() { std::fs::create_dir_all(&next_dest)?; } copy_embedded_entries(dir, &next_dest, overwrite_existing)?; } DirEntry::File(file) => { let Some(name) = file.path().file_name() else { continue; }; if should_skip_embedded_name(name) { continue; } let out_path = destination.join(name); if overwrite_existing || !out_path.exists() { tracing::debug!("Writing file: {}", out_path.display()); std::fs::write(out_path, file.contents())?; } } } } Ok(()) } fn should_skip_embedded_name(name: &std::ffi::OsStr) -> bool { let value = name.to_string_lossy(); if value == ".DS_Store" || value == "__pycache__" { return true; } matches!( Path::new(value.as_ref()) .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext.to_ascii_lowercase()) .as_deref(), Some("pyc" | "pyo") ) } #[derive(Debug, Deserialize, Default)] struct SkillFrontmatter { #[serde(default)] source: String, #[serde(default)] platforms: Vec<String>, #[serde(default)] deps: Vec<String>, #[serde(default)] compatibility: SkillCompatibility, } #[derive(Debug, Deserialize, Default)] struct SkillCompatibility { #[serde(default)] os: Vec<String>, #[serde(default)] deps: Vec<String>, } fn parse_frontmatter(content: &str) -> Option<SkillFrontmatter> { let trimmed = content.trim_start_matches('\u{feff}'); let rest = trimmed.strip_prefix("---\n")?; let end_idx = rest.find("\n---\n").or_else(|| rest.find("\n...\n"))?; let yaml = &rest[..end_idx]; serde_yaml::from_str(yaml).ok() } fn skill_skip_reason(content: &str) -> Option<String> { let fm = parse_frontmatter(content)?; let mut supported = fm.platforms; supported.extend(fm.compatibility.os); supported.sort(); supported.dedup(); if !platform_allowed(&supported) { return Some(format!( "unsupported platform (current: {}, supported: {})", current_platform(), supported.join(", ") )); } let mut deps = fm.deps; deps.extend(fm.compatibility.deps); deps.sort(); deps.dedup(); let missing: Vec<String> = deps.into_iter().filter(|d| !command_exists(d)).collect(); if !missing.is_empty() { return Some(format!("missing dependencies: {}", missing.join(", "))); } None } fn current_platform() -> &'static str { if cfg!(target_os = "macos") { "darwin" } else if cfg!(target_os = "linux") { "linux" } else if cfg!(target_os = "windows") { "windows" } else { "unknown" } } fn normalize_platform(value: &str) -> String { let v = value.trim().to_ascii_lowercase(); match v.as_str() { "macos" | "osx" => "darwin".to_string(), _ => v, } } fn platform_allowed(platforms: &[String]) -> bool { if platforms.is_empty() { return true; } let current = current_platform(); platforms.iter().any(|p| { let p = normalize_platform(p); p == "all" || p == "*" || p == current }) } fn command_exists(command: &str) -> bool { if command.trim().is_empty() { return true; } let path_var = std::env::var_os("PATH").unwrap_or_default(); let paths = std::env::split_paths(&path_var); #[cfg(target_os = "windows")] let candidates: Vec<String> = { let exts = std::env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into()); let ext_list: Vec<String> = exts .split(';') .map(|s| s.trim().to_ascii_lowercase()) .filter(|s| !s.is_empty()) .collect(); let lower = command.to_ascii_lowercase(); if ext_list.iter().any(|ext| lower.ends_with(ext)) { vec![command.to_string()] } else { let mut c = vec![command.to_string()]; for ext in ext_list { c.push(format!("{command}{ext}")); } c } }; #[cfg(not(target_os = "windows"))] let candidates: Vec<String> = vec![command.to_string()]; for base in paths { for candidate in &candidates { let full = base.join(candidate); if full.is_file() { return true; } } } false } #[cfg(test)] mod tests { use super::*; fn temp_root() -> std::path::PathBuf { std::env::temp_dir().join(format!( "drugclaw_builtin_skills_test_{}", uuid::Uuid::new_v4() )) } fn cleanup(path: &Path) { let _ = std::fs::remove_dir_all(path); } #[test] fn test_ensure_builtin_skills_writes_missing_files() { let root = temp_root(); let skills_root = root.join("skills"); ensure_builtin_skills(&skills_root).unwrap(); let sample = skills_root.join("pdf").join("SKILL.md"); assert!(sample.exists()); let content = std::fs::read_to_string(sample).unwrap(); assert!(!content.trim().is_empty()); cleanup(&root); } #[test] fn test_ensure_builtin_skills_does_not_overwrite_existing_file() { let root = temp_root(); let skills_root = root.join("skills"); let custom_pdf = skills_root.join("pdf"); std::fs::create_dir_all(&custom_pdf).unwrap(); let custom_file = custom_pdf.join("SKILL.md"); std::fs::write(&custom_file, "custom-content").unwrap(); ensure_builtin_skills(&skills_root).unwrap(); let content = std::fs::read_to_string(custom_file).unwrap(); assert_eq!(content, "custom-content"); cleanup(&root); } #[test] fn test_ensure_builtin_skills_refreshes_managed_skill_files() { let root = temp_root(); let skills_root = root.join("skills"); let managed_skill = skills_root.join("bio-tools"); std::fs::create_dir_all(&managed_skill).unwrap(); let managed_file = managed_skill.join("SKILL.md"); std::fs::write( &managed_file, r#"--- name: bio-tools description: stale source: drugclaw --- stale-content "#, ) .unwrap(); ensure_builtin_skills(&skills_root).unwrap(); let embedded = SCIENCE_SKILLS_DIR.get_dir("bio-tools").unwrap(); let expected = String::from_utf8_lossy( get_file_with_relative_dir(embedded, "SKILL.md") .unwrap() .contents(), ) .to_string(); let actual = std::fs::read_to_string(managed_file).unwrap(); assert_eq!(actual, expected); cleanup(&root); } #[test] fn test_get_file_with_relative_dir_handles_include_dir_subdirs() { let pdf_dir = BUILTIN_SKILLS_DIR.get_dir("pdf").unwrap(); assert!(pdf_dir.get_file("SKILL.md").is_none()); assert!(get_file_with_relative_dir(pdf_dir, "SKILL.md").is_some()); } #[test] fn test_ensure_builtin_skills_includes_new_macos_and_weather_skills() { let root = temp_root(); let skills_root = root.join("skills"); ensure_builtin_skills(&skills_root).unwrap(); for skill in [ "pdf", "docx", "xlsx", "pptx", "skill-creator", "bio-tools", "bio-db-tools", "bayesian-optimization-tools", "grn-tools", "omics-tools", "pharma-db-tools", "chem-tools", "pharma-ml-tools", "literature-review-tools", "medical-data-tools", "clinical-research-tools", "medical-qms-tools", "stat-modeling-tools", "survival-analysis-tools", "scientific-visualization-tools", "scientific-workflow-tools", "target-intelligence-tools", "variant-analysis-tools", "docking-tools", ] {drugclaw-drug-discovery-agent/crates/drugclaw-app/src/lib.rscode · 0.1 KB
//! Application-level runtime and CLI orchestration for DrugClaw. pub mod builtin_skills; pub mod logging; pub mod transcribe;drugclaw-drug-discovery-agent/crates/drugclaw-app/src/logging.rscode · 10.0 KB
use anyhow::{Context, Result}; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use drugclaw_observability::logs::OtlpLogExporter; use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; use opentelemetry_sdk::logs::{SdkLogger, SdkLoggerProvider}; use serde_yaml::Value; use std::collections::VecDeque; use std::fs::{self, File, OpenOptions}; use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, OnceLock}; use tracing_subscriber::fmt::writer::MakeWriter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; pub const LOG_FILE_PREFIX: &str = "drugclaw-"; pub const LOG_FILE_SUFFIX: &str = ".log"; pub const LOG_RETENTION_DAYS: i64 = 30; static OTEL_LOG_EXPORTER: OnceLock<Arc<OtlpLogExporter>> = OnceLock::new(); pub fn init_logging(runtime_data_dir: &str, observability: Option<&Value>) -> Result<()> { let log_dir = PathBuf::from(runtime_data_dir).join("logs"); fs::create_dir_all(&log_dir) .with_context(|| format!("Failed to create log directory: {}", log_dir.display()))?; cleanup_old_logs(&log_dir, Utc::now(), LOG_RETENTION_DAYS)?; let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); let writer = HourlyLogWriter::new(log_dir, LOG_RETENTION_DAYS)?; let fmt_layer = tracing_subscriber::fmt::layer() .with_ansi(false) .with_writer(writer); let otel_layer = build_otel_log_layer(observability); if let Some(otel_layer) = otel_layer { tracing_subscriber::registry() .with(filter) .with(fmt_layer) .with(otel_layer) .init(); } else { tracing_subscriber::registry() .with(filter) .with(fmt_layer) .init(); } Ok(()) } pub fn init_console_logging(observability: Option<&Value>) { let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")); let otel_layer = build_otel_log_layer(observability); #[cfg(feature = "journald")] { let use_journald = std::env::var("JOURNAL_STREAM").is_ok() || std::env::var("RUST_LOG_STYLE").is_ok_and(|v| v.eq_ignore_ascii_case("systemd")); if use_journald { if let Ok(journald_layer) = tracing_journald::layer() { if let Some(otel_layer) = otel_layer { tracing_subscriber::registry() .with(filter) .with(journald_layer) .with(otel_layer) .init(); } else { tracing_subscriber::registry() .with(filter) .with(journald_layer) .init(); } return; } } } let fmt_layer = tracing_subscriber::fmt::layer(); if let Some(otel_layer) = otel_layer { tracing_subscriber::registry() .with(filter) .with(fmt_layer) .with(otel_layer) .init(); } else { tracing_subscriber::registry() .with(filter) .with(fmt_layer) .init(); } } fn build_otel_log_layer( observability: Option<&Value>, ) -> Option<OpenTelemetryTracingBridge<SdkLoggerProvider, SdkLogger>> { let exporter = OtlpLogExporter::from_observability(observability)?; let exporter = OTEL_LOG_EXPORTER.get_or_init(|| exporter.clone()); let provider = exporter.logger_provider(); Some(OpenTelemetryTracingBridge::new(provider.as_ref())) } #[derive(Debug)] struct HourlyState { current_hour_key: String, file: File, } #[derive(Clone, Debug)] struct HourlyLogWriter { log_dir: PathBuf, retention_days: i64, state: Arc<Mutex<HourlyState>>, } impl HourlyLogWriter { fn new(log_dir: PathBuf, retention_days: i64) -> Result<Self> { let now = Utc::now(); let hour_key = hour_key(now); let file = open_log_file(&log_dir, &hour_key)?; let state = HourlyState { current_hour_key: hour_key, file, }; Ok(Self { log_dir, retention_days, state: Arc::new(Mutex::new(state)), }) } } impl<'a> MakeWriter<'a> for HourlyLogWriter { type Writer = HourlyLogGuard; fn make_writer(&'a self) -> Self::Writer { HourlyLogGuard { log_dir: self.log_dir.clone(), retention_days: self.retention_days, state: self.state.clone(), } } } struct HourlyLogGuard { log_dir: PathBuf, retention_days: i64, state: Arc<Mutex<HourlyState>>, } impl Write for HourlyLogGuard { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { let now = Utc::now(); let now_key = hour_key(now); let mut state = self .state .lock() .map_err(|_| io::Error::other("failed to lock log writer"))?; if state.current_hour_key != now_key { state.file.flush()?; state.file = open_log_file(&self.log_dir, &now_key)?; state.current_hour_key = now_key; let _ = cleanup_old_logs(&self.log_dir, now, self.retention_days); } state.file.write(buf) } fn flush(&mut self) -> io::Result<()> { let mut state = self .state .lock() .map_err(|_| io::Error::other("failed to lock log writer"))?; state.file.flush() } } fn hour_key(now: DateTime<Utc>) -> String { now.format("%Y-%m-%d-%H").to_string() } fn log_file_path(log_dir: &Path, hour: &str) -> PathBuf { log_dir.join(format!("{LOG_FILE_PREFIX}{hour}{LOG_FILE_SUFFIX}")) } fn open_log_file(log_dir: &Path, hour: &str) -> io::Result<File> { OpenOptions::new() .create(true) .append(true) .open(log_file_path(log_dir, hour)) } pub fn cleanup_old_logs(log_dir: &Path, now: DateTime<Utc>, retention_days: i64) -> Result<()> { let cutoff = now - Duration::days(retention_days); let entries = match fs::read_dir(log_dir) { Ok(v) => v, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()), Err(e) => return Err(e).with_context(|| format!("Failed to read {}", log_dir.display())), }; for entry in entries { let entry = entry?; let path = entry.path(); if !path.is_file() { continue; } let Some(file_name) = path.file_name().and_then(|s| s.to_str()) else { continue; }; let Some(log_time) = parse_log_filename_time(file_name) else { continue; }; if log_time < cutoff { let _ = fs::remove_file(&path); } } Ok(()) } fn parse_log_filename_time(file_name: &str) -> Option<DateTime<Utc>> { if !(file_name.starts_with(LOG_FILE_PREFIX) && file_name.ends_with(LOG_FILE_SUFFIX)) { return None; } let body = &file_name[LOG_FILE_PREFIX.len()..file_name.len() - LOG_FILE_SUFFIX.len()]; let naive = NaiveDateTime::parse_from_str(&format!("{body}:00:00"), "%Y-%m-%d-%H:%M:%S").ok()?; Some(DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc)) } pub fn list_log_files_sorted(log_dir: &Path) -> Result<Vec<PathBuf>> { let mut files = Vec::new(); let entries = match fs::read_dir(log_dir) { Ok(v) => v, Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(files), Err(e) => return Err(e).with_context(|| format!("Failed to read {}", log_dir.display())), }; for entry in entries { let entry = entry?; let path = entry.path(); let Some(name) = path.file_name().and_then(|v| v.to_str()) else { continue; }; if parse_log_filename_time(name).is_some() { files.push(path); } } files.sort(); Ok(files) } pub fn read_last_lines_from_logs(log_dir: &Path, max_lines: usize) -> Result<Vec<String>> { let mut queue: VecDeque<String> = VecDeque::new(); for file in list_log_files_sorted(log_dir)? { let content = fs::read_to_string(&file) .with_context(|| format!("Failed to read {}", file.display()))?; for line in content.lines() { queue.push_back(line.to_string()); if queue.len() > max_lines { queue.pop_front(); } } } Ok(queue.into_iter().collect()) } #[cfg(test)] mod tests { use super::*; use uuid::Uuid; fn test_dir() -> PathBuf { std::env::temp_dir().join(format!("drugclaw_logging_test_{}", Uuid::new_v4())) } #[test] fn test_parse_log_filename_time() { assert!(parse_log_filename_time("drugclaw-2026-02-08-10.log").is_some()); assert!(parse_log_filename_time("drugclaw-2026-02-08.log").is_none()); assert!(parse_log_filename_time("other-2026-02-08-10.log").is_none()); } #[test] fn test_cleanup_old_logs_keeps_recent_removes_old() { let dir = test_dir(); fs::create_dir_all(&dir).unwrap(); fs::write(dir.join("drugclaw-2025-01-01-00.log"), "old").unwrap(); fs::write(dir.join("drugclaw-2026-02-08-10.log"), "new").unwrap(); let now = DateTime::parse_from_rfc3339("2026-02-08T11:00:00Z") .unwrap() .with_timezone(&Utc); cleanup_old_logs(&dir, now, 30).unwrap(); assert!(!dir.join("drugclaw-2025-01-01-00.log").exists()); assert!(dir.join("drugclaw-2026-02-08-10.log").exists()); let _ = fs::remove_dir_all(&dir); } #[test] fn test_read_last_lines_from_logs() { let dir = test_dir(); fs::create_dir_all(&dir).unwrap(); fs::write(dir.join("drugclaw-2026-02-08-09.log"), "a\nb\n").unwrap(); fs::write(dir.join("drugclaw-2026-02-08-10.log"), "c\nd\n").unwrap(); let lines = read_last_lines_from_logs(&dir, 3).unwrap(); assert_eq!(lines, vec!["b", "c", "d"]); let _ = fs::remove_dir_all(&dir); } }drugclaw-drug-discovery-agent/crates/drugclaw-app/src/transcribe.rscode · 1.3 KB
use reqwest::multipart; pub async fn transcribe_audio(api_key: &str, audio_bytes: &[u8]) -> Result<String, String> { let client = reqwest::Client::new(); let part = multipart::Part::bytes(audio_bytes.to_vec()) .file_name("audio.ogg") .mime_str("audio/ogg") .map_err(|e| e.to_string())?; let form = multipart::Form::new() .text("model", "whisper-1") .part("file", part); let resp = client .post("https://api.openai.com/v1/audio/transcriptions") .header("Authorization", format!("Bearer {api_key}")) .multipart(form) .send() .await .map_err(|e| format!("Whisper API request failed: {e}"))?; if !resp.status().is_success() { let status = resp.status(); let body = resp.text().await.unwrap_or_default(); return Err(format!("Whisper API error HTTP {status}: {body}")); } let body: serde_json::Value = resp .json() .await .map_err(|e| format!("Failed to parse Whisper response: {e}"))?; body.get("text") .and_then(|t| t.as_str()) .map(|s| s.to_string()) .ok_or_else(|| "Whisper response missing 'text' field".into()) } #[cfg(test)] mod tests { #[test] fn test_transcribe_module_exists() { // Basic smoke test that the module compiles } }drugclaw-drug-discovery-agent/crates/drugclaw-channels/Cargo.tomlcode · 0.3 KB
[package] name = "drugclaw-channels" version = "0.1.0" edition = "2021" license = "Apache-2.0" [dependencies] async-trait = "0.1" chrono = { version = "0.4", features = ["serde"] } drugclaw-storage = { path = "../drugclaw-storage" } serde_json = "1" uuid = { version = "1", features = ["v4"] }drugclaw-drug-discovery-agent/crates/drugclaw-channels/src/channel.rscode · 8.0 KB
use std::sync::Arc; use crate::channel_adapter::ChannelRegistry; use drugclaw_storage::db::{call_blocking, Database, StoredMessage}; #[derive(Clone, Debug)] struct ToolAuthContext { caller_chat_id: i64, } fn auth_context_from_input(input: &serde_json::Value) -> Option<ToolAuthContext> { let ctx = input.get("__drugclaw_auth")?; let caller_chat_id = ctx.get("caller_chat_id")?.as_i64()?; Some(ToolAuthContext { caller_chat_id }) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConversationKind { Private, Group, } impl ConversationKind { pub fn as_agent_chat_type(self) -> &'static str { match self { ConversationKind::Private => "private", ConversationKind::Group => "group", } } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ChatRouting { pub channel_name: String, pub conversation: ConversationKind, } pub fn parse_chat_routing(registry: &ChannelRegistry, db_chat_type: &str) -> Option<ChatRouting> { let (channel_name, kind) = registry.resolve_routing(db_chat_type)?; Some(ChatRouting { channel_name: channel_name.to_string(), conversation: kind, }) } fn infer_channel_from_chat_type(chat_type: &str) -> Option<&'static str> { if chat_type.starts_with("telegram_") { return Some("telegram"); } if chat_type.starts_with("discord_") { return Some("discord"); } if chat_type.starts_with("slack_") { return Some("slack"); } if chat_type.starts_with("feishu_") { return Some("feishu"); } if chat_type.starts_with("matrix_") { return Some("matrix"); } if chat_type.starts_with("whatsapp_") { return Some("whatsapp"); } if chat_type.starts_with("imessage_") { return Some("imessage"); } if chat_type.starts_with("email_") { return Some("email"); } if chat_type.starts_with("nostr_") { return Some("nostr"); } if chat_type.starts_with("signal_") { return Some("signal"); } if chat_type.starts_with("dingtalk_") { return Some("dingtalk"); } if chat_type.starts_with("qq_") { return Some("qq"); } if chat_type.starts_with("irc_") { return Some("irc"); } None } pub async fn get_chat_type_raw(db: Arc<Database>, chat_id: i64) -> Result<Option<String>, String> { call_blocking(db, move |d| d.get_chat_type(chat_id)) .await .map_err(|e| format!("Failed to read chat type for chat {chat_id}: {e}")) } pub async fn get_chat_channel_raw( db: Arc<Database>, chat_id: i64, ) -> Result<Option<String>, String> { call_blocking(db, move |d| d.get_chat_channel(chat_id)) .await .map_err(|e| format!("Failed to read chat channel for chat {chat_id}: {e}")) } pub async fn get_chat_routing( registry: &ChannelRegistry, db: Arc<Database>, chat_id: i64, ) -> Result<Option<ChatRouting>, String> { let chat_type = get_chat_type_raw(db.clone(), chat_id).await?; let Some(chat_type) = chat_type else { return Ok(None); }; let kind = registry .resolve_routing(&chat_type) .map(|(_, k)| k) .unwrap_or(ConversationKind::Group); if let Some(channel_name) = get_chat_channel_raw(db, chat_id).await? { let normalized = channel_name.trim(); if !normalized.is_empty() && registry.get(normalized).is_some() { return Ok(Some(ChatRouting { channel_name: normalized.to_string(), conversation: kind, })); } } Ok(parse_chat_routing(registry, &chat_type)) } pub async fn get_required_chat_routing( registry: &ChannelRegistry, db: Arc<Database>, chat_id: i64, ) -> Result<ChatRouting, String> { let chat_type = get_chat_type_raw(db.clone(), chat_id) .await? .ok_or_else(|| format!("target chat {chat_id} not found"))?; if let Some(routing) = get_chat_routing(registry, db.clone(), chat_id).await? { return Ok(routing); } let hinted_channel = get_chat_channel_raw(db, chat_id) .await? .map(|v| v.trim().to_string()) .filter(|v| !v.is_empty()) .or_else(|| infer_channel_from_chat_type(&chat_type).map(ToOwned::to_owned)); if let Some(channel_name) = hinted_channel { if registry.get(&channel_name).is_none() { return Err(format!( "channel '{channel_name}' is not enabled for chat {chat_id} (chat_type='{chat_type}')" )); } } Err(format!( "unsupported chat type '{chat_type}' for chat {chat_id}" )) } pub fn session_source_for_chat( registry: &ChannelRegistry, chat_type: &str, chat_title: Option<&str>, ) -> String { // Legacy discord detection: some old records have generic types like "private" // but a title starting with "discord-" if matches!(chat_type, "private" | "group" | "supergroup" | "channel") && chat_title.is_some_and(|t| t.starts_with("discord-")) { return "discord".to_string(); } if let Some((channel_name, _)) = registry.resolve_routing(chat_type) { return channel_name.to_string(); } chat_type.to_string() } pub async fn is_web_chat(registry: &ChannelRegistry, db: Arc<Database>, chat_id: i64) -> bool { get_chat_routing(registry, db, chat_id) .await .ok() .flatten() .map(|r| r.channel_name == "web") .unwrap_or(false) } pub async fn enforce_channel_policy( registry: &ChannelRegistry, db: Arc<Database>, input: &serde_json::Value, target_chat_id: i64, ) -> Result<(), String> { let Some(auth) = auth_context_from_input(input) else { return Ok(()); }; // Check if the caller's channel disallows cross-chat operations if let Ok(Some(routing)) = get_chat_routing(registry, db.clone(), auth.caller_chat_id).await { if let Some(adapter) = registry.get(&routing.channel_name) { if !adapter.allows_cross_chat() && auth.caller_chat_id != target_chat_id { return Err(format!( "Permission denied: {} chats cannot operate on other chats", routing.channel_name )); } } } Ok(()) } pub async fn deliver_and_store_bot_message( registry: &ChannelRegistry, db: Arc<Database>, bot_username: &str, chat_id: i64, text: &str, ) -> Result<(), String> { let routing = get_required_chat_routing(registry, db.clone(), chat_id).await?; let external_chat_id = call_blocking(db.clone(), move |d| d.get_chat_external_id(chat_id)) .await .map_err(|e| format!("Failed to read external chat id for chat {chat_id}: {e}"))? .unwrap_or_else(|| chat_id.to_string()); if let Some(adapter) = registry.get(&routing.channel_name) { if !adapter.is_local_only() { adapter.send_text(&external_chat_id, text).await?; } } else { return Err(format!( "No adapter registered for channel '{}'", routing.channel_name )); } let msg = StoredMessage { id: uuid::Uuid::new_v4().to_string(), chat_id, sender_name: bot_username.to_string(), content: text.to_string(), is_from_bot: true, timestamp: chrono::Utc::now().to_rfc3339(), }; call_blocking(db.clone(), move |d| d.store_message(&msg)) .await .map_err(|e| format!("Failed to store sent message: {e}")) } #[cfg(test)] mod tests { use super::infer_channel_from_chat_type; #[test] fn test_infer_channel_from_prefixed_chat_type() { assert_eq!( infer_channel_from_chat_type("telegram_private"), Some("telegram") ); assert_eq!(infer_channel_from_chat_type("discord_dm"), Some("discord")); } #[test] fn test_infer_channel_from_unknown_chat_type() { assert_eq!(infer_channel_from_chat_type("private"), None); assert_eq!(infer_channel_from_chat_type("group"), None); } }drugclaw-drug-discovery-agent/crates/drugclaw-channels/src/channel_adapter.rscode · 3.0 KB
use std::collections::HashMap; use std::path::Path; use std::sync::Arc; use async_trait::async_trait; use crate::channel::ConversationKind; #[async_trait] pub trait ChannelAdapter: Send + Sync { /// Unique name: "telegram", "discord", "slack", "web" fn name(&self) -> &str; /// DB chat_type strings this adapter handles + whether each is private/group. fn chat_type_routes(&self) -> Vec<(&str, ConversationKind)>; /// Whether this channel stores messages only (no external delivery). Web = true. fn is_local_only(&self) -> bool { false } /// Whether chats on this channel can operate on other chats. Web = false. fn allows_cross_chat(&self) -> bool { true } /// Send text to external chat. Called by deliver_and_store_bot_message. async fn send_text(&self, external_chat_id: &str, text: &str) -> Result<(), String>; /// Send file attachment. Default: not supported. async fn send_attachment( &self, _external_chat_id: &str, _file_path: &Path, _caption: Option<&str>, ) -> Result<String, String> { Err(format!("attachments not supported for {}", self.name())) } } #[derive(Default)] pub struct ChannelRegistry { adapters: HashMap<String, Arc<dyn ChannelAdapter>>, /// "slack_dm" -> "slack", "telegram_private" -> "telegram", etc. type_to_channel: HashMap<String, String>, /// "slack_dm" -> Private, "group" -> Group, etc. type_to_conversation: HashMap<String, ConversationKind>, } impl ChannelRegistry { pub fn new() -> Self { Self::default() } pub fn register(&mut self, adapter: Arc<dyn ChannelAdapter>) { let name = adapter.name().to_string(); for (chat_type, kind) in adapter.chat_type_routes() { self.type_to_channel .insert(chat_type.to_string(), name.clone()); self.type_to_conversation .insert(chat_type.to_string(), kind); } self.adapters.insert(name, adapter); } pub fn get(&self, name: &str) -> Option<&Arc<dyn ChannelAdapter>> { self.adapters.get(name) } /// Resolve a DB chat_type string to the adapter and conversation kind. pub fn resolve( &self, db_chat_type: &str, ) -> Option<(&Arc<dyn ChannelAdapter>, ConversationKind)> { let channel_name = self.type_to_channel.get(db_chat_type)?; let adapter = self.adapters.get(channel_name)?; let kind = self.type_to_conversation.get(db_chat_type)?; Some((adapter, *kind)) } /// Resolve only the channel name and conversation kind (without needing the adapter). pub fn resolve_routing(&self, db_chat_type: &str) -> Option<(&str, ConversationKind)> { let channel_name = self.type_to_channel.get(db_chat_type)?; let kind = self.type_to_conversation.get(db_chat_type)?; Some((channel_name.as_str(), *kind)) } pub fn has_any(&self) -> bool { !self.adapters.is_empty() } }drugclaw-drug-discovery-agent/crates/drugclaw-channels/src/delivery.rscode · 0.1 KB
// Delivery logic has been absorbed into individual ChannelAdapter implementations. // Text splitting helpers are in drugclaw-core::text.drugclaw-drug-discovery-agent/crates/drugclaw-channels/src/lib.rscode · 0.1 KB
//! Channel abstractions and adapters for DrugClaw. pub mod channel; pub mod channel_adapter; pub mod delivery;drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/Cargo.tomlcode · 0.4 KB
[package] name = "drugclaw-clawhub" version = "0.1.0" edition = "2021" license = "Apache-2.0" [dependencies] drugclaw-core = { path = "../drugclaw-core" } reqwest = { version = "0.12", features = ["json", "blocking"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" zip = "2" chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] uuid = { version = "1", features = ["v4"] }drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/src/client.rscode · 5.7 KB
use crate::types::*; use drugclaw_core::error::DrugClawError; pub struct ClawHubClient { base_url: String, token: Option<String>, client: reqwest::Client, } impl ClawHubClient { pub fn new(base_url: &str, token: Option<String>) -> Self { Self { base_url: base_url.to_string(), token, client: reqwest::Client::new(), } } /// Search skills by query pub async fn search( &self, query: &str, limit: usize, _sort: &str, ) -> Result<Vec<SearchResult>, DrugClawError> { // Use the dedicated search endpoint that actually filters by query let url = format!( "{}/api/v1/search?q={}&limit={}", self.base_url, query, limit ); let mut req = self.client.get(&url); if let Some(ref token) = self.token { req = req.header("Authorization", format!("Bearer {}", token)); } let resp = req .send() .await .map_err(|e| DrugClawError::Config(format!("ClawHub request failed: {}", e)))?; let search_response: ApiSearchResponse = resp .json() .await .map_err(|e| DrugClawError::Config(format!("Failed to parse search results: {}", e)))?; // Convert API response items to internal SearchResult type Ok(search_response .results .into_iter() .take(limit) .map(SearchResult::from) .collect()) } /// Get skill metadata by slug pub async fn get_skill(&self, slug: &str) -> Result<SkillMeta, DrugClawError> { let url = format!("{}/api/v1/skills/{}", self.base_url, slug); let mut req = self.client.get(&url); if let Some(ref token) = self.token { req = req.header("Authorization", format!("Bearer {}", token)); } let resp = req .send() .await .map_err(|e| DrugClawError::Config(format!("ClawHub request failed: {}", e)))?; let get_response: GetSkillResponse = resp .json() .await .map_err(|e| DrugClawError::Config(format!("Failed to parse skill metadata: {}", e)))?; // Convert API response to internal SkillMeta type Ok(SkillMeta::from(get_response)) } /// Download skill as ZIP bytes pub async fn download_skill( &self, slug: &str, version: &str, ) -> Result<Vec<u8>, DrugClawError> { // Prefer the configured registry domain first. let mut candidate_urls = vec![ format!( "{}/api/v1/download?slug={}&version={}", self.base_url, slug, version ), format!( "{}/api/v1/skills/{}/download?version={}", self.base_url, slug, version ), ]; // Backward-compatible fallback used by the hosted registry. if self.base_url.contains("clawhub.ai") { candidate_urls.push(format!( "https://wry-manatee-359.convex.site/api/v1/download?slug={}&version={}", slug, version )); } let mut last_error: Option<DrugClawError> = None; for url in candidate_urls { let mut req = self.client.get(&url); if let Some(ref token) = self.token { req = req.header("Authorization", format!("Bearer {}", token)); } let resp = match req.send().await { Ok(resp) => resp, Err(e) => { last_error = Some(DrugClawError::Config(format!( "ClawHub download failed at {}: {}", url, e ))); continue; } }; let resp = match resp.error_for_status() { Ok(resp) => resp, Err(e) => { last_error = Some(DrugClawError::Config(format!( "ClawHub download HTTP error at {}: {}", url, e ))); continue; } }; let bytes = resp.bytes().await.map_err(|e| { DrugClawError::Config(format!("Failed to read download from {}: {}", url, e)) })?; return Ok(bytes.to_vec()); } Err(last_error.unwrap_or_else(|| { DrugClawError::Config("ClawHub download failed: no usable endpoint".into()) })) } /// List versions for a skill pub async fn get_versions(&self, slug: &str) -> Result<Vec<SkillVersion>, DrugClawError> { let url = format!("{}/api/v1/skills/{}/versions", self.base_url, slug); let mut req = self.client.get(&url); if let Some(ref token) = self.token { req = req.header("Authorization", format!("Bearer {}", token)); } let resp = req .send() .await .map_err(|e| DrugClawError::Config(format!("ClawHub request failed: {}", e)))?; let versions: Vec<SkillVersion> = resp .json() .await .map_err(|e| DrugClawError::Config(format!("Failed to parse versions: {}", e)))?; Ok(versions) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_client_construction() { let client = ClawHubClient::new("https://clawhub.ai", None); assert_eq!(client.base_url, "https://clawhub.ai"); assert!(client.token.is_none()); } #[test] fn test_client_with_token() { let client = ClawHubClient::new("https://clawhub.ai", Some("test-token".into())); assert!(client.token.is_some()); } }drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/src/gate.rscode · 3.9 KB
use crate::types::Requires; use std::env; pub struct GateCheckResult { pub missing_bins: Vec<String>, pub missing_envs: Vec<String>, pub wrong_os: bool, } /// Check if skill requirements are met pub fn check_requirements(requires: &Option<Requires>, os: &[String]) -> GateCheckResult { let mut result = GateCheckResult { missing_bins: vec![], missing_envs: vec![], wrong_os: false, }; if let Some(req) = requires { // Check bins for bin in &req.bins { if !command_exists(bin) { result.missing_bins.push(bin.clone()); } } // Check anyBins (at least one must exist) if !req.any_bins.is_empty() { let any_exists = req.any_bins.iter().any(|b| command_exists(b)); if !any_exists && !req.bins.is_empty() { // If anyBins is specified and no bins passed, check if we should warn } } // Check env vars for env_var in &req.env { if env::var(env_var).is_err() { result.missing_envs.push(env_var.clone()); } } } // Check OS if !os.is_empty() { let current_os = current_platform(); let os_matches = os.iter().any(|o| { let o = o.to_lowercase(); o == "all" || o == "*" || normalize_platform(&o) == current_os }); result.wrong_os = !os_matches; } result } fn current_platform() -> String { #[cfg(target_os = "macos")] return "darwin".to_string(); #[cfg(target_os = "linux")] return "linux".to_string(); #[cfg(target_os = "windows")] return "windows".to_string(); #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] return "unknown".to_string(); } fn normalize_platform(s: &str) -> String { let s = s.trim().to_lowercase(); match s.as_str() { "macos" | "osx" => "darwin".to_string(), _ => s, } } fn command_exists(cmd: &str) -> bool { if cmd.trim().is_empty() { return true; } let path_var = env::var_os("PATH").unwrap_or_default(); let paths = env::split_paths(&path_var); #[cfg(target_os = "windows")] let candidates: Vec<String> = { let exts = env::var("PATHEXT").unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".into()); let ext_list: Vec<String> = exts .split(';') .map(|s| s.trim().to_ascii_lowercase()) .filter(|s| !s.is_empty()) .collect(); let lower = cmd.to_ascii_lowercase(); if ext_list.iter().any(|ext| lower.ends_with(ext)) { vec![cmd.to_string()] } else { let mut c = vec![cmd.to_string()]; for ext in ext_list { c.push(format!("{}{}", cmd, ext)); } c } }; #[cfg(not(target_os = "windows"))] let candidates: Vec<String> = vec![cmd.to_string()]; for base in paths { for candidate in &candidates { let full = base.join(candidate); if full.is_file() { return true; } } } false } #[cfg(test)] mod tests { use super::*; #[test] fn test_current_platform() { let platform = current_platform(); assert!(!platform.is_empty()); } #[test] fn test_normalize_platform() { assert_eq!(normalize_platform("darwin"), "darwin"); assert_eq!(normalize_platform("macOS"), "darwin"); assert_eq!(normalize_platform("osx"), "darwin"); } #[test] fn test_command_exists() { // Should find system commands assert!(command_exists("ls") || command_exists("dir")); } #[test] fn test_check_requirements_empty() { let result = check_requirements(&None, &[]); assert!(result.missing_bins.is_empty()); assert!(result.missing_envs.is_empty()); assert!(!result.wrong_os); } }drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/src/install.rscode · 5.1 KB
use crate::client::ClawHubClient; use crate::gate::check_requirements; use crate::lockfile::{is_clawhub_managed, read_lockfile, write_lockfile}; use crate::types::{LockEntry, LockFile}; use drugclaw_core::error::DrugClawError; use sha2::{Digest, Sha256}; use std::path::Path; use zip::ZipArchive; #[derive(Clone)] pub struct InstallOptions { pub force: bool, pub skip_gates: bool, pub skip_security: bool, } pub struct InstallResult { pub success: bool, pub message: String, pub requires_restart: bool, } /// Gate check warning for user display pub struct GateWarning { pub missing_bins: Vec<String>, pub missing_envs: Vec<String>, pub wrong_os: bool, } /// Security warning for user display pub struct SecurityWarning { pub report_count: i32, pub pending_scan: bool, pub status: String, pub url: String, } /// Main install function pub async fn install_skill( client: &ClawHubClient, slug: &str, version: Option<&str>, skills_dir: &Path, lockfile_path: &Path, options: &InstallOptions, ) -> Result<InstallResult, DrugClawError> { // 1. Get skill metadata let meta = client.get_skill(slug).await?; // 2. Resolve version let target_version = version.unwrap_or("latest"); let actual_version = if target_version == "latest" { meta.versions .iter() .find(|v| v.latest) .map(|v| v.version.clone()) .unwrap_or_else(|| "latest".to_string()) } else { target_version.to_string() }; // 3. Gate checks (unless skipped) if !options.skip_gates { let req = &meta .metadata .openclaw .as_ref() .and_then(|o| o.requires.clone()) .or_else(|| { meta.metadata .clawdbot .as_ref() .and_then(|c| c.requires.clone()) }); let os_list = meta .metadata .openclaw .as_ref() .map(|o| o.os.clone()) .unwrap_or_default(); let _gate_result = check_requirements(req, &os_list); // TODO: Return warning info if gates fail } // 4. Security check if !options.skip_security { if let Some(vt) = &meta.virustotal { if vt.report_count >= 3 || vt.pending_scan { // TODO: Return warning } } } // 5. Check existing installation let skill_path = skills_dir.join(slug); let lock = read_lockfile(lockfile_path)?; let is_managed = is_clawhub_managed(&lock, slug); if skill_path.exists() && !options.force && is_managed { return Ok(InstallResult { success: false, message: format!( "Skill '{}' is already installed. Use --force to update.", slug ), requires_restart: false, }); } if skill_path.exists() && !options.force { // Manual skill exists - hybrid: warn but allow } // 6. Download let bytes = client.download_skill(slug, &actual_version).await?; // 7. Verify hash (if provided) let hash = format!("sha256:{:x}", Sha256::digest(&bytes)); // 8. Extract if skill_path.exists() && options.force { std::fs::remove_dir_all(&skill_path)?; } std::fs::create_dir_all(&skill_path)?; let cursor = std::io::Cursor::new(bytes); let mut archive = ZipArchive::new(cursor) .map_err(|e| DrugClawError::Config(format!("Failed to read ZIP: {}", e)))?; archive .extract(&skill_path) .map_err(|e| DrugClawError::Config(format!("Failed to extract ZIP: {}", e)))?; // 9. Update lockfile let mut lock = read_lockfile(lockfile_path)?; let now = chrono::Utc::now().to_rfc3339(); lock.skills.insert( slug.to_string(), LockEntry { slug: slug.to_string(), installed_version: actual_version.clone(), installed_at: now, content_hash: hash, local_path: skill_path.to_string_lossy().to_string(), }, ); write_lockfile(lockfile_path, &lock)?; Ok(InstallResult { success: true, message: format!("Installed {} v{}", slug, actual_version), requires_restart: true, }) } /// Check if update is needed pub fn check_update_available( _lock: &LockFile, current_version: &str, latest_version: &str, ) -> bool { // Simple version comparison - could use semver for more accuracy current_version != latest_version } #[cfg(test)] mod tests { use crate::types::LockFile; use std::collections::HashMap; use super::check_update_available; #[test] fn test_check_update_available_true_when_version_changes() { let lock = LockFile { version: 1, skills: HashMap::new(), }; assert!(check_update_available(&lock, "1.0.0", "1.1.0")); } #[test] fn test_check_update_available_false_when_version_unchanged() { let lock = LockFile { version: 1, skills: HashMap::new(), }; assert!(!check_update_available(&lock, "1.0.0", "1.0.0")); } }drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/src/lib.rscode · 0.1 KB
pub mod client; pub mod gate; pub mod install; pub mod lockfile; pub mod types; pub use types::*;drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/src/lockfile.rscode · 3.3 KB
use crate::types::LockFile; use drugclaw_core::error::DrugClawError; use std::collections::HashMap; use std::path::Path; /// Read lockfile from disk pub fn read_lockfile(path: &Path) -> Result<LockFile, DrugClawError> { if !path.exists() { return Ok(LockFile { version: 1, skills: HashMap::new(), }); } let content = std::fs::read_to_string(path) .map_err(|e| DrugClawError::Config(format!("Failed to read lockfile: {}", e)))?; let lock: LockFile = serde_json::from_str(&content) .map_err(|e| DrugClawError::Config(format!("Failed to parse lockfile: {}", e)))?; Ok(lock) } /// Write lockfile to disk pub fn write_lockfile(path: &Path, lock: &LockFile) -> Result<(), DrugClawError> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .map_err(|e| DrugClawError::Config(format!("Failed to create lockfile dir: {}", e)))?; } let content = serde_json::to_string_pretty(lock) .map_err(|e| DrugClawError::Config(format!("Failed to serialize lockfile: {}", e)))?; std::fs::write(path, content) .map_err(|e| DrugClawError::Config(format!("Failed to write lockfile: {}", e)))?; Ok(()) } /// Check if a skill is managed by ClawHub (in lockfile) pub fn is_clawhub_managed(lock: &LockFile, slug: &str) -> bool { lock.skills.contains_key(slug) } #[cfg(test)] mod tests { use super::*; use crate::types::LockEntry; #[test] fn test_lockfile_missing_returns_empty() { let temp_path = std::env::temp_dir().join(format!("nonexistent_{}", uuid::Uuid::new_v4())); let lock = read_lockfile(&temp_path).unwrap(); assert_eq!(lock.version, 1); assert!(lock.skills.is_empty()); } #[test] fn test_lockfile_roundtrip() { let temp_dir = std::env::temp_dir().join(format!("clawhub_test_{}", uuid::Uuid::new_v4())); std::fs::create_dir_all(&temp_dir).unwrap(); let lock_path = temp_dir.join("clawhub.lock.json"); let mut lock = LockFile { version: 1, skills: HashMap::new(), }; lock.skills.insert( "test-skill".into(), LockEntry { slug: "test-skill".into(), installed_version: "1.0.0".into(), installed_at: "2026-02-18T00:00:00Z".into(), content_hash: "sha256:abc".into(), local_path: "/tmp/test".into(), }, ); write_lockfile(&lock_path, &lock).unwrap(); let read = read_lockfile(&lock_path).unwrap(); assert_eq!( read.skills.get("test-skill").unwrap().installed_version, "1.0.0" ); std::fs::remove_dir_all(&temp_dir).ok(); } #[test] fn test_is_clawhub_managed() { let mut lock = LockFile { version: 1, skills: HashMap::new(), }; lock.skills.insert( "my-skill".into(), LockEntry { slug: "my-skill".into(), installed_version: "1.0.0".into(), installed_at: "2026-02-18T00:00:00Z".into(), content_hash: "sha256:abc".into(), local_path: "/tmp/test".into(), }, ); assert!(is_clawhub_managed(&lock, "my-skill")); assert!(!is_clawhub_managed(&lock, "other-skill")); } }drugclaw-drug-discovery-agent/crates/drugclaw-clawhub/src/types.rscode · 7.4 KB
use serde::{Deserialize, Serialize}; use std::collections::HashMap; // ============================================================================= // API Response Types - These match the actual ClawHub API responses // ============================================================================= /// Dedicated search API response (the one that actually filters by query) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiSearchResponse { pub results: Vec<ApiSearchResult>, } /// Search result from dedicated search API #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ApiSearchResult { pub score: f64, pub slug: String, #[serde(rename = "displayName")] pub display_name: String, pub summary: String, pub version: String, #[serde(rename = "updatedAt")] pub updated_at: i64, } impl From<ApiSearchResult> for SearchResult { fn from(item: ApiSearchResult) -> Self { Self { slug: item.slug, name: item.display_name, description: item.summary, install_count: 0, // Not available in search response virustotal: None, } } } /// Search API response wrapper (list endpoint, doesn't filter by query) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResponse { pub items: Vec<SearchItem>, #[serde(rename = "nextCursor")] pub next_cursor: Option<String>, } /// Search result item from API #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchItem { pub slug: String, #[serde(rename = "displayName")] pub display_name: String, pub summary: String, pub tags: HashMap<String, String>, pub stats: SkillStats, #[serde(rename = "createdAt")] pub created_at: i64, #[serde(rename = "updatedAt")] pub updated_at: i64, #[serde(rename = "latestVersion")] pub latest_version: Option<VersionItem>, } /// Skill statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SkillStats { pub comments: i32, pub downloads: i32, #[serde(rename = "installsAllTime")] pub installs_all_time: i32, #[serde(rename = "installsCurrent")] pub installs_current: i32, pub stars: i32, pub versions: i32, } /// Version item from API #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VersionItem { pub version: String, #[serde(rename = "createdAt")] pub created_at: i64, pub changelog: String, } /// Get skill API response wrapper #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GetSkillResponse { pub skill: SkillItem, #[serde(rename = "latestVersion")] pub latest_version: VersionItem, pub owner: Owner, pub moderation: Option<serde_json::Value>, } /// Skill item from get skill API #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SkillItem { pub slug: String, #[serde(rename = "displayName")] pub display_name: String, pub summary: String, pub tags: HashMap<String, String>, pub stats: SkillStats, #[serde(rename = "createdAt")] pub created_at: i64, #[serde(rename = "updatedAt")] pub updated_at: i64, } /// Owner information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Owner { pub handle: String, #[serde(rename = "userId")] pub user_id: String, #[serde(rename = "displayName")] pub display_name: String, pub image: String, } // ============================================================================= // Internal Types - These are what the rest of the app uses // ============================================================================= /// Lockfile format for tracking ClawHub-installed skills #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LockFile { pub version: u32, pub skills: HashMap<String, LockEntry>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LockEntry { pub slug: String, #[serde(rename = "installedVersion")] pub installed_version: String, #[serde(rename = "installedAt")] pub installed_at: String, #[serde(rename = "contentHash")] pub content_hash: String, #[serde(rename = "localPath")] pub local_path: String, } /// Skill metadata from ClawHub API #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SkillMeta { pub slug: String, pub name: String, pub description: String, pub versions: Vec<SkillVersion>, #[serde(default)] pub virustotal: Option<VirusTotal>, #[serde(default)] pub metadata: SkillMetadata, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SkillVersion { pub version: String, #[serde(default)] pub latest: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VirusTotal { #[serde(rename = "reportCount")] pub report_count: i32, #[serde(rename = "pendingScan")] pub pending_scan: bool, pub status: String, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SkillMetadata { #[serde(default)] pub openclaw: Option<OpenClawMeta>, #[serde(default)] pub clawdbot: Option<ClawdbotMeta>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OpenClawMeta { #[serde(default)] pub requires: Option<Requires>, #[serde(default)] pub os: Vec<String>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClawdbotMeta { #[serde(default)] pub requires: Option<Requires>, #[serde(default)] pub os: Vec<String>, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Requires { #[serde(default)] pub bins: Vec<String>, #[serde(default)] pub env: Vec<String>, #[serde(default, rename = "anyBins")] pub any_bins: Vec<String>, } /// Search result item (internal representation) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResult { pub slug: String, pub name: String, pub description: String, #[serde(rename = "installCount")] pub install_count: i32, #[serde(default)] pub virustotal: Option<VirusTotal>, } impl From<SearchItem> for SearchResult { fn from(item: SearchItem) -> Self { Self { slug: item.slug, name: item.display_name, description: item.summary, install_count: item.stats.installs_current, virustotal: None, // Not available in search response } } } impl From<GetSkillResponse> for SkillMeta { fn from(resp: GetSkillResponse) -> Self { Self { slug: resp.skill.slug, name: resp.skill.display_name, description: resp.skill.summary, versions: vec![SkillVersion { version: resp.latest_version.version, latest: true, }], virustotal: None, // Not available in this response metadata: SkillMetadata::default(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_lockfile_serde() { let lock = LockFile { version: 1, skills: std::collections::HashMap::new(), }; let json = serde_json::to_string(&lock).unwrap(); assert!(json.contains(r#""version":1"#)); } #[test] fn test_skill_meta_deserialize() { let json = r#"{ "slug": "test-skill", "name": "Test Skill", "description": "A test skill", "versions": [{"version": "1.0.0", "latest": true}] }"#; let _meta: SkillMeta = serde_json::from_str(json).unwrap(); } }drugclaw-drug-discovery-agent/crates/drugclaw-core/Cargo.tomlcode · 0.3 KB
[package] name = "drugclaw-core" version = "0.1.0" edition = "2021" license = "Apache-2.0" [dependencies] reqwest = { version = "0.12", features = ["json", "blocking"] } rusqlite = { version = "0.37", features = ["bundled"] } serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2"drugclaw-drug-discovery-agent/crates/drugclaw-core/src/error.rscode · 2.0 KB
use thiserror::Error; #[derive(Error, Debug)] #[allow(dead_code)] pub enum DrugClawError { #[error("LLM API error: {0}")] LlmApi(String), #[error("Rate limited, retry after backoff")] RateLimited, #[error("Database error: {0}")] Database(#[from] rusqlite::Error), #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Tool execution error: {0}")] ToolExecution(String), #[error("Config error: {0}")] Config(String), #[error("Max tool iterations reached ({0})")] MaxIterations(usize), } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_display_messages() { let e = DrugClawError::LlmApi("bad request".into()); assert_eq!(e.to_string(), "LLM API error: bad request"); let e = DrugClawError::RateLimited; assert_eq!(e.to_string(), "Rate limited, retry after backoff"); let e = DrugClawError::ToolExecution("tool failed".into()); assert_eq!(e.to_string(), "Tool execution error: tool failed"); let e = DrugClawError::Config("missing key".into()); assert_eq!(e.to_string(), "Config error: missing key"); let e = DrugClawError::MaxIterations(25); assert_eq!(e.to_string(), "Max tool iterations reached (25)"); } #[test] fn test_error_from_io() { let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found"); let e: DrugClawError = io_err.into(); assert!(e.to_string().contains("not found")); } #[test] fn test_error_from_json() { let json_err = serde_json::from_str::<serde_json::Value>("{{invalid").unwrap_err(); let e: DrugClawError = json_err.into(); assert!(e.to_string().contains("JSON error")); } #[test] fn test_error_debug() { let e = DrugClawError::RateLimited; let debug = format!("{:?}", e); assert!(debug.contains("RateLimited")); } }drugclaw-drug-discovery-agent/crates/drugclaw-core/src/lib.rscode · 0.1 KB
//! Shared foundational types and helpers for DrugClaw. pub mod error; pub mod llm_types; pub mod text;drugclaw-drug-discovery-agent/crates/drugclaw-core/src/llm_types.rscode · 8.7 KB
use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ToolDefinition { pub name: String, pub description: String, pub input_schema: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageSource { #[serde(rename = "type")] pub source_type: String, pub media_type: String, pub data: String, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ContentBlock { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "image")] Image { source: ImageSource }, #[serde(rename = "tool_use")] ToolUse { id: String, name: String, input: serde_json::Value, #[serde(skip_serializing_if = "Option::is_none")] thought_signature: Option<String>, }, #[serde(rename = "tool_result")] ToolResult { tool_use_id: String, content: String, #[serde(skip_serializing_if = "Option::is_none")] is_error: Option<bool>, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Message { pub role: String, pub content: MessageContent, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum MessageContent { Text(String), Blocks(Vec<ContentBlock>), } #[derive(Debug, Clone, Serialize)] pub struct MessagesRequest { pub model: String, pub max_tokens: u32, pub system: String, pub messages: Vec<Message>, #[serde(skip_serializing_if = "Option::is_none")] pub tools: Option<Vec<ToolDefinition>>, #[serde(skip_serializing_if = "Option::is_none")] pub stream: Option<bool>, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct MessagesResponse { pub content: Vec<ResponseContentBlock>, pub stop_reason: Option<String>, pub usage: Option<Usage>, } #[derive(Debug, Clone, Deserialize)] #[serde(tag = "type")] pub enum ResponseContentBlock { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "tool_use")] ToolUse { id: String, name: String, input: serde_json::Value, #[serde(default)] thought_signature: Option<String>, }, /// Catch-all for unknown block types (e.g. "thinking" from MiniMax M2.5) #[serde(other)] Other, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct Usage { pub input_tokens: u32, pub output_tokens: u32, } #[cfg(test)] mod tests { use super::*; use serde_json::json; #[test] fn test_content_block_text_serialization() { let block = ContentBlock::Text { text: "hello".into(), }; let json = serde_json::to_value(&block).unwrap(); assert_eq!(json["type"], "text"); assert_eq!(json["text"], "hello"); } #[test] fn test_content_block_tool_use_serialization() { let block = ContentBlock::ToolUse { id: "id_123".into(), name: "bash".into(), input: json!({"command": "ls"}), thought_signature: None, }; let json = serde_json::to_value(&block).unwrap(); assert_eq!(json["type"], "tool_use"); assert_eq!(json["id"], "id_123"); assert_eq!(json["name"], "bash"); assert_eq!(json["input"]["command"], "ls"); } #[test] fn test_content_block_tool_result_serialization() { let block = ContentBlock::ToolResult { tool_use_id: "id_123".into(), content: "output".into(), is_error: Some(true), }; let json = serde_json::to_value(&block).unwrap(); assert_eq!(json["type"], "tool_result"); assert_eq!(json["tool_use_id"], "id_123"); assert_eq!(json["is_error"], true); } #[test] fn test_content_block_tool_result_skips_none_is_error() { let block = ContentBlock::ToolResult { tool_use_id: "id_123".into(), content: "output".into(), is_error: None, }; let json = serde_json::to_value(&block).unwrap(); assert!(json.get("is_error").is_none()); } #[test] fn test_message_content_text_serialization() { let msg = Message { role: "user".into(), content: MessageContent::Text("hello".into()), }; let json = serde_json::to_value(&msg).unwrap(); assert_eq!(json["role"], "user"); assert_eq!(json["content"], "hello"); } #[test] fn test_message_content_blocks_serialization() { let msg = Message { role: "assistant".into(), content: MessageContent::Blocks(vec![ContentBlock::Text { text: "thinking...".into(), }]), }; let json = serde_json::to_value(&msg).unwrap(); assert_eq!(json["role"], "assistant"); assert!(json["content"].is_array()); assert_eq!(json["content"][0]["type"], "text"); } #[test] fn test_messages_response_deserialization() { let json = json!({ "content": [ {"type": "text", "text": "Hello!"} ], "stop_reason": "end_turn", "usage": { "input_tokens": 10, "output_tokens": 5 } }); let resp: MessagesResponse = serde_json::from_value(json).unwrap(); assert_eq!(resp.stop_reason.as_deref(), Some("end_turn")); assert_eq!(resp.content.len(), 1); match &resp.content[0] { ResponseContentBlock::Text { text } => assert_eq!(text, "Hello!"), _ => panic!("Expected Text block"), } assert_eq!(resp.usage.as_ref().unwrap().input_tokens, 10); assert_eq!(resp.usage.as_ref().unwrap().output_tokens, 5); } #[test] fn test_response_content_block_tool_use_deserialization() { let json = json!({ "type": "tool_use", "id": "tu_abc", "name": "bash", "input": {"command": "echo hi"} }); let block: ResponseContentBlock = serde_json::from_value(json).unwrap(); match block { ResponseContentBlock::ToolUse { id, name, input, thought_signature, } => { assert_eq!(id, "tu_abc"); assert_eq!(name, "bash"); assert_eq!(input["command"], "echo hi"); assert!(thought_signature.is_none()); } _ => panic!("Expected ToolUse block"), } } #[test] fn test_messages_request_serialization() { let req = MessagesRequest { model: "claude-sonnet-4-5-20250929".into(), max_tokens: 4096, system: "You are helpful.".into(), messages: vec![Message { role: "user".into(), content: MessageContent::Text("hi".into()), }], tools: None, stream: None, }; let json = serde_json::to_value(&req).unwrap(); assert_eq!(json["model"], "claude-sonnet-4-5-20250929"); assert_eq!(json["max_tokens"], 4096); assert!(json.get("tools").is_none()); // skip_serializing_if None } #[test] fn test_messages_request_with_tools() { let req = MessagesRequest { model: "test".into(), max_tokens: 100, system: "sys".into(), messages: vec![], tools: Some(vec![ToolDefinition { name: "bash".into(), description: "Run bash".into(), input_schema: json!({"type": "object"}), }]), stream: None, }; let json = serde_json::to_value(&req).unwrap(); assert!(json["tools"].is_array()); assert_eq!(json["tools"][0]["name"], "bash"); } #[test] fn test_content_block_image_serialization() { let block = ContentBlock::Image { source: ImageSource { source_type: "base64".into(), media_type: "image/jpeg".into(), data: "abc123".into(), }, }; let json = serde_json::to_value(&block).unwrap(); assert_eq!(json["type"], "image"); assert_eq!(json["source"]["type"], "base64"); assert_eq!(json["source"]["media_type"], "image/jpeg"); assert_eq!(json["source"]["data"], "abc123"); } #[test] fn test_image_source_serialization() { let source = ImageSource { source_type: "base64".into(), media_type: "image/png".into(), data: "ABCDEF".into(), }; let json = serde_json::to_value(&source).unwrap(); assert_eq!(json["type"], "base64"); assert_eq!(json["media_type"], "image/png"); } }drugclaw-drug-discovery-agent/crates/drugclaw-core/src/text.rscode · 0.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/Cargo.tomlcode · 0.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/adapters/agentops.rscode · 0.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/adapters/langfuse.rscode · 1.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/adapters/mod.rscode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/lib.rscode · 0.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/logs.rscode · 4.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/metrics.rscode · 9.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/sdk.rscode · 1.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-observability/src/traces.rscode · 7.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-storage/Cargo.tomlcode · 0.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-storage/src/db.rstext · 214.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-storage/src/lib.rscode · 0.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-storage/src/memory.rscode · 7.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-storage/src/memory_quality.rscode · 6.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-storage/src/usage.rscode · 6.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/Cargo.tomlcode · 0.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/command_runner.rscode · 1.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/env_file.rscode · 2.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/lib.rscode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/path_guard.rscode · 12.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/runtime.rscode · 10.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/sandbox.rscode · 26.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/todo_store.rscode · 3.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/types.rscode · 1.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/web_content_validation.rscode · 6.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/web_fetch.rscode · 27.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/web_html.rscode · 7.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/crates/drugclaw-tools/src/web_search.rscode · 1.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/deploy.shcode · 2.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docker-compose.yamlcode · 0.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docker/drug-sandbox-docking.Dockerfiletext · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docker/drug-sandbox.Dockerfiletext · 1.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docker/requirements-docking.txtcode · 0.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docker/requirements-science.txtcode · 0.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/a2a.mdmarkdown · 1.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/assets/readme/agent-loop.svgtext · 3.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/assets/readme/drugclaw-architecture.svgtext · 7.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/assets/readme/memory-architecture.svgtext · 2.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/assets/readme/plan-execute.svgtext · 1.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/assets/readme/skills-lifecycle.svgtext · 2.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/assets/readme/task-scheduler.svgtext · 2.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/clawhub/overview.mdmarkdown · 1.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/generated/config-defaults.mdmarkdown · 4.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/generated/provider-matrix.mdmarkdown · 2.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/generated/tools.mdmarkdown · 1.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/hooks/HOOK.mdmarkdown · 1.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/llm-provider-conventions.mdmarkdown · 1.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/mcp-sdk-evaluation.mdmarkdown · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/observability/metrics.mdmarkdown · 3.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/operations/acp-stdio.mdmarkdown · 1.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/operations/hapi-bridge.mdmarkdown · 1.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/operations/http-hook-trigger.mdmarkdown · 2.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/operations/runbook.mdmarkdown · 8.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/operations/science-runtime.mdmarkdown · 12.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/operations/stability-plan-2026-q1.mdmarkdown · 4.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/plugins/overview.mdmarkdown · 2.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/releases/nixpkgs-upstream-guide.mdmarkdown · 2.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/releases/pr-release-checklist.mdmarkdown · 1.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/releases/upgrade-guide.mdmarkdown · 2.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/reports/sandbox-comparison-openclaw-nanoclaw-zeroclaw-2026-03.mdmarkdown · 3.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/rfcs/0001-authn-authz-model.mdmarkdown · 2.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/rfcs/0002-hooks-event-model.mdmarkdown · 2.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/rfcs/0003-session-fork-model.mdmarkdown · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/rfcs/0004-metrics-naming.mdmarkdown · 1.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/rfcs/0005-subagents-runtime-v1.mdmarkdown · 4.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/rfcs/README.mdmarkdown · 0.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/roadmap/feature-completion-pr-plan.mdmarkdown · 7.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/roadmap/feature-completion-tracking-board.mdmarkdown · 3.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/roadmap/stability-tracking-board-2026-q1.mdmarkdown · 1.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/roadmaps/sandbox-adoption-plan-2026-02.mdmarkdown · 1.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/security/execution-model.mdmarkdown · 2.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/docs/test/blackbox-core-20-cases.mdmarkdown · 8.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/drugclaw.config.example.yamlcode · 7.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/examples/plugins/README.mdmarkdown · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/examples/plugins/context-test.yamlcode · 0.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/examples/plugins/smoke-test.yamlcode · 2.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/flake.locktext · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/flake.nixtext · 2.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/README.mdmarkdown · 0.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/REDESIGN.mdmarkdown · 2.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/dist/assets/icon-BNruh3L4.pngbinary · 1328.7 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/gui/dist/assets/index-6bbKFz-Y.csscode · 53.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/dist/assets/index-Cxq8-Uqi.jstext · 170.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/dist/index.htmlcode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/index.htmlcode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/package-lock.jsontext · 85.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/package.jsoncode · 0.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/postcss.config.jscode · 0.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/Cargo.locktext · 119.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/Cargo.tomlcode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/build.rscode · 0.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/gen/schemas/acl-manifests.jsoncode · 62.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/gen/schemas/capabilities.jsoncode · 0.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/gen/schemas/desktop-schema.jsontext · 110.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/gen/schemas/macOS-schema.jsontext · 110.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/icons/icon.pngbinary · 1328.7 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/gui/src-tauri/src/main.rscode · 30.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src-tauri/tauri.conf.jsoncode · 0.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/App.tsxcode · 20.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/components/Charts.tsxcode · 1.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/components/Field.tsxcode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/components/StatusBadge.tsxcode · 1.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/components/Toggle.tsxcode · 0.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/config/providers.tscode · 1.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/main.tsxcode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/styles.csscode · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/types.tscode · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/views/OperationsView.tsxcode · 11.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/views/SetupView.tsxcode · 14.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/src/vite-env.d.tscode · 0.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/tailwind.config.jscode · 0.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/tsconfig.jsoncode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/gui/vite.config.tscode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/block-bash/HOOK.mdmarkdown · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/block-bash/hook.shcode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/block-global-memory/HOOK.mdmarkdown · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/block-global-memory/hook.shcode · 0.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/filter-global-structured-memory/HOOK.mdmarkdown · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/filter-global-structured-memory/hook.shcode · 1.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/redact-tool-output/HOOK.mdmarkdown · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/hooks/redact-tool-output/hook.shcode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/icon.pngbinary · 1171.5 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/install.ps1text · 7.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/install.shcode · 13.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/mcp.example.jsoncode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/mcp.hapi-bridge.example.jsoncode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/mcp.minimal.example.jsoncode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/mcp.peekaboo.example.jsoncode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/mcp.windows.desktop.example.jsoncode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/rust-toolchain.tomlcode · 0.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/ci/nightly_stability.shcode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/ci/stability_smoke.shcode · 0.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/generate_docs_artifacts.mjscode · 7.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/matrix-smoke-test.shcode · 10.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/release_finalize.shcode · 10.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/release_homebrew.shcode · 5.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/test_http_hooks.shcode · 8.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/scripts/update-nixpkgs.shcode · 6.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/apple-calendar/SKILL.mdmarkdown · 1.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/apple-notes/SKILL.mdmarkdown · 1.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/apple-reminders/SKILL.mdmarkdown · 1.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/docx/SKILL.mdmarkdown · 16.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/find-skills/SKILL.mdmarkdown · 1.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/github/SKILL.mdmarkdown · 1.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/pdf/SKILL.mdmarkdown · 7.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/pptx/SKILL.mdmarkdown · 9.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/skill-creator/SKILL.mdmarkdown · 17.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/weather/SKILL.mdmarkdown · 0.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/built-in/xlsx/SKILL.mdmarkdown · 11.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/genomics/grn-tools/SKILL.mdmarkdown · 2.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/genomics/grn-tools/templates/arboreto_grn.pycode · 6.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/genomics/variant-analysis-tools/SKILL.mdmarkdown · 2.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/genomics/variant-analysis-tools/templates/variant_report.pycode · 10.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/medical/clinical-research-tools/SKILL.mdmarkdown · 4.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/medical/medical-data-tools/SKILL.mdmarkdown · 4.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/medical/medical-data-tools/templates/clinical_cohort_profile.pycode · 6.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/medical/medical-data-tools/templates/dicom_inspect.pycode · 8.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/medical/medical-data-tools/templates/neuro_signal_analyze.pycode · 4.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/medical/medical-qms-tools/SKILL.mdmarkdown · 2.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/SKILL.mdmarkdown · 18.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/admet_screen.pycode · 5.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/assay_data_prepare.pycode · 13.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/binding_affinity_predict.pycode · 6.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/bioactivity_predict.pycode · 8.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/chem_ml_utils.pycode · 11.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/deepchem_featurize.pycode · 4.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/drugbank_lookup.pycode · 25.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/pdbbind_prepare.pycode · 14.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/protein_ligand_affinity.pycode · 26.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/protein_ligand_benchmark.pycode · 11.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/pyscf_single_point.pycode · 2.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/qsar_benchmark.pycode · 13.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/rdkit_descriptors.pycode · 4.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/chem-tools/templates/virtual_screen.pycode · 9.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/docking-tools/SKILL.mdmarkdown · 8.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/docking-tools/templates/README.mdmarkdown · 4.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/docking-tools/templates/docking_manifest.example.jsoncode · 1.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/docking-tools/templates/docking_workflow.pytext · 110.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-db-tools/SKILL.mdmarkdown · 5.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-db-tools/templates/pharma_db_lookup.pycode · 41.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-ml-tools/SKILL.mdmarkdown · 4.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-ml-tools/templates/datamol_library_profile.pycode · 4.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-ml-tools/templates/medchem_screen.pycode · 4.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-ml-tools/templates/molfeat_featurize.pycode · 3.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/pharma/pharma-ml-tools/templates/pytdc_dataset_fetch.pycode · 2.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/research/bayesian-optimization-tools/SKILL.mdmarkdown · 3.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/research/bayesian-optimization-tools/templates/bayesian_optimize.pycode · 12.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/research/target-intelligence-tools/SKILL.mdmarkdown · 2.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/research/target-intelligence-tools/templates/target_dossier.pycode · 15.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/bio-db-tools/SKILL.mdmarkdown · 5.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/bio-db-tools/templates/bio_db_lookup.pycode · 48.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/bio-tools/SKILL.mdmarkdown · 7.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/knowledge-graph-tools/SKILL.mdmarkdown · 6.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/knowledge-graph-tools/template/knowledge_graph.pycode · 23.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/literature-review-tools/SKILL.mdmarkdown · 3.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/literature-review-tools/templates/citation_table_normalize.pycode · 5.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/literature-review-tools/templates/evidence_matrix.pycode · 3.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/omics-tools/SKILL.mdmarkdown · 4.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/omics-tools/templates/mzml_summary.pycode · 2.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/omics-tools/templates/pysam_region_profile.pycode · 2.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/omics-tools/templates/single_cell_profile.pycode · 2.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/patent(US)-landscape-tools/SKILL.mdmarkdown · 7.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/patent(US)-landscape-tools/template/patent(us)_landscape.pycode · 13.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/scientific-visualization-tools/SKILL.mdmarkdown · 2.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/scientific-visualization-tools/templates/interactive_plot.pycode · 2.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/scientific-visualization-tools/templates/publication_plot.pycode · 3.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/scientific-workflow-tools/SKILL.mdmarkdown · 2.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/scientific-workflow-tools/templates/reproducibility_checklist.pycode · 3.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/stat-modeling-tools/SKILL.mdmarkdown · 3.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/stat-modeling-tools/templates/stat_test_report.pycode · 6.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/stat-modeling-tools/templates/statsmodels_regression.pycode · 3.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/survival-analysis-tools/SKILL.mdmarkdown · 2.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/skills/science/survival-analysis-tools/templates/survival_analysis.pycode · 6.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/a2a.rscode · 4.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/acp.rscode · 16.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/agent_engine.rstext · 133.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/dingtalk.rscode · 16.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/discord.rscode · 24.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/email.rscode · 19.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/feishu.rstext · 116.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/imessage.rscode · 7.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/irc.rscode · 22.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/matrix.rstext · 84.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/mod.rscode · 0.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/nostr.rscode · 16.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/qq.rscode · 15.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/signal.rscode · 15.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/slack.rscode · 48.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/startup_guard.rscode · 3.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/telegram.rstext · 88.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/channels/whatsapp.rscode · 22.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/chat_commands.rscode · 37.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/clawhub/cli.rscode · 7.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/clawhub/mod.rscode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/clawhub/service.rscode · 2.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/clawhub/tools.rscode · 6.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/codex_auth.rscode · 21.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/config.rstext · 102.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/doctor.rscode · 49.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/embedding.rscode · 6.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/gateway.rscode · 39.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/hooks.rscode · 22.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/http_client.rscode · 1.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/lib.rscode · 1.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/llm.rstext · 133.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/main.rscode · 26.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/mcp.rscode · 31.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/memory_backend.rscode · 44.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/memory_service.rscode · 19.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/plugins.rscode · 54.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/run_control.rscode · 3.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/runtime.rscode · 24.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/scheduler.rscode · 23.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/setup.rstext · 401.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/setup_def.rscode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/skills.rscode · 29.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/a2a.rscode · 10.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/activate_skill.rscode · 6.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/bash.rscode · 22.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/browser.rscode · 10.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/edit_file.rscode · 7.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/export_chat.rscode · 7.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/glob.rscode · 5.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/grep.rscode · 9.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/mcp.rscode · 3.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/memory.rscode · 24.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/mod.rscode · 30.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/read_file.rscode · 6.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/schedule.rscode · 48.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/send_message.rscode · 19.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/structured_memory.rscode · 18.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/subagents.rstext · 67.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/sync_skills.rscode · 14.9 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/time_math.rscode · 14.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/todo.rscode · 13.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/web_fetch.rscode · 5.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/web_search.rscode · 4.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/tools/write_file.rscode · 6.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web.rstext · 152.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/a2a.rscode · 3.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/auth.rscode · 14.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/config.rscode · 26.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/metrics.rscode · 9.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/middleware.rscode · 15.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/sessions.rscode · 10.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/skills.rscode · 2.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/stream.rscode · 14.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/src/web/ws.rscode · 21.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/start.shcode · 1.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/tests/config_validation.rscode · 7.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/tests/db_integration.rscode · 11.1 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/tests/tool_permissions.rscode · 6.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/uninstall.ps1text · 4.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/uninstall.shcode · 2.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/dist/assets/index-BNkQQi4D.jstext · 748.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/dist/assets/index-CQEtTM9z.csstext · 762.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/dist/favicon.icobinary · 14.7 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/web/dist/icon.pngbinary · 18.4 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/web/dist/index.htmlcode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/index.htmlcode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/package-lock.jsontext · 216.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/package.jsoncode · 0.8 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/public/favicon.icobinary · 14.7 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/web/public/icon.pngbinary · 18.4 KB
Binary — download to inspect.
drugclaw-drug-discovery-agent/web/src/components/chat-timeline.tsxcode · 1.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/components/config-field-card.tsxcode · 0.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/components/message-markdown.tsxcode · 2.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/components/session-sidebar.tsxcode · 15.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/components/skills-settings.tsxcode · 5.0 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/components/usage-panel.tsxcode · 15.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/lib/api.tscode · 1.7 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/lib/ui.tscode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/lib/utils.tscode · 0.2 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/main.tsxtext · 212.5 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/styles.csscode · 21.6 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/src/types.tscode · 0.3 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/tsconfig.jsoncode · 0.4 KB
No inline preview for this entry.
drugclaw-drug-discovery-agent/web/vite.config.tscode · 0.2 KB
No inline preview for this entry.