diff --git a/.github/workflows/update-claude-code.yml b/.github/workflows/update-claude-code.yml new file mode 100644 index 00000000..99dbbf2c --- /dev/null +++ b/.github/workflows/update-claude-code.yml @@ -0,0 +1,63 @@ +name: Update Claude Code + +on: + schedule: + # Run daily at 6 AM UTC + - cron: '0 6 * * *' + workflow_dispatch: # Allow manual trigger + +jobs: + update: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + extra_nix_config: | + experimental-features = nix-command flakes + + - name: Update claude-code package + id: update + run: | + # Script exits 0 if already at latest (no changes to commit) + ./nix/claude-code/update.sh + + # Capture version for PR title + VERSION=$(grep 'version = "' nix/claude-code/package.nix | head -1 | sed 's/.*version = "\([^"]*\)".*/\1/') + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Update flake.lock + run: nix flake update + + - name: Verify build + run: nix develop --command claude --version + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore(deps): update claude-code to ${{ steps.update.outputs.version }}" + title: "chore(deps): update claude-code to ${{ steps.update.outputs.version }}" + body: | + Automated update of claude-code package. + + **Changes:** + - claude-code updated to ${{ steps.update.outputs.version }} + - Updated flake.lock + + **Verification:** + - Package builds successfully + - `claude --version` returns expected version + + --- + *This PR was automatically created by the update-claude-code workflow.* + branch: update-claude-code + delete-branch: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 85e8994d..3a4b5cdb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,11 +145,13 @@ The Nix environment provides: | Tool | Description | |------|-------------| | `deepwork` | CLI using your local source code (editable install) | +| `claude` | Claude Code CLI (built from source for version control) | | `pytest` | Test runner with all plugins | | `ruff` | Fast Python linter and formatter | | `mypy` | Static type checker | | `uv` | Python package manager | | `python` | Python 3.11 interpreter | +| `update` | Updates claude-code and flake inputs | #### CI Usage @@ -161,6 +163,25 @@ nix develop --command ruff check src/ nix develop --command mypy src/ ``` +#### Updating Development Dependencies + +The Nix environment includes Claude Code built from source to ensure version control (the nixpkgs version can lag behind npm releases). Use the `update` command to keep dependencies current: + +```bash +# In the dev shell - updates claude-code and flake inputs +update +``` + +To manually update Claude Code: + +```bash +./nix/claude-code/update.sh +``` + +This fetches the latest version from npm, computes the necessary hashes using `prefetch-npm-deps`, and updates `package.nix` automatically. + +A GitHub Action automatically checks for new Claude Code versions daily and creates PRs when updates are available. + ### Option 2: Manual Setup (Without Nix) If you prefer not to use Nix: diff --git a/flake.lock b/flake.lock index 5a9df819..ce228ff8 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1768564909, - "narHash": "sha256-Kell/SpJYVkHWMvnhqJz/8DqQg2b6PguxVWOuadbHCc=", + "lastModified": 1769018530, + "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e4bae1bd10c9c57b2cf517953ab70060a828ee6f", + "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e2367a1f..d2218afb 100644 --- a/flake.nix +++ b/flake.nix @@ -14,6 +14,8 @@ # Allow unfree packages to support the Business Source License 1.1 config.allowUnfree = true; }; + # Local claude-code package for version control (update via nix/claude-code/update.sh) + claude-code = pkgs.callPackage ./nix/claude-code/package.nix { }; # Read version from pyproject.toml to avoid duplication pyproject = builtins.fromTOML (builtins.readFile ./pyproject.toml); deepwork = pkgs.python311Packages.buildPythonPackage { @@ -44,8 +46,8 @@ # System tools jq # For JSON processing - # CLI tools - claude-code # Claude Code CLI + # CLI tools (claude-code is locally built, see nix/claude-code/) + claude-code gh # GitHub CLI ]; @@ -79,6 +81,9 @@ # Set PYTHONPATH for editable install access to src/ export PYTHONPATH="$PWD/src:$PYTHONPATH" + # Add nix/ scripts to PATH (for 'update' command) + export PATH="$PWD/nix:$PATH" + # Only show welcome message in interactive shells if [[ $- == *i* ]]; then echo "" @@ -94,6 +99,7 @@ echo " mypy src/ Type check" echo " claude-code Claude Code CLI" echo " gh GitHub CLI" + echo " update Update claude-code and flake inputs" echo "" fi ''; diff --git a/nix/claude-code/package-lock.json b/nix/claude-code/package-lock.json new file mode 100644 index 00000000..f9766e4c --- /dev/null +++ b/nix/claude-code/package-lock.json @@ -0,0 +1,314 @@ +{ + "name": "@anthropic-ai/claude-code", + "version": "2.1.15", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anthropic-ai/claude-code", + "version": "2.1.15", + "license": "SEE LICENSE IN README.md", + "bin": { + "claude": "cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-darwin-x64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-arm64": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-linuxmusl-arm64": "^0.33.5", + "@img/sharp-linuxmusl-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + } + } +} diff --git a/nix/claude-code/package.nix b/nix/claude-code/package.nix new file mode 100644 index 00000000..053d1204 --- /dev/null +++ b/nix/claude-code/package.nix @@ -0,0 +1,78 @@ +# Claude Code package - locally maintained for version control +# Based on nixpkgs: https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/cl/claude-code +# +# To update: Run ./update.sh from this directory +{ + lib, + stdenv, + buildNpmPackage, + fetchzip, + versionCheckHook, + writableTmpDirAsHomeHook, + bubblewrap, + procps, + socat, +}: +buildNpmPackage (finalAttrs: { + pname = "claude-code"; + version = "2.1.15"; + + src = fetchzip { + url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${finalAttrs.version}.tgz"; + hash = "sha256-3zhjeAwKj1fMLuriX1qpVA8zaCk1oekJ1UmeEdDx4Xg="; + }; + + npmDepsHash = "sha256-K5re0co3Tkz5peXHe/UUlsqAWq4YzSULdY9+xncfL5A="; + + strictDeps = true; + + postPatch = '' + cp ${./package-lock.json} package-lock.json + + # Replace hardcoded `/bin/bash` with `/usr/bin/env bash` for Nix compatibility + # https://github.com/anthropics/claude-code/issues/15195 + substituteInPlace cli.js \ + --replace-warn '#!/bin/bash' '#!/usr/bin/env bash' + ''; + + dontNpmBuild = true; + + env.AUTHORIZED = "1"; + + # `claude-code` tries to auto-update by default, this disables that functionality. + # https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview#environment-variables + # The DEV=true env var causes claude to crash with `TypeError: window.WebSocket is not a constructor` + postInstall = '' + wrapProgram $out/bin/claude \ + --set DISABLE_AUTOUPDATER 1 \ + --unset DEV \ + --prefix PATH : ${ + lib.makeBinPath ( + [ + # claude-code uses [node-tree-kill](https://github.com/pkrumins/node-tree-kill) which requires procps's pgrep(darwin) or ps(linux) + procps + ] + # the following packages are required for the sandbox to work (Linux only) + ++ lib.optionals stdenv.hostPlatform.isLinux [ + bubblewrap + socat + ] + ) + } + ''; + + doInstallCheck = true; + nativeInstallCheckInputs = [ + writableTmpDirAsHomeHook + versionCheckHook + ]; + versionCheckKeepEnvironment = [ "HOME" ]; + + meta = { + description = "Agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster"; + homepage = "https://github.com/anthropics/claude-code"; + downloadPage = "https://www.npmjs.com/package/@anthropic-ai/claude-code"; + license = lib.licenses.unfree; + mainProgram = "claude"; + }; +}) diff --git a/nix/claude-code/update.sh b/nix/claude-code/update.sh new file mode 100755 index 00000000..cfc648bc --- /dev/null +++ b/nix/claude-code/update.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Update claude-code package to latest npm version +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Get versions +OLD_VERSION=$(grep 'version = "' package.nix | head -1 | sed 's/.*version = "\([^"]*\)".*/\1/') +VERSION=$(npm view @anthropic-ai/claude-code version 2>/dev/null) + +if [[ "$VERSION" == "$OLD_VERSION" ]]; then + echo "Already at latest version: $OLD_VERSION" + exit 0 +fi + +echo "Updating claude-code: $OLD_VERSION -> $VERSION" + +# Download tarball +TARBALL_URL="https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${VERSION}.tgz" +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +curl -sL "$TARBALL_URL" -o "$TMPDIR/claude-code.tgz" + +# Extract and compute source hash +mkdir -p "$TMPDIR/src" +tar -xzf "$TMPDIR/claude-code.tgz" -C "$TMPDIR/src" --strip-components=1 +SRC_HASH=$(nix hash path "$TMPDIR/src") + +# Get package-lock.json from tarball +if [[ -f "$TMPDIR/src/package-lock.json" ]]; then + cp "$TMPDIR/src/package-lock.json" package-lock.json +else + echo "Error: No package-lock.json in tarball" + exit 1 +fi + +# Compute npmDepsHash using prefetch-npm-deps +NPM_DEPS_HASH=$(nix shell nixpkgs#prefetch-npm-deps -c prefetch-npm-deps package-lock.json 2>/dev/null) + +# Update package.nix +sed -i "s/version = \"[^\"]*\"/version = \"$VERSION\"/" package.nix +sed -i "s|hash = \"sha256-[^\"]*\"|hash = \"$SRC_HASH\"|" package.nix +sed -i "s|npmDepsHash = \"sha256-[^\"]*\"|npmDepsHash = \"$NPM_DEPS_HASH\"|" package.nix + +echo "Updated to version $VERSION" +echo " Source hash: $SRC_HASH" +echo " Deps hash: $NPM_DEPS_HASH" diff --git a/nix/update b/nix/update new file mode 100755 index 00000000..95057b45 --- /dev/null +++ b/nix/update @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Update all Nix dependencies (claude-code package and flake inputs) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +cd "$REPO_ROOT" + +echo "Updating claude-code package..." +"$SCRIPT_DIR/claude-code/update.sh" + +echo "" +echo "Updating flake inputs..." +nix flake update + +echo "" +echo "Done! Run 'nix develop' to reload the environment."