TalentDelta

Our site is fully accessible by humans and agents (via WebMCP).

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

Provenance

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
    website
    
  • drugclaw-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 --release
    
  • drugclaw-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 .#drugclaw
  • drugclaw-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 1
  • drugclaw-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}" \
                --clobber
    
  • drugclaw-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 boot
    • agent_engine.rs: shared agent loop (process_with_agent), explicit-memory fast path, compaction, tool loop
    • hooks.rs: hooks discovery/runtime/CLI (hooks list/info/enable/disable)
    • llm.rs: provider implementations + stream handling + format translation
    • otlp.rs: OTLP metrics exporter (HTTP/protobuf)
    • web.rs: Web API router, shared web state, stream APIs, config endpoints
    • web/auth.rs: auth handlers (session login/logout, password, API key lifecycle)
    • web/config.rs: config read/update + config self-check handlers
    • web/sessions.rs: session/history/reset/delete/fork/tree handlers
    • web/metrics.rs: metrics snapshot/history handlers
    • web/stream.rs: streaming send/status/SSE handlers
    • scheduler.rs: scheduled-task runner + memory reflector loop
    • skills.rs: skill discovery/activation
    • mcp.rs: MCP server/tool integration
    • gateway.rs: event stream / request lifecycle infra
    • setup.rs: interactive setup wizard and provider presets
    • doctor.rs: environment diagnostics
    • channels/*.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 assembly
    • drugclaw-tools: tool runtime primitives, sandbox, path guards, web/todo helpers
    • drugclaw-channels: channel abstractions (channel, channel_adapter, delivery boundary)
    • drugclaw-app: app-level support modules (logging, builtin skills, transcribe)

    Tool system

    src/tools/mod.rs assembles built-in tools, while shared runtime primitives live in drugclaw-tools::runtime:

    • Tool trait (name, definition, execute)
    • ToolRegistry dispatch 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.md
    • website/docs/generated-tools.md

    Regenerate docs artifacts with:

    node scripts/generate_docs_artifacts.mjs
    

    Skills Storage

    • Default skills dir: <data_dir>/skills (default data_dir is ~/.drugclaw)
    • Config override: skills_dir in drugclaw.config.yaml (e.g. ~/.drugclaw/skills)
    • Compatibility policy: existing configured data_dir / skills_dir / working_dir keep working; new defaults apply only when unset
    • ClawHub lockfile: ~/.drugclaw/clawhub.lock.json

    Agent loop (high level)

    process_with_agent flow:

    1. Optional explicit-memory fast path (remember .../记住...) writes structured memory directly
    2. Load resumable session from sessions, or rebuild from chat history
    3. Build system prompt from file memory + structured memory context + skills catalog
    4. Compact old context if session exceeds limits
    5. Call provider with tool schemas
    6. If tool_use: execute tool(s), append results, loop
    7. If end_turn: persist session and return text

    Memory architecture

    Two layers:

    1. File memory:
    • Global: runtime/groups/AGENTS.md
    • Chat: runtime/groups/{chat_id}/AGENTS.md
    1. Structured memory (memories table):
    • 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_runs
    • memory_injection_logs

    Surfaces:

    • /api/usage summary block
    • /api/memory_observability time-window series API
    • Web Usage Panel trends/cards

    Database

    drugclaw-storage::db includes:

    • 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.rs routes 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_capacity
    • otlp_retry_max_attempts
    • otlp_retry_base_ms
    • otlp_retry_max_ms

    Build and test

    cargo build
    cargo test
    npm --prefix web run build
    npm --prefix website run build
    

    Docs drift guard (CI + local):

    node scripts/generate_docs_artifacts.mjs --check
    

    Collaboration conventions

    • For the separate website repository (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 GUI
    • src/ -- Rust source for the bot binary
    • web/ -- Built-in Web UI (React + Vite). Compiled to web/dist/ and embedded into the Rust binary via include_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 modules
    • crates/drugclaw-tools/ -- tool runtime primitives (trait/auth/risk/schema/path) + sandbox
    • crates/drugclaw-channels/ -- channel abstraction and delivery boundary modules
    • crates/drugclaw-app/ -- app-level support modules (logging, builtin skills, transcribe)
    • src/main.rs -- entry point, CLI
    • src/runtime.rs -- app wiring (AppState), provider/tool initialization, channel boot
    • src/agent_engine.rs -- shared agent loop (process_with_agent)
    • src/llm.rs -- provider implementations + format translation
    • src/web.rs -- web API routes and streaming
    • src/memory.rs -- file-memory manager (runtime/groups/.../AGENTS.md)
    • src/scheduler.rs -- background scheduler + memory reflector loops
    • src/channels/*.rs -- Telegram/Discord/Slack/Feishu adapters
    • src/tools/*.rs -- concrete built-in tools; registry assembly in src/tools/mod.rs

    Key patterns

    • Agentic loop in agent_engine.rs:process_with_agent: call provider -> if tool_use -> execute -> loop (up to max_tool_iterations)
    • Session resume: full Vec<Message> (including tool_use/tool_result blocks) persisted in sessions table; on next invocation, loaded and appended with new user messages. /reset clears 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_agent tool 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: AppState in Arc, tools hold Bot / Arc<Database> as needed
    • Group catch-up: db.get_messages_since_last_bot_response() loads all messages since bot's last reply
    • Scheduler: tokio::spawn loop, polls DB for due tasks, calls process_with_agent with override_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_guard module
    • 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_path config, data_dir/SOUL.md, or ./SOUL.md. Per-chat overrides via data_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 -- help
    

    Configuration

    DrugClaw uses drugclaw.config.yaml (or .yml) for configuration. Override the path with MICROCLAW_CONFIG env var. See drugclaw.config.example.yaml for all available fields.

    Soul (personality customization)

    DrugClaw supports a SOUL.md file 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):

    1. soul_path in config (explicit path)
    2. <data_dir>/SOUL.md
    3. ./SOUL.md (project root, ships with the repo as the default soul)

    Per-chat override: place a SOUL.md at <data_dir>/runtime/groups/<chat_id>/SOUL.md to give a specific chat a different personality.

    Implementation: load_soul_content() and build_system_prompt() in src/agent_engine.rs. The soul content is wrapped in <soul> XML tags in the system prompt.

    Adding a tool

    1. Create src/tools/my_tool.rs implementing the Tool trait
    2. Add pub mod my_tool; to src/tools/mod.rs
    3. Register in ToolRegistry::new() with Box::new(my_tool::MyTool::new(...))

    Database

    Core persistence is provided by drugclaw-storage (Database wrapper 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 -- start
    

    Prerequisites

    • 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 assembly
    

    Architecture 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 SQLite
    

    The 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_agent flow.

    Key types

    TypeLocationDescription
    AppStateruntime.rsShared runtime state for all adapters
    Databasedrugclaw_storage::dbSQLite wrapper with Mutex<Connection>
    ToolRegistrytools/mod.rsHolds all Box<dyn Tool>, dispatches by name
    Tool traitdrugclaw_tools::runtimename(), definition(), execute()
    LlmProviderllm.rsProvider abstraction for Anthropic and OpenAI-compatible APIs
    MemoryManagermemory.rsAGENTS.md file memory reader/writer

    Shared state

    AppState is wrapped in Arc and 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 Bot or Database hold their own clones/arcs (passed at construction)

    Multi-chat permission model

    • control_chat_ids in drugclaw.config.yaml defines 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_memory with scope: "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:

    ColumnTypeDescription
    chat_idINTEGER PKChannel-scoped chat ID
    chat_titleTEXTChat title (nullable)
    chat_typeTEXT"private" or "group"
    last_message_timeTEXTISO 8601 timestamp

    messages:

    ColumnTypeDescription
    idTEXTMessage ID (PK with chat_id)
    chat_idINTEGERChannel-scoped chat ID
    sender_nameTEXTUsername or first name
    contentTEXTMessage text
    is_from_botINTEGER0 or 1
    timestampTEXTISO 8601 timestamp

    scheduled_tasks:

    ColumnTypeDescription
    idINTEGER PKAuto-increment task ID
    chat_idINTEGERTarget chat for results
    promptTEXTInstruction to execute
    schedule_typeTEXT"cron" or "once"
    schedule_valueTEXTCron expression or ISO timestamp
    next_runTEXTNext scheduled execution time
    last_runTEXTLast execution time (nullable)
    statusTEXT"active", "paused", "completed", "cancelled"
    created_atTEXTCreation timestamp

    sessions:

    ColumnTypeDescription
    chat_idINTEGER PKChannel-scoped chat ID
    messages_jsonTEXTSerialized Vec JSON (full conversation state)
    updated_atTEXTISO 8601 timestamp of last save

    task_run_logs:

    ColumnTypeDescription
    idINTEGER PKAuto-increment log ID
    task_idINTEGERAssociated scheduled task
    chat_idINTEGERChat the task ran in
    started_atTEXTRun start timestamp
    finished_atTEXTRun end timestamp
    duration_msINTEGERRun duration in milliseconds
    successINTEGER0 or 1
    result_summaryTEXTSummary of run result (nullable)

    Adding a new tool

    1. 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}"))
        }
    }
    
    1. Add pub mod my_tool; to src/tools/mod.rs

    2. Register in ToolRegistry::new():

    Box::new(my_tool::MyTool),
    

    If your tool needs shared state (like Bot or Arc<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:

    1. Add a new runtime module (for example src/<platform>.rs) that listens to platform events.
    2. 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
    3. Reuse process_with_agent(state, chat_id, sender, chat_type, override_prompt) for inference/tool loop/session handling.
    4. Implement outbound reply sending with:
      • platform length splitting policy
      • mention semantics for group/server channels
      • attachment support parity with send_message where applicable
    5. Keep chat identity stable across restarts so sessions, messages, scheduler, and memory scopes remain consistent.
    6. Enforce existing authorization boundaries (for example control_chat_ids) in any platform-specific entry points.
    7. Add end-to-end tests to TEST.md mirroring existing platform suites (DM/private, group mention, reset, limits, failure handling).

    Scheduler internals

    The scheduler is a tokio::spawn task started in runtime::run(). Every 60 seconds it:

    1. Queries scheduled_tasks for rows where status = 'active' AND next_run <= NOW()
    2. For each due task, calls process_with_agent() with override_prompt = Some(task.prompt)
    3. Sends the agent's response to the task's chat_id
    4. For cron tasks: computes next run from the cron expression and updates the row
    5. For one-shot tasks: sets status = 'completed'

    Cron expressions use the cron crate'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

    TaskHow
    Change the modelSet model in drugclaw.config.yaml based on llm_provider
    Increase context windowSet max_history_messages: 100 in drugclaw.config.yaml (uses more tokens)
    Increase tool iterationsSet max_tool_iterations: 200 in drugclaw.config.yaml
    Reset memoryDelete files under drugclaw.data/runtime/groups/
    Reset all dataDelete the drugclaw.data/ directory
    Tune compaction thresholdSet max_session_messages: 60 in drugclaw.config.yaml (higher = more context before compaction)
    Keep more recent messagesSet compact_keep_recent: 30 in drugclaw.config.yaml (more recent messages kept verbatim)
    Reset a chat sessionSend /reset in the chat, or: sqlite3 drugclaw.data/runtime/drugclaw.db "DELETE FROM sessions WHERE chat_id=XXXX;"
    Cancel all scheduled taskssqlite3 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 help
    

    The release binary is fully self-contained -- no runtime dependencies, no database server, no config files beyond drugclaw.config.yaml.

    Dependencies

    CrateVersionPurpose
    teloxide0.17Telegram Bot API
    serenity0.12Discord Gateway/API
    tokio1Async runtime
    reqwest0.12HTTP client (Anthropic API, web fetch/search)
    rusqlite0.32SQLite (bundled)
    serde / serde_json1Serialization
    async-trait0.1Async trait support
    chrono0.4Date/time handling
    cron0.13Cron expression parsing
    urlencoding2URL encoding for web search
    regex1Regex for grep tool and HTML parsing
    glob0.3File pattern matching
    uuid1Message ID generation
    thiserror2Error derive macro
    anyhow1Error propagation
    tracing0.1Logging
  • 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 CONDITIONS
    
  • drugclaw-drug-discovery-agent/NIXPKGS_README.mdmarkdown · 1.1 KB

    Nixpkgs Automation

    This repository includes automation for keeping drugclaw updated in NixOS/nixpkgs.

    One-command Flow

    Run from repo root:

    scripts/update-nixpkgs.sh
    

    By default, the script will:

    • detect version from Cargo.toml
    • clone <your-gh-user>/nixpkgs into /tmp/nixpkgs-<timestamp>
    • branch from upstream/nixos-unstable
    • update pkgs/by-name/mi/drugclaw/package.nix
    • resolve hash and cargoHash
    • run nix-build -A drugclaw and result/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.sh
    

    Useful 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/nixpkgs
    

    Failure 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-dir to reuse a local checkout
  • drugclaw-drug-discovery-agent/README.mdmarkdown · 22.7 KB

    DrugClaw

    English | 中文

    Website License: Apache%202.0

    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, and drugclaw start.
    • Treat docking/QSAR/ADMET outputs as prioritization signals, not experimental proof.

    Table of Contents

    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
    • Email
    • Nostr
    • Signal
    • DingTalk
    • QQ
    • 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.

    1. Protein Structure Rendering

    Fetch a PDB structure, render it in rainbow coloring with PyMOL, and send the image.

    1. PubMed Literature Search

    Search PubMed for recent high-impact papers and provide structured summaries.

    1. Hydrogen Bond Analysis

    Visualize hydrogen bonds between a ligand and protein in PDB 3BIK.

    1. Target Intelligence Dossier

    Build a concise target dossier by combining UniProt, OpenTargets, Reactome, STRING, ClinVar, and known-drug evidence into one brief.

    1. 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.

    1. 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 | bash
    

    When 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 | iex
    

    From source

    git clone https://github.com/DrugClaw/DrugClaw.git
    cd drugclaw
    cargo build
    npm --prefix web install
    npm --prefix web run build
    

    Uninstall

    ./uninstall.sh
    

    Quick Start

    1. Create config

    cp drugclaw.config.example.yaml drugclaw.config.yaml
    

    2. Run setup and diagnostics

    drugclaw setup
    drugclaw doctor
    

    If the default sandbox image is already present locally, drugclaw setup defaults the bash sandbox to enabled.

    3. Start runtime

    drugclaw start
    

    4. 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: 10961
    

    Recommended next steps:

    • enable one chat channel under channels:
    • set soul_path or add SOUL.md
    • enable sandboxing for code execution when you need stronger isolation
    • use drugclaw web password-generate for Web operator access

    Core Concepts

    Agent loop

    The runtime does one thing consistently across channels:

    1. load chat state and memory
    2. build the system prompt plus skills catalog
    3. call the selected model with tool schemas
    4. execute tools when requested
    5. 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.md plus chat-scoped files under runtime/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-tools
    • bio-db-tools
    • bayesian-optimization-tools
    • omics-tools
    • grn-tools
    • target-intelligence-tools
    • variant-analysis-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
    • docking-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:

    • BeforeLLMCall
    • BeforeToolCall
    • AfterToolCall

    Supported outcomes:

    • allow
    • block
    • modify

    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 list
    

    Reference: 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/agent
    • POST /api/hooks/agent
    • POST /hooks/wake
    • POST /api/hooks/wake

    Reference: docs/operations/http-hook-trigger.md

    Science Skills

    DrugClaw now ships a non-trivial scientific workflow layer.

    bio-tools

    Use for:

    • FASTA / FASTQ / BAM / BED workflows
    • BLAST, alignment, QC, plotting, structure rendering
    • literature search and general bioinformatics scripting

    bio-db-tools

    Use 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-tools

    Use for:

    • h5ad and 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.py
    • skills/science/omics-tools/templates/pysam_region_profile.py
    • skills/science/omics-tools/templates/mzml_summary.py

    grn-tools

    Use 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-tools

    Use 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-tools

    Use 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-tools

    Use 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-tools

    Use 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-tools

    Use 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.py
    • skills/pharma/pharma-ml-tools/templates/molfeat_featurize.py
    • skills/pharma/pharma-ml-tools/templates/pytdc_dataset_fetch.py
    • skills/pharma/pharma-ml-tools/templates/medchem_screen.py

    literature-review-tools

    Use 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.py
    • skills/science/literature-review-tools/templates/evidence_matrix.py

    medical-data-tools

    Use 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.py
    • skills/medical/medical-data-tools/templates/neuro_signal_analyze.py
    • skills/medical/medical-data-tools/templates/clinical_cohort_profile.py

    clinical-research-tools

    Use 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-tools

    Use 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-tools

    Use 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.py
    • skills/science/stat-modeling-tools/templates/statsmodels_regression.py

    survival-analysis-tools

    Use 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-tools

    Use 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.py
    • skills/science/scientific-visualization-tools/templates/interactive_plot.py

    scientific-workflow-tools

    Use 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-tools

    Use 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-tools

    Use 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.py
    • skills/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-*.txt so the science stack does not drift on every rebuild.

    Design choice:

    • drug-sandbox is the canonical shared runtime for bio, omics, chemistry, literature, medical-research, and docking skills
    • the legacy drug-sandbox-docking tag is retained only as a compatibility alias for older configs and scripts
    • dedicated med-sandbox or chem-sandbox images 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, model
    • data_dir, working_dir, working_dir_isolation
    • channels.*
    • sandbox.*
    • soul_path, souls_dir
    • plugins.*
    • clawhub_*
    • voice_provider, voice_transcription_command
    • web_host, web_port

    Useful facts:

    • channels: is the preferred configuration surface for modern channel setup.
    • SOUL.md can be global, channel-level, or account-level.
    • built-in skills are installed under <data_dir>/skills unless skills_dir overrides that path.
    • runtime env overrides still use MICROCLAW_* prefixes for compatibility, including MICROCLAW_CONFIG and MICROCLAW_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 version
    

    Examples:

    drugclaw web password-generate
    drugclaw hooks list
    drugclaw gateway status
    drugclaw skill search docking
    

    ACP stdio mode (optional)

    DrugClaw can run as an Agent Client Protocol (ACP) server over stdio:

    drugclaw acp
    

    Use 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 Linux
    • uninstall.sh: remove the installed binary
    • start.sh: build the Web UI and run the local runtime from source
    • check.sh: run the standard local validation set
    • deploy.sh: run release automation and optional nixpkgs follow-up
    • scripts/test_http_hooks.sh: smoke-test Web hook endpoints
    • scripts/matrix-smoke-test.sh: end-to-end Matrix ingestion smoke test
    • scripts/update-nixpkgs.sh: update the drugclaw package in nixpkgs

    These scripts are repository workflow glue, not a second control plane.

    Development

    Build

    cargo build
    npm --prefix web run build
    npm --prefix website run build
    

    Test

    cargo test
    ./check.sh
    

    Docs drift guard

    node scripts/generate_docs_artifacts.mjs --check
    

    Architecture

    Key files:

    Supporting crates:

    • drugclaw-core
    • drugclaw-storage
    • drugclaw-tools
    • drugclaw-channels
    • drugclaw-app

    Documentation Map

    Start here for deeper docs:

    License

    Apache License 2.0. See LICENSE.

    Star History

    Star History Chart


    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 LICENSE file 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

    English | 中文

    Website License: Apache%202.0

    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 的输出应视为优先级信号,而非实验结论。

    目录

    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
    • Email
    • Nostr
    • Signal
    • DingTalk
    • QQ
    • 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 处理真实任务的演示示例。

    1. Protein Structure Rendering

    获取一个 PDB 结构,用 PyMOL 按 rainbow coloring 渲染,并把图片发送回来。

    1. PubMed Literature Search

    检索近期高影响力的 PubMed 论文,并输出结构化摘要。

    1. Hydrogen Bond Analysis

    可视化 PDB 3BIK 中配体与蛋白之间的氢键。

    1. Target Intelligence Dossier

    整合 UniProt、OpenTargets、Reactome、STRING、ClinVar 和已知药物证据,生成一份紧凑的靶点 dossier。

    1. Compound Database Triage

    针对某个化合物或靶点检索 PubChem、ChEMBL 和 BindingDB,标准化活性记录,并返回排序后的摘要表。

    1. 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.yaml
    

    2. 跑 setup 和 doctor

    drugclaw setup
    drugclaw doctor
    

    如果本地已经准备好默认 sandbox 镜像,drugclaw setup 会默认把 bash sandbox 设为开启。

    3. 启动运行时

    drugclaw start
    

    4. 打开本地 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 在所有渠道里遵循同一条主链路:

    1. 加载聊天状态和记忆
    2. 组装 system prompt 与技能目录
    3. 调用模型并暴露工具 schema
    4. 在模型请求时执行工具
    5. 持久化新的会话和产物

    共享循环位于 src/agent_engine.rs。渠道只是收发适配层,不是独立 agent。

    记忆系统

    DrugClaw 有两层记忆:

    • 文件记忆:AGENTS.md 与 runtime/groups/ 下的聊天级文件
    • 结构化记忆:SQLite 中的事实、置信度、覆盖关系与观测日志

    这让长期上下文不必全部硬塞进一次 prompt。

    技能系统

    当前随包技能包括:

    • bio-tools
    • bio-db-tools
    • bayesian-optimization-tools
    • omics-tools
    • grn-tools
    • target-intelligence-tools
    • variant-analysis-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
    • docking-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 / 工具流量。

    支持事件:

    • BeforeLLMCall
    • BeforeToolCall
    • AfterToolCall

    支持动作:

    • allow
    • block
    • modify

    参考:docs/hooks/HOOK.md

    ClawHub

    ClawHub 是 DrugClaw 的技能发现与安装层。

    常用命令:

    drugclaw skill search <query>
    drugclaw skill install <slug>
    drugclaw skill list
    

    参考:docs/clawhub/overview.md

    Web UI 与 Hooks

    本地 Web 面不是附属功能,而是操作面的一部分。它暴露:

    • 跨渠道会话与历史浏览
    • 鉴权与 API key 管理
    • metrics 与 memory observability
    • 配置自检与运行操作
    • 供自动化接入的 HTTP hook 入口

    重要接口:

    • POST /hooks/agent
    • POST /api/hooks/agent
    • POST /hooks/wake
    • POST /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.py
    • skills/science/omics-tools/templates/pysam_region_profile.py
    • skills/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.py
    • skills/pharma/pharma-ml-tools/templates/molfeat_featurize.py
    • skills/pharma/pharma-ml-tools/templates/pytdc_dataset_fetch.py
    • skills/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.py
    • skills/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.py
    • skills/medical/medical-data-tools/templates/neuro_signal_analyze.py
    • skills/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.py
    • skills/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.py
    • skills/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.py
    • skills/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-docking tag 只保留为兼容别名,方便旧配置和旧脚本继续工作
    • 当前不额外提供 med-sandbox 或 chem-sandbox,因为依赖重叠太高,拆分只会增加运维复杂度

    参考:docs/operations/science-runtime.md

    配置模型

    主配置文件是 drugclaw.config.yaml。

    关键字段:

    • llm_provider, api_key, model
    • data_dir, working_dir, working_dir_isolation
    • channels.*
    • sandbox.*
    • soul_path, souls_dir
    • plugins.*
    • clawhub_*
    • voice_provider, voice_transcription_command
    • web_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 docking
    

    Shell 脚本

    仓库内保留了一组与运行时配套的脚本:

    • 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
    

    架构入口

    关键文件:

    支撑 crate:

    • drugclaw-core
    • drugclaw-storage
    • drugclaw-tools
    • drugclaw-channels
    • drugclaw-app

    文档索引

    需要更细的文档时,从这里继续:

    许可证

    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 check
    

    Black-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

    1. A working drugclaw.config.yaml file with valid credentials
    2. cargo build succeeds with zero errors
    3. Bot is running: cargo run -- start
    4. A Telegram account to send messages
    5. (For group tests) A Telegram group with the bot added as a member

    1. Startup & Configuration

    #User StoryStepsExpected
    1.1First launch without configRun drugclaw start with no config.yaml presentAuto-launches setup wizard
    1.2Normal startupFill config.yaml, run drugclaw startLogs: Database initialized / Memory manager initialized / Scheduler started
    1.3Missing required fieldRemove api_key from config, startError message + launches setup wizard
    1.4Invalid timezoneSet timezone: "Mars/Olympus"Startup error with invalid timezone message
    1.5CLI helpdrugclaw helpFull output: commands, features, config docs
    1.6CLI versiondrugclaw versionOutput: drugclaw {VERSION}
    1.7Unknown commanddrugclaw foobar"Unknown command: foobar" + help text
    1.8Setup wizarddrugclaw setupTUI interactive guide, provider/model selection
    1.9MICROCLAW_CONFIG env varMICROCLAW_CONFIG=/tmp/test.yaml drugclaw startLoads config from specified path

    2. Private Chat -- Basic Conversation

    #User StoryStepsExpected
    2.1Send plain textPrivate chat: send "Hello"Bot responds; typing indicator visible during processing
    2.2Empty messageSend message with no text/image/voiceBot does not respond, no crash
    2.3Very long inputSend 10000-character textBot processes and responds normally
    2.4Special charactersSend <script>alert('xss')</script>XML-escaped, processed safely, no injection
    2.5Emoji/UnicodeSend various emoji and CJK charactersProcessed and responded to normally
    2.6Long response splittingAsk for a 5000-character essayResponse split at newline boundaries into multiple messages (max 4096 chars each)
    2.7Typing indicator persistenceSend request requiring multiple tool callsTyping refreshes every ~4s until response completes
    2.8Rapid consecutive messagesSend 5 messages quicklyAll stored; bot processes each without dropping any

    3. Image Handling

    #User StoryStepsExpected
    3.1JPEG photoSend a JPEG photoBot describes image content
    3.2PNG screenshotSend a PNG imageCorrectly identified as PNG, processed
    3.3WebP imageSend a WebP format imageCorrectly identified as WebP, processed
    3.4GIF imageSend an animated GIFCorrectly identified as GIF, processed
    3.5Image + captionSend photo with caption "What is this?"Bot sees both image and caption, responds accordingly
    3.6Image without captionSend photo with no captionBot proactively describes image content
    3.7Image session persistenceSend image, then ask "What was in that image?"Bot recalls image from session (stored as [image was sent] placeholder)
    3.8Large imageSend high-resolution image (5MB+)Downloads largest resolution, processes without timeout

    4. Voice Messages

    #User StoryStepsExpected
    4.1Voice transcription (with key)Send voice message (openai_api_key configured)Bot transcribes via Whisper, processes as text
    4.2Voice without keySend voice message (no openai_api_key)"Voice messages not supported (no Whisper API key configured)"
    4.3Transcription failureSend corrupted audio fileEmbeds [transcription failed: {error}], no crash
    4.4Long voice messageSend 2-minute voice messageTranscribed and processed normally

    5. Session Management

    #User StoryStepsExpected
    5.1Conversation continuityMsg 1: "My name is Alice"; Msg 2: "What's my name?"Bot remembers "Alice"
    5.2Tool context preservedMsg 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 sessionChat several turns, send /reset, ask "What did we talk about?"Bot does not remember previous session context
    5.4/archive sessionChat several turns, send /archiveSession archived to <data_dir>/groups/<channel>/<chat_id>/conversations/<timestamp>.md
    5.5Context compaction triggerChat beyond max_session_messages thresholdOld messages auto-summarized; recent messages kept verbatim
    5.6Memory after compactionAfter compaction, ask about an early topicBot recalls key facts from summary (details may be lost)
    5.7Corrupted session recoveryManually corrupt sessions table JSON, then send messageFalls back to DB history, no crash
    5.8Session survives restartChat several turns → restart bot → continue chattingSession loaded from DB, conversation continues seamlessly

    6. Group Chat

    #User StoryStepsExpected
    6.1No @mention = no replySend plain message in groupBot does not reply; message is stored in DB
    6.2@mention triggers replySend @botusername helloBot responds
    6.3Group catch-upMultiple users send messages, then @bot "summarize"Bot's response covers ALL messages since its last reply
    6.4allowed_groups whitelist (allowed)Configure allowed_groups with current group ID@mention works normally
    6.5allowed_groups whitelist (blocked)Configure allowed_groups WITHOUT current group ID@mention gets no response; messages still stored
    6.6allowed_groups emptySet allowed_groups to [] or omitAll groups can @mention and get replies
    6.7Multi-group isolationChat in Group A, then @mention in Group BGroup B has independent session/memory, unaffected by Group A

    7. Tool -- Bash Execution

    #User StoryStepsExpected
    7.1Simple command"Run echo hello world"Returns "hello world"
    7.2Non-zero exit code"Run exit 1"Reports exit code 1
    7.3stderr output"Run ls /nonexistent 2>&1"Returns stderr content
    7.4Command timeout"Run sleep 300"Times out after 120s
    7.5Output truncationCommand producing >30KB outputOutput truncated with marker
    7.6Multi-step command"Count the .rs files in this directory"Bot uses bash correctly, returns result

    8. Tool -- File Operations

    #User StoryStepsExpected
    8.1Create file"Create /tmp/test.txt with content hello"File created successfully
    8.2Read file"Read /tmp/test.txt"Returns file content with line numbers
    8.3Edit file"Change hello to world in /tmp/test.txt"Find-replace succeeds
    8.4Read nonexistent file"Read /nonexistent/file.txt"Error: file not found, no crash
    8.5Edit non-unique stringFile has multiple "hello", request replaceError: string not unique
    8.6Read with offset/limit"Read /tmp/big.txt from line 100, 20 lines"Returns specified range
    8.7Auto-create directories"Create /tmp/a/b/c/test.txt"Intermediate directories auto-created, file written
    8.8Path guard: .ssh"Read ~/.ssh/id_rsa"Blocked by path_guard
    8.9Path guard: .env"Read .env"Blocked by path_guard
    8.10Path guard: .aws"Read ~/.aws/credentials"Blocked by path_guard
    8.11Path guard: /etc/shadow"Read /etc/shadow"Blocked by path_guard

    9. Tool -- File Search

    #User StoryStepsExpected
    9.1Glob search"Find all .rs files in the project"Returns matching file list
    9.2Glob no match"Find *.xyz files"Returns empty result or "no matches"
    9.3Grep content search"Search for 'fn main' in source code"Returns file names, line numbers, matching content
    9.4Grep regex search"Search for async fn.*execute"Regex matching works correctly
    9.5Grep no match"Search for 'xyznotexist123'"Returns no matches
    9.6Search excludes sensitive pathsGlob/Grep resultspath_guard filters sensitive paths from results

    10. Tool -- Web Search & Fetch

    #User StoryStepsExpected
    10.1Web search"Search for Rust programming language"Returns DuckDuckGo results (title + URL + snippet)
    10.2Web search no resultsSearch for extremely obscure keywordsReturns empty or "no results"
    10.3Web fetch"Fetch https://example.com"Returns plain text with HTML tags stripped
    10.4Web fetch large pageFetch page >20KBContent truncated to 20KB
    10.5Web fetch invalid URL"Fetch https://thisdomaindoesnotexist12345.com"Returns network error, no crash
    10.6Combined web research"Look up today's news and summarize"Bot combines web_search + web_fetch to complete task

    11. Tool -- Mid-Conversation Messaging

    #User StoryStepsExpected
    11.1Progress update"Send 'Processing...' first, then tell me today's date"Receive TWO messages: progress + final reply
    11.2Only send_message"Use send_message to say 'test' and nothing else"Receive exactly ONE message "test", no empty reply
    11.3Cross-chat send (regular user)"Send a message to chat_id 99999"Permission denied
    11.4Cross-chat send (control chat)From control_chat_id, specify another chat_idMessage sent successfully

    12. Memory System

    #User StoryStepsExpected
    12.1Write chat memory"Remember I like Rust"Written to <data_dir>/groups/{chat_id}/AGENTS.md
    12.2Read chat memory"What language do I like?"Bot recalls "Rust" from memory
    12.3Memory persists across /reset/reset, then "What do I like?"Still recalls (memory is independent of session)
    12.4Memory persists across restartRestart bot, then askStill recalls (memory is file-persisted)
    12.5Write global memory (control)From control chat, write global memoryWritten to <data_dir>/groups/AGENTS.md
    12.6Write global memory (regular)From regular chat, attempt global write"Permission denied: chat {id} cannot write global memory"
    12.7Read empty memoryNew chat, first read"No memory file found (not yet created)."
    12.8Memory overwriteWrite twice with different contentSecond write completely replaces first
    12.9Cross-chat read memory (regular)Read another chat_id's memoryPermission denied
    12.10Cross-chat read memory (control)From control chat, read other chat memoryReturns content normally

    13. Scheduled Tasks

    #User StoryStepsExpected
    13.1Create cron task"Remind me to drink water every 5 minutes"Returns Task #{id} scheduled, cron: 0 */5 * * * *
    13.2Create one-time task"Send 'Happy New Year' at 2099-12-31T23:59:00+00:00"Returns Task #{id} scheduled (once)
    13.3Task actually firesCreate per-minute task, wait 60-90sAutomatically receive task execution result
    13.4List tasks"List my scheduled tasks"Shows all active/paused tasks with details
    13.5Pause task"Pause task #1"Confirmed paused; task stops firing
    13.6Resume task"Resume task #1"Confirmed resumed; task resumes firing
    13.7Cancel task"Cancel task #1"Confirmed cancelled; permanently stopped
    13.8View execution history"Show task #1 execution history"Shows recent runs (time, duration, success/fail, summary)
    13.9Nonexistent task"Pause task #9999""Task #9999 not found"
    13.10Invalid cron expressionPass "not a cron"Invalid cron expression error
    13.11Invalid timezoneCreate task with timezone "invalid"Invalid timezone error
    13.12Cross-chat schedule (regular)Regular chat schedules for other chat_idPermission denied
    13.13Cross-chat schedule (control)Control chat schedules for other chat_idCreated successfully
    13.14Empty task listNew chat lists tasks"No scheduled tasks found for this chat."
    13.15One-time task completionCreate imminent one-time taskAfter firing, status becomes "completed"

    14. Todo System

    #User StoryStepsExpected
    14.1Read empty todoNew chat, read todos"No tasks in the todo list."
    14.2Create todo list"Create 3 todo items for me"Bot uses todo_write, returns formatted list
    14.3Update todo status"Mark the first item as completed"Bot reads existing → modifies status → writes back
    14.4Todo full replacementWrite, then write completely different listOld list fully replaced
    14.5Cross-chat read todo (regular)Read another chat_id's todosPermission denied
    14.6Cross-chat read todo (control)Control chat reads other chat_id's todosReturns normally
    14.7Todo status iconsCreate list with pending/in_progress/completedShows [ ], [~], [x] respectively

    15. Chat Export

    #User StoryStepsExpected
    15.1Export current chat"Export this chat's history"Generates markdown file, returns path and message count
    15.2Export to specific pathSpecify path parameterFile written to specified path
    15.3Export empty chatExport chat_id with no messages"No messages found for chat {id}."
    15.4Export format verificationExport, then read the fileEach message: **{sender}** ({timestamp})\n\n{content}\n\n---
    15.5Cross-chat export (regular)Export another chat_idPermission denied
    15.6Cross-chat export (control)Control chat exports other chat_idExported successfully

    16. Sub-Agent

    #User StoryStepsExpected
    16.1Basic sub-agent task"Use sub_agent to count .rs files in this project"Sub-agent uses glob/bash, result returned to main conversation
    16.2Sub-agent cannot schedule"Use sub_agent to create a scheduled task"Sub-agent reports no scheduling tools available
    16.3Sub-agent cannot send_message"Use sub_agent to send me a message"Sub-agent reports no send_message tool
    16.4Sub-agent cannot write memory"Use sub_agent to save a memory"Sub-agent has no write_memory tool
    16.5Sub-agent cannot recurse"Use sub_agent to start another sub_agent"Sub-agent has no sub_agent tool
    16.6Sub-agent iteration limitGive sub-agent a task requiring >10 iterationsReturns "reached maximum iterations"
    16.7Sub-agent with contextPass context parameter to sub-agentSub-agent uses the extra context to complete task

    16A. ACP Stdio Mode

    #User StoryStepsExpected
    16A.1ACP startupRun drugclaw acpDrugClaw starts as an ACP stdio server
    16A.2ACP session continuityUse one ACP client session for two turnsSecond turn keeps prior context
    16A.3ACP cancellationStart a long-running ACP request, then send /stopActive ACP run is cancelled cleanly

    17. Browser Automation

    #User StoryStepsExpected
    17.1Open webpage"Open https://example.com in browser"Returns page content/status
    17.2Get page textAfter opening, "get the page title"Returns "Example Domain"
    17.3Screenshot"Take a screenshot of the current page"Screenshot saved successfully
    17.4Timeout handlingOpen a very slow pageReturns timeout message after 30s
    17.5Browser session persistenceLog into a site, then restart conversationBrowser profile retains cookies/localStorage
    17.6Output truncationGet content of a very large pageOutput truncated to 30000 characters

    18. Skills System

    #User StoryStepsExpected
    18.1/skills listSend /skillsLists all available skills (name + description)
    18.2/skills when emptyDelete skills directory, send /skills"No skills available."
    18.3Activate skillAsk bot to use a specific skillBot uses activate_skill to load full instructions
    18.4Skill auto-discoveryAdd new skill directory + SKILL.md under <data_dir>/skills/After restart, /skills shows new skill

    19. MCP Integration

    #User StoryStepsExpected
    19.1MCP tools loadedConfigure mcp.json, start botLog: "MCP initialized: N tools available"
    19.2MCP tool usageAsk bot to use an MCP-provided toolMCP server called, result returned
    19.3No MCP configStart without mcp.jsonNormal startup, no MCP logs

    20. Multi-Step Tool Use (Agentic Loop)

    #User StoryStepsExpected
    20.1Multi-step task"Find all TODO comments and write to /tmp/todos.txt"Bot uses grep → read_file → write_file across multiple iterations
    20.2Error recoveryRequest reading nonexistent file; bot should adaptBot receives tool error, adjusts strategy
    20.3Iteration limit reachedSend request requiring >max_tool_iterations"I reached the maximum number of tool iterations..."
    20.4Tool composition"Find the longest function and summarize it"Bot combines grep → read_file → analysis

    21. Permission Model Matrix

    #OperationRegular→SelfRegular→OtherControl→Other
    21.1send_messageAllowDenyAllow
    21.2schedule_taskAllowDenyAllow
    21.3pause/resume/cancel_taskAllow (own)DenyAllow
    21.4list_tasksAllow (own)DenyAllow
    21.5get_task_historyAllow (own)DenyAllow
    21.6write_memory (chat)AllowDenyAllow
    21.7write_memory (global)DenyN/AAllow
    21.8read_memory (chat)AllowDenyAllow
    21.9export_chatAllowDenyAllow
    21.10todo_read/writeAllowDenyAllow

    22. Security -- Path Guard

    #Test PathExpected
    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.13Path traversal ../../.ssh/id_rsaStill blocked
    22.14Normal path /tmp/test.txtAllowed

    23. Discord Platform

    #User StoryStepsExpected
    23.1DM direct replySend DM to bot on DiscordBot responds directly
    23.2Server requires @mentionSend message without @mention in server channelNo reply; message stored
    23.3Server @mention@Bot in server channelBot responds
    23.4Response splittingTrigger response >2000 charsSplit at newline boundaries, max 2000 chars per message
    23.5/reset commandSend /resetDiscord chat session cleared
    23.6/skills commandSend /skillsLists available skills
    23.7/archive commandSend /archiveArchives current session
    23.8allowed_channels whitelistConfigure discord_allowed_channelsOnly responds in allowed channels
    23.9Ignore other botsAnother bot sends a messageBot does not respond

    24. WhatsApp Platform

    #User StoryStepsExpected
    24.1Webhook verification (valid)GET /webhook with correct verify_tokenReturns challenge
    24.2Webhook verification (invalid)GET /webhook with wrong verify_tokenRejected
    24.3Text message processingSend text via WhatsAppBot responds
    24.4Non-text messageSend image/audioSilently skipped, no crash
    24.5/reset commandSend /resetSession cleared
    24.6Response splittingTrigger long responseSplit at 4096-char boundaries
    24.7Send failureWhatsApp API unreachableError logged; HTTP 200 still returned

    25. Gateway Service Management

    #User StoryStepsExpected
    25.1Install servicedrugclaw gateway installmacOS: LaunchAgent; Linux: systemd service
    25.2Start servicedrugclaw gateway startService starts
    25.3Check statusdrugclaw gateway statusReports running state
    25.4View logsdrugclaw gateway logs 50Shows last 50 log lines
    25.5Stop servicedrugclaw gateway stopService stops
    25.6Uninstall servicedrugclaw gateway uninstallService removed

    26. Error Handling & Recovery

    #User StoryStepsExpected
    26.1API rate limitingTrigger Anthropic 429 responseExponential backoff retry, up to 3 attempts
    26.2API unavailableAPI completely unreachableReturns "Error: {message}", no crash
    26.3DB corruption recoveryDelete DB file, then send messageRecreates DB or reports error, no crash
    26.4Tool execution exceptionTool internal panic/errorToolResult::error returned to Claude, agentic loop continues
    26.5No messages to processEmpty history"I didn't receive any message to process."
    26.6Role merge: consecutive userDB history has user+user messagesAuto-merged into one user message
    26.7Role fix: trailing assistantHistory ends with assistant messageTrailing 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 StoryStepsExpected
    P.1Private/DM replySend direct messageBot responds
    P.2Group/channel no triggerSend group message without triggerNo reply; message persistence matches policy
    P.3Group/channel triggerMention or trigger bot in group/channelBot responds
    P.4Long response splittingAsk for response > platform limitSplit correctly at boundary with no truncation
    P.5Session resetSend /reset equivalentSession cleared for this platform chat
    P.6Session resumeMulti-turn chat -> restart bot -> continueSession restored from DB
    P.7Attachment ingressSend supported attachmentParsed/handled with expected behavior
    P.8Attachment egressTrigger send_message with attachmentDelivery succeeds or clear error reported
    P.9Channel allowlistConfigure adapter channel allowlistReplies limited to configured channels
    P.10Ignore bot/system messagesSend from another bot/system userBot does not self-trigger or loop
    P.11API failure handlingSimulate platform API/network failureError logged; process stays healthy
    P.12Rate limit handlingTrigger adapter/platform rate limitBackoff/retry policy works, no crash

    Persistence and schema checks

    • messages rows are written with correct chat_id, sender_name, is_from_bot, and timestamp.
    • sessions row key remains stable for the same platform conversation across restarts.
    • chats.chat_type semantics are consistent with trigger policy.
    • Scheduled task replies route correctly back to the originating platform chat.

    Security and authorization checks

    • control_chat_ids policy 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';"
    
  • 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.