diff --git a/.gitignore b/.gitignore index efa45c6..c305765 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,13 @@ tags* .tags* tmp *.sw* + +# pi — runtime state, never commit +configs/pi/auth.json +configs/pi/trust.json +configs/pi/sessions +configs/pi/npm +configs/pi/git +configs/pi/models-store.json +configs/pi/mcp-cache.json +Makefile.bak-* diff --git a/Makefile b/Makefile index eb0984d..0a2ba3c 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +# Default to this repository's location while allowing an explicit override. +DOTFILES ?= $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) + # Help command to see all available things. help: @echo "Available commands:" @@ -10,7 +13,7 @@ msg: echo "Already installed." .PHONY: install # Symlinks all the configuration files -install: bash brew git vim +install: bash brew git vim pi ## DISABLED COMMANDS # code_complete ctags @@ -57,3 +60,29 @@ vim: # Install vim plugins curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim || make msg + + +# Pi coding agent configuration +PI_SHA := ac4ac9eaf69f2b01ca3af984a5c48f3b99b84278 +PI_REPO := https://github.com/earendil-works/pi.git +PI_CONFIG := $(DOTFILES)/configs/pi +PI_SCRIPT := $(DOTFILES)/scripts/pi-config.sh +PI_ENV := PI_SHA="$(PI_SHA)" PI_REPO="$(PI_REPO)" PI_CONFIG="$(PI_CONFIG)" + +.PHONY: pi # Install pi and link the portable configuration +pi: pi_check + npm install -g --ignore-scripts @earendil-works/pi-coding-agent + @$(PI_ENV) "$(PI_SCRIPT)" install + @echo "Pi is installed. Run 'pi' to configure this machine." + +.PHONY: pi_check # Verify tracked pi configuration is portable and runtime-state-free +pi_check: + @$(PI_ENV) "$(PI_SCRIPT)" check + +.PHONY: pi_verify # Verify the managed checkout and installed links +pi_verify: + @$(PI_ENV) "$(PI_SCRIPT)" verify + +.PHONY: pi_unlink # Remove only symlinks managed by make pi +pi_unlink: + @$(PI_ENV) "$(PI_SCRIPT)" unlink diff --git a/README.md b/README.md index 9e7fc74..903ff7f 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,46 @@ - [Install Iterm 2](https://iterm2.com/downloads/stable/latest) - Install Brew + ```sh /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` + - Export variables -``` + +```sh CODE_PATH=~/Code DOTFILES=$CODE_PATH/dotfiles ``` -- Install Vim Plugins, by running `vim` and `:PlugInstall` - https://github.com/junegunn/vim-plug + +- Install Vim Plugins, by running `vim` and `:PlugInstall` - + +## Pi coding agent + +Install pi and this repository's portable configuration with: + +```sh +make pi +``` + +The command is safe to run repeatedly. It installs the coding agent, maintains a +sparse checkout of [`earendil-works/pi`](https://github.com/earendil-works/pi) +at a pinned revision under `~/.pi/src/pi`, and links the selected upstream and +custom extensions into `~/.pi/agent/extensions/`. + +Machine-local runtime state is not copied or linked by this repository. Complete +any required setup through pi itself after installation. + +Useful checks and cleanup commands: + +```sh +make pi_check # validate the tracked configuration +make pi_verify # verify the installed checkout and links +make pi_unlink # remove only links managed by make pi +``` ## Brew Packages + - ag - bash-completion - git @@ -20,6 +49,7 @@ DOTFILES=$CODE_PATH/dotfiles - wget ## Vim Plugins + - [vim-plug](https://github.com/junegunn/vim-plug) - [ale](https://github.com/w0rp/ale) - [ag](https://github.com/rking/ag.vim) diff --git a/configs/bash/.bash_profile b/configs/bash/.bash_profile index a8d53b7..7beb61c 100644 --- a/configs/bash/.bash_profile +++ b/configs/bash/.bash_profile @@ -18,37 +18,62 @@ if [ ! -f ~/.git-completion.bash ]; then fi # Set the git branch color based on status +# set_git_prompt() { +# local branch=$(git branch --show-current 2>/dev/null) +# +# if [ -n "$branch" ]; then +# local git_status="$(git status --porcelain 2>/dev/null)" +# local color="32" # Default green +# +# if [ -n "$git_status" ]; then +# if git status 2>/dev/null | grep -q "Changes to be committed"; then +# color="33" # Yellow (staged) +# else +# color="31" # Red (unstaged) +# fi +# elif git status 2>/dev/null | grep -q "Your branch is ahead"; then +# color="36" # Cyan (ahead) +# fi +# +# # Store just the color code and branch name +# GIT_COLOR="$color" +# GIT_BRANCH="$branch" +# else +# GIT_COLOR="" +# GIT_BRANCH="" +# fi +# } + set_git_prompt() { local branch=$(git branch --show-current 2>/dev/null) + local git_part="" if [ -n "$branch" ]; then local git_status="$(git status --porcelain 2>/dev/null)" - local color="32" # Default green + local color="32" if [ -n "$git_status" ]; then if git status 2>/dev/null | grep -q "Changes to be committed"; then - color="33" # Yellow (staged) + color="33" else - color="31" # Red (unstaged) + color="31" fi elif git status 2>/dev/null | grep -q "Your branch is ahead"; then - color="36" # Cyan (ahead) + color="36" fi - # Store just the color code and branch name - GIT_COLOR="$color" - GIT_BRANCH="$branch" - else - GIT_COLOR="" - GIT_BRANCH="" + git_part="(\001\033[1;${color}m\002${branch} ⛙\001\033[0m\002)" fi + + PS1="\001\033[4;1;${PROMPT_COLOR}m\002\w\001\033[0m\002${git_part}\$ " } # Update prompt before each command PROMPT_COMMAND=set_git_prompt # Build PS1 with proper escaping done at PS1 evaluation time -export PS1='\[\033[4;1;${PROMPT_COLOR}m\]\w\[\033[0m\]${GIT_BRANCH:+(}\[\033[1;${GIT_COLOR}m\]${GIT_BRANCH} ⛙\[\033[0m\]${GIT_BRANCH:+)}$ ' +export PS1='\[\033[4;1;${PROMPT_COLOR}m\]\w\[\033[0m\]${GIT_BRANCH:+(\[\033[1;${GIT_COLOR}m\]${GIT_BRANCH} ⛙\[\033[0m\])}$ ' +# export PS1='\[\033[4;1;${PROMPT_COLOR}m\]\w\[\033[0m\]${GIT_BRANCH:+(}\[\033[1;${GIT_COLOR}m\]${GIT_BRANCH} ⛙\[\033[0m\]${GIT_BRANCH:+)}$ ' ###################################### # Aliases # diff --git a/configs/nvim/lua/keymaps.lua b/configs/nvim/lua/keymaps.lua index b80da20..96178e0 100644 --- a/configs/nvim/lua/keymaps.lua +++ b/configs/nvim/lua/keymaps.lua @@ -1,6 +1,8 @@ -- Keymaps (matching your vimrc workflow) --- Command typo fixes +-- ============================================ +-- Typos +-- ============================================ vim.cmd([[ cnoreabbrev W w cnoreabbrev Q q @@ -10,6 +12,9 @@ vim.cmd([[ cnoreabbrev Bd bd ]]) +vim.keymap.set("n", ":w'", ":w") +vim.keymap.set("n", ":W'", ":w") + -- Helper function local function map(mode, lhs, rhs, desc) vim.keymap.set(mode, lhs, rhs, { noremap = true, silent = true, desc = desc }) diff --git a/configs/nvim/lua/lsp-config.lua b/configs/nvim/lua/lsp-config.lua index 6cb63be..9b2c41d 100644 --- a/configs/nvim/lua/lsp-config.lua +++ b/configs/nvim/lua/lsp-config.lua @@ -52,6 +52,12 @@ vim.lsp.config("pyright", { }, }) +vim.api.nvim_create_autocmd("CursorHold", { + callback = function() + vim.diagnostic.open_float(nil, { focus = false }) + end, +}) + -- Enable servers (only if command exists) local servers = { { name = "ts_ls", cmd = "typescript-language-server" }, diff --git a/configs/nvim/lua/plugins.lua b/configs/nvim/lua/plugins.lua index 1736714..5191fd0 100644 --- a/configs/nvim/lua/plugins.lua +++ b/configs/nvim/lua/plugins.lua @@ -51,14 +51,14 @@ require("lazy").setup({ formatters_by_ft = { ["_"] = { "trim_whitespace", "trim_newlines" }, python = { "ruff_organize_imports", "ruff_format" }, - javascript = { "prettier" }, - javascriptreact = { "prettier" }, - typescript = { "prettier" }, - typescriptreact = { "prettier" }, - css = { "prettier" }, - json = { "prettier" }, - yaml = { "prettier" }, - markdown = { "prettier" }, + javascript = { "oxfmt" }, + javascriptreact = { "oxfmt" }, + typescript = { "oxfmt" }, + typescriptreact = { "oxfmt" }, + css = { "oxfmt" }, + json = { "oxfmt" }, + yaml = { "oxfmt" }, + markdown = { "oxfmt" }, }, format_on_save = true, }) diff --git a/configs/pi/extensions/vim-editor.ts b/configs/pi/extensions/vim-editor.ts new file mode 100644 index 0000000..68a2c82 --- /dev/null +++ b/configs/pi/extensions/vim-editor.ts @@ -0,0 +1,230 @@ +/** + * vim-editor.ts — modal (vim-like) editing for the Pi prompt editor. + * + * Extends the official example at + * packages/coding-agent/examples/extensions/modal-editor.ts + * which ships h/j/k/l/0/$/x/i/a. This adds word motions, operators (d/c), + * counts, line-wise commands, and open-line. + * + * Install: + * cp vim-editor.ts ~/.pi/agent/extensions/vim-editor.ts + * # then in pi: /reload + * + * Modes: + * Insert (default) — everything types through normally. + * Normal — Escape from insert. Escape again aborts the agent run. + * + * Normal mode: + * motions h j k l w e b 0 ^ $ (accept counts, e.g. 3w) + * insert i a I A o O + * edits x X s S D C p + * operators dw db dd d$ d0 dh dl / cw cb cc c$ c0 + * + * HOW THIS WORKS (and its limits): Pi's editor has no vim engine. This + * translates normal-mode keys into the terminal control sequences that Pi's + * default readline-style keybindings already understand. So it is a faithful + * *feel*, not a faithful vim: there is no undo tree, no registers, no text + * objects (ciw, di"), no visual mode, and no marks. `p` pastes Pi's kill-ring + * (ctrl+y), not a vim register. + * + * IMPORTANT: j/k at the first/last line browse prompt history — that is Pi's + * built-in cursorUp/cursorDown behavior, not a bug here. Counts are capped so + * a stray `999j` cannot stampede through your history. + * + * If you rebind tui.editor.* keys in keybindings.json away from the defaults + * listed beside each sequence below, update K to match. + */ + +import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; + +/** Control sequences matching Pi's DEFAULT tui.editor.* / tui.input.* bindings. */ +const K = { + left: "\x1b[D", // cursorLeft + right: "\x1b[C", // cursorRight + up: "\x1b[A", // cursorUp + down: "\x1b[B", // cursorDown + lineStart: "\x01", // ctrl+a cursorLineStart + lineEnd: "\x05", // ctrl+e cursorLineEnd + wordLeft: "\x1bb", // alt+b cursorWordLeft + wordRight: "\x1bf", // alt+f cursorWordRight + backspace: "\x7f", // deleteCharBackward + delForward: "\x1b[3~", // deleteCharForward + delWordBack: "\x17", // ctrl+w deleteWordBackward + delWordForward: "\x1bd", // alt+d deleteWordForward + delToLineStart: "\x15", // ctrl+u deleteToLineStart + delToLineEnd: "\x0b", // ctrl+k deleteToLineEnd + yank: "\x19", // ctrl+y yank + newLine: "\x0a", // ctrl+j input.newLine +} as const; + +const MAX_COUNT = 100; + +type Mode = "normal" | "insert"; +type Operator = "d" | "c"; + +class VimEditor extends CustomEditor { + private mode: Mode = "insert"; + private count = ""; + private operator: Operator | null = null; + + // --- input routing ---------------------------------------------------- + + handleInput(data: string): void { + if (matchesKey(data, "escape")) { + if (this.mode === "insert") { + this.mode = "normal"; + this.clearPending(); + } else if (this.operator !== null || this.count !== "") { + this.clearPending(); // cancel half-typed command, stay in normal + } else { + super.handleInput(data); // abort agent run + } + return; + } + + if (this.mode === "insert") { + super.handleInput(data); + return; + } + + this.handleNormal(data); + } + + private handleNormal(data: string): void { + // Accumulate count prefix. Bare "0" is the line-start motion. + if (/^[1-9]$/.test(data) || (data === "0" && this.count !== "")) { + this.count += data; + return; + } + + const n = Math.min(Math.max(parseInt(this.count || "1", 10) || 1, 1), MAX_COUNT); + const op = this.operator; + this.clearPending(); + + if (op !== null) { + this.applyOperator(op, data, n); + return; + } + + switch (data) { + // motions + case "h": return this.repeat(K.left, n); + case "l": return this.repeat(K.right, n); + case "j": return this.repeat(K.down, n); + case "k": return this.repeat(K.up, n); + case "w": + case "e": return this.repeat(K.wordRight, n); + case "b": return this.repeat(K.wordLeft, n); + case "0": + case "^": return this.send(K.lineStart); + case "$": return this.send(K.lineEnd); + + // enter insert mode + case "i": this.mode = "insert"; return; + case "a": this.send(K.right); this.mode = "insert"; return; + case "I": this.send(K.lineStart); this.mode = "insert"; return; + case "A": this.send(K.lineEnd); this.mode = "insert"; return; + case "o": + this.send(K.lineEnd); + this.send(K.newLine); + this.mode = "insert"; + return; + case "O": + this.send(K.lineStart); + this.send(K.newLine); + this.send(K.up); + this.mode = "insert"; + return; + + // single-key edits + case "x": return this.repeat(K.delForward, n); + case "X": return this.repeat(K.backspace, n); + case "s": this.send(K.delForward); this.mode = "insert"; return; + case "D": return this.send(K.delToLineEnd); + case "C": this.send(K.delToLineEnd); this.mode = "insert"; return; + case "S": + this.send(K.lineStart); + this.send(K.delToLineEnd); + this.mode = "insert"; + return; + case "p": return this.send(K.yank); + + // pending operators + case "d": this.operator = "d"; return; + case "c": this.operator = "c"; return; + } + + // Swallow unmapped printable keys so they don't leak into the buffer. + // Pass control sequences (ctrl+c, enter, tab, ...) through to Pi. + if (data.length === 1 && data.charCodeAt(0) >= 32) return; + super.handleInput(data); + } + + private applyOperator(op: Operator, motion: string, n: number): void { + // dd / cc — line-wise + if (motion === op) { + this.send(K.lineStart); + this.send(K.delToLineEnd); + if (op === "c") this.mode = "insert"; + return; + } + + switch (motion) { + case "w": + case "e": this.repeat(K.delWordForward, n); break; + case "b": this.repeat(K.delWordBack, n); break; + case "$": this.send(K.delToLineEnd); break; + case "0": + case "^": this.send(K.delToLineStart); break; + case "h": this.repeat(K.backspace, n); break; + case "l": this.repeat(K.delForward, n); break; + default: return; // unsupported motion — abort, change nothing + } + + if (op === "c") this.mode = "insert"; + } + + // --- helpers ---------------------------------------------------------- + + private send(seq: string): void { + super.handleInput(seq); + } + + private repeat(seq: string, times: number): void { + for (let i = 0; i < times; i++) super.handleInput(seq); + } + + private clearPending(): void { + this.count = ""; + this.operator = null; + } + + // --- rendering -------------------------------------------------------- + + render(width: number): string[] { + const lines = super.render(width); + if (lines.length === 0) return lines; + + const pending = `${this.count}${this.operator ?? ""}`; + const label = + this.mode === "normal" + ? pending + ? ` NORMAL ${pending} ` + : " NORMAL " + : " INSERT "; + + const last = lines.length - 1; + if (visibleWidth(lines[last]!) >= label.length) { + lines[last] = truncateToWidth(lines[last]!, width - label.length, "") + label; + } + return lines; + } +} + +export default function (pi: ExtensionAPI) { + pi.on("session_start", (_event, ctx) => { + if (ctx.mode !== "tui") return; // editor components are TUI-only + ctx.ui.setEditorComponent((tui, theme, kb) => new VimEditor(tui, theme, kb)); + }); +} diff --git a/configs/pi/keybindings.json b/configs/pi/keybindings.json new file mode 100644 index 0000000..c9bda7e --- /dev/null +++ b/configs/pi/keybindings.json @@ -0,0 +1,48 @@ +{ + "//": "~/.pi/agent/keybindings.json — macOS. Run /reload after editing.", + + "//1": "NOTE: user bindings REPLACE defaults for an action, so defaults worth", + "//2": "keeping are re-listed explicitly below.", + + "//3": "--- editor: ctrl-only, no Option needed ---", + "tui.editor.cursorLeft": ["left", "ctrl+b"], + "tui.editor.cursorRight": ["right", "ctrl+f"], + "tui.editor.cursorUp": ["up"], + "tui.editor.cursorDown": ["down"], + "tui.editor.cursorWordLeft": ["alt+left", "ctrl+left"], + "tui.editor.cursorWordRight": ["alt+right", "ctrl+right"], + "tui.editor.cursorLineStart": ["home", "ctrl+a"], + "tui.editor.cursorLineEnd": ["end", "ctrl+e"], + + "//4": "--- deletion ---", + "tui.editor.deleteCharBackward": ["backspace"], + "tui.editor.deleteWordBackward": ["ctrl+w"], + "tui.editor.deleteToLineStart": ["ctrl+u"], + "tui.editor.deleteToLineEnd": ["ctrl+k"], + + "//5": "--- prompt history on ctrl+p / ctrl+n ---", + "//6": "These take precedence over model cycling while the editor is focused,", + "//7": "so ctrl+p still works inside selectors.", + "tui.editor.historyPrevious": "ctrl+p", + "tui.editor.historyNext": "ctrl+n", + + "//8": "--- model cycling, moved off the ctrl+p we just took ---", + "app.model.cycleForward": "ctrl+shift+l", + "app.model.cycleBackward": "ctrl+shift+h", + + "//9": "--- follow-up / dequeue: Pi defaults are alt+enter and alt+up.", + "//10": "Kept as-is because Option+Enter and Option+Up survive on macOS even", + "//11": "without Option-as-Meta. Add ctrl+shift fallbacks alongside them.", + "app.message.followUp": ["alt+enter", "ctrl+shift+j"], + "app.message.dequeue": ["alt+up", "ctrl+shift+k"], + + "//12": "--- session commands (OpenCode leader-key equivalents) ---", + "app.session.tree": "ctrl+shift+t", + "app.session.new": "ctrl+shift+n", + "app.session.resume": "ctrl+shift+r", + "app.session.fork": "ctrl+shift+f", + + "//13": "--- fullscreen transcript half-page scroll (vim ctrl+d/u feel) ---", + "tui.altScreen.halfPageDown": "ctrl+shift+d", + "tui.altScreen.halfPageUp": "ctrl+shift+u" +} diff --git a/configs/pi/settings.json b/configs/pi/settings.json new file mode 100644 index 0000000..f7fbd01 --- /dev/null +++ b/configs/pi/settings.json @@ -0,0 +1,43 @@ +{ + "theme": "Tokyo Night Storm", + "outputPad": 1, + "editorPaddingX": 1, + "autocompleteMaxVisible": 10, + "externalEditor": "nvim", + "defaultProvider": "openrouter", + "defaultModel": "openai/gpt-5.6-sol", + "defaultThinkingLevel": "high", + "compaction": { + "enabled": true, + "reserveTokens": 16384, + "keepRecentTokens": 20000 + }, + "retry": { + "enabled": true, + "maxRetries": 3, + "baseDelayMs": 2000, + "provider": { + "maxRetries": 0 + } + }, + "doubleEscapeAction": "tree", + "treeFilterMode": "default", + "branchSummary": { + "skipPrompt": false + }, + "defaultProjectTrust": "ask", + "tuiMode": "fullscreen", + "lastChangelogVersion": "0.84.4", + "packages": [ + "npm:pi-mcp-adapter", + "npm:pi-lens", + "npm:@juanibiapina/pi-powerbar", + "npm:pi-simplify", + "npm:@devkade/pi-plan", + "git:github.com/tmustier/pi-extensions", + "npm:@juanibiapina/pi-extension-settings" + ], + "markdown": { + "mermaid": "streaming" + } +} \ No newline at end of file diff --git a/configs/pi/themes/tokyonight_day.json b/configs/pi/themes/tokyonight_day.json new file mode 100644 index 0000000..16261bc --- /dev/null +++ b/configs/pi/themes/tokyonight_day.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "Tokyo Night Day", + "colors": { + "accent": "#2e7de9", + "border": "#4094a3", + "borderAccent": "#007197", + "borderMuted": "#b4b5b9", + "success": "#387068", + "error": "#c64343", + "warning": "#8c6c3e", + "muted": "#6172b0", + "dim": "#68709a", + "text": "", + "thinkingText": "#848cb5", + + "selectedBg": "#b7c1e3", + "userMessageBg": "#d0d5e3", + "userMessageText": "", + "customMessageBg": "#d0d5e3", + "customMessageText": "", + "customMessageLabel": "#7847bd", + "toolPendingBg": "#c4c8da", + "toolSuccessBg": "#c4c8da", + "toolErrorBg": "#e0dadf", + "toolTitle": "", + "toolOutput": "#6172b0", + + "mdHeading": "#2e7de9", + "mdLink": "#118c74", + "mdLinkUrl": "#848cb5", + "mdCode": "#2e7de9", + "mdCodeBlock": "#d0d5e3", + "mdCodeBlockBorder": "#848cb5", + "mdQuote": "#848cb5", + "mdQuoteBorder": "#848cb5", + "mdHr": "#b15c00", + "mdListBullet": "#b15c00", + + "toolDiffAdded": "#b7ced5", + "toolDiffRemoved": "#dababe", + "toolDiffContext": "#92a6d5", + + "syntaxComment": "#848cb5", + "syntaxKeyword": "#7847bd", + "syntaxFunction": "#2e7de9", + "syntaxVariable": "#3760bf", + "syntaxString": "#587539", + "syntaxNumber": "#b15c00", + "syntaxType": "#188092", + "syntaxOperator": "#006a83", + "syntaxPunctuation": "#006a83", + + "thinkingOff": "#8990b3", + "thinkingMinimal": "#68709a", + "thinkingLow": "#92a6d5", + "thinkingMedium": "#2e7de9", + "thinkingHigh": "#9854f1", + "thinkingXhigh": "#7847bd", + + "bashMode": "#b15c00" + } +} diff --git a/configs/pi/themes/tokyonight_moon.json b/configs/pi/themes/tokyonight_moon.json new file mode 100644 index 0000000..7f8e6fd --- /dev/null +++ b/configs/pi/themes/tokyonight_moon.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "Tokyo Night Moon", + "colors": { + "accent": "#82aaff", + "border": "#589ed7", + "borderAccent": "#86e1fc", + "borderMuted": "#1b1d2b", + "success": "#4fd6be", + "error": "#c53b53", + "warning": "#ffc777", + "muted": "#828bb8", + "dim": "#737aa2", + "text": "", + "thinkingText": "#636da6", + + "selectedBg": "#2d3f76", + "userMessageBg": "#1e2030", + "userMessageText": "", + "customMessageBg": "#1e2030", + "customMessageText": "", + "customMessageLabel": "#fca7ea", + "toolPendingBg": "#2f334d", + "toolSuccessBg": "#2f334d", + "toolErrorBg": "#2a2537", + "toolTitle": "", + "toolOutput": "#828bb8", + + "mdHeading": "#82aaff", + "mdLink": "#4fd6be", + "mdLinkUrl": "#636da6", + "mdCode": "#82aaff", + "mdCodeBlock": "#1e2030", + "mdCodeBlockBorder": "#636da6", + "mdQuote": "#636da6", + "mdQuoteBorder": "#636da6", + "mdHr": "#ff966c", + "mdListBullet": "#ff966c", + + "toolDiffAdded": "#2a4556", + "toolDiffRemoved": "#4b2a3d", + "toolDiffContext": "#394b70", + + "syntaxComment": "#636da6", + "syntaxKeyword": "#fca7ea", + "syntaxFunction": "#82aaff", + "syntaxVariable": "#c8d3f5", + "syntaxString": "#c3e88d", + "syntaxNumber": "#ff966c", + "syntaxType": "#65bcff", + "syntaxOperator": "#89ddff", + "syntaxPunctuation": "#89ddff", + + "thinkingOff": "#545c7e", + "thinkingMinimal": "#737aa2", + "thinkingLow": "#394b70", + "thinkingMedium": "#82aaff", + "thinkingHigh": "#c099ff", + "thinkingXhigh": "#fca7ea", + + "bashMode": "#ff966c" + } +} diff --git a/configs/pi/themes/tokyonight_night.json b/configs/pi/themes/tokyonight_night.json new file mode 100644 index 0000000..2c69c03 --- /dev/null +++ b/configs/pi/themes/tokyonight_night.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "Tokyo Night", + "colors": { + "accent": "#7aa2f7", + "border": "#27a1b9", + "borderAccent": "#7dcfff", + "borderMuted": "#15161e", + "success": "#73daca", + "error": "#db4b4b", + "warning": "#e0af68", + "muted": "#a9b1d6", + "dim": "#737aa2", + "text": "", + "thinkingText": "#565f89", + + "selectedBg": "#283457", + "userMessageBg": "#16161e", + "userMessageText": "", + "customMessageBg": "#16161e", + "customMessageText": "", + "customMessageLabel": "#9d7cd8", + "toolPendingBg": "#292e42", + "toolSuccessBg": "#292e42", + "toolErrorBg": "#241d28", + "toolTitle": "", + "toolOutput": "#a9b1d6", + + "mdHeading": "#7aa2f7", + "mdLink": "#1abc9c", + "mdLinkUrl": "#565f89", + "mdCode": "#7aa2f7", + "mdCodeBlock": "#16161e", + "mdCodeBlockBorder": "#565f89", + "mdQuote": "#565f89", + "mdQuoteBorder": "#565f89", + "mdHr": "#ff9e64", + "mdListBullet": "#ff9e64", + + "toolDiffAdded": "#243e4a", + "toolDiffRemoved": "#4a272f", + "toolDiffContext": "#394b70", + + "syntaxComment": "#565f89", + "syntaxKeyword": "#9d7cd8", + "syntaxFunction": "#7aa2f7", + "syntaxVariable": "#c0caf5", + "syntaxString": "#9ece6a", + "syntaxNumber": "#ff9e64", + "syntaxType": "#2ac3de", + "syntaxOperator": "#89ddff", + "syntaxPunctuation": "#89ddff", + + "thinkingOff": "#545c7e", + "thinkingMinimal": "#737aa2", + "thinkingLow": "#394b70", + "thinkingMedium": "#7aa2f7", + "thinkingHigh": "#bb9af7", + "thinkingXhigh": "#9d7cd8", + + "bashMode": "#ff9e64" + } +} diff --git a/configs/pi/themes/tokyonight_storm.json b/configs/pi/themes/tokyonight_storm.json new file mode 100644 index 0000000..b03caf7 --- /dev/null +++ b/configs/pi/themes/tokyonight_storm.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json", + "name": "Tokyo Night Storm", + "colors": { + "accent": "#7aa2f7", + "border": "#29a4bd", + "borderAccent": "#7dcfff", + "borderMuted": "#1d202f", + "success": "#73daca", + "error": "#db4b4b", + "warning": "#e0af68", + "muted": "#a9b1d6", + "dim": "#737aa2", + "text": "", + "thinkingText": "#565f89", + + "selectedBg": "#2e3c64", + "userMessageBg": "#1f2335", + "userMessageText": "", + "customMessageBg": "#1f2335", + "customMessageText": "", + "customMessageLabel": "#9d7cd8", + "toolPendingBg": "#292e42", + "toolSuccessBg": "#292e42", + "toolErrorBg": "#2d2a3c", + "toolTitle": "", + "toolOutput": "#a9b1d6", + + "mdHeading": "#7aa2f7", + "mdLink": "#1abc9c", + "mdLinkUrl": "#565f89", + "mdCode": "#7aa2f7", + "mdCodeBlock": "#1f2335", + "mdCodeBlockBorder": "#565f89", + "mdQuote": "#565f89", + "mdQuoteBorder": "#565f89", + "mdHr": "#ff9e64", + "mdListBullet": "#ff9e64", + + "toolDiffAdded": "#2b485a", + "toolDiffRemoved": "#52313f", + "toolDiffContext": "#394b70", + + "syntaxComment": "#565f89", + "syntaxKeyword": "#9d7cd8", + "syntaxFunction": "#7aa2f7", + "syntaxVariable": "#c0caf5", + "syntaxString": "#9ece6a", + "syntaxNumber": "#ff9e64", + "syntaxType": "#2ac3de", + "syntaxOperator": "#89ddff", + "syntaxPunctuation": "#89ddff", + + "thinkingOff": "#545c7e", + "thinkingMinimal": "#737aa2", + "thinkingLow": "#394b70", + "thinkingMedium": "#7aa2f7", + "thinkingHigh": "#bb9af7", + "thinkingXhigh": "#9d7cd8", + + "bashMode": "#ff9e64" + } +} diff --git a/scripts/pi-config.sh b/scripts/pi-config.sh new file mode 100755 index 0000000..26acb0b --- /dev/null +++ b/scripts/pi-config.sh @@ -0,0 +1,194 @@ +#!/bin/sh + +set -eu + +PI_SHA=${PI_SHA:?PI_SHA is required} +PI_REPO=${PI_REPO:-https://github.com/earendil-works/pi.git} +PI_CONFIG=${PI_CONFIG:?PI_CONFIG is required} +PI_AGENT=${PI_AGENT:-"$HOME/.pi/agent"} +PI_SRC=${PI_SRC:-"$HOME/.pi/src/pi"} +PI_UPSTREAM="$PI_SRC/packages/coding-agent/examples/extensions" + +UPSTREAM_FILES="git-checkpoint.ts permission-gate.ts protected-paths.ts status-line.ts todo.ts" +UPSTREAM_DIRS="plan-mode subagent" +CUSTOM_EXTENSIONS="vim-editor.ts" + +fail() { + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +link_managed() { + source_path=$1 + destination=$2 + + if [ -L "$destination" ]; then + current=$(readlink "$destination") + [ "$current" = "$source_path" ] || fail "$destination points to unexpected target $current." + elif [ -e "$destination" ]; then + fail "$destination already exists and was left unchanged." + else + ln -s "$source_path" "$destination" + printf 'linked %s\n' "$destination" + fi +} + +check_managed_link() { + source_path=$1 + destination=$2 + + [ -L "$destination" ] || fail "expected $destination to be a symlink." + [ "$(readlink "$destination")" = "$source_path" ] || fail "expected $destination -> $source_path." + [ -e "$destination" ] || fail "$destination is dangling." +} + +unlink_managed() { + source_path=$1 + destination=$2 + + if [ -L "$destination" ] && [ "$(readlink "$destination")" = "$source_path" ]; then + rm "$destination" + printf 'unlinked %s\n' "$destination" + fi +} + +check_config() { + [ -f "$PI_CONFIG/settings.json" ] || fail "settings.json is missing." + [ -f "$PI_CONFIG/keybindings.json" ] || fail "keybindings.json is missing." + [ -d "$PI_CONFIG/themes" ] || fail "themes are missing." + [ -f "$PI_CONFIG/extensions/vim-editor.ts" ] || fail "vim-editor.ts is missing." + + for name in $UPSTREAM_FILES $UPSTREAM_DIRS; do + [ ! -e "$PI_CONFIG/extensions/$name" ] || fail "upstream extension $name is vendored in configs/pi." + done + + links=$(find "$PI_CONFIG" -type l -print) + [ -z "$links" ] || fail "tracked pi configuration contains symlinks:\n$links" + + if grep -R -n -E '(/Users/|/home/|~/src/)' "$PI_CONFIG" --include='*.json' 2>/dev/null; then + fail "machine-specific paths found in configs/pi." + fi + + for path in auth.json trust.json sessions npm git models-store.json mcp-cache.json; do + [ ! -e "$PI_CONFIG/$path" ] || fail "runtime state found at configs/pi/$path." + done + + if grep -R -n -E '(sk-[A-Za-z0-9_-]{16,}|Bearer[[:space:]]+[A-Za-z0-9._-]{16,}|"(apiKey|api_key|accessToken|refreshToken)"[[:space:]]*:)' "$PI_CONFIG" 2>/dev/null; then + fail "possible credential material found in configs/pi." + fi + + printf 'configs/pi is portable and contains only custom configuration.\n' +} + +install_source() { + mkdir -p "$(dirname "$PI_SRC")" "$PI_AGENT" + + if [ ! -d "$PI_SRC/.git" ]; then + [ ! -e "$PI_SRC" ] || fail "$PI_SRC exists but is not a git checkout." + git clone --filter=blob:none --sparse "$PI_REPO" "$PI_SRC" + fi + + [ "$(git -C "$PI_SRC" remote get-url origin)" = "$PI_REPO" ] || fail "$PI_SRC has an unexpected origin." + git -C "$PI_SRC" sparse-checkout set packages/coding-agent/examples/extensions + git -C "$PI_SRC" fetch --quiet origin + git -C "$PI_SRC" cat-file -e "$PI_SHA^{commit}" 2>/dev/null || fail "revision $PI_SHA was not found in $PI_REPO." + git -C "$PI_SRC" checkout --quiet --detach --force "$PI_SHA" +} + +remove_legacy_links() { + for name in AGENTS.md prompts; do + destination="$PI_AGENT/$name" + legacy_source="$PI_CONFIG/$name" + if [ -L "$destination" ] && [ "$(readlink "$destination")" = "$legacy_source" ] && [ ! -e "$legacy_source" ]; then + rm "$destination" + printf 'removed legacy dangling link %s\n' "$destination" + fi + done +} + +prepare_extensions_directory() { + if [ -L "$PI_AGENT/extensions" ]; then + current=$(readlink "$PI_AGENT/extensions") + if [ "$current" = "$PI_CONFIG/extensions" ]; then + rm "$PI_AGENT/extensions" + else + fail "$PI_AGENT/extensions points to unexpected target $current." + fi + elif [ -e "$PI_AGENT/extensions" ] && [ ! -d "$PI_AGENT/extensions" ]; then + fail "$PI_AGENT/extensions exists but is not a directory." + fi + + mkdir -p "$PI_AGENT/extensions" +} + +install_links() { + link_managed "$PI_CONFIG/settings.json" "$PI_AGENT/settings.json" + link_managed "$PI_CONFIG/keybindings.json" "$PI_AGENT/keybindings.json" + link_managed "$PI_CONFIG/themes" "$PI_AGENT/themes" + + for name in $UPSTREAM_FILES $UPSTREAM_DIRS; do + link_managed "$PI_UPSTREAM/$name" "$PI_AGENT/extensions/$name" + done + + for name in $CUSTOM_EXTENSIONS; do + link_managed "$PI_CONFIG/extensions/$name" "$PI_AGENT/extensions/$name" + done +} + +verify_install() { + [ -d "$PI_SRC/.git" ] || fail "pi source checkout is missing." + [ "$(git -C "$PI_SRC" rev-parse HEAD)" = "$PI_SHA" ] || fail "pi source checkout is at the wrong revision." + + check_managed_link "$PI_CONFIG/settings.json" "$PI_AGENT/settings.json" + check_managed_link "$PI_CONFIG/keybindings.json" "$PI_AGENT/keybindings.json" + check_managed_link "$PI_CONFIG/themes" "$PI_AGENT/themes" + + for name in $UPSTREAM_FILES $UPSTREAM_DIRS; do + check_managed_link "$PI_UPSTREAM/$name" "$PI_AGENT/extensions/$name" + done + + for name in $CUSTOM_EXTENSIONS; do + check_managed_link "$PI_CONFIG/extensions/$name" "$PI_AGENT/extensions/$name" + done + + printf 'Pi checkout and configuration links verified.\n' +} + +remove_links() { + unlink_managed "$PI_CONFIG/settings.json" "$PI_AGENT/settings.json" + unlink_managed "$PI_CONFIG/keybindings.json" "$PI_AGENT/keybindings.json" + unlink_managed "$PI_CONFIG/themes" "$PI_AGENT/themes" + + for name in $UPSTREAM_FILES $UPSTREAM_DIRS; do + unlink_managed "$PI_UPSTREAM/$name" "$PI_AGENT/extensions/$name" + done + + for name in $CUSTOM_EXTENSIONS; do + unlink_managed "$PI_CONFIG/extensions/$name" "$PI_AGENT/extensions/$name" + done + + printf 'Managed links removed; machine-local runtime state was left unchanged.\n' +} + +case ${1:-} in +check) + check_config + ;; +install) + check_config + install_source + remove_legacy_links + prepare_extensions_directory + install_links + verify_install + ;; +verify) + verify_install + ;; +unlink) + remove_links + ;; +*) + fail "usage: $0 {check|install|verify|unlink}" + ;; +esac