diff --git a/.env.example b/.env.example index a83181436..cd9b66bb0 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,5 @@ CLIPROXY_MANAGEMENT_PASSWORD=your-management-key-here CLAWDBOT_TELEGRAM_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz # Get your Anthropic API key from: https://console.anthropic.com/ CLAWDBOT_ANTHROPIC_KEY=sk-ant-api03-your-key-here +# Gateway token for remote mode clients (from kyber: cat ~/.config/clawdbot/gateway-token) +CLAWDBOT_GATEWAY_TOKEN=your-gateway-token-here diff --git a/Makefile b/Makefile index 71915e8e9..ec312c07d 100644 --- a/Makefile +++ b/Makefile @@ -250,7 +250,7 @@ nix-build: nix-connect ## Build Nix configuration. elif [ "$(NIX_CONFIG_TYPE)" = "nixosConfigurations" ]; then \ $(NIX_ALLOW_UNFREE) $(NIX_EXEC) build .#nixosConfigurations.runner.config.system.build.toplevel $(NIX_FLAGS) --impure --no-update-lock-file --show-trace; \ elif [ "$(NIX_CONFIG_TYPE)" = "homeConfigurations" ]; then \ - $(NIX_ALLOW_UNFREE) $(NIX_EXEC) build .#$(NIX_CONFIG_TYPE)."$(NIX_USERNAME)@$(NIX_SYSTEM)".activationPackage $(NIX_FLAGS) --impure --no-update-lock-file --show-trace; \ + HOST=$(DETECTED_HOST) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) build .#$(NIX_CONFIG_TYPE)."$(NIX_USERNAME)@$(NIX_SYSTEM)".activationPackage $(NIX_FLAGS) --impure --no-update-lock-file --show-trace; \ else \ echo "Unsupported OS $(OS) for non-CI build"; \ exit 1; \ @@ -264,7 +264,7 @@ nix-build: nix-connect ## Build Nix configuration. elif [ "$(NIX_CONFIG_TYPE)" = "nixosConfigurations" ]; then \ sudo $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) nixpkgs#nixos-rebuild -- build --flake .#$(NIX_SYSTEM) --impure; \ elif [ "$(NIX_CONFIG_TYPE)" = "homeConfigurations" ]; then \ - $(NIX_ALLOW_UNFREE) $(NIX_EXEC) build .#$(NIX_CONFIG_TYPE)."$(NIX_USERNAME)@$(NIX_SYSTEM)".activationPackage $(NIX_FLAGS) --impure --show-trace; \ + HOST=$(DETECTED_HOST) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) build .#$(NIX_CONFIG_TYPE)."$(NIX_USERNAME)@$(NIX_SYSTEM)".activationPackage $(NIX_FLAGS) --impure --show-trace; \ else \ echo "Unsupported OS $(OS) for non-CI build"; \ exit 1; \ @@ -332,22 +332,22 @@ nix-switch: ## Activate Nix configuration. elif [ "$(OS)" = "Darwin" ]; then \ if [ -n "$(HOST)" ]; then \ echo "Switching named host: $(HOST)"; \ - sudo $(NIX_ALLOW_UNFREE) $(DARWIN_REBUILD) switch --flake .#$(HOST) --impure; \ + sudo HOST=$(HOST) $(NIX_ALLOW_UNFREE) $(DARWIN_REBUILD) switch --flake .#$(HOST) --impure; \ elif [ -n "$(DETECTED_HOST)" ]; then \ echo "Auto-detected host: $(DETECTED_HOST)"; \ - sudo $(NIX_ALLOW_UNFREE) $(DARWIN_REBUILD) switch --flake .#$(DETECTED_HOST) --impure; \ + sudo HOST=$(DETECTED_HOST) $(NIX_ALLOW_UNFREE) $(DARWIN_REBUILD) switch --flake .#$(DETECTED_HOST) --impure; \ else \ - sudo $(NIX_ALLOW_UNFREE) $(DARWIN_REBUILD) switch --flake .#$(NIX_SYSTEM) --impure; \ + sudo HOST=$(NIX_SYSTEM) $(NIX_ALLOW_UNFREE) $(DARWIN_REBUILD) switch --flake .#$(NIX_SYSTEM) --impure; \ fi; \ elif [ "$(NIX_CONFIG_TYPE)" = "nixosConfigurations" ]; then \ sudo $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure nixpkgs#nixos-rebuild -- switch --flake .#$(NIX_SYSTEM); \ elif [ "$(NIX_CONFIG_TYPE)" = "homeConfigurations" ]; then \ if [ -n "$(HOST)" ]; then \ echo "Switching named home config: $(HOST)"; \ - USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#homeConfigurations.$(HOST).activationPackage; \ + HOST=$(HOST) USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#homeConfigurations.$(HOST).activationPackage; \ elif [ -n "$(DETECTED_HOST)" ]; then \ echo "Auto-detected host: $(DETECTED_HOST)"; \ - USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#homeConfigurations.$(DETECTED_HOST).activationPackage; \ + HOST=$(DETECTED_HOST) USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#homeConfigurations.$(DETECTED_HOST).activationPackage; \ else \ USER=$(NIX_USERNAME) $(NIX_ALLOW_UNFREE) $(NIX_EXEC) run $(NIX_FLAGS) --impure .#$(NIX_CONFIG_TYPE)."$(NIX_USERNAME)@$(NIX_SYSTEM)".activationPackage; \ fi; \ diff --git a/config/claude/default.nix b/config/claude/default.nix index 55a812ca0..612aaef5a 100644 --- a/config/claude/default.nix +++ b/config/claude/default.nix @@ -28,10 +28,4 @@ executable = true; force = true; }; - - home.file.".claude/install-skills.sh" = { - source = ./install-skills.sh; - executable = true; - force = true; - }; } diff --git a/config/claude/install-skills.sh b/config/claude/install-skills.sh deleted file mode 100755 index de81021be..000000000 --- a/config/claude/install-skills.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -# Install agent skills from external repositories -# Usage: ./install-skills.sh -# Docs: https://agentskills.io - -set -euo pipefail - -echo "Installing agent skills..." - -# Install Vercel agent-skills (React best practices + Web design guidelines) -# https://github.com/vercel-labs/agent-skills -# Targets all supported agents: opencode, claude-code, codex, cursor -npx add-skill vercel-labs/agent-skills \ - --global \ - --agent opencode \ - --agent claude-code \ - --agent codex \ - --agent cursor \ - --skill vercel-react-best-practices \ - --skill web-design-guidelines \ - --yes - -echo "Done! Skills installed to ~/.claude/skills/" diff --git a/config/codex/config.toml b/config/codex/config.toml index 5382b37e4..9fa49ca24 100644 --- a/config/codex/config.toml +++ b/config/codex/config.toml @@ -36,7 +36,3 @@ model_provider = "lmstudio" [profiles.qwen3-coder-30b] model = "qwen/qwen3-coder-30b" model_provider = "lmstudio" - -[profiles.kimi-k2] -model = "kimi-k2" -model_provider = "cliproxyapi" diff --git a/config/default.nix b/config/default.nix index bf2859fd8..a6fec88ca 100644 --- a/config/default.nix +++ b/config/default.nix @@ -11,8 +11,10 @@ ./jj ./k3s ./karabiner + ./llm ./opencode ./pi ./serena ./starship + ./zellij ] diff --git a/config/llm/default.nix b/config/llm/default.nix new file mode 100644 index 000000000..518bf4e6c --- /dev/null +++ b/config/llm/default.nix @@ -0,0 +1,14 @@ +{ config, pkgs, ... }: +{ + home.file."Library/Application Support/io.datasette.llm/extra-openai-models.yaml" = { + enable = pkgs.stdenv.isDarwin; + source = config.lib.file.mkOutOfStoreSymlink ./extra-openai-models.yaml; + force = true; + }; + + home.file.".config/io.datasette.llm/extra-openai-models.yaml" = { + enable = pkgs.stdenv.isLinux; + source = config.lib.file.mkOutOfStoreSymlink ./extra-openai-models.yaml; + force = true; + }; +} diff --git a/config/llm/extra-openai-models.yaml b/config/llm/extra-openai-models.yaml new file mode 100644 index 000000000..6255e696d --- /dev/null +++ b/config/llm/extra-openai-models.yaml @@ -0,0 +1,18 @@ +# Custom OpenAI-compatible models for llm +# Uses cliproxyapi as the backend (http://localhost:8317/v1) +- model_id: cliproxyapi + model_name: claude-sonnet-4-20250514 + api_base: http://localhost:8317/v1 + api_key_name: cliproxyapi +- model_id: claude-sonnet + model_name: claude-sonnet-4-20250514 + api_base: http://localhost:8317/v1 + api_key_name: cliproxyapi +- model_id: claude-opus + model_name: claude-opus-4-20250514 + api_base: http://localhost:8317/v1 + api_key_name: cliproxyapi +- model_id: claude-haiku + model_name: claude-haiku-4-20250514 + api_base: http://localhost:8317/v1 + api_key_name: cliproxyapi diff --git a/config/zellij/config.kdl b/config/zellij/config.kdl new file mode 100644 index 000000000..f583cdaeb --- /dev/null +++ b/config/zellij/config.kdl @@ -0,0 +1,536 @@ +keybinds clear-defaults=true { + locked { + bind "Ctrl g" { SwitchToMode "normal"; } + } + pane { + bind "left" { MoveFocus "left"; } + bind "down" { MoveFocus "down"; } + bind "up" { MoveFocus "up"; } + bind "right" { MoveFocus "right"; } + bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; } + bind "d" { NewPane "down"; SwitchToMode "normal"; } + bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "normal"; } + bind "f" { ToggleFocusFullscreen; SwitchToMode "normal"; } + bind "h" { MoveFocus "left"; } + bind "i" { TogglePanePinned; SwitchToMode "normal"; } + bind "j" { MoveFocus "down"; } + bind "k" { MoveFocus "up"; } + bind "l" { MoveFocus "right"; } + bind "n" { NewPane; SwitchToMode "normal"; } + bind "p" { SwitchFocus; } + bind "Ctrl p" { SwitchToMode "normal"; } + bind "r" { NewPane "right"; SwitchToMode "normal"; } + bind "s" { NewPane "stacked"; SwitchToMode "normal"; } + bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; } + bind "z" { TogglePaneFrames; SwitchToMode "normal"; } + } + tab { + bind "left" { GoToPreviousTab; } + bind "down" { GoToNextTab; } + bind "up" { GoToPreviousTab; } + bind "right" { GoToNextTab; } + bind "Shift Left" { MoveTab "Left"; } + bind "Shift Right" { MoveTab "Right"; } + bind "H" { MoveTab "Left"; } + bind "L" { MoveTab "Right"; } + bind "1" { GoToTab 1; SwitchToMode "normal"; } + bind "2" { GoToTab 2; SwitchToMode "normal"; } + bind "3" { GoToTab 3; SwitchToMode "normal"; } + bind "4" { GoToTab 4; SwitchToMode "normal"; } + bind "5" { GoToTab 5; SwitchToMode "normal"; } + bind "6" { GoToTab 6; SwitchToMode "normal"; } + bind "7" { GoToTab 7; SwitchToMode "normal"; } + bind "8" { GoToTab 8; SwitchToMode "normal"; } + bind "9" { GoToTab 9; SwitchToMode "normal"; } + bind "[" { BreakPaneLeft; SwitchToMode "normal"; } + bind "]" { BreakPaneRight; SwitchToMode "normal"; } + bind "b" { BreakPane; SwitchToMode "normal"; } + bind "h" { GoToPreviousTab; } + bind "j" { GoToNextTab; } + bind "k" { GoToPreviousTab; } + bind "l" { GoToNextTab; } + bind "n" { NewTab; SwitchToMode "normal"; } + bind "r" { SwitchToMode "renametab"; TabNameInput 0; } + bind "s" { ToggleActiveSyncTab; SwitchToMode "normal"; } + bind "Ctrl t" { SwitchToMode "normal"; } + bind "x" { CloseTab; SwitchToMode "normal"; } + bind "tab" { ToggleTab; } + } + resize { + bind "left" { Resize "Increase left"; } + bind "down" { Resize "Increase down"; } + bind "up" { Resize "Increase up"; } + bind "right" { Resize "Increase right"; } + bind "+" { Resize "Increase"; } + bind "-" { Resize "Decrease"; } + bind "=" { Resize "Increase"; } + bind "H" { Resize "Decrease left"; } + bind "J" { Resize "Decrease down"; } + bind "K" { Resize "Decrease up"; } + bind "L" { Resize "Decrease right"; } + bind "h" { Resize "Increase left"; } + bind "j" { Resize "Increase down"; } + bind "k" { Resize "Increase up"; } + bind "l" { Resize "Increase right"; } + bind "Ctrl n" { SwitchToMode "normal"; } + } + move { + bind "left" { MovePane "left"; } + bind "down" { MovePane "down"; } + bind "up" { MovePane "up"; } + bind "right" { MovePane "right"; } + bind "h" { MovePane "left"; } + bind "Ctrl h" { SwitchToMode "normal"; } + bind "j" { MovePane "down"; } + bind "k" { MovePane "up"; } + bind "l" { MovePane "right"; } + bind "n" { MovePane; } + bind "p" { MovePaneBackwards; } + bind "tab" { MovePane; } + } + scroll { + bind "e" { EditScrollback; SwitchToMode "normal"; } + bind "s" { SwitchToMode "entersearch"; SearchInput 0; } + } + search { + bind "c" { SearchToggleOption "CaseSensitivity"; } + bind "n" { Search "down"; } + bind "o" { SearchToggleOption "WholeWord"; } + bind "p" { Search "up"; } + bind "w" { SearchToggleOption "Wrap"; } + } + session { + bind "a" { + LaunchOrFocusPlugin "zellij:about" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + bind "c" { + LaunchOrFocusPlugin "configuration" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + bind "Ctrl o" { SwitchToMode "normal"; } + bind "p" { + LaunchOrFocusPlugin "plugin-manager" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + bind "s" { + LaunchOrFocusPlugin "zellij:share" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + bind "w" { + LaunchOrFocusPlugin "session-manager" { + floating true + move_to_focused_tab true + } + SwitchToMode "normal" + } + } + shared_except "locked" { + bind "Alt left" { MoveFocusOrTab "left"; } + bind "Alt down" { MoveFocus "down"; } + bind "Alt up" { MoveFocus "up"; } + bind "Alt right" { MoveFocusOrTab "right"; } + bind "Alt +" { Resize "Increase"; } + bind "Alt -" { Resize "Decrease"; } + bind "Alt =" { Resize "Increase"; } + bind "Alt [" { PreviousSwapLayout; } + bind "Alt ]" { NextSwapLayout; } + bind "Alt f" { ToggleFloatingPanes; } + bind "Ctrl g" { SwitchToMode "locked"; } + bind "Alt h" { MoveFocusOrTab "left"; } + bind "Alt i" { MoveTab "left"; } + bind "Alt j" { MoveFocus "down"; } + bind "Alt k" { MoveFocus "up"; } + bind "Alt l" { MoveFocusOrTab "right"; } + bind "Alt n" { NewPane; } + bind "Alt o" { MoveTab "right"; } + bind "Alt p" { TogglePaneInGroup; } + bind "Alt Shift p" { ToggleGroupMarking; } + bind "Ctrl q" { Quit; } + } + shared_except "locked" "move" { + bind "Ctrl h" { SwitchToMode "move"; } + } + shared_except "locked" "session" { + bind "Ctrl o" { SwitchToMode "session"; } + } + shared_except "locked" "scroll" "search" "tmux" { + bind "Ctrl b" { SwitchToMode "tmux"; } + } + shared_except "locked" "scroll" "search" { + bind "Ctrl s" { SwitchToMode "scroll"; } + } + shared_except "locked" "tab" { + bind "Ctrl t" { SwitchToMode "tab"; } + } + shared_except "locked" "pane" { + bind "Ctrl p" { SwitchToMode "pane"; } + } + shared_except "locked" "resize" { + bind "Ctrl n" { SwitchToMode "resize"; } + } + shared_except "normal" "locked" "entersearch" { + bind "enter" { SwitchToMode "normal"; } + } + shared_except "normal" "locked" "entersearch" "renametab" "renamepane" { + bind "esc" { SwitchToMode "normal"; } + } + shared_among "pane" "tmux" { + bind "x" { CloseFocus; SwitchToMode "normal"; } + } + shared_among "scroll" "search" { + bind "PageDown" { PageScrollDown; } + bind "PageUp" { PageScrollUp; } + bind "left" { PageScrollUp; } + bind "down" { ScrollDown; } + bind "up" { ScrollUp; } + bind "right" { PageScrollDown; } + bind "Ctrl b" { PageScrollUp; } + bind "Ctrl c" { ScrollToBottom; SwitchToMode "normal"; } + bind "d" { HalfPageScrollDown; } + bind "Ctrl f" { PageScrollDown; } + bind "h" { PageScrollUp; } + bind "j" { ScrollDown; } + bind "k" { ScrollUp; } + bind "l" { PageScrollDown; } + bind "Ctrl s" { SwitchToMode "normal"; } + bind "u" { HalfPageScrollUp; } + } + entersearch { + bind "Ctrl c" { SwitchToMode "scroll"; } + bind "esc" { SwitchToMode "scroll"; } + bind "enter" { SwitchToMode "search"; } + } + renametab { + bind "esc" { UndoRenameTab; SwitchToMode "tab"; } + } + shared_among "renametab" "renamepane" { + bind "Ctrl c" { SwitchToMode "normal"; } + } + renamepane { + bind "esc" { UndoRenamePane; SwitchToMode "pane"; } + } + shared_among "session" "tmux" { + bind "d" { Detach; } + } + tmux { + bind "left" { MoveFocus "left"; SwitchToMode "normal"; } + bind "down" { MoveFocus "down"; SwitchToMode "normal"; } + bind "up" { MoveFocus "up"; SwitchToMode "normal"; } + bind "right" { MoveFocus "right"; SwitchToMode "normal"; } + bind "space" { NextSwapLayout; } + bind "\"" { NewPane "down"; SwitchToMode "normal"; } + bind "%" { NewPane "right"; SwitchToMode "normal"; } + bind "," { SwitchToMode "renametab"; } + bind "[" { SwitchToMode "scroll"; } + bind "Ctrl b" { Write 2; SwitchToMode "normal"; } + bind "c" { NewTab; SwitchToMode "normal"; } + bind "h" { MoveFocus "left"; SwitchToMode "normal"; } + bind "j" { MoveFocus "down"; SwitchToMode "normal"; } + bind "k" { MoveFocus "up"; SwitchToMode "normal"; } + bind "l" { MoveFocus "right"; SwitchToMode "normal"; } + bind "n" { GoToNextTab; SwitchToMode "normal"; } + bind "o" { FocusNextPane; } + bind "p" { GoToPreviousTab; SwitchToMode "normal"; } + bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; } + } +} + +// Plugin aliases - can be used to change the implementation of Zellij +// changing these requires a restart to take effect +plugins { + about location="zellij:about" + compact-bar location="zellij:compact-bar" + configuration location="zellij:configuration" + filepicker location="zellij:strider" { + cwd "/" + } + plugin-manager location="zellij:plugin-manager" + session-manager location="zellij:session-manager" + status-bar location="zellij:status-bar" + strider location="zellij:strider" + tab-bar location="zellij:tab-bar" + welcome-screen location="zellij:session-manager" { + welcome_screen true + } +} + +// Plugins to load in the background when a new session starts +// eg. "file:/path/to/my-plugin.wasm" +// eg. "https://example.com/my-plugin.wasm" +load_plugins { +} +web_client { + font "monospace" +} + +// Use a simplified UI without special fonts (arrow glyphs) +// Options: +// - true +// - false (Default) +// +// simplified_ui true + +// Choose the theme that is specified in the themes section. +// Default: default +// +// theme "dracula" + +// Choose the base input mode of zellij. +// Default: normal +// +// default_mode "locked" + +// Choose the path to the default shell that zellij will use for opening new panes +// Default: $SHELL +// +// default_shell "fish" + +// Choose the path to override cwd that zellij will use for opening new panes +// +// default_cwd "/tmp" + +// The name of the default layout to load on startup +// Default: "default" +// +// default_layout "compact" + +// The folder in which Zellij will look for layouts +// (Requires restart) +// +// layout_dir "/tmp" + +// The folder in which Zellij will look for themes +// (Requires restart) +// +// theme_dir "/tmp" + +// Toggle enabling the mouse mode. +// On certain configurations, or terminals this could +// potentially interfere with copying text. +// Options: +// - true (default) +// - false +// +// mouse_mode false + +// Toggle having pane frames around the panes +// Options: +// - true (default, enabled) +// - false +// +// pane_frames false + +// When attaching to an existing session with other users, +// should the session be mirrored (true) +// or should each user have their own cursor (false) +// (Requires restart) +// Default: false +// +// mirror_session true + +// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP +// eg. when terminal window with an active zellij session is closed +// (Requires restart) +// Options: +// - detach (Default) +// - quit +// +// on_force_close "quit" + +// Configure the scroll back buffer size +// This is the number of lines zellij stores for each pane in the scroll back +// buffer. Excess number of lines are discarded in a FIFO fashion. +// (Requires restart) +// Valid values: positive integers +// Default value: 10000 +// +// scroll_buffer_size 10000 + +// Provide a command to execute when copying text. The text will be piped to +// the stdin of the program to perform the copy. This can be used with +// terminal emulators which do not support the OSC 52 ANSI control sequence +// that will be used by default if this option is not set. +// Examples: +// +// copy_command "xclip -selection clipboard" // x11 +// copy_command "wl-copy" // wayland +// copy_command "pbcopy" // osx +// +// copy_command "pbcopy" + +// Choose the destination for copied text +// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard. +// Does not apply when using copy_command. +// Options: +// - system (default) +// - primary +// +// copy_clipboard "primary" + +// Enable automatic copying (and clearing) of selection when releasing mouse +// Default: true +// +// copy_on_select true + +// Path to the default editor to use to edit pane scrollbuffer +// Default: $EDITOR or $VISUAL +// scrollback_editor "/usr/bin/vim" + +// A fixed name to always give the Zellij session. +// Consider also setting `attach_to_session true,` +// otherwise this will error if such a session exists. +// Default: +// +// session_name "My singleton session" + +// When `session_name` is provided, attaches to that session +// if it is already running or creates it otherwise. +// Default: false +// +// attach_to_session true + +// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible +// Options: +// - true (default) +// - false +// +// auto_layout false + +// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected +// Options: +// - true (default) +// - false +// +// session_serialization false + +// Whether pane viewports are serialized along with the session, default is false +// Options: +// - true +// - false (default) +// +// serialize_pane_viewport false + +// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0 +// defaults to the scrollback size. If this number is higher than the scrollback size, it will +// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true. +// +// scrollback_lines_to_serialize 10000 + +// Enable or disable the rendering of styled and colored underlines (undercurl). +// May need to be disabled for certain unsupported terminals +// (Requires restart) +// Default: true +// +// styled_underlines false + +// How often in seconds sessions are serialized +// +// serialization_interval 10000 + +// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know +// metadata info on this session) +// (Requires restart) +// Default: false +// +// disable_session_metadata false + +// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it) +// (Requires restart) +// Default: true (if the host terminal supports it) +// +// support_kitty_keyboard_protocol false +// Whether to make sure a local web server is running when a new Zellij session starts. +// This web server will allow creating new sessions and attaching to existing ones that have +// opted in to being shared in the browser. +// When enabled, navigate to http://127.0.0.1:8082 +// (Requires restart) +// +// Note: a local web server can still be manually started from within a Zellij session or from the CLI. +// If this is not desired, one can use a version of Zellij compiled without +// `web_server_capability` +// +// Possible values: +// - true +// - false +// Default: false +// +// web_server false +// Whether to allow sessions started in the terminal to be shared through a local web server, assuming one is +// running (see the `web_server` option for more details). +// (Requires restart) +// +// Note: This is an administrative separation and not intended as a security measure. +// +// Possible values: +// - "on" (allow web sharing through the local web server if it +// is online) +// - "off" (do not allow web sharing unless sessions explicitly opt-in to it) +// - "disabled" (do not allow web sharing and do not permit sessions started in the terminal to opt-in to it) +// Default: "off" +// +// web_sharing "off" +// A path to a certificate file to be used when setting up the web client to serve the +// connection over HTTPs +// +// web_server_cert "/path/to/cert.pem" +// A path to a key file to be used when setting up the web client to serve the +// connection over HTTPs +// +// web_server_key "/path/to/key.pem" +/// Whether to enforce https connections to the web server when it is bound to localhost +/// (127.0.0.0/8) +/// +/// Note: https is ALWAYS enforced when bound to non-local interfaces +/// +/// Default: false +// +// enforce_https_for_localhost false + +// Whether to stack panes when resizing beyond a certain size +// Default: true +// +// stacked_resize false + +// Whether to show tips on startup +// Default: true +// +// show_startup_tips false + +// Whether to show release notes on first version run +// Default: true +// +// show_release_notes false + +// Whether to enable mouse hover effects and pane grouping functionality +// default is true +// advanced_mouse_actions false + +// The ip address the web server should listen on when it starts +// Default: "127.0.0.1" +// (Requires restart) +// web_server_ip "127.0.0.1" + +// The port the web server should listen on when it starts +// Default: 8082 +// (Requires restart) +// web_server_port 8082 + +// A command to run (will be wrapped with sh -c and provided the RESURRECT_COMMAND env variable) +// after Zellij attempts to discover a command inside a pane when resurrecting sessions, the STDOUT +// of this command will be used instead of the discovered RESURRECT_COMMAND +// can be useful for removing wrappers around commands +// Note: be sure to escape backslashes and similar characters properly +// post_command_discovery_hook "echo $RESURRECT_COMMAND | sed " diff --git a/config/zellij/default.nix b/config/zellij/default.nix new file mode 100644 index 000000000..59017e843 --- /dev/null +++ b/config/zellij/default.nix @@ -0,0 +1,6 @@ +{ config, ... }: +{ + xdg.configFile."zellij/config.kdl" = { + source = config.lib.file.mkOutOfStoreSymlink ./config.kdl; + }; +} diff --git a/dotagents b/dotagents index 736f14ff9..cd946d903 160000 --- a/dotagents +++ b/dotagents @@ -1 +1 @@ -Subproject commit 736f14ff9f30b9df597545ba650eea9ddbe8296c +Subproject commit cd946d903679e04b3403befc439b9a5be1c2137a diff --git a/flake.lock b/flake.lock index 39ee43f3e..213969612 100644 --- a/flake.lock +++ b/flake.lock @@ -428,11 +428,11 @@ ] }, "locked": { - "lastModified": 1768606334, - "narHash": "sha256-FCO2W3Cxe8j8xRanxqS+mmUa5Dgh9ydbJ55GkW3aLnQ=", + "lastModified": 1768663942, + "narHash": "sha256-2g98+Vq0pfX6Bp8g7+MiRhCgR1JirHkMdsHV6b7h8RQ=", "owner": "clawdbot", "repo": "nix-clawdbot", - "rev": "9962f4fa1ffb674579690dddde2ae4c80b4a683e", + "rev": "8143f1f268383ef8a7044f00dfd1d4602def0727", "type": "github" }, "original": { @@ -559,11 +559,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1768456270, - "narHash": "sha256-NgaL2CCiUR6nsqUIY4yxkzz07iQUlUCany44CFv+OxY=", + "lastModified": 1768569498, + "narHash": "sha256-bB6Nt99Cj8Nu5nIUq0GLmpiErIT5KFshMQJGMZwgqUo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f4606b01b39e09065df37905a2133905246db9ed", + "rev": "be5afa0fcb31f0a96bf9ecba05a516c66fcd8114", "type": "github" }, "original": { @@ -581,11 +581,11 @@ ] }, "locked": { - "lastModified": 1768610160, - "narHash": "sha256-dF3Y5qili6pVDhKMqceu6bR0gsdsJxGhQK3SPo8xFFQ=", + "lastModified": 1768656368, + "narHash": "sha256-5wrh3bNQ7ilC6i0GwuPqAJVt+ps6kfgY7+CsYIjagPg=", "owner": "nix-community", "repo": "NUR", - "rev": "d3c34a70451b918e956ebcb860a79e60d0b7554f", + "rev": "b69095a6f9f176945c16c619ad6117422f5b83bd", "type": "github" }, "original": { diff --git a/home-manager/modules/clawdbot/default.nix b/home-manager/modules/clawdbot/default.nix index aea02b0bd..c7e6ad30c 100644 --- a/home-manager/modules/clawdbot/default.nix +++ b/home-manager/modules/clawdbot/default.nix @@ -6,9 +6,14 @@ }: let env = import ../../../lib/env.nix; + host = import ../../../lib/host.nix; homeDir = config.home.homeDirectory; clawdbotDir = "${homeDir}/.config/clawdbot"; + # Remote gateway URL for non-kyber machines (macOS nodes connect here) + # Using Tailscale MagicDNS for direct connectivity (bridge is TCP, not HTTP) + remoteGatewayUrl = "ws://kyber.tail950b36.ts.net:18789"; + # Script to extract secrets from cliproxyapi auth and .env # Uses pkgs.replaceVars to substitute tool paths at build time extractSecretsScript = pkgs.replaceVars ./extract-secrets.sh { @@ -28,7 +33,8 @@ lib.mkIf (!env.isCI) { # Prevent home-manager from auto-restarting clawdbot during activation # Restart only happens via explicit `make switch` (which runs systemctl-clawdbot) - systemd.user.services.clawdbot-gateway = lib.mkIf pkgs.stdenv.isLinux { + # Only applies on kyber where the gateway runs + systemd.user.services.clawdbot-gateway = lib.mkIf host.isKyber { Unit.X-RestartIfChanged = "false"; }; @@ -39,7 +45,44 @@ lib.mkIf (!env.isCI) { '' ); + # Inject gateway token into clawdbot.json for remote mode (tokenFile not supported upstream) + home.activation.clawdbotRemoteToken = lib.mkIf (lib ? hm && lib.hm ? dag && !host.isKyber) ( + lib.hm.dag.entryAfter [ "clawdbotSecrets" "clawdbotConfigFiles" ] '' + TOKEN_FILE="${clawdbotDir}/gateway-token" + CONFIG_FILE="${homeDir}/.clawdbot/clawdbot.json" + if [ -f "$TOKEN_FILE" ] && [ -f "$CONFIG_FILE" ]; then + TOKEN=$(${pkgs.coreutils}/bin/cat "$TOKEN_FILE" | ${pkgs.coreutils}/bin/tr -d '\n') + # Inject token into gateway.remote.token and remove tokenFile + ${pkgs.jq}/bin/jq --arg token "$TOKEN" \ + '.gateway.remote.token = $token | del(.gateway.remote.tokenFile)' \ + "$CONFIG_FILE" > "$CONFIG_FILE.tmp" && \ + ${pkgs.coreutils}/bin/mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" + echo "Injected gateway token into clawdbot config" + fi + '' + ); + + # Auto-start Clawdbot.app on login (galactica only) + # App is installed to /Applications/Nix Apps/ via nix-darwin + launchd.agents.clawdbot-app = lib.mkIf (pkgs.stdenv.isDarwin && host.isGalactica) { + enable = true; + config = { + Label = "com.clawdbot.app"; + ProgramArguments = [ + "/Applications/Clawdbot.app/Contents/MacOS/Clawdbot" + ]; + RunAtLoad = true; + KeepAlive = false; + StandardOutPath = "/tmp/clawdbot-app.log"; + StandardErrorPath = "/tmp/clawdbot-app.error.log"; + }; + }; + programs.clawdbot = { + # App installed via nix-darwin to /Applications/Nix Apps/ + installApp = false; + appPackage = null; + # First-party plugins (all macOS-only) firstParty = { summarize.enable = pkgs.stdenv.isDarwin; # Link -> clean text -> summary (macOS only) @@ -64,37 +107,63 @@ lib.mkIf (!env.isCI) { instances.default = { enable = true; - # Service configuration (launchd for macOS, systemd for Linux) - launchd.enable = pkgs.stdenv.isDarwin; - systemd.enable = pkgs.stdenv.isLinux; + # Service configuration: + # - Kyber only: systemd runs the gateway daemon + # - All other hosts (macOS + non-kyber Linux): no local gateway + launchd.enable = false; # Remote mode, no local gateway + systemd.enable = host.isKyber; # Only kyber runs the gateway - # Browser configuration (headless on Linux, GUI on macOS) - # Gateway binds to LAN on Linux for k8s ingress access + # Platform-specific config overrides # NOTE: uses configOverrides because upstream nix-clawdbot doesn't merge `config` into output - configOverrides = { - browser = { - enabled = true; - headless = pkgs.stdenv.isLinux; + configOverrides = + # Kyber only: Local gateway mode with browser + bridge for nodes + lib.optionalAttrs host.isKyber { + gateway = { + mode = "local"; + bind = "lan"; + auth = { + tokenFile = "${clawdbotDir}/gateway-token"; + }; + }; + bridge = { + enabled = true; + bind = "lan"; # Allow nodes to connect from LAN/ingress + }; + browser = { + enabled = true; + headless = true; + executablePath = "${pkgs.chromium}/bin/chromium"; + noSandbox = true; # SUID sandbox requires root-owned binary with mode 4755 + }; } - // lib.optionalAttrs pkgs.stdenv.isLinux { - executablePath = "${pkgs.chromium}/bin/chromium"; - noSandbox = true; # SUID sandbox requires root-owned binary with mode 4755 - }; - } - // lib.optionalAttrs pkgs.stdenv.isLinux { - gateway = { - bind = "lan"; + # All other hosts (macOS + non-kyber Linux): Remote mode - connect to kyber gateway + // lib.optionalAttrs (!host.isKyber) { + gateway = { + mode = "remote"; + # Remote gateway config with client identity and auth token + remote = { + url = remoteGatewayUrl; + tokenFile = "${clawdbotDir}/gateway-token"; + client = { + name = host.nodeName; + }; + }; + }; + browser = { + enabled = true; + headless = pkgs.stdenv.isLinux; # Headless on Linux, GUI on macOS + }; }; - }; # Anthropic API provider (reads from ~/.config/clawdbot/anthropic-key) + # Only needed on the gateway (Linux), but harmless to keep on nodes providers.anthropic = { apiKeyFile = "${clawdbotDir}/anthropic-key"; }; - # Telegram provider - Linux only (reads from ~/.config/clawdbot/telegram-token) + # Telegram provider - kyber gateway only (reads from ~/.config/clawdbot/telegram-token) providers.telegram = { - enable = pkgs.stdenv.isLinux; + enable = host.isKyber; botTokenFile = "${clawdbotDir}/telegram-token"; allowFrom = [ 983653361 diff --git a/home-manager/modules/clawdbot/extract-secrets.sh b/home-manager/modules/clawdbot/extract-secrets.sh index 4f56f76d3..b18cb3041 100755 --- a/home-manager/modules/clawdbot/extract-secrets.sh +++ b/home-manager/modules/clawdbot/extract-secrets.sh @@ -9,9 +9,13 @@ DOTFILES_ENV="$HOME/dotfiles/.env" mkdir -p "$CLAWDBOT_DIR" chmod 700 "$CLAWDBOT_DIR" -# Extract Telegram token from .env +# Extract secrets from .env if [[ -f $DOTFILES_ENV ]]; then + # Telegram token (Linux gateway only) @grep@ -E "^CLAWDBOT_TELEGRAM_TOKEN=" "$DOTFILES_ENV" 2>/dev/null | @cut@ -d= -f2- | @tr@ -d '"' >"$CLAWDBOT_DIR/telegram-token" || true + + # Gateway token (for remote mode clients / nodes) + @grep@ -E "^CLAWDBOT_GATEWAY_TOKEN=" "$DOTFILES_ENV" 2>/dev/null | @cut@ -d= -f2- | @tr@ -d '"' >"$CLAWDBOT_DIR/gateway-token" || true fi # Read Claude OAuth access_token from cliproxyapi's synced auth (primary source) diff --git a/home-manager/packages/default.nix b/home-manager/packages/default.nix index bce1432bb..91295585f 100644 --- a/home-manager/packages/default.nix +++ b/home-manager/packages/default.nix @@ -26,10 +26,10 @@ with pkgs; curlie cursor-cli delta - doppler difftastic direnv doggo + doppler duf dust entr @@ -39,19 +39,20 @@ with pkgs; fswatch fzf-make gh - google-cloud-sdk git glance glow gnumake - gping + go-task + google-cloud-sdk goose-cli + gping grc htop httpie hyperfine - jujutsu jq + jujutsu just k9s kind @@ -61,6 +62,7 @@ with pkgs; kubernetes-helm kustomize llama-cpp + llm mariadb mkcert navi diff --git a/home-manager/programs/fish/default.nix b/home-manager/programs/fish/default.nix index 06514e737..8c56a4558 100644 --- a/home-manager/programs/fish/default.nix +++ b/home-manager/programs/fish/default.nix @@ -51,8 +51,6 @@ ''; shellAliases = { neofetch = "fastfetch"; - - cliproxyapi = "cd ~/.cli-proxy-api && /opt/homebrew/bin/cliproxyapi -config config.yaml"; ocd = "bun run ${config.home.homeDirectory}/ghq/github.com/shunkakinoki/open-composer/apps/cli/src/index.ts"; }; shellAbbrs = { @@ -66,6 +64,7 @@ v = "nvim"; # Function-based abbreviations + cliproxyapi = "_cliproxyapi_function"; clxe = "_clxe_function"; clxeh = "_clxeh_function"; coxe = "_coxe_function"; @@ -142,6 +141,7 @@ value.source = ./functions/${name}.fish; }) [ + "_cliproxyapi_function" "_clxe_function" "_clxeh_function" "_coxe_function" diff --git a/home-manager/programs/fish/functions/_cliproxyapi_function.fish b/home-manager/programs/fish/functions/_cliproxyapi_function.fish new file mode 100644 index 000000000..2c9655726 --- /dev/null +++ b/home-manager/programs/fish/functions/_cliproxyapi_function.fish @@ -0,0 +1,7 @@ +function _cliproxyapi_function --description "Start cliproxyapi service" + if test (uname) = Darwin + cd ~/.cli-proxy-api && /opt/homebrew/bin/cliproxyapi -config config.yaml + else + systemctl --user restart cliproxyapi && journalctl --user -u cliproxyapi -f + end +end diff --git a/lib/host.nix b/lib/host.nix new file mode 100644 index 000000000..7df9c9436 --- /dev/null +++ b/lib/host.nix @@ -0,0 +1,17 @@ +{ + # Detect if running on kyber (requires --impure flag, which Makefile already uses) + isKyber = builtins.getEnv "HOSTNAME" == "kyber" || builtins.getEnv "HOST" == "kyber"; + + # Detect if running on galactica (macOS node) + isGalactica = builtins.getEnv "HOSTNAME" == "galactica" || builtins.getEnv "HOST" == "galactica"; + + # Get the node name for clawdbot remote mode + # Falls back to "unknown" if no hostname is detected + nodeName = + if builtins.getEnv "HOSTNAME" == "kyber" || builtins.getEnv "HOST" == "kyber" then + "kyber" + else if builtins.getEnv "HOSTNAME" == "galactica" || builtins.getEnv "HOST" == "galactica" then + "galactica" + else + "unknown"; +} diff --git a/nix-darwin/default.nix b/nix-darwin/default.nix index 0a092ee1a..9ed78773d 100644 --- a/nix-darwin/default.nix +++ b/nix-darwin/default.nix @@ -6,6 +6,8 @@ ... }: let + env = import ../lib/env.nix; + host = import ../lib/host.nix; dock = import ./config/dock.nix; fonts = import ./config/fonts.nix { inherit pkgs; }; homebrew = import ./config/homebrew.nix { inherit isRunner; }; @@ -26,4 +28,11 @@ in system time ]; + + # Clawdbot.app: Symlink to /Applications/ (galactica only) + system.activationScripts.postActivation.text = lib.mkIf (!env.isCI && host.isGalactica) '' + echo "Installing Clawdbot.app to /Applications..." + rm -rf /Applications/Clawdbot.app + cp -R ${pkgs.clawdbot-app}/Applications/Clawdbot.app /Applications/ + ''; } diff --git a/overlays/default.nix b/overlays/default.nix index 8a3477aef..bb71efd26 100644 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -1,8 +1,118 @@ { inputs }: +let + # Override clawdbot source to v2026.1.16-2 + clawdbotSourceOverride = { + owner = "clawdbot"; + repo = "clawdbot"; + rev = "be37b39782e0799ba5b9533561de6d128d50c863"; + hash = "sha256-y1ToqEcfl0yVAJkVld0k5AX5tztiE7yJt/F7Rhg+dAc="; + pnpmDepsHash = "sha256-NPQrkhhvAoIYzR1gopqsErps1K/HkfxmrPXpyMlN0Bc="; + }; + # Override clawdbot-app to v2026.1.16-2 (fixes broken app package) + clawdbotAppOverride = { + version = "2026.1.16-2"; + url = "https://github.com/clawdbot/clawdbot/releases/download/v2026.1.16-2/Clawdbot-2026.1.16-2.zip"; + hash = "sha256-CQDGFA+/2McVxIw7WXtJZgr6LmtWTy0Dks++pjdU4rU="; + }; +in [ inputs.nur.overlays.default inputs.neovim-nightly-overlay.overlays.default - inputs.nix-clawdbot.overlays.default + # Custom clawdbot overlay with version override (replaces inputs.nix-clawdbot.overlays.default) + ( + final: prev: + let + clawdbotVersion = "2026.1.16-2"; + basePkgs = import "${inputs.nix-clawdbot}/nix/packages" { + pkgs = prev; + sourceInfo = clawdbotSourceOverride; + }; + # Override version in clawdbot-gateway (it's hardcoded in the derivation) + # src is already correct from sourceInfo, just need to update version string + clawdbot-gateway = basePkgs.clawdbot-gateway.overrideAttrs (old: { + version = clawdbotVersion; + __intentionallyOverridingVersion = true; + }); + # Build clawdbot-app with fixed version (upstream has broken app package) + clawdbot-app = + if prev.stdenv.isDarwin then + prev.stdenvNoCC.mkDerivation { + pname = "clawdbot-app"; + version = clawdbotAppOverride.version; + src = prev.fetchzip { + url = clawdbotAppOverride.url; + hash = clawdbotAppOverride.hash; + stripRoot = false; + }; + dontUnpack = true; + installPhase = '' + mkdir -p "$out/Applications" + app_path="$(find "$src" -maxdepth 2 -name '*.app' -print -quit)" + if [ -z "$app_path" ]; then + echo "Clawdbot.app not found in $src" >&2 + exit 1 + fi + cp -R "$app_path" "$out/Applications/Clawdbot.app" + ''; + meta = with prev.lib; { + description = "Clawdbot macOS app bundle"; + homepage = "https://github.com/clawdbot/clawdbot"; + license = licenses.mit; + platforms = platforms.darwin; + }; + } + else + null; + # Rebuild batteries bundle with the overridden gateway and app + clawdbot = prev.callPackage "${inputs.nix-clawdbot}/nix/packages/clawdbot-batteries.nix" { + clawdbot-gateway = clawdbot-gateway; + inherit clawdbot-app; + extendedTools = (import "${inputs.nix-clawdbot}/nix/tools/extended.nix" { pkgs = prev; }).tools; + }; + toolNames = (import "${inputs.nix-clawdbot}/nix/tools/extended.nix" { pkgs = prev; }).toolNames; + withTools = + { + toolNamesOverride ? null, + excludeToolNames ? [ ], + }: + let + toolSets = import "${inputs.nix-clawdbot}/nix/tools/extended.nix" { + pkgs = prev; + inherit toolNamesOverride excludeToolNames; + }; + innerPkgs = import "${inputs.nix-clawdbot}/nix/packages" { + pkgs = prev; + sourceInfo = clawdbotSourceOverride; + inherit toolNamesOverride excludeToolNames; + }; + innerGateway = innerPkgs.clawdbot-gateway.overrideAttrs (old: { + version = clawdbotVersion; + __intentionallyOverridingVersion = true; + }); + in + innerPkgs + // { + clawdbot-gateway = innerGateway; + clawdbot = prev.callPackage "${inputs.nix-clawdbot}/nix/packages/clawdbot-batteries.nix" { + clawdbot-gateway = innerGateway; + inherit clawdbot-app; + extendedTools = toolSets.tools; + }; + }; + in + { + inherit clawdbot clawdbot-gateway; + clawdbotPackages = { + inherit + clawdbot + clawdbot-gateway + toolNames + withTools + ; + }; + } + // (if prev.stdenv.isDarwin then { inherit clawdbot-app; } else { }) + ) (final: prev: { # Ensure neovim-unwrapped exposes a lua attribute for wrapper consumers (e.g., home-manager) neovim-unwrapped = diff --git a/spec/coverage_spec.sh b/spec/coverage_spec.sh index 46231b80a..f1991f3c2 100644 --- a/spec/coverage_spec.sh +++ b/spec/coverage_spec.sh @@ -21,10 +21,6 @@ It 'has spec file for config/claude/statusline-git.sh' The path "spec/statusline_git_spec.sh" should be exist End -It 'has spec file for config/claude/install-skills.sh' -The path "spec/install_skills_spec.sh" should be exist -End - It 'has spec file for home-manager/programs/neovim/run_tests.sh' The path "spec/neovim_tests_spec.sh" should be exist End @@ -97,8 +93,7 @@ Describe 'no shell scripts are missing from coverage list' It 'covers all non-spec shell scripts in the repository' # List of all shell scripts that should have tests # Update this list when adding new shell scripts -covered_scripts="config/claude/install-skills.sh -config/claude/notify.sh +covered_scripts="config/claude/notify.sh config/claude/pushover.sh config/claude/security.sh config/claude/statusline-git.sh diff --git a/spec/install_skills_spec.sh b/spec/install_skills_spec.sh deleted file mode 100644 index 0e3b26659..000000000 --- a/spec/install_skills_spec.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2329,SC2016 - -Describe 'install-skills.sh' -SCRIPT="$PWD/config/claude/install-skills.sh" - -Describe 'script structure' -It 'has valid bash syntax' -When run bash -n "$SCRIPT" -The status should be success -End - -It 'uses strict mode' -When run grep -q 'set -euo pipefail' "$SCRIPT" -The status should be success -End - -It 'has executable permission' -When run test -x "$SCRIPT" -The status should be success -End -End - -Describe 'npx add-skill invocation' -setup() { - mock_bin_setup npx -} -cleanup() { - mock_bin_cleanup -} -Before 'setup' -After 'cleanup' - -It 'calls npx add-skill with correct arguments' -When run bash "$SCRIPT" -The status should be success -The output should include 'Installing agent skills' -End - -It 'passes vercel-labs/agent-skills as source' -When run bash -c 'bash '"$SCRIPT"'; cat "$MOCK_LOG"' -The status should be success -The output should include 'vercel-labs/agent-skills' -End - -It 'specifies claude-code as target agent' -When run bash -c 'bash '"$SCRIPT"'; cat "$MOCK_LOG"' -The status should be success -The output should include '--agent claude-code' -End - -It 'includes vercel-react-best-practices skill' -When run bash -c 'bash '"$SCRIPT"'; cat "$MOCK_LOG"' -The status should be success -The output should include 'vercel-react-best-practices' -End - -It 'includes web-design-guidelines skill' -When run bash -c 'bash '"$SCRIPT"'; cat "$MOCK_LOG"' -The status should be success -The output should include 'web-design-guidelines' -End - -It 'uses --yes flag for non-interactive mode' -When run bash -c 'bash '"$SCRIPT"'; cat "$MOCK_LOG"' -The status should be success -The output should include '--yes' -End -End -End