ICWY2FLS75I6PRBQYSHNA5HJHGNL3CMWGI66XW7XMKJAVL3LYMJAC VLOAUA5ABETV3BE2RUHYTWWTWMJV2LWGRTQ2VIDNG4QTUKJDVIWAC IPLKCI7O62O3H55DWRXZUANPBNF6P3DKVOCCQRQDOABOIICTSD7QC ILAGTYHND3Y4JI7DKCWYHVTSH67TP72LJV6ZDTOROUTQDIIIAM4AC 4I26WBI2OTXMVOZABBYSFW24WLXDDR424PTM27TGRB7JSSZNYVJAC UQ5YMM34MV2YCANG5H6ZATSFR6RTBJM7MFJU2SY262CIH4DL6O3AC UBB7TTAXVPQQCOVHACKWXSPV2NPARSHREYJB6J3RSEDZZIXPFGOAC CTCIAQGFIKAIP7JGXIBYWNGE7A5QMF6U662OV3KZOT4HKIWOSTZQC MQT74AV47PUYNCX27OMFK6BFN7PP4DX46JAACN2EPRYXUXV7EL3AC YVII7NUI5Z6DPUDQZG2ZVJ6QNQRCEUYX5547IYCMPZI7GJ3GDZQAC QGBCI5OMIHDEXEJIO2BFCRRAQP4445DO2ZVWDU3FMEFUD6WWBKSQC YLW6ZFTIGIC2V6ZUA3I2T7MMPW5BZCOORLB2WSJYNUB3AGWLSEKQC AHAA7UNL2RRXP7KERGVCC42GB564WS5BYSSFEOXW3EKVMB2UVEOAC 5YZRDAU4ABMFJIJEBGDRMLQKNHIOC22OK2ZJ4MOI4NELAGVVK5ZQC UXKPHE3UG2HUJ7SQUCMU7VU2V75634ZJQDH4RJOGX2JHOW2PPBBAC RD44H3Z4QK3LSNHNIS6N4DAEWIW7BNUF3S552JQOFXRRM3VQ5SAAC V2RX2Y7VB7Y3BHX3ALK4GTCMFJATMLCKIAT4U3XCYZZZIUBIIZ5QC MOQV2NY4ETD5D727YAFZNDIGSD6OVGEEDSCS2K5G6G2TOKI5B2HQC MH3526BUZ3JHPCJ2NOL2W7IZHRIM3JHSILRO3FD2TOWLW6S7D7KQC EOAV3J3R7X5GJGKGGBBRIU3GPHRAV6ZOYFPLIY4VSUWCPETWLJUQC 4MYQL5YX476W5NSIAKXGVQ63GSVVYXEMAKSEXMFKZTSRTL5MK4QAC 4VOAN6JG5TRYVGJFWW5I5LCN6HVFTOL7KVJ2RBP44F44EI4ATHBQC PAC4TJKFSSQAOZDUM5O7KJMZADW72H4UMI6C7DYFXKGAWJEY6CDAC 46RUDLGYFOIQKDS7WWE5U6TIMYO3XY6IIIJI55HU3RXYORIVYVXQC CBGXBY2CSVY6SK5KLBIHTWKDVUKXZIB5EXJKULHP75QGOMH6G2WAC TSX4OAMHBTCC3T6IK3HDNQYKI2GTW7PG7XFAGI2HOKIVLXK42G4AC KSUIUTK56M2DPFUEK5BQ6TPJL7546ADTOI3XXAAFHEMDXPYYUFYAC Q2XLHBS67AEUCGEJPDR7HCWTVIYKBDNW7XBK4WN2NK4BHUNCRAOQC PGLPROQVXGPBWEM3MSLMDCQMGFHBXPIA3KLAQ4MZ7UST5C5UTXSAC 53IQQGOH2GVM745ZEKO4S6RPSQG4UZH6J5G7WUKO4ZCP22D3IO6QC YODKS7KTG7ICBTVD5N32GVALERPWMG4JPL4QIR2NTL3PTIFKHEOAC K2QR5DH4B46SHW4YYNROPVXPNQ7PYEUL6ZNILU3CDHJBYJFZLBNAC {flake.modules.nixos.gammastep = {services.geoclue2.enable = true;};flake.modules.hjem.gammastep ={pkgs,lib,isDesktop,...}:
letgammastepBase ={ pkgs, lib, ... }:
xdg.config.files."gammastep/config.ini".source = ini.generate "gammastep-config.ini" settings;
hjem.extraModules = singleton {packages = singleton pkgs.gammastep;xdg.config.files."gammastep/config.ini".source = ini.generate "gammastep-config.ini" settings;};
{flake-file.inputs = {opencode = {url = "github:anomalyco/opencode";inputs.nixpkgs.follows = "os";};claude-code = {url = "github:sadjow/claude-code-nix";inputs.nixpkgs.follows = "os";};};flake.modules.hjem.ai =
letaiBase =
packages = [pkgs.codexpkgs.gemini-cli# pkgs.qwen-codepkgs.python3pkgs.uv# claude-code sandboxing deps.# pkgs.socat# pkgs.bubblewrapclaudeCodePackageopencodePackage];
# TODO: Add claude-code and opencode config files.files.".claude/settings.json" = {generator = toJSON { };value = {cleanupPeriodDays = 1000;alwaysThinkingEnabled = false;includeCoAuthoredBy = false;
# claude-code sandboxing deps.# pkgs.socat# pkgs.bubblewrap
sandbox = {enabled = false;autoAllowBashIfSandboxed = true;excludedCommands = ["git""jj""docker""just"];allowUnsandboxedCommands = true;network.allowUnixSockets = [ "/run/user/1000/docker.sock" ];network.allowLocalBinding = true;};
# TODO: Add claude-code and opencode config files.files.".claude/settings.json" = {generator = toJSON { };value = {cleanupPeriodDays = 1000;alwaysThinkingEnabled = false;includeCoAuthoredBy = false;
ANTHROPIC_DEFAULT_HAIKU_MODEL = "glm-4.5-air";ANTHROPIC_DEFAULT_SONNET_MODEL = "glm-4.7";ANTHROPIC_DEFAULT_OPUS_MODEL = "glm-4.7";
sandbox = {enabled = false;autoAllowBashIfSandboxed = true;excludedCommands = ["git""jj""docker""just"];allowUnsandboxedCommands = true;network.allowUnixSockets = [ "/run/user/1000/docker.sock" ];network.allowLocalBinding = true;};
CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = 1;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = 1;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = 1;DISABLE_NON_ESSENTIAL_MODEL_CALLS = 1;};
env = {ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic";API_TIMEOUT_MS = 3000000;
hooks = {# Unreliable right now: https://github.com/anthropics/claude-code/issues/11947# Stop = [{# hooks = [{# type = "prompt";# prompt = ''You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed.'';# timeout = 30;# }];# }];
ANTHROPIC_DEFAULT_HAIKU_MODEL = "glm-4.5-air";ANTHROPIC_DEFAULT_SONNET_MODEL = "glm-4.7";ANTHROPIC_DEFAULT_OPUS_MODEL = "glm-4.7";
# SubagentStop = [{# hooks = [{# type = "prompt";# prompt = ''Evaluate if this subagent should stop. Input: $ARGUMENTS\n\nCheck if:\n- The subagent completed its assigned task\n- Any errors occurred that need fixing\n- Additional context gathering is needed.'';# }];# }];
CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = 1;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = 1;CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = 1;DISABLE_NON_ESSENTIAL_MODEL_CALLS = 1;};
Notification = [{matcher = "permission_prompt|elicitation_dialog";hooks = [{type = "command";command = "${pkgs.libnotify}/bin/notify-send --expire-time=15000 'Claude' 'Waiting for approval.'";}];}{matcher = "idle_prompt";hooks = [{type = "command";command = "${pkgs.libnotify}/bin/notify-send --expire-time=15000 'Claude' 'Waiting for next message.'";}];}];
hooks = {# Unreliable right now: https://github.com/anthropics/claude-code/issues/11947# Stop = [{# hooks = [{# type = "prompt";# prompt = ''You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nAnalyze the conversation and determine if:\n1. All user-requested tasks are complete\n2. Any errors need to be addressed\n3. Follow-up work is needed.'';# timeout = 30;# }];# }];
PostToolUse = [{matcher = "Edit|MultiEdit|Write";hooks = [{type = "command";command = "~/.claude/hooks/format-files";}];}];};
# SubagentStop = [{# hooks = [{# type = "prompt";# prompt = ''Evaluate if this subagent should stop. Input: $ARGUMENTS\n\nCheck if:\n- The subagent completed its assigned task\n- Any errors occurred that need fixing\n- Additional context gathering is needed.'';# }];# }];
permissions = {allow = ["Edit(PROJECT.md)""Edit(CURRENT.md)""Edit(STATE.md)""Update(PROJECT.md)""Update(CURRENT.md)""Update(STATE.md)"
Notification = [{matcher = "permission_prompt|elicitation_dialog";hooks = [{type = "command";command = "${pkgs.libnotify}/bin/notify-send --expire-time=15000 'Claude' 'Waiting for approval.'";}];}{matcher = "idle_prompt";hooks = [{type = "command";command = "${pkgs.libnotify}/bin/notify-send --expire-time=15000 'Claude' 'Waiting for next message.'";}];}];
deny = ["Read(*.env)""Read(*.envrc)""Bash(git push:*)""Bash(git commit:*)"];};mcpServers = {gh_grep = {type = "http";url = "https://mcp.grep.app";};# No support for reading secrets from files yet. These are added with# `~/.claude/claude-mcps.sh` instead.# context7 = {# type = "http";# url = "https://mcp.context7.com/mcp";# headers = {# # We need this for higher limits but for now it's fine and doesn't stop us using it.# CONTEXT7_API_KEY = "{file:${secrets.context7Key.path}}";# };# };## web-reader = {# type = "http";# url = "https://api.z.ai/api/mcp/web_reader/mcp";# headers = {# Authorization = "Bearer {file:${secrets.zaiKey.path}}";# };# };## web-search-prime = {# type = "http";# url = "https://api.z.ai/api/mcp/web_search_prime/mcp";# headers = {# Authorization = "Bearer {file:${secrets.zaiKey.path}}";# };# };
"Bash(fj issue search:*)""Bash(fj issue view:*)""Bash(fj actions tasks:*)""Bash(fj wiki contents:*)""Bash(fj wiki view:*)""mcp__context7""mcp__gh_grep""mcp__web-reader""mcp__web-search-prime""mcp__nixos"];
mcpServers = {gh_grep = {type = "http";url = "https://mcp.grep.app";};# No support for reading secrets from files yet. These are added with# `~/.claude/claude-mcps.sh` instead.# context7 = {# type = "http";# url = "https://mcp.context7.com/mcp";# headers = {# # We need this for higher limits but for now it's fine and doesn't stop us using it.# CONTEXT7_API_KEY = "{file:${secrets.context7Key.path}}";# };# };## web-reader = {# type = "http";# url = "https://api.z.ai/api/mcp/web_reader/mcp";# headers = {# Authorization = "Bearer {file:${secrets.zaiKey.path}}";# };# };## web-search-prime = {# type = "http";# url = "https://api.z.ai/api/mcp/web_search_prime/mcp";# headers = {# Authorization = "Bearer {file:${secrets.zaiKey.path}}";# };# };
playwriter = {type = "stdio";command = "/run/current-system/sw/bin/npx";args = [ "playwriter@latest" ];
nixos = {type = "stdio";command = "/run/current-system/sw/bin/nix";args = ["run""github:utensils/mcp-nixos""--"];};playwriter = {type = "stdio";command = "/run/current-system/sw/bin/npx";args = [ "playwriter@latest" ];};
keybinds = {app_exit = "ctrl+c";messages_half_page_up = "ctrl+u";messages_half_page_down = "ctrl+d";input_newline = "shift+enter";};lsp = {nixd = {command = [ "nixd" ];extensions = [ ".nix" ];
keybinds = {app_exit = "ctrl+c";messages_half_page_up = "ctrl+u";messages_half_page_down = "ctrl+d";input_newline = "shift+enter";
provider.zai-coding-plan.models = {"glm-4.7".options = {# do_sample = false;stream = true;thinking.type = "enabled";# temperature = 0.3;# max_tokens = 32768;
formatter = {rustfmt = {command = ["cargo""fmt""--""$FILE"];extensions = [ ".rs" ];};
mcp = {context7 = {type = "remote";url = "https://mcp.context7.com/mcp";headers = {CONTEXT7_API_KEY = "{file:${secrets.context7Key.path}}";
provider.zai-coding-plan.models = {"glm-4.7".options = {# do_sample = false;stream = true;thinking.type = "enabled";# temperature = 0.3;# max_tokens = 32768;
gh_grep = {type = "remote";url = "https://mcp.grep.app";};
mcp = {context7 = {type = "remote";url = "https://mcp.context7.com/mcp";headers = {CONTEXT7_API_KEY = "{file:${secrets.context7Key.path}}";};};
web-search-prime = {type = "remote";url = "https://api.z.ai/api/mcp/web_search_prime/mcp";headers = {Authorization = "Bearer {file:${secrets.zaiKey.path}}";
web-reader = {type = "remote";url = "https://api.z.ai/api/mcp/web_reader/mcp";headers = {Authorization = "Bearer {file:${secrets.zaiKey.path}}";};
zread = {type = "remote";url = "https://api.z.ai/api/mcp/zread/mcp";headers = {Authorization = "Bearer {file:${secrets.zaiKey.path}}";
web-search-prime = {type = "remote";url = "https://api.z.ai/api/mcp/web_search_prime/mcp";headers = {Authorization = "Bearer {file:${secrets.zaiKey.path}}";};
# Create hooks with home-manager to avoid permissions issues.files.".claude/hooks/format-files" = {text = # nu''#!/usr/bin/env nulet json_input = (^cat)let file_path = ""
# Create hooks with home-manager to avoid permissions issues.files.".claude/hooks/format-files" = {text = # nu''#!/usr/bin/env nulet json_input = (^cat)let file_path = ""
let extension = ($file_path | path parse | get extension)let command = match ($extension | str trim) {"rs" if (which cargo | is-not-empty) => { ["rustfmt" $file_path] }"toml" if (which taplo | is-not-empty) => { ["taplo" "fmt" $file_path] }_ => {print "This file extension is not covered by this script"exit 0
let extension = ($file_path | path parse | get extension)let command = match ($extension | str trim) {"rs" if (which cargo | is-not-empty) => { ["rustfmt" $file_path] }"toml" if (which taplo | is-not-empty) => { ["taplo" "fmt" $file_path] }_ => {print "This file extension is not covered by this script"exit 0}
# Statusline script.files.".claude/scripts/statusline.py" = {text = # py''#!/usr/bin/env python3"""PlumJam's Claude Code Statusline<https://git.plumj.am/plumjam/nixos/src/branch/master/modules/ai.nix>"""
# Statusline script.files.".claude/scripts/statusline.py" = {text = # py''#!/usr/bin/env python3"""PlumJam's Claude Code Statusline<https://git.plumj.am/plumjam/nixos/src/branch/master/modules/ai.nix>"""
def parse_context_from_transcript(transcript_path):if not transcript_path or not os.path.exists(transcript_path):return None
def parse_context_from_transcript(transcript_path):if not transcript_path or not os.path.exists(transcript_path):return None
# Method 1: Parse usage tokens from assistant messages.if data.get('type') == 'assistant':message = data.get('message', {})usage = message.get('usage', {})
# Method 1: Parse usage tokens from assistant messages.if data.get('type') == 'assistant':message = data.get('message', {})usage = message.get('usage', {})
if usage:input_tokens = usage.get('input_tokens', 0)cache_read = usage.get('cache_read_input_tokens', 0)cache_creation = usage.get('cache_creation_input_tokens', 0)
if usage:input_tokens = usage.get('input_tokens', 0)cache_read = usage.get('cache_read_input_tokens', 0)cache_creation = usage.get('cache_creation_input_tokens', 0)
# Estimate context usage (assume 200k).total_tokens = input_tokens + cache_read + cache_creationif total_tokens > 0:percent_used = min(100, (total_tokens / 200000) * 100)return {'percent': percent_used,'tokens': total_tokens,'method': 'usage'}
# Estimate context usage (assume 200k).total_tokens = input_tokens + cache_read + cache_creationif total_tokens > 0:percent_used = min(100, (total_tokens / 200000) * 100)return {'percent': percent_used,'tokens': total_tokens,'method': 'usage'}
# "Context left until auto-compact: X%"match = re.search(r'Context left until auto-compact: (\d+)%', content)if match:percent_left = int(match.group(1))return {'percent': 100 - percent_left,'warning': 'auto-compact','method': 'system'}
# "Context left until auto-compact: X%"match = re.search(r'Context left until auto-compact: (\d+)%', content)if match:percent_left = int(match.group(1))return {'percent': 100 - percent_left,'warning': 'auto-compact','method': 'system'}
# "Context low (X% remaining)"match = re.search(r'Context low \((\d+)% remaining\)', content)if match:percent_left = int(match.group(1))return {'percent': 100 - percent_left,'warning': 'low','method': 'system'}
# "Context low (X% remaining)"match = re.search(r'Context low \((\d+)% remaining\)', content)if match:percent_left = int(match.group(1))return {'percent': 100 - percent_left,'warning': 'low','method': 'system'}
# Create progress bar.segments = 10filled = int((percent / 100) * segments)bar = "█" * filled + " " * (segments - filled)
# Create progress bar.segments = 10filled = int((percent / 100) * segments)bar = "█" * filled + " " * (segments - filled)
def get_directory_display(workspace_data):current_dir = workspace_data.get('current_dir', "")project_dir = workspace_data.get('project_dir', "")
def get_directory_display(workspace_data):current_dir = workspace_data.get('current_dir', "")project_dir = workspace_data.get('project_dir', "")
if current_dir and project_dir:if current_dir.startswith(project_dir):rel_path = current_dir[len(project_dir):].lstrip('/')return rel_path or os.path.basename(project_dir)else:return os.path.basename(current_dir)elif project_dir:return os.path.basename(project_dir)elif current_dir:return os.path.basename(current_dir)else:return "unknown"
if current_dir and project_dir:if current_dir.startswith(project_dir):rel_path = current_dir[len(project_dir):].lstrip('/')return rel_path or os.path.basename(project_dir)else:return os.path.basename(current_dir)elif project_dir:return os.path.basename(project_dir)elif current_dir:return os.path.basename(current_dir)else:return "unknown"
def get_session_duration(duration_ms):if duration_ms > 0:minutes = duration_ms / 60000if minutes < 1:return f"{duration_ms//1000}s"else:return f"{minutes:.0f}m"
def get_session_duration(duration_ms):if duration_ms > 0:minutes = duration_ms / 60000if minutes < 1:return f"{duration_ms//1000}s"else:return f"{minutes:.0f}m"
def main():try:# Read JSON input from claude-code.data = json.load(sys.stdin)
model_name = data.get('model', {}).get('display_name', 'Claude')workspace = data.get('workspace', {})transcript_path = data.get('transcript_path', "")cost_data = data.get('cost', {})
model_name = data.get('model', {}).get('display_name', 'Claude')workspace = data.get('workspace', {})transcript_path = data.get('transcript_path', "")cost_data = data.get('cost', {})
thinking_enabled = os.environ.get('THINKING_ENABLED', "").lower() in ('true', '1', 'yes')
thinking_indicator = " \033[38;5;172m|\033[0m \033[38;5;142mTHINKING\033[0m " if thinking_enabled else " "
# Combine all components in the correct format.status_line = f"\033[38;5;142m[{model_name}]\033[0m \033[38;5;214m{directory}\033[0m{thinking_indicator}{context_display}"
# Combine all components in the correct format.status_line = f"\033[38;5;142m[{model_name}]\033[0m \033[38;5;214m{directory}\033[0m{thinking_indicator}{context_display}"
if session_duration:status_line += f" \033[38;5;172m|\033[0m \033[38;5;214m{session_duration}\033[0m"
# Helper script to add MCPs.files.".claude/claude-mcps.sh" = {text = # bash''#!/usr/bin/env bash# Run this once to add the MCP servers that need API keysclaude mcp add -s user -t http context7 https://mcp.context7.com/mcp --header "CONTEXT7_API_KEY: $(cat ${secrets.context7Key.path})"claude mcp add -s user -t http web-reader https://api.z.ai/api/mcp/web_reader/mcp --header "Authorization: Bearer $(cat ${secrets.zaiKey.path})"claude mcp add -s user -t http web-search-prime https://api.z.ai/api/mcp/web_search_prime/mcp --header "Authorization: Bearer $(cat ${secrets.zaiKey.path})"claude mcp add -s user -t http zread https://api.z.ai/api/mcp/zread/mcp --header "Authorization: Bearer your_api_key"'';executable = true;};
# Helper script to add MCPs.files.".claude/claude-mcps.sh" = {text = # bash''#!/usr/bin/env bash# Run this once to add the MCP servers that need API keysclaude mcp add -s user -t http context7 https://mcp.context7.com/mcp --header "CONTEXT7_API_KEY: $(cat ${secrets.context7Key.path})"claude mcp add -s user -t http web-reader https://api.z.ai/api/mcp/web_reader/mcp --header "Authorization: Bearer $(cat ${secrets.zaiKey.path})"claude mcp add -s user -t http web-search-prime https://api.z.ai/api/mcp/web_search_prime/mcp --header "Authorization: Bearer $(cat ${secrets.zaiKey.path})"claude mcp add -s user -t http zread https://api.z.ai/api/mcp/zread/mcp --header "Authorization: Bearer your_api_key"'';executable = true;
aiExtra ={ pkgs, lib, ... }:letinherit (lib.lists) singleton;in{hjem.extraModules = singleton {packages = [pkgs.codexpkgs.gemini-clipkgs.qwen-code];