commit 2478108d10a26777bdc5c307d4bf42f6003d3f23
Author: Damian Myrda <damian@prime8.dev>
Date: Tue, 6 Jan 2026 09:24:21 -0600
Initial commit: macOS dotfiles and setup scripts
Diffstat:
27 files changed, 1745 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+nvim/lazy-lock.json
diff --git a/Brewfile b/Brewfile
@@ -0,0 +1,76 @@
+# Taps
+tap "withgraphite/tap"
+tap "mongodb/brew"
+
+# CLI Tools
+brew "bat"
+brew "btop"
+brew "eza"
+brew "fish"
+brew "git"
+brew "gnupg"
+brew "neovim"
+brew "neovim-remote"
+brew "pfetch"
+brew "ripgrep"
+brew "trash-cli"
+brew "tree"
+brew "zoxide"
+## install 1password-cli manually
+
+# Development
+brew "fnm"
+brew "gh"
+brew "go"
+brew "node"
+brew "openjdk"
+brew "opencode"
+brew "python"
+brew "yarn"
+
+# Work (Mandolin)
+brew "doppler"
+brew "mongodb-community"
+brew "redis"
+brew "temporal"
+brew "withgraphite/tap/graphite"
+cask "microsoft-office"
+cask "mongodb-compass"
+
+# Apps - Communication
+cask "beeper"
+cask "microsoft-teams"
+cask "signal"
+cask "slack"
+
+# Apps - Productivity
+cask "chatgpt"
+cask "grammarly-desktop"
+cask "libreoffice"
+cask "notion"
+
+# Apps - Utilities
+cask "1password"
+cask "balenaetcher"
+cask "iina"
+cask "postman"
+cask "soduto"
+cask "spotify"
+cask "tailscale-app"
+cask "thinkorswim"
+cask "wakatime"
+
+# Desktop Environment
+cask "alt-tab"
+cask "ghostty"
+cask "jordanbaird-ice"
+cask "neovide-app"
+cask "orion"
+cask "raycast"
+
+# Input
+cask "colemak-dh"
+cask "scroll-reverser"
+
+# Fonts
+cask "font-jetbrains-mono-nerd-font"
diff --git a/btop/btop.conf b/btop/btop.conf
@@ -0,0 +1,224 @@
+#? Config file for btop v. 1.4.5
+
+#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
+#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
+color_theme = "Default"
+
+#* If the theme set background should be shown, set to False if you want terminal background transparency.
+theme_background = True
+
+#* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false.
+truecolor = True
+
+#* Set to true to force tty mode regardless if a real tty has been detected or not.
+#* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols.
+force_tty = False
+
+#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.
+#* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box.
+#* Use whitespace " " as separator between different presets.
+#* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty"
+presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"
+
+#* Set to True to enable "h,j,k,l,g,G" keys for directional control in lists.
+#* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift.
+vim_keys = False
+
+#* Rounded corners on boxes, is ignored if TTY mode is ON.
+rounded_corners = True
+
+#* Default symbols to use for graph creation, "braille", "block" or "tty".
+#* "braille" offers the highest resolution but might not be included in all fonts.
+#* "block" has half the resolution of braille but uses more common characters.
+#* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY.
+#* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view.
+graph_symbol = "braille"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_cpu = "default"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_mem = "default"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_net = "default"
+
+# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
+graph_symbol_proc = "default"
+
+#* Manually set which boxes to show. Available values are "cpu mem net proc" and "gpu0" through "gpu5", separate values with whitespace.
+shown_boxes = "cpu mem net proc"
+
+#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs.
+update_ms = 100
+
+#* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu direct",
+#* "cpu lazy" sorts top process over time (easier to follow), "cpu direct" updates top process directly.
+proc_sorting = "cpu lazy"
+
+#* Reverse sorting order, True or False.
+proc_reversed = False
+
+#* Show processes as a tree.
+proc_tree = False
+
+#* Use the cpu graph colors in the process list.
+proc_colors = True
+
+#* Use a darkening gradient in the process list.
+proc_gradient = True
+
+#* If process cpu usage should be of the core it's running on or usage of the total available cpu power.
+proc_per_core = False
+
+#* Show process memory as bytes instead of percent.
+proc_mem_bytes = True
+
+#* Show cpu graph for each process.
+proc_cpu_graphs = True
+
+#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)
+proc_info_smaps = False
+
+#* Show proc box on left side of screen instead of right.
+proc_left = False
+
+#* (Linux) Filter processes tied to the Linux kernel(similar behavior to htop).
+proc_filter_kernel = False
+
+#* In tree-view, always accumulate child process resources in the parent process.
+proc_aggregate = False
+
+#* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available.
+#* Select from a list of detected attributes from the options menu.
+cpu_graph_upper = "Auto"
+
+#* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available.
+#* Select from a list of detected attributes from the options menu.
+cpu_graph_lower = "Auto"
+
+#* Toggles if the lower CPU graph should be inverted.
+cpu_invert_lower = True
+
+#* Set to True to completely disable the lower CPU graph.
+cpu_single_graph = False
+
+#* Show cpu box at bottom of screen instead of top.
+cpu_bottom = False
+
+#* Shows the system uptime in the CPU box.
+show_uptime = True
+
+#* Shows the CPU package current power consumption in watts. Requires running `make setcap` or `make setuid` or running with sudo.
+show_cpu_watts = True
+
+#* Show cpu temperature.
+check_temp = True
+
+#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors.
+cpu_sensor = "Auto"
+
+#* Show temperatures for cpu cores also if check_temp is True and sensors has been found.
+show_coretemp = True
+
+#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.
+#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.
+#* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.
+#* Example: "4:0 5:1 6:3"
+cpu_core_map = ""
+
+#* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine".
+temp_scale = "celsius"
+
+#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024.
+base_10_sizes = False
+
+#* Show CPU frequency.
+show_cpu_freq = True
+
+#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
+#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
+clock_format = "%X"
+
+#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort.
+background_update = True
+
+#* Custom cpu model name, empty string to disable.
+custom_cpu_name = ""
+
+#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace " ".
+#* Only disks matching the filter will be shown. Prepend exclude= to only show disks not matching the filter. Examples: disk_filter="/boot /home/user", disks_filter="exclude=/boot /home/user"
+disks_filter = ""
+
+#* Show graphs instead of meters for memory values.
+mem_graphs = True
+
+#* Show mem box below net box instead of above.
+mem_below_net = False
+
+#* Count ZFS ARC in cached and available memory.
+zfs_arc_cached = True
+
+#* If swap memory should be shown in memory box.
+show_swap = True
+
+#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk.
+swap_disk = True
+
+#* If mem box should be split to also show disks info.
+show_disks = False
+
+#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar.
+only_physical = True
+
+#* Read disks list from /etc/fstab. This also disables only_physical.
+use_fstab = True
+
+#* Setting this to True will hide all datasets, and only show ZFS pools. (IO stats will be calculated per-pool)
+zfs_hide_datasets = False
+
+#* Set to true to show available disk space for privileged users.
+disk_free_priv = False
+
+#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view.
+show_io_stat = True
+
+#* Toggles io mode for disks, showing big graphs for disk read/write speeds.
+io_mode = True
+
+#* Set to True to show combined read/write io graphs in io mode.
+io_graph_combined = False
+
+#* Set the top speed for the io graphs in MiB/s (100 by default), use format "mountpoint:speed" separate disks with whitespace " ".
+#* Example: "/mnt/media:100 /:20 /boot:1".
+io_graph_speeds = ""
+
+#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False.
+net_download = 100
+
+net_upload = 100
+
+#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest.
+net_auto = True
+
+#* Sync the auto scaling for download and upload to whichever currently has the highest scale.
+net_sync = True
+
+#* Starts with the Network Interface specified here.
+net_iface = ""
+
+#* "True" shows bitrates in base 10 (Kbps, Mbps). "False" shows bitrates in binary sizes (Kibps, Mibps, etc.). "Auto" uses base_10_sizes.
+base_10_bitrate = "Auto"
+
+#* Show battery stats in top right if battery is present.
+show_battery = True
+
+#* Which battery to use if multiple are present. "Auto" for auto detection.
+selected_battery = "Auto"
+
+#* Show power stats of battery next to charge indicator.
+show_battery_watts = True
+
+#* Set loglevel for "~/.config/btop/btop.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG".
+#* The level set includes all lower levels, i.e. "DEBUG" will show all logging info.
+log_level = "WARNING"
diff --git a/fish/config.fish b/fish/config.fish
@@ -0,0 +1,65 @@
+# defaults
+set -g EDITOR nvim
+set -g BROWSER "open -a orion"
+
+# path
+set -gx PATH "$HOME/.local/bin" $PATH
+
+# secrets
+source ~/.config/fish/secrets.fish
+
+if status is-interactive
+ # fish
+ set -U fish_greeting
+ fish_config theme choose "Base16 Eighties"
+
+ # tide prompt character
+ set -U tide_character_icon "→"
+ set -U tide_character_vi_icon_default "→"
+
+ # system
+ alias fetch pfetch
+ alias top btop
+
+ # terminal
+ alias x exit
+ alias clr clear
+
+ # files
+ zoxide init fish | source
+ function zl
+ z $argv && eza
+ if set -q NVIM
+ command nvr --servername $NVIM -c "cd "(pwd)
+ end
+ end
+ alias cd zl
+ alias ls eza
+ alias la "eza -lA --git"
+ alias ll "eza -l --git"
+ alias cp "cp -rv"
+ alias mv "mv -v"
+ alias rm trash
+ alias cat bat
+
+ # programs
+ alias ed $EDITOR
+ alias web $BROWSER
+
+ # git
+ alias dash "gh dash"
+ alias repo "gh repo"
+ alias issue "gh issue"
+ alias branch "git branch"
+ alias checkout "git checkout"
+ alias commit "git commit"
+ alias pr "gh pr"
+ alias release "gh release"
+
+ # nx
+ set -gx NX_TUI false
+ set -gx NX_PARALLEL "100%"
+
+ # fnm
+ fnm env --use-on-cd --shell fish | source
+end
diff --git a/fish/fish_plugins b/fish/fish_plugins
@@ -0,0 +1,3 @@
+jorgebucaran/fisher
+ilancosman/tide@v6
+ik11235/wakatime.fish
diff --git a/gh/config.yml b/gh/config.yml
@@ -0,0 +1,27 @@
+# The current version of the config schema
+version: 1
+# What protocol to use when performing git operations. Supported values: ssh, https
+git_protocol: https
+# What editor gh should run when creating issues, pull requests, etc. If blank, will refer to environment.
+editor:
+# When to interactively prompt. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
+prompt: enabled
+# Preference for editor-based interactive prompting. This is a global config that cannot be overridden by hostname. Supported values: enabled, disabled
+prefer_editor_prompt: disabled
+# A pager program to send command output to, e.g. "less". If blank, will refer to environment. Set the value to "cat" to disable the pager.
+pager:
+# Aliases allow you to create nicknames for gh commands
+aliases:
+ co: pr checkout
+# The path to a unix socket through which to send HTTP connections. If blank, HTTP traffic will be handled by net/http.DefaultTransport.
+http_unix_socket:
+# What web browser gh should use when opening URLs. If blank, will refer to environment.
+browser:
+# Whether to display labels using their RGB hex color codes in terminals that support truecolor. Supported values: enabled, disabled
+color_labels: disabled
+# Whether customizable, 4-bit accessible colors should be used. Supported values: enabled, disabled
+accessible_colors: disabled
+# Whether an accessible prompter should be used. Supported values: enabled, disabled
+accessible_prompter: disabled
+# Whether to use a animated spinner as a progress indicator. If disabled, a textual progress indicator is used instead. Supported values: enabled, disabled
+spinner: enabled
diff --git a/link.sh b/link.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+echo "🔗 Symlinking config files..."
+
+link_config() {
+ local src="$1"
+ local dest="$2"
+
+ if [ -e "$dest" ] && [ ! -L "$dest" ]; then
+ echo "→ Backing up existing $dest to $dest.bak"
+ mv "$dest" "$dest.bak"
+ fi
+
+ mkdir -p "$(dirname "$dest")"
+ ln -sf "$src" "$dest"
+ echo "✅ Linked $dest"
+}
+
+# Fish
+link_config "$SCRIPT_DIR/fish/config.fish" "$HOME/.config/fish/config.fish"
+link_config "$SCRIPT_DIR/fish/fish_plugins" "$HOME/.config/fish/fish_plugins"
+
+# Neovim
+link_config "$SCRIPT_DIR/nvim" "$HOME/.config/nvim"
+
+# Btop
+link_config "$SCRIPT_DIR/btop/btop.conf" "$HOME/.config/btop/btop.conf"
+
+# GitHub CLI
+link_config "$SCRIPT_DIR/gh/config.yml" "$HOME/.config/gh/config.yml"
+
+# OpenCode
+link_config "$SCRIPT_DIR/opencode/opencode.json" "$HOME/.config/opencode/opencode.json"
+link_config "$SCRIPT_DIR/opencode/command" "$HOME/.config/opencode/command"
+link_config "$SCRIPT_DIR/opencode/plugin" "$HOME/.config/opencode/plugin"
+
+echo "✅ Config symlinks complete!"
diff --git a/nvim/init.lua b/nvim/init.lua
@@ -0,0 +1,27 @@
+-- This file simply bootstraps the installation of Lazy.nvim and then calls other files for execution
+-- This file doesn't necessarily need to be touched, BE CAUTIOUS editing this file and proceed at your own risk.
+local lazypath = vim.env.LAZY or vim.fn.stdpath "data" .. "/lazy/lazy.nvim"
+
+if not (vim.env.LAZY or (vim.uv or vim.loop).fs_stat(lazypath)) then
+ -- stylua: ignore
+ local result = vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", lazypath })
+ if vim.v.shell_error ~= 0 then
+ -- stylua: ignore
+ vim.api.nvim_echo({ { ("Error cloning lazy.nvim:\n%s\n"):format(result), "ErrorMsg" }, { "Press any key to exit...", "MoreMsg" } }, true, {})
+ vim.fn.getchar()
+ vim.cmd.quit()
+ end
+end
+
+vim.opt.rtp:prepend(lazypath)
+
+-- validate that lazy is available
+if not pcall(require, "lazy") then
+ -- stylua: ignore
+ vim.api.nvim_echo({ { ("Unable to load lazy from: %s\n"):format(lazypath), "ErrorMsg" }, { "Press any key to exit...", "MoreMsg" } }, true, {})
+ vim.fn.getchar()
+ vim.cmd.quit()
+end
+
+require "lazy_setup"
+require "polish"
diff --git a/nvim/lua/community.lua b/nvim/lua/community.lua
@@ -0,0 +1,59 @@
+---@type LazySpec
+return {
+ "AstroNvim/astrocommunity",
+ { import = "astrocommunity.pack.lua" },
+ { import = "astrocommunity.bars-and-lines.dropbar-nvim" },
+ { import = "astrocommunity.color.nvim-highlight-colors" },
+ { import = "astrocommunity.colorscheme.oldworld-nvim" },
+ { import = "astrocommunity.ai.opencode-nvim" },
+ { import = "astrocommunity.completion.cmp-cmdline" },
+ { import = "astrocommunity.completion.cmp-emoji" },
+ { import = "astrocommunity.completion.cmp-git" },
+ { import = "astrocommunity.completion.cmp-spell" },
+ { import = "astrocommunity.completion.nvim-cmp" },
+ { import = "astrocommunity.completion.supermaven-nvim" },
+ {
+ "supermaven-inc/supermaven-nvim",
+ opts = {
+ keymaps = {
+ accept_suggestion = "<Tab>",
+ },
+ },
+ },
+ { import = "astrocommunity.recipes.ai" },
+ { import = "astrocommunity.debugging.nvim-dap-virtual-text" },
+ { import = "astrocommunity.editing-support.rainbow-delimiters-nvim" },
+ { import = "astrocommunity.editing-support.telescope-undo-nvim" },
+ { import = "astrocommunity.editing-support.todo-comments-nvim" },
+ { import = "astrocommunity.editing-support.treesj" },
+ -- { import = "astrocommnuity.editing-support.vim-move" },
+ { import = "astrocommunity.editing-support.wildfire-nvim" },
+ { import = "astrocommunity.git.blame-nvim" },
+ { import = "astrocommunity.indent.indent-blankline-nvim" },
+ { import = "astrocommunity.lsp.lsp-signature-nvim" },
+ { import = "astrocommunity.markdown-and-latex.render-markdown-nvim" },
+ { import = "astrocommunity.media.vim-wakatime" },
+ { import = "astrocommunity.motion.hop-nvim" },
+ { import = "astrocommunity.motion.harpoon" },
+ { import = "astrocommunity.pack.bash" },
+ { import = "astrocommunity.pack.cpp" },
+ { import = "astrocommunity.pack.docker" },
+ { import = "astrocommunity.pack.eslint" },
+ { import = "astrocommunity.pack.fish" },
+ { import = "astrocommunity.pack.go" },
+ { import = "astrocommunity.pack.golangci-lint" },
+ { import = "astrocommunity.pack.html-css" },
+ { import = "astrocommunity.pack.java" },
+ { import = "astrocommunity.pack.json" },
+ { import = "astrocommunity.pack.kotlin" },
+ { import = "astrocommunity.pack.lua" },
+ { import = "astrocommunity.pack.markdown" },
+ { import = "astrocommunity.pack.prisma" },
+ { import = "astrocommunity.pack.python" },
+ { import = "astrocommunity.pack.rust" },
+ { import = "astrocommunity.pack.swift" },
+ { import = "astrocommunity.pack.typescript" },
+ { import = "astrocommunity.pack.yaml" },
+ { import = "astrocommunity.pack.zig" },
+ { import = "astrocommunity.project.projectmgr-nvim" },
+}
diff --git a/nvim/lua/lazy_setup.lua b/nvim/lua/lazy_setup.lua
@@ -0,0 +1,29 @@
+require("lazy").setup({
+ {
+ "AstroNvim/AstroNvim",
+ version = "^5",
+ import = "astronvim.plugins",
+ opts = {
+ mapleader = " ",
+ maplocalleader = ",",
+ icons_enabled = true,
+ pin_plugins = nil,
+ update_notifications = true,
+ },
+ },
+ { import = "community" },
+ { import = "plugins" },
+} --[[@as LazySpec]], {
+ ui = { backdrop = 100 },
+ performance = {
+ rtp = {
+ disabled_plugins = {
+ "gzip",
+ "netrwPlugin",
+ "tarPlugin",
+ "tohtml",
+ "zipPlugin",
+ },
+ },
+ },
+} --[[@as LazyConfig]])
diff --git a/nvim/lua/plugins/buffers.lua b/nvim/lua/plugins/buffers.lua
@@ -0,0 +1,28 @@
+---@type LazySpec
+return {
+ {
+ "j-morano/buffer_manager.nvim",
+ dependencies = { "nvim-lua/plenary.nvim" },
+ config = true,
+ },
+ {
+ "AstroNvim/astrocore",
+ ---@param opts AstroCoreOpts
+ opts = {
+ mappings = {
+ n = {
+ ["<leader>b"] = {
+ function() require("buffer_manager.ui").toggle_quick_menu() end,
+ desc = "Open Buffer Manager",
+ },
+ },
+ i = {
+ ["<C-n>"] = { "<Esc>", desc = "Exit insert mode" },
+ },
+ t = {
+ ["<C-n>"] = { "<C-\\><C-n>", desc = "Exit terminal mode" },
+ },
+ },
+ },
+ },
+}
diff --git a/nvim/lua/plugins/colorschemes.lua b/nvim/lua/plugins/colorschemes.lua
@@ -0,0 +1,10 @@
+---@type LazySpec
+return {
+ {
+ "AstroNvim/astroui",
+ ---@type AstroUIOpts
+ opts = {
+ colorscheme = "oldworld",
+ },
+ },
+}
diff --git a/nvim/lua/plugins/completions.lua b/nvim/lua/plugins/completions.lua
@@ -0,0 +1,19 @@
+---@type LazySpec
+return {
+ {
+ "hrsh7th/nvim-cmp",
+ optional = true,
+ opts = function(_, opts)
+ opts.sources = opts.sources or {}
+
+ local function add_source(sources, name, config)
+ for _, source in ipairs(sources) do
+ if source.name == name then return end
+ end
+ table.insert(sources, vim.tbl_deep_extend("force", { name = name }, config or {}))
+ end
+
+ add_source(opts.sources, "emoji", { priority = 700 })
+ end,
+ },
+}
diff --git a/nvim/lua/plugins/hop.lua b/nvim/lua/plugins/hop.lua
@@ -0,0 +1,14 @@
+---@type LazySpec
+return {
+ "AstroNvim/astrocore",
+ ---@type AstroCoreOpts
+ opts = {
+ mappings = {
+ n = {
+ ["<Leader>hw"] = { "<cmd>HopWord<cr>", desc = "Hop word" },
+ ["<Leader>hl"] = { "<cmd>HopLineStart<cr>", desc = "Hop line start" },
+ ["<leader>ha"] = { "<cmd>HopAnywhere<cr>", desc = "Hop anywhere" },
+ },
+ },
+ },
+}
diff --git a/nvim/lua/plugins/projects.lua b/nvim/lua/plugins/projects.lua
@@ -0,0 +1,15 @@
+---@type LazySpec
+return {
+ "AstroNvim/astrocore",
+ ---@param opts AstroCoreOpts
+ opts = {
+ mappings = {
+ n = {
+ ["<leader>o"] = {
+ "<cmd>ProjectMgr<cr>",
+ desc = "Project manager",
+ },
+ },
+ },
+ },
+}
diff --git a/nvim/lua/plugins/rooter.lua b/nvim/lua/plugins/rooter.lua
@@ -0,0 +1,22 @@
+return {
+ "AstroNvim/astrocore",
+ ---@type AstroCoreOpts
+ opts = {
+ rooter = {
+ detector = {
+ "lsp",
+ ".git",
+ {
+ "build.zig",
+ "Cargo.toml",
+ "go.mod",
+ "package.json",
+ "Makefile",
+ },
+ },
+ autochdir = true,
+ scope = "global",
+ notify = false,
+ },
+ },
+}
diff --git a/nvim/lua/plugins/sessions.lua b/nvim/lua/plugins/sessions.lua
@@ -0,0 +1,52 @@
+---@type LazySpec
+return {
+ {
+ "stevearc/resession.nvim",
+ enabled = false,
+ opts = function(_, opts)
+ opts.dir = vim.fn.stdpath "data" .. "/sessions"
+ return opts
+ end,
+ },
+ {
+ "AstroNvim/astrocore",
+ ---@type AstroCoreOpts
+ opts = {
+ sessions = {
+ autosave = {
+ last = false,
+ cwd = false,
+ },
+ },
+ autocmds = {
+ -- restore_session = {
+ -- {
+ -- event = "VimEnter",
+ -- desc = "Restore session for current working directory",
+ -- nested = true,
+ -- callback = function()
+ -- if vim.fn.argc(-1) == 0 then
+ -- require("resession").load(vim.fn.getcwd(), {
+ -- dir = vim.fn.stdpath "data" .. "/sessions",
+ -- silence_errors = true,
+ -- })
+ -- end
+ -- end,
+ -- },
+ -- },
+ -- save_session = {
+ -- {
+ -- event = "VimLeavePre",
+ -- desc = "Save session for current working directory",
+ -- callback = function()
+ -- require("resession").save(vim.fn.getcwd(), {
+ -- dir = vim.fn.stdpath "data" .. "/sessions",
+ -- notify = false,
+ -- })
+ -- end,
+ -- },
+ -- },
+ },
+ },
+ },
+}
diff --git a/nvim/lua/plugins/tabline.lua b/nvim/lua/plugins/tabline.lua
@@ -0,0 +1,17 @@
+---@type LazySpec
+return {
+ {
+ "AstroNvim/astrocore",
+ ---@param opts AstroCoreOpts
+ opts = function(_, opts)
+ if vim.tbl_get(opts, "options", "opt", "showtabline") then opts.options.opt.showtabline = nil end
+ for k, _ in pairs(opts.mappings.n) do
+ if k:find "^<Leader>b" and k ~= "<leader>b" then opts.mappings.n[k] = false end
+ end
+ end,
+ },
+ {
+ "rebelot/heirline.nvim",
+ opts = function(_, opts) opts.tabline = nil end,
+ },
+}
diff --git a/nvim/lua/plugins/terminal.lua b/nvim/lua/plugins/terminal.lua
@@ -0,0 +1,15 @@
+---@type LazySpec
+return {
+ "AstroNvim/astrocore",
+ ---@param opts AstroCoreOpts
+ opts = {
+ mappings = {
+ n = {
+ ["<leader>tb"] = {
+ "<cmd>terminal<cr>",
+ desc = "Terminal buffer",
+ },
+ },
+ },
+ },
+}
diff --git a/nvim/lua/plugins/window-splits.lua b/nvim/lua/plugins/window-splits.lua
@@ -0,0 +1,25 @@
+return {
+ "AstroNvim/astrocore",
+ ---@type AstroCoreOpts
+ opts = {
+ mappings = {
+ n = {
+ -- Override the backslash and pipe defaults
+ ["|"] = {
+ function()
+ vim.cmd "vsplit"
+ vim.cmd "enew"
+ end,
+ desc = "Vertical split with new buffer",
+ },
+ ["\\"] = {
+ function()
+ vim.cmd "split"
+ vim.cmd "enew"
+ end,
+ desc = "Horizontal split with new buffer",
+ },
+ },
+ },
+ },
+}
diff --git a/nvim/lua/polish.lua b/nvim/lua/polish.lua
@@ -0,0 +1,50 @@
+-- Disable statusline
+vim.opt.laststatus = 0
+vim.opt.showmode = false
+
+-- Cursor color by mode
+vim.opt.guicursor = {
+ "n:block-Cursor", -- Normal mode: block cursor
+ "i:ver25-iCursor", -- Insert mode: vertical line in insert color
+ "r:hor20-rCursor", -- Replace mode: horizontal line in replace color
+ "v:block-vCursor", -- Visual mode: block cursor in visual color
+ "c:block-cCursor", -- Command mode: block cursor in command color
+ "t:block-tCursor", -- Terminal mode: block cursor in terminal color
+}
+
+-- Enable true color support
+vim.opt.termguicolors = true
+
+-- Define cursor colors (set after colorscheme loads)
+vim.api.nvim_create_autocmd("ColorScheme", {
+ callback = function()
+ vim.cmd [[
+ highlight iCursor ctermfg=black ctermbg=yellow guifg=black guibg=yellow
+ highlight rCursor ctermfg=black ctermbg=red guifg=black guibg=red
+ highlight vCursor ctermfg=red ctermbg=magenta guifg=#ff0000 guibg=#ff00ff
+ highlight cCursor ctermfg=black ctermbg=brown guifg=black guibg=#ff9500
+ highlight tCursor ctermfg=black ctermbg=brown guifg=black guibg=#ff9500
+ ]]
+ end,
+})
+
+-- Apply highlights on startup
+vim.cmd [[
+ highlight iCursor ctermfg=black ctermbg=yellow guifg=black guibg=yellow
+ highlight rCursor ctermfg=black ctermbg=red guifg=black guibg=red
+ highlight vCursor ctermfg=red ctermbg=magenta guifg=#ff0000 guibg=#ff00ff
+ highlight cCursor ctermfg=black ctermbg=brown guifg=black guibg=#ff9500
+ highlight tCursor ctermfg=black ctermbg=brown guifg=black guibg=#ff9500
+]]
+
+-- Tab navigation with Alt+Tab
+vim.keymap.set("n", "<A-Tab>", "]t", { noremap = true, desc = "Next tab" })
+vim.keymap.set("n", "<S-A-Tab>", "[t", { noremap = true, desc = "Previous tab" })
+
+-- Neovide configuration
+if vim.g.neovide then
+ vim.o.guifont = "JetBrainsMono Nerd Font:h14"
+ vim.g.neovide_opacity = 0.8
+ vim.g.neovide_window_blurred = true
+ vim.g.neovide_remember_window_size = true
+end
diff --git a/nvim/neovim.yml b/nvim/neovim.yml
@@ -0,0 +1,6 @@
+---
+base: lua51
+
+globals:
+ vim:
+ any: true
diff --git a/nvim/selene.toml b/nvim/selene.toml
@@ -0,0 +1,8 @@
+std = "neovim"
+
+[rules]
+global_usage = "allow"
+if_same_then_else = "allow"
+incorrect_standard_library_use = "allow"
+mixed_table = "allow"
+multiple_statements = "allow"
diff --git a/opencode/command/supermemory-init.md b/opencode/command/supermemory-init.md
@@ -0,0 +1,150 @@
+---
+description: Initialize Supermemory with comprehensive codebase knowledge
+---
+
+# Initializing Supermemory
+
+You are initializing persistent memory for this codebase. This is not just data collection - you're building context that will make you significantly more effective across all future sessions.
+
+## Understanding Context
+
+You are a **stateful** coding agent. Users expect to work with you over extended periods - potentially the entire lifecycle of a project. Your memory is how you get better over time and maintain continuity.
+
+## What to Remember
+
+### 1. Procedures (Rules & Workflows)
+Explicit rules that should always be followed:
+- "Never commit directly to main - always use feature branches"
+- "Always run lint before tests"
+- "Use conventional commits format"
+
+### 2. Preferences (Style & Conventions)
+Project and user coding style:
+- "Prefer functional components over class components"
+- "Use early returns instead of nested conditionals"
+- "Always add JSDoc to exported functions"
+
+### 3. Architecture & Context
+How the codebase works and why:
+- "Auth system was refactored in v2.0 - old patterns deprecated"
+- "The monorepo used to have 3 modules before consolidation"
+- "This pagination bug was fixed before - similar to PR #234"
+
+## Memory Scopes
+
+**Project-scoped** (`scope: "project"`):
+- Build/test/lint commands
+- Architecture and key directories
+- Team conventions specific to this codebase
+- Technology stack and framework choices
+- Known issues and their solutions
+
+**User-scoped** (`scope: "user"`):
+- Personal coding preferences across all projects
+- Communication style preferences
+- General workflow habits
+
+## Research Approach
+
+This is a **deep research** initialization. Take your time and be thorough (~50+ tool calls). The goal is to genuinely understand the project, not just collect surface-level facts.
+
+**What to uncover:**
+- Tech stack and dependencies (explicit and implicit)
+- Project structure and architecture
+- Build/test/deploy commands and workflows
+- Contributors & team dynamics (who works on what?)
+- Commit conventions and branching strategy
+- Code evolution (major refactors, architecture changes)
+- Pain points (areas with lots of bug fixes)
+- Implicit conventions not documented anywhere
+
+## Research Techniques
+
+### File-based
+- README.md, CONTRIBUTING.md, AGENTS.md, CLAUDE.md
+- Package manifests (package.json, Cargo.toml, pyproject.toml, go.mod)
+- Config files (.eslintrc, tsconfig.json, .prettierrc)
+- CI/CD configs (.github/workflows/)
+
+### Git-based
+- `git log --oneline -20` - Recent history
+- `git branch -a` - Branching strategy
+- `git log --format="%s" -50` - Commit conventions
+- `git shortlog -sn --all | head -10` - Main contributors
+
+### Explore Agent
+Fire parallel explore queries for broad understanding:
+```
+Task(explore, "What is the tech stack and key dependencies?")
+Task(explore, "What is the project structure? Key directories?")
+Task(explore, "How do you build, test, and run this project?")
+Task(explore, "What are the main architectural patterns?")
+Task(explore, "What conventions or patterns are used?")
+```
+
+## How to Do Thorough Research
+
+**Don't just collect data - analyze and cross-reference.**
+
+Bad (shallow):
+- Run commands, copy output
+- List facts without understanding
+
+Good (thorough):
+- Cross-reference findings (if inconsistent, dig deeper)
+- Resolve ambiguities (don't leave questions unanswered)
+- Read actual file content, not just names
+- Look for patterns (what do commits tell you about workflow?)
+- Think like a new team member - what would you want to know?
+
+## Saving Memories
+
+Use the `supermemory` tool for each distinct insight:
+
+```
+supermemory(mode: "add", content: "...", type: "...", scope: "project")
+```
+
+**Types:**
+- `project-config` - tech stack, commands, tooling
+- `architecture` - codebase structure, key components, data flow
+- `learned-pattern` - conventions specific to this codebase
+- `error-solution` - known issues and their fixes
+- `preference` - coding style preferences (use with user scope)
+
+**Guidelines:**
+- Save each distinct insight as a separate memory
+- Be concise but include enough context to be useful
+- Include the "why" not just the "what" when relevant
+- Update memories incrementally as you research (don't wait until the end)
+
+**Good memories:**
+- "Uses Bun runtime and package manager. Commands: bun install, bun run dev, bun test"
+- "API routes in src/routes/, handlers in src/handlers/. Hono framework."
+- "Auth uses Redis sessions, not JWT. Implementation in src/lib/auth.ts"
+- "Never use `any` type - strict TypeScript. Use `unknown` and narrow."
+- "Database migrations must be backward compatible - we do rolling deploys"
+
+## Upfront Questions
+
+Before diving in, ask:
+1. "Any specific rules I should always follow?"
+2. "Preferences for how I communicate? (terse/detailed)"
+
+## Reflection Phase
+
+Before finishing, reflect:
+1. **Completeness**: Did you cover commands, architecture, conventions, gotchas?
+2. **Quality**: Are memories concise and searchable?
+3. **Scope**: Did you correctly separate project vs user knowledge?
+
+Then ask: "I've initialized memory with X insights. Want me to continue refining, or is this good?"
+
+## Your Task
+
+1. Ask upfront questions (research depth, rules, preferences)
+2. Check existing memories: `supermemory(mode: "list", scope: "project")`
+3. Research based on chosen depth
+4. Save memories incrementally as you discover insights
+5. Reflect and verify completeness
+6. Summarize what was learned and ask if user wants refinement
diff --git a/opencode/opencode.json b/opencode/opencode.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://opencode.ai/config.json",
+ "plugin": ["opencode-supermemory@latest"]
+}
diff --git a/opencode/plugin/wakatime.js b/opencode/plugin/wakatime.js
@@ -0,0 +1,677 @@
+// src/index.ts
+import * as path5 from "node:path";
+
+// src/logger.ts
+import * as fs from "node:fs";
+import * as os from "node:os";
+import * as path from "node:path";
+var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
+ LogLevel2[LogLevel2["DEBUG"] = 0] = "DEBUG";
+ LogLevel2[LogLevel2["INFO"] = 1] = "INFO";
+ LogLevel2[LogLevel2["WARN"] = 2] = "WARN";
+ LogLevel2[LogLevel2["ERROR"] = 3] = "ERROR";
+ return LogLevel2;
+})(LogLevel || {});
+var LOG_FILE = path.join(os.homedir(), ".wakatime", "opencode.log");
+var Logger = class {
+ level = 1 /* INFO */;
+ setLevel(level) {
+ this.level = level;
+ }
+ debug(msg) {
+ this.log(0 /* DEBUG */, msg);
+ }
+ info(msg) {
+ this.log(1 /* INFO */, msg);
+ }
+ warn(msg) {
+ this.log(2 /* WARN */, msg);
+ }
+ error(msg) {
+ this.log(3 /* ERROR */, msg);
+ }
+ warnException(err) {
+ const message = err instanceof Error ? err.message : String(err);
+ this.warn(message);
+ }
+ errorException(err) {
+ const message = err instanceof Error ? err.message : String(err);
+ this.error(message);
+ }
+ log(level, msg) {
+ if (level < this.level) return;
+ const levelName = LogLevel[level];
+ const timestamp2 = (/* @__PURE__ */ new Date()).toISOString();
+ const line = `[${timestamp2}][${levelName}] ${msg}
+`;
+ try {
+ const dir = path.dirname(LOG_FILE);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ fs.appendFileSync(LOG_FILE, line);
+ } catch {
+ }
+ }
+};
+var logger = new Logger();
+
+// src/state.ts
+import * as fs2 from "node:fs";
+import * as os2 from "node:os";
+import * as path2 from "node:path";
+var STATE_FILE = path2.join(os2.homedir(), ".wakatime", "opencode.json");
+function readState() {
+ try {
+ const content = fs2.readFileSync(STATE_FILE, "utf-8");
+ return JSON.parse(content);
+ } catch {
+ return {};
+ }
+}
+function writeState(state) {
+ try {
+ const dir = path2.dirname(STATE_FILE);
+ if (!fs2.existsSync(dir)) {
+ fs2.mkdirSync(dir, { recursive: true });
+ }
+ fs2.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
+ } catch {
+ }
+}
+function timestamp() {
+ return Math.floor(Date.now() / 1e3);
+}
+function shouldSendHeartbeat(force = false) {
+ if (force) return true;
+ try {
+ const state = readState();
+ const lastHeartbeat = state.lastHeartbeatAt ?? 0;
+ return timestamp() - lastHeartbeat >= 60;
+ } catch {
+ return true;
+ }
+}
+function updateLastHeartbeat() {
+ writeState({ lastHeartbeatAt: timestamp() });
+}
+
+// src/wakatime.ts
+import { execFile } from "node:child_process";
+import * as fs4 from "node:fs";
+import * as os4 from "node:os";
+import * as path4 from "node:path";
+import { fileURLToPath } from "node:url";
+
+// src/dependencies.ts
+import { execSync } from "node:child_process";
+import * as fs3 from "node:fs";
+import { createWriteStream } from "node:fs";
+import * as https from "node:https";
+import * as os3 from "node:os";
+import * as path3 from "node:path";
+function whichSync(cmd) {
+ try {
+ const isWindows2 = os3.platform() === "win32";
+ const result = execSync(isWindows2 ? `where ${cmd}` : `which ${cmd}`, {
+ encoding: "utf-8",
+ stdio: ["pipe", "pipe", "pipe"]
+ });
+ const firstLine = result.trim().split("\n")[0];
+ return firstLine || null;
+ } catch {
+ return null;
+ }
+}
+var GITHUB_RELEASES_URL = "https://api.github.com/repos/wakatime/wakatime-cli/releases/latest";
+var GITHUB_DOWNLOAD_URL = "https://github.com/wakatime/wakatime-cli/releases/latest/download";
+var UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1e3;
+var Dependencies = class {
+ resourcesLocation;
+ cliLocation;
+ stateFile;
+ constructor() {
+ this.resourcesLocation = path3.join(os3.homedir(), ".wakatime");
+ this.stateFile = path3.join(
+ this.resourcesLocation,
+ "opencode-cli-state.json"
+ );
+ }
+ isWindows() {
+ return os3.platform() === "win32";
+ }
+ getOsName() {
+ const platform3 = os3.platform();
+ if (platform3 === "win32") return "windows";
+ return platform3;
+ }
+ getArchitecture() {
+ const arch2 = os3.arch();
+ if (arch2 === "x64") return "amd64";
+ if (arch2 === "ia32" || arch2.includes("32")) return "386";
+ if (arch2 === "arm64") return "arm64";
+ if (arch2 === "arm") return "arm";
+ return arch2;
+ }
+ getCliBinaryName() {
+ const osname = this.getOsName();
+ const arch2 = this.getArchitecture();
+ const ext = this.isWindows() ? ".exe" : "";
+ return `wakatime-cli-${osname}-${arch2}${ext}`;
+ }
+ getCliDownloadUrl() {
+ const osname = this.getOsName();
+ const arch2 = this.getArchitecture();
+ return `${GITHUB_DOWNLOAD_URL}/wakatime-cli-${osname}-${arch2}.zip`;
+ }
+ readState() {
+ try {
+ if (fs3.existsSync(this.stateFile)) {
+ return JSON.parse(fs3.readFileSync(this.stateFile, "utf-8"));
+ }
+ } catch {
+ }
+ return {};
+ }
+ writeState(state) {
+ try {
+ fs3.mkdirSync(path3.dirname(this.stateFile), { recursive: true });
+ fs3.writeFileSync(this.stateFile, JSON.stringify(state, null, 2));
+ } catch {
+ }
+ }
+ getCliLocationGlobal() {
+ const binaryName = `wakatime-cli${this.isWindows() ? ".exe" : ""}`;
+ try {
+ const globalPath = whichSync(binaryName);
+ if (globalPath) {
+ logger.debug(`Found global wakatime-cli: ${globalPath}`);
+ return globalPath;
+ }
+ } catch {
+ }
+ return void 0;
+ }
+ getCliLocation() {
+ if (this.cliLocation) return this.cliLocation;
+ const globalCli = this.getCliLocationGlobal();
+ if (globalCli) {
+ this.cliLocation = globalCli;
+ return this.cliLocation;
+ }
+ const binary = this.getCliBinaryName();
+ this.cliLocation = path3.join(this.resourcesLocation, binary);
+ return this.cliLocation;
+ }
+ isCliInstalled() {
+ const location = this.getCliLocation();
+ return fs3.existsSync(location);
+ }
+ shouldCheckForUpdates() {
+ if (this.getCliLocationGlobal()) {
+ return false;
+ }
+ const state = this.readState();
+ if (!state.lastChecked) return true;
+ return Date.now() - state.lastChecked > UPDATE_CHECK_INTERVAL;
+ }
+ async checkAndInstallCli() {
+ if (this.getCliLocationGlobal()) {
+ logger.debug("Using global wakatime-cli, skipping installation check");
+ return;
+ }
+ if (!this.isCliInstalled()) {
+ logger.info("wakatime-cli not found, downloading...");
+ await this.installCli();
+ return;
+ }
+ if (this.shouldCheckForUpdates()) {
+ logger.debug("Checking for wakatime-cli updates...");
+ const latestVersion = await this.getLatestVersion();
+ const state = this.readState();
+ if (latestVersion && latestVersion !== state.version) {
+ logger.info(`Updating wakatime-cli to ${latestVersion}...`);
+ await this.installCli();
+ this.writeState({ lastChecked: Date.now(), version: latestVersion });
+ } else {
+ this.writeState({ ...state, lastChecked: Date.now() });
+ }
+ }
+ }
+ async getLatestVersion() {
+ return new Promise((resolve) => {
+ const options = {
+ headers: {
+ "User-Agent": "opencode-wakatime"
+ }
+ };
+ https.get(GITHUB_RELEASES_URL, options, (res) => {
+ let data = "";
+ res.on("data", (chunk) => {
+ data += chunk;
+ });
+ res.on("end", () => {
+ try {
+ const json = JSON.parse(data);
+ resolve(json.tag_name);
+ } catch {
+ resolve(void 0);
+ }
+ });
+ }).on("error", () => {
+ resolve(void 0);
+ });
+ });
+ }
+ async installCli() {
+ const zipUrl = this.getCliDownloadUrl();
+ const zipFile = path3.join(
+ this.resourcesLocation,
+ `wakatime-cli-${Date.now()}.zip`
+ );
+ try {
+ fs3.mkdirSync(this.resourcesLocation, { recursive: true });
+ logger.debug(`Downloading wakatime-cli from ${zipUrl}`);
+ await this.downloadFile(zipUrl, zipFile);
+ logger.debug(`Extracting wakatime-cli to ${this.resourcesLocation}`);
+ await this.extractZip(zipFile, this.resourcesLocation);
+ if (!this.isWindows()) {
+ const cliPath = this.getCliLocation();
+ if (fs3.existsSync(cliPath)) {
+ fs3.chmodSync(cliPath, 493);
+ logger.debug(`Set executable permission on ${cliPath}`);
+ }
+ }
+ logger.info("wakatime-cli installed successfully");
+ } catch (err) {
+ logger.errorException(err);
+ throw err;
+ } finally {
+ try {
+ if (fs3.existsSync(zipFile)) {
+ fs3.unlinkSync(zipFile);
+ }
+ } catch {
+ }
+ }
+ }
+ downloadFile(url, dest) {
+ return new Promise((resolve, reject) => {
+ const followRedirect = (url2, redirectCount = 0) => {
+ if (redirectCount > 5) {
+ reject(new Error("Too many redirects"));
+ return;
+ }
+ https.get(url2, (res) => {
+ if (res.statusCode === 301 || res.statusCode === 302) {
+ const location = res.headers.location;
+ if (location) {
+ followRedirect(location, redirectCount + 1);
+ return;
+ }
+ }
+ if (res.statusCode !== 200) {
+ reject(new Error(`HTTP ${res.statusCode}`));
+ return;
+ }
+ const file = createWriteStream(dest);
+ res.pipe(file);
+ file.on("finish", () => {
+ file.close();
+ resolve();
+ });
+ file.on("error", (err) => {
+ fs3.unlinkSync(dest);
+ reject(err);
+ });
+ }).on("error", reject);
+ };
+ followRedirect(url);
+ });
+ }
+ async extractZip(zipFile, destDir) {
+ const { execSync: execSync2 } = await import("node:child_process");
+ try {
+ if (this.isWindows()) {
+ execSync2(
+ `powershell -command "Expand-Archive -Force '${zipFile}' '${destDir}'"`,
+ {
+ windowsHide: true
+ }
+ );
+ } else {
+ execSync2(`unzip -o "${zipFile}" -d "${destDir}"`, {
+ stdio: "ignore"
+ });
+ }
+ } catch (_err) {
+ logger.warn("Native unzip failed, attempting manual extraction");
+ await this.extractZipManual(zipFile, destDir);
+ }
+ }
+ async extractZipManual(zipFile, destDir) {
+ const data = fs3.readFileSync(zipFile);
+ let offset = 0;
+ while (offset < data.length - 4) {
+ if (data[offset] === 80 && data[offset + 1] === 75 && data[offset + 2] === 3 && data[offset + 3] === 4) {
+ const compressedSize = data.readUInt32LE(offset + 18);
+ const uncompressedSize = data.readUInt32LE(offset + 22);
+ const fileNameLength = data.readUInt16LE(offset + 26);
+ const extraFieldLength = data.readUInt16LE(offset + 28);
+ const fileName = data.slice(offset + 30, offset + 30 + fileNameLength).toString();
+ const dataStart = offset + 30 + fileNameLength + extraFieldLength;
+ const compressionMethod = data.readUInt16LE(offset + 8);
+ if (fileName.includes("wakatime-cli")) {
+ const destPath = path3.join(destDir, path3.basename(fileName));
+ if (compressionMethod === 0) {
+ const fileData = data.slice(
+ dataStart,
+ dataStart + uncompressedSize
+ );
+ fs3.writeFileSync(destPath, fileData);
+ } else if (compressionMethod === 8) {
+ const { inflateRawSync } = await import("node:zlib");
+ const compressedData = data.slice(
+ dataStart,
+ dataStart + compressedSize
+ );
+ const decompressed = inflateRawSync(compressedData);
+ fs3.writeFileSync(destPath, decompressed);
+ }
+ logger.debug(`Extracted ${fileName} to ${destPath}`);
+ return;
+ }
+ offset = dataStart + compressedSize;
+ } else {
+ offset++;
+ }
+ }
+ throw new Error("Could not find wakatime-cli in zip file");
+ }
+};
+var dependencies = new Dependencies();
+
+// src/wakatime.ts
+function getVersion() {
+ try {
+ const __dirname = path4.dirname(fileURLToPath(import.meta.url));
+ const pkg = JSON.parse(
+ fs4.readFileSync(path4.join(__dirname, "..", "package.json"), "utf-8")
+ );
+ return pkg.version;
+ } catch {
+ return "unknown";
+ }
+}
+var VERSION = getVersion();
+function isWindows() {
+ return os4.platform() === "win32";
+}
+function buildExecOptions() {
+ const options = {
+ windowsHide: true
+ };
+ if (!isWindows() && !process.env.WAKATIME_HOME && !process.env.HOME) {
+ options.env = { ...process.env, WAKATIME_HOME: os4.homedir() };
+ }
+ return options;
+}
+function formatArgs(args) {
+ return args.map((arg) => {
+ if (arg.includes(" ")) {
+ return `"${arg.replace(/"/g, '\\"')}"`;
+ }
+ return arg;
+ }).join(" ");
+}
+async function ensureCliInstalled() {
+ try {
+ await dependencies.checkAndInstallCli();
+ return dependencies.isCliInstalled();
+ } catch (err) {
+ logger.errorException(err);
+ return false;
+ }
+}
+function sendHeartbeat(params) {
+ const cliLocation = dependencies.getCliLocation();
+ if (!dependencies.isCliInstalled()) {
+ logger.warn("wakatime-cli not installed, skipping heartbeat");
+ return;
+ }
+ const args = [
+ "--entity",
+ params.entity,
+ "--entity-type",
+ "file",
+ "--category",
+ params.category ?? "ai coding",
+ "--plugin",
+ `opencode/1.0.0 opencode-wakatime/${VERSION}`
+ ];
+ if (params.projectFolder) {
+ args.push("--project-folder", params.projectFolder);
+ }
+ if (params.lineChanges !== void 0 && params.lineChanges !== 0) {
+ args.push("--ai-line-changes", params.lineChanges.toString());
+ }
+ if (params.isWrite) {
+ args.push("--write");
+ }
+ logger.debug(`Sending heartbeat: wakatime-cli ${formatArgs(args)}`);
+ const execOptions = buildExecOptions();
+ execFile(cliLocation, args, execOptions, (error, stdout, stderr) => {
+ const output = (stdout?.toString().trim() ?? "") + (stderr?.toString().trim() ?? "");
+ if (output) {
+ logger.debug(`wakatime-cli output: ${output}`);
+ }
+ if (error) {
+ logger.error(`wakatime-cli error: ${error.message}`);
+ }
+ });
+}
+
+// src/index.ts
+var fileChanges = /* @__PURE__ */ new Map();
+function extractFileChanges(tool, metadata, output, title) {
+ const changes = [];
+ if (!metadata) return changes;
+ switch (tool) {
+ case "edit": {
+ const filediff = metadata.filediff;
+ if (filediff?.file) {
+ changes.push({
+ file: filediff.file,
+ info: {
+ additions: filediff.additions ?? 0,
+ deletions: filediff.deletions ?? 0,
+ isWrite: false
+ }
+ });
+ } else {
+ const filePath = metadata.filePath;
+ if (filePath) {
+ changes.push({
+ file: filePath,
+ info: { additions: 0, deletions: 0, isWrite: false }
+ });
+ }
+ }
+ break;
+ }
+ case "write": {
+ const filepath = metadata.filepath;
+ const exists = metadata.exists;
+ if (filepath) {
+ changes.push({
+ file: filepath,
+ info: {
+ additions: 0,
+ deletions: 0,
+ isWrite: !exists
+ // New file creation
+ }
+ });
+ }
+ break;
+ }
+ case "patch": {
+ const diff = metadata.diff;
+ const lines = output.split("\n");
+ const files = [];
+ for (const line of lines) {
+ if (line.startsWith(" ") && !line.startsWith(" ")) {
+ const file = line.trim();
+ if (file && !file.includes(" ")) {
+ files.push(file);
+ }
+ }
+ }
+ const perFileDiff = files.length > 0 ? Math.round((diff ?? 0) / files.length) : 0;
+ for (const file of files) {
+ changes.push({
+ file,
+ info: {
+ additions: perFileDiff > 0 ? perFileDiff : 0,
+ deletions: perFileDiff < 0 ? Math.abs(perFileDiff) : 0,
+ isWrite: false
+ }
+ });
+ }
+ break;
+ }
+ case "multiedit": {
+ const results = metadata.results;
+ if (results) {
+ for (const result of results) {
+ if (result.filediff?.file) {
+ changes.push({
+ file: result.filediff.file,
+ info: {
+ additions: result.filediff.additions ?? 0,
+ deletions: result.filediff.deletions ?? 0,
+ isWrite: false
+ }
+ });
+ }
+ }
+ }
+ break;
+ }
+ case "read": {
+ if (title) {
+ changes.push({
+ file: title,
+ info: { additions: 0, deletions: 0, isWrite: false }
+ });
+ }
+ break;
+ }
+ case "glob":
+ case "grep": {
+ break;
+ }
+ case "bash": {
+ break;
+ }
+ }
+ return changes;
+}
+function processHeartbeat(projectFolder, force = false) {
+ if (!shouldSendHeartbeat(force) && !force) {
+ logger.debug("Skipping heartbeat (rate limited)");
+ return;
+ }
+ if (fileChanges.size === 0) {
+ logger.debug("No file changes to report");
+ return;
+ }
+ for (const [file, info] of fileChanges.entries()) {
+ const lineChanges = info.additions - info.deletions;
+ sendHeartbeat({
+ entity: file,
+ projectFolder,
+ lineChanges,
+ category: "ai coding",
+ isWrite: info.isWrite
+ });
+ logger.debug(
+ `Sent heartbeat for ${file}: +${info.additions}/-${info.deletions} lines`
+ );
+ }
+ fileChanges.clear();
+ updateLastHeartbeat();
+}
+function trackFileChange(file, info) {
+ const existing = fileChanges.get(file) ?? {
+ additions: 0,
+ deletions: 0,
+ lastModified: Date.now(),
+ isWrite: false
+ };
+ fileChanges.set(file, {
+ additions: existing.additions + (info.additions ?? 0),
+ deletions: existing.deletions + (info.deletions ?? 0),
+ lastModified: Date.now(),
+ isWrite: existing.isWrite || (info.isWrite ?? false)
+ });
+}
+var plugin = async (ctx) => {
+ const { project, worktree } = ctx;
+ const projectName = path5.basename(worktree || project.worktree);
+ const cliInstalled = await ensureCliInstalled();
+ if (!cliInstalled) {
+ logger.warn(
+ "WakaTime CLI could not be installed. Please install it manually: https://wakatime.com/terminal"
+ );
+ } else {
+ logger.info(
+ `OpenCode WakaTime plugin initialized for project: ${projectName}`
+ );
+ }
+ const projectFolder = worktree || process.cwd();
+ const hooks = {
+ // Track tool executions that modify files
+ "tool.execute.after": async (input, output) => {
+ const { tool } = input;
+ const { metadata, title } = output;
+ logger.debug(`Tool executed: ${tool} - ${title}`);
+ const changes = extractFileChanges(
+ tool,
+ metadata,
+ output.output,
+ title
+ );
+ for (const change of changes) {
+ trackFileChange(change.file, change.info);
+ logger.debug(
+ `Tracked: ${change.file} (+${change.info.additions ?? 0}/-${change.info.deletions ?? 0})`
+ );
+ }
+ if (changes.length > 0) {
+ processHeartbeat(projectFolder);
+ }
+ },
+ // Track chat activity
+ "chat.message": async (_input, _output) => {
+ logger.debug("Chat message received");
+ if (fileChanges.size > 0) {
+ processHeartbeat(projectFolder);
+ }
+ },
+ // Listen to all events for session lifecycle
+ event: async ({ event }) => {
+ if (event.type === "session.deleted" || event.type === "session.idle") {
+ logger.debug(`Session event: ${event.type} - sending final heartbeat`);
+ processHeartbeat(projectFolder, true);
+ }
+ }
+ };
+ return hooks;
+};
+var index_default = plugin;
+export {
+ index_default as default,
+ extractFileChanges,
+ plugin
+};
diff --git a/setup.sh b/setup.sh
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+echo "🔧 Starting macOS setup..."
+
+# Hostname
+HOSTNAME="macbox"
+echo "🖥️ Setting system hostname to '$HOSTNAME'..."
+sudo scutil --set HostName "$HOSTNAME" && echo "✅ HostName set"
+sudo scutil --set ComputerName "$HOSTNAME" && echo "✅ ComputerName set"
+sudo scutil --set LocalHostName "$HOSTNAME" && echo "✅ LocalHostName set"
+
+# Dock
+echo "📦 Configuring Dock preferences..."
+defaults write com.apple.dock orientation -string "bottom" && echo "✅ Dock position set to bottom"
+defaults write com.apple.dock autohide -bool true && echo "✅ Dock autohide enabled"
+defaults write com.apple.dock autohide-delay -float 0 && echo "✅ Dock autohide delay set"
+defaults write com.apple.dock no-bouncing -bool true && echo "✅ Dock bounce disabled"
+killall Dock && echo "🔄 Dock restarted"
+
+# Windows
+defaults write -g NSWindowShouldDragOnGesture -bool true && echo "✅ Window drag by gesture enabled"
+
+# Homebrew
+echo "🍺 Installing Homebrew packages with Brewfile..."
+if ! command -v brew >/dev/null 2>&1; then
+ echo "❌ Homebrew not found. Please install it first: https://brew.sh"
+ exit 1
+fi
+
+brew update && echo "✅ Homebrew updated"
+brew upgrade && echo "✅ Homebrew upgraded"
+BREWFILE_PATH="./Brewfile"
+if [ -f "$BREWFILE_PATH" ]; then
+ brew bundle --file="$BREWFILE_PATH" && echo "✅ Brewfile installed successfully"
+else
+ echo "❌ Brewfile not found at $BREWFILE_PATH"
+ exit 1
+fi
+brew cleanup && echo "✅ Homebrew cleanup completed"
+
+# Symlink configs
+"$SCRIPT_DIR/link.sh"
+
+# Set fish as default shell
+echo "🐟 Setting fish as default shell..."
+FISH_PATH="$(which fish)"
+if ! grep -q "$FISH_PATH" /etc/shells; then
+ echo "$FISH_PATH" | sudo tee -a /etc/shells >/dev/null
+fi
+if [ "$SHELL" != "$FISH_PATH" ]; then
+ chsh -s "$FISH_PATH" && echo "✅ Default shell set to fish"
+else
+ echo "✅ Fish is already the default shell"
+fi
+
+# Keyboard layout
+echo "⌨️ Checking for Colemak-DH layout bundle..."
+CASK_BUNDLE="$(find /opt/homebrew/Caskroom/colemak-dh -name "Colemak*.bundle" 2>/dev/null | head -1)"
+LAYOUT_DEST="/Library/Keyboard Layouts"
+
+if [ -n "$CASK_BUNDLE" ] && [ -d "$CASK_BUNDLE/Contents/Resources" ]; then
+ echo "→ Found layout bundle at $CASK_BUNDLE"
+
+ if sudo cp "$CASK_BUNDLE/Contents/Resources/"*.keylayout "$LAYOUT_DEST" 2>/dev/null; then
+ echo "✅ Keylayout copied to $LAYOUT_DEST"
+ else
+ echo "⚠️ No .keylayout file found or copy failed"
+ fi
+
+ if sudo cp "$CASK_BUNDLE/Contents/Resources/"*.icns "$LAYOUT_DEST" 2>/dev/null; then
+ echo "✅ Icon file copied to $LAYOUT_DEST"
+ else
+ echo "⚠️ No .icns file found or copy failed (optional)"
+ fi
+else
+ echo "⚠️ Colemak-DH bundle not found. Install with: brew install --cask colemak-dh"
+fi
+
+echo "✅ macOS setup script completed!"