configs

configs
git clone https://git.prime8.dev/configs.git
Log | Files | Refs | README

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:
A.gitignore | 1+
ABrewfile | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abtop/btop.conf | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afish/config.fish | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afish/fish_plugins | 3+++
Agh/config.yml | 27+++++++++++++++++++++++++++
Alink.sh | 40++++++++++++++++++++++++++++++++++++++++
Anvim/init.lua | 27+++++++++++++++++++++++++++
Anvim/lua/community.lua | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anvim/lua/lazy_setup.lua | 29+++++++++++++++++++++++++++++
Anvim/lua/plugins/buffers.lua | 28++++++++++++++++++++++++++++
Anvim/lua/plugins/colorschemes.lua | 10++++++++++
Anvim/lua/plugins/completions.lua | 19+++++++++++++++++++
Anvim/lua/plugins/hop.lua | 14++++++++++++++
Anvim/lua/plugins/projects.lua | 15+++++++++++++++
Anvim/lua/plugins/rooter.lua | 22++++++++++++++++++++++
Anvim/lua/plugins/sessions.lua | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Anvim/lua/plugins/tabline.lua | 17+++++++++++++++++
Anvim/lua/plugins/terminal.lua | 15+++++++++++++++
Anvim/lua/plugins/window-splits.lua | 25+++++++++++++++++++++++++
Anvim/lua/polish.lua | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Anvim/neovim.yml | 6++++++
Anvim/selene.toml | 8++++++++
Aopencode/command/supermemory-init.md | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aopencode/opencode.json | 4++++
Aopencode/plugin/wakatime.js | 677+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asetup.sh | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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!"