5YZRDAU4ABMFJIJEBGDRMLQKNHIOC22OK2ZJ4MOI4NELAGVVK5ZQC IKGH34S4Q4EK3BV6XE7BFH53DVLY4VQIANNRM7RITZO635GESFLAC RWWWW4A6Q6LX6ZB742GFTTWYQJ2DHPLL2SJKEX5L735JXXSIEPLQC 4MYQL5YX476W5NSIAKXGVQ63GSVVYXEMAKSEXMFKZTSRTL5MK4QAC PGLPROQVXGPBWEM3MSLMDCQMGFHBXPIA3KLAQ4MZ7UST5C5UTXSAC MQT74AV47PUYNCX27OMFK6BFN7PP4DX46JAACN2EPRYXUXV7EL3AC MOQV2NY4ETD5D727YAFZNDIGSD6OVGEEDSCS2K5G6G2TOKI5B2HQC JCNUNSIDM2TPNHVQTSHWJGPETQHG2LVECL2GUIIRUGQIWETYST2AC HWCVAVGHMRTGNMV7WXWM6XB6KMMZQCMZQXWHQV4MPKXZOCAOGXKAC RTBMBSBABSGTRICJ4AWBKWO3JJHBRKV6FGOMYPDD7X6SS6X35ZIQC QOY7D3GZ3WF7HAMEHAW2CIUB5TDQCFE3YDZEO23R5MFQEBN635WAC 4TIHVAKKNUZV5IL5YUIQD7JXQ32PPYUT3IRJUSAI46UM2EKSLPLQC AHAA7UNL2RRXP7KERGVCC42GB564WS5BYSSFEOXW3EKVMB2UVEOAC UH6ZL2HFCGZTK5LBAIWXYWP7F7ZO5ZY3OLHVCY6DOCDSH6ATSILQC GOKVITZZNI4HOXK7WUBCCKWE7C23RVGMWMGMBLESWKWLHPGTLHWAC CBGXBY2CSVY6SK5KLBIHTWKDVUKXZIB5EXJKULHP75QGOMH6G2WAC VH6LF7NBVR35IZV7MQXEZUCFWAGSLRXTTAHJV22D4GXIH3EFLW3QC USORQI4YM2FOILPSRJ444BEV6APPCI76CLGI5ZBDBOGCWAUALEQQC 53IQQGOH2GVM745ZEKO4S6RPSQG4UZH6J5G7WUKO4ZCP22D3IO6QC YFWDBAWXXEYS2Q2JAAL56B3NDNC4AV5KFNM3VPH6OXNKEOX6RDKAC 3ZG2UVJL4R4YYAOVPHQD3PHYAGKUZHIT7NSCHXPIPDPOQPUTDLAAC XDXGBTX3IE22PZJQ5MMRUKBLAFDRQEEIXPIK7GVWQAGG2DRIFIAQC EJD5XE5I2XRXYMQDQZMYPE5NLWDN4LMCVU5YAXCDQACFQ2IAJ2VQC K2QR5DH4B46SHW4YYNROPVXPNQ7PYEUL6ZNILU3CDHJBYJFZLBNAC KOXYNEPMHOWPUOUDAIDAVC2ZPUCLFGN23BM6QJ4UIDGVN73SUO7AC KSUIUTK56M2DPFUEK5BQ6TPJL7546ADTOI3XXAAFHEMDXPYYUFYAC MH3526BUZ3JHPCJ2NOL2W7IZHRIM3JHSILRO3FD2TOWLW6S7D7KQC 46RUDLGYFOIQKDS7WWE5U6TIMYO3XY6IIIJI55HU3RXYORIVYVXQC 4VOAN6JG5TRYVGJFWW5I5LCN6HVFTOL7KVJ2RBP44F44EI4ATHBQC PAC4TJKFSSQAOZDUM5O7KJMZADW72H4UMI6C7DYFXKGAWJEY6CDAC Q2XLHBS67AEUCGEJPDR7HCWTVIYKBDNW7XBK4WN2NK4BHUNCRAOQC YODKS7KTG7ICBTVD5N32GVALERPWMG4JPL4QIR2NTL3PTIFKHEOAC {config.flake.modules.homeModules.bat ={ lib, pkgs, config, inputs, secrets, ... }:letinherit (lib) mkIf;claudeCodePackage = pkgs.symlinkJoin {name = "claude-code-wrapped";paths = [ inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default ];buildInputs = [ pkgs.makeWrapper ];postBuild = # sh''wrapProgram $out/bin/claude \--run 'export ANTHROPIC_AUTH_TOKEN=$(cat ${secrets.z-ai-key.path})''';};packages = mkIf config.isDesktop [pkgs.codexpkgs.gemini-cli# pkgs.qwen-codepkgs.python3pkgs.uv# claude-code sandboxing deps.pkgs.socatpkgs.bubblewrapclaudeCodePackage];in{inherit packages;# TODO: Add claude-code and opencode config files.programs.nushell.aliases = {claude = "claude --continue --fork-session";codex = "codex resume --ask-for-approval untrusted";oc = "opencode --continue";};# 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 = ""if (which jq | is-empty) { exit 2 }let $file_path = ($json_input | from json | get tool_input.file_path)if ($file_path | is-empty) { exit 2 }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}}^$command.0 ...($command | skip 1)'';executable = true;};# 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/common/ai.nix>"""import jsonimport sysimport osimport reimport timedef parse_context_from_transcript(transcript_path):if not transcript_path or not os.path.exists(transcript_path):return Nonetry:with open(transcript_path, 'r', encoding='utf-8', errors='replace') as f:lines = f.readlines()# Check last 15 lines for context information.recent_lines = lines[-15:] if len(lines) > 15 else linesfor line in reversed(recent_lines):try:data = json.loads(line.strip())# 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)# 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'}# Method 2: Parse system context warnings.elif data.get('type') == 'system_message':content = data.get('content', "")# "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'}except (json.JSONDecodeError, KeyError, ValueError):continuereturn Noneexcept (FileNotFoundError, PermissionError):return Nonedef get_context_display(context_info):if not context_info:return f"\033[38;5;109m━┫ ┣━ 0%\033[0m"percent = context_info.get('percent', 0)warning = context_info.get('warning')# Create progress bar.segments = 10filled = int((percent / 100) * segments)bar = "█" * filled + " " * (segments - filled)return f"\033[38;5;109m━┫ [{bar}] ┣━{percent:.0f}%\033[0m"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"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"return ""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', {})thinking_enabled = os.environ.get('THINKING_ENABLED', "").lower() in ('true', '1', 'yes')context_info = parse_context_from_transcript(transcript_path)context_display = get_context_display(context_info)directory = get_directory_display(workspace)duration_ms = cost_data.get('total_duration_ms', 0)session_duration = get_session_duration(duration_ms)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}"if session_duration:status_line += f" \033[38;5;172m|\033[0m \033[38;5;214m{session_duration}\033[0m"print(status_line)except Exception as e:# Fallback display on any error.print("error")if __name__ == "__main__":main()'';};# 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.z-ai-key.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.z-ai-key.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;};};}
{ self, pkgs, lib, config, inputs, ... }: letinherit (lib) enabled mkIf;in {unfree.allowedNames = mkIf config.isDesktop [ "claude-code" "codex" ];environment.shellAliases = mkIf config.isDesktop {claude = "claude --continue --fork-session";codex = "codex resume --ask-for-approval untrusted";oc = "opencode --continue";};environment.systemPackages = mkIf config.isDesktop [pkgs.codexpkgs.gemini-cli# pkgs.qwen-codepkgs.python3pkgs.uv# claude-code sandboxing deps.pkgs.socatpkgs.bubblewrap];age.secrets.z-ai-key = {rekeyFile = self + /secrets/z-ai-key.age;owner = "jam";mode = "0400";};age.secrets.context7Key = {rekeyFile = self + /secrets/context7-key.age;owner = "jam";mode = "0400";};
{config.flake.modules.hjem.ai ={lib,pkgs,inputs,secrets,isDesktop,...}:letinherit (lib) mkIf;
home-manager.sharedModules = [{programs.claude-code = mkIf config.isDesktop (enabled {package = pkgs.symlinkJoin {name = "claude-code-wrapped";paths = [ inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default ];
claudeCodePackage = pkgs.symlinkJoin {name = "claude-code-wrapped";paths = [ inputs.claude-code.packages.${pkgs.stdenv.hostPlatform.system}.default ];
postBuild = ''wrapProgram $out/bin/claude \--run 'export ANTHROPIC_AUTH_TOKEN=$(cat ${config.age.secrets.z-ai-key.path})''';
postBuild = # sh''wrapProgram $out/bin/claude \--run 'export ANTHROPIC_AUTH_TOKEN=$(cat ${secrets.z-ai-key.path})''';
statusLine = {type = "command";command = "python3 ~/.claude/scripts/statusline.py";};
packages = mkIf isDesktop [pkgs.codexpkgs.gemini-cli# pkgs.qwen-codepkgs.python3pkgs.uv
sandbox = {enabled = true;autoAllowBashIfSandboxed = true;excludedCommands = [ "git" "jj" "docker" "just" ];allowUnsandboxedCommands = true;network.allowUnixSockets = [ "/run/user/1000/docker.sock" ];network.allowLocalBinding = true;};
# claude-code sandboxing deps.pkgs.socatpkgs.bubblewrap
ANTHROPIC_DEFAULT_HAIKU_MODEL = "glm-4.5-air";ANTHROPIC_DEFAULT_SONNET_MODEL = "glm-4.7";ANTHROPIC_DEFAULT_OPUS_MODEL = "glm-4.7";
# TODO: Add claude-code and opencode config files.files.".claude/settings.json" = {generator = lib.generators.toJSON { };value = {cleanupPeriodDays = 1000;alwaysThinkingEnabled = false;includeCoAuthoredBy = false;
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;# }];# }];
sandbox = {enabled = true;autoAllowBashIfSandboxed = true;excludedCommands = ["git""jj""docker""just"];allowUnsandboxedCommands = true;network.allowUnixSockets = [ "/run/user/1000/docker.sock" ];network.allowLocalBinding = true;};
# 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.'';# }];# }];
env = {ANTHROPIC_BASE_URL = "https://api.z.ai/api/anthropic";API_TIMEOUT_MS = "3000000";
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.'";}];}];
ANTHROPIC_DEFAULT_HAIKU_MODEL = "glm-4.5-air";ANTHROPIC_DEFAULT_SONNET_MODEL = "glm-4.7";ANTHROPIC_DEFAULT_OPUS_MODEL = "glm-4.7";
PostToolUse = [{matcher = "Edit|MultiEdit|Write";hooks = [{type = "command";command = "~/.claude/hooks/format-files";}];}];};
CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR = "1";CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";DISABLE_NON_ESSENTIAL_MODEL_CALLS = "1";};
permissions = {allow = ["Edit(PROJECT.md)""Edit(CURRENT.md)""Edit(STATE.md)""Update(PROJECT.md)""Update(CURRENT.md)""Update(STATE.md)"
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;# }];# }];
"Bash(curl http://localhost:*)""Bash(curl -X GET http://localhost:*)"
# 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.'';# }];# }];
"Bash(find:*)""Bash(rg:*)"
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.'";}];}];
"Bash(fj issue search:*)""Bash(fj issue view:*)""Bash(fj actions tasks:*)""Bash(fj wiki contents:*)""Bash(fj wiki view:*)"
PostToolUse = [{matcher = "Edit|MultiEdit|Write";hooks = [{type = "command";command = "~/.claude/hooks/format-files";}];}];};
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:${config.age.secrets.context7Key.path}}";# };# };## web-reader = {# type = "http";# url = "https://api.z.ai/api/mcp/web_reader/mcp";# headers = {# Authorization = "Bearer {file:${config.age.secrets.z-ai-key.path}}";# };# };## web-search-prime = {# type = "http";# url = "https://api.z.ai/api/mcp/web_search_prime/mcp";# headers = {# Authorization = "Bearer {file:${config.age.secrets.z-ai-key.path}}";# };# };
"Bash(fj issue search:*)""Bash(fj issue view:*)""Bash(fj actions tasks:*)""Bash(fj wiki contents:*)""Bash(fj wiki view:*)"
playwriter = {type = "stdio";command = "/run/current-system/sw/bin/npx";args = [ "playwriter@latest" ];};
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.z-ai-key.path}}";# };# };## web-search-prime = {# type = "http";# url = "https://api.z.ai/api/mcp/web_search_prime/mcp";# headers = {# Authorization = "Bearer {file:${secrets.z-ai-key.path}}";# };# };
# TODO: Add nixpkgs#mcp-grafana?
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" ];};};};
programs.opencode = enabled {# We don't need latest for CI runners.package = mkIf config.isDesktop inputs.opencode.packages.${pkgs.stdenv.hostPlatform.system}.default;
files."opencode/opencode.jsonc" = {generator = lib.generators.toJSON { };value = {
settings = {theme = "gruvbox";autoupdate = false;model = "zai-coding-plan/glm-4.7";
theme = "gruvbox";autoupdate = false;model = "zai-coding-plan/glm-4.7";
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:${config.age.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;
web-reader = {type = "remote";url = "https://api.z.ai/api/mcp/web_reader/mcp";headers = {Authorization = "Bearer {file:${config.age.secrets.z-ai-key.path}}";
gh_grep = {type = "remote";url = "https://mcp.grep.app";
web-search-prime = {type = "remote";url = "https://api.z.ai/api/mcp/web_search_prime/mcp";headers = {Authorization = "Bearer {file:${config.age.secrets.z-ai-key.path}}";
web-reader = {type = "remote";url = "https://api.z.ai/api/mcp/web_reader/mcp";headers = {Authorization = "Bearer {file:${secrets.z-ai-key.path}}";};
zread = {type = "remote";url = "https://api.z.ai/api/mcp/zread/mcp";headers = {Authorization = "Bearer {file:${config.age.secrets.z-ai-key.path}}";
web-search-prime = {type = "remote";url = "https://api.z.ai/api/mcp/web_search_prime/mcp";headers = {Authorization = "Bearer {file:${secrets.z-ai-key.path}}";};
nixos = {type = "local";command = [ "/run/current-system/sw/bin/nix" "run" "github:utensils/mcp-nixos" "--" ];};
zread = {type = "remote";url = "https://api.z.ai/api/mcp/zread/mcp";headers = {Authorization = "Bearer {file:${secrets.z-ai-key.path}}";};};
playwriter = {type = "local";command = [ "/run/current-system/sw/bin/npx" "playwriter@latest" ];};
nixos = {type = "local";command = ["/run/current-system/sw/bin/nix""run""github:utensils/mcp-nixos""--"];};
};# Create hooks with home-manager to avoid permissions issues.home.file.".claude/hooks/format-files" = {text = /* nu */ ''#!/usr/bin/env nulet json_input = (^cat)let file_path = ""
let $file_path = ($json_input | from json | get tool_input.file_path)if ($file_path | is-empty) { exit 2 }
# 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}}^$command.0 ...($command | skip 1)'';executable = true;};
if (which jq | is-empty) { exit 2 }
# Statusline script.home.file.".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/common/ai.nix>"""
let $file_path = ($json_input | from json | get tool_input.file_path)if ($file_path | is-empty) { exit 2 }
import jsonimport sysimport osimport reimport time
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}}^$command.0 ...($command | skip 1)'';executable = true;};
def parse_context_from_transcript(transcript_path):if not transcript_path or not os.path.exists(transcript_path):return None
# 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>"""
# Method 1: Parse usage tokens from assistant messages.if data.get('type') == 'assistant':message = data.get('message', {})usage = message.get('usage', {})
# Check last 15 lines for context information.recent_lines = lines[-15:] if len(lines) > 15 else lines
# 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'}
# Method 1: Parse usage tokens from assistant messages.if data.get('type') == 'assistant':message = data.get('message', {})usage = message.get('usage', {})
# Method 2: Parse system context warnings.elif data.get('type') == 'system_message':content = data.get('content', "")
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)
# "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'}
# 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 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'}
# Method 2: Parse system context warnings.elif data.get('type') == 'system_message':content = data.get('content', "")
except (json.JSONDecodeError, KeyError, ValueError):continue
# "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'}
def get_directory_display(workspace_data):current_dir = workspace_data.get('current_dir', "")project_dir = workspace_data.get('project_dir', "")
# Create progress bar.segments = 10filled = int((percent / 100) * segments)bar = "█" * filled + " " * (segments - filled)
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"
return f"\033[38;5;109m━┫ [{bar}] ┣━{percent:.0f}%\033[0m"
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_directory_display(workspace_data):current_dir = workspace_data.get('current_dir', "")project_dir = workspace_data.get('project_dir', "")
return ""
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"
# 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}"
duration_ms = cost_data.get('total_duration_ms', 0)session_duration = get_session_duration(duration_ms)
# Helper script to add MCPs.home.file.".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 ${config.age.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 ${config.age.secrets.z-ai-key.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 ${config.age.secrets.z-ai-key.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.z-ai-key.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.z-ai-key.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;};};