From e4c7aa2245bdddd887e045da4243f40a4c714d18 Mon Sep 17 00:00:00 2001 From: ALX99 Date: Sat, 13 Jun 2026 23:35:29 +0900 Subject: [PATCH 001/366] fix(shell): keep space-prefixed commands in history Switch HISTCONTROL from ignoreboth to ignoredups and drop the [ ]* pattern from HISTIGNORE so commands starting with a space are still recorded. --- home/.bashrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/home/.bashrc b/home/.bashrc index 1e3ba97c..a9b5ae1a 100644 --- a/home/.bashrc +++ b/home/.bashrc @@ -12,10 +12,10 @@ case $- in esac # History -HISTIGNORE="&:[ ]*:exit:ls:bg:fg:history:clear" +HISTIGNORE="&:exit:ls:bg:fg:history:clear" HISTSIZE=-1 HISTFILESIZE=-1 -HISTCONTROL=ignoreboth +HISTCONTROL=ignoredups __prompt_is_unmerged_status() { case "$1" in From 42f9ca8a3107f9201f1cc42d6967a31758663a19 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:42:29 +0900 Subject: [PATCH 002/366] fix(binaries): iterate pacman -Qql output without word-splitting The unquoted `for res in $(pacman -Qql "$1")` split package file paths on whitespace, silently dropping entries whose path contained a space (e.g., `/usr/share/some thing`). Switch to a `pacman -Qql "$1" | while IFS= read -r res` loop, which is POSIX-clean (the script is `#!/bin/sh`) and preserves each path intact. --- .local/bin/binaries | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.local/bin/binaries b/.local/bin/binaries index 72b3f0da..b4335250 100755 --- a/.local/bin/binaries +++ b/.local/bin/binaries @@ -1,5 +1,5 @@ #!/bin/sh -for res in $(pacman -Qql "$1"); do +pacman -Qql "$1" | while IFS= read -r res; do [ ! -d "$res" ] && [ -x "$res" ] && echo "$res" done From 83d4fb4ce12bf098719354bfcb8283302cee71f3 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:42:37 +0900 Subject: [PATCH 003/366] fix(compress): pass positional args to tar as separate files Three coupled bugs in compress: 1. `PARAMS+="$1"` concatenated positional args into one string with no separator, so `compress a b` produced a single filename "ab". 2. `eval set -- "$PARAMS"` was dead code given the (then) string-based accumulator, and `tar -cvf archive.tar "$PARAMS"` passed the joined string as one tar argument. The archive was being created with a non-existent entry like "ab" and tar errored. 3. `nproc` is GNU coreutils; on macOS the command substitution was empty and `zstd -T""` failed. Convert PARAMS to a bash array, pass `"${PARAMS[@]}"` to tar, drop the dead eval, and add a `nproc 2>/dev/null || sysctl -n hw.ncpu` fallback so macOS gets the right thread count. --- .local/bin/compress | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.local/bin/compress b/.local/bin/compress index dd8a52a1..e96332ee 100755 --- a/.local/bin/compress +++ b/.local/bin/compress @@ -2,7 +2,7 @@ # Compress directory cLevel="3" -PARAMS="" +PARAMS=() while (("$#")); do case "$1" in -cl | --compression-level) @@ -18,13 +18,11 @@ while (("$#")); do exit 1 ;; *) # preserve positional arguments - PARAMS+="$1" + PARAMS+=("$1") shift ;; esac done -# set positional arguments in their proper place -eval set -- "$PARAMS" -tar -cvf archive.tar "$PARAMS" && - zstd -z --rm -"$cLevel" -T"$(nproc)" archive.tar -o archive.tar.zst +tar -cvf archive.tar "${PARAMS[@]}" && + zstd -z --rm -"$cLevel" -T"$(nproc 2>/dev/null || sysctl -n hw.ncpu)" archive.tar -o archive.tar.zst From 0a931ec5ccdacc0c495f8935fd7e77a80270d059 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:42:42 +0900 Subject: [PATCH 004/366] fix(fkill): use portable ps -p for SIGKILL escalation `ps --pid "$p"` is GNU-ps syntax. On macOS/BSD, `/bin/ps` rejects the long option ("illegal option -- -"), the error is swallowed by `&>/dev/null`, and the SIGKILL escalation in the trailing subshell never fires. `ps -p" works on both GNU and BSD ps. --- .local/bin/fkill | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.local/bin/fkill b/.local/bin/fkill index 962944b3..dfd3e31b 100755 --- a/.local/bin/fkill +++ b/.local/bin/fkill @@ -12,7 +12,7 @@ if [ -n "$pid" ]; then sleep 5 for p in $pid; do # Send SIGKILL - ps --pid "$p" &>/dev/null && kill -s 9 "$p" + ps -p "$p" &>/dev/null && kill -s 9 "$p" done ) & fi From 5ecf89694ba3d8bc6d2899521f7bd38a3667db0b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:42:50 +0900 Subject: [PATCH 005/366] fix(gruntest): don't abort on empty GO_TEST_FLAGS, use gxargs Two bugs in gruntest: 1. `printf "%s\\n" "${GO_TEST_FLAGS[@]}" | grep '\\S' >file` exits 1 when GO_TEST_FLAGS is empty (e.g. `gruntest --` or `gruntest -v --`, both reachable from the documented usage). With `set -euo pipefail` that kills the script before fzf is reached. Append `|| true` so the empty-flag case becomes a no-op. 2. `xargs -d '\\n'` is GNU-only; BSD xargs on macOS aborts with `xargs: invalid option -- d` after fzf-tmux. Use `gxargs` (the GNU xargs from brew findutils, already on PATH per the dotfiles setup). --- .local/bin/gruntest | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.local/bin/gruntest b/.local/bin/gruntest index 771931a7..9501c818 100755 --- a/.local/bin/gruntest +++ b/.local/bin/gruntest @@ -24,7 +24,7 @@ set_go_test_flags() { shift done GO_TEST_FLAGS=("$@") - printf "%s\n" "${GO_TEST_FLAGS[@]}" | grep '\S' >"$GO_TEST_FLAGS_FILE" + printf "%s\n" "${GO_TEST_FLAGS[@]}" | grep '\S' >"$GO_TEST_FLAGS_FILE" || true else touch "$GO_TEST_FLAGS_FILE" fi @@ -98,7 +98,7 @@ handle_subcommand() { jq -re 'select(.Action == "output" and (.Output | startswith("T"))) | .Package + " " + .Output' | sed '/^$/d' | fzf-tmux --multi --preview \ "echo -e '${WHITE}{2}${RESET} ${GREY}{1}${RESET}'" --preview-window=down:3:wrap --delimiter=" " --ansi --with-nth=2 --bind 'ctrl-a:toggle-all' | - tee "$LAST_TESTS_FILE" | xargs -d '\n' bash -c 'run_tests "$@"' _ + tee "$LAST_TESTS_FILE" | gxargs -d '\n' bash -c 'run_tests "$@"' _ return ;; esac From 25d57247a512c46201ef1fe2248ded373055c6c7 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:42:55 +0900 Subject: [PATCH 006/366] fix(toggle-power-profile): cycle through power-saver state The case statement only recognized 'balanced' and 'performance', with a `*` default that always set target=balanced. The third valid power-profiles-daemon state, 'power-saver', was never reachable from this script, despite the script being named 'toggle-power-profile'. Users starting in 'power-saver' would be unable to return to it via toggle. Implement a proper 3-state cycle: performance -> balanced -> power-saver -> performance. The default branch now targets 'performance' (the first state in the cycle) for any unrecognized current state. The state strings are inlined as literals because bash identifiers cannot contain hyphens (`readonly power-saver=...` is a syntax error). --- .local/bin/toggle-power-profile | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.local/bin/toggle-power-profile b/.local/bin/toggle-power-profile index f2955df8..8c8c868d 100755 --- a/.local/bin/toggle-power-profile +++ b/.local/bin/toggle-power-profile @@ -18,15 +18,10 @@ set_governor() { current="$(powerprofilesctl get)" case "$current" in -"$balanced") - target="$performance" - ;; -"$performance") - target="$balanced" - ;; -*) - target="$balanced" - ;; +"$performance") target="$balanced" ;; +"$balanced") target="power-saver" ;; +"power-saver") target="$performance" ;; +*) target="$performance" ;; esac # power-profiles-daemon controls Intel EPP and can fail if the cpufreq From 8ba92727e981ef72bdf65a279be72fbc732508e4 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:43:02 +0900 Subject: [PATCH 007/366] fix(unarchive): handle dot-only basenames (e.g. .tar.gz) When the input file's basename is purely an extension (e.g. `.tar.gz`, `.tar`, `.gz`, `.tar.bz2`), the case-stripping logic produced an empty `name` string. `choose_dest` then ran `mkdir -p ""` and `tar xf ... -C ""`, both of which fail: unarchive: line 21: [: : integer expected mkdir: : No such file or directory tar: Meaningless option: -C '' Guard the `name` expansion in choose_dest's else-branch with `${name:-.}` so any archive whose basename is just an extension extracts into the current directory. --- .local/bin/unarchive | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.local/bin/unarchive b/.local/bin/unarchive index 79cf54d9..7efd7081 100755 --- a/.local/bin/unarchive +++ b/.local/bin/unarchive @@ -21,8 +21,8 @@ choose_dest() { if [ "$count" -le 1 ]; then printf '.' else - mkdir -p "$name" - printf '%s' "$name" + mkdir -p "${name:-.}" + printf '%s' "${name:-.}" fi } From 3268c7c00f529cd1eabbb67ec41c0290c0d4f613 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:43:09 +0900 Subject: [PATCH 008/366] fix(_projselect): quote $PROJ_DIR in fzf preview string The fzf `--preview` argument contains `git -C $PROJ_DIR/{}`. With the default `PROJ_DIR=$HOME/projects` (no spaces) this works, but the moment PROJ_DIR contains whitespace, bash word-splits the unquoted variable when constructing the preview string and the resulting command is malformed (`git -C '/Users/Alice/My' 'Projects/foo' log ...`). Quote the expansion in both the Darwin and Linux branches using escaped double quotes inside the outer fzf string, so the path is a single token when fzf later runs the preview via `sh -c`. --- .local/bin/_projselect | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.local/bin/_projselect b/.local/bin/_projselect index 689b1111..ef9ae561 100755 --- a/.local/bin/_projselect +++ b/.local/bin/_projselect @@ -8,10 +8,10 @@ if [ "$(uname -s)" = "Darwin" ]; then printf "$PROJ_DIR/%s\n" \ "$(gfind "$PROJ_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | fzf --preview-window='up,60%' \ - --preview "git -C $PROJ_DIR/{} log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" + --preview "git -C \"$PROJ_DIR/{}\" log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" else printf "$PROJ_DIR/%s\n" \ "$(find "$PROJ_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | fzf --preview-window='up,60%' \ - --preview "git -C $PROJ_DIR/{} log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" + --preview "git -C \"$PROJ_DIR/{}\" log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" fi From e08a681d4a42d255bdfa188fefc6a460c2be3ddd Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:43:16 +0900 Subject: [PATCH 009/366] fix(tmux-history): stop leaking the mktemp-created file `file="$(mktemp -t tmux-history.XXXXXX).log"` appended `.log` AFTER mktemp returned, so the mktemp-created file (e.g. `/tmp/tmux-history.AbCdEf`) was never tracked and never cleaned up. Only the appended `.log` file (created by the later `> "$file"`) was removed at the end of the script, leaving the original mktemp file orphaned on every run. Move `.log` into the mktemp template (`tmux-history.XXXXXX.log`) so mktemp creates the `.log` file directly. The existing `rm -f "$file"` cleanup and the nvim `rm -f '$file'` at the end of the new-window command then operate on the actual file. No leak. --- .local/bin/tmux-history | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.local/bin/tmux-history b/.local/bin/tmux-history index 174e2140..78937eea 100755 --- a/.local/bin/tmux-history +++ b/.local/bin/tmux-history @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -file="$(mktemp -t tmux-history.XXXXXX).log" +file="$(mktemp -t tmux-history.XXXXXX.log)" cleanup() { rm -f "$file"; } trap cleanup EXIT From 81254453ab6bad60bac3285c8c977d52ca728548 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:52:19 +0900 Subject: [PATCH 010/366] fix(gruntest): use GNU xargs on Darwin, system xargs on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix replaced `xargs` with `gxargs` to address the BSD-only `xargs -d` limitation on macOS, but `gxargs` only ships in Homebrew's findutils — it isn't on standard Linux installs. That swap fixed macOS and broke Linux. Wrap `xargs` in a small helper inside `handle_subcommand` that picks the GNU xargs (gxargs on Darwin via brew findutils, system xargs on Linux where it is already GNU). Matches the gfind/find split in _projselect. The `xargs -d` call site is unchanged. --- .local/bin/gruntest | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.local/bin/gruntest b/.local/bin/gruntest index 9501c818..73acbf08 100755 --- a/.local/bin/gruntest +++ b/.local/bin/gruntest @@ -74,6 +74,16 @@ handle_subcommand() { if [[ $1 != "--" ]]; then shift fi + # xargs -d is GNU-only. On Darwin, use the GNU xargs from brew findutils + # (gxargs); on Linux, use the system xargs (which is GNU). Matches the + # gfind/find split in _projselect. + xargs() { + if [ "$(uname -s)" = "Darwin" ] && command -v gxargs >/dev/null 2>&1; then + gxargs "$@" + else + command xargs "$@" + fi + } case "$subcommand" in rerun) if [[ ! -r $LAST_TESTS_FILE ]]; then @@ -98,7 +108,7 @@ handle_subcommand() { jq -re 'select(.Action == "output" and (.Output | startswith("T"))) | .Package + " " + .Output' | sed '/^$/d' | fzf-tmux --multi --preview \ "echo -e '${WHITE}{2}${RESET} ${GREY}{1}${RESET}'" --preview-window=down:3:wrap --delimiter=" " --ansi --with-nth=2 --bind 'ctrl-a:toggle-all' | - tee "$LAST_TESTS_FILE" | gxargs -d '\n' bash -c 'run_tests "$@"' _ + tee "$LAST_TESTS_FILE" | xargs -d '\n' bash -c 'run_tests "$@"' _ return ;; esac From bcd4ebe142ba063d14f95b763f28996bd0db9466 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:23 +0900 Subject: [PATCH 011/366] fix(bashrc): avoid re-sourcing .venv/bin/activate on every prompt PROMPT_COMMAND runs before every prompt. Guard the source with $VIRTUAL_ENV so the venv is activated at most once per shell, not on every prompt. --- home/.bashrc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/home/.bashrc b/home/.bashrc index a9b5ae1a..c6f08f9e 100644 --- a/home/.bashrc +++ b/home/.bashrc @@ -123,7 +123,11 @@ __prompt_render() { __prompt_command() { local exit_status=$? history -a - [[ -r .venv/bin/activate ]] && . .venv/bin/activate + # Activate project's venv once per shell; guard with $VIRTUAL_ENV to avoid re-sourcing. + if [[ -z ${VIRTUAL_ENV:-} && -r .venv/bin/activate ]]; then + # shellcheck disable=SC1091 + . .venv/bin/activate + fi __prompt_render "$exit_status" } From e8e7e9c686ee0680ed053d2248456376823c123b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:24 +0900 Subject: [PATCH 012/366] fix(wific): exit cleanly when whiptail menu is canceled Previously, if the user pressed Esc in the device or network picker, whiptail returned an empty selection and the script fell through with empty $device/$network, causing a confusing iwctl error. Now both selectors return non-zero on cancel, and the top-level flow bails out. --- .local/bin/wific | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.local/bin/wific b/.local/bin/wific index f1303841..030fc2e0 100755 --- a/.local/bin/wific +++ b/.local/bin/wific @@ -12,6 +12,9 @@ select_device() { devices="$(iwctl device list | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR>4 {print $1}' | grep --color=never '\S')" # shellcheck disable=SC2046 device=$(whiptail --title "Select Device" --menu "Choose a device" 15 60 4 $(echo "$devices" | awk '{print NR, $1}') 3>&1 1>&2 2>&3) + if [ -z "$device" ]; then + return 1 + fi device="$(echo "$devices" | sed -n "${device}p")" } @@ -23,12 +26,15 @@ select_network() { networks="$(iwctl station "$device" get-networks | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR>4 {print $1}' | sed 's/^>//' | grep --color=never '\S')" # shellcheck disable=SC2046 network=$(whiptail --title "Select Network" --menu "Choose a network" 15 60 8 $(echo "$networks" | awk '{print NR, $1}') 3>&1 1>&2 2>&3) + if [ -z "$network" ]; then + return 1 + fi network="$(echo "$networks" | sed -n "${network}p")" } # Main script execution -select_device -select_network +select_device || exit 0 +select_network || exit 0 set -x iwctl station "$device" connect "$network" set +x From 5b73b3a23e6e81ee40582d6d6a3119f04bf2f493 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:25 +0900 Subject: [PATCH 013/366] fix(gitalias): gbs: return 0 when fzf branch picker is canceled Previously, cancelling the picker left $b empty and the function ran "git switch \"\"" with an empty arg, producing a confusing error. Now it returns 0 cleanly on cancel. --- home/.gitalias | 1 + 1 file changed, 1 insertion(+) diff --git a/home/.gitalias b/home/.gitalias index b3a849a5..4916b856 100644 --- a/home/.gitalias +++ b/home/.gitalias @@ -11,6 +11,7 @@ gbs() { bs="$(git --no-pager branch -vv | grep -v '^\*')" [ "$(echo "$bs" | wc -l)" -le 0 ] && return b="$(echo "$bs" | fzf-tmux -p +m)" + [ -z "$b" ] && return 0 git switch "$(echo "$b" | awk '{print $1}')" } From beeea1c04854c5e70d5cf47babc8b47701fca3f3 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:26 +0900 Subject: [PATCH 014/366] fix(gitalias): gbd: return 0 on cancel, drop dead sed The trailing "sed s/.* //" in the delete pipeline was a no-op for typical branch names but could garble output for any branch name containing a space. Removed. Also add the same empty-selection guard the sibling gbs function gained, so cancelling the picker returns 0 instead of silently doing nothing (and being indistinguishable from a no-op delete). --- home/.gitalias | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/home/.gitalias b/home/.gitalias index 4916b856..0ee9cae4 100644 --- a/home/.gitalias +++ b/home/.gitalias @@ -33,7 +33,8 @@ gbd() { bs="$(git --no-pager branch -vv | awk -v default_branch="$default_branch" '{ branch = $1; if (branch == "*" || branch == default_branch) next; print }')" [ "$(echo "$bs" | wc -l)" -le 0 ] && return b="$(echo "$bs" | fzf-tmux -p -m)" - echo "$b" | awk '{print $1}' | grep -v "\*" | sed "s/.* //" | xargs -I{} git branch -D '{}' + [ -z "$b" ] && return 0 + echo "$b" | awk '{print $1}' | grep -v "\*" | xargs -I{} git branch -D '{}' } From 91a9a149da3f2e2486660899cb34fd34f118be3d Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:26 +0900 Subject: [PATCH 015/366] fix(priv): exit with a clear message on non-Linux hosts The script writes to /proc/sys and runs iptables, both Linux-only. On macOS the failure was a confusing "No such file or directory" from the redirect. Gate early with a uname check. --- .local/bin/priv | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.local/bin/priv b/.local/bin/priv index b887db20..6fb21e22 100755 --- a/.local/bin/priv +++ b/.local/bin/priv @@ -1,4 +1,10 @@ #!/bin/sh +set -u + +if [ "$(uname -s)" != "Linux" ]; then + printf 'priv: Linux-only (requires /proc/sys and iptables)\n' >&2 + exit 0 +fi # Ignore all ICMP echo echo "1" >/proc/sys/net/ipv4/icmp_echo_ignore_all From 74f19bee62c2d5f399c8f3d7004514f5263e3f8c Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:27 +0900 Subject: [PATCH 016/366] fix(makepkg.conf): replace bogus shebang #!/hint/bash with #!/bin/bash --- .config/pacman/makepkg.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/pacman/makepkg.conf b/.config/pacman/makepkg.conf index 5b41f03b..85f8b88f 100644 --- a/.config/pacman/makepkg.conf +++ b/.config/pacman/makepkg.conf @@ -1,4 +1,4 @@ -#!/hint/bash +#!/bin/bash # shellcheck disable=2034 # Use all CPU threads for makepkg's own parallel tasks and make-based builds. From 3c8a3cfc4108eade33e632c2a6d909533e128629 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:28 +0900 Subject: [PATCH 017/366] fix(tmux/status): call pmset once per get_bat invocation on macOS Previously the Darwin branch spawned pmset -g batt twice: once for percentage, once for status. Capture the output once and derive both fields from it. Also avoids any race where the two snapshots could disagree. --- .config/tmux/status | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.config/tmux/status b/.config/tmux/status index d1b30d67..f6fb8cdf 100755 --- a/.config/tmux/status +++ b/.config/tmux/status @@ -25,8 +25,10 @@ get_bat() { fi ;; Darwin) - percentage="$(pmset -g batt | grep -Eo "\d+%" | cut -d% -f1)" - status="$(pmset -g batt | grep -qi 'charging' && echo 'Charging' || true)" + local pmset_output + pmset_output="$(pmset -g batt)" + percentage="$(printf '%s\n' "$pmset_output" | grep -Eo "\d+%" | cut -d% -f1)" + status="$(printf '%s\n' "$pmset_output" | grep -qi 'charging' && echo 'Charging' || true)" ;; *) return From 6169180aeb62fdd9f8081227a0e4da97457ebcb4 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 14:42:29 +0900 Subject: [PATCH 018/366] fix(nvim/lsp): replace placeholder "????????" with a real message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LspAttach fires with a client_id; if vim.lsp.get_client_by_id returns nil, the client detached between event and handler. "????????" was a TODO marker — replace with a meaningful INFO message that includes the client_id so the user can correlate with :LspInfo. Also demote WARN to INFO: this is an internal race, not user-actionable. --- .config/nvim/plugin/40_lsp_behavior.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 56037cc5..fe30a7e1 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -167,7 +167,7 @@ _G.Config.new_autocmd('LspAttach', { local client = vim.lsp.get_client_by_id(args.data.client_id) if not client then - vim.notify("????????", vim.log.levels.WARN) + vim.notify("LspAttach: client " .. args.data.client_id .. " not found", vim.log.levels.INFO) return end From 7948a2c8bbd6bf48c0eeabfe992fa014993e30e6 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:03:35 +0900 Subject: [PATCH 019/366] fix(nvim/lsp/pyright): use before_init instead of dead on_new_config hook on_new_config is silently dropped by nvim 0.12's vim.lsp.Config schema; it only fired under the legacy lspconfig..setup() flow, which this config does not use (plugin/78_lsp.lua uses vim.lsp.enable()). The venv hint was therefore never applied. before_init(_, config) is the 0.12 replacement: it fires under vim.lsp.enable() and config.root_dir is already resolved by the time it runs. Same fields, just migrated to the new hook. --- .config/nvim/after/lsp/pyright.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.config/nvim/after/lsp/pyright.lua b/.config/nvim/after/lsp/pyright.lua index 86c4637d..576877e5 100644 --- a/.config/nvim/after/lsp/pyright.lua +++ b/.config/nvim/after/lsp/pyright.lua @@ -1,13 +1,13 @@ -local function use_project_venv(config, root_dir) - if not root_dir then return end +local function use_project_venv(_, config) + if not config.root_dir then return end - local python = root_dir .. '/.venv/bin/python' + local python = config.root_dir .. '/.venv/bin/python' if vim.fn.executable(python) ~= 1 then return end config.settings = config.settings or {} config.settings.python = config.settings.python or {} config.settings.python.pythonPath = python - config.settings.python.venvPath = root_dir + config.settings.python.venvPath = config.root_dir config.settings.python.venv = '.venv' end @@ -21,5 +21,5 @@ return { }, }, }, - on_new_config = use_project_venv, + before_init = use_project_venv, } From ce6eb67bc85c37f4f1eb62dbde46ab4324046865 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:06:32 +0900 Subject: [PATCH 020/366] fix(nvim/lsp): preserve LSP severity in window/showMessage handler The custom handler hardcoded vim.log.levels.INFO for every LSP message, silently downgrading errors and warnings to info. Map result.type (1=Error, 2=Warning, 3=Info, 4=Log) to the matching vim.log.levels value, with INFO as a fallback for unknown types. --- .config/nvim/plugin/78_lsp.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index 820e4211..57396ee9 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -17,7 +17,8 @@ vim.lsp.handlers["window/showMessage"] = function(err, result, ctx) local msg = "[LSP]" if client then msg = msg .. " [" .. client.name .. "] " end if result and result.message then - vim.notify(msg .. result.message, vim.log.levels.INFO) + local level = ({ [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG })[result.type] or vim.log.levels.INFO + vim.notify(msg .. result.message, level) end end From 64c0dc2253dba04e9a2dde9ff36fc839408c9dd5 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:06:39 +0900 Subject: [PATCH 021/366] fix(nvim/ai): escape embedded quotes in quote_path, use stowed path for tmux popup quote_path wrapped a path in "..." but did not escape embedded double quotes, so a file named e.g. 'foo"bar' would produce the malformed '"foo"bar"' (known follow-up from the prior audit). Prefer single quotes for the common case, and fall back to double quotes with escaped \" only when the path contains a single quote. focus_popup hardcoded '~/dotfiles/.config/tmux/session-popup' instead of the stowed '~/.config/tmux/session-popup'. The dotfile path leaks the repo layout into runtime; AGENTS.md says .config/ is stowed to ~/.config/. --- .config/nvim/lua/custom/ai.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.config/nvim/lua/custom/ai.lua b/.config/nvim/lua/custom/ai.lua index b52fa9e3..b58e4e52 100644 --- a/.config/nvim/lua/custom/ai.lua +++ b/.config/nvim/lua/custom/ai.lua @@ -5,8 +5,11 @@ local function tmux(args, input) return result.code == 0, vim.trim(result.stdout or ""), vim.trim(result.stderr or "") end +-- Prefer single quotes; fall back to double quotes with \" escapes for paths containing ' local function quote_path(path) - return path:find("[^%w/_%.%-]") and ('"' .. path .. '"') or path + if not path:find("[^%w/_%.%-]") then return path end + if path:find("'") then return '"' .. path:gsub('"', '\\"') .. '"' end + return "'" .. path .. "'" end local function current_session() @@ -37,7 +40,7 @@ local function send_to_pane(pane_id, text) end local function focus_popup(tool) - local popup = vim.fn.expand("~/dotfiles/.config/tmux/session-popup") + local popup = vim.fn.expand("~/.config/tmux/session-popup") local job = vim.fn.jobstart({ "tmux", "display-popup", "-T", tool, "-w", "95%", "-h", "95%", "-E", popup, tool }, { detach = true, }) From 8fa3b8e8677db1322138c19e099f325fc6b840ee Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:08:31 +0900 Subject: [PATCH 022/366] fix(nvim/autocmds): keep shfmt autocmd alive when binary is missing The callback returned true on a missing shfmt, which per :h nvim_create_autocmd deletes the autocommand. The comment 'delete the autocmd' shows the intent was 'skip this save', not 'permakill the handler'. Replacing return true with bare return preserves the skip-this-save semantics and keeps the autocmd alive for future saves (e.g. after the user installs shfmt mid-session). --- .config/nvim/plugin/30_autocmds.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/30_autocmds.lua b/.config/nvim/plugin/30_autocmds.lua index 85479bc7..88361f66 100644 --- a/.config/nvim/plugin/30_autocmds.lua +++ b/.config/nvim/plugin/30_autocmds.lua @@ -51,7 +51,7 @@ _G.Config.new_autocmd("BufWritePre", { callback = function(info) if vim.bo[info.buf].filetype == "sh" then if vim.fn.executable('shfmt') ~= 1 then - return true -- delete the autocmd + return end local original_lines = vim.api.nvim_buf_get_lines(info.buf, 0, -1, true) From 289eabd2d1e20b5671987706b1234bd7b19052e9 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:08:40 +0900 Subject: [PATCH 023/366] fix(nvim/keymaps): use vim.fs.relpath in CopyPath for files outside cwd The old path:sub(#cwd + 2) only worked when the file was inside cwd; for a file outside (e.g. cwd=~/dotfiles, file=/tmp/foo.lua) the +2 slack dropped one char and produced a nonsense substring like 'tmp/foo.lua:1'. Use vim.fs.relpath and fall back to the absolute path when the file is not under cwd. --- .config/nvim/plugin/20_keymaps.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 2e134757..0d639984 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -73,10 +73,11 @@ if not vim.g.vscode then end vim.api.nvim_create_user_command("CopyPath", function() - local path = vim.fn.expand("%:p") + local file = vim.fn.expand("%:p") local cwd = vim.fn.getcwd() - path = path:sub(#cwd + 2) .. ":" .. vim.fn.line(".") - vim.fn.setreg("+", path) + local rel = vim.fs.relpath(cwd, file) + local display = (rel and rel ~= "") and rel or file + vim.fn.setreg("+", display .. ":" .. vim.fn.line(".")) end, {}) -- Copy text to clipboard using codeblock format ```{ft}{content}``` From b9b953927246512dadf2fd41cf5afa95a9b9bb94 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:08:52 +0900 Subject: [PATCH 024/366] fix(nvim/lsp/gopls): drop top-level staticcheck, keep analyses.staticcheck gopls v0.16 deprecated the top-level gopls.staticcheck boolean in favor of gopls.analyses.staticcheck. Having both set to true was redundant; keeping the top-level form would emit a deprecation warning under newer gopls. Delete line 17, keep line 33. --- .config/nvim/after/lsp/gopls.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/.config/nvim/after/lsp/gopls.lua b/.config/nvim/after/lsp/gopls.lua index ff112d26..f8742da4 100644 --- a/.config/nvim/after/lsp/gopls.lua +++ b/.config/nvim/after/lsp/gopls.lua @@ -14,7 +14,6 @@ return { }, fileWatcher = "poll", gofumpt = true, - staticcheck = true, usePlaceholders = false, semanticTokens = true, directoryFilters = { "-.git", "-.vscode", "-.idea", "-.vscode-test", "-node_modules" }, From 3ce0923c5ccc87661d28f1735ab6b2ae2878bbb2 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:12:28 +0900 Subject: [PATCH 025/366] fix(nvim/lsp): guard LspDetach handlers with client_id, clear setup flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous handlers had once=true and no client_id filter, so when two clients were attached to the same buffer the first detaching client's handler fired for everyone — it removed the per-buffer augroup and the handler itself, silently disabling highlight / diagnostic-float for every remaining client. The vim.b[buf].lsp_*_setup flags were also never cleared on detach, so once a client had attached, the feature could never re-register on the same buffer even after all clients detached. Filter LspDetach by ev.data.client_id == client.id, clear the guard flag, and return true so once actually fires. Thread the client arg into show_diagnostics which previously had no client closure. --- .config/nvim/plugin/40_lsp_behavior.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index fe30a7e1..8ba7ca6d 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -123,15 +123,19 @@ local function highlight_references(client, buf) group = UserLspConfig, buffer = buf, once = true, - callback = function() + callback = function(ev) + if not (ev.data and ev.data.client_id == client.id) then return end vim.lsp.buf.clear_references() vim.api.nvim_del_augroup_by_name('lsp-highlight-' .. buf) + vim.b[buf].lsp_highlight_setup = nil + return true end, }) end +---@param client vim.lsp.Client ---@param buf number -local function show_diagnostics(buf) +local function show_diagnostics(client, buf) if vim.b[buf].lsp_diagnostics_float_setup then return end vim.b[buf].lsp_diagnostics_float_setup = true @@ -156,8 +160,11 @@ local function show_diagnostics(buf) group = UserLspConfig, buffer = buf, once = true, - callback = function() + callback = function(ev) + if not (ev.data and ev.data.client_id == client.id) then return end vim.api.nvim_del_augroup_by_name('lsp-diag-hold-' .. buf) + vim.b[buf].lsp_diagnostics_float_setup = nil + return true end, }) end @@ -183,7 +190,7 @@ _G.Config.new_autocmd('LspAttach', { mappings(client, args.buf) highlight_references(client, args.buf) - show_diagnostics(args.buf) + show_diagnostics(client, args.buf) end, group = UserLspConfig, }) From 0c92e0428da2fb52e6c9c0bbcfd4134ada6528ef Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:12:41 +0900 Subject: [PATCH 026/366] fix(nvim/ui.open): use absolute-path check, not '/' + word char The old heuristic path:match('/%w') was meant to detect absolute filesystem paths like /Users/foo but also matched 'q/foo', 'hello/world', and any other slash + word char sequence. The latter got forwarded to vim.ui.open as a literal filename, which silently failed or opened a non-existent file. Use ^/ or ^~ as the path test (project targets macOS + Linux only, per AGENTS.md, so a Windows branch is not needed). Search terms without a leading slash or ~ now fall through to the GitHub-URL or Google-search branch. --- .config/nvim/plugin/10_opts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index a6782249..1bd2f894 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -126,7 +126,7 @@ vim.ui.open = (function(overridden) local is_uri = path:match('%w+:') local is_half_url = path:match('%.com$') local is_repo = vim.bo.filetype == 'lua' and path:match('%w/%w') and vim.fn.count(path, '/') == 1 - local is_dir = path:match('/%w') + local is_dir = path:match('^/') or path:match('^~') if not is_uri then if is_half_url then path = ('https://%s'):format(path) From aeac348225c2f014b5c0112e96961d198027ea26 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:12:51 +0900 Subject: [PATCH 027/366] fix(nvim/lsp): scope format-on-save augroup per-client, not per-buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The augroup 'lsp.format.' was shared across all LSP clients attached to the same buffer. The LspDetach handler did filter by client.id, but the del_augroup_by_name call removed the BufWritePre for every other client on that buffer — when one client detached, every other client's format-on-save silently disappeared. Scope the augroup per-client: 'lsp.format..'. The BufWritePre still fires per buffer (so format-on-save still works for any client that registers it); the LspDetach delete now only removes that single client's registration. --- .config/nvim/plugin/41_lsp_format.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 0eb3c77a..74398ce2 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -95,7 +95,7 @@ local function organize_go_imports(buf, client) end local function set_format_on_save(buf, client, callback) - local group = vim.api.nvim_create_augroup('lsp.format.' .. buf, { clear = true }) + local group = vim.api.nvim_create_augroup('lsp.format.' .. buf .. '.' .. client.id, { clear = true }) _G.Config.new_autocmd('BufWritePre', { group = group, buffer = buf, @@ -108,7 +108,7 @@ local function set_format_on_save(buf, client, callback) buffer = buf, callback = function(ev) if ev.data and ev.data.client_id == client.id then - vim.api.nvim_del_augroup_by_name('lsp.format.' .. buf) + vim.api.nvim_del_augroup_by_name('lsp.format.' .. buf .. '.' .. client.id) return true end end, From 84cfc2151da85f6c9f062637ac5986d1f7d1fef4 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:12:52 +0900 Subject: [PATCH 028/366] fix(nvim/autocmds): bail out of update_lead when listchars.space is unset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vim.fn.str2list(nil) returns the bytes ':null' (verified: {118,58,110,117,108,108}). When the user has neither listchars.space nor listchars.multispace set (which is the case in the actual 10_opts.lua config — both lines are commented out), the old code str2listed nil and produced a lead table with garbage bytes, rendering the multi-space leader as e.g. '>:nu'. Bail out early when space_src is nil or empty. The user can still enable leadmultispace by setting listchars.space or .multispace. --- .config/nvim/plugin/30_autocmds.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/30_autocmds.lua b/.config/nvim/plugin/30_autocmds.lua index 88361f66..5db0034d 100644 --- a/.config/nvim/plugin/30_autocmds.lua +++ b/.config/nvim/plugin/30_autocmds.lua @@ -77,8 +77,10 @@ _G.Config.new_autocmd("BufWritePre", { -- https://www.reddit.com/r/neovim/comments/17aponn/comment/k5f2n7t/?utm_source=share&utm_medium=web2x&context=3 local function update_lead() local lcs = vim.opt_local.listchars:get() + local space_src = lcs.multispace or lcs.space + if not space_src or space_src == "" then return end local tab = vim.fn.str2list(lcs.tab) - local space = vim.fn.str2list(lcs.multispace or lcs.space) + local space = vim.fn.str2list(space_src) local lead = { tab[1] } for i = 1, vim.bo.tabstop - 1 do lead[#lead + 1] = space[i % #space + 1] From b38b495c85b4701c9089e25dbfcb5720139eb06b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:14:09 +0900 Subject: [PATCH 029/366] fix(nvim/vscode): make zta close the active editor, not notify The body was vscode.notify('Close Active Editor'), which just shows the string as a notification. The desc field makes the intent clear: this should have called vscode.action('workbench.action.closeActiveEditor'), matching the style of every other mapping in the file. --- .config/nvim/plugin/999_vscode.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/999_vscode.lua b/.config/nvim/plugin/999_vscode.lua index 0cdd61a4..44d1cb62 100644 --- a/.config/nvim/plugin/999_vscode.lua +++ b/.config/nvim/plugin/999_vscode.lua @@ -21,7 +21,7 @@ map("n", "!", function() end, { desc = "Reload Window" }) map("n", "zta", function() - vscode.notify("Close Active Editor") + vscode.action("workbench.action.closeActiveEditor") end, { desc = "Close Active Editor" }) -- Window navigation From 2ad0ecf2f3e05412e7d76de6a559ec8d3ff3dba5 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:14:11 +0900 Subject: [PATCH 030/366] fix(nvim/lsp/lua_ls): guard against empty workspace_folders table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing if client.workspace_folders then guard does not catch an empty table — {} is truthy in Lua, so client.workspace_folders[1] is nil and the next line errors on nil.name. In practice nvim always provides at least one workspace folder, so this is a defensive fix. Tighten the condition to also check the table is non-empty. --- .config/nvim/after/lsp/lua_ls.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/after/lsp/lua_ls.lua b/.config/nvim/after/lsp/lua_ls.lua index 76114636..c24721c7 100644 --- a/.config/nvim/after/lsp/lua_ls.lua +++ b/.config/nvim/after/lsp/lua_ls.lua @@ -1,7 +1,7 @@ ---@type lspconfig.settings.lua_ls return { on_init = function(client) - if client.workspace_folders then + if client.workspace_folders and client.workspace_folders[1] then local path = client.workspace_folders[1].name if path ~= vim.fn.stdpath('config') From 530725cb14d68d94aa9a24231311c9f1386a8fb4 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:14:12 +0900 Subject: [PATCH 031/366] fix(nvim/sidekick): drop unreachable pcall(require, 'sidekick') in tab keymap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 74_sidekick.lua itself called require('sidekick').setup(...) at load time (line 5), so the pcall wrapper inside the keymap could never fail. Replace it with a direct require call — clearer and removes the dead ok/sk locals. --- .config/nvim/plugin/74_sidekick.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.config/nvim/plugin/74_sidekick.lua b/.config/nvim/plugin/74_sidekick.lua index e8ca3c41..ebad1ccd 100644 --- a/.config/nvim/plugin/74_sidekick.lua +++ b/.config/nvim/plugin/74_sidekick.lua @@ -17,8 +17,7 @@ require('sidekick').setup({ }) map("n", "", function() - local ok, sk = pcall(require, "sidekick") - if ok and sk.nes_jump_or_apply() then + if require('sidekick').nes_jump_or_apply() then return "" end return "" From 66ea38dac2b7a96622917d324b9a8f2abfd57292 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:14:13 +0900 Subject: [PATCH 032/366] fix(nvim/session): correct misleading comment about argc() check The old comment said 'Don't save if no files are open' but the code returns false when argc() > 0, which is exactly when files WERE passed on the command line. The comment was the inverse of the logic. Update it to 'Don't save/restore if files were passed on the command line'. --- .config/nvim/plugin/999_session.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/999_session.lua b/.config/nvim/plugin/999_session.lua index ba58d066..d4a8b7a6 100644 --- a/.config/nvim/plugin/999_session.lua +++ b/.config/nvim/plugin/999_session.lua @@ -14,7 +14,7 @@ local skip_dirs = { local function should_save_session() local cwd = vim.fn.getcwd() - -- Don't save if no files are open + -- Don't save/restore if files were passed on the command line if vim.fn.argc() > 0 then return false end From 545c51679707ebdd46e0a8b78c36347e4cd671ca Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:14:14 +0900 Subject: [PATCH 033/366] fix(nvim/colemak): use ipairs instead of pairs for ordered mappings list mappings is an array. pairs iterates a table in undefined order; for arrays, ipairs is the correct iterator. Today every lhs is a single key so order is irrelevant, but if a multi-key lhs is ever added the order will silently matter. Use ipairs in both apply and unapply. --- .config/nvim/lua/colemak.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/nvim/lua/colemak.lua b/.config/nvim/lua/colemak.lua index 306c91bf..b466c84d 100644 --- a/.config/nvim/lua/colemak.lua +++ b/.config/nvim/lua/colemak.lua @@ -37,7 +37,7 @@ function colemak.setup(_) end function colemak.apply() - for _, mapping in pairs(mappings) do + for _, mapping in ipairs(mappings) do local desc = mapping.desc if desc then desc = desc .. ' [COLEMAK]' @@ -52,7 +52,7 @@ function colemak.apply() end function colemak.unapply() - for _, mapping in pairs(mappings) do + for _, mapping in ipairs(mappings) do vim.keymap.del(mapping.modes, mapping.lhs) end end From c28950ae7d577b760fd9115ce1e57d7f25b75ea2 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:31:01 +0900 Subject: [PATCH 034/366] chore: pi things --- home/.pi/agent/agents/reviewer.md | 49 +++ home/.pi/agent/agents/scout.md | 28 ++ home/.pi/agent/agents/worker.md | 32 ++ .../.pi/agent/extensions/ask-user-question.ts | 41 +- home/.pi/agent/extensions/brainstorm.ts | 394 ++++++++++++++++++ home/.pi/agent/extensions/btw.ts | 32 ++ home/.pi/agent/extensions/caffeinate.ts | 2 +- home/.pi/agent/extensions/cost-saver.ts | 4 +- home/.pi/agent/extensions/cost-tracker.ts | 4 +- home/.pi/agent/extensions/footer.ts | 27 +- home/.pi/agent/extensions/goals/index.ts | 213 ++++++++++ home/.pi/agent/extensions/memory/index.ts | 215 ++++++++++ home/.pi/agent/extensions/rate-limit-debug.ts | 46 ++ home/.pi/agent/extensions/subagent/agents.ts | 71 ++++ home/.pi/agent/extensions/subagent/index.ts | 320 ++++++++++++++ .../.pi/agent/extensions/tokenrouter/index.ts | 35 ++ home/.pi/agent/prompts/review-diff.md | 11 + home/.pi/agent/prompts/scout.md | 9 + home/.pi/agent/settings.json | 16 +- home/.pi/agent/skills/init/SKILL.md | 69 +++ home/.pi/agent/supervisor.md | 160 +++++++ 21 files changed, 1746 insertions(+), 32 deletions(-) create mode 100644 home/.pi/agent/agents/reviewer.md create mode 100644 home/.pi/agent/agents/scout.md create mode 100644 home/.pi/agent/agents/worker.md create mode 100644 home/.pi/agent/extensions/brainstorm.ts create mode 100644 home/.pi/agent/extensions/btw.ts create mode 100644 home/.pi/agent/extensions/goals/index.ts create mode 100644 home/.pi/agent/extensions/memory/index.ts create mode 100644 home/.pi/agent/extensions/rate-limit-debug.ts create mode 100644 home/.pi/agent/extensions/subagent/agents.ts create mode 100644 home/.pi/agent/extensions/subagent/index.ts create mode 100644 home/.pi/agent/extensions/tokenrouter/index.ts create mode 100644 home/.pi/agent/prompts/review-diff.md create mode 100644 home/.pi/agent/prompts/scout.md create mode 100644 home/.pi/agent/skills/init/SKILL.md create mode 100644 home/.pi/agent/supervisor.md diff --git a/home/.pi/agent/agents/reviewer.md b/home/.pi/agent/agents/reviewer.md new file mode 100644 index 00000000..e800486e --- /dev/null +++ b/home/.pi/agent/agents/reviewer.md @@ -0,0 +1,49 @@ +--- +name: reviewer +description: Focused read-only review for correctness risks, future compatibility, and deviations from current structure or standard patterns +tools: read, grep, find, ls, bash +model: tokenrouter/MiniMax-M3:medium +--- + +You are a focused reviewer lens for Pi escalation. Review only the task, diff, or files requested. + +Focus: +- Correctness bugs, regressions, security issues, data loss, and unsafe commands. +- Future compatibility issues that are likely to matter soon, not speculative edge cases. +- Deviations from the repository's current structure, conventions, and established extension points. +- Deviations from standard patterns for the language, framework, or Pi API in use. + +Review standard: +- Flag only issues the original author would likely fix if made aware. +- Issues must be discrete, actionable, and introduced by the change under review. +- Do not flag pre-existing problems unless the change makes them materially worse. +- Do not rely on unstated assumptions about author intent or hypothetical downstream breakage; identify the concrete affected path. +- Treat intentional behavior changes as acceptable unless they create a clear correctness, safety, compatibility, or maintainability problem. +- Prefer no findings over low-confidence or merely possible findings. + +Rules: +- Read-only only. Do not edit files or change state. +- Bash is only for read-only commands: git status, git diff, git diff --cached, rg, fd, and relevant test/typecheck commands when explicitly requested. +- When reviewing repository changes, inspect `git status --short` first so unstaged, staged, and untracked files are not confused. +- Do not redesign broadly, propose alternate architectures, or create implementation plans. +- Do not nitpick style unless it hides a real bug or maintainability risk. +- Return only actionable concerns. If there are no material issues, say so clearly. +- Keep output concise; prefer exact paths, symbols, and line numbers. +- If severity matters, use P0/P1/P2/P3 priority language: P0 blocking, P1 urgent, P2 normal, P3 low. + +Output exactly: + +## Critical +- `path:line` — issue, impact, fix direction. Use "None" if none. + +## Warnings +- `path:line` — issue, impact, fix direction. Use "None" if none. + +## Compatibility / Structure +- `path:line` — future compatibility or structure/pattern concern, impact, fix direction. Use "None" if none. + +## Notes +- Verification gaps, assumptions, or commands run. + +## Verdict +One sentence: pass, pass with warnings, or fail. diff --git a/home/.pi/agent/agents/scout.md b/home/.pi/agent/agents/scout.md new file mode 100644 index 00000000..27ca309a --- /dev/null +++ b/home/.pi/agent/agents/scout.md @@ -0,0 +1,28 @@ +--- +name: scout +description: Fast read-only codebase recon that returns compressed context +tools: read, grep, find, ls, bash +model: tokenrouter/MiniMax-M3:medium +--- + +You are a scout. Quickly investigate a codebase and return compressed findings that another agent can use without re-reading everything. + +Rules: +- Read-only only. Do not edit files or change state. +- Use bash only for read-only commands such as rg, fd, git status, git diff, or test discovery. +- Stop when you have enough context for the requested decision; do not exhaustively map unrelated code. +- Prefer exact paths, symbols, and line ranges over broad summaries. + +Output exactly: + +## Findings +- Concise bullets with exact paths/symbols and important constraints. + +## Files Inspected +- `path` lines/ranges — why it mattered. + +## Recommended Starting Point +- The first file/symbol the main agent should inspect or modify, and why. + +## Risks / Unknowns +- Concrete blockers or assumptions. Use "None" if none. diff --git a/home/.pi/agent/agents/worker.md b/home/.pi/agent/agents/worker.md new file mode 100644 index 00000000..6fb64aef --- /dev/null +++ b/home/.pi/agent/agents/worker.md @@ -0,0 +1,32 @@ +--- +name: worker +description: General-purpose worker for scoped coding tasks; full tools, returns evidence not prose +model: tokenrouter/MiniMax-M3:medium +--- + +You are a worker dispatched by the supervisor. Complete the assigned scoped task. + +Rules: +- Stay strictly within the task scope. Do not modify files outside the scope of the assigned change. +- When the task requires a code change: show the exact diff. Use `OLD` → `NEW` for every edit, or paste the new file content with line numbers. +- When the task requires verification: run the exact command the supervisor named, and report its full output verbatim. Do not paraphrase, summarize, or claim success without showing the output. +- When the task is an investigation: return the exact file paths, line ranges, and symbols you found, with the commands you ran to find them. +- Prefer the smallest change that resolves the task. Do not refactor adjacent code, reformat unrelated lines, or "improve" things not asked for. +- Do not commit, push, or publish. Leave changes in the working tree. +- If the task is ambiguous, state the ambiguity and the assumption you made, then proceed. Do not block on permission to proceed. +- If you discover the task is impossible or out of scope, say so explicitly with evidence (what you tried, what blocked you). Do not silently give up. + +Output exactly: + +## Task +- Restate the task in one line, with the gate (command + expected outcome) that must pass. + +## Changes +- `path` — exact diff or new content with line numbers. Use "None" if no change was needed. + +## Verification +- Command run. +- Output (verbatim, or a digest if very long with the path to the full output if preserved). + +## Issues +- Anything you found outside the task scope, anything ambiguous, anything you could not verify. Use "None" if none. diff --git a/home/.pi/agent/extensions/ask-user-question.ts b/home/.pi/agent/extensions/ask-user-question.ts index f597402a..04349f8b 100644 --- a/home/.pi/agent/extensions/ask-user-question.ts +++ b/home/.pi/agent/extensions/ask-user-question.ts @@ -1,16 +1,19 @@ /** - * Ask User Question — multiple choice with an automatic "Something else" option. + * Ask User Question — multiple choice with automatic follow-up options. * - * The AI provides a question and 2-5 alternatives. The tool appends "Something else" - * as the final option. If the user picks it, a free-form input prompt is shown. + * The AI provides a question and 2-5 alternatives. The tool appends "Ask AI for pros and cons" + * and "Something else" as final options. If the user asks for pros/cons, the AI should + * explain the trade-offs and call this tool again with the same question/alternatives. + * If the user picks "Something else", a free-form input prompt is shown. */ -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; -import { Text } from "@mariozechner/pi-tui"; +import { Text } from "@earendil-works/pi-tui"; +const PROS_CONS_OPTION = "Ask AI for pros and cons"; const OTHER_OPTION = "Something else"; -const NO_ANSWER_MSG = "The user did not answer the question. Wait until further input from the user."; +const NO_ANSWER_MSG = "User declined to answer, await further instructions."; interface AskUserQuestionDetails { question: string; @@ -24,20 +27,19 @@ const AskUserQuestionParams = Type.Object({ alternatives: Type.Array(Type.String({ description: "One alternative answer option" }), { minItems: 2, maxItems: 5, - description: "2 to 5 alternative answer options. Do NOT include 'Something else' — it is appended automatically.", + description: "2 to 5 alternative answer options." }), }); -export default function (pi: ExtensionAPI) { +export default function(pi: ExtensionAPI) { pi.registerTool({ name: "ask_user_question", label: "Ask User Question", description: - "Ask the user a multiple-choice question. Provide 2-5 alternatives; a 'Something else' option is appended automatically. Use when you need the user to choose between specific options or provide a custom answer.", + "Ask the user a multiple-choice question. Provide 2-5 alternatives. The tool automatically adds 'Ask AI for pros and cons' and 'Something else'. Use when you need the user to choose between specific options, ask for trade-offs, or provide a custom answer.", promptSnippet: "Ask the user a multiple-choice question with 2-5 alternatives", promptGuidelines: [ - "Use ask_user_question when you need the user to pick from specific options or provide a custom answer.", - "Provide exactly the question and 2-5 concise alternatives. Do NOT include 'Something else' — it is added automatically.", + "Use ask_user_question when you need the user to pick from specific options, ask for trade-offs, or provide a custom answer.", "Keep alternatives short and mutually exclusive.", ], parameters: AskUserQuestionParams, @@ -47,16 +49,25 @@ export default function (pi: ExtensionAPI) { return makeResult(params, "Error: UI not available (running in non-interactive mode)", null, false); } - const options = [...params.alternatives, OTHER_OPTION]; + const options = [...params.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; const choice = await ctx.ui.select(params.question, options, { signal }); - if (choice === null) { + if (choice == null) { return makeResult(params, NO_ANSWER_MSG, null, false); } + if (choice === PROS_CONS_OPTION) { + return makeResult( + params, + "User asked for pros and cons. Explain the pros and cons of each alternative, then call ask_user_question again with the same question and alternatives.", + choice, + false, + ); + } + if (choice === OTHER_OPTION) { const custom = await ctx.ui.input("Something else", "Type your answer...", { signal }); - if (custom === null) { + if (custom == null) { return makeResult(params, NO_ANSWER_MSG, null, false); } return makeResult(params, `User answered (custom): ${custom}`, custom, true); @@ -66,7 +77,7 @@ export default function (pi: ExtensionAPI) { }, renderCall(args, theme, _context) { - const opts = [...args.alternatives, OTHER_OPTION]; + const opts = [...args.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; const optsText = opts.map((o, i) => `${i + 1}. ${o}`).join(", "); const text = theme.fg("toolTitle", theme.bold("ask_user_question ")) + diff --git a/home/.pi/agent/extensions/brainstorm.ts b/home/.pi/agent/extensions/brainstorm.ts new file mode 100644 index 00000000..76c194ff --- /dev/null +++ b/home/.pi/agent/extensions/brainstorm.ts @@ -0,0 +1,394 @@ +/** + * Brainstorm Mode + * + * Lightweight developer/CEO collaboration mode: + * - The agent acts as a senior developer / technical lead. + * - The user acts as CEO/product owner. + * - The conversation can go back and forth until direction is clear. + * - /brainstorm-recommend asks for a token-budgeted implementation recommendation. + * - /brainstorm-finalize asks the current agent to write and save an implementation plan. + * - /brainstorm-implement starts a fresh implementation session from the saved plan. + * - No hidden planner calls, no automatic implementation session. + */ + +import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; + +type BrainstormPhase = "idle" | "brainstorm"; + +const FINALIZED_PLAN_ENTRY = "brainstorm-finalized-plan"; +const FINALIZED_PLAN_HEADINGS = [ + "## Goal", + "## Context", + "## Assumptions", + "## Stop / Ask Conditions", + "## File Map", + "## Implementation Plan", + "## Final Verification", + "## Completion Report", +]; + +const BRAINSTORM_CONTEXT = `[BRAINSTORM MODE ACTIVE] +You are acting as a senior developer / technical lead. The user is the CEO/product owner. + +Primary job: +Help turn rough business/product direction into a practical technical direction through back-and-forth discussion. + +Operating principles: +- Treat the user as the decision-maker for product/business trade-offs. +- You own technical judgment: feasibility, simplicity, maintainability, risks, and implementation shape. +- Be direct. Push back on vague goals, overengineering, risky shortcuts, or weak assumptions. +- Prefer the smallest coherent slice that delivers value. +- Do not create durable specs, plans, or implementation handoff documents unless explicitly asked. +- Do not start implementation or make durable code/config changes unless the user explicitly asks you to. +- Reversible experiments are allowed only after stating purpose, files affected, expected learning, and rollback plan. + +Tool use: +- Use tools to understand reality before making technical claims. +- Token discipline matters: default to direct reasoning from established context when confidence is adequate. +- If the subagent tool is available, use scout only when broad read-only reconnaissance would materially improve the recommendation. +- If the subagent tool is available, use reviewer only when correctness risk, future compatibility, or deviation from current structure/standard patterns justifies the spend. +- Do not run a multi-agent pipeline; use at most one focused subagent call for a recommendation turn unless the user explicitly asks otherwise. +- Use direct reads for focused follow-up after scout/reviewer returns compressed findings. +- Read-only commands, git status/diff, tests, and targeted diagnostics are encouraged when they improve the recommendation. +- If tool output contradicts assumptions, update the recommendation. + +Conversation style: +- Keep responses concise: usually 3-6 bullets. +- Ask at most one question at a time, and only when it blocks a meaningful recommendation. +- If ambiguity is not blocking, state a reasonable assumption and proceed. +- Recommend one path by default. Mention alternatives only when they materially affect the CEO decision. +- Separate business decisions from technical decisions when useful. +- Avoid generic questionnaires and long checklists. + +Useful response shapes: +- Recommendation: what I would do, why, trade-offs, next decision. +- Pushback: what is risky, why it matters, safer alternative. +- Understanding check: goal, constraints, likely approach, open decision. +- Implementation readiness: files/areas likely touched, risks, verification approach.`; + +interface BrainstormState { + phase: BrainstormPhase; + awaitingFinalizedPlan: boolean; + finalizedPlan?: string; + finalizedPlanTimestamp?: number; +} + +interface FinalizedPlanEntry { + type: string; + customType?: string; + data?: { + plan?: unknown; + timestamp?: unknown; + }; +} + +function isAssistantMessage(message: AgentMessage): message is AssistantMessage { + return message.role === "assistant" && Array.isArray(message.content); +} + +function getTextContent(message: AssistantMessage): string { + return message.content + .filter((block): block is TextContent => block.type === "text") + .map((block) => block.text) + .join("\n"); +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function getMissingPlanHeadings(plan: string): string[] { + return FINALIZED_PLAN_HEADINGS.filter((heading) => !new RegExp(`^${escapeRegExp(heading)}\\s*$`, "m").test(plan)); +} + +function getLastAssistantText(messages: readonly AgentMessage[]): string | undefined { + const message = [...messages].reverse().find(isAssistantMessage); + if (!message) return undefined; + + const text = getTextContent(message).trim(); + return text || undefined; +} + +export default function(pi: ExtensionAPI) { + const state: BrainstormState = { + phase: "idle", + awaitingFinalizedPlan: false, + }; + + function updateStatus(ctx: ExtensionContext): void { + const label = state.phase === "brainstorm" ? ctx.ui.theme.fg("warning", "brainstorm") : undefined; + ctx.ui.setStatus("brainstorm", label); + } + + function enterBrainstorm(ctx: ExtensionContext, topic: string): void { + state.phase = "brainstorm"; + updateStatus(ctx); + ctx.ui.notify("Brainstorm mode enabled.", "info"); + pi.sendUserMessage(`Brainstorm topic: ${topic}`); + } + + function exitBrainstorm(ctx?: ExtensionContext): void { + state.phase = "idle"; + state.awaitingFinalizedPlan = false; + if (ctx) updateStatus(ctx); + } + + function restoreFinalizedPlan(ctx: ExtensionContext): void { + const entry = ctx.sessionManager + .getEntries() + .filter((entry): entry is FinalizedPlanEntry => { + const maybeEntry = entry as FinalizedPlanEntry; + return maybeEntry.type === "custom" && maybeEntry.customType === FINALIZED_PLAN_ENTRY; + }) + .pop(); + + if (typeof entry?.data?.plan === "string") { + state.finalizedPlan = entry.data.plan; + state.finalizedPlanTimestamp = typeof entry.data.timestamp === "number" ? entry.data.timestamp : undefined; + } + } + + pi.registerFlag("brainstorm", { + description: "Start brainstorm mode", + type: "boolean", + default: false, + }); + + pi.registerCommand("brainstorm", { + description: "Start brainstorm mode: developer/CEO back-and-forth without automatic implementation", + handler: async (args, ctx) => { + await ctx.waitForIdle(); + + let topic = args.trim(); + + if (state.phase === "brainstorm") { + if (topic) { + pi.sendUserMessage(`Brainstorm update: ${topic}`); + return; + } + ctx.ui.notify("Brainstorm mode is already active. Use /brainstorm-done to exit.", "warning"); + return; + } + + if (!topic && ctx.hasUI) { + const input = await ctx.ui.input("What do you want to brainstorm?", "Describe the topic..."); + topic = input?.trim() ?? ""; + } + + if (!topic) { + ctx.ui.notify("Usage: /brainstorm ", "error"); + return; + } + + enterBrainstorm(ctx, topic); + }, + }); + + pi.registerCommand("brainstorm-recommend", { + description: "Recommend the next implementation direction with optional scout/reviewer escalation", + handler: async (args, ctx) => { + await ctx.waitForIdle(); + + if (state.phase !== "brainstorm") { + ctx.ui.notify("Brainstorm mode is not active. Start with /brainstorm .", "error"); + return; + } + + const instructions = args.trim(); + pi.sendUserMessage(`Recommend the next implementation direction from our brainstorm conversation.${instructions ? `\n\nAdditional CEO instructions:\n${instructions}` : ""} + +Token-budgeted escalation rules: +- Normal low-risk flow should complete without subagent calls. +- Use scout only when current context is too weak and broad read-only reconnaissance would materially change the recommendation. +- Use reviewer only when existing context is sufficient but correctness risk, future compatibility, or deviation from current structure/standard patterns is meaningful. +- Use at most one subagent call in this turn. If both scout and reviewer seem useful, prefer scout first and list reviewer as a later optional check. +- Do not create or request planner/architect/risk/fix agents. Do not implement anything yet. + +Output exactly: + +## Recommendation +One concise recommendation and why. + +## Confidence +High, Medium, or Low — one sentence explaining why. + +## Scout +Needed: yes/no. If yes, say whether you invoked scout or why it should be the next step. + +## Reviewer +Needed: yes/no. If yes, say whether you invoked reviewer or why it should be the next step. + +## Token Spend Rationale +One sentence explaining why escalation spend is or is not justified. + +## Next Action +One concrete next step.`); + }, + }); + + pi.registerCommand("brainstorm-finalize", { + description: "Create an implementation plan from the brainstorm conversation", + handler: async (args, ctx) => { + await ctx.waitForIdle(); + + if (state.phase !== "brainstorm") { + ctx.ui.notify("Brainstorm mode is not active. Start with /brainstorm .", "error"); + return; + } + + const instructions = args.trim(); + state.awaitingFinalizedPlan = true; + pi.sendUserMessage(`Create an implementation plan from our brainstorm conversation.${instructions ? `\n\nAdditional CEO instructions:\n${instructions}` : ""} + +Rules: +- Base the plan only on what we discussed and any code findings already established. +- Do not implement anything yet. +- Do not invent missing code details; add an explicit inspection or scout step instead. +- Keep the plan minimal, practical, and suitable for a fresh coding agent. +- Include reviewer only as an optional focused check when correctness risk, future compatibility, or structural deviation justifies the token spend. +- Include stop/ask conditions for decisions that should not be guessed. + +Output exactly: + +## Goal +One sentence. + +## Context +Bullets with relevant CEO decisions, constraints, code findings, non-goals, and trade-offs. + +## Assumptions +Bullets. Use "None" if none. + +## Stop / Ask Conditions +Bullets for ambiguity that should pause implementation. + +## File Map +- Modify/Create/Test/Inspect: path when known — expected responsibility or question to answer. + +## Implementation Plan +Numbered concrete tasks with verification for each task when practical. + +## Final Verification +Commands/checks to run and expected result. + +## Completion Report +What the implementor should report back.`); + }, + }); + + pi.registerCommand("brainstorm-implement", { + description: "Start a fresh implementation session from the finalized brainstorm plan", + handler: async (args, ctx) => { + await ctx.waitForIdle(); + restoreFinalizedPlan(ctx); + + if (!state.finalizedPlan) { + ctx.ui.notify("No finalized brainstorm plan found. Run /brainstorm-finalize first.", "error"); + return; + } + + const instructions = args.trim(); + const parentSession = ctx.sessionManager.getSessionFile(); + const kickoff = `Implement this finalized brainstorm plan in this fresh session. + +Rules: +- Treat the plan below as the source of truth. +- Do not revisit brainstorm decisions unless implementation is blocked. +- Keep changes scoped to the plan. +- Stop and ask if a Stop / Ask Condition is hit.${instructions ? `\n- Additional implementation instruction: ${instructions}` : ""} + +Finalized plan: + +${state.finalizedPlan}`; + + const result = await ctx.newSession({ + parentSession, + withSession: async (ctx) => { + await ctx.sendUserMessage(kickoff); + }, + }); + + if (result.cancelled) { + ctx.ui.notify("Brainstorm implementation handoff was cancelled.", "warning"); + } + }, + }); + + pi.registerCommand("brainstorm-done", { + description: "Exit brainstorm mode, optionally sending a normal follow-up prompt", + handler: async (args, ctx) => { + await ctx.waitForIdle(); + + const nextPrompt = args.trim(); + exitBrainstorm(ctx); + ctx.ui.notify("Brainstorm mode disabled.", "info"); + + if (nextPrompt) { + pi.sendUserMessage(nextPrompt); + } + }, + }); + + pi.on("session_start", async (_event, ctx) => { + restoreFinalizedPlan(ctx); + + if (pi.getFlag("brainstorm") === true && state.phase === "idle") { + state.phase = "brainstorm"; + } + updateStatus(ctx); + }); + + pi.on("agent_end", async (event, ctx) => { + if (!state.awaitingFinalizedPlan) return; + state.awaitingFinalizedPlan = false; + + const plan = getLastAssistantText(event.messages as readonly AgentMessage[]); + if (!plan) { + ctx.ui.notify("Brainstorm finalize did not produce an assistant plan. Run /brainstorm-finalize again.", "error"); + return; + } + + const missingHeadings = getMissingPlanHeadings(plan); + if (missingHeadings.length > 0) { + ctx.ui.notify(`Brainstorm plan was not saved; missing headings: ${missingHeadings.join(", ")}`, "error"); + return; + } + + state.finalizedPlan = plan; + state.finalizedPlanTimestamp = Date.now(); + pi.appendEntry(FINALIZED_PLAN_ENTRY, { + plan, + timestamp: state.finalizedPlanTimestamp, + }); + ctx.ui.notify("Brainstorm finalized plan saved. Use /brainstorm-implement to start a fresh implementation session.", "info"); + }); + + pi.on("session_shutdown", async () => { + exitBrainstorm(); + }); + + pi.on("context", async (event) => { + if (state.phase !== "idle") return; + + return { + messages: event.messages.filter((message) => { + const maybeCustom = message as { customType?: string }; + return maybeCustom.customType !== "brainstorm-context"; + }), + }; + }); + + pi.on("before_agent_start", async () => { + if (state.phase !== "brainstorm") return; + + return { + message: { + customType: "brainstorm-context", + content: BRAINSTORM_CONTEXT, + display: false, + }, + }; + }); +} diff --git a/home/.pi/agent/extensions/btw.ts b/home/.pi/agent/extensions/btw.ts new file mode 100644 index 00000000..24c6b00e --- /dev/null +++ b/home/.pi/agent/extensions/btw.ts @@ -0,0 +1,32 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +function buildBtwPrompt(question: string): string { + return `Side question / BTW: ${question} + +Treat this as a lightweight side conversation: +- Answer the side question without derailing or broadening the main task. +- Do not make durable code/config changes unless the user explicitly asks in this side question. +- Prefer read-only inspection. If broad reconnaissance is needed and the subagent tool is available, use the scout subagent once. +- Keep the response concise and call out any uncertainty or follow-up needed.`; +} + +export default function(pi: ExtensionAPI) { + pi.registerCommand("btw", { + description: "Ask a lightweight side question without derailing the main task", + handler: async (args, ctx) => { + const question = args.trim(); + if (!question) { + ctx.ui.notify("Usage: /btw ", "warning"); + return; + } + + const prompt = buildBtwPrompt(question); + if (ctx.isIdle()) { + pi.sendUserMessage(prompt); + } else { + pi.sendUserMessage(prompt, { deliverAs: "followUp" }); + ctx.ui.notify("BTW queued as a follow-up side question.", "info"); + } + }, + }); +} diff --git a/home/.pi/agent/extensions/caffeinate.ts b/home/.pi/agent/extensions/caffeinate.ts index e1878be6..89bd16fb 100644 --- a/home/.pi/agent/extensions/caffeinate.ts +++ b/home/.pi/agent/extensions/caffeinate.ts @@ -6,7 +6,7 @@ * against orphaned processes from rapid successive prompts. */ -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { spawn } from "node:child_process"; export default function (pi: ExtensionAPI) { diff --git a/home/.pi/agent/extensions/cost-saver.ts b/home/.pi/agent/extensions/cost-saver.ts index 30dc170c..74584ddd 100644 --- a/home/.pi/agent/extensions/cost-saver.ts +++ b/home/.pi/agent/extensions/cost-saver.ts @@ -11,8 +11,8 @@ * cheap "no changes" message instead of flooding the context. */ -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; -import { isToolCallEventType } from "@mariozechner/pi-coding-agent"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { isToolCallEventType } from "@earendil-works/pi-coding-agent"; import { createHash } from "node:crypto"; import { readFile, stat } from "node:fs/promises"; import { resolve } from "node:path"; diff --git a/home/.pi/agent/extensions/cost-tracker.ts b/home/.pi/agent/extensions/cost-tracker.ts index dea60f5b..99de4166 100644 --- a/home/.pi/agent/extensions/cost-tracker.ts +++ b/home/.pi/agent/extensions/cost-tracker.ts @@ -10,13 +10,13 @@ * (just timestamps + tool call counts). */ -import { getAgentDir, type ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { type Component, matchesKey, Key, truncateToWidth, -} from "@mariozechner/pi-tui"; +} from "@earendil-works/pi-tui"; import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; diff --git a/home/.pi/agent/extensions/footer.ts b/home/.pi/agent/extensions/footer.ts index 2e4c0cb3..52551381 100644 --- a/home/.pi/agent/extensions/footer.ts +++ b/home/.pi/agent/extensions/footer.ts @@ -2,13 +2,13 @@ * Footer Extension — Full custom footer replacement. * * Shows on the left: other extension statuses, cwd, git branch - * Shows on the right: (provider) model, thinking level, context bar, session tokens + * Shows on the right: (provider) model, thinking level, context bar, session tokens, latest cache hit rate * * Right-aligned with space padding so stats stay flush to the terminal edge. */ -import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; -import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; import { homedir } from "node:os"; /* ─── formatting ─── */ @@ -140,12 +140,27 @@ function setupFooter(ctx: ExtensionContext, pi: ExtensionAPI): () => void { function buildSessionTokens(ctx: ExtensionContext): string { let input = 0; let output = 0; + let cacheRead = 0; + let cacheWrite = 0; + let latestCacheHitRate: number | undefined; + for (const e of ctx.sessionManager.getBranch()) { if (e.type === "message" && e.message.role === "assistant" && e.message.usage) { - input += e.message.usage.input ?? 0; - output += e.message.usage.output ?? 0; + const usage = e.message.usage; + input += usage.input ?? 0; + output += usage.output ?? 0; + cacheRead += usage.cacheRead ?? 0; + cacheWrite += usage.cacheWrite ?? 0; + + const latestPromptTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0); + latestCacheHitRate = latestPromptTokens > 0 ? ((usage.cacheRead ?? 0) / latestPromptTokens) * 100 : undefined; } } + const sep = ctx.ui.theme.fg("dim", "/"); - return `${ctx.ui.theme.fg("accent", "↑")}${fmt(input)}${sep}${ctx.ui.theme.fg("accent", "↓")}${fmt(output)}`; + let tokens = `${ctx.ui.theme.fg("accent", "↑")}${fmt(input)}${sep}${ctx.ui.theme.fg("accent", "↓")}${fmt(output)}`; + if ((cacheRead > 0 || cacheWrite > 0) && latestCacheHitRate !== undefined) { + tokens += `${sep}${ctx.ui.theme.fg("accent", "CH")}${latestCacheHitRate.toFixed(1)}%`; + } + return tokens; } diff --git a/home/.pi/agent/extensions/goals/index.ts b/home/.pi/agent/extensions/goals/index.ts new file mode 100644 index 00000000..9e106035 --- /dev/null +++ b/home/.pi/agent/extensions/goals/index.ts @@ -0,0 +1,213 @@ +/** + * Goals — autonomous supervisor for ad-hoc goals. + * + * Commands: + * /goal Start a goal; current session becomes the supervisor + * /goal-status Show current goal's progress (tail of progress.md) + * /goal-cancel Mark current goal as cancelled + * /goals List all goals (past and current) + * + * Protocol: + * The supervisor protocol lives at ~/.pi/agent/supervisor.md. + * When /goal is invoked, the current session's model is told (via a directive + * message) to read the protocol and operate as supervisor. State is written + * to ~/.pi/agent/goals// following the protocol's file conventions. + * + * Lifecycle: + * The active goal id is tracked in a closure variable. The footer status + * badge reflects it. A goal is "active" until final-report.md, stuck.md, or + * cancelled.md is written — the user can run /goal-cancel to mark one as + * cancelled explicitly, or the supervisor writes the other two itself. + * + * Non-goals (v1): + * - Spawning a fresh session via ctx.newSession (current session IS the supervisor) + * - Auto-detecting goal completion (supervisor reports via final-report.md; user checks /goal-status) + * - Backgrounded / detached supervisor runs + */ + +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { homedir } from "node:os"; +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; + +const GOALS_DIR = path.join(homedir(), ".pi/agent/goals"); + +function shortId(): string { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 6); +} + +async function ensureGoalsDir(): Promise { + await fs.mkdir(GOALS_DIR, { recursive: true }); +} + +async function fileExists(file: string): Promise { + try { + await fs.access(file); + return true; + } catch { + return false; + } +} + +async function readFirstLine(file: string): Promise { + try { + const content = await fs.readFile(file, "utf-8"); + const first = content.split("\n").find((line) => line.trim().length > 0) ?? ""; + return first.replace(/^#+\s*/, "").trim(); + } catch { + return "(not yet written)"; + } +} + +async function readTail(file: string, lines: number): Promise { + try { + const content = await fs.readFile(file, "utf-8"); + return content.split("\n").slice(-lines).join("\n"); + } catch { + return "(not yet written)"; + } +} + +type GoalStatus = "running" | "completed" | "stuck" | "cancelled"; + +async function goalStatus(id: string): Promise { + const dir = path.join(GOALS_DIR, id); + if (await fileExists(path.join(dir, "cancelled.md"))) return "cancelled"; + if (await fileExists(path.join(dir, "final-report.md"))) return "completed"; + if (await fileExists(path.join(dir, "stuck.md"))) return "stuck"; + return "running"; +} + +export default function(pi: ExtensionAPI) { + let activeGoalId: string | undefined; + + function setStatus(ctx: ExtensionContext): void { + if (activeGoalId) { + ctx.ui.setStatus("goal", ctx.ui.theme.fg("warning", `goal:${activeGoalId}`)); + } else { + ctx.ui.setStatus("goal", undefined); + } + } + + pi.on("session_start", (_event, ctx) => { + setStatus(ctx); + }); + + pi.registerCommand("goal", { + description: "Start an autonomous goal: current session becomes the supervisor with no budget caps", + handler: async (args, ctx) => { + await ctx.waitForIdle(); + + let text = args.trim(); + if (!text && ctx.hasUI) { + const input = await ctx.ui.input("What is the goal?", "Describe the goal for the supervisor..."); + text = input?.trim() ?? ""; + } + if (!text) { + ctx.ui.notify("Usage: /goal ", "error"); + return; + } + + await ensureGoalsDir(); + const id = shortId(); + const dir = path.join(GOALS_DIR, id); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile( + path.join(dir, "goal.md"), + `# Goal\n\n${text}\n\n## Restated\n\n(Supervisor will restate this in concrete terms here.)\n`, + "utf-8", + ); + + activeGoalId = id; + setStatus(ctx); + ctx.ui.notify(`Goal ${id} started. State at ~/.pi/agent/goals/${id}/`, "info"); + + const directive = `You are now the supervisor for a new goal. + +Goal: ${text} + +Goal state directory: ~/.pi/agent/goals/${id}/ + +Required first actions: +1. Read ~/.pi/agent/supervisor.md — the supervisor protocol (read fully). +2. Read ~/.pi/agent/goals/${id}/goal.md and restate the goal concretely. +3. Ask 2-4 clarifying questions via ctx.ui.ask before dispatching anything; write answers to clarifications.md. + +Then operate per the protocol: plan → dispatch via the subagent tool (scout/worker/reviewer) → verify gates yourself → iterate. No budget caps. Mark each item resolved or stuck; do not stop early. When complete, write final-report.md to the goal state directory and give me a one-paragraph summary.`; + + pi.sendUserMessage(directive); + }, + }); + + pi.registerCommand("goal-status", { + description: "Show the current goal's status and latest progress entries", + handler: async (_args, ctx) => { + if (!activeGoalId) { + ctx.ui.notify("No active goal. Use /goal to start one.", "warning"); + return; + } + const dir = path.join(GOALS_DIR, activeGoalId); + const status = await goalStatus(activeGoalId); + const goal = await readFirstLine(path.join(dir, "goal.md")); + const progress = await readTail(path.join(dir, "progress.md"), 12); + + const output = `Goal ${activeGoalId} — ${status}\n${goal}\n\n--- latest progress ---\n${progress}`; + ctx.ui.notify(output, "info"); + }, + }); + + pi.registerCommand("goal-cancel", { + description: "Mark the current goal as cancelled (writes cancelled.md; does not delete state)", + handler: async (_args, ctx) => { + if (!activeGoalId) { + ctx.ui.notify("No active goal to cancel.", "warning"); + return; + } + const id = activeGoalId; + const dir = path.join(GOALS_DIR, id); + await fs.writeFile( + path.join(dir, "cancelled.md"), + `# Cancelled at ${new Date().toISOString()}\n`, + "utf-8", + ); + activeGoalId = undefined; + setStatus(ctx); + ctx.ui.notify(`Goal ${id} marked as cancelled.`, "info"); + }, + }); + + pi.registerCommand("goals", { + description: "List all goals with status (most recent first)", + handler: async (_args, ctx) => { + await ensureGoalsDir(); + const entries = await fs.readdir(GOALS_DIR, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()); + if (dirs.length === 0) { + ctx.ui.notify("No goals yet. Use /goal to start one.", "info"); + return; + } + + const lines: string[] = []; + const sorted = await Promise.all( + dirs.map(async (entry) => { + const stat = await fs.stat(path.join(GOALS_DIR, entry.name)); + return { entry, mtime: stat.mtimeMs }; + }), + ); + sorted.sort((a, b) => b.mtime - a.mtime); + + for (const { entry } of sorted) { + const id = entry.name; + const dir = path.join(GOALS_DIR, id); + const stat = await fs.stat(dir); + const goal = await readFirstLine(path.join(dir, "goal.md")); + const status = await goalStatus(id); + const active = id === activeGoalId ? " (active)" : ""; + const date = stat.mtime.toISOString().slice(0, 10); + lines.push(`${date} ${status.padEnd(10)} ${id}${active} — ${goal.slice(0, 60)}`); + } + + ctx.ui.notify(lines.join("\n"), "info"); + }, + }); +} diff --git a/home/.pi/agent/extensions/memory/index.ts b/home/.pi/agent/extensions/memory/index.ts new file mode 100644 index 00000000..f44f4bcd --- /dev/null +++ b/home/.pi/agent/extensions/memory/index.ts @@ -0,0 +1,215 @@ +import { StringEnum } from "@earendil-works/pi-ai"; +import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import { mkdir, readFile, stat, appendFile } from "node:fs/promises"; +import * as os from "node:os"; +import * as path from "node:path"; + +const MAX_MEMORY_BYTES = 32 * 1024; +const GLOBAL_MEMORY_PATH = path.join( + process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state"), + "pi-agent", + "memory", + "global.md", +); + +type MemoryScope = "global" | "repo"; + +interface MemoryFile { + scope: MemoryScope; + label: string; + filePath: string; + content?: string; + truncated: boolean; +} + +async function findRepoRoot(pi: ExtensionAPI, cwd: string): Promise { + const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd, timeout: 3000 }); + if (result.code !== 0) return undefined; + const root = result.stdout.trim(); + return root || undefined; +} + +function repoMemoryPath(repoRoot: string): string { + return path.join(repoRoot, ".pi", "memory", "repo.md"); +} + +async function readMemoryFile(scope: MemoryScope, label: string, filePath: string): Promise { + try { + const info = await stat(filePath); + let content = await readFile(filePath, "utf8"); + let truncated = false; + + if (info.size > MAX_MEMORY_BYTES) { + content = content.slice(-MAX_MEMORY_BYTES); + truncated = true; + } + + return { scope, label, filePath, content: content.trim(), truncated }; + } catch { + return { scope, label, filePath, truncated: false }; + } +} + +async function loadMemoryFiles(pi: ExtensionAPI, ctx: ExtensionContext | ExtensionCommandContext): Promise { + const files = [await readMemoryFile("global", "Global memory", GLOBAL_MEMORY_PATH)]; + + const repoRoot = await findRepoRoot(pi, ctx.cwd); + if (repoRoot) { + files.push(await readMemoryFile("repo", "Repository memory", repoMemoryPath(repoRoot))); + } + + return files; +} + +function formatMemoryBlock(files: MemoryFile[]): string | undefined { + const loaded = files.filter((file) => file.content); + if (!loaded.length) return undefined; + + const sections = loaded.map((file) => { + const truncation = file.truncated ? "\n[Only the latest portion is included because the memory file exceeded the size cap.]" : ""; + return `## ${file.label}\nPath: ${file.filePath}${truncation}\n\n${file.content}`; + }); + + return ` +These memories are advisory context from previous sessions. Prefer explicit user instructions and repository files when they conflict. If a memory becomes stable project policy, suggest promoting it to AGENTS.md. + +${sections.join("\n\n")} +`; +} + +function parseCaptureArgs(args: string): { scope?: MemoryScope; note: string } { + const trimmed = args.trim(); + const match = /^(global|repo)\s+([\s\S]+)/.exec(trimmed); + if (!match) return { note: trimmed }; + return { scope: match[1] as MemoryScope, note: match[2].trim() }; +} + +async function chooseCaptureTarget( + pi: ExtensionAPI, + ctx: ExtensionCommandContext, + requestedScope?: MemoryScope, +): Promise<{ scope: MemoryScope; filePath: string } | undefined> { + if (requestedScope === "global") return { scope: "global", filePath: GLOBAL_MEMORY_PATH }; + + const repoRoot = await findRepoRoot(pi, ctx.cwd); + if (requestedScope === "repo") { + if (!repoRoot) { + ctx.ui.notify("/memory-capture repo requires running inside a git repository.", "warning"); + return undefined; + } + return { scope: "repo", filePath: repoMemoryPath(repoRoot) }; + } + + if (repoRoot) return { scope: "repo", filePath: repoMemoryPath(repoRoot) }; + return { scope: "global", filePath: GLOBAL_MEMORY_PATH }; +} + +async function appendMemory(filePath: string, note: string): Promise { + const timestamp = new Date().toISOString(); + await mkdir(path.dirname(filePath), { recursive: true }); + await appendFile(filePath, `\n## ${timestamp}\n\n${note.trim()}\n`, "utf8"); +} + +function statusText(files: MemoryFile[]): string { + return files + .map((file) => { + const state = file.content ? `loaded${file.truncated ? " (truncated)" : ""}` : "missing"; + return `${file.label}: ${state}\n${file.filePath}`; + }) + .join("\n\n"); +} + +const MEMORY_INSTRUCTIONS = ` +When you learn durable information that would help future sessions, proactively call memory_save. +Good candidates: stable user preferences, recurring repo conventions, architecture gotchas, commands, workflows, and decisions likely to remain true. +Do not save secrets, credentials, one-off task details, guesses, transient debugging notes, or information already clearly covered by AGENTS.md. +Use scope "repo" for project-specific memories and "global" for cross-repository user preferences. +The user will be asked to approve or reject each memory_save call before anything is written. +`; + +export default function(pi: ExtensionAPI) { + pi.on("before_agent_start", async (event, ctx) => { + const block = formatMemoryBlock(await loadMemoryFiles(pi, ctx)); + return { systemPrompt: `${event.systemPrompt}\n\n${MEMORY_INSTRUCTIONS}${block ? `\n\n${block}` : ""}` }; + }); + + pi.registerCommand("memory", { + description: "Show lightweight memory status and file paths", + handler: async (_args, ctx) => { + const files = await loadMemoryFiles(pi, ctx); + ctx.ui.notify(statusText(files), "info"); + }, + }); + + pi.registerCommand("memory-capture", { + description: "Append a note to repo or global memory: /memory-capture [repo|global] ", + handler: async (args, ctx) => { + const { scope, note } = parseCaptureArgs(args); + if (!note) { + ctx.ui.notify("Usage: /memory-capture [repo|global] ", "warning"); + return; + } + + const target = await chooseCaptureTarget(pi, ctx, scope); + if (!target) return; + + await appendMemory(target.filePath, note); + ctx.ui.notify(`Saved ${target.scope} memory:\n${target.filePath}`, "info"); + }, + }); + + pi.registerTool({ + name: "memory_save", + label: "Save Memory", + description: "Propose saving a durable global or repository-scoped memory. The user must approve before it is written.", + promptSnippet: "Save approved durable memories for future sessions", + promptGuidelines: [ + "Use memory_save proactively when you learn durable information useful for future sessions.", + "Use memory_save with scope repo for project-specific conventions, gotchas, commands, or decisions.", + "Use memory_save with scope global for cross-repository user preferences.", + "Do not use memory_save for secrets, transient task details, guesses, or information already clearly covered by AGENTS.md.", + ], + parameters: Type.Object({ + scope: StringEnum(["global", "repo"], { description: "Where to save the memory" }), + note: Type.String({ description: "Concise durable memory to save" }), + }), + + async execute(_toolCallId, params, signal, _onUpdate, ctx) { + const note = params.note.trim(); + if (!note) { + return { content: [{ type: "text", text: "Memory not saved: note was empty." }], details: { saved: false } }; + } + + const target = await chooseCaptureTarget(pi, ctx, params.scope); + if (!target) { + return { content: [{ type: "text", text: "Memory not saved: target scope is unavailable." }], details: { saved: false } }; + } + + if (!ctx.hasUI) { + return { + content: [{ type: "text", text: "Memory not saved: interactive approval is required but UI is unavailable." }], + details: { saved: false, scope: target.scope, filePath: target.filePath, note }, + }; + } + + const ok = await ctx.ui.confirm( + `Save ${target.scope} memory?`, + `${note}\n\nPath:\n${target.filePath}`, + { signal }, + ); + if (!ok) { + return { + content: [{ type: "text", text: "Memory not saved: user rejected it." }], + details: { saved: false, scope: target.scope, filePath: target.filePath, note }, + }; + } + + await appendMemory(target.filePath, note); + return { + content: [{ type: "text", text: `Saved ${target.scope} memory to ${target.filePath}.` }], + details: { saved: true, scope: target.scope, filePath: target.filePath, note }, + }; + }, + }); +} diff --git a/home/.pi/agent/extensions/rate-limit-debug.ts b/home/.pi/agent/extensions/rate-limit-debug.ts new file mode 100644 index 00000000..1eb2b65b --- /dev/null +++ b/home/.pi/agent/extensions/rate-limit-debug.ts @@ -0,0 +1,46 @@ +/** + * Diagnostic: captures and logs rate-limit headers from provider responses. + * Check pi's stderr output after a few prompts to see the headers. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +/** Only log for providers that actually send rate-limit headers. */ +const WATCHED_PROVIDERS = new Set(["codex", "openai", "anthropic"]); + +export default function(pi: ExtensionAPI) { + pi.on("after_provider_response", (event, ctx) => { + const provider = ctx.model?.provider; + if (!provider || !WATCHED_PROVIDERS.has(provider)) return; + + const headers = event.headers ?? {}; + const rateHeaders: Record = {}; + + for (const [key, value] of Object.entries(headers)) { + const lower = key.toLowerCase(); + if ( + lower.includes("ratelimit") || + lower.includes("rate-limit") || + lower.includes("rate_limit") || + lower.startsWith("anthropic-") || + lower.startsWith("x-ratelimit") + ) { + rateHeaders[key] = value; + } + } + + if (Object.keys(rateHeaders).length > 0) { + process.stderr.write(`\n[RATE-LIMIT-DEBUG] provider=${provider} status=${event.status}\n`); + for (const [k, v] of Object.entries(rateHeaders)) { + process.stderr.write(` ${k}: ${v}\n`); + } + } else { + process.stderr.write( + `\n[RATE-LIMIT-DEBUG] provider=${provider} status=${event.status} — no rate-limit headers found. All headers:\n`, + ); + for (const [k, v] of Object.entries(headers)) { + process.stderr.write(` ${k}: ${v}\n`); + } + } + }); +} diff --git a/home/.pi/agent/extensions/subagent/agents.ts b/home/.pi/agent/extensions/subagent/agents.ts new file mode 100644 index 00000000..f8a1934d --- /dev/null +++ b/home/.pi/agent/extensions/subagent/agents.ts @@ -0,0 +1,71 @@ +/** + * Minimal user-level subagent discovery. + * + * Agents live in ~/.pi/agent/agents/*.md and use simple YAML frontmatter: + * --- + * name: scout + * description: Fast read-only codebase recon + * tools: read, grep, find, ls, bash + * model: optional-model-id + * --- + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; + +export interface AgentConfig { + name: string; + description: string; + tools?: string[]; + model?: string; + systemPrompt: string; + filePath: string; +} + +export function discoverAgents(): AgentConfig[] { + const dir = path.join(getAgentDir(), "agents"); + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return []; + } + + const agents: AgentConfig[] = []; + for (const entry of entries) { + if (!entry.name.endsWith(".md")) continue; + if (!entry.isFile() && !entry.isSymbolicLink()) continue; + + const filePath = path.join(dir, entry.name); + let content: string; + try { + content = fs.readFileSync(filePath, "utf8"); + } catch { + continue; + } + + const { frontmatter, body } = parseFrontmatter>(content); + if (!frontmatter.name || !frontmatter.description) continue; + + const tools = frontmatter.tools + ?.split(",") + .map((tool) => tool.trim()) + .filter(Boolean); + + agents.push({ + name: frontmatter.name, + description: frontmatter.description, + tools: tools?.length ? tools : undefined, + model: frontmatter.model?.trim() || undefined, + systemPrompt: body.trim(), + filePath, + }); + } + + return agents.sort((a, b) => a.name.localeCompare(b.name)); +} + +export function formatAgentList(agents: AgentConfig[]): string { + return agents.map((agent) => `${agent.name}: ${agent.description}`).join("; ") || "none"; +} diff --git a/home/.pi/agent/extensions/subagent/index.ts b/home/.pi/agent/extensions/subagent/index.ts new file mode 100644 index 00000000..b2525485 --- /dev/null +++ b/home/.pi/agent/extensions/subagent/index.ts @@ -0,0 +1,320 @@ +/** + * Minimal Subagent Tool + * + * Runs one user-level agent from ~/.pi/agent/agents/*.md in an isolated + * `pi --mode json --print --no-session` subprocess and returns its final text. + * Intentionally single-agent only: no project-local prompts, no chaining, no + * custom renderer. Keep orchestration in the main conversation unless repeated + * use proves more is needed. + */ + +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import type { Message } from "@earendil-works/pi-ai"; +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Text } from "@earendil-works/pi-tui"; +import { Type } from "typebox"; +import { discoverAgents, formatAgentList } from "./agents.ts"; + +interface SubagentDetails { + agent: string; + task: string; + exitCode: number; + model?: string; + cwd?: string; + messages: Message[]; + stderr: string; + aborted: boolean; +} + +const SubagentParams = Type.Object({ + agent: Type.String({ description: "Name of the user-level agent to run" }), + task: Type.String({ description: "Self-contained task to delegate" }), + cwd: Type.Optional(Type.String({ description: "Working directory for the child agent. Defaults to current cwd." })), +}); + +function getFinalText(messages: Message[]): string { + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + if (message.role !== "assistant") continue; + for (const part of message.content) { + if (part.type === "text" && part.text.trim()) return part.text.trim(); + } + } + return ""; +} + +type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record }; + +function getDisplayItems(messages: Message[]): DisplayItem[] { + const items: DisplayItem[] = []; + for (const message of messages) { + if (message.role !== "assistant") continue; + for (const part of message.content) { + if (part.type === "text" && part.text.trim()) { + items.push({ type: "text", text: part.text.trim() }); + } else if (part.type === "toolCall") { + items.push({ type: "toolCall", name: part.name, args: part.arguments }); + } + } + } + return items; +} + +function taskPreview(task: string): string { + const singleLine = task.replace(/\s+/g, " ").trim(); + return singleLine.length > 100 ? `${singleLine.slice(0, 97)}...` : singleLine; +} + +function shortenPath(filePath: string): string { + const home = os.homedir(); + return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath; +} + +function formatToolCall(item: Extract): string { + switch (item.name) { + case "bash": { + const command = typeof item.args.command === "string" ? item.args.command : "..."; + const preview = command.replace(/\s+/g, " ").trim(); + return `$ ${preview.length > 80 ? `${preview.slice(0, 77)}...` : preview}`; + } + case "read": + case "write": + case "edit": { + const filePath = item.args.path ?? item.args.file_path ?? "..."; + return `${item.name} ${shortenPath(String(filePath))}`; + } + default: + return item.name; + } +} + +function formatDisplayItem(item: DisplayItem): string { + if (item.type === "toolCall") return `→ ${formatToolCall(item)}`; + return item.text; +} + +function getUpdateText(details: SubagentDetails): string { + const items = getDisplayItems(details.messages); + const latest = items.at(-1); + if (latest) return `⏳ ${details.agent}: ${formatDisplayItem(latest)}`; + return `⏳ ${details.agent} running...`; +} + +function getPiInvocation(args: string[]): { command: string; args: string[] } { + const currentScript = process.argv[1]; + const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/"); + if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) { + return { command: process.execPath, args: [currentScript, ...args] }; + } + + const execName = path.basename(process.execPath).toLowerCase(); + const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName); + if (!isGenericRuntime) return { command: process.execPath, args }; + + return { command: "pi", args }; +} + +async function writeTempPrompt(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-")); + const safeName = agentName.replace(/[^\w.-]+/g, "_"); + const filePath = path.join(dir, `${safeName}.md`); + await fs.promises.writeFile(filePath, prompt, { encoding: "utf8", mode: 0o600 }); + return { dir, filePath }; +} + +async function runAgent(params: { + defaultCwd: string; + agentName: string; + task: string; + cwd?: string; + signal?: AbortSignal; + onUpdate?: (text: string, details: SubagentDetails) => void; +}): Promise { + const agents = discoverAgents(); + const agent = agents.find((candidate) => candidate.name === params.agentName); + if (!agent) { + return { + agent: params.agentName, + task: params.task, + exitCode: 1, + messages: [], + stderr: `Unknown agent "${params.agentName}". Available agents: ${formatAgentList(agents)}.`, + aborted: false, + }; + } + + const args = ["--mode", "json", "--print", "--no-session"]; + if (agent.model) args.push("--model", agent.model); + if (agent.tools?.length) args.push("--tools", agent.tools.join(",")); + + let tempDir: string | undefined; + let promptPath: string | undefined; + if (agent.systemPrompt) { + const temp = await writeTempPrompt(agent.name, agent.systemPrompt); + tempDir = temp.dir; + promptPath = temp.filePath; + args.push("--append-system-prompt", promptPath); + } + args.push(`Task: ${params.task}`); + + const details: SubagentDetails = { + agent: agent.name, + task: params.task, + exitCode: 0, + model: agent.model, + cwd: params.cwd, + messages: [], + stderr: "", + aborted: false, + }; + + const emitUpdate = () => params.onUpdate?.(getUpdateText(details), details); + + try { + const invocation = getPiInvocation(args); + emitUpdate(); + details.exitCode = await new Promise((resolve) => { + const proc = spawn(invocation.command, invocation.args, { + cwd: params.cwd ?? params.defaultCwd, + stdio: ["ignore", "pipe", "pipe"], + }); + + let buffer = ""; + const processLine = (line: string) => { + if (!line.trim()) return; + let event: { type?: string; message?: Message }; + try { + event = JSON.parse(line); + } catch { + return; + } + + if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) { + details.messages.push(event.message); + emitUpdate(); + } + }; + + proc.stdout.on("data", (chunk) => { + buffer += chunk.toString(); + const lines = buffer.split("\n"); + buffer = lines.pop() ?? ""; + for (const line of lines) processLine(line); + }); + + proc.stderr.on("data", (chunk) => { + details.stderr += chunk.toString(); + }); + + proc.on("close", (code) => { + if (buffer.trim()) processLine(buffer); + resolve(code ?? 0); + }); + + proc.on("error", (error) => { + details.stderr += error.message; + resolve(1); + }); + + const abort = () => { + details.aborted = true; + proc.kill("SIGTERM"); + setTimeout(() => { + if (!proc.killed) proc.kill("SIGKILL"); + }, 5000).unref(); + }; + + if (params.signal?.aborted) abort(); + else params.signal?.addEventListener("abort", abort, { once: true }); + }); + } finally { + if (promptPath) await fs.promises.rm(promptPath, { force: true }); + if (tempDir) await fs.promises.rm(tempDir, { force: true, recursive: true }); + } + + return details; +} + +export default function (pi: ExtensionAPI) { + pi.registerTool({ + name: "subagent", + label: "Subagent", + description: "Delegate one self-contained task to a user-level subagent with isolated context. Agents are loaded from ~/.pi/agent/agents/*.md.", + promptSnippet: "Delegate narrow read-only reconnaissance or review to an isolated subagent", + promptGuidelines: [ + "Use subagent for broad read-only reconnaissance or focused review that would otherwise pollute the main context.", + "Pass a self-contained task with the relevant goal, paths, constraints, and expected output.", + "Prefer one subagent call over multi-step orchestration unless the user explicitly asks for more.", + ], + parameters: SubagentParams, + + async execute(_toolCallId, params, signal, onUpdate, ctx) { + const details = await runAgent({ + defaultCwd: ctx.cwd, + agentName: params.agent, + task: params.task, + cwd: params.cwd, + signal, + onUpdate: onUpdate + ? (text, details) => onUpdate({ content: [{ type: "text", text }], details }) + : undefined, + }); + + const finalText = getFinalText(details.messages); + if (details.aborted) { + return { content: [{ type: "text" as const, text: "Subagent aborted." }], details, isError: true }; + } + if (details.exitCode !== 0) { + const message = details.stderr.trim() || finalText || `Subagent exited with code ${details.exitCode}.`; + return { content: [{ type: "text" as const, text: message }], details, isError: true }; + } + return { content: [{ type: "text" as const, text: finalText || "(no output)" }], details }; + }, + + renderCall(args, theme, _context) { + let text = theme.fg("toolTitle", theme.bold("subagent ")) + theme.fg("accent", args.agent); + if (args.cwd) text += theme.fg("muted", ` in ${shortenPath(args.cwd)}`); + text += `\n${theme.fg("dim", ` ${taskPreview(args.task)}`)}`; + return new Text(text, 0, 0); + }, + + renderResult(result, { expanded }, theme, _context) { + const details = result.details as SubagentDetails | undefined; + if (!details) { + const content = result.content[0]; + return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0); + } + + const failed = details.aborted || details.exitCode !== 0; + const icon = failed ? theme.fg("error", "✗") : theme.fg("success", "✓"); + let text = `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))}`; + if (details.model) text += theme.fg("muted", ` (${details.model})`); + if (details.cwd) text += theme.fg("muted", ` in ${shortenPath(details.cwd)}`); + + if (expanded) text += `\n${theme.fg("dim", `Task: ${details.task}`)}`; + + const toolCalls = getDisplayItems(details.messages).filter((item) => item.type === "toolCall"); + const shownToolCalls = expanded ? toolCalls : toolCalls.slice(-8); + if (toolCalls.length > shownToolCalls.length) { + text += `\n${theme.fg("muted", `... ${toolCalls.length - shownToolCalls.length} earlier actions`)}`; + } + for (const item of shownToolCalls) { + text += `\n${theme.fg("muted", formatDisplayItem(item))}`; + } + + const finalText = getFinalText(details.messages); + if (finalText) { + const preview = expanded ? finalText : finalText.split("\n").slice(0, 6).join("\n"); + text += `\n\n${theme.fg("toolOutput", preview)}`; + } else if (!failed) { + text += `\n${theme.fg("muted", "(no output)")}`; + } + + if (failed && details.stderr.trim()) text += `\n${theme.fg("error", details.stderr.trim())}`; + return new Text(text, 0, 0); + }, + }); +} diff --git a/home/.pi/agent/extensions/tokenrouter/index.ts b/home/.pi/agent/extensions/tokenrouter/index.ts new file mode 100644 index 00000000..d3e64f04 --- /dev/null +++ b/home/.pi/agent/extensions/tokenrouter/index.ts @@ -0,0 +1,35 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +export default function(pi: ExtensionAPI) { + pi.registerProvider("tokenrouter", { + name: "TokenRouter", + baseUrl: "https://api.tokenrouter.com/v1", + apiKey: "dummy", + api: "openai-completions", + authHeader: true, + + compat: { + supportsDeveloperRole: false, + supportsReasoningEffort: false, + supportsUsageInStreaming: true, + maxTokensField: "max_tokens", + }, + + models: [ + { + id: "MiniMax-M3", + name: "MiniMax M3", + reasoning: true, + input: ["text", "image"], + contextWindow: 1_000_000, + maxTokens: 16_384, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + }, + }, + ], + }); +} diff --git a/home/.pi/agent/prompts/review-diff.md b/home/.pi/agent/prompts/review-diff.md new file mode 100644 index 00000000..0e845c7b --- /dev/null +++ b/home/.pi/agent/prompts/review-diff.md @@ -0,0 +1,11 @@ +--- +description: Delegate focused read-only review of the current git diff +argument-hint: "[focus]" +--- +Use the subagent tool with agent "reviewer" to review unstaged changes in the current repository for correctness risks, future compatibility issues, and deviations from current structure or standard patterns. + +The reviewer should inspect `git status --short`, `git diff`, and relevant untracked files. Do not review staged or committed changes unless they are needed to understand the unstaged diff. + +Focus, if supplied: $@ + +Return the reviewer's findings. Do not edit files. diff --git a/home/.pi/agent/prompts/scout.md b/home/.pi/agent/prompts/scout.md new file mode 100644 index 00000000..0638c2c4 --- /dev/null +++ b/home/.pi/agent/prompts/scout.md @@ -0,0 +1,9 @@ +--- +description: Delegate read-only codebase reconnaissance to a scout subagent +argument-hint: "" +--- +Use the subagent tool with agent "scout" to investigate this read-only question: + +$@ + +Return the scout's findings and then give only the shortest useful recommendation for the next step. Do not implement. diff --git a/home/.pi/agent/settings.json b/home/.pi/agent/settings.json index 942fb153..20b87dea 100644 --- a/home/.pi/agent/settings.json +++ b/home/.pi/agent/settings.json @@ -1,15 +1,19 @@ { - "defaultProvider": "openai-codex", - "defaultModel": "gpt-5.5", - "defaultThinkingLevel": "medium", + "defaultProvider": "tokenrouter", + "defaultModel": "MiniMax-M3", + "defaultThinkingLevel": "high", + "defaultProjectTrust": "always", "collapseChangelog": true, "quietStartup": true, - "lastChangelogVersion": "0.74.0", + "lastChangelogVersion": "0.79.3", "retry": { "maxRetries": 10 }, "packages": [], "enabledModels": [ - "openai-codex/gpt-5.5" + "openai-codex/gpt-5.5", + "deepseek/deepseek-v4-flash", + "deepseek/deepseek-v4-pro", + "tokenrouter/MiniMax-M3" ] -} \ No newline at end of file +} diff --git a/home/.pi/agent/skills/init/SKILL.md b/home/.pi/agent/skills/init/SKILL.md new file mode 100644 index 00000000..153cc176 --- /dev/null +++ b/home/.pi/agent/skills/init/SKILL.md @@ -0,0 +1,69 @@ +--- +name: init +description: Create or update AGENTS.md with high-signal, non-obvious repository knowledge +--- + +Create or update AGENTS.md for this repository. + +## Objective + +Produce a high-signal reference that helps future agents: +- understand stable architecture and patterns +- avoid non-obvious mistakes +- find things quickly + +Do not restate obvious details. + +--- + +## Approach + +- Identify project type from root files +- Extract real commands from scripts/config +- Read a small set of representative files +- Infer patterns from repetition (not single instances) +- Stop once patterns stabilize + +--- + +## Include + +### Essential Commands +Only what an agent will actually run (build/test/run/lint). +Include flags only if non-obvious. + +### Architecture +- overall structure (monolith, layered, etc.) +- key directories and responsibilities +- control/data flow at a high level + +### Patterns & Conventions +Only stable, repeated patterns: +- naming +- structure +- layering +- error handling + +### Gotchas +Non-obvious rules, edge cases, surprising behavior. + +### Navigation +Where to start and how to trace features. + +### Testing +Only if non-trivial. + +--- + +## Exclude +- obvious facts from a single file +- full file listings +- CI/CD, infra, deployment +- speculation + +--- + +## Rules +- Prefer dense, useful information +- No filler, no repetition +- Only include what is clearly supported by the code diff --git a/home/.pi/agent/supervisor.md b/home/.pi/agent/supervisor.md new file mode 100644 index 00000000..032dd931 --- /dev/null +++ b/home/.pi/agent/supervisor.md @@ -0,0 +1,160 @@ +You are the user's autonomous engineering proxy. The user has set a goal +and asked you to drive it to completion. You inherit their full environment: +their context files (AGENTS.md / CLAUDE.md), their skills, their tools, and +their conventions. Operate as if you are them, with two differences: + +1. You have the `subagent` tool to delegate work. +2. You must produce a final report they can review. + +The user has explicitly opted out of budget caps. You do not stop early. +You drive until the goal is verifiably met, every tracked bug is either +resolved or honestly marked STUCK with a reason, and your final report +is written. + +## Files (the goal state directory) + +All state lives in a single directory the user provides at kickoff +(typically `~/.pi/agent/goals//`). + +- `goal.md` — the goal, restated by you in concrete terms. Not copied. +- `clarifications.md` — upfront questions and the user's answers. Append-only thereafter. +- `plan.md` — your working plan. Ordered bug list with file paths, the gate + each bug fails, and dependencies between bugs. +- `verification.md` — the gates you have derived. Each gate is a runnable + command with a captured pass/fail status. Append-only. +- `progress.md` — append-only dated log. Every dispatch, every gate run, + every state transition, every reviewer verdict. Make it greppable. +- `stuck.md` — bugs you could not resolve. One paragraph each: what you + tried, what you observed, what would unblock. +- `final-report.md` — the deliverable. + +## Phase 1: Upfront clarification (do not skip) + +Before dispatching anything, ask the user 2-4 clarifying questions via +`ctx.ui.ask` (or `ctx.ui.question` for multi-select). Write the answers +to `clarifications.md`. + +Bias toward asking now, asking well, then driving. Do not ask +permission-to-proceed questions ("should I continue?", "is this OK?"). +Ask only when requirements are genuinely ambiguous or off-limits items +exist. Cheap questions now save expensive wrong-direction work later. + +Default questions to consider, adapted to context: +- What counts as a bug here? (syntax errors only? behavior? style? perf?) +- Is there an authoritative test command, linter, or CI gate I must honor? +- Any paths or files off-limits? (generated, vendored, secrets, lockfiles) +- Should fixes be committed as I go, or batched at the end? + +If the user declines to answer, record the declined question and your +chosen default in `clarifications.md`, then proceed. + +## Phase 2: Triage and plan + +1. Run every gate you can identify (linters, syntax checks, typecheck, + tests) and capture the output. These gates define "bug" for this goal. +2. Dispatch scout agent(s) — read-only, fast, cheap model — to enumerate + candidate bugs your gate runs may have missed. Triage their output: + real bug, false positive, or out of scope. +3. Write `plan.md` with the ordered list. Group by file or module. Note + dependencies. Note which gate each bug fails. Note the worker's model + and toolset you will use. +4. Tell the user the plan in one short paragraph. Do not wait for approval — + they have opted into autonomy. + +## Phase 3: Iterate (the loop) + +For each bug in plan order, with the freedom to re-order when you learn +something: + +1. **Pre-flight.** Re-read `goal.md`, `plan.md`, and the relevant + `progress.md` entries. State the bug, its gate, and the expected fix + shape in one line in `progress.md`. +2. **Dispatch a worker.** Use the `subagent` tool. Give one bug per + dispatch. Demand evidence: a diff and the exact command output for + the gate. If the bug requires investigation first, dispatch a scout + before the worker. +3. **Independently verify.** Run the gate yourself. Do not trust the + worker's claim. Append the command and its output to `verification.md`. +4. **Independent review** for non-trivial fixes. Dispatch a reviewer + agent to inspect the diff. They must return a verdict: approve, + request-changes, or reject, with a one-line reason. +5. **State transition.** Update `plan.md` and `progress.md`. A bug moves + to `resolved` only if the gate passes *and* any reviewer agrees. + Otherwise back to `fix-in-progress` with a one-line note. +6. **Re-run the full gate set** after each fix. Earlier fixes can + regress. New bugs can surface. Update `plan.md`. + +Append a dated entry to `progress.md` for every dispatch, every gate +run, every reviewer verdict, and every state transition. Format is +yours; make it greppable. + +## Phase 4: Stuck detection + +A budget cap is forbidden. A stuck detection is not. + +If 3 consecutive attempts on the same bug produce no new state — same +gate output, same error, same blocker — mark the bug `STUCK` in +`plan.md`, write a paragraph to `stuck.md` (what you tried, what you +observed, what would unblock), and move to the next bug. Do not retry +the same approach with cosmetic variations. If a genuinely fresh +approach occurs to you, attempt it once and re-enter the 3-attempt +window. + +Stuck is not failure of the goal. The goal is "drive to completion"; +completion means "every bug resolved or honestly stuck with evidence." + +## Delegation: how to use `subagent` + +- **Scout** — read-only, fast, cheap model. "Find all the X", "where is + Y used", "summarize Z". Triage, exploration, inventory. +- **Worker** — full tools, default model. Scoped fixes, code changes, + command runs. One bug per dispatch. Demand evidence, not prose. +- **Reviewer** — read-only or write-light; may re-run commands. + Independent diff review. Verdict required. + +When a worker reports "done" without showing the exact command and its +exact output for the gate, push back: "show me the command, show me the +output." If they cannot produce it, treat the fix as unverified and stay +in `fix-in-progress`. + +## Verification protocol (the load-bearing rule) + +Every bug has an objective gate: a command that can be run, with output +that can be captured. Examples: + +- Linter passes: `shellcheck file.sh` returns 0, output saved +- Syntax valid: `bash -n script.sh` returns 0 +- No broken links: `find ... -xtype l` returns empty +- Test passes: the relevant test command, output section captured +- No regression: the full gate set, re-run, has not grown in failures + +You (the supervisor) run the gates. The worker may run them too, but +you re-run independently. Capture the command and a digest of the +output, not a paraphrase. Never accept "I think it's fixed" — only +"gate X runs cleanly, output below." + +## Output discipline + +- Workers return: file paths, command output (verbatim where short, + digested where long), and diffs. Not summaries. +- You log: dispatch summaries, gate runs with output, state transitions, + reviewer verdicts, surprises. +- The final report (`final-report.md`) contains: bugs found (count, + categories), bugs resolved (with `verification.md` line references), + bugs stuck (with reasons and what would unblock each), and any new + bugs surfaced during the work that the user should know about. + +## What you do not do + +- Do not ask permission-to-proceed questions. The user opted into autonomy. +- Do not stop on a budget. There is no budget. +- Do not skip verification. Every fix must have captured gate output. +- Do not trust worker claims of "done." Re-run gates yourself. +- Do not retry the same failed approach 4+ times. That is a stuck loop, + not persistence. +- Do not modify files outside the active bug's scope. If a fix needs a + refactor, file the refactor as a new bug and move on. +- Do not commit, push, or publish. The user owns VCS. Leave changes in + the working tree unless explicitly told otherwise. +- Do not ask the user mid-flight. If something is truly blocking, write + it to `stuck.md` and continue with other bugs. From e3ca5cdbf40240acad7d0d92b3c6dd7937c8179c Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:15 +0900 Subject: [PATCH 035/366] fix(nvim/lsp): drop once=true from LspDetach cleanup autocmds --- .config/nvim/plugin/40_lsp_behavior.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 8ba7ca6d..d31f9432 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -122,7 +122,6 @@ local function highlight_references(client, buf) desc = "Remove highlight autocmds", group = UserLspConfig, buffer = buf, - once = true, callback = function(ev) if not (ev.data and ev.data.client_id == client.id) then return end vim.lsp.buf.clear_references() @@ -159,7 +158,6 @@ local function show_diagnostics(client, buf) desc = "Remove diagnostics float autocmd", group = UserLspConfig, buffer = buf, - once = true, callback = function(ev) if not (ev.data and ev.data.client_id == client.id) then return end vim.api.nvim_del_augroup_by_name('lsp-diag-hold-' .. buf) From 2f29611c9f66c75c60ce21c956288399451c8cc6 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:15 +0900 Subject: [PATCH 036/366] fix(nvim/options): url-encode path in vim.ui.open google-search fallback When vim.ui.open is called with free-form text (no URI scheme, no recognised TLD, not a path, not a repo), the override forwards the text to Google search. Previously the text was interpolated as-is into the query string, so 'hello world' became a malformed URL 'https://google.com/search?q=hello world' that the OS rejected. Wrap the path with vim.uri_encode so spaces and other reserved characters are percent-encoded before the URL is built. --- .config/nvim/plugin/10_opts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index 1bd2f894..a7a50055 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -133,7 +133,7 @@ vim.ui.open = (function(overridden) elseif is_repo then path = ('https://github.com/%s'):format(path) elseif not is_dir then - path = ('https://google.com/search?q=%s'):format(path) + path = ('https://google.com/search?q=%s'):format(vim.uri_encode(path)) end end overridden(path) From 714c02b0c0830300b171857f543e45043039381a Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:15 +0900 Subject: [PATCH 037/366] fix(nvim/options): broaden vim.ui.open half-URL TLD match beyond .com The override matched path:match('\%.com$'), so domain-like input such as 'github.io', 'foo.dev', or 'example.org' fell through to the Google search branch instead of being prefixed with 'https://'. Replace the hard-coded TLD with a generic pattern that matches any 2+ alphabetic TLD at the end of the path: path:match('\%.%a%a+'). This catches every real TLD (.com, .io, .dev, .org, .net, ...) while leaving non-domain input (single letters, free-form text) to the Google search fallback. --- .config/nvim/plugin/10_opts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index a7a50055..bd8e7612 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -124,7 +124,7 @@ vim.ui.open = (function(overridden) path = { path, 'string' }, }) local is_uri = path:match('%w+:') - local is_half_url = path:match('%.com$') + local is_half_url = path:match('%.%a%a+') local is_repo = vim.bo.filetype == 'lua' and path:match('%w/%w') and vim.fn.count(path, '/') == 1 local is_dir = path:match('^/') or path:match('^~') if not is_uri then From 449b0caf33dfa76a9823c879d3a0f79d7ebb93ec Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:15 +0900 Subject: [PATCH 038/366] fix(nvim/options): expand leading ~ in vim.ui.open path branch The override treated any path starting with '/' or '~' as-is and forwarded it to the OS. On macOS the 'open' command does not expand '~', so 'vim.ui.open("~/foo")' failed silently. Also, with the broader half-URL TLD match (previous commit), paths like '~/foo.com' would have been misclassified as half-URLs and prefixed with 'https://'. Reorder the branches so an 'is_dir' path with a leading '~' is expanded first, then half-URLs, then repos, then the Google-search fallback for everything else. vim.fn.expand() handles '~', '~user', '$VAR', and other shell-style path expansions natively. --- .config/nvim/plugin/10_opts.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index bd8e7612..915e8773 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -128,7 +128,9 @@ vim.ui.open = (function(overridden) local is_repo = vim.bo.filetype == 'lua' and path:match('%w/%w') and vim.fn.count(path, '/') == 1 local is_dir = path:match('^/') or path:match('^~') if not is_uri then - if is_half_url then + if is_dir and path:sub(1, 1) == '~' then + path = vim.fn.expand(path) + elseif is_half_url then path = ('https://%s'):format(path) elseif is_repo then path = ('https://github.com/%s'):format(path) From 439bfabf95ae2e37f6ed19371e9f8a0e140d36c6 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 039/366] refactor: cut over-engineering from ponytail audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nvim: drop .venv walk in black_command, inline default_picker_opts, simplify showMessage handler, replace vimscript VSetSearch with Lua, swap custom quote_path for shellescape, collapse 25-line dedent - shell: replace python url{,en,de}code, drop venv auto-activate (direnv handles it), replace inline _x aliases with plain functions, add yoink() that does gh+sparse-checkout in 17 lines - scripts: rewrite keys/_arg_help/_projselect/tmux-go-session awk block in shell; hoist paccy preview array - extensions: collapse brainstorm to context-injection mode (5 cmds + plan persistence removed), drop rate-limit-debug Net: -552 lines, 2 scripts deleted (yoink, rate-limit-debug). Not in commit: pre-existing modifications in home/.pi/agent/extensions/{goals, subagent,tokenrouter,settings,supervisor} and untracked files (Brewfile, reboot.py, webm, symbol-layer-proposal) — not part of this change. --- .config/nvim/lua/custom/ai.lua | 15 +- .config/nvim/lua/utils.lua | 28 +- .config/nvim/plugin/20_keymaps.lua | 31 +- .config/nvim/plugin/40_lsp_behavior.lua | 48 ++- .config/nvim/plugin/41_lsp_format.lua | 17 +- .config/nvim/plugin/73_git.lua | 25 +- .config/nvim/plugin/78_lsp.lua | 14 +- .local/bin/_arg_help | 30 +- .local/bin/_projselect | 18 +- .local/bin/keys | 152 +++----- .local/bin/paccy | 4 +- .local/bin/tmux-go-session | 99 ++--- .local/bin/yoink | 100 ----- home/.aliasrc | 37 +- home/.bashrc | 5 - home/.pi/agent/extensions/brainstorm.ts | 341 ++---------------- home/.pi/agent/extensions/rate-limit-debug.ts | 46 --- 17 files changed, 229 insertions(+), 781 deletions(-) delete mode 100755 .local/bin/yoink delete mode 100644 home/.pi/agent/extensions/rate-limit-debug.ts diff --git a/.config/nvim/lua/custom/ai.lua b/.config/nvim/lua/custom/ai.lua index b58e4e52..2af16aba 100644 --- a/.config/nvim/lua/custom/ai.lua +++ b/.config/nvim/lua/custom/ai.lua @@ -5,13 +5,6 @@ local function tmux(args, input) return result.code == 0, vim.trim(result.stdout or ""), vim.trim(result.stderr or "") end --- Prefer single quotes; fall back to double quotes with \" escapes for paths containing ' -local function quote_path(path) - if not path:find("[^%w/_%.%-]") then return path end - if path:find("'") then return '"' .. path:gsub('"', '\\"') .. '"' end - return "'" .. path .. "'" -end - local function current_session() local ok, stdout = tmux({ "display-message", "-p", "#S" }) return ok and stdout ~= "" and stdout or nil @@ -22,9 +15,11 @@ local function target_pane(session_name) return ok and stdout:match("[^\n]+") or nil end -local function send_to_pane(pane_id, text) +-- Send a path to a tmux pane using shellescape for safe quoting. +local function send_to_pane(pane_id, path) local buffer = "nvim-ai-file-" .. pane_id:gsub("^%%", "") - local ok, _, err = tmux({ "load-buffer", "-b", buffer, "-" }, text) + local payload = "@" .. vim.fn.shellescape(path) .. "\n" + local ok, _, err = tmux({ "load-buffer", "-b", buffer, "-" }, payload) if not ok then vim.notify("Failed to load tmux buffer: " .. err, vim.log.levels.ERROR) return false @@ -83,7 +78,7 @@ function M.send_file_to_popup() end local rel = vim.fs.relpath(vim.fn.getcwd(), file) or file - if send_to_pane(pane_id, "@" .. quote_path(rel) .. "\n") then + if send_to_pane(pane_id, rel) then focus_popup(tool) vim.notify("Sent " .. rel .. " to " .. tool, vim.log.levels.INFO) end diff --git a/.config/nvim/lua/utils.lua b/.config/nvim/lua/utils.lua index 227ead92..bf5ff846 100644 --- a/.config/nvim/lua/utils.lua +++ b/.config/nvim/lua/utils.lua @@ -2,35 +2,19 @@ local M = { } ----strip leading spaces +---strip leading spaces (smallest indent wins; empty lines ignored) ---@param lines table ---@return table local function strip_leading_spaces(lines) - local spaces_to_trim_cnt = nil - + local min = math.huge for _, line in ipairs(lines) do if line ~= "" then - local space_count = #line:match("^(%s*)") - - if not spaces_to_trim_cnt or space_count < spaces_to_trim_cnt then - spaces_to_trim_cnt = space_count - end + local n = #line:match("^%s*") + if n < min then min = n end end end - - -- If all lines are empty, return them as is - if not spaces_to_trim_cnt then - return lines - end - - -- Strip the leading spaces from each line - local stripped_lines = {} - for _, line in ipairs(lines) do - -- Remove the leading spaces - table.insert(stripped_lines, line:sub(spaces_to_trim_cnt + 1)) - end - - return stripped_lines + if min == math.huge then return lines end + return vim.tbl_map(function(line) return line:sub(min + 1) end, lines) end diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 0d639984..7277ebc5 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -93,18 +93,25 @@ map("n", "dd", function() end end, { noremap = true, expr = true }) --- makes * and # act on whole selection in visual mode ("very nomagic") --- allows to easily find weird strings like /*foo*/ -vim.cmd([[ -function! g:VSetSearch(cmdtype) - let temp = @s - norm! gv"sy - let @/ = '\V' . substitute(escape(@s, a:cmdtype.'\'), '\n', '\\n', 'g') - let @s = temp -endfunction -xnoremap * :call g:VSetSearch('/')/=@/ -xnoremap # :call g:VSetSearch('?')?=@/ -]]) +-- Visual-mode "very nomagic" search: yank selection into /, then re-run * or #. +-- Makes * and # search the literal selection (handles /*foo*/ cleanly). +local function vset_search() + local temp = vim.fn.getreg('s') + vim.cmd('normal! gv"sy') + local s = vim.fn.getreg('s'):gsub('\n', '\\n') + s = vim.fn.escape(s, [[/\?]]) + vim.fn.setreg('/', [[\V]] .. s) + vim.fn.setreg('s', temp) +end + +map('x', '*', function() + vset_search() + return '/' .. vim.fn.getreg('/') .. '' +end, { expr = true }) +map('x', '#', function() + vset_search() + return '?' .. vim.fn.getreg('/') .. '' +end, { expr = true }) -- Search inside visual selection -- https://www.reddit.com/r/neovim/comments/1mxeghf/using_as_a_multipurpose_search_tool/ diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index d31f9432..976da96e 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -18,34 +18,28 @@ local function mappings(client, buf) utils.map(mode, lhs, rhs, options) end - local default_picker_opts = { - layout = { - layout = { - backdrop = false, - width = 0.5, - min_width = 80, - height = 0.8, - min_height = 30, - box = "vertical", - border = true, - title = "{title} {live} {flags}", - title_pos = "center", - { win = "input", height = 1, border = "bottom" }, - { win = "list", border = "none" }, - { win = "preview", title = "{preview}", height = 0.4, border = "top" }, - }, - }, - focus = "list", -- Focus the list view - } - - -- helper to create Snacks picker functions with default options - local function lsp_picker(picker_fn, override_opts) + local function lsp_picker(picker_fn) return function() - local opts = vim.tbl_extend("force", default_picker_opts, override_opts or {}) - if vim.bo.filetype == "go" then - opts = vim.tbl_extend("force", opts, { pattern = "!_test.go" }) - end - picker_fn(opts) + picker_fn({ + layout = { + layout = { + backdrop = false, + width = 0.5, + min_width = 80, + height = 0.8, + min_height = 30, + box = "vertical", + border = true, + title = "{title} {live} {flags}", + title_pos = "center", + { win = "input", height = 1, border = "bottom" }, + { win = "list", border = "none" }, + { win = "preview", title = "{preview}", height = 0.4, border = "top" }, + }, + }, + focus = "list", + pattern = vim.bo.filetype == "go" and "!_test.go" or nil, + }) end end diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 74398ce2..9e16fd2a 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -21,21 +21,8 @@ local fmt = { local format_group = vim.api.nvim_create_augroup('lsp.format', {}) local missing_black_notified = false -local function black_command(buf) - local buf_name = vim.api.nvim_buf_get_name(buf) - local start = buf_name ~= '' and vim.fs.dirname(buf_name) or vim.uv.cwd() - local venv = vim.fs.find('.venv', { path = start, upward = true, type = 'directory' })[1] - if venv then - local black = venv .. '/bin/black' - if vim.fn.executable(black) == 1 then return black end - end - - if vim.fn.executable('black') == 1 then return 'black' end -end - local function format_python_black(buf) - local black = black_command(buf) - if not black then + if vim.fn.executable('black') ~= 1 then if not missing_black_notified then missing_black_notified = true vim.notify('black not found; install it in .venv or on PATH', vim.log.levels.WARN) @@ -47,7 +34,7 @@ local function format_python_black(buf) if filename == '' then filename = 'stdin.py' end local input = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, true), '\n') - local output = vim.fn.systemlist({ black, '--quiet', '--stdin-filename', filename, '-' }, input) + local output = vim.fn.systemlist({ 'black', '--quiet', '--stdin-filename', filename, '-' }, input) if vim.v.shell_error ~= 0 then vim.notify('black failed: ' .. table.concat(output, '\n'), vim.log.levels.ERROR) return diff --git a/.config/nvim/plugin/73_git.lua b/.config/nvim/plugin/73_git.lua index 1ee2ba4b..e1555480 100644 --- a/.config/nvim/plugin/73_git.lua +++ b/.config/nvim/plugin/73_git.lua @@ -8,23 +8,24 @@ vim.pack.add({ local map = require('utils').map local gitgud = require('custom.gitgud') -map('n', 'Gl', function() gitgud.copy_github_permalink() end, { desc = "Copy GitHub permalink" }) -map('x', 'Gl', function() +local function visual_range() local start_line = vim.fn.line("v") - local end_line = vim.fn.line(".") + local end_line = vim.fn.line(".") if end_line < start_line then start_line, end_line = end_line, start_line end - gitgud.copy_github_permalink({ start_line = start_line, end_line = end_line }) + return { start_line = start_line, end_line = end_line } +end + +local function leave_visual() vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) -end, { desc = "Copy GitHub permalink (range)" }) +end + +map('n', 'Gl', function() gitgud.copy_github_permalink() end, { desc = "Copy GitHub permalink" }) +map('x', 'Gl', function() gitgud.copy_github_permalink(visual_range()); leave_visual() end, + { desc = "Copy GitHub permalink (range)" }) map('n', 'Go', function() gitgud.open_github_file() end, { desc = "Open GitHub file" }) -map('x', 'Go', function() - local start_line = vim.fn.line("v") - local end_line = vim.fn.line(".") - if end_line < start_line then start_line, end_line = end_line, start_line end - gitgud.open_github_file({ start_line = start_line, end_line = end_line }) - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) -end, { desc = "Open GitHub file (range)" }) +map('x', 'Go', function() gitgud.open_github_file(visual_range()); leave_visual() end, + { desc = "Open GitHub file (range)" }) -- blame.nvim require('blame').setup({ diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index 57396ee9..840664fa 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -12,14 +12,12 @@ capabilities = vim.tbl_deep_extend('force', capabilities, require('blink.cmp').g vim.lsp.config('*', { capabilities = capabilities }) -vim.lsp.handlers["window/showMessage"] = function(err, result, ctx) - local client = ctx.client_id and vim.lsp.get_client_by_id(ctx.client_id) - local msg = "[LSP]" - if client then msg = msg .. " [" .. client.name .. "] " end - if result and result.message then - local level = ({ [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG })[result.type] or vim.log.levels.INFO - vim.notify(msg .. result.message, level) - end +vim.lsp.handlers["window/showMessage"] = function(_, result, ctx) + if not (result and result.message) then return end + local client = ctx and ctx.client_id and vim.lsp.get_client_by_id(ctx.client_id) + local prefix = client and ("[LSP] [" .. client.name .. "]") or "[LSP]" + local levels = { [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG } + vim.notify(prefix .. " " .. result.message, levels[result.type] or vim.log.levels.INFO) end local enabled_lsps = { diff --git a/.local/bin/_arg_help b/.local/bin/_arg_help index 07f210d9..db865f3b 100755 --- a/.local/bin/_arg_help +++ b/.local/bin/_arg_help @@ -3,19 +3,14 @@ set -euo pipefail show_help() { - if ! "$@" >/dev/null 2>&1; then - echo "failed: $*" - return - fi - - if command -v bat >/dev/null 2>&1; then - "$@" | bat --plain --language=help && - echo "$@" && - exit 0 + if "$@" >/dev/null 2>&1; then + if command -v bat >/dev/null 2>&1; then + "$@" | bat --plain --language=help + else + "$@" + fi else - "$@" && - echo "$@" && - exit 0 + echo "failed: $*" fi } @@ -39,10 +34,11 @@ for arg in "${args[@]}"; do done while [ ${#cmd[@]} -gt 0 ]; do - show_help "${cmd[@]}" --help - show_help "${cmd[@]}" -h - show_help "${cmd[@]}" -? - show_help "${cmd[@]}" help - show_help "${cmd[@]}" usage + for help_flag in --help -h -? help usage; do + if show_help "${cmd[@]}" "$help_flag" 2>/dev/null; then + echo "${cmd[*]} $help_flag" + exit 0 + fi + done cmd=("${cmd[@]:0:${#cmd[@]}-1}") done diff --git a/.local/bin/_projselect b/.local/bin/_projselect index ef9ae561..f2d0a3af 100755 --- a/.local/bin/_projselect +++ b/.local/bin/_projselect @@ -4,14 +4,10 @@ set -euo pipefail PROJ_DIR="${PROJ_DIR:-$HOME/projects}" -if [ "$(uname -s)" = "Darwin" ]; then - printf "$PROJ_DIR/%s\n" \ - "$(gfind "$PROJ_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | - fzf --preview-window='up,60%' \ - --preview "git -C \"$PROJ_DIR/{}\" log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" -else - printf "$PROJ_DIR/%s\n" \ - "$(find "$PROJ_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | - fzf --preview-window='up,60%' \ - --preview "git -C \"$PROJ_DIR/{}\" log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" -fi +find_cmd=find +[ "$(uname -s)" = "Darwin" ] && command -v gfind >/dev/null 2>&1 && find_cmd=gfind + +printf "$PROJ_DIR/%s\n" \ + "$("$find_cmd" "$PROJ_DIR" -mindepth 1 -maxdepth 1 -type d -printf "%f\n" | + fzf --preview-window='up,60%' \ + --preview "git -C \"$PROJ_DIR/{}\" log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit")" diff --git a/.local/bin/keys b/.local/bin/keys index 83119ea0..43b50583 100755 --- a/.local/bin/keys +++ b/.local/bin/keys @@ -1,104 +1,48 @@ -#!/usr/bin/env python3 -import argparse -import csv -import os -from pathlib import Path -import sys -from typing import Iterable, List - -CONFIG_HOME = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) -KEYS_DIR = CONFIG_HOME / "keys" - -def list_programs() -> List[str]: - if not KEYS_DIR.is_dir(): - raise FileNotFoundError(f"No key directory found at {KEYS_DIR}") - programs = sorted(p.stem for p in KEYS_DIR.glob("*.csv")) - if not programs: - raise FileNotFoundError(f"No keybinding files found in {KEYS_DIR}") - return programs - -def load_rows(program: str) -> List[List[str]]: - file_path = KEYS_DIR / f"{program}.csv" - if not file_path.is_file(): - raise FileNotFoundError(file_path) - - rows: List[List[str]] = [] - with file_path.open(newline="", encoding="utf-8") as csvfile: - for raw_line in csvfile: - stripped = raw_line.strip() - if not stripped or stripped.startswith("#"): - continue - rows.append([cell.strip() for cell in next(csv.reader([raw_line]))]) - if not rows: - raise ValueError(f"No keybindings found in {file_path}") - return rows - -def format_table(rows: List[List[str]]) -> str: - max_cols = max(len(row) for row in rows) - padded = [row + [""] * (max_cols - len(row)) for row in rows] - widths = [max(len(row[i]) for row in padded) for i in range(max_cols)] - - def hline() -> str: - return "+" + "+".join("-" * (w + 2) for w in widths) + "+" - - def fmt_row(row: List[str]) -> str: - cells = [f" {row[i].ljust(widths[i])} " for i in range(max_cols)] - return "|" + "|".join(cells) + "|" - - lines = [hline(), fmt_row(padded[0])] - if len(padded) > 1: - lines.append(hline()) - lines.extend(fmt_row(row) for row in padded[1:]) - lines.append(hline()) - return "\n".join(lines) - -def parse_args(argv: Iterable[str]) -> argparse.Namespace: - parser = argparse.ArgumentParser( - prog="keys", - description="Display keybindings stored in CSV files", - usage="keys |--list|--help", - add_help=True, - ) - parser.add_argument("program", nargs="?", help="Program name to display") - parser.add_argument("-l", "--list", action="store_true", help="List available programs") - return parser.parse_args(argv) - -def main(argv: Iterable[str]) -> int: - args = parse_args(argv) - - if args.list: - try: - for program in list_programs(): - print(program) - except FileNotFoundError as exc: - print(exc, file=sys.stderr) - return 1 - return 0 - - if not args.program: - print("Usage: keys |--list|--help", file=sys.stderr) - return 1 - - try: - rows = load_rows(args.program) - except FileNotFoundError: - print(f"No keybindings defined for '{args.program}'.", file=sys.stderr) - try: - programs = list_programs() - except FileNotFoundError as exc: - print(exc, file=sys.stderr) - else: - print("Available programs:", file=sys.stderr) - for program in programs: - print(f" {program}", file=sys.stderr) - return 1 - except ValueError as exc: - print(exc, file=sys.stderr) - return 1 - - print(f"Key bindings for '{args.program}':\n") - print(format_table(rows)) - return 0 - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) +#!/usr/bin/env bash +# Display CSV keybindings from ~/.config/keys/.csv +set -euo pipefail + +keys_dir="${XDG_CONFIG_HOME:-$HOME/.config}/keys" + +if [[ ${1:-} == "-l" || ${1:-} == "--list" ]]; then + for f in "$keys_dir"/*.csv; do + [[ -e $f ]] && basename "$f" .csv + done | sort + exit 0 +fi + +if [[ -z ${1:-} ]]; then + echo "Usage: keys |--list" >&2 + exit 1 +fi + +file="$keys_dir/$1.csv" +if [[ ! -r $file ]]; then + echo "No keybindings defined for '$1'." >&2 + for f in "$keys_dir"/*.csv; do + [[ -e $f ]] && echo " $(basename "$f" .csv)" + done | sort >&2 + exit 1 +fi + +echo "Key bindings for '$1':" +echo +grep -v '^[[:space:]]*#\|^[[:space:]]*$' "$file" | awk -F, ' +{ + for (i = 1; i <= NF; i++) { + gsub(/^ +| +$/, "", $i) + if (length($i) > w[i]) w[i] = length($i) + } + for (i = 1; i <= NF; i++) f[NR "," i] = $i + if (NF > cols) cols = NF + rows = NR +} +END { + for (r = 1; r <= rows; r++) { + for (i = 1; i <= cols; i++) { + v = ((r "," i) in f) ? f[r "," i] : "" + printf "%-*s ", w[i] + 2, v + } + print "" + } +}' diff --git a/.local/bin/paccy b/.local/bin/paccy index f8af817d..415588fd 100755 --- a/.local/bin/paccy +++ b/.local/bin/paccy @@ -106,6 +106,9 @@ get_untracked() { <(comm -23 <(pacman -Qeq | sort) <(pacman -Qgq base-devel | sort)) \ <(get_tracked) || true } + +preview=(fzf --preview 'pacman -Qil {}') + main() { case "${1-}" in -h | --help) @@ -226,5 +229,4 @@ main() { esac } -preview=(fzf --preview 'pacman -Qil {}') main "$@" diff --git a/.local/bin/tmux-go-session b/.local/bin/tmux-go-session index 1095a5bb..ea399f00 100755 --- a/.local/bin/tmux-go-session +++ b/.local/bin/tmux-go-session @@ -40,70 +40,41 @@ collect_project_paths() { build_project_rows() { collect_project_paths >"$project_paths_file" - awk -F '\t' ' - function basename(path, value) { - value = path - sub(/^.*\//, "", value) - return value - } - - function parent_basename(path, parent) { - parent = path - sub(/\/[^\/]*$/, "", parent) - sub(/^.*\//, "", parent) - return parent - } - - function sanitize(name) { - gsub(/[^[:alnum:]_-]/, "_", name) - sub(/^_+/, "", name) - sub(/_+$/, "", name) - return name - } - - FILENAME == ARGV[1] { - existing[$0] = 1 - next - } - - { - paths[++path_count] = $0 - base = basename($0) - bases[path_count] = base - base_count[base]++ - } - - END { - for (i = 1; i <= path_count; i++) { - unsanitized = base_count[bases[i]] > 1 ? parent_basename(paths[i]) "_" bases[i] : bases[i] - session_name = sanitize(unsanitized) - - if (session_name == "") { - printf "Cannot derive a tmux session name for project: %s\n", paths[i] > "/dev/stderr" - exit 1 - } - - if (seen_path[session_name] != "" && seen_path[session_name] != paths[i]) { - if (!found_collision) { - print "Project paths derive the same tmux session name after sanitization:" > "/dev/stderr" - } - printf " %s: %s and %s\n", session_name, seen_path[session_name], paths[i] > "/dev/stderr" - found_collision = 1 - continue - } - seen_path[session_name] = paths[i] - - if (!existing[session_name]) { - printf "project\t\033[38;5;114m[project]\033[0m %s\t%s\t%s\n", unsanitized, session_name, paths[i] - } - } - - if (found_collision) { - print "Rename one project or choose a collision policy." > "/dev/stderr" - exit 1 - } - } - ' "$session_names_file" "$project_paths_file" + declare -A existing seen base_count + while IFS= read -r s; do existing["$s"]=1; done < "$session_names_file" + + declare -a paths bases + while IFS= read -r p; do + paths+=("$p") + b=$(basename "$p") + bases+=("$b") + base_count["$b"]=$(( ${base_count["$b"]:-0} + 1 )) + done < "$project_paths_file" + + local found_collision=0 path b unsanitized s + for i in "${!paths[@]}"; do + path="${paths[$i]}"; b="${bases[$i]}" + if [[ ${base_count["$b"]:-0} -gt 1 ]]; then + unsanitized="$(basename "$(dirname "$path")")_${b}" + else + unsanitized="$b" + fi + s=$(printf '%s' "$unsanitized" | tr -c '[:alnum:]_-' '_' | sed 's/^_*//; s/_*$//') + [[ -z $s ]] && { echo "Cannot derive a tmux session name for project: $path" >&2; return 1; } + + if [[ -n ${seen[$s]} && ${seen[$s]} != "$path" ]]; then + (( found_collision )) || echo "Project paths derive the same tmux session name after sanitization:" >&2 + echo " $s: ${seen[$s]} and $path" >&2 + found_collision=1 + continue + fi + seen[$s]="$path" + + [[ -z ${existing[$s]} ]] && \ + printf "project\t\033[38;5;114m[project]\033[0m %s\t%s\t%s\n" "$unsanitized" "$s" "$path" + done + + (( found_collision )) && { echo "Rename one project or choose a collision policy." >&2; return 1; } } emit_picker_rows() { diff --git a/.local/bin/yoink b/.local/bin/yoink deleted file mode 100755 index 763ef364..00000000 --- a/.local/bin/yoink +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import os -import re -import json -import urllib.request -import urllib.parse -import urllib.error - -def parse_github_url(url): - """Extract owner, repo, ref, and path from GitHub URL.""" - pattern = r'github\.com/([^/]+)/([^/]+)/tree/([^/]+)(/.*)?' - match = re.search(pattern, url) - - if not match: - print(f"Error: Invalid GitHub URL format", file=sys.stderr) - print(f"Expected: https://github.com/owner/repo/tree/branch/path", file=sys.stderr) - sys.exit(1) - - owner = match.group(1) - repo = match.group(2) - ref = match.group(3) - path = match.group(4).lstrip('/') if match.group(4) else '' - - return owner, repo, ref, path - -def fetch_contents(owner, repo, path, ref): - """Fetch directory contents from GitHub API.""" - url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={urllib.parse.quote(ref)}" - - try: - with urllib.request.urlopen(url) as response: - data = response.read().decode('utf-8') - return json.loads(data) - except urllib.error.HTTPError as e: - if e.code == 404: - print(f"Error: Path not found in repository", file=sys.stderr) - else: - print(f"Error: GitHub API returned status {e.code}", file=sys.stderr) - print(f"Message: {e.read().decode('utf-8')}", file=sys.stderr) - sys.exit(1) - except Exception as e: - print(f"Error: {e}", file=sys.stderr) - sys.exit(1) - -def download_directory(owner, repo, remote_path, local_path, ref): - """Recursively download directory contents.""" - contents = fetch_contents(owner, repo, remote_path, ref) - - if not isinstance(contents, list): - print(f"Error: Path is not a directory", file=sys.stderr) - sys.exit(1) - - for item in contents: - item_local_path = os.path.join(local_path, item['name']) - - if item['type'] == 'file': - print(f"Downloading: {item['path']}") - try: - with urllib.request.urlopen(item['download_url']) as file_response: - os.makedirs(os.path.dirname(item_local_path), exist_ok=True) - with open(item_local_path, 'wb') as f: - f.write(file_response.read()) - except Exception as e: - print(f"Warning: Failed to download {item['path']}: {e}", file=sys.stderr) - continue - - elif item['type'] == 'dir': - os.makedirs(item_local_path, exist_ok=True) - download_directory(owner, repo, item['path'], item_local_path, ref) - -def main(): - if len(sys.argv) != 3: - print("Usage: yoink ", file=sys.stderr) - print("Example: yoink https://github.com/owner/repo/tree/main/path ./local-dir", file=sys.stderr) - sys.exit(1) - - github_url = sys.argv[1] - local_dir = sys.argv[2] - - if os.path.exists(local_dir): - print(f"Error: Directory '{local_dir}' already exists", file=sys.stderr) - sys.exit(1) - - owner, repo, ref, remote_path = parse_github_url(github_url) - - print(f"Fetching {owner}/{repo}:{ref}/{remote_path}") - - os.makedirs(local_dir, exist_ok=True) - - try: - download_directory(owner, repo, remote_path, local_dir, ref) - print(f"\nSuccessfully downloaded to {local_dir}") - except Exception as e: - print(f"\nError: {e}", file=sys.stderr) - sys.exit(1) - -if __name__ == '__main__': - main() diff --git a/home/.aliasrc b/home/.aliasrc index 8c6f72ba..d02be69f 100644 --- a/home/.aliasrc +++ b/home/.aliasrc @@ -57,8 +57,7 @@ alias \ kgn='kubectl config get-contexts --no-headers "$(kgc)" | awk "{print \$5}" | sed "s/^$/default/"' \ klc='yq ".contexts.[].name" ~/.kube/config' \ kln='kubectl get namespaces -o custom-columns=NAME:.metadata.name --no-headers' \ - ksn='kubectl config set-context --current --namespace "$(kln | fzf --header "select ns. ctx: ["$(kubectl config current-context)"]")"' \ - ksc='_x() { set -euo pipefail; s="$(klc | fzf)"; yq e -i ".current-context = \"$s\"" ~/.kube/config; }; (_x)' + ksn='kubectl config set-context --current --namespace "$(kln | fzf --header "select ns. ctx: ["$(kubectl config current-context)"]")"' # ============================================================================= # Helm @@ -74,7 +73,6 @@ alias \ # Docker # ============================================================================= alias \ - dshell='_x() { docker run --entrypoint /bin/bash --rm -it "$1" || docker run --entrypoint /bin/sh --rm -it "$1"; }; _x' \ dprune='docker system prune -a --volumes' \ drr='docker run --rm' @@ -90,16 +88,16 @@ alias \ mimetype="file --dereference --brief --mime-type" \ archive="7z a -m0=LZMA2 -mx=9 -mmt\$(nproc) archive.7z" \ update="paccy -Syu" \ - cheat='_x() { curl cht.sh/"$1"; }; _x' \ + cheat='curl cht.sh' \ tgs='tmux-go-session' \ pnav='cd "$(_projselect)"' \ dlmv='mv ~/Downloads/"$(\ls -At ~/Downloads | head -n1)" .' \ - urldecode='python3 -c "import sys, urllib.parse as ul; print(ul.unquote_plus(sys.argv[1]))"' \ - urlencode='python3 -c "import sys, urllib.parse as ul; print (ul.quote_plus(sys.argv[1]))"' \ + urldecode='printf "%b" "${1//%/\\x}"' \ + urlencode='printf "%s" "$1" | jq -sRr @uri' \ vset='_x() { read -r "$1" && export "$1"; }; _x' \ set_aws_profile='export AWS_PROFILE="$(aws configure list-profiles | fzf)"' \ unset_aws_profile='unset AWS_PROFILE' \ - ytmp3='_x() { yt-dlp -x --audio-format mp3 --audio-quality 0 -o "$HOME/Downloads/%(title)s.%(ext)s" "$1"; }; _x' + ytmp3='yt-dlp -x --audio-format mp3 --audio-quality 0 -o "$HOME/Downloads/%(title)s.%(ext)s"' # Fuzzy file picker alias \ @@ -153,6 +151,31 @@ _man_help() { help "$res" 2>/dev/null || man "$res" } +dshell() { docker run --entrypoint /bin/bash --rm -it "$1" 2>/dev/null || docker run --entrypoint /bin/sh --rm -it "$1"; } + +ksc() { set -euo pipefail; s="$(klc | fzf)"; yq e -i ".current-context = \"$s\"" ~/.kube/config; } + +# Download a directory from a GitHub repo via sparse checkout. +# Usage: yoink +yoink() { + local url=$1 dest=$2 + [[ -e $dest ]] && { echo "yoink: $dest already exists" >&2; return 1; } + local p=${url#https://github.com/} + local owner_repo=${p%%/tree/*} branch_path=${p#*/tree/} + local branch=${branch_path%%/*} subpath=${branch_path#*/} + [[ $subpath == "$branch_path" ]] && subpath="" + local tmp; tmp=$(mktemp -d) + gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse 2>/dev/null + (cd "$tmp/repo" && git sparse-checkout set "$subpath") + mkdir -p "$dest" + if [[ -n $subpath ]]; then + cp -r "$tmp/repo/$subpath"/. "$dest"/ + else + cp -r "$tmp/repo"/. "$dest"/ + fi + rm -rf "$tmp" +} + _fzf_file_insert() { local preview_cmd picker file command -v fd >/dev/null 2>&1 || { diff --git a/home/.bashrc b/home/.bashrc index c6f08f9e..6e7836a5 100644 --- a/home/.bashrc +++ b/home/.bashrc @@ -123,11 +123,6 @@ __prompt_render() { __prompt_command() { local exit_status=$? history -a - # Activate project's venv once per shell; guard with $VIRTUAL_ENV to avoid re-sourcing. - if [[ -z ${VIRTUAL_ENV:-} && -r .venv/bin/activate ]]; then - # shellcheck disable=SC1091 - . .venv/bin/activate - fi __prompt_render "$exit_status" } diff --git a/home/.pi/agent/extensions/brainstorm.ts b/home/.pi/agent/extensions/brainstorm.ts index 76c194ff..5332a91f 100644 --- a/home/.pi/agent/extensions/brainstorm.ts +++ b/home/.pi/agent/extensions/brainstorm.ts @@ -1,34 +1,15 @@ /** * Brainstorm Mode * - * Lightweight developer/CEO collaboration mode: - * - The agent acts as a senior developer / technical lead. - * - The user acts as CEO/product owner. - * - The conversation can go back and forth until direction is clear. - * - /brainstorm-recommend asks for a token-budgeted implementation recommendation. - * - /brainstorm-finalize asks the current agent to write and save an implementation plan. - * - /brainstorm-implement starts a fresh implementation session from the saved plan. - * - No hidden planner calls, no automatic implementation session. + * Lightweight developer/CEO collaboration mode. The agent acts as a senior + * developer / technical lead; the user is the CEO/product owner. /brainstorm + * toggles a context-injected mode. Plan persistence, finalize/recommend/ + * implement subcommands and heading validators are intentionally not provided + * — call them out in the conversation and copy/paste the plan when ready. */ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, TextContent } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; -type BrainstormPhase = "idle" | "brainstorm"; - -const FINALIZED_PLAN_ENTRY = "brainstorm-finalized-plan"; -const FINALIZED_PLAN_HEADINGS = [ - "## Goal", - "## Context", - "## Assumptions", - "## Stop / Ask Conditions", - "## File Map", - "## Implementation Plan", - "## Final Verification", - "## Completion Report", -]; - const BRAINSTORM_CONTEXT = `[BRAINSTORM MODE ACTIVE] You are acting as a senior developer / technical lead. The user is the CEO/product owner. @@ -68,321 +49,39 @@ Useful response shapes: - Understanding check: goal, constraints, likely approach, open decision. - Implementation readiness: files/areas likely touched, risks, verification approach.`; -interface BrainstormState { - phase: BrainstormPhase; - awaitingFinalizedPlan: boolean; - finalizedPlan?: string; - finalizedPlanTimestamp?: number; -} - -interface FinalizedPlanEntry { - type: string; - customType?: string; - data?: { - plan?: unknown; - timestamp?: unknown; - }; -} - -function isAssistantMessage(message: AgentMessage): message is AssistantMessage { - return message.role === "assistant" && Array.isArray(message.content); -} - -function getTextContent(message: AssistantMessage): string { - return message.content - .filter((block): block is TextContent => block.type === "text") - .map((block) => block.text) - .join("\n"); -} - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function getMissingPlanHeadings(plan: string): string[] { - return FINALIZED_PLAN_HEADINGS.filter((heading) => !new RegExp(`^${escapeRegExp(heading)}\\s*$`, "m").test(plan)); -} - -function getLastAssistantText(messages: readonly AgentMessage[]): string | undefined { - const message = [...messages].reverse().find(isAssistantMessage); - if (!message) return undefined; - - const text = getTextContent(message).trim(); - return text || undefined; -} - -export default function(pi: ExtensionAPI) { - const state: BrainstormState = { - phase: "idle", - awaitingFinalizedPlan: false, - }; +export default function (pi: ExtensionAPI) { + let active = false; function updateStatus(ctx: ExtensionContext): void { - const label = state.phase === "brainstorm" ? ctx.ui.theme.fg("warning", "brainstorm") : undefined; - ctx.ui.setStatus("brainstorm", label); - } - - function enterBrainstorm(ctx: ExtensionContext, topic: string): void { - state.phase = "brainstorm"; - updateStatus(ctx); - ctx.ui.notify("Brainstorm mode enabled.", "info"); - pi.sendUserMessage(`Brainstorm topic: ${topic}`); - } - - function exitBrainstorm(ctx?: ExtensionContext): void { - state.phase = "idle"; - state.awaitingFinalizedPlan = false; - if (ctx) updateStatus(ctx); - } - - function restoreFinalizedPlan(ctx: ExtensionContext): void { - const entry = ctx.sessionManager - .getEntries() - .filter((entry): entry is FinalizedPlanEntry => { - const maybeEntry = entry as FinalizedPlanEntry; - return maybeEntry.type === "custom" && maybeEntry.customType === FINALIZED_PLAN_ENTRY; - }) - .pop(); - - if (typeof entry?.data?.plan === "string") { - state.finalizedPlan = entry.data.plan; - state.finalizedPlanTimestamp = typeof entry.data.timestamp === "number" ? entry.data.timestamp : undefined; - } + ctx.ui.setStatus("brainstorm", active ? ctx.ui.theme.fg("warning", "brainstorm") : undefined); } pi.registerFlag("brainstorm", { - description: "Start brainstorm mode", + description: "Start in brainstorm mode", type: "boolean", default: false, }); pi.registerCommand("brainstorm", { - description: "Start brainstorm mode: developer/CEO back-and-forth without automatic implementation", + description: "Toggle brainstorm mode (developer/CEO back-and-forth)", handler: async (args, ctx) => { - await ctx.waitForIdle(); - - let topic = args.trim(); - - if (state.phase === "brainstorm") { - if (topic) { - pi.sendUserMessage(`Brainstorm update: ${topic}`); - return; - } - ctx.ui.notify("Brainstorm mode is already active. Use /brainstorm-done to exit.", "warning"); - return; - } - - if (!topic && ctx.hasUI) { - const input = await ctx.ui.input("What do you want to brainstorm?", "Describe the topic..."); - topic = input?.trim() ?? ""; + const topic = args.trim(); + active = !active; + updateStatus(ctx); + ctx.ui.notify(active ? "Brainstorm mode enabled." : "Brainstorm mode disabled.", "info"); + if (active && topic) { + pi.sendUserMessage(`Brainstorm topic: ${topic}`); } - - if (!topic) { - ctx.ui.notify("Usage: /brainstorm ", "error"); - return; - } - - enterBrainstorm(ctx, topic); }, }); - pi.registerCommand("brainstorm-recommend", { - description: "Recommend the next implementation direction with optional scout/reviewer escalation", - handler: async (args, ctx) => { - await ctx.waitForIdle(); - - if (state.phase !== "brainstorm") { - ctx.ui.notify("Brainstorm mode is not active. Start with /brainstorm .", "error"); - return; - } - - const instructions = args.trim(); - pi.sendUserMessage(`Recommend the next implementation direction from our brainstorm conversation.${instructions ? `\n\nAdditional CEO instructions:\n${instructions}` : ""} - -Token-budgeted escalation rules: -- Normal low-risk flow should complete without subagent calls. -- Use scout only when current context is too weak and broad read-only reconnaissance would materially change the recommendation. -- Use reviewer only when existing context is sufficient but correctness risk, future compatibility, or deviation from current structure/standard patterns is meaningful. -- Use at most one subagent call in this turn. If both scout and reviewer seem useful, prefer scout first and list reviewer as a later optional check. -- Do not create or request planner/architect/risk/fix agents. Do not implement anything yet. - -Output exactly: - -## Recommendation -One concise recommendation and why. - -## Confidence -High, Medium, or Low — one sentence explaining why. - -## Scout -Needed: yes/no. If yes, say whether you invoked scout or why it should be the next step. - -## Reviewer -Needed: yes/no. If yes, say whether you invoked reviewer or why it should be the next step. - -## Token Spend Rationale -One sentence explaining why escalation spend is or is not justified. - -## Next Action -One concrete next step.`); - }, - }); - - pi.registerCommand("brainstorm-finalize", { - description: "Create an implementation plan from the brainstorm conversation", - handler: async (args, ctx) => { - await ctx.waitForIdle(); - - if (state.phase !== "brainstorm") { - ctx.ui.notify("Brainstorm mode is not active. Start with /brainstorm .", "error"); - return; - } - - const instructions = args.trim(); - state.awaitingFinalizedPlan = true; - pi.sendUserMessage(`Create an implementation plan from our brainstorm conversation.${instructions ? `\n\nAdditional CEO instructions:\n${instructions}` : ""} - -Rules: -- Base the plan only on what we discussed and any code findings already established. -- Do not implement anything yet. -- Do not invent missing code details; add an explicit inspection or scout step instead. -- Keep the plan minimal, practical, and suitable for a fresh coding agent. -- Include reviewer only as an optional focused check when correctness risk, future compatibility, or structural deviation justifies the token spend. -- Include stop/ask conditions for decisions that should not be guessed. - -Output exactly: - -## Goal -One sentence. - -## Context -Bullets with relevant CEO decisions, constraints, code findings, non-goals, and trade-offs. - -## Assumptions -Bullets. Use "None" if none. - -## Stop / Ask Conditions -Bullets for ambiguity that should pause implementation. - -## File Map -- Modify/Create/Test/Inspect: path when known — expected responsibility or question to answer. - -## Implementation Plan -Numbered concrete tasks with verification for each task when practical. - -## Final Verification -Commands/checks to run and expected result. - -## Completion Report -What the implementor should report back.`); - }, - }); - - pi.registerCommand("brainstorm-implement", { - description: "Start a fresh implementation session from the finalized brainstorm plan", - handler: async (args, ctx) => { - await ctx.waitForIdle(); - restoreFinalizedPlan(ctx); - - if (!state.finalizedPlan) { - ctx.ui.notify("No finalized brainstorm plan found. Run /brainstorm-finalize first.", "error"); - return; - } - - const instructions = args.trim(); - const parentSession = ctx.sessionManager.getSessionFile(); - const kickoff = `Implement this finalized brainstorm plan in this fresh session. - -Rules: -- Treat the plan below as the source of truth. -- Do not revisit brainstorm decisions unless implementation is blocked. -- Keep changes scoped to the plan. -- Stop and ask if a Stop / Ask Condition is hit.${instructions ? `\n- Additional implementation instruction: ${instructions}` : ""} - -Finalized plan: - -${state.finalizedPlan}`; - - const result = await ctx.newSession({ - parentSession, - withSession: async (ctx) => { - await ctx.sendUserMessage(kickoff); - }, - }); - - if (result.cancelled) { - ctx.ui.notify("Brainstorm implementation handoff was cancelled.", "warning"); - } - }, - }); - - pi.registerCommand("brainstorm-done", { - description: "Exit brainstorm mode, optionally sending a normal follow-up prompt", - handler: async (args, ctx) => { - await ctx.waitForIdle(); - - const nextPrompt = args.trim(); - exitBrainstorm(ctx); - ctx.ui.notify("Brainstorm mode disabled.", "info"); - - if (nextPrompt) { - pi.sendUserMessage(nextPrompt); - } - }, - }); - - pi.on("session_start", async (_event, ctx) => { - restoreFinalizedPlan(ctx); - - if (pi.getFlag("brainstorm") === true && state.phase === "idle") { - state.phase = "brainstorm"; - } + pi.on("session_start", (_event, ctx) => { + if (pi.getFlag("brainstorm") === true) active = true; updateStatus(ctx); }); - pi.on("agent_end", async (event, ctx) => { - if (!state.awaitingFinalizedPlan) return; - state.awaitingFinalizedPlan = false; - - const plan = getLastAssistantText(event.messages as readonly AgentMessage[]); - if (!plan) { - ctx.ui.notify("Brainstorm finalize did not produce an assistant plan. Run /brainstorm-finalize again.", "error"); - return; - } - - const missingHeadings = getMissingPlanHeadings(plan); - if (missingHeadings.length > 0) { - ctx.ui.notify(`Brainstorm plan was not saved; missing headings: ${missingHeadings.join(", ")}`, "error"); - return; - } - - state.finalizedPlan = plan; - state.finalizedPlanTimestamp = Date.now(); - pi.appendEntry(FINALIZED_PLAN_ENTRY, { - plan, - timestamp: state.finalizedPlanTimestamp, - }); - ctx.ui.notify("Brainstorm finalized plan saved. Use /brainstorm-implement to start a fresh implementation session.", "info"); - }); - - pi.on("session_shutdown", async () => { - exitBrainstorm(); - }); - - pi.on("context", async (event) => { - if (state.phase !== "idle") return; - - return { - messages: event.messages.filter((message) => { - const maybeCustom = message as { customType?: string }; - return maybeCustom.customType !== "brainstorm-context"; - }), - }; - }); - pi.on("before_agent_start", async () => { - if (state.phase !== "brainstorm") return; - + if (!active) return; return { message: { customType: "brainstorm-context", @@ -391,4 +90,6 @@ ${state.finalizedPlan}`; }, }; }); + + pi.on("session_shutdown", () => { active = false; }); } diff --git a/home/.pi/agent/extensions/rate-limit-debug.ts b/home/.pi/agent/extensions/rate-limit-debug.ts deleted file mode 100644 index 1eb2b65b..00000000 --- a/home/.pi/agent/extensions/rate-limit-debug.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Diagnostic: captures and logs rate-limit headers from provider responses. - * Check pi's stderr output after a few prompts to see the headers. - */ - -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -/** Only log for providers that actually send rate-limit headers. */ -const WATCHED_PROVIDERS = new Set(["codex", "openai", "anthropic"]); - -export default function(pi: ExtensionAPI) { - pi.on("after_provider_response", (event, ctx) => { - const provider = ctx.model?.provider; - if (!provider || !WATCHED_PROVIDERS.has(provider)) return; - - const headers = event.headers ?? {}; - const rateHeaders: Record = {}; - - for (const [key, value] of Object.entries(headers)) { - const lower = key.toLowerCase(); - if ( - lower.includes("ratelimit") || - lower.includes("rate-limit") || - lower.includes("rate_limit") || - lower.startsWith("anthropic-") || - lower.startsWith("x-ratelimit") - ) { - rateHeaders[key] = value; - } - } - - if (Object.keys(rateHeaders).length > 0) { - process.stderr.write(`\n[RATE-LIMIT-DEBUG] provider=${provider} status=${event.status}\n`); - for (const [k, v] of Object.entries(rateHeaders)) { - process.stderr.write(` ${k}: ${v}\n`); - } - } else { - process.stderr.write( - `\n[RATE-LIMIT-DEBUG] provider=${provider} status=${event.status} — no rate-limit headers found. All headers:\n`, - ); - for (const [k, v] of Object.entries(headers)) { - process.stderr.write(` ${k}: ${v}\n`); - } - } - }); -} From 8366f05cf818ed643950efd01a59b1234c5087ef Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 040/366] feat(shell): set VISUAL and EDITOR to code --wait Use VS Code as the visual editor on both macOS and Linux, with nvim as the fallback when `code` is not on PATH. --wait is required so tools like git commit, crontab -e, and kubectl edit block until the editor closes. Removes the now-redundant Linux-only VISUAL override. --- home/.profile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/home/.profile b/home/.profile index 4fdab926..50b5d2e2 100644 --- a/home/.profile +++ b/home/.profile @@ -24,7 +24,13 @@ XDG_CONFIG_HOME="$HOME/.config" # Default programs # ============================================================================= TERMINAL="ghostty" -EDITOR="nvim" +if command -v code >/dev/null 2>&1; then + EDITOR="code --wait" + VISUAL="code --wait" +else + EDITOR="nvim" + VISUAL="nvim" +fi # ============================================================================= # Platform-specific @@ -44,7 +50,6 @@ else BUN_INSTALL_CACHE_DIR="$XDG_CACHE_HOME/bun" # Default programs - VISUAL="nvim" BROWSER="brave" FILE="pcmanfm" From 80d6fec64da8daf18a372f917f1963f46d63bb36 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 041/366] fix(shell): keep EDITOR as nvim, only set VISUAL to code --wait $EDITOR stays as nvim for line/ex-mode tooling (sudoedit, git rebase -i, fcitx5 input, etc.). Only $VISUAL, the full-screen visual editor, points at VS Code. If `code` is not on PATH, VISUAL falls back to whatever $EDITOR is set to (or stays unset for the rare tool that only checks VISUAL). --- home/.profile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/home/.profile b/home/.profile index 50b5d2e2..36e4f815 100644 --- a/home/.profile +++ b/home/.profile @@ -25,12 +25,9 @@ XDG_CONFIG_HOME="$HOME/.config" # ============================================================================= TERMINAL="ghostty" if command -v code >/dev/null 2>&1; then - EDITOR="code --wait" VISUAL="code --wait" -else - EDITOR="nvim" - VISUAL="nvim" fi +EDITOR="nvim" # ============================================================================= # Platform-specific From 574b70136da892fad33b639cc76cc66d5ed3e1c9 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 042/366] fix(tmux-go-session): survive set -eu in build_project_rows The ponytail refactor moved build_project_rows from awk to bash and introduced two set -eu landmines: 1. ${seen[$s]} and ${existing[$s]} are read before being written on the first loop iteration. Under 'set -u' that aborts with 'unbound variable' on the first project. 2. The trailing '(( found_collision )) && { ...; return 1; }' returns exit 1 when found_collision is 0, because ((0)) is false. The &&- short-circuit skips the brace, but the (( )) itself already produced rc=1, and 'set -e' fails the function. build_project_rows is the last command in emit_picker_rows, which is the last command in 'emit_picker_rows > picker_rows_file', so the whole script aborts before fzf ever runs and tmux reports 'tmux-go-session returned 1' on M-g / cmd+g. Fix: default-expand the assoc reads with ${var:-}, and use a plain 'if' for the collision check so the function returns 0 on the happy path. Verified end-to-end with stub fzf/tmux on PATH: both the existing-session and new-session branches reach switch_or_attach with exit 0. --- .local/bin/tmux-go-session | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.local/bin/tmux-go-session b/.local/bin/tmux-go-session index ea399f00..653693bb 100755 --- a/.local/bin/tmux-go-session +++ b/.local/bin/tmux-go-session @@ -62,19 +62,22 @@ build_project_rows() { s=$(printf '%s' "$unsanitized" | tr -c '[:alnum:]_-' '_' | sed 's/^_*//; s/_*$//') [[ -z $s ]] && { echo "Cannot derive a tmux session name for project: $path" >&2; return 1; } - if [[ -n ${seen[$s]} && ${seen[$s]} != "$path" ]]; then + if [[ -n ${seen[$s]:-} && ${seen[$s]:-} != "$path" ]]; then (( found_collision )) || echo "Project paths derive the same tmux session name after sanitization:" >&2 - echo " $s: ${seen[$s]} and $path" >&2 + echo " $s: ${seen[$s]:-} and $path" >&2 found_collision=1 continue fi seen[$s]="$path" - [[ -z ${existing[$s]} ]] && \ + [[ -z ${existing[$s]:-} ]] && \ printf "project\t\033[38;5;114m[project]\033[0m %s\t%s\t%s\n" "$unsanitized" "$s" "$path" done - (( found_collision )) && { echo "Rename one project or choose a collision policy." >&2; return 1; } + if (( found_collision )); then + echo "Rename one project or choose a collision policy." >&2 + return 1 + fi } emit_picker_rows() { From 06eca108e8b63ff4ad2672887b0de7375dfbb3d6 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 043/366] fix(aliasrc): make yoink() honor branch in GitHub URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The function parsed out the branch from the URL but never used it (shellcheck SC2034) — it always did a shallow clone of the default branch. Pass --branch to gh repo clone when the URL has one. --- home/.aliasrc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/home/.aliasrc b/home/.aliasrc index d02be69f..bf1fad79 100644 --- a/home/.aliasrc +++ b/home/.aliasrc @@ -157,6 +157,10 @@ ksc() { set -euo pipefail; s="$(klc | fzf)"; yq e -i ".current-context = \"$s\"" # Download a directory from a GitHub repo via sparse checkout. # Usage: yoink +# The URL may point at a specific branch and subpath: +# https://github.com/owner/repo -> repo, default branch +# https://github.com/owner/repo/tree/branch -> repo @ branch +# https://github.com/owner/repo/tree/branch/dir -> dir under repo @ branch yoink() { local url=$1 dest=$2 [[ -e $dest ]] && { echo "yoink: $dest already exists" >&2; return 1; } @@ -165,7 +169,11 @@ yoink() { local branch=${branch_path%%/*} subpath=${branch_path#*/} [[ $subpath == "$branch_path" ]] && subpath="" local tmp; tmp=$(mktemp -d) - gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse 2>/dev/null + if [[ -n $branch ]]; then + gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse --branch "$branch" 2>/dev/null + else + gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse 2>/dev/null + fi (cd "$tmp/repo" && git sparse-checkout set "$subpath") mkdir -p "$dest" if [[ -n $subpath ]]; then From bd27763916348de682d3ef2925c801faaa3b8556 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 044/366] fix(aliasrc): make urlencode/urldecode work with multi-word args Two bugs in one line each: - $1 inside an alias body refers to the *caller's* positional $1, not the alias's first argument. 'urldecode "a%20b"' expanded to 'printf %b "${1//%/\\x}" "a%20b"' and $1 was unset, so the substitution produced '' and the input was echoed verbatim. - 'urlencode "hello world"' passed 2 args; the alias body only ever used $1, and the leftover 'world' was treated by jq as a filename. Convert both to shell functions so $1 binds to the function's arg. Repro (before): urlencode 'hello world' -> jq: error: Could not open file hello world; urldecode 'a%20b%20c' -> a%20b%20c (not decoded). --- home/.aliasrc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/home/.aliasrc b/home/.aliasrc index bf1fad79..d6e47f8d 100644 --- a/home/.aliasrc +++ b/home/.aliasrc @@ -92,8 +92,7 @@ alias \ tgs='tmux-go-session' \ pnav='cd "$(_projselect)"' \ dlmv='mv ~/Downloads/"$(\ls -At ~/Downloads | head -n1)" .' \ - urldecode='printf "%b" "${1//%/\\x}"' \ - urlencode='printf "%s" "$1" | jq -sRr @uri' \ + vset='_x() { read -r "$1" && export "$1"; }; _x' \ vset='_x() { read -r "$1" && export "$1"; }; _x' \ set_aws_profile='export AWS_PROFILE="$(aws configure list-profiles | fzf)"' \ unset_aws_profile='unset AWS_PROFILE' \ @@ -151,6 +150,9 @@ _man_help() { help "$res" 2>/dev/null || man "$res" } +urldecode() { printf "%b" "${1//%/\\x}"; } +urlencode() { printf "%s" "$1" | jq -sRr @uri; } + dshell() { docker run --entrypoint /bin/bash --rm -it "$1" 2>/dev/null || docker run --entrypoint /bin/sh --rm -it "$1"; } ksc() { set -euo pipefail; s="$(klc | fzf)"; yq e -i ".current-context = \"$s\"" ~/.kube/config; } From 0a9686a66f5565e4b1db897940f7fc49c8bd5f99 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 045/366] fix(_arg_help): make show_help return non-zero on command failure The else branch echoed 'failed: $*' to stdout, which always exits 0. The caller's 'if show_help ...; then' saw success and exited the loop, reporting a non-existent help flag as a hit and printing 'cmd --flag' without ever showing the help text. Repro (before): $ _arg_help ls nonsense --help -> shows 'failed: ls nonsense --help' but treats it as success, exits with 'ls nonsense --help' printed and no help body. --- .local/bin/_arg_help | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.local/bin/_arg_help b/.local/bin/_arg_help index db865f3b..a45e5909 100755 --- a/.local/bin/_arg_help +++ b/.local/bin/_arg_help @@ -10,7 +10,8 @@ show_help() { "$@" fi else - echo "failed: $*" + echo "failed: $*" >&2 + return 1 fi } From 216093bfbe3372357913d99e1bd49787c4765917 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 046/366] fix(paccy): make read_pkgs use a local accumulator The function appended to a global 'pkgs' variable, which is later re-bound to an array by 'read -ra pkgs' in the -S and -Rns branches. On the next read_pkgs call, 'pkgs+="word "' (string append on an array) collapses the existing array back to a string and concatenates the new token, so 'read -ra pkgs2 <<<$(read_pkgs world)' ends up as 'pkgs2=(helloworld)' instead of 'pkgs2=(world)'. Repro (before): read -ra pkgs <<< "$(read_pkgs hello | tr , '\n')" read -ra pkgs2 <<< "$(read_pkgs world | tr , '\n')" echo ${pkgs2[@]} # -> helloworld (expected: world) --- .local/bin/paccy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.local/bin/paccy b/.local/bin/paccy index 415588fd..c6f9a9b3 100755 --- a/.local/bin/paccy +++ b/.local/bin/paccy @@ -47,11 +47,12 @@ log_pkg() { # Read packages to either install or remove read_pkgs() { + local result="" while [[ $# -gt 0 ]] && [[ ${1:0:1} != - ]]; do - pkgs+="$1 " + result+="$1 " shift done - echo "$pkgs" + echo "$result" } # Remove a tracked package From d1dc2a6041001b6df8c550910bd7075b3c416a20 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 047/366] fix(compress): reject non-integer and out-of-range -cl values cLevel is interpolated unquoted into the zstd arg list as -"$cLevel". A non-numeric value (e.g. -cl abc) makes zstd print its help and exit 1; the && chain then aborts and the user gets two pages of usage with no clear error. -cl 99 silently clamps to 19 (zstd's max), so the user thinks they got level 99 compression when they got level 19. Validate up front: integer 0-19, with a clear error otherwise. Repro (before): $ compress -cl abc a b zstd: Incorrect parameter: -a zstd: [entire usage page] $ compress -cl 99 a b archive.tar : ... (silently compressed at level 19, not 99) --- .local/bin/compress | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.local/bin/compress b/.local/bin/compress index e96332ee..c92ea561 100755 --- a/.local/bin/compress +++ b/.local/bin/compress @@ -6,7 +6,15 @@ PARAMS=() while (("$#")); do case "$1" in -cl | --compression-level) + if ! [[ $2 =~ ^[0-9]+$ ]]; then + echo "Error: compression level must be an integer 0-19, got '$2'" >&2 + exit 1 + fi cLevel=$2 + if (( cLevel < 0 || cLevel > 19 )); then + echo "Error: compression level out of range, must be 0-19, got '$cLevel'" >&2 + exit 1 + fi shift 2 ;; --) # end argument parsing From fd5004481634652aee0dd2724378e1bb457bb45a Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 048/366] fix(wific): make select_device/select_network return their value device and network were assigned as global variables inside the functions, then read in the script's main body. With 'set -u' this worked, but the globals leaked any time the script was sourced (for example from a shell function library) and would clobber any caller-scope 'device' / 'network' variables. set -euo pipefail also tripped on the first 'device=$(...)' if any iwctl call failed, because the $() set -e was inherited from the function body. Convert both functions to print the value, and capture it in the caller. The functions also take 'device' as an argument now, so they no longer share state via globals. Repro (before, source the script): . wific select_device # sets 'device' in the *sourcer's* scope --- .local/bin/wific | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.local/bin/wific b/.local/bin/wific index 030fc2e0..8fb7abee 100755 --- a/.local/bin/wific +++ b/.local/bin/wific @@ -8,7 +8,7 @@ message_box() { # Function to select a device select_device() { - local devices + local devices device devices="$(iwctl device list | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR>4 {print $1}' | grep --color=never '\S')" # shellcheck disable=SC2046 device=$(whiptail --title "Select Device" --menu "Choose a device" 15 60 4 $(echo "$devices" | awk '{print NR, $1}') 3>&1 1>&2 2>&3) @@ -16,13 +16,14 @@ select_device() { return 1 fi device="$(echo "$devices" | sed -n "${device}p")" + printf '%s\n' "$device" } # Function to select a network select_network() { + local device=$1 networks network iwctl station "$device" scan iwctl station "$device" show - local networks networks="$(iwctl station "$device" get-networks | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR>4 {print $1}' | sed 's/^>//' | grep --color=never '\S')" # shellcheck disable=SC2046 network=$(whiptail --title "Select Network" --menu "Choose a network" 15 60 8 $(echo "$networks" | awk '{print NR, $1}') 3>&1 1>&2 2>&3) @@ -30,11 +31,12 @@ select_network() { return 1 fi network="$(echo "$networks" | sed -n "${network}p")" + printf '%s\n' "$network" } # Main script execution -select_device || exit 0 -select_network || exit 0 +device=$(select_device) || exit 0 +network=$(select_network "$device") || exit 0 set -x iwctl station "$device" connect "$network" set +x From b12280a41f19a0ab2cd57d880b225570af568681 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 049/366] fix(unarchive): handle empty tar listings and missing-file errors cleanly Two issues: 1. The precheck '[ ! -r "$1" ] && echo ... && exit 1' is fragile: if the echo itself failed (set -e or a broken stdout), exit 1 would not run. Replaced with a proper if/then/fi. 2. choose_dest fed an empty listing to awk, which printed nothing, so 'count' was empty and '[[ "$count" -le 1 ]]' triggered "integer expression expected". Then tar ran with -C "" and failed with a different error. Forced awk to print 0 and check for empty count up front. Repro (before): $ touch /tmp/empty.tar && unarchive /tmp/empty.tar line 21: [: : integer expression expected tar: Error is not recoverable: exiting now $ echo garbage > /tmp/bad.tar && unarchive /tmp/bad.tar same: integer expression, then tar: Unrecognized archive format --- .local/bin/unarchive | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.local/bin/unarchive b/.local/bin/unarchive index 7efd7081..6a3d4f6e 100755 --- a/.local/bin/unarchive +++ b/.local/bin/unarchive @@ -1,7 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -[ ! -r "$1" ] && echo "'$1' is not a readable file" && exit 1 +if [ ! -r "$1" ]; then + echo "'$1' is not a readable file" >&2 + exit 1 +fi # Strip archive extension(s) to derive target dir name name=$(basename "$1") @@ -17,8 +20,8 @@ esac # Given a newline-separated file listing, print "." if single top-level entry, # otherwise mkdir $name and print it choose_dest() { - count=$(awk -F/ '{k=($1=="."?$2:$1)} k && !seen[k]++ {n++} END {print n}' <<< "$1") - if [ "$count" -le 1 ]; then + count=$(awk -F/ '{k=($1=="."?$2:$1)} k && !seen[k]++ {n++} END {print n+0}' <<< "$1") + if [ -z "$count" ] || [ "$count" -le 1 ]; then printf '.' else mkdir -p "${name:-.}" From 935ff4aaf1ee675c6f9bbaff53046981e2cd0d8b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 050/366] fix(lawk,ljq): quote $input in the fzf --preview command $input was unquoted inside the double-quoted --preview string, so a path with spaces was word-split before fzf ever saw it. fzf then ran 'gawk {q} /tmp/file with spaces/data' (or the jq equivalent), which gawk/jq interpreted as multiple input files and failed with 'can't open file /tmp/file'. Repro (before): $ mkdir -p '/tmp/file with spaces' && echo a > '/tmp/file with spaces/data' $ echo '{print $1}' | lawk '/tmp/file with spaces/data' gawk: can't open file /tmp/file (the rest of the path is also rejected) After: the path is double-quoted inside the preview arg, so a path with spaces works end-to-end. --- .local/bin/lawk | 2 +- .local/bin/ljq | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.local/bin/lawk b/.local/bin/lawk index c552c646..90edaec7 100755 --- a/.local/bin/lawk +++ b/.local/bin/lawk @@ -11,4 +11,4 @@ echo '' | fzf --disabled \ --preview-window='down:95%' \ --print-query \ - --preview "gawk {q} $input" + --preview "gawk {q} \"$input\"" diff --git a/.local/bin/ljq b/.local/bin/ljq index 30b8b6e9..a8087279 100755 --- a/.local/bin/ljq +++ b/.local/bin/ljq @@ -12,4 +12,4 @@ echo '' | --preview-window='down:95%' \ --query="." \ --print-query \ - --preview "jq --color-output -r {q} $input" + --preview "jq --color-output -r {q} \"$input\"" From 213d6d19b06c5a6c365dc14b7fffabef6ef6105b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:16 +0900 Subject: [PATCH 051/366] fix(nvim/lsp): return single-formatter name from formatter_name When a filetype isn't in the fmt table and has exactly one textDocument/formatting client, formatter_name used to return nil, causing vim.lsp.buf.format to fall back to all clients (which for a single client is harmless, but for any later re-routing of the helper loses the per-client lock the call site expects). Match the >= 2 branch by warning only when ambiguous, and return the sole client's name otherwise. --- .config/nvim/plugin/41_lsp_format.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 9e16fd2a..7f1c57f5 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -55,7 +55,7 @@ local function formatter_name(buf) return nil end - return nil + return clients[1] and clients[1].name or nil end local function organize_go_imports(buf, client) From ff88dc2322ffaf5f4b4c6550840e08e839818278 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:17 +0900 Subject: [PATCH 052/366] fix(nvim/lsp): scope Go test exclusion to definition-like pickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The !_test.go pattern was passed to every lsp_picker-wrapped call: implementations, references, incoming_calls, outgoing_calls. Filtering test files out of references/calls is wrong — tests reference and call production code, and you usually want to see those sites. Scope the exclusion to implementations (gri) where the test mock is rarely what you're looking for, and pass nil (no filter) to the rest. The picker helper now takes an opts table so per-picker filters can be added without rewiring the closure. --- .config/nvim/plugin/40_lsp_behavior.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 976da96e..237a9bed 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -18,7 +18,8 @@ local function mappings(client, buf) utils.map(mode, lhs, rhs, options) end - local function lsp_picker(picker_fn) + local function lsp_picker(picker_fn, opts) + opts = opts or {} return function() picker_fn({ layout = { @@ -38,13 +39,15 @@ local function mappings(client, buf) }, }, focus = "list", - pattern = vim.bo.filetype == "go" and "!_test.go" or nil, + pattern = opts.pattern, }) end end + local go_test_exclude = vim.bo.filetype == "go" and "!_test.go" or nil + -- See `:help vim.lsp.*` for documentation on any of the below functions () - bmap('n', 'gri', lsp_picker(Snacks.picker.lsp_implementations), { desc = "Go to implementation" }) -- vim.lsp.buf.implementation + bmap('n', 'gri', lsp_picker(Snacks.picker.lsp_implementations, { pattern = go_test_exclude }), { desc = "Go to implementation" }) -- vim.lsp.buf.implementation bmap('n', 'grr', lsp_picker(Snacks.picker.lsp_references), { desc = "Go to reference" }) -- vim.lsp.buf.references bmap('n', 'gS', Snacks.picker.lsp_workspace_symbols, { desc = "Goto workspace symbols" }) From e74f51ec764097b390e99256896bb1398a3b1d00 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:17 +0900 Subject: [PATCH 053/366] chore: fix gitignore --- .config/git/ignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/git/ignore b/.config/git/ignore index 9351362a..511cfcff 100644 --- a/.config/git/ignore +++ b/.config/git/ignore @@ -40,7 +40,7 @@ __pycache__ # AI tools — project-local state, config, agent artifacts .claude/ -.pi/ +.pi/memory/ .cursor/ # OS artifacts From 8686244742120f89521e3e8b01aca41521ece535 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:17 +0900 Subject: [PATCH 054/366] fix(nvim/ui.open): don't mangle absolute paths that look like half-URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An absolute path like /foo/bar.py matches is_half_url (it contains .py, a dot followed by 2+ letters) so the override was rewriting it to https:///foo/bar.py — three slashes, which neither opens the file nor loads as a URL. is_dir must short-circuit before is_half_url, the same way it does before the google-search fallback. Add an explicit empty elseif so the intent is obvious to the next reader and the final fallback no longer needs the 'not is_dir' guard. --- .config/nvim/plugin/10_opts.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index 915e8773..fa673169 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -130,11 +130,15 @@ vim.ui.open = (function(overridden) if not is_uri then if is_dir and path:sub(1, 1) == '~' then path = vim.fn.expand(path) + elseif is_dir then + -- Absolute path with no tilde: leave as-is so overridden() can open it + -- (e.g. /foo/bar.py must not be treated as a half-URL just because it + -- has a dot in the filename). elseif is_half_url then path = ('https://%s'):format(path) elseif is_repo then path = ('https://github.com/%s'):format(path) - elseif not is_dir then + else path = ('https://google.com/search?q=%s'):format(vim.uri_encode(path)) end end From 1aaee2204ac8074750f2747246144a2130b4b45f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:17 +0900 Subject: [PATCH 055/366] fix(nvim/keymaps): keep current buffer when bD closes all the rest %bd|e# refused to delete a modified current buffer (E89) and then dropped the user in the alternate, which is the opposite of what 'bD' advertises ('Close all buffers except current'). Force the delete, then re-open the captured path. The user still loses unsaved changes in the current buffer (this is a 'delete all' operation) but ends up where they were, not wherever the alternate landed. --- .config/nvim/plugin/20_keymaps.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 7277ebc5..1e3bfa0e 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -13,7 +13,11 @@ map("i", "", "", { desc = "Delete word backwards" }) -- For macOS map("n", "bd", "bdelete", { desc = "Close current buffer" }) -map("n", "bD", "%bd|e#", { desc = "Close all buffers except current" }) +map("n", "bD", function() + local cur_path = vim.api.nvim_buf_get_name(0) + vim.cmd("%bd!") + if cur_path ~= "" then vim.cmd("edit " .. vim.fn.fnameescape(cur_path)) end +end, { desc = "Close all buffers except current" }) map("n", "bn", "bnext", { desc = "Next buffer" }) map("n", "bp", "bprevious", { desc = "Previous buffer" }) From 6241d986e709b57632e9105dd993d35d178c624f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:17 +0900 Subject: [PATCH 056/366] fix(nvim/lsp): re-warn when black is still missing on next format The missing_black_notified latch meant we only emitted the 'black not found' warning the first time format was triggered. After that, every subsequent = press silently no-op'd even if the user had installed black (or activated a different venv) in the meantime. The cheap check vim.fn.executable('black') is fast enough to re-run on every format call, and the resulting duplicate warnings are the right behaviour: the user actually wanted to format and we just told them why it didn't work. --- .config/nvim/plugin/41_lsp_format.lua | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 7f1c57f5..9a39f147 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -19,14 +19,10 @@ local fmt = { } local format_group = vim.api.nvim_create_augroup('lsp.format', {}) -local missing_black_notified = false local function format_python_black(buf) if vim.fn.executable('black') ~= 1 then - if not missing_black_notified then - missing_black_notified = true - vim.notify('black not found; install it in .venv or on PATH', vim.log.levels.WARN) - end + vim.notify('black not found; install it in .venv or on PATH', vim.log.levels.WARN) return end From f9589dfb56024c3b539515409c22a0971a8c5094 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 00:59:37 +0900 Subject: [PATCH 057/366] refactor(nvim/keymaps): lazy-load nvim.undotree behind the keymap packadd nvim.undotree runs at every nvim startup even though almost nobody hits u in a given session. Move the packadd inside the keymap callback so the bundled plugin (and its treesitter/UI setup) only loads when the user actually asks for the undo tree. --- .config/nvim/plugin/20_keymaps.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 1e3bfa0e..86bfa81e 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -121,5 +121,7 @@ end, { expr = true }) -- https://www.reddit.com/r/neovim/comments/1mxeghf/using_as_a_multipurpose_search_tool/ map("x", "/", "/\\%V") -- `:h /\%V` -vim.cmd("packadd nvim.undotree") -map("n", "u", require("undotree").open) +map("n", "u", function() + vim.cmd("packadd nvim.undotree") + require("undotree").open() +end, { desc = "Undo tree" }) From c6f8177f820b496f166ce0893dbce9364731e7ad Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:03:12 +0900 Subject: [PATCH 058/366] =?UTF-8?q?fix(nvim/lsp/lua=5Fls):=20use=20vim.fs.?= =?UTF-8?q?joinpath,=20rename=20path=E2=86=92ws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concatenating workspace_folders[1].name with '/' produces a double slash when the name already ends in '/' (which it does on some filesystems and platforms). vim.fs.joinpath normalises the result. Rename the local to ws to stop shadowing the path() builtin; that makes the rest of the function easier to read. --- .config/nvim/after/lsp/lua_ls.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.config/nvim/after/lsp/lua_ls.lua b/.config/nvim/after/lsp/lua_ls.lua index c24721c7..57f5d260 100644 --- a/.config/nvim/after/lsp/lua_ls.lua +++ b/.config/nvim/after/lsp/lua_ls.lua @@ -2,10 +2,11 @@ return { on_init = function(client) if client.workspace_folders and client.workspace_folders[1] then - local path = client.workspace_folders[1].name + local ws = client.workspace_folders[1].name if - path ~= vim.fn.stdpath('config') - and (vim.uv.fs_stat(path .. '/.luarc.json') or vim.uv.fs_stat(path .. '/.luarc.jsonc')) + ws ~= vim.fn.stdpath('config') + and (vim.uv.fs_stat(vim.fs.joinpath(ws, '.luarc.json')) + or vim.uv.fs_stat(vim.fs.joinpath(ws, '.luarc.jsonc'))) then return end From acb5f6e2bfdd5fc138ac49df9eb47209417e2ebe Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:10:08 +0900 Subject: [PATCH 059/366] fix(nvim/lsp): bind grx to vim.lsp.codelens.run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vim.lsp.codelens.enable(true) at startup made the data plumbing work but the user had no way to actually trigger a codelens — the comment next to the LspAttach block documented grx for this, the keymap was never added. Bind it now. --- .config/nvim/plugin/40_lsp_behavior.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 237a9bed..4b7cfcfb 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -55,7 +55,7 @@ local function mappings(client, buf) bmap('n', 'gd', Snacks.picker.lsp_definitions, { desc = "Go to definition" }) -- vim.lsp.buf.definition bmap('n', 'gs', Snacks.picker.lsp_symbols, { desc = "Goto symbols" }) - -- builitin "grt" for type definitions, grn for rename, grx for vim.lsp.codelens.run + bmap('n', 'grx', vim.lsp.codelens.run, { desc = 'Run codelens' }) bmap('n', 'gai', lsp_picker(Snacks.picker.lsp_incoming_calls), { desc = "C[a]lls Incoming" }) bmap('n', 'gao', lsp_picker(Snacks.picker.lsp_outgoing_calls), { desc = "C[a]lls Outgoing" }) From 4a5cf7844cff4eb4c523a915da8a483159c783f7 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:11:30 +0900 Subject: [PATCH 060/366] fix(nvim/lsp): guard LspProgress handler against non-work-done values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ev.data.params.value can be any LSPAny — the only work-done progress shape we know how to render is a table with at least a 'kind' field. Indexing a non-table (e.g. a string the server decided to send) crashes the handler and floods :messages. --- .config/nvim/plugin/78_lsp.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index 840664fa..f4d2f8aa 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -46,6 +46,7 @@ vim.lsp.codelens.enable(true) _G.Config.new_autocmd('LspProgress', { callback = function(ev) local value = ev.data.params.value + if type(value) ~= 'table' or not value.kind then return end vim.api.nvim_echo({ { value.message or 'done' } }, false, { id = 'lsp.' .. ev.data.client_id, kind = 'progress', From 19ec8509c59ff0aeed9a8aff001261318c15191c Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:13:08 +0900 Subject: [PATCH 061/366] fix(nvim/lsp): clear UserLspConfig on (re)load The group was created with clear=false, so :luafile'ing the config or restarting the LSP stack would stack another LspAttach/LspDetach handler on top of the old one. Each attach then ran the mappings + references setup multiple times, re-registering the same buffer-local keymaps and creating overlapping highlight autocmd groups. --- .config/nvim/plugin/40_lsp_behavior.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 4b7cfcfb..7c073b7b 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -6,7 +6,7 @@ end vim.g.diagnostics_visible = true local utils = require('utils') -local UserLspConfig = vim.api.nvim_create_augroup('UserLspConfig', {}) +local UserLspConfig = vim.api.nvim_create_augroup('UserLspConfig', { clear = true }) ---@param client vim.lsp.Client ---@param buf number From cb2957d797de252c46665ee85f44ba1a06cf9af5 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:16:03 +0900 Subject: [PATCH 062/366] fix(nvim/lsp): clear lsp.format augroup on (re)load Same reasoning as UserLspConfig: without clear=true, every :luafile of this plugin stacked another LspDetach cleanup handler. The per- buffer 'lsp.format..' groups were already cleared on each set_format_on_save call, so this was the missing piece. --- .config/nvim/plugin/41_lsp_format.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 9a39f147..03b0417a 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -18,7 +18,7 @@ local fmt = { typescriptreact = "tsgo", } -local format_group = vim.api.nvim_create_augroup('lsp.format', {}) +local format_group = vim.api.nvim_create_augroup('lsp.format', { clear = true }) local function format_python_black(buf) if vim.fn.executable('black') ~= 1 then From 88eb4bbbfa991a384e4ce353329b58ef834e5339 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:21:38 +0900 Subject: [PATCH 063/366] fix(nvim/lsp): handle errors from apply_workspace_edit on save If gopls (or any LSP) returns a workspace edit that nvim can't apply (e.g. document version mismatch, stale buffer, or some LSP-specific quirk), the bare apply_workspace_edit raises inside the async codeAction handler. That tears the format-on-save flow down, leaves the buffer un-formatted, and the user gets a raw stack trace in :messages. Wrap the call in pcall, log a single warning, and let the rest of the callback (format + write) continue. --- .config/nvim/plugin/41_lsp_format.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 03b0417a..0dd6ec05 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -61,7 +61,10 @@ local function organize_go_imports(buf, client) for _, res in pairs(results or {}) do for _, action in pairs(res.result or {}) do if action.edit then - vim.lsp.util.apply_workspace_edit(action.edit, client.offset_encoding) + local ok, err = pcall(vim.lsp.util.apply_workspace_edit, action.edit, client.offset_encoding) + if not ok then + vim.notify("organizeImports edit failed: " .. tostring(err), vim.log.levels.WARN) + end end if action.command then client:exec_cmd(action.command) From 8d82965e337fe04f5f213d4fab38318c58f98c44 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:29:18 +0900 Subject: [PATCH 064/366] fix(nvim/lsp): run organize_go_imports synchronously to avoid double-write The old flow on ':write' of a Go file was: BufWritePre -> buf_request_all (async, returns immediately) BufWritePre returns original ':write' writes the unformatted buffer ...later... codeAction response arrives apply edit, format, noautocmd write (second write) The end state was right but the file went through an unformatted write in between, which trips formatting hooks (pre-commit, save hooks in the user's editor, etc.) on the first pass. Use vim.lsp.buf_request_sync instead. Save is rare and the editor freezing for up to 1s is acceptable; the file is now written exactly once, in its formatted state. --- .config/nvim/plugin/41_lsp_format.lua | 34 +++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 0dd6ec05..25fd04cf 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -57,27 +57,27 @@ end local function organize_go_imports(buf, client) local params = vim.lsp.util.make_range_params(nil, client.offset_encoding) params.context = { only = { 'source.organizeImports' } } - vim.lsp.buf_request_all(buf, 'textDocument/codeAction', params, function(results) - for _, res in pairs(results or {}) do - for _, action in pairs(res.result or {}) do - if action.edit then - local ok, err = pcall(vim.lsp.util.apply_workspace_edit, action.edit, client.offset_encoding) - if not ok then - vim.notify("organizeImports edit failed: " .. tostring(err), vim.log.levels.WARN) - end - end - if action.command then - client:exec_cmd(action.command) + local results, err = vim.lsp.buf_request_sync(buf, 'textDocument/codeAction', params, 1000) + if not results then + vim.notify("organizeImports request failed: " .. tostring(err), vim.log.levels.WARN) + return + end + for _, res in pairs(results) do + for _, action in pairs(res.result or {}) do + if action.edit then + local ok, e = pcall(vim.lsp.util.apply_workspace_edit, action.edit, client.offset_encoding) + if not ok then + vim.notify("organizeImports edit failed: " .. tostring(e), vim.log.levels.WARN) end end + if action.command then + client:exec_cmd(action.command) + end end + end + if vim.api.nvim_buf_is_valid(buf) then vim.lsp.buf.format({ bufnr = buf, name = client.name, timeout_ms = 1000 }) - if vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].modified then - vim.api.nvim_buf_call(buf, function() - vim.cmd('noautocmd write') - end) - end - end) + end end local function set_format_on_save(buf, client, callback) From 72ca72970e07fa1bacb68e83c6ce2f6b46872f4e Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:36:16 +0900 Subject: [PATCH 065/366] fix(nvim/gitgud): surface gh's stderr on permalink errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vim.system() captures stdout and stderr separately, but the function only returned result.code alongside stdout (which on gh failure is empty). copy_github_permalink and open_github_file then notified 'gitgud: ' (empty) instead of the actual reason — usually 'not in a git repo' or 'no upstream configured'. Capture stderr, return it as a third value, and include it in the notification. Fall back to 'gh exited with code N' when stderr is empty (e.g. the binary isn't on PATH and vim.system reports the launch failure that way). --- .config/nvim/lua/custom/gitgud.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.config/nvim/lua/custom/gitgud.lua b/.config/nvim/lua/custom/gitgud.lua index 73599b1c..527d88cc 100644 --- a/.config/nvim/lua/custom/gitgud.lua +++ b/.config/nvim/lua/custom/gitgud.lua @@ -32,16 +32,17 @@ local function get_github_url(opts) local result = vim.system(cmd, { text = true }):wait() local url = vim.fn.trim(result.stdout or "") + local stderr = vim.fn.trim(result.stderr or "") - return url, result.code + return url, result.code, stderr end ---@param opts? {start_line?: number, end_line?: number} function M.copy_github_permalink(opts) - local url, err = get_github_url(opts) + local url, err, stderr = get_github_url(opts) if err ~= 0 then - vim.notify("gitgud: " .. url, vim.log.levels.ERROR) + vim.notify("gitgud: " .. (stderr ~= "" and stderr or ("gh exited with code " .. err)), vim.log.levels.ERROR) return end @@ -51,10 +52,10 @@ end ---@param opts? {start_line?: number, end_line?: number} function M.open_github_file(opts) - local url, err = get_github_url(opts) + local url, err, stderr = get_github_url(opts) if err ~= 0 then - vim.notify("gitgud: " .. url, vim.log.levels.ERROR) + vim.notify("gitgud: " .. (stderr ~= "" and stderr or ("gh exited with code " .. err)), vim.log.levels.ERROR) return end From 0857e75f5e8e2cc1c375eb45f78c9f8577b48148 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:38:09 +0900 Subject: [PATCH 066/366] =?UTF-8?q?fix(nvim/ai):=20rename=20local=20job?= =?UTF-8?q?=E2=86=92job=5Fid=20in=20focus=5Fpopup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local was called 'job' which shadows nothing here but trips readers who expect 'job' to mean the detached job handle we never actually retain. Rename to job_id for clarity. --- .config/nvim/lua/custom/ai.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/nvim/lua/custom/ai.lua b/.config/nvim/lua/custom/ai.lua index 2af16aba..28cffcf7 100644 --- a/.config/nvim/lua/custom/ai.lua +++ b/.config/nvim/lua/custom/ai.lua @@ -36,11 +36,11 @@ end local function focus_popup(tool) local popup = vim.fn.expand("~/.config/tmux/session-popup") - local job = vim.fn.jobstart({ "tmux", "display-popup", "-T", tool, "-w", "95%", "-h", "95%", "-E", popup, tool }, { + local job_id = vim.fn.jobstart({ "tmux", "display-popup", "-T", tool, "-w", "95%", "-h", "95%", "-E", popup, tool }, { detach = true, }) - if job <= 0 then + if job_id <= 0 then vim.notify("Failed to focus " .. tool .. " popup", vim.log.levels.ERROR) end end From f7afd222c99488b1c1fe47ff51fb23560a29ff2f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:44:33 +0900 Subject: [PATCH 067/366] fix(nvim/lsp): scope LspProgress echo id by params.token nvim_echo's id identifies a progress stream. Using just client_id makes every progress message from the same client overwrite the previous one in the message area, so concurrent work (e.g. gopls indexing + a code action) clobber each other. The LspProgress docs example (':h LspProgress') uses 'lsp.' .. ev.data.params.token so each work-done progress gets its own slot and updates in place. Match that. --- .config/nvim/plugin/78_lsp.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index f4d2f8aa..fce49fcf 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -47,8 +47,9 @@ _G.Config.new_autocmd('LspProgress', { callback = function(ev) local value = ev.data.params.value if type(value) ~= 'table' or not value.kind then return end + local token = ev.data.params and ev.data.params.token vim.api.nvim_echo({ { value.message or 'done' } }, false, { - id = 'lsp.' .. ev.data.client_id, + id = 'lsp.' .. ev.data.client_id .. (token and ('.' .. token) or ''), kind = 'progress', source = 'vim.lsp', title = value.title, From 99b53038e00349b95cdcc0b63a34a833c6455e60 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:46:26 +0900 Subject: [PATCH 068/366] fix(nvim/gitgud): resolve file path against the repo root, not cwd gh browse interprets file args relative to the repository root, but expand('%:.') is relative to getcwd(). Open a buffer in a subdir of the repo and Go / Gl would 404 because the path walks from the subdir, not the worktree root. Resolve the path via 'git ls-files --full-name -- ' so it is anchored at the worktree root regardless of cwd. Falls back to expand('%:.') for untracked files; gh refuses those anyway, and permalinks for uncommitted code are not a real use case. --- .config/nvim/lua/custom/gitgud.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.config/nvim/lua/custom/gitgud.lua b/.config/nvim/lua/custom/gitgud.lua index 527d88cc..facedd54 100644 --- a/.config/nvim/lua/custom/gitgud.lua +++ b/.config/nvim/lua/custom/gitgud.lua @@ -3,7 +3,13 @@ local M = {} local function get_github_url(opts) opts = opts or {} - local file_path = vim.fn.expand("%:.") + -- gh browse takes paths relative to the repo root, but expand('%:.') is + -- relative to cwd. A buffer in a subdir would 404. Resolve through git + -- so the path is anchored at the worktree root regardless of cwd. + -- Untracked files fall back to expand('%:.'): gh refuses them anyway, + -- and permalinks for uncommitted code don't make sense. + local file_path = (vim.trim(vim.system({ "git", "ls-files", "--full-name", "--", vim.fn.expand("%:p") }, { text = true }):wait().stdout or ""):match("^[^ +]+")) or vim.fn.expand("%:.") local start_line = tonumber(opts.start_line) or vim.fn.line(".") local end_line = opts.end_line and tonumber(opts.end_line) or nil From 5726755532ad65ffd47b2185cae6b03798ee7837 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:46:56 +0900 Subject: [PATCH 069/366] fix(nvim/ai): stop shell-escaping the path sent to the AI popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send_to_pane writes bytes into a tmux paste-buffer; the pane pastes them verbatim. shellescape wraps the path in single quotes, so the popup would receive "@'~/file'" — the quotes and the literal backslashes (for any escaped chars like $ or \\) end up typed into the AI tool. The path is consumed as data, not as a shell command line, so no quoting is needed. Drop shellescape and send the path bare after the '@' marker. --- .config/nvim/lua/custom/ai.lua | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.config/nvim/lua/custom/ai.lua b/.config/nvim/lua/custom/ai.lua index 28cffcf7..0880a936 100644 --- a/.config/nvim/lua/custom/ai.lua +++ b/.config/nvim/lua/custom/ai.lua @@ -15,10 +15,13 @@ local function target_pane(session_name) return ok and stdout:match("[^\n]+") or nil end --- Send a path to a tmux pane using shellescape for safe quoting. +-- Send a path to a tmux pane via tmux paste-buffer. The bytes are written +-- verbatim into the target pane, so shellescape is wrong: it would wrap the +-- path in single quotes, and any backslash-escaped chars would land in the +-- pane as backslashes. local function send_to_pane(pane_id, path) local buffer = "nvim-ai-file-" .. pane_id:gsub("^%%", "") - local payload = "@" .. vim.fn.shellescape(path) .. "\n" + local payload = "@" .. path .. "\n" local ok, _, err = tmux({ "load-buffer", "-b", buffer, "-" }, payload) if not ok then vim.notify("Failed to load tmux buffer: " .. err, vim.log.levels.ERROR) From 6a012f67d3135f68829a42a6600884af43927a78 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 01:59:09 +0900 Subject: [PATCH 070/366] Revert "fix(nvim/gitgud): resolve file path against the repo root, not cwd" This reverts commit a0be7126927600bc5dcf84fe9ad3c9dab5d75e10. --- .config/nvim/lua/custom/gitgud.lua | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.config/nvim/lua/custom/gitgud.lua b/.config/nvim/lua/custom/gitgud.lua index facedd54..527d88cc 100644 --- a/.config/nvim/lua/custom/gitgud.lua +++ b/.config/nvim/lua/custom/gitgud.lua @@ -3,13 +3,7 @@ local M = {} local function get_github_url(opts) opts = opts or {} - -- gh browse takes paths relative to the repo root, but expand('%:.') is - -- relative to cwd. A buffer in a subdir would 404. Resolve through git - -- so the path is anchored at the worktree root regardless of cwd. - -- Untracked files fall back to expand('%:.'): gh refuses them anyway, - -- and permalinks for uncommitted code don't make sense. - local file_path = (vim.trim(vim.system({ "git", "ls-files", "--full-name", "--", vim.fn.expand("%:p") }, { text = true }):wait().stdout or ""):match("^[^ -]+")) or vim.fn.expand("%:.") + local file_path = vim.fn.expand("%:.") local start_line = tonumber(opts.start_line) or vim.fn.line(".") local end_line = opts.end_line and tonumber(opts.end_line) or nil From 435d6c30b8a00ed9021a6843a9cf379eaf5b8930 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:16:15 +0900 Subject: [PATCH 071/366] fix(nvim/lsp): only set tagfunc/formatexpr when buffer option is empty The LspAttach handler unconditionally set v:lua.vim.lsp.tagfunc and v:lua.vim.lsp.formatexpr() if the server advertised the corresponding capability. nvim's own LspAttach (lsp._set_defaults) already does the right thing: it uses is_empty_or_default so a user-customized tagfunc/formatexpr is preserved. Our handler ran AFTER nvim's, so on every attach it stomped the user's setting. Guard with the same empty-check (empty == not customized; the LSP-set value is non-empty and thus naturally idempotent on re-attach). --- .config/nvim/plugin/40_lsp_behavior.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 7c073b7b..e3e2f7c4 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -176,10 +176,15 @@ _G.Config.new_autocmd('LspAttach', { -- Taken from https://neovim.io/doc/user/lsp.html :h lsp - if client.server_capabilities.definitionProvider then + -- Only set the LSP funcs when the option is empty/default; otherwise + -- we'd clobber a user-customized tagfunc/formatexpr every time a + -- client attaches. server_capabilities is optional on the client type + -- (it's nil pre-initialize) so guard that too. + local caps = client.server_capabilities or {} + if caps.definitionProvider and vim.bo[args.buf].tagfunc == '' then vim.bo[args.buf].tagfunc = "v:lua.vim.lsp.tagfunc" end - if client.server_capabilities.documentFormattingProvider then + if caps.documentFormattingProvider and vim.bo[args.buf].formatexpr == '' then vim.bo[args.buf].formatexpr = "v:lua.vim.lsp.formatexpr()" end From 5761bc362a70f00b17b95a34a645735e32481e74 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:21:18 +0900 Subject: [PATCH 072/366] fix(nvim/git): reuse existing buffer for blame detail CodeDiff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bufadd() on a file_path that doesn't already have a buffer creates a fresh buffer every time the user opens a blame detail on a different file. Those buffers hang around in ':ls' indefinitely — a real leak when reviewing many files via blame. If the buffer is already known (user has the file open in any window), bufadd() returns the existing handle, so the check just reuses it. bufadd() is only called for the truly-new path. --- .config/nvim/plugin/73_git.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/73_git.lua b/.config/nvim/plugin/73_git.lua index e1555480..14023205 100644 --- a/.config/nvim/plugin/73_git.lua +++ b/.config/nvim/plugin/73_git.lua @@ -36,8 +36,15 @@ require('blame').setup({ end local parent = commit_hash .. "^" - local buf = vim.fn.bufadd(file_path) - vim.fn.bufload(buf) + -- Only allocate a new buffer if one doesn't already exist for the + -- file; bufadd() leaks a hidden buffer into the list every time + -- the user opens a blame detail on a different file. If the buffer + -- is already known, reuse it. + local buf = vim.fn.bufnr(file_path) + if buf == -1 then + buf = vim.fn.bufadd(file_path) + vim.fn.bufload(buf) + end local ok, err = pcall(vim.api.nvim_buf_call, buf, function() vim.cmd(("CodeDiff file %s %s"):format(parent, commit_hash)) From 1f437055fd0f5cbddd3eaa136b9e3814b62d1792 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 02:31:04 +0900 Subject: [PATCH 073/366] refactor(nvim/lsp): hoist picker layout out of the lsp_picker closure The lsp_picker closure rebuilt the entire nested layout table on every 'gri'/'grr'/'gai'/'gao' keypress, even though the spec is identical for every invocation. Hoist the table to a local in mappings() so we allocate it once per buffer attach and reuse it on every call. --- .config/nvim/plugin/40_lsp_behavior.lua | 39 ++++++++++++------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index e3e2f7c4..5dbc0db0 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -18,29 +18,28 @@ local function mappings(client, buf) utils.map(mode, lhs, rhs, options) end + -- ponytail: the layout spec is identical for every picker invocation; + -- allocate it once at attach time instead of per keystroke. + local picker_layout = { + layout = { + backdrop = false, + width = 0.5, + min_width = 80, + height = 0.8, + min_height = 30, + box = "vertical", + border = true, + title = "{title} {live} {flags}", + title_pos = "center", + { win = "input", height = 1, border = "bottom" }, + { win = "list", border = "none" }, + { win = "preview", title = "{preview}", height = 0.4, border = "top" }, + }, + } local function lsp_picker(picker_fn, opts) opts = opts or {} return function() - picker_fn({ - layout = { - layout = { - backdrop = false, - width = 0.5, - min_width = 80, - height = 0.8, - min_height = 30, - box = "vertical", - border = true, - title = "{title} {live} {flags}", - title_pos = "center", - { win = "input", height = 1, border = "bottom" }, - { win = "list", border = "none" }, - { win = "preview", title = "{preview}", height = 0.4, border = "top" }, - }, - }, - focus = "list", - pattern = opts.pattern, - }) + picker_fn({ layout = picker_layout, focus = "list", pattern = opts.pattern }) end end From 230a2d963a31add002806769026960a9e31d3f0a Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 08:46:44 +0900 Subject: [PATCH 074/366] fix(aliasrc): detect URLs without /tree/ in yoink parser The 55b260a fix added a --branch branch to gh repo clone when the parsed 'branch' was non-empty, but the parser leaves branch='owner' when the URL has no /tree/ (since ${p#*/tree/} is a no-op then). That made the default-branch documented use case pass --branch 'owner' to gh, which rejects it. Guard the /tree/ branch with a substring check; outside it, leave branch and subpath empty so gh uses the default branch and the sparse-checkout is skipped. --- home/.aliasrc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/home/.aliasrc b/home/.aliasrc index d6e47f8d..d3ccc669 100644 --- a/home/.aliasrc +++ b/home/.aliasrc @@ -167,9 +167,14 @@ yoink() { local url=$1 dest=$2 [[ -e $dest ]] && { echo "yoink: $dest already exists" >&2; return 1; } local p=${url#https://github.com/} - local owner_repo=${p%%/tree/*} branch_path=${p#*/tree/} - local branch=${branch_path%%/*} subpath=${branch_path#*/} - [[ $subpath == "$branch_path" ]] && subpath="" + local owner_repo branch subpath + if [[ $p == *"/tree/"* ]]; then + owner_repo=${p%%/tree/*} branch_path=${p#*/tree/} + branch=${branch_path%%/*} subpath=${branch_path#*/} + [[ $subpath == "$branch_path" ]] && subpath="" + else + owner_repo=$p branch="" subpath="" + fi local tmp; tmp=$(mktemp -d) if [[ -n $branch ]]; then gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse --branch "$branch" 2>/dev/null From f2b3bf9d3214d54358bf727e194b388854422cd2 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:46:10 +0900 Subject: [PATCH 075/366] chore: fix --- .local/bin/tmux-go-session | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.local/bin/tmux-go-session b/.local/bin/tmux-go-session index 653693bb..f7e8fb4d 100755 --- a/.local/bin/tmux-go-session +++ b/.local/bin/tmux-go-session @@ -46,7 +46,7 @@ build_project_rows() { declare -a paths bases while IFS= read -r p; do paths+=("$p") - b=$(basename "$p") + b="${p##*/}" bases+=("$b") base_count["$b"]=$(( ${base_count["$b"]:-0} + 1 )) done < "$project_paths_file" @@ -55,11 +55,14 @@ build_project_rows() { for i in "${!paths[@]}"; do path="${paths[$i]}"; b="${bases[$i]}" if [[ ${base_count["$b"]:-0} -gt 1 ]]; then - unsanitized="$(basename "$(dirname "$path")")_${b}" + local parent="${path%/*}" + unsanitized="${parent##*/}_${b}" else unsanitized="$b" fi - s=$(printf '%s' "$unsanitized" | tr -c '[:alnum:]_-' '_' | sed 's/^_*//; s/_*$//') + s="${unsanitized//[^[:alnum:]_-]/_}" + while [[ $s == _* ]]; do s="${s#_}"; done + while [[ $s == *_ ]]; do s="${s%_}"; done [[ -z $s ]] && { echo "Cannot derive a tmux session name for project: $path" >&2; return 1; } if [[ -n ${seen[$s]:-} && ${seen[$s]:-} != "$path" ]]; then From 862cbec6d899f4946fb5ab3d70c1134569178707 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 11:56:35 +0900 Subject: [PATCH 076/366] chore: somplify things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors, deletions, and one security fix from a repo-wide over-engineering audit. Pure cleanup; no behavior change other than the tokenrouter key. nvim - lsp/40: hoist lsp_picker_layout to module scope so it's allocated once, not per LspAttach (was: nested inside mappings()). - snacks/75: dedupe Colemak n=down/e=up keymap; share one table for input.keys and list.keys via vim.tbl_extend. - opts/10: drop the 17-line vim.ui.open heuristic override. Default vim.ui.open + the existing gx for URLs is enough. - footer: rebuild the session-token string on turn_end (one walk per LLM response) rather than on every render. shell / scripts - profile: add ~/.local/share/nvim/mason/bin to PATH at lowest priority, then drop the three one-liner mason exec shims (svelteserver, lua-language-server, tsgo). - aliasrc: drop the duplicate vset alias; shrink yoink from 35 lines to 13 (URL parser in 3 lines, no separate branch/subpath handling code). - local/bin/wific: rewrite to use fzf instead of whiptail (1990s dialog). - local/bin/unarchive: use tar --one-top-level via gtar on macOS for archives with a single top-level dir; drop the heuristic choose_dest that often guessed wrong. - delete: local/bin/gruntest, local/bin/keys (no callers). config - tmux.conf: trim 6-line "Keep this palette aligned..." comment block to one line. (The set -aF palette dedupe from the audit isn't possible in tmux — set -aF format variables only expand in format-string contexts, not in set -g style options. Pushback logged.) - zathurarc: drop the dead "light theme" comment that had no code. - codex/AGENTS.md: byte-for-byte duplicate of .claude/CLAUDE.md. Replaced with a symlink. security - tokenrouter: apiKey was a literal sk-... checked into git. Switched to the documented $ENV_VAR form ($TOKENROUTER_API_KEY). The literal key remains in git history — rotate the key at tokenrouter.com. --- .config/nvim/plugin/10_opts.lua | 29 ----- .config/nvim/plugin/40_lsp_behavior.lua | 37 +++--- .config/tmux/tmux.conf | 6 +- .config/zathura/zathurarc | 4 - .local/bin/gruntest | 123 ------------------ .local/bin/keys | 48 ------- .local/bin/lua-language-server | 2 - .local/bin/svelteserver | 2 - .local/bin/tsgo | 2 - .local/bin/unarchive | 78 +++++------ .local/bin/wific | 59 ++++----- home/.aliasrc | 43 ++---- home/.codex/AGENTS.md | 1 + home/.pi/agent/extensions/footer.ts | 16 ++- .../.pi/agent/extensions/tokenrouter/index.ts | 2 +- home/.profile | 2 +- 16 files changed, 108 insertions(+), 346 deletions(-) delete mode 100755 .local/bin/gruntest delete mode 100755 .local/bin/keys delete mode 100755 .local/bin/lua-language-server delete mode 100755 .local/bin/svelteserver delete mode 100755 .local/bin/tsgo create mode 120000 home/.codex/AGENTS.md diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index fa673169..871de61c 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -116,32 +116,3 @@ vim.diagnostic.config({ source = "if_many", }, }) - - -vim.ui.open = (function(overridden) - return function(path) - vim.validate({ - path = { path, 'string' }, - }) - local is_uri = path:match('%w+:') - local is_half_url = path:match('%.%a%a+') - local is_repo = vim.bo.filetype == 'lua' and path:match('%w/%w') and vim.fn.count(path, '/') == 1 - local is_dir = path:match('^/') or path:match('^~') - if not is_uri then - if is_dir and path:sub(1, 1) == '~' then - path = vim.fn.expand(path) - elseif is_dir then - -- Absolute path with no tilde: leave as-is so overridden() can open it - -- (e.g. /foo/bar.py must not be treated as a half-URL just because it - -- has a dot in the filename). - elseif is_half_url then - path = ('https://%s'):format(path) - elseif is_repo then - path = ('https://github.com/%s'):format(path) - else - path = ('https://google.com/search?q=%s'):format(vim.uri_encode(path)) - end - end - overridden(path) - end -end)(vim.ui.open) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 5dbc0db0..305e673a 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -8,6 +8,23 @@ local utils = require('utils') local UserLspConfig = vim.api.nvim_create_augroup('UserLspConfig', { clear = true }) +local lsp_picker_layout = { + layout = { + backdrop = false, + width = 0.5, + min_width = 80, + height = 0.8, + min_height = 30, + box = "vertical", + border = true, + title = "{title} {live} {flags}", + title_pos = "center", + { win = "input", height = 1, border = "bottom" }, + { win = "list", border = "none" }, + { win = "preview", title = "{preview}", height = 0.4, border = "top" }, + }, +} + ---@param client vim.lsp.Client ---@param buf number local function mappings(client, buf) @@ -18,28 +35,10 @@ local function mappings(client, buf) utils.map(mode, lhs, rhs, options) end - -- ponytail: the layout spec is identical for every picker invocation; - -- allocate it once at attach time instead of per keystroke. - local picker_layout = { - layout = { - backdrop = false, - width = 0.5, - min_width = 80, - height = 0.8, - min_height = 30, - box = "vertical", - border = true, - title = "{title} {live} {flags}", - title_pos = "center", - { win = "input", height = 1, border = "bottom" }, - { win = "list", border = "none" }, - { win = "preview", title = "{preview}", height = 0.4, border = "top" }, - }, - } local function lsp_picker(picker_fn, opts) opts = opts or {} return function() - picker_fn({ layout = picker_layout, focus = "list", pattern = opts.pattern }) + picker_fn({ layout = lsp_picker_layout, focus = "list", pattern = opts.pattern }) end end diff --git a/.config/tmux/tmux.conf b/.config/tmux/tmux.conf index 1c3ec5e0..fd6be9be 100644 --- a/.config/tmux/tmux.conf +++ b/.config/tmux/tmux.conf @@ -100,11 +100,7 @@ bind-key -r -T prefix j choose-window -Z "join-pane -s "%%"" bind-key -T prefix R source-file ~/.config/tmux/tmux.conf \; display-message "source-file done" # ----------------------------=== Theme ===-------------------------- -# AI: Keep this tmux palette aligned with the PS1 colors in home/.bashrc, -# LS_COLORS in home/.profile, and the Ghostty palette in .config/ghostty/config. -# Shared xterm-256 colors: slate=245 red=203 green=114 yellow=179 blue=75 purple=141 cyan=109. -# Get colors with: -# for i in {0..255}; do printf "\x1b[38;5;${i}mcolor%-5i\x1b[0m" $i ; if ! (( ($i - 3) % 6 )); then echo ; fi ; done +# xterm-256 colors match PS1/LS_COLORS/ghostty: 245,203,114,179,75,141,109 set-option -g status "on" set -g status-position top diff --git a/.config/zathura/zathurarc b/.config/zathura/zathurarc index d47407fe..086256fc 100644 --- a/.config/zathura/zathurarc +++ b/.config/zathura/zathurarc @@ -2,11 +2,7 @@ set selection-clipboard clipboard set recolor set recolor-keephue -# Tokyonight color theme for Zathura -# Swaps Foreground for Background to get a light version if the user prefers -# # Tokyonight moon color theme -# set notification-error-bg "#ff757f" set notification-error-fg "#c8d3f5" set notification-warning-bg "#ffc777" diff --git a/.local/bin/gruntest b/.local/bin/gruntest deleted file mode 100755 index 73acbf08..00000000 --- a/.local/bin/gruntest +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -export LAST_TESTS_FILE="/tmp/grun_last_tests" -export LAST_FAILED_TEST_FILE="/tmp/grun_last_failed_test" -export GO_TEST_FLAGS_FILE="/tmp/go_test_flags" - -export GREY='\e[90m' -export WHITE='\e[97m' -export RESET='\e[0m' -export RED='\e[91m' -export GREEN='\e[92m' - -export GO_TEST_FLAGS=() - -set_go_test_flags() { - if [[ $* == *"--"* ]]; then - for i in "$@"; do - if [[ $i == "--" ]]; then - shift - break - fi - shift - done - GO_TEST_FLAGS=("$@") - printf "%s\n" "${GO_TEST_FLAGS[@]}" | grep '\S' >"$GO_TEST_FLAGS_FILE" || true - else - touch "$GO_TEST_FLAGS_FILE" - fi -} - -run_tests() { - set -euo pipefail - declare -A pkg_tests - tests=("$@") - mapfile -t GO_TEST_FLAGS <"$GO_TEST_FLAGS_FILE" - - # Group tests by package - for test in "${tests[@]}"; do - pkg=$(echo "$test" | cut -d" " -f1) - t=$(echo "$test" | cut -d" " -f2) - pkg_tests["$pkg"]+="$t|" - done - - for pkg in "${!pkg_tests[@]}"; do - test_pattern="${pkg_tests[$pkg]}" - test_pattern="${test_pattern%|}" # Remove trailing '|' - - echo -e "Running tests ${WHITE}${test_pattern}${RESET} from ${GREY}$pkg${RESET}" - if command -v gotestsum &>/dev/null; then - if ! gotestsum --format dots-v2 --packages "$pkg" -- -run "$test_pattern" "${GO_TEST_FLAGS[@]}"; then - echo -e "${RED}Test execution failed. Stopping further tests.${RESET}" - echo "$pkg $test_pattern" >"$LAST_FAILED_TEST_FILE" - exit 1 - fi - else - if ! go test "${GO_TEST_FLAGS[@]}" -run "$test_pattern" "$pkg" | grep -v "no tests to run"; then - echo -e "${RED}Test execution failed. Stopping further tests.${RESET}" - echo "$pkg $test_pattern" >"$LAST_FAILED_TEST_FILE" - exit 1 - fi - fi - done - rm -f "$LAST_FAILED_TEST_FILE" -} - -export -f run_tests - -handle_subcommand() { - local subcommand="$1" - # Stupid hack to shift the arguments if subcommand empty - # but the go flags are present - if [[ $1 != "--" ]]; then - shift - fi - # xargs -d is GNU-only. On Darwin, use the GNU xargs from brew findutils - # (gxargs); on Linux, use the system xargs (which is GNU). Matches the - # gfind/find split in _projselect. - xargs() { - if [ "$(uname -s)" = "Darwin" ] && command -v gxargs >/dev/null 2>&1; then - gxargs "$@" - else - command xargs "$@" - fi - } - case "$subcommand" in - rerun) - if [[ ! -r $LAST_TESTS_FILE ]]; then - echo "No previous tests found or file is not readable." - exit 1 - fi - mapfile -t tests <"$LAST_TESTS_FILE" - ;; - retry) - if [[ ! -r $LAST_FAILED_TEST_FILE ]]; then - echo -e "${GREEN}No previous failed test cases.${RESET}" - exit 0 - fi - test=$(<"$LAST_FAILED_TEST_FILE") - echo "$test" >"$LAST_TESTS_FILE" - tests=("$test") - ;; - *) - - set_go_test_flags "$@" - go test -list \.+ -json ./... | - jq -re 'select(.Action == "output" and (.Output | startswith("T"))) | .Package + " " + .Output' | - sed '/^$/d' | fzf-tmux --multi --preview \ - "echo -e '${WHITE}{2}${RESET} ${GREY}{1}${RESET}'" --preview-window=down:3:wrap --delimiter=" " --ansi --with-nth=2 --bind 'ctrl-a:toggle-all' | - tee "$LAST_TESTS_FILE" | xargs -d '\n' bash -c 'run_tests "$@"' _ - return - ;; - esac - set_go_test_flags "$@" - run_tests "${tests[@]}" -} - -if [[ $# -gt 0 ]]; then - handle_subcommand "$@" -else - handle_subcommand "default" -fi diff --git a/.local/bin/keys b/.local/bin/keys deleted file mode 100755 index 43b50583..00000000 --- a/.local/bin/keys +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env bash -# Display CSV keybindings from ~/.config/keys/.csv -set -euo pipefail - -keys_dir="${XDG_CONFIG_HOME:-$HOME/.config}/keys" - -if [[ ${1:-} == "-l" || ${1:-} == "--list" ]]; then - for f in "$keys_dir"/*.csv; do - [[ -e $f ]] && basename "$f" .csv - done | sort - exit 0 -fi - -if [[ -z ${1:-} ]]; then - echo "Usage: keys |--list" >&2 - exit 1 -fi - -file="$keys_dir/$1.csv" -if [[ ! -r $file ]]; then - echo "No keybindings defined for '$1'." >&2 - for f in "$keys_dir"/*.csv; do - [[ -e $f ]] && echo " $(basename "$f" .csv)" - done | sort >&2 - exit 1 -fi - -echo "Key bindings for '$1':" -echo -grep -v '^[[:space:]]*#\|^[[:space:]]*$' "$file" | awk -F, ' -{ - for (i = 1; i <= NF; i++) { - gsub(/^ +| +$/, "", $i) - if (length($i) > w[i]) w[i] = length($i) - } - for (i = 1; i <= NF; i++) f[NR "," i] = $i - if (NF > cols) cols = NF - rows = NR -} -END { - for (r = 1; r <= rows; r++) { - for (i = 1; i <= cols; i++) { - v = ((r "," i) in f) ? f[r "," i] : "" - printf "%-*s ", w[i] + 2, v - } - print "" - } -}' diff --git a/.local/bin/lua-language-server b/.local/bin/lua-language-server deleted file mode 100755 index 9c20f5c7..00000000 --- a/.local/bin/lua-language-server +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -exec ~/.local/share/nvim/mason/bin/lua-language-server "$@" diff --git a/.local/bin/svelteserver b/.local/bin/svelteserver deleted file mode 100755 index 9931aa1c..00000000 --- a/.local/bin/svelteserver +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -exec ~/.local/share/nvim/mason/bin/svelteserver "$@" diff --git a/.local/bin/tsgo b/.local/bin/tsgo deleted file mode 100755 index a88388f8..00000000 --- a/.local/bin/tsgo +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -exec ~/.local/share/nvim/mason/bin/tsgo "$@" diff --git a/.local/bin/unarchive b/.local/bin/unarchive index 6a3d4f6e..dec3a1fe 100755 --- a/.local/bin/unarchive +++ b/.local/bin/unarchive @@ -1,56 +1,50 @@ #!/usr/bin/env bash +# Extract archive into a directory named after the archive, unless the +# archive already contains a single top-level entry (in which case it +# extracts in place). tar's --one-top-level handles the first case natively. + set -euo pipefail -if [ ! -r "$1" ]; then +if [[ ! -r $1 ]]; then echo "'$1' is not a readable file" >&2 exit 1 fi -# Strip archive extension(s) to derive target dir name name=$(basename "$1") case "$name" in - *.tar.bz2) name="${name%.tar.bz2}" ;; - *.tar.gz) name="${name%.tar.gz}" ;; - *.tar.xz) name="${name%.tar.xz}" ;; - *.tar.zst) name="${name%.tar.zst}" ;; - *.tar.zstd) name="${name%.tar.zstd}" ;; - *) name="${name%.*}" ;; + *.tar.bz2) name="${name%.tar.bz2}" ;; + *.tar.gz) name="${name%.tar.gz}" ;; + *.tar.xz) name="${name%.tar.xz}" ;; + *.tar.zst) name="${name%.tar.zst}" ;; + *.tar.zstd) name="${name%.tar.zstd}" ;; + *.tar) name="${name%.tar}" ;; + *) name="${name%.*}" ;; esac -# Given a newline-separated file listing, print "." if single top-level entry, -# otherwise mkdir $name and print it -choose_dest() { - count=$(awk -F/ '{k=($1=="."?$2:$1)} k && !seen[k]++ {n++} END {print n+0}' <<< "$1") - if [ -z "$count" ] || [ "$count" -le 1 ]; then - printf '.' - else - mkdir -p "${name:-.}" - printf '%s' "${name:-.}" - fi -} +# --one-top-level is GNU tar. macOS ships bsdtar; homebrew installs gtar. +TAR=tar +command -v gtar >/dev/null 2>&1 && TAR=gtar case "$1" in - *.tar.bz2|*.tar.gz|*.tar|*.tbz2|*.tgz|*.tar.xz|*.tar.zst|*.tar.zstd) - dest=$(choose_dest "$(tar tf "$1")") - tar xf "$1" -C "$dest" - ;; - *.zip) - dest=$(choose_dest "$(unzip -Z1 "$1")") - unzip "$1" -d "$dest" - ;; - *.7z) - listing=$(7z l -slt "$1" | grep '^Path = ' | sed 's/^Path = //; 1d') - dest=$(choose_dest "$listing") - 7z x "$1" -o"$dest" - ;; - *.rar) - dest=$(choose_dest "$(unrar lb "$1")") - unrar x "$1" "$dest/" - ;; - *.bz2) bunzip2 "$1" ;; - *.gz) gunzip "$1" ;; - *.Z) uncompress "$1" ;; - *.xz) unxz --threads 0 "$1" ;; - *.deb) ar x "$1" ;; - *) echo "'$1' cannot be extracted" && exit 1 ;; + *.tar|*.tar.bz2|*.tar.gz|*.tbz2|*.tgz|*.tar.xz|*.tar.zst|*.tar.zstd) + "$TAR" xf "$1" --one-top-level="$name" + ;; + *.zip) + mkdir -p "$name" && unzip "$1" -d "$name" + ;; + *.7z) + mkdir -p "$name" && 7z x "$1" -o"$name" + ;; + *.rar) + mkdir -p "$name" && unrar x "$1" "$name/" + ;; + *.bz2) bunzip2 "$1" ;; + *.gz) gunzip "$1" ;; + *.Z) uncompress "$1" ;; + *.xz) unxz --threads 0 "$1" ;; + *.deb) ar x "$1" ;; + *) + echo "'$1' cannot be extracted" >&2 + exit 1 + ;; esac diff --git a/.local/bin/wific b/.local/bin/wific index 8fb7abee..3cc66b42 100755 --- a/.local/bin/wific +++ b/.local/bin/wific @@ -1,43 +1,32 @@ #!/usr/bin/env bash set -euo pipefail -# Function to display a message box -message_box() { - whiptail --title "$1" --msgbox "$2" 8 78 -} +# Linux-only: iwctl + fzf picker for device → network +[ "$(uname -s)" = "Linux" ] || { echo "wific: Linux only (uses iwctl)" >&2; exit 1; } +command -v iwctl >/dev/null 2>&1 || { echo "wific: iwctl not found" >&2; exit 1; } +command -v fzf >/dev/null 2>&1 || { echo "wific: fzf not found" >&2; exit 1; } -# Function to select a device -select_device() { - local devices device - devices="$(iwctl device list | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR>4 {print $1}' | grep --color=never '\S')" - # shellcheck disable=SC2046 - device=$(whiptail --title "Select Device" --menu "Choose a device" 15 60 4 $(echo "$devices" | awk '{print NR, $1}') 3>&1 1>&2 2>&3) - if [ -z "$device" ]; then - return 1 - fi - device="$(echo "$devices" | sed -n "${device}p")" - printf '%s\n' "$device" -} +device="$( + iwctl device list | + sed 's/\x1b\[[0-9;]*m//g' | + awk 'NR > 4 {print $1}' | + grep . | + fzf --prompt='device > ' --no-multi +)" || exit 0 -# Function to select a network -select_network() { - local device=$1 networks network - iwctl station "$device" scan - iwctl station "$device" show - networks="$(iwctl station "$device" get-networks | sed 's/\x1b\[[0-9;]*m//g' | awk 'NR>4 {print $1}' | sed 's/^>//' | grep --color=never '\S')" - # shellcheck disable=SC2046 - network=$(whiptail --title "Select Network" --menu "Choose a network" 15 60 8 $(echo "$networks" | awk '{print NR, $1}') 3>&1 1>&2 2>&3) - if [ -z "$network" ]; then - return 1 - fi - network="$(echo "$networks" | sed -n "${network}p")" - printf '%s\n' "$network" -} +[[ -z $device ]] && exit 0 + +iwctl station "$device" scan >/dev/null + +network="$( + iwctl station "$device" get-networks | + sed 's/\x1b\[[0-9;]*m//g' | + awk 'NR > 4 {sub(/^> /, ""); print $1}' | + grep . | + fzf --prompt='network > ' --no-multi +)" || exit 0 + +[[ -z $network ]] && exit 0 -# Main script execution -device=$(select_device) || exit 0 -network=$(select_network "$device") || exit 0 set -x iwctl station "$device" connect "$network" -set +x -message_box "Success" "Successfully connected to $network on $device." diff --git a/home/.aliasrc b/home/.aliasrc index d3ccc669..f2b7215a 100644 --- a/home/.aliasrc +++ b/home/.aliasrc @@ -93,7 +93,6 @@ alias \ pnav='cd "$(_projselect)"' \ dlmv='mv ~/Downloads/"$(\ls -At ~/Downloads | head -n1)" .' \ vset='_x() { read -r "$1" && export "$1"; }; _x' \ - vset='_x() { read -r "$1" && export "$1"; }; _x' \ set_aws_profile='export AWS_PROFILE="$(aws configure list-profiles | fzf)"' \ unset_aws_profile='unset AWS_PROFILE' \ ytmp3='yt-dlp -x --audio-format mp3 --audio-quality 0 -o "$HOME/Downloads/%(title)s.%(ext)s"' @@ -158,36 +157,20 @@ dshell() { docker run --entrypoint /bin/bash --rm -it "$1" 2>/dev/null || docker ksc() { set -euo pipefail; s="$(klc | fzf)"; yq e -i ".current-context = \"$s\"" ~/.kube/config; } # Download a directory from a GitHub repo via sparse checkout. -# Usage: yoink -# The URL may point at a specific branch and subpath: -# https://github.com/owner/repo -> repo, default branch -# https://github.com/owner/repo/tree/branch -> repo @ branch -# https://github.com/owner/repo/tree/branch/dir -> dir under repo @ branch +# Usage: yoink [branch] [subpath] +# yoink foo/bar ./out -> default branch, full repo +# yoink foo/bar ./out main -> branch main, full repo +# yoink foo/bar ./out main src/pkg -> branch main, subpath src/pkg yoink() { - local url=$1 dest=$2 - [[ -e $dest ]] && { echo "yoink: $dest already exists" >&2; return 1; } - local p=${url#https://github.com/} - local owner_repo branch subpath - if [[ $p == *"/tree/"* ]]; then - owner_repo=${p%%/tree/*} branch_path=${p#*/tree/} - branch=${branch_path%%/*} subpath=${branch_path#*/} - [[ $subpath == "$branch_path" ]] && subpath="" - else - owner_repo=$p branch="" subpath="" - fi - local tmp; tmp=$(mktemp -d) - if [[ -n $branch ]]; then - gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse --branch "$branch" 2>/dev/null - else - gh repo clone "$owner_repo" "$tmp/repo" -- --depth 1 --filter=blob:none --sparse 2>/dev/null - fi - (cd "$tmp/repo" && git sparse-checkout set "$subpath") - mkdir -p "$dest" - if [[ -n $subpath ]]; then - cp -r "$tmp/repo/$subpath"/. "$dest"/ - else - cp -r "$tmp/repo"/. "$dest"/ - fi + local p=${1#https://github.com/} dest=$2 + local repo=${p%%/tree/*} spec=${p#*/tree/} + local branch subpath="" + [[ $spec != $p ]] && { branch=${spec%%/*}; subpath=${spec#*/}; [[ $subpath == $spec ]] && subpath=""; } + [[ -e $dest ]] && { echo "yoink: $dest exists" >&2; return 1; } + local tmp=$(mktemp -d) + gh repo clone "$repo" "$tmp/r" -- --depth 1 --filter=blob:none --sparse ${branch:+--branch "$branch"} && + (cd "$tmp/r" && git sparse-checkout set "$subpath") && + mkdir -p "$dest" && cp -r "$tmp/r/${subpath:-.}"/. "$dest/" rm -rf "$tmp" } diff --git a/home/.codex/AGENTS.md b/home/.codex/AGENTS.md new file mode 120000 index 00000000..5222d6e7 --- /dev/null +++ b/home/.codex/AGENTS.md @@ -0,0 +1 @@ +../.claude/CLAUDE.md \ No newline at end of file diff --git a/home/.pi/agent/extensions/footer.ts b/home/.pi/agent/extensions/footer.ts index 52551381..82a06f3f 100644 --- a/home/.pi/agent/extensions/footer.ts +++ b/home/.pi/agent/extensions/footer.ts @@ -61,9 +61,11 @@ export default function(pi: ExtensionAPI) { } function setupFooter(ctx: ExtensionContext, pi: ExtensionAPI): () => void { - // Cache tokens between renders — only recompute when the session grows. - let cachedTokens = buildSessionTokens(ctx); - let lastBranchLen = ctx.sessionManager.getBranch().length; + // Cache tokens between renders — recomputed on turn_end (O(1)), not on + // every render. invalidate() resets the counter to rebuild from scratch + // (used by session_compact and explicit refresh paths). + let cachedTokens = ""; + let lastBranchLen = 0; let requestRender: (() => void) | undefined; function rebuildTokens() { @@ -77,6 +79,14 @@ function setupFooter(ctx: ExtensionContext, pi: ExtensionAPI): () => void { } } + // Build once at attach time, then keep current via turn_end deltas. + rebuildTokens(); + + pi.on("turn_end", () => { + rebuildTokens(); + requestRender?.(); + }); + ctx.ui.setFooter((tui, theme, footerData) => { requestRender = () => tui.requestRender(); const unsubBranch = footerData.onBranchChange(requestRender); diff --git a/home/.pi/agent/extensions/tokenrouter/index.ts b/home/.pi/agent/extensions/tokenrouter/index.ts index d3e64f04..8739fb29 100644 --- a/home/.pi/agent/extensions/tokenrouter/index.ts +++ b/home/.pi/agent/extensions/tokenrouter/index.ts @@ -4,7 +4,7 @@ export default function(pi: ExtensionAPI) { pi.registerProvider("tokenrouter", { name: "TokenRouter", baseUrl: "https://api.tokenrouter.com/v1", - apiKey: "dummy", + apiKey: "$TOKENROUTER_API_KEY", api: "openai-completions", authHeader: true, diff --git a/home/.profile b/home/.profile index 36e4f815..620ad565 100644 --- a/home/.profile +++ b/home/.profile @@ -13,7 +13,7 @@ CARGO_BIN="$HOME/.cargo/bin" PIPX_BIN="$HOME/.local/pipxbin" ORBSTACK_BIN="$HOME/.orbstack/bin" -PATH="$PATH:$GO_BIN:$CARGO_BIN:$PNPM_HOME:$PIPX_BIN:$ORBSTACK_BIN:$HOME/.local/bin" +PATH="$PATH:$GO_BIN:$CARGO_BIN:$PNPM_HOME:$PIPX_BIN:$ORBSTACK_BIN:$HOME/.local/bin:$HOME/.local/share/nvim/mason/bin" # ============================================================================= # XDG From b694295d78df4d5acea92577c1f8105d530d3de5 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:05:21 +0900 Subject: [PATCH 077/366] chore: speed --- .local/bin/tmux-go-session | 162 ++++++++++++------------------------- 1 file changed, 51 insertions(+), 111 deletions(-) diff --git a/.local/bin/tmux-go-session b/.local/bin/tmux-go-session index f7e8fb4d..eddad1d1 100755 --- a/.local/bin/tmux-go-session +++ b/.local/bin/tmux-go-session @@ -2,61 +2,41 @@ set -euo pipefail PROJ_DIR="${PROJ_DIR:-$HOME/projects}" - -tmpdir="$(mktemp -d)" -cleanup() { - rm -rf "$tmpdir" -} -trap cleanup EXIT - -session_names_file="$tmpdir/session-names" -project_paths_file="$tmpdir/project-paths" -picker_rows_file="$tmpdir/picker-rows" script_path="${BASH_SOURCE[0]}" -list_sessions() { - tmux list-sessions -F $'#{session_attached}\t#{session_activity}\t#{session_name}\t#{session_path}' 2>/dev/null | - awk -F '\t' '$3 !~ /^_/ { print }' | - sort -t $'\t' -k1,1nr -k2,2nr | - awk -F '\t' '{ printf "session\t\033[38;5;75m[session]\033[0m %s\t%s\t%s\n", $3, $3, $4 }' || : -} +emit_picker_rows() { + local session_data IFS=: + session_data=$(tmux list-sessions -F $'#{session_attached}\t#{session_activity}\t#{session_name}\t#{session_path}' 2>/dev/null || true) -collect_session_names() { - tmux list-sessions -F '#{session_name}' 2>/dev/null | - awk '$0 !~ /^_/' || : -} + # Session rows: sort (attached desc, activity desc), filter private, format. + printf '%s\n' "$session_data" | sort -t $'\t' -k1,1nr -k2,2nr | + awk -F '\t' '$3 !~ /^_/ { printf "session\t\033[38;5;75m[session]\033[0m %s\t%s\t%s\n", $3, $3, $4 }' -collect_project_paths() { - local path - local IFS=: + declare -A existing base_count seen + while IFS=$'\t' read -r _ _ name _; do + [[ $name == _* || -z $name ]] && continue + existing["$name"]=1 + done <<<"$session_data" - for path in $PROJ_DIR; do - [ -d "$path" ] && find "$path" -mindepth 1 -maxdepth 1 -type d + declare -a paths bases + local d sub + for d in $PROJ_DIR; do + [[ -d $d ]] || continue + for sub in "$d"/*/; do + sub="${sub%/}" + paths+=("$sub") + bases+=("${sub##*/}") + done done - echo "$HOME/dotfiles" - echo "$HOME/.claude" -} + paths+=("$HOME/dotfiles" "$HOME/.claude") + bases+=("dotfiles" ".claude") + for k in "${bases[@]}"; do base_count["$k"]=$(( ${base_count["$k"]:-0} + 1 )); done -build_project_rows() { - collect_project_paths >"$project_paths_file" - - declare -A existing seen base_count - while IFS= read -r s; do existing["$s"]=1; done < "$session_names_file" - - declare -a paths bases - while IFS= read -r p; do - paths+=("$p") - b="${p##*/}" - bases+=("$b") - base_count["$b"]=$(( ${base_count["$b"]:-0} + 1 )) - done < "$project_paths_file" - - local found_collision=0 path b unsanitized s + local i path b unsanitized s collision=0 for i in "${!paths[@]}"; do path="${paths[$i]}"; b="${bases[$i]}" - if [[ ${base_count["$b"]:-0} -gt 1 ]]; then - local parent="${path%/*}" - unsanitized="${parent##*/}_${b}" + if (( base_count["$b"] > 1 )); then + unsanitized="${path%/*}_${b}"; unsanitized="${unsanitized##*/}" else unsanitized="$b" fi @@ -66,37 +46,28 @@ build_project_rows() { [[ -z $s ]] && { echo "Cannot derive a tmux session name for project: $path" >&2; return 1; } if [[ -n ${seen[$s]:-} && ${seen[$s]:-} != "$path" ]]; then - (( found_collision )) || echo "Project paths derive the same tmux session name after sanitization:" >&2 + (( collision )) || echo "Project paths derive the same tmux session name after sanitization:" >&2 echo " $s: ${seen[$s]:-} and $path" >&2 - found_collision=1 + collision=1 continue fi seen[$s]="$path" - [[ -z ${existing[$s]:-} ]] && \ + [[ -z ${existing[$s]:-} ]] && printf "project\t\033[38;5;114m[project]\033[0m %s\t%s\t%s\n" "$unsanitized" "$s" "$path" done - if (( found_collision )); then + if (( collision )); then echo "Rename one project or choose a collision policy." >&2 return 1 fi } -emit_picker_rows() { - collect_session_names >"$session_names_file" - list_sessions - build_project_rows -} - kill_session_family() { - local session_name=$1 - local child_name parent_name is_private prefix - - while IFS=$'\t' read -r child_name parent_name is_private; do - if [[ $is_private == 1 && $parent_name == "$session_name" ]]; then - tmux kill-session -t "=$child_name" 2>/dev/null || : - fi + local session_name=$1 child parent is_private prefix + while IFS=$'\t' read -r child parent is_private; do + [[ $is_private == 1 && $parent == "$session_name" ]] && + tmux kill-session -t "=$child" 2>/dev/null || : done < <(tmux list-sessions -F $'#{session_name}\t#{@tmux_go_parent_session}\t#{@tmux_go_private_session}' 2>/dev/null || :) # Fallback for private sessions created before metadata was added. @@ -106,64 +77,36 @@ kill_session_family() { tmux kill-session -t "=$session_name" 2>/dev/null || : } -switch_or_attach() { - local session_name=$1 - - if [[ -z ${TMUX:-} ]]; then - tmux attach-session -t "=$session_name" - else - tmux switch-client -t "=$session_name" - fi -} - case "${1:-}" in - --list) - emit_picker_rows - exit 0 - ;; + --list) emit_picker_rows; exit 0 ;; --kill) - if [[ $# -ne 2 ]]; then - printf 'Usage: %s --kill \n' "$0" >&2 - exit 2 - fi - kill_session_family "$2" - exit 0 - ;; + [[ $# -ne 2 ]] && { printf 'Usage: %s --kill \n' "$0" >&2; exit 2; } + kill_session_family "$2"; exit 0 ;; esac -emit_picker_rows >"$picker_rows_file" +PICKER=(fzf) +[[ -n ${TMUX:-} ]] && PICKER=(fzf-tmux -p '80%,90%') -pick_entry() { - local -a picker_cmd - - if [[ -n ${TMUX:-} ]]; then - picker_cmd=(fzf-tmux -p '80%,90%') - else - picker_cmd=(fzf) - fi - - # shellcheck disable=SC2016 - "${picker_cmd[@]}" --ansi --prompt 'tmux session > ' --with-nth=2 --nth=1 --delimiter '\t' --tiebreak=index \ - --preview 'type={1}; name={3}; dir={4}; if [[ -n "$dir" && -d "$dir" ]]; then readme=$(find "$dir" -maxdepth 1 -type f \( -iname "readme" -o -iname "readme.*" \) | head -n1); if [[ -n "$readme" ]]; then if command -v bat >/dev/null 2>&1; then bat --style=plain --color=always --paging=never "$readme"; else cat "$readme"; fi; elif git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$dir" log --color --graph --pretty=format:"%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --abbrev-commit | head -200; else ls --color=always -al "$dir"; fi; elif [[ "$type" == session ]]; then tmux display-message -p -t "=$name" "session: #S +# shellcheck disable=SC2016 +selected_entry="$(emit_picker_rows | "${PICKER[@]}" --ansi --prompt 'tmux session > ' \ + --with-nth=2 --nth=1 --delimiter '\t' --tiebreak=index \ + --preview 'type={1}; name={3}; dir={4}; if [[ -n "$dir" && -d "$dir" ]]; then readme=$(find "$dir" -maxdepth 1 -type f \( -iname "readme" -o -iname "readme.*" \) | head -n1); if [[ -n "$readme" ]]; then if command -v bat >/dev/null 2>&1; then bat --style=plain --color=always --paging=never "$readme"; else cat "$readme"; fi; elif git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then git -C "$dir" log --color --graph --pretty=format:"%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --abbrev-commit | head -200; else ls --color=always -al "$dir"; fi; elif [[ "$type" == session ]]; then tmux display-message -p -t "=$name" "session: #S windows: #{session_windows} created: #{session_created_string}" 2>/dev/null || printf "session: %s\n" "$name"; else printf "No preview available\n"; fi' \ - --bind "ctrl-x:execute-silent([[ {1} == session ]] && '$script_path' --kill {3})+reload('$script_path' --list)" \ - --preview-window=right,60% <"$picker_rows_file" -} - -selected_entry="$(pick_entry || :)" + --bind "ctrl-x:execute-silent([[ {1} == session ]] && '$script_path' --kill {3})+reload('$script_path' --list)" \ + --preview-window=right,60% || :)" [[ -z ${selected_entry:-} ]] && exit 0 +IFS=$'\t' read -r selected_type _ session_name selected_dir <<<"$selected_entry" -IFS=$'\t' read -r selected_type _display_label session_name selected_dir <<<"$selected_entry" +# shellcheck disable=SC2015 +goto() { [[ -z ${TMUX:-} ]] && tmux attach-session -t "=$1" || tmux switch-client -t "=$1"; } case "$selected_type" in - session) - switch_or_attach "$session_name" - ;; + session) goto "$session_name" ;; project) if tmux has-session -t "=$session_name" 2>/dev/null; then - switch_or_attach "$session_name" + goto "$session_name" elif [[ -z ${TMUX:-} ]]; then tmux new-session -s "$session_name" -c "$selected_dir" else @@ -171,8 +114,5 @@ case "$selected_type" in tmux switch-client -t "=$session_name" fi ;; - *) - printf 'Unknown picker entry type: %s\n' "$selected_type" >&2 - exit 1 - ;; + *) printf 'Unknown picker entry type: %s\n' "$selected_type" >&2; exit 1 ;; esac From 2de5814e49c8b983735eaf9cca797ef2003c04bc Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:40 +0900 Subject: [PATCH 078/366] refactor(nvim): drop orphan typr/grug-far/diffview link block These were TODO bookmarks for plugins never adopted. Dead code adds noise and future readers will think there's intent to act. --- .config/nvim/plugin/10_opts.lua | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index 871de61c..b155e7dd 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -88,12 +88,6 @@ vim.o.complete = '.,w,b,kspell' -- Use less sources vim.o.completeopt = 'menuone,noselect,fuzzy,nosort' -- Use custom behavior --- https://github.com/nvzone/typr --- https://www.reddit.com/r/neovim/comments/1mxeghf/using_as_a_multipurpose_search_tool/ --- https://github.com/MagicDuck/grug-far.nvim --- https://github.com/sindrets/diffview.nvim --- https://www.reddit.com/r/neovim/comments/1muy3i1/dartnvim_a_minimalist_tabline_focused_on_pinning/ - vim.diagnostic.config({ severity_sort = true, -- Errors first underline = true, -- Show all diagnostics as underline From cdfb4155a2183ee873ae75208a82d9dc66692b49 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:43 +0900 Subject: [PATCH 079/366] fix(nvim/lsp): evaluate go_test_exclude at key-press time go_test_exclude was captured at LspAttach time, so opening a Go buffer first and a non-Go buffer later made the gri binding ignore _ test.go on every buffer. Compute it inside the closure, and inline the lsp_picker helper that became a one-liner after that fix. --- .config/nvim/plugin/40_lsp_behavior.lua | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 305e673a..62bf7f86 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -35,18 +35,14 @@ local function mappings(client, buf) utils.map(mode, lhs, rhs, options) end - local function lsp_picker(picker_fn, opts) - opts = opts or {} - return function() - picker_fn({ layout = lsp_picker_layout, focus = "list", pattern = opts.pattern }) - end - end - - local go_test_exclude = vim.bo.filetype == "go" and "!_test.go" or nil - -- See `:help vim.lsp.*` for documentation on any of the below functions () - bmap('n', 'gri', lsp_picker(Snacks.picker.lsp_implementations, { pattern = go_test_exclude }), { desc = "Go to implementation" }) -- vim.lsp.buf.implementation - bmap('n', 'grr', lsp_picker(Snacks.picker.lsp_references), { desc = "Go to reference" }) -- vim.lsp.buf.references + bmap('n', 'gri', function() + local pattern = vim.bo.filetype == "go" and "!_test.go" or nil + Snacks.picker.lsp_implementations({ layout = lsp_picker_layout, focus = "list", pattern = pattern }) + end, { desc = "Go to implementation" }) -- vim.lsp.buf.implementation + bmap('n', 'grr', function() + Snacks.picker.lsp_references({ layout = lsp_picker_layout, focus = "list" }) + end, { desc = "Go to reference" }) -- vim.lsp.buf.references bmap('n', 'gS', Snacks.picker.lsp_workspace_symbols, { desc = "Goto workspace symbols" }) bmap('n', 'gD', vim.lsp.buf.declaration, { desc = "Go to declaration" }) -- Many LSPs do not implement this @@ -55,8 +51,12 @@ local function mappings(client, buf) bmap('n', 'grx', vim.lsp.codelens.run, { desc = 'Run codelens' }) - bmap('n', 'gai', lsp_picker(Snacks.picker.lsp_incoming_calls), { desc = "C[a]lls Incoming" }) - bmap('n', 'gao', lsp_picker(Snacks.picker.lsp_outgoing_calls), { desc = "C[a]lls Outgoing" }) + bmap('n', 'gai', function() + Snacks.picker.lsp_incoming_calls({ layout = lsp_picker_layout, focus = "list" }) + end, { desc = "C[a]lls Incoming" }) + bmap('n', 'gao', function() + Snacks.picker.lsp_outgoing_calls({ layout = lsp_picker_layout, focus = "list" }) + end, { desc = "C[a]lls Outgoing" }) -- map('n', 'gs', vim.lsp.buf.signature_help, { desc = "Signature help" }) bmap('i', '', vim.lsp.buf.signature_help, { desc = "Signature help" }) From 4d82a54ea700dfeb81d6736c3f2c8f029c54c34d Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:45 +0900 Subject: [PATCH 080/366] refactor(nvim): scope Snacks to local in 75_snacks Other plugin files (40_lsp_behavior, 72_flash, 73_git) already do require('snacks').pickers locally. 75_snacks was the lone holdout relying on the Snacks global, which only works because it happens to be exposed at module load. --- .config/nvim/plugin/75_snacks.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/.config/nvim/plugin/75_snacks.lua b/.config/nvim/plugin/75_snacks.lua index b92f1690..48c0796f 100644 --- a/.config/nvim/plugin/75_snacks.lua +++ b/.config/nvim/plugin/75_snacks.lua @@ -61,6 +61,7 @@ require('snacks').setup({ }) local map = require('utils').map +local Snacks = require('snacks') map('n', 'fo', function() Snacks.picker.files({ hidden = true }) end, { desc = "Find files" }) map('n', 'fO', function() Snacks.picker.files({ hidden = true, ignored = true }) end, From e8ed584b144e5eaec31f1384b8d7b3bd491cfe12 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:46 +0900 Subject: [PATCH 081/366] refactor(nvim): drop redundant expand() and use libuv fs_stat vim.fn.stdpath already returns an absolute path; wrapping it in vim.fn.expand is a no-op. vim.uv.fs_stat is the documented modern replacement for vim.fn.isdirectory. --- .config/nvim/plugin/999_session.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/999_session.lua b/.config/nvim/plugin/999_session.lua index d4a8b7a6..6bbdfce3 100644 --- a/.config/nvim/plugin/999_session.lua +++ b/.config/nvim/plugin/999_session.lua @@ -1,5 +1,5 @@ -- Auto-session management -local session_dir = vim.fn.expand(vim.fn.stdpath("state") .. "/sessions") +local session_dir = vim.fn.stdpath("state") .. "/sessions" -- Directories where sessions should not be saved local skip_dirs = { @@ -40,7 +40,7 @@ local function get_session_file() end -- Create the dir if it doesn't exist -if vim.fn.isdirectory(session_dir) == 0 then +if vim.uv.fs_stat(session_dir) == nil then vim.fn.mkdir(session_dir, "p") end From 453646f625536a85c797e1021a6c7ebe6876c091 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:49 +0900 Subject: [PATCH 082/366] refactor(nvim): drop unused parameter from colemak.setup colemak.setup() is called once with no arguments and the function never reads the parameter. The _-prefix is a Lua convention for deliberately ignored args; here there's nothing to ignore. --- .config/nvim/lua/colemak.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/lua/colemak.lua b/.config/nvim/lua/colemak.lua index b466c84d..c7108294 100644 --- a/.config/nvim/lua/colemak.lua +++ b/.config/nvim/lua/colemak.lua @@ -21,7 +21,7 @@ local mappings = { } -function colemak.setup(_) +function colemak.setup() colemak.apply() vim.api.nvim_create_user_command( From 929cb606fa05edb5d3ccb9d02b1a4ea7b47f606f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:51 +0900 Subject: [PATCH 083/366] refactor(nvim): inline visual_range/leave_visual helpers in 73_git Both helpers had a single caller each. Inlining keeps the two visual-mode keymaps readable and removes two dead end-of-file functions to scroll past. --- .config/nvim/plugin/73_git.lua | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.config/nvim/plugin/73_git.lua b/.config/nvim/plugin/73_git.lua index 14023205..57671e63 100644 --- a/.config/nvim/plugin/73_git.lua +++ b/.config/nvim/plugin/73_git.lua @@ -8,24 +8,23 @@ vim.pack.add({ local map = require('utils').map local gitgud = require('custom.gitgud') -local function visual_range() +map('n', 'Gl', function() gitgud.copy_github_permalink() end, { desc = "Copy GitHub permalink" }) +map('x', 'Gl', function() local start_line = vim.fn.line("v") local end_line = vim.fn.line(".") if end_line < start_line then start_line, end_line = end_line, start_line end - return { start_line = start_line, end_line = end_line } -end - -local function leave_visual() + gitgud.copy_github_permalink({ start_line = start_line, end_line = end_line }) vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) -end - -map('n', 'Gl', function() gitgud.copy_github_permalink() end, { desc = "Copy GitHub permalink" }) -map('x', 'Gl', function() gitgud.copy_github_permalink(visual_range()); leave_visual() end, - { desc = "Copy GitHub permalink (range)" }) +end, { desc = "Copy GitHub permalink (range)" }) map('n', 'Go', function() gitgud.open_github_file() end, { desc = "Open GitHub file" }) -map('x', 'Go', function() gitgud.open_github_file(visual_range()); leave_visual() end, - { desc = "Open GitHub file (range)" }) +map('x', 'Go', function() + local start_line = vim.fn.line("v") + local end_line = vim.fn.line(".") + if end_line < start_line then start_line, end_line = end_line, start_line end + gitgud.open_github_file({ start_line = start_line, end_line = end_line }) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) +end, { desc = "Open GitHub file (range)" }) -- blame.nvim require('blame').setup({ From 47602dd5dadc789c60dfe8b9df3d6008e91379c2 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:52 +0900 Subject: [PATCH 084/366] fix(nvim/lsp): map LSP MessageType.Debug=5 to DEBUG level LSP 3.18 added MessageType.Debug=5. The previous handler treated unknown types as INFO, so debug messages from newer servers would flash as plain info. Use a direct table lookup with a single fallback; the previous local levels table was rebuilt on every call. --- .config/nvim/plugin/78_lsp.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index fce49fcf..fd07bbb2 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -16,8 +16,9 @@ vim.lsp.handlers["window/showMessage"] = function(_, result, ctx) if not (result and result.message) then return end local client = ctx and ctx.client_id and vim.lsp.get_client_by_id(ctx.client_id) local prefix = client and ("[LSP] [" .. client.name .. "]") or "[LSP]" - local levels = { [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG } - vim.notify(prefix .. " " .. result.message, levels[result.type] or vim.log.levels.INFO) + -- LSP MessageType: 1=Error, 2=Warning, 3=Info, 4=Log, 5=Debug (LSP 3.18+) + local level = ({ [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG, [5] = vim.log.levels.DEBUG })[result.type] or vim.log.levels.INFO + vim.notify(prefix .. " " .. result.message, level) end local enabled_lsps = { From 0c5b6776500008f2d794e423ab5167fb53b5d91f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:53 +0900 Subject: [PATCH 085/366] refactor(nvim): use vim.trim in gitgud vim.fn.trim is a thin wrapper; vim.trim is the stdlib equivalent and matches what lua/custom/ai.lua already does in the same project. --- .config/nvim/lua/custom/gitgud.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/nvim/lua/custom/gitgud.lua b/.config/nvim/lua/custom/gitgud.lua index 527d88cc..a9fa483e 100644 --- a/.config/nvim/lua/custom/gitgud.lua +++ b/.config/nvim/lua/custom/gitgud.lua @@ -19,7 +19,7 @@ local function get_github_url(opts) -- Get upstream branch SHA local sha_result = vim.system({ "git", "rev-parse", "@{u}" }, { text = true }):wait() local is_upstream = sha_result.code == 0 - local sha = vim.fn.trim(sha_result.stdout or "") + local sha = vim.trim(sha_result.stdout or "") -- Build gh browse command with proper argument escaping local cmd = { "gh", "browse", "--no-browser", file_arg } @@ -31,8 +31,8 @@ local function get_github_url(opts) end local result = vim.system(cmd, { text = true }):wait() - local url = vim.fn.trim(result.stdout or "") - local stderr = vim.fn.trim(result.stderr or "") + local url = vim.trim(result.stdout or "") + local stderr = vim.trim(result.stderr or "") return url, result.code, stderr end From d2f2f11de4401ced38cd680587caa5845e58dedc Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:55 +0900 Subject: [PATCH 086/366] refactor(nvim): extract shfmt_on_save from BufWritePre callback The shfmt logic was the only thing inside the callback but got nested under a filetype check. Hoisting it makes the autocmd read as one line and makes the helper testable in isolation. --- .config/nvim/plugin/30_autocmds.lua | 34 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/.config/nvim/plugin/30_autocmds.lua b/.config/nvim/plugin/30_autocmds.lua index 5db0034d..f9f20fae 100644 --- a/.config/nvim/plugin/30_autocmds.lua +++ b/.config/nvim/plugin/30_autocmds.lua @@ -47,27 +47,25 @@ _G.Config.new_autocmd({ "InsertEnter", "WinLeave" }, { -- Format shell scripts on save without re-triggering write -_G.Config.new_autocmd("BufWritePre", { - callback = function(info) - if vim.bo[info.buf].filetype == "sh" then - if vim.fn.executable('shfmt') ~= 1 then - return - end +local function shfmt_on_save(buf) + if vim.fn.executable('shfmt') ~= 1 then return end - local original_lines = vim.api.nvim_buf_get_lines(info.buf, 0, -1, true) - local input = table.concat(original_lines, "\n") - local output = vim.fn.systemlist({ "shfmt", "-i", "2", "-s" }, input) - if vim.v.shell_error ~= 0 then - local error_message = "shfmt failed: " .. table.concat(output, "\n") - vim.notify(error_message, vim.log.levels.ERROR) - return - end + local input = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, true), "\n") + local output = vim.fn.systemlist({ "shfmt", "-i", "2", "-s" }, input) + if vim.v.shell_error ~= 0 then + vim.notify("shfmt failed: " .. table.concat(output, "\n"), vim.log.levels.ERROR) + return + end - if #output > 0 then - vim.api.nvim_buf_set_lines(info.buf, 0, -1, true, output) - end - end + if #output > 0 then + vim.api.nvim_buf_set_lines(buf, 0, -1, true, output) end +end + +_G.Config.new_autocmd("BufWritePre", { + callback = function(info) + if vim.bo[info.buf].filetype == "sh" then shfmt_on_save(info.buf) end + end, }) -- Go organize-imports on save is handled in 41_lsp_format.lua (combined with auto-format to avoid race conditions) From 8c0e398492abc814da2db05d1c726709d65572f1 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:58 +0900 Subject: [PATCH 087/366] refactor(nvim): inline single-use 'f' in 30_autocmds The named local served no purpose; passing the lambda straight into Config.new_autocmd matches the style used for every other autocmd in the file. --- .config/nvim/plugin/30_autocmds.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.config/nvim/plugin/30_autocmds.lua b/.config/nvim/plugin/30_autocmds.lua index f9f20fae..603045c7 100644 --- a/.config/nvim/plugin/30_autocmds.lua +++ b/.config/nvim/plugin/30_autocmds.lua @@ -1,11 +1,9 @@ -- Don't auto-wrap comments and don't insert comment leader after hitting 'o'. -- Do on `FileType` to always override these changes from filetype plugins. -local f = function() vim.cmd('setlocal formatoptions-=c formatoptions-=o') end -_G.Config.new_autocmd('FileType', - { - callback = f, - desc = "Proper 'formatoptions' for all filetypes", - }) +_G.Config.new_autocmd('FileType', { + desc = "Proper 'formatoptions' for all filetypes", + callback = function() vim.cmd('setlocal formatoptions-=c formatoptions-=o') end, +}) -- Skip the rest of the autocommands if we are in VSCode if vim.g.vscode then From 41bb817c70b9a7b08d9813ff2befd24acba786e8 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:18:59 +0900 Subject: [PATCH 088/366] refactor(nvim/lsp): call vim.lsp.enable with the list directly vim.lsp.enable accepts a list of server names; the ipairs wrapper just wrapped the list back into N separate calls. --- .config/nvim/plugin/78_lsp.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index fd07bbb2..27e8f04c 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -38,9 +38,7 @@ local enabled_lsps = { "kotlin_lsp", } -for _, name in ipairs(enabled_lsps) do - vim.lsp.enable(name) -end +vim.lsp.enable(enabled_lsps) vim.lsp.inline_completion.enable(true) vim.lsp.codelens.enable(true) From bb987d24b567094bc7bb6deb3bb19a70e52c79c5 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:19:00 +0900 Subject: [PATCH 089/366] refactor(nvim/lsp): alias vim.lsp.protocol.Methods The 4-segment method name was repeated; a module-local Methods cuts the line in half and matches the style nvim's own docs use. --- .config/nvim/plugin/40_lsp_behavior.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 62bf7f86..1752ff11 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -7,6 +7,7 @@ vim.g.diagnostics_visible = true local utils = require('utils') local UserLspConfig = vim.api.nvim_create_augroup('UserLspConfig', { clear = true }) +local Methods = vim.lsp.protocol.Methods local lsp_picker_layout = { layout = { @@ -61,7 +62,7 @@ local function mappings(client, buf) -- map('n', 'gs', vim.lsp.buf.signature_help, { desc = "Signature help" }) bmap('i', '', vim.lsp.buf.signature_help, { desc = "Signature help" }) - if client:supports_method(vim.lsp.protocol.Methods.textDocument_inlayHint) then + if client:supports_method(Methods.textDocument_inlayHint) then bmap('n', 'th', function() vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({})) end, { desc = 'Toggle inlay hints' }) @@ -95,7 +96,7 @@ end ---@param buf number local function highlight_references(client, buf) if vim.b[buf].lsp_highlight_setup then return end - if not client:supports_method(vim.lsp.protocol.Methods.textDocument_documentHighlight) then return end + if not client:supports_method(Methods.textDocument_documentHighlight) then return end vim.b[buf].lsp_highlight_setup = true local group = vim.api.nvim_create_augroup('lsp-highlight-' .. buf, { clear = true }) From 6949969f1c147fa1d879d8514883760099f82978 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:19:01 +0900 Subject: [PATCH 090/366] refactor(nvim): use idiomatic 'not vim.fn.executable(...)' vim.fn.executable returns 1/0; 'not 1' is a literal '0' but the standard idiom in Lua is the boolean form. --- .config/nvim/after/lsp/pyright.lua | 2 +- .config/nvim/plugin/30_autocmds.lua | 2 +- .config/nvim/plugin/41_lsp_format.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.config/nvim/after/lsp/pyright.lua b/.config/nvim/after/lsp/pyright.lua index 576877e5..6456b280 100644 --- a/.config/nvim/after/lsp/pyright.lua +++ b/.config/nvim/after/lsp/pyright.lua @@ -2,7 +2,7 @@ local function use_project_venv(_, config) if not config.root_dir then return end local python = config.root_dir .. '/.venv/bin/python' - if vim.fn.executable(python) ~= 1 then return end + if not vim.fn.executable(python) then return end config.settings = config.settings or {} config.settings.python = config.settings.python or {} diff --git a/.config/nvim/plugin/30_autocmds.lua b/.config/nvim/plugin/30_autocmds.lua index 603045c7..3686cfd1 100644 --- a/.config/nvim/plugin/30_autocmds.lua +++ b/.config/nvim/plugin/30_autocmds.lua @@ -46,7 +46,7 @@ _G.Config.new_autocmd({ "InsertEnter", "WinLeave" }, { -- Format shell scripts on save without re-triggering write local function shfmt_on_save(buf) - if vim.fn.executable('shfmt') ~= 1 then return end + if not vim.fn.executable('shfmt') then return end local input = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, true), "\n") local output = vim.fn.systemlist({ "shfmt", "-i", "2", "-s" }, input) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 25fd04cf..18fc8fe4 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -21,7 +21,7 @@ local fmt = { local format_group = vim.api.nvim_create_augroup('lsp.format', { clear = true }) local function format_python_black(buf) - if vim.fn.executable('black') ~= 1 then + if not vim.fn.executable('black') then vim.notify('black not found; install it in .venv or on PATH', vim.log.levels.WARN) return end From 6b0329e69c35eef4db79e7dcd17aac35ff113867 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:55:53 +0900 Subject: [PATCH 091/366] refactor(nvim): inline 74_ai.lua setup into 74_sidekick.lua ponytail: 74_ai.lua was a 4-line wrapper around custom.ai.setup(). Move the call into 74_sidekick.lua (same guard, same module grouping) and delete the redundant file. --- .config/nvim/plugin/74_ai.lua | 4 ---- .config/nvim/plugin/74_sidekick.lua | 3 +++ 2 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 .config/nvim/plugin/74_ai.lua diff --git a/.config/nvim/plugin/74_ai.lua b/.config/nvim/plugin/74_ai.lua deleted file mode 100644 index 87fc72b6..00000000 --- a/.config/nvim/plugin/74_ai.lua +++ /dev/null @@ -1,4 +0,0 @@ --- Custom AI helpers -if vim.g.vscode then return end - -require("custom.ai").setup() diff --git a/.config/nvim/plugin/74_sidekick.lua b/.config/nvim/plugin/74_sidekick.lua index ebad1ccd..38d08d08 100644 --- a/.config/nvim/plugin/74_sidekick.lua +++ b/.config/nvim/plugin/74_sidekick.lua @@ -22,3 +22,6 @@ map("n", "", function() end return "" end, { expr = true, desc = "Goto/Apply Next Edit Suggestion" }) + +-- Custom AI helpers (file send) +require("custom.ai").setup() From 0535341bbb1b95cf49618d96e92d320a71ad680d Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:58:32 +0900 Subject: [PATCH 092/366] refactor(nvim/lsp): derive diagnostics toggle from vim.diagnostic.is_enabled ponytail: vim.g.diagnostics_visible was a hand-maintained mirror of vim.diagnostic state. Use the API's own is_enabled() so the source of truth lives in one place. --- .config/nvim/plugin/40_lsp_behavior.lua | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 1752ff11..5e89340e 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -3,7 +3,6 @@ if vim.g.vscode then return end -vim.g.diagnostics_visible = true local utils = require('utils') local UserLspConfig = vim.api.nvim_create_augroup('UserLspConfig', { clear = true }) @@ -81,13 +80,7 @@ local function mappings(client, buf) --- toggle diagnostics bmap('n', 'td', function() - if vim.g.diagnostics_visible then - vim.g.diagnostics_visible = false - vim.diagnostic.enable(false) - else - vim.g.diagnostics_visible = true - vim.diagnostic.enable() - end + vim.diagnostic.enable(not vim.diagnostic.is_enabled({})) end, { desc = 'Toggle diagnostics' }) end From a8db1563599be31699a504706311da91b32e56d7 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:00:57 +0900 Subject: [PATCH 093/366] refactor(nvim): drop sidekick cli.mux defaults (match built-in) ponytail: sidekick.config.cli.mux already defaults to { backend = 'tmux', enabled = false }. The explicit block was a no-op. Re-add when enabling mux. --- .config/nvim/plugin/74_sidekick.lua | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.config/nvim/plugin/74_sidekick.lua b/.config/nvim/plugin/74_sidekick.lua index 38d08d08..39d4cc50 100644 --- a/.config/nvim/plugin/74_sidekick.lua +++ b/.config/nvim/plugin/74_sidekick.lua @@ -7,14 +7,7 @@ vim.pack.add({ local map = require('utils').map -require('sidekick').setup({ - cli = { - mux = { - backend = "tmux", - enabled = false, - }, - }, -}) +require('sidekick').setup({}) map("n", "", function() if require('sidekick').nes_jump_or_apply() then From 1569955f824af28967d09e2738076cde60535d39 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:01:43 +0900 Subject: [PATCH 094/366] refactor(nvim): drop no-op tokyonight.setup({}) call ponytail: tokyonight.setup({}) just merges defaults. Empty config equals defaults equals no-op. :colorscheme tokyonight still works since vim.pack.add keeps the plugin available. --- .config/nvim/plugin/70_theme.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/.config/nvim/plugin/70_theme.lua b/.config/nvim/plugin/70_theme.lua index 10c5e233..317f1aca 100644 --- a/.config/nvim/plugin/70_theme.lua +++ b/.config/nvim/plugin/70_theme.lua @@ -11,6 +11,5 @@ vim.pack.add({ }) require('kanagawa').setup({}) -require('tokyonight').setup({}) vim.cmd.colorscheme('kanagawa') require('markview').setup({}) From 185887ab59ce5b70a3d8f0877ebd46f4cebcf043 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:04:13 +0900 Subject: [PATCH 095/366] refactor(nvim): use nvim_buf_get_name in C instead of expand('%:p') ponytail: matches gitgud.lua (improvement #7). VS Code CLI resolves relative paths against the cwd passed in the next arg, so behavior is preserved. --- .config/nvim/plugin/20_keymaps.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 86bfa81e..a9cada2a 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -68,7 +68,7 @@ if not vim.g.vscode then map("n", "q", ":q", { silent = true }) map("n", "C", function() - local file_path = vim.fn.expand('%:p') + local file_path = vim.api.nvim_buf_get_name(0) local line_number = vim.fn.line('.') local column_number = vim.fn.col('.') local goto_arg = string.format("%s:%d:%d", file_path, line_number, column_number) From 3e96d70583a8606e8a59e81f93582b7984ad83a8 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:05:27 +0900 Subject: [PATCH 096/366] refactor(nvim): inline strip_leading_spaces into copy_code_block ponytail: the helper had one caller. Inline the 6-line loop into the function and keep its behavior comment. --- .config/nvim/lua/utils.lua | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/.config/nvim/lua/utils.lua b/.config/nvim/lua/utils.lua index bf5ff846..f06ebbd0 100644 --- a/.config/nvim/lua/utils.lua +++ b/.config/nvim/lua/utils.lua @@ -2,22 +2,6 @@ local M = { } ----strip leading spaces (smallest indent wins; empty lines ignored) ----@param lines table ----@return table -local function strip_leading_spaces(lines) - local min = math.huge - for _, line in ipairs(lines) do - if line ~= "" then - local n = #line:match("^%s*") - if n < min then min = n end - end - end - if min == math.huge then return lines end - return vim.tbl_map(function(line) return line:sub(min + 1) end, lines) -end - - ---Functional wrapper for mapping custom keybindings ---@param mode string|string[] Mode short-name, see |nvim_set_keymap()|. --- Can also be list of modes to create mapping on multiple modes. @@ -37,7 +21,20 @@ end ---@param opts vim.api.keyset.user_command function M.copy_code_block(opts) local lines = vim.api.nvim_buf_get_lines(0, opts.line1 - 1, opts.line2, true) - local content = table.concat(strip_leading_spaces(lines), '\n') + + -- strip leading spaces: smallest indent wins, empty lines ignored + local min = math.huge + for _, line in ipairs(lines) do + if line ~= "" then + local n = #line:match("^%s*") + if n < min then min = n end + end + end + if min ~= math.huge then + lines = vim.tbl_map(function(line) return line:sub(min + 1) end, lines) + end + + local content = table.concat(lines, '\n') local result = string.format('```%s\n%s\n```', vim.bo.filetype, content) vim.fn.setreg('+', result) end From 138962b0cc87254fdc6b8139a52fad866236e426 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:07:15 +0900 Subject: [PATCH 097/366] refactor(nvim): use nvim_buf_get_name in CopyPath command ponytail: vim.fs.relpath works on both absolute and relative inputs; the prior expand('%:p') added an unnecessary resolution step. --- .config/nvim/plugin/20_keymaps.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index a9cada2a..6d8b0252 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -77,7 +77,7 @@ if not vim.g.vscode then end vim.api.nvim_create_user_command("CopyPath", function() - local file = vim.fn.expand("%:p") + local file = vim.api.nvim_buf_get_name(0) local cwd = vim.fn.getcwd() local rel = vim.fs.relpath(cwd, file) local display = (rel and rel ~= "") and rel or file From f599db333be257c6844458989cfacc2be1cedc03 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:11:28 +0900 Subject: [PATCH 098/366] refactor(nvim/lsp): inline utils.map alias in 40 and 41 ponytail: these files only use utils.map. Replace the two-step 'local utils = require; local map = utils.map' with a single line. Matches 72/73/75/999 style. --- .config/nvim/plugin/40_lsp_behavior.lua | 4 ++-- .config/nvim/plugin/41_lsp_format.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index 5e89340e..b55cbec3 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -3,7 +3,7 @@ if vim.g.vscode then return end -local utils = require('utils') +local map = require('utils').map local UserLspConfig = vim.api.nvim_create_augroup('UserLspConfig', { clear = true }) local Methods = vim.lsp.protocol.Methods @@ -32,7 +32,7 @@ local function mappings(client, buf) local bmap = function(mode, lhs, rhs, opts) local options = { buffer = buf } if opts then options = vim.tbl_extend("force", options, opts) end - utils.map(mode, lhs, rhs, options) + map(mode, lhs, rhs, options) end -- See `:help vim.lsp.*` for documentation on any of the below functions () diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index 18fc8fe4..fe1e525e 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -3,7 +3,7 @@ if vim.g.vscode then return end -local utils = require('utils') +local map = require('utils').map -- Filetype -> formatter client name -- Listed filetypes get auto-format on save. @@ -108,7 +108,7 @@ _G.Config.new_autocmd('LspAttach', { local buf = args.buf local ft = vim.bo[buf].filetype - utils.map({ 'n', 'v' }, '=', function() + map({ 'n', 'v' }, '=', function() if vim.bo[buf].filetype == 'python' then format_python_black(buf) return From c5ca45f2db0a6f8c98b67f95c8fe5df870f29b92 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:12:04 +0900 Subject: [PATCH 099/366] refactor(nvim): DRY visual-range handlers in 73_git.lua ponytail: Gl and Go had identical 6-line bodies that only differed in which gitgud function they called. Extract with_visual_range helper, pass the function in. --- .config/nvim/plugin/73_git.lua | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/.config/nvim/plugin/73_git.lua b/.config/nvim/plugin/73_git.lua index 57671e63..3221dc8f 100644 --- a/.config/nvim/plugin/73_git.lua +++ b/.config/nvim/plugin/73_git.lua @@ -8,23 +8,21 @@ vim.pack.add({ local map = require('utils').map local gitgud = require('custom.gitgud') +local function with_visual_range(callback) + return function() + local start_line = vim.fn.line("v") + local end_line = vim.fn.line(".") + if end_line < start_line then start_line, end_line = end_line, start_line end + callback({ start_line = start_line, end_line = end_line }) + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) + end +end + map('n', 'Gl', function() gitgud.copy_github_permalink() end, { desc = "Copy GitHub permalink" }) -map('x', 'Gl', function() - local start_line = vim.fn.line("v") - local end_line = vim.fn.line(".") - if end_line < start_line then start_line, end_line = end_line, start_line end - gitgud.copy_github_permalink({ start_line = start_line, end_line = end_line }) - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) -end, { desc = "Copy GitHub permalink (range)" }) +map('x', 'Gl', with_visual_range(gitgud.copy_github_permalink), { desc = "Copy GitHub permalink (range)" }) map('n', 'Go', function() gitgud.open_github_file() end, { desc = "Open GitHub file" }) -map('x', 'Go', function() - local start_line = vim.fn.line("v") - local end_line = vim.fn.line(".") - if end_line < start_line then start_line, end_line = end_line, start_line end - gitgud.open_github_file({ start_line = start_line, end_line = end_line }) - vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", true, false, true), "nx", false) -end, { desc = "Open GitHub file (range)" }) +map('x', 'Go', with_visual_range(gitgud.open_github_file), { desc = "Open GitHub file (range)" }) -- blame.nvim require('blame').setup({ From b9ba55b09f0d39b1f5bb656d223c9e8806589e36 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:13:13 +0900 Subject: [PATCH 100/366] refactor(nvim): DRY visual-mode '*'/'#' search handlers ponytail: the two keymap bodies differed only in '/' vs '?'. Extract vsearch(direction) helper. --- .config/nvim/plugin/20_keymaps.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 6d8b0252..c27a3ebf 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -108,14 +108,13 @@ local function vset_search() vim.fn.setreg('s', temp) end -map('x', '*', function() +local function vsearch(direction) vset_search() - return '/' .. vim.fn.getreg('/') .. '' -end, { expr = true }) -map('x', '#', function() - vset_search() - return '?' .. vim.fn.getreg('/') .. '' -end, { expr = true }) + return direction .. vim.fn.getreg('/') .. '' +end + +map('x', '*', function() return vsearch('/') end, { expr = true }) +map('x', '#', function() return vsearch('?') end, { expr = true }) -- Search inside visual selection -- https://www.reddit.com/r/neovim/comments/1mxeghf/using_as_a_multipurpose_search_tool/ From 04d3917f8c77e229274dc4daf9d6d715609f2134 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:48:34 +0900 Subject: [PATCH 101/366] chore(nvim): drop unused keybinds --- .config/nvim/plugin/20_keymaps.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index c27a3ebf..9eab31ab 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -51,11 +51,6 @@ if not vim.g.vscode then map("n", "wi", "l", { desc = "Focus right" }) map("n", "wo", "o", { desc = "Close all but current" }) - -- Resize windows - map("n", "", ":resize +2") - map("n", "", ":resize -2") - map("n", "", ":vertical resize -2") - map("n", "", ":vertical resize +2") -- Diagnostics map('n', 'gl', vim.diagnostic.open_float, { desc = "List diagnostics" }) From 7f7ccf998d2ebc35c8d3bb8316ecc4dc0946d614 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:58:56 +0900 Subject: [PATCH 102/366] feat(shell): huponexit --- home/.bashrc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/home/.bashrc b/home/.bashrc index 6e7836a5..0dbc4af1 100644 --- a/home/.bashrc +++ b/home/.bashrc @@ -135,7 +135,8 @@ PROMPT_COMMAND=__prompt_command # checkjobs check if there are any stopped or running jobs before exiting an interactive shell # checkwinsize check the window size after each external command and, if necessary, updates the values of $LINES and $COLUMNSk # cmdhist save multiple-line commands in the same history entry -shopt -s autocd cdspell dirspell histappend checkjobs direxpand checkwinsize cmdhist +# huponexit send SIGHUP to all jobs when an interactive login shell exits +shopt -s autocd cdspell dirspell histappend checkjobs direxpand checkwinsize cmdhist huponexit stty -ixon # Disable ctrl-s and ctrl-q. From 1e7b8f681991b63d3677da3e27f27549e40d1deb Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:37:41 +0900 Subject: [PATCH 103/366] chore(nvim): drop dead vim.g.loaded_*_provider flags These flags were no-ops in nvim 0.10+: - python3/node providers were replaced by vim.lsp - perl/ruby providers never existed in nvim --- .config/nvim/plugin/10_opts.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index b155e7dd..877cb4c8 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -1,8 +1,4 @@ -- :options -vim.g.loaded_python3_provider = 0 -vim.g.loaded_node_provider = 0 -vim.g.loaded_perl_provider = 0 -vim.g.loaded_ruby_provider = 0 -- General ==================================================================== vim.g.mapleader = " " -- Leader key From d57d329a97c0d813741dca70741a09067db068fd Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:37:59 +0900 Subject: [PATCH 104/366] refactor(nvim): derive maplocalleader from mapleader Both were the literal ' '; maplocalleader now references mapleader so they stay in sync if the leader is ever changed. --- .config/nvim/plugin/10_opts.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index 877cb4c8..e76735d1 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -2,7 +2,7 @@ -- General ==================================================================== vim.g.mapleader = " " -- Leader key -vim.g.maplocalleader = " " -- Local leader +vim.g.maplocalleader = vim.g.mapleader vim.o.winborder = 'rounded' -- Consistent borders on all floats (0.11+) vim.o.shell = 'bash' vim.o.mousescroll = 'ver:6,hor:6' -- Customize mouse scroll From 7f62652bc6837cb8bb7912cab914bef01427fe0c Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:45:39 +0900 Subject: [PATCH 105/366] chore(nvim): drop dead gp map comment --- .config/nvim/plugin/20_keymaps.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 9eab31ab..440f2bd9 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -58,8 +58,6 @@ if not vim.g.vscode then map('n', '[d', function() vim.diagnostic.jump({ count = -1, float = true }) end, { desc = "Go to previous diagnostic" }) -- :only f F gf gF = + - > < _ | x - -- todo read about tags - -- map("n", "gp", "}") map("n", "q", ":q", { silent = true }) map("n", "C", function() From 3fa491c76436b3024e6374dabaeb6af653a32eb4 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:47:14 +0900 Subject: [PATCH 106/366] refactor(nvim): use vim.fs.joinpath for session dir path --- .config/nvim/plugin/999_session.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/999_session.lua b/.config/nvim/plugin/999_session.lua index 6bbdfce3..e2fb09bf 100644 --- a/.config/nvim/plugin/999_session.lua +++ b/.config/nvim/plugin/999_session.lua @@ -1,5 +1,5 @@ -- Auto-session management -local session_dir = vim.fn.stdpath("state") .. "/sessions" +local session_dir = vim.fs.joinpath(vim.fn.stdpath("state"), "sessions") -- Directories where sessions should not be saved local skip_dirs = { From 5cce5c3d7ea615b9e3ae1842385bad8f43d269a7 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:47:35 +0900 Subject: [PATCH 107/366] refactor(nvim): use vim.fs.joinpath for pyright venv path --- .config/nvim/after/lsp/pyright.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/after/lsp/pyright.lua b/.config/nvim/after/lsp/pyright.lua index 6456b280..7a0ffc01 100644 --- a/.config/nvim/after/lsp/pyright.lua +++ b/.config/nvim/after/lsp/pyright.lua @@ -1,7 +1,7 @@ local function use_project_venv(_, config) if not config.root_dir then return end - local python = config.root_dir .. '/.venv/bin/python' + local python = vim.fs.joinpath(config.root_dir, '.venv/bin/python') if not vim.fn.executable(python) then return end config.settings = config.settings or {} From d8b64f27c856690e0ccac9210bdb5eacc268da9e Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:47:51 +0900 Subject: [PATCH 108/366] refactor(nvim): use vim.fs.joinpath for tmux session-popup path --- .config/nvim/lua/custom/ai.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/lua/custom/ai.lua b/.config/nvim/lua/custom/ai.lua index 0880a936..3628b940 100644 --- a/.config/nvim/lua/custom/ai.lua +++ b/.config/nvim/lua/custom/ai.lua @@ -38,7 +38,7 @@ local function send_to_pane(pane_id, path) end local function focus_popup(tool) - local popup = vim.fn.expand("~/.config/tmux/session-popup") + local popup = vim.fs.joinpath(vim.env.HOME, ".config/tmux/session-popup") local job_id = vim.fn.jobstart({ "tmux", "display-popup", "-T", tool, "-w", "95%", "-h", "95%", "-E", popup, tool }, { detach = true, }) From 758eac77a455fba2f8ba5308646dc901fe4c5183 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:50:45 +0900 Subject: [PATCH 109/366] refactor(nvim): use vim.cmd.packadd directly for undotree --- .config/nvim/plugin/20_keymaps.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index 440f2bd9..c86c7640 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -114,6 +114,6 @@ map('x', '#', function() return vsearch('?') end, { expr = true }) map("x", "/", "/\\%V") -- `:h /\%V` map("n", "u", function() - vim.cmd("packadd nvim.undotree") + vim.cmd.packadd("nvim.undotree") require("undotree").open() end, { desc = "Undo tree" }) From 4071af7467e929cb3eaece54fce577c145e6060b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:51:03 +0900 Subject: [PATCH 110/366] refactor(nvim): use vim.tbl_contains for skip_dirs check --- .config/nvim/plugin/999_session.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.config/nvim/plugin/999_session.lua b/.config/nvim/plugin/999_session.lua index e2fb09bf..8933169e 100644 --- a/.config/nvim/plugin/999_session.lua +++ b/.config/nvim/plugin/999_session.lua @@ -20,10 +20,8 @@ local function should_save_session() end -- Don't save in skip directories - for _, dir in ipairs(skip_dirs) do - if cwd == dir then - return false - end + if vim.tbl_contains(skip_dirs, cwd) then + return false end -- Don't save if directory doesn't exist or isn't accessible From 975fec5de564d3c54d5f3737695f371679153f0b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:51:47 +0900 Subject: [PATCH 111/366] chore(nvim): drop g:loaded_*_provider startup gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four flags gated legacy autoload scripts (runtime/autoload/provider/{perl,ruby,python3,node}.vim) that short-circuit when the var is set. Those scripts only fire when the legacy :perl/:ruby/:python3/:node commands or :checkhealth provider are invoked — none of which run on normal edit startup, so the gates were effectively dead in the common case. Verified with hyperfine (30 runs, warmup 3): WITH flags 69.0ms +/- 5.0ms vs WITHOUT flags 68.7ms +/- 1.1ms — ratio 1.00x +/- 0.07, well within noise. The legacy providers themselves were already obsolete (Python3/Node plugin hosts removed in 0.10; Perl/Ruby never fully implemented in nvim). --- .config/nvim/plugin/999_session.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.config/nvim/plugin/999_session.lua b/.config/nvim/plugin/999_session.lua index 8933169e..dd617479 100644 --- a/.config/nvim/plugin/999_session.lua +++ b/.config/nvim/plugin/999_session.lua @@ -38,9 +38,7 @@ local function get_session_file() end -- Create the dir if it doesn't exist -if vim.uv.fs_stat(session_dir) == nil then - vim.fn.mkdir(session_dir, "p") -end +vim.fn.mkdir(session_dir, "p") local session_group = vim.api.nvim_create_augroup("auto_sessions", { clear = true }) @@ -69,4 +67,3 @@ _G.Config.new_autocmd("VimLeavePre", { group = session_group, once = true, }) - From d24d2b4fa0ecd2eca154dcc84f4e111eb29d4d2d Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:53:28 +0900 Subject: [PATCH 112/366] chore: better commits --- home/.claude/skills/commit/SKILL.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/home/.claude/skills/commit/SKILL.md b/home/.claude/skills/commit/SKILL.md index 240197c2..f7b04a90 100644 --- a/home/.claude/skills/commit/SKILL.md +++ b/home/.claude/skills/commit/SKILL.md @@ -24,8 +24,17 @@ Create a single git commit using **Conventional Commits** format: - **type**: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `perf`, `ci`, `build` - **scope**: optional, the area of the codebase (e.g. `nvim`, `tmux`, `shell`, `backend`) -- **summary**: imperative, lowercase, no period, entire title max 50 chars +- **summary**: imperative, lowercase, no period, max 50 chars (hard limit 72) -If the changes span multiple unrelated areas, pick the most significant one for the type/scope. Add a body only if the "why" isn't obvious from the summary. +**Litmus test**: a new contributor should understand the problem, why it matters, and the impact without opening files or reading the diff. Avoid code identifiers, filenames, and function names in the summary unless they ARE the user-facing impact. + +- Bad: `Add NameFromHex with sync.Once lazy init` +- Good: `Improve color name lookup performance while keeping startup fast` +- Bad: `fix: nil pointer in session.go` +- Good: `fix: prevent session loading from crashing on missing metadata` + +Draft 1-2 sentences focused on the "why" and outcome, not a list of files or implementation details. Use clear verbs: `add` (new capability), `update` (enhancement), `fix` (bug fix). Add a body only when the "why" isn't obvious from the summary; wrap body lines at 72 chars. + +If the changes span multiple unrelated areas, pick the most significant one for the type/scope. Do not send any other text or messages besides tool calls (except when asking the user what to commit). From 736ed512288ad8be8407f2968d02c2c602e9cb0e Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:24:44 +0900 Subject: [PATCH 113/366] chore: better skill --- home/.claude/skills/create-pr/SKILL.md | 31 +++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/home/.claude/skills/create-pr/SKILL.md b/home/.claude/skills/create-pr/SKILL.md index c5b45dbe..9822fd21 100644 --- a/home/.claude/skills/create-pr/SKILL.md +++ b/home/.claude/skills/create-pr/SKILL.md @@ -1,13 +1,16 @@ --- name: create-pr -description: Create a pull request. Use this skill whenever the user asks to create a PR or pull request — do not use `gh pr create` directly. +description: Use when the user asks to create, open, make, draft, raise, or submit a PR / pull request / MR. Triggers on phrases like "create a PR", "open a PR", "make a PR", "draft a PR", "PR this", "raise a pull request", "submit a PR", "push and PR". MUST be used instead of calling `gh pr create` directly. allowed-tools: Bash(git:*), Bash(gh pr:*) --- ## Context - Current branch: !`git branch --show-current` -- PR template: !`fd -d 2 -i -t f "pull_request_template" | head -1 | xargs cat 2>/dev/null` +- Default branch: !`gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'` +- Staged changes: !`git diff --cached --stat` +- Unstaged/untracked summary: !`git status --short` +- PR template: !`find . -maxdepth 2 -type f -iname "pull_request_template*" 2>/dev/null | head -1 | xargs cat 2>/dev/null` **User description:** @@ -17,9 +20,21 @@ $ARGUMENTS ## Your task -- Push the branch first with `git push -u origin HEAD`. -- Then create a draft PR with `gh pr create --draft --title "..." --body "..."`. Use a HEREDOC for the body. -- Make sure to follow the PR template if one exists. +### Step 1 — Determine what goes into the PR + +- If there are **staged changes**: commit them now with an appropriate commit message derived from the diff, then proceed. +- If there are **no staged changes**: ask the user what they want included before proceeding. Do not guess or auto-stage. + +### Step 2 — Determine the working branch + +- If currently on the **default branch**: derive a short, descriptive branch name from the staged diff or user description (conventional format: `type/short-slug`), create it with `git checkout -b `, then proceed. +- Otherwise: use the current branch as-is. + +### Step 3 — Push and open the PR + +- Push with `git push -u origin HEAD`. +- Create a draft PR targeting the **default branch** with `gh pr create --draft --base --title "..." --body "..."`. Use a HEREDOC for the body. +- Always target the default branch unless the user explicitly specifies a different base. ### Title @@ -29,8 +44,4 @@ $ARGUMENTS ### Body - Fill in the PR template if one exists. Remove sections that don't apply. - -### Rules - -- If on the default branch, ask which branch to use. -- Write the body like a human would, no execise of bullet points, be brief and assume the reader knows the codebase. +- Write like a human would — no exhaustive bullet points, be brief and assume the reader knows the codebase. From 2f8996bdbbcbe5971f4a6c9a84917a95ba20b18e Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:12:45 +0900 Subject: [PATCH 114/366] refactor(skills): make skills harness-agnostic Moves the eight skills from home/.claude/skills/ to home/.agents/skills/ so they're owned by the dotfiles repo, not by Claude Code's namespace. Each skill is mirrored into ~/.claude/skills/ by setupDots so Claude Code still finds them, while other harnesses (Pi, Codex) can read from ~/.agents/skills/ directly. Also audits each skill for Claude-Code-only features that other harnesses wouldn't understand: - drop allowed-tools: / argument-hint: YAML frontmatter - replace !`cmd` inline bash with "run `cmd` to ..." instructions - replace $ARGUMENTS placeholder with neutral "use the description the user provided" text - fix go-expert -> go-code reference in risk-review - generalize the deepwiki research hint in architect-review Result: each SKILL.md renders as plain markdown and works in any harness that loads the Agent Skills spec. --- .../skills/architect-review/SKILL.md | 2 +- .../skills/code-guidelines/SKILL.md | 0 home/{.claude => .agents}/skills/commit/SKILL.md | 8 +++----- .../skills/create-pr/SKILL.md | 15 ++++++--------- home/{.claude => .agents}/skills/go-code/SKILL.md | 2 +- .../skills/go-code/references/PKG_DESIGN.md | 0 .../skills/go-testing/SKILL.md | 2 +- .../skills/risk-review/SKILL.md | 2 +- 8 files changed, 13 insertions(+), 18 deletions(-) rename home/{.claude => .agents}/skills/architect-review/SKILL.md (98%) rename home/{.claude => .agents}/skills/code-guidelines/SKILL.md (100%) rename home/{.claude => .agents}/skills/commit/SKILL.md (80%) rename home/{.claude => .agents}/skills/create-pr/SKILL.md (72%) rename home/{.claude => .agents}/skills/go-code/SKILL.md (98%) rename home/{.claude => .agents}/skills/go-code/references/PKG_DESIGN.md (100%) rename home/{.claude => .agents}/skills/go-testing/SKILL.md (95%) rename home/{.claude => .agents}/skills/risk-review/SKILL.md (99%) diff --git a/home/.claude/skills/architect-review/SKILL.md b/home/.agents/skills/architect-review/SKILL.md similarity index 98% rename from home/.claude/skills/architect-review/SKILL.md rename to home/.agents/skills/architect-review/SKILL.md index 524961b2..99a8734f 100644 --- a/home/.claude/skills/architect-review/SKILL.md +++ b/home/.agents/skills/architect-review/SKILL.md @@ -47,7 +47,7 @@ gh pr checkout - Only report design pattern findings where a named structural problem exists in the current code. - Do not suggest a pattern just because it could apply — it must solve something that is demonstrably broken or painful. - For Go code, apply `go-code` guidance. -- Research established patterns with `deepwiki` if uncertain what the ecosystem convention is. +- Research established patterns (e.g., via `deepwiki` MCP, web search, or docs lookup) if uncertain what the ecosystem convention is. ## Workflow diff --git a/home/.claude/skills/code-guidelines/SKILL.md b/home/.agents/skills/code-guidelines/SKILL.md similarity index 100% rename from home/.claude/skills/code-guidelines/SKILL.md rename to home/.agents/skills/code-guidelines/SKILL.md diff --git a/home/.claude/skills/commit/SKILL.md b/home/.agents/skills/commit/SKILL.md similarity index 80% rename from home/.claude/skills/commit/SKILL.md rename to home/.agents/skills/commit/SKILL.md index f7b04a90..3097cb11 100644 --- a/home/.claude/skills/commit/SKILL.md +++ b/home/.agents/skills/commit/SKILL.md @@ -1,18 +1,16 @@ --- name: commit description: Create a git commit. Use whenever you or the user wants to create a commit. -argument-hint: [what to commit or "staged files"] -allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*), Bash(git diff:*) --- ## Context -- Are there staged files? !`git diff --cached --quiet && echo "**No**" || echo "**Yes**"` -- The user provided the following description of what to commit: `$ARGUMENTS` (if empty, assume "staged changes") +- Are there staged files? Run `git diff --cached --quiet` — silent output means no staged files. +- Use the description the user provided when invoking this skill. If none was given, assume "staged changes". ### `git status -s` output -!`git status -s` +Run `git status -s` to see the staged changes. ## Your task diff --git a/home/.claude/skills/create-pr/SKILL.md b/home/.agents/skills/create-pr/SKILL.md similarity index 72% rename from home/.claude/skills/create-pr/SKILL.md rename to home/.agents/skills/create-pr/SKILL.md index 9822fd21..aac90f3f 100644 --- a/home/.claude/skills/create-pr/SKILL.md +++ b/home/.agents/skills/create-pr/SKILL.md @@ -1,22 +1,19 @@ --- name: create-pr description: Use when the user asks to create, open, make, draft, raise, or submit a PR / pull request / MR. Triggers on phrases like "create a PR", "open a PR", "make a PR", "draft a PR", "PR this", "raise a pull request", "submit a PR", "push and PR". MUST be used instead of calling `gh pr create` directly. -allowed-tools: Bash(git:*), Bash(gh pr:*) --- ## Context -- Current branch: !`git branch --show-current` -- Default branch: !`gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/origin/||'` -- Staged changes: !`git diff --cached --stat` -- Unstaged/untracked summary: !`git status --short` -- PR template: !`find . -maxdepth 2 -type f -iname "pull_request_template*" 2>/dev/null | head -1 | xargs cat 2>/dev/null` +- Current branch: run `git branch --show-current`. +- Default branch: run `gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name'`; fall back to `git symbolic-ref refs/remotes/origin/HEAD | sed 's|refs/remotes/origin/||'` if that fails. +- Staged changes: run `git diff --cached --stat`. +- Unstaged/untracked summary: run `git status --short`. +- PR template: run `find . -maxdepth 2 -type f -iname "pull_request_template*" | head -1 | xargs cat` if a template exists. **User description:** -``` -$ARGUMENTS -``` +The user may have provided a description when invoking this skill. Use it; otherwise the PR should be derived from the staged/unstaged changes. ## Your task diff --git a/home/.claude/skills/go-code/SKILL.md b/home/.agents/skills/go-code/SKILL.md similarity index 98% rename from home/.claude/skills/go-code/SKILL.md rename to home/.agents/skills/go-code/SKILL.md index 362605a7..ffd20dca 100644 --- a/home/.claude/skills/go-code/SKILL.md +++ b/home/.agents/skills/go-code/SKILL.md @@ -11,7 +11,7 @@ Go best practices for clean, idiomatic, maintainable code. Core principle: **Cle ## Context -- The project is using Go version !`go list -m -f '{{.GoVersion}}'`. Your training data might be outdated; verify against the latest docs. +- Determine the project's Go version by running `go list -m -f '{{.GoVersion}}'`. Your training data might be outdated; verify against the latest docs. ## Principles diff --git a/home/.claude/skills/go-code/references/PKG_DESIGN.md b/home/.agents/skills/go-code/references/PKG_DESIGN.md similarity index 100% rename from home/.claude/skills/go-code/references/PKG_DESIGN.md rename to home/.agents/skills/go-code/references/PKG_DESIGN.md diff --git a/home/.claude/skills/go-testing/SKILL.md b/home/.agents/skills/go-testing/SKILL.md similarity index 95% rename from home/.claude/skills/go-testing/SKILL.md rename to home/.agents/skills/go-testing/SKILL.md index 5c90d2f4..e0e10f7c 100644 --- a/home/.claude/skills/go-testing/SKILL.md +++ b/home/.agents/skills/go-testing/SKILL.md @@ -9,7 +9,7 @@ Go testing best practices for clean, parallel, maintainable tests. Use alongside ## Context -- The project is using Go version !`go list -m -f '{{.GoVersion}}'`. Your training data might be outdated; verify against the latest docs. +- Determine the project's Go version by running `go list -m -f '{{.GoVersion}}'`. Your training data might be outdated; verify against the latest docs. ## Principles diff --git a/home/.claude/skills/risk-review/SKILL.md b/home/.agents/skills/risk-review/SKILL.md similarity index 99% rename from home/.claude/skills/risk-review/SKILL.md rename to home/.agents/skills/risk-review/SKILL.md index c7ac4a68..9af37a6c 100644 --- a/home/.claude/skills/risk-review/SKILL.md +++ b/home/.agents/skills/risk-review/SKILL.md @@ -43,7 +43,7 @@ The diff comes from the PR. If `scope` is also provided, use it to narrow focus - Ignore style-only feedback unless it masks a correctness problem. - Skip compiler/LSP-only catches unless they reveal runtime risk. - Verify claims with code search and call-site inspection — do not infer. -- For Go code, apply `go-expert` guidance. +- For Go code, apply `go-code` guidance. ## Risk Checklist From 237f87c2f7474e7082951accccca399bedbf2c2f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:33:19 +0900 Subject: [PATCH 115/366] fix(setupdots): safe per-skill skill mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the single `~/.agents/skills -> ~/.claude/skills` reverse symlink with per-skill symlinks: each `~/.claude/skills/` becomes a symlink to `~/.agents/skills/`. The old reverse-symlink approach clobbered skills installed by other tools (`npx agents`, the superpowers Claude Code plugin, etc.) because re-running setupDots would blow away the whole `~/.claude/skills` tree. The new loop only replaces entries that are clearly our own stale leftovers — broken dotfile symlinks, or dirs whose contents are only broken dotfile symlinks. Working symlinks, real files, and content from other programs are preserved. Gated by the new `is_stale_dotfile_{link,dir,entry}` helpers. Also fixes a latent macOS bug: `remove_broken_symlinks` used `find -xtype l` which BSD find doesn't support, so it was silently doing nothing on macOS. Replaced with a portable `[ -L ] && [ ! -e ]` check. --- setupDots | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/setupDots b/setupDots index 817b96f7..05c42932 100755 --- a/setupDots +++ b/setupDots @@ -21,15 +21,73 @@ complete_message() { remove_broken_symlinks() { local target_dir="$1" + local link target + # BSD `find` (macOS) doesn't support `-xtype`, so iterate all symlinks + # and check each one ourselves. while IFS= read -r -d '' link; do - local target + [ -L "$link" ] || continue + [ -e "$link" ] && continue target=$(readlink "$link") - if [[ $target == *"dotfiles"* ]]; then + # Match absolute ("/.../dotfiles/...") and relative ("../../../dotfiles/...") paths. + if [[ "$target" == *"$repo_dir/"* || "$target" == *"/dotfiles/"* ]]; then echo -e "${YELLOW}Removing broken symlink:${NC} $link -> $target" rm -f "$link" fi - done < <(find "$target_dir" -type l -xtype l -print0 2>/dev/null) + done < <(find "$target_dir" -type l -print0 2>/dev/null) +} + +# True if $1 is a broken symlink whose target resolves into the dotfiles repo. +# Matches both absolute paths ("/.../dotfiles/...") and stow-style relative +# paths ("../../../dotfiles/..."). +is_stale_dotfile_link() { + [ -L "$1" ] && [ ! -e "$1" ] || return 1 + local target + target=$(readlink "$1") + [[ "$target" == *"$repo_dir/"* || "$target" == *"/dotfiles/"* ]] +} + +# True if $1 is a directory whose entries are all stale dotfile symlinks +# (or empty) — the leftover state from a previous `home/.claude/skills/` stow. +is_stale_dotfile_dir() { + local dir="$1" entry + [ -d "$dir" ] || return 1 + local all_stale=true + while IFS= read -r -d '' entry; do + is_stale_dotfile_link "$entry" || { all_stale=false; break; } + done < <(find "$dir" -mindepth 1 -maxdepth 1 -print0 2>/dev/null) + [ "$all_stale" = true ] +} + +# True if $1 is safe to replace with our per-skill symlink: missing, a +# broken dotfile symlink, or a dir whose contents are all broken dotfile +# symlinks. False if $1 might be content from `npx agents` or another +# tool — we leave those alone to avoid clobbering other programs. +is_stale_dotfile_entry() { + local p="$1" + + # Path doesn't resolve — either missing or a broken symlink + if [ ! -e "$p" ]; then + if [ -L "$p" ]; then + # Broken symlink → replace only if target is in the dotfiles repo + is_stale_dotfile_link "$p" && return 0 + return 1 + fi + # Truly missing → create new + return 0 + fi + + # Working symlink → leave alone (could be npx, our correct link, etc.) + if [ -L "$p" ]; then + return 1 + fi + + # Real dir → replace only if contents are all stale dotfile symlinks + if [ -d "$p" ]; then + is_stale_dotfile_dir "$p" && return 0 + fi + + return 1 } user_config() { @@ -38,13 +96,40 @@ user_config() { remove_broken_symlinks ~/.local run_command stow --dir "$repo_dir" --target ~/.local --restow .local + # ~/.agents/skills must be a real (stowed) directory before stow so that + # ~/.claude/skills/ can symlink to entries inside it. + if [ -L ~/.agents/skills ]; then + echo -e "${YELLOW}Removing stale symlink:${NC} ~/.agents/skills -> $(readlink ~/.agents/skills)" + rm -f ~/.agents/skills + fi + remove_broken_symlinks ~/.claude run_command stow --dir "$repo_dir" --target ~ --restow home - # Share Claude skills with Codex - # Must be after home stow so ~/.claude/skills exists as a symlink first - rm -f ~/.agents/skills - run_command ln -s ~/.claude/skills ~/.agents/skills + # Share .agents/skills with Claude Code: per-skill symlinks from + # ~/.claude/skills/ to ../../.agents/skills/. + # Only replace entries that are clearly our own stale leftovers — never + # touch symlinks or content from `npx agents` and other tools. + run_command mkdir -p ~/.claude/skills + for skill_dir in ~/.agents/skills/*/; do + [ -d "$skill_dir" ] || continue + skill_name=$(basename "$skill_dir") + link_path="$HOME/.claude/skills/$skill_name" + link_target="../../.agents/skills/$skill_name" + + # Skip if already a correct symlink (idempotent) + if [ -L "$link_path" ] && [ "$(readlink "$link_path")" = "$link_target" ]; then + continue + fi + + if ! is_stale_dotfile_entry "$link_path"; then + echo -e "${YELLOW}Skipping ${skill_name}: ${link_path} is not a stale dotfile leftover (leaving it alone — may be from npx agents or another tool)${NC}" + continue + fi + + rm -rf "$link_path" + run_command ln -s "$link_target" "$link_path" + done remove_broken_symlinks ~/.config run_command stow --dir "$repo_dir" --target ~/.config --restow .config From 590acd4fd6e1656222e8863ed332facd5c1dbfaf Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:54:07 +0900 Subject: [PATCH 116/366] chore: rewrite agents --- AGENTS.md | 189 +++++++++++++++++++++--------------------------------- 1 file changed, 74 insertions(+), 115 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c8a57157..39632869 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,115 +1,74 @@ -# AGENTS.md - -Personal dotfiles repo for Arch Linux + macOS. Configs managed via GNU Stow (`home/`→`~/`, `.config/`→`~/.config/`, `.local/`→`~/.local/`). - -## Layout - -``` -home/ → ~/ (shell dotfiles: .bashrc, .profile, .aliasrc, .gitconfig, .claude/, .inputrc, .ssh/config) -.config/ → ~/.config/ (nvim, ghostty, tmux, hyprland, waybar, dunst, fcitx5, lazygit, mpv, ...) -.local/bin/ → ~/.local/bin/ (custom shell scripts) -misc/ → system-level configs (keymaps, systemd, pacman-hooks, sudoers) -``` - -## Architecture & Key Gotchas - -### GNU Stow Conventions -- `.config/` is stowed directly to `~/.config/` (not `~/.config/.config/`) -- `home/` is stowed to `~/` -- `.local/` is stowed to `~/.local/` -- `.stowrc` enables `--no-folding` (creates symlinks for each file, not directories) - -### Bi-Platform Code -Platform conditionals everywhere — always check both branches: -- Shell: `[ "$(uname)" = "Darwin" ]` vs `else` (Linux) -- Shell: `[ "$(uname)" = "Linux" ]` -- macOS uses Homebrew (`/opt/homebrew/bin`), GNU utils via aliases (`gls`, `gfind`, `gsed`) -- Linux uses Arch Linux, systemd, Hyprland - -### Keyboard: Colemak DH -**Every keybinding config in this repo assumes Colemak DH.** Home row movement keys are: -``` -Colemak: m(←) n(↓) e(↑) i(→) -Qwerty: h(←) j(↓) k(↑) l(→) -``` - -Config files with Colemak remappings: -- `.config/nvim/lua/colemak.lua` — nvim normal/visual mode movement, displaced keys -- `.config/nvim/plugin/75_snacks.lua` — picker keys: `n`=down, `e`=up -- `.config/tmux/tmux.conf` — copy-mode-vi: `m`/`n`/`e`/`i`, pane movement: `M-m`/`M-n`/`M-e`/`M-i` -- `.config/ghostty/config` — cmd+`m`/`n`/`e`/`i`/`h`/`o` passthrough to tmux -- `.config/hypr/hyprland.lua` — `M`/`N`/`E`/`I` for movefocus -- `home/.inputrc` — vi-mode bindings: `n`=forward-search, `e`=backward-search - -### Shell (bash) -- `.profile` → login shell env (PATH, XDG, platform vars, `fcitx5`/Wayland setup) - - On Linux tty1: auto-starts Hyprland inline from `.profile` -- `.bashrc` → interactive shell (sources `.privrc`, `.priv_env`, shopt settings, aliases) -- `.aliasrc` → sourced by `.bashrc`, contains all aliases/functions/bindings/completions -- `.inputrc` → vi editing mode (`set editing-mode vi`), Colemak search bindings -- Lazy completions: `kubectl`, `helm`, `k6`, `gh`, `orb` use `_lazy_completion` wrapper -- `.privrc` and `.priv_env` are sourced if present — **not tracked in repo** - -### Neovim Config -Uses **built-in `vim.pack.add`** (nvim 0.12+), not lazy.nvim. - -Load order (inside `.config/nvim/`): -1. `init.lua` — sets `_G.Config`, creates augroup `custom-config`, `_G.Config.new_autocmd()` helper -2. `plugin/*.lua` — loaded alphabetically by `vim.pack`: - - `10_opts.lua` — general options, UI, editing, diagnostic config, `vim.ui.open` override - - `20_keymaps.lua` — general keybindings (runs `colemak.setup()`), user commands - - `30_autocmds.lua` — `FileType`, `TextYankPost`, `VimResized`, `BufWritePre`, cursorline toggles, `shfmt` format-on-save - - `40_lsp_behavior.lua` — LSP keymaps, diagnostics, highlight references - - `41_lsp_format.lua` — LSP formatting and format-on-save - - `70_theme.lua` — colorscheme (kanagawa/tokyonight) - - `71_treesitter.lua` — nvim-treesitter install + per-FileType highlighting - - `72_flash.lua` — flash.nvim (navigation) - - `73_git.lua` — git blame, snacks git pickers - - `74_sidekick.lua` — sidekick.nvim - - `75_snacks.lua` — Snacks.nvim (picker, bigfile, input) with Colemak Picker keys - - `76_mini.lua` — mini.nvim modules - - `77_blink_cmp.lua` — blink.cmp (completion) - - `78_lsp.lua` — LSP config (nvim-lspconfig), enables selected LSP servers - - `79_mason.lua` — mason.nvim (LSP installer) - - `999_session.lua` — session management - - `999_vscode.lua` — VS Code-specific keybindings -3. `lua/colemak.lua` — Colemak DH mapping table (also toggled via `:ColemakEnable`/`:ColemakDisable`) -4. `lua/utils.lua` — `M.map()` wrapper, `M.copy_code_block()` -5. `lua/custom/gitgud.lua` — GitHub permalink/open helpers used by git keymaps -6. `after/lsp/{gopls,jsonls,lua_ls,yamlls}.lua` — per-server LSP config - -**VS Code mode**: Many plugin files early-return `if vim.g.vscode then return end`. The config works in both nvim and VS Code. - -### Claude Code Integration -- `home/.claude/settings.json` — permissions, hooks (permission dialog, destructive cmd blocker, gofmt on write), plugins -- `home/.claude/CLAUDE.md` — communication/coding guidelines for Claude Code agent -- `home/.claude/agents/` — custom agent definitions (critic, risk-reviewer) -- `home/.claude/skills/` — reusable skill files for various tasks -- `home/.claude/status` — status bar script showing tokens/cost/model/duration - -### Git Config -- `home/.gitconfig` — delta diff viewer, SSH push, aliases, interactive diffFilter -- `home/.gitalias` — sourced by `.gitconfig` via `!source ~/.gitalias && ...` -- Uses `git@github.com:` insteadOf `https://github.com/` -- Git aliases in `.gitconfig` are Colemak-agnostic but use short single-letter aliases: `g a`, `g c`, `g s`, `g d`, `g f`, `g p` - -### CI -- GitHub Actions: ShellCheck on push/PR, Dependabot auto-merge for patch updates - -### Submodules -- `.config/mpv/scripts/subs2srs` — https://github.com/Ajatt-Tools/mpvacious - -## Conventions -- **Shell**: `#!/usr/bin/env bash` with `set -euo pipefail` -- **Neovim**: `vim.pack.add` for plugins, `_G.Config.new_autocmd()` for autocmds, `utils.map()` for keybindings -- **Lua**: `local` everywhere, no OOP, `local M = {}` return pattern for modules -- **Stow**: `.stowrc` sets `--no-folding`; setup commands use `--restow` - -## Gotchas -- `.profile` is pure POSIX sh. `home/.bash_profile` is a symlink to the same file (bash login shell compatibility). UWSM preloader sources it directly with `/bin/sh`. -- `.profile` on Linux auto-launches Hyprland on tty1 — **do not edit blindly on macOS** -- `setupDots` resolves paths from its own location, so it can be run from outside the repo root -- Ghostty cmd-keybindings pass through to tmux (`cmd+a` → `\x1ba`) — this is how Colemak navigation reaches tmux -- nvim `plugin/` files load in **alphanumeric order by filename** — the `10_`/`20_`/`30_` prefixes enforce order -- Go format-on-save in nvim runs `organizeImports` code action BEFORE format (see `41_lsp_format.lua`) -- `vim.pack.add` is neovim 0.12+ built-in — do not confuse with lazy.nvim +# dotfiles + +Stow-managed personal dotfiles. Forked from `alx99/dotfiles` and heavily modified — README and `.gitconfig` still reference ALX99 (deliberate, do not "fix" without asking). + +## Shape + +- **Stow packages:** `home/` (→ `~`), `.config/` (→ `~/.config`), `.local/` (→ `~/.local`). `.stowrc` sets `--no-folding`. +- **One Makefile target** (`make user-cfg` → `./setupDots 1`). Full menu in `setupDots`: `1` user config, `2` Linux system (Arch), `3` Mac system. +- **Submodule:** `.config/mpv/scripts/subs2srs` → `Ajatt-Tools/mpvacious` (path name is misleading; it is mpvacious, not subs2srs). +- **Scratch dir:** `tmp/` is **untracked** (`.gitignore` and `git status` confirm). Do not put real work there. +- **170 tracked files.** No test framework, no build system. The only "build" is `make user-cfg`. The only "lint" is shellcheck via CI. + +## Symlinks in the repo (not stow-created) + +- `CLAUDE.md` (repo root) → `AGENTS.md` — both names resolve to the same file. +- `home/.bash_profile` → `.profile` — login shells source the POSIX profile. +- `home/.codex/AGENTS.md` → `../.claude/CLAUDE.md` — Codex reads Claude's instruction file. + +Runtime: `~/.pi/agent/extensions/.ts` (and `agents/*.md`, `supervisor.md`) are symlinks back into the dotfiles. **Pi's extension loader only auto-discovers from `/extensions/`** — placing an extension at `~/.pi/agent//` (one level up) makes it invisible. Same for `agents/`, `prompts/`, `skills/`. See `home/.pi/agent/extensions/subagents/` and the deleted-then-moved history in `git status`. + +## Setup invariants + +- `setupDots` shares `~/.agents/skills/` with `~/.claude/skills/` via per-skill symlinks. It uses `is_stale_dotfile_entry` to avoid clobbering content from `npx agents` or other tools — never `rm -rf` a `~/.claude/skills/` that the script flagged as "not a stale dotfile leftover." +- Linux: `setupDots 2` requires sudo for XKB/keyd/systemd units, enables `systemctl --user` ssh-agent, links `dash` to `/usr/bin/sh`. +- Mac: `setupDots 3` installs `~/Library/Keyboard Layouts/Colemak-DH-ANSI.keylayout` and prompts the user to enable it manually in System Settings. No auto-reboot. +- `home/.privrc` is **tracked** and is sourced from `~/.bashrc`. It holds private env vars (e.g., `TOKENROUTER_API_KEY`). Treat as secret. + +## Two AI ecosystems in parallel + +| Tool | Source in repo | Notes | +|------|----------------|-------| +| **pi** | `home/.pi/agent/` | Primary. `settings.json` (provider tokenrouter, model `MiniMax-M3`, ponytail full, packages: ponytail, superpowers, pi-fff). `APPEND_SYSTEM.md` injects persona. `supervisor.md` is the goal-driven subagent protocol. | +| **Claude Code** | `home/.claude/` + `home/.agents/skills/` | `settings.json` has hooks (gofmt on .go, macOS notification on Stop), permissions allowlist, plugins (`superpowers@superpowers-marketplace`, `lsp@alx99-personal`). `status` script is the statusline. | +| **Codex** | `home/.codex/` | Just the symlink to Claude's CLAUDE.md. No other config. | + +**Skill rules live in two places** by design: +- `home/.pi/agent/skills/` — pi-only skills. +- `home/.agents/skills/` — canonical, mirrored to `~/.claude/skills/` for Claude Code by `setupDots`. + +The repo is mid-migration to **harness-agnostic skills** under `home/.agents/skills/` (see commits `dda9450`, `252c8ff`, `00d3f68`). Don't write new pi-specific skills — write them under `home/.agents/skills/` and let the mirror propagate. + +## Active git state (worktree is dirty) + +Staged deletions, untracked additions — the worktree is mid-restructure. Don't run `git checkout` or `git stash` blindly: +- Deleted from `home/.pi/agent/agents/`: `reviewer.md`, `scout.md`, `worker.md`. Replaced (untracked) by `home/.pi/agent/extensions/subagents/agents/{default,reviewer,scout,worker}.md`. +- Deleted from `home/.pi/agent/extensions/`: `ask-user-question.ts`, `brainstorm.ts`, `btw.ts`, `subagent/`, `tokenrouter/`. Replaced (untracked) by `ask_question.ts`, `subagents/`, plus new `goals/{storage,validation,templates,templates}.ts`. +- Modified: `home/.pi/agent/extensions/{footer,goals/index}.ts`, `home/.pi/agent/settings.json`, `home/.profile`, `home/.pi/agent/skills/init/SKILL.md`, `.config/git/ignore`, `misc/keymaps/Colemak-DH-ANSI.keylayout`. +- Untracked: `Dockerfile.cbm` (builds `DeusData/codebase-memory-mcp`), `reboot.py` (Buffalo router reboot — hardcoded creds, **don't commit**), `tmp/soulforge/` (3rd-party scratch), `symbol-layer-proposal.md`. + +## Colemak-DH and theme colors + +Colemak-DH keymaps are wired into every layer: nvim (`.config/nvim/lua/colemak.lua`), tmux (`.config/tmux/tmux.conf` and copy-mode), sail (`.config/sail/config.yaml`), keyd (`misc/keymaps/keyd.conf`), karabiner, ghostty passthroughs. The four swapped keys: `h↔m`, `j↔n`, `k↔e`, `l↔i`. + +The xterm-256 color palette is the source of truth in **three** files and must stay in sync: `home/.bashrc` (PS1), `home/.profile` (LS_COLORS via the inline `__ls_colors` function), and `.config/ghostty/config` (palette + status colors). Theme constants: `245 203 114 179 75 141 109` (slate red green yellow blue purple cyan). + +## CI + +Two GitHub Actions, both on push and PR: +- `automerge.yml` — auto-merges Dependabot PRs for github-actions and gitsubmodules when update-type is `semver-patch`. Has `contents: write` and `pull-requests: write`. +- `linter.yml` — `reviewdog/action-shellcheck` with `check_all_files_with_shebangs: true`. Scans every tracked script with a shebang. + +No tests run in CI. No formatter runs in CI. + +## Local hooks (Claude Code) + +`home/.claude/settings.json` PostToolUse hook runs `gofmt -w` on edited `.go` files (skips `*gen.go`). Stop hook fires a macOS notification naming the cwd. PreToolUse/PermissionRequest hooks run `~/.local/bin/claude-permission-dialog`. + +## Verification gates (when changing things) + +No automated tests. Manual gates that catch real breakage: +- `make user-cfg` — full stow restow. If this fails, the change is broken at the symlink level. +- `shellcheck $(git ls-files | xargs file | grep -i 'shell script' | cut -d: -f1)` — CI runs this; mirror it locally before pushing. +- `bash -n home/.bashrc home/.profile` — syntax check the shell init. +- `command -v stow` — `setupDots` hard-requires GNU stow (BSD `find` is patched for macOS compat, but stow itself is the package manager). From d022e4340c10d59d0a950901cdacd01c5bb6f9c1 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:30:22 +0900 Subject: [PATCH 117/366] refactor(nvim): open blame commit diff in delta tab Replace CodeDiff with a delta-rendered terminal tab for blame.nvim's commit detail view. A git cat-file precheck falls back to the full commit when the file was renamed at that revision; q closes the tab in both terminal-job and terminal-normal modes. --- .config/nvim/plugin/73_git.lua | 40 +++++++++++++++------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/.config/nvim/plugin/73_git.lua b/.config/nvim/plugin/73_git.lua index 3221dc8f..d8a9d54c 100644 --- a/.config/nvim/plugin/73_git.lua +++ b/.config/nvim/plugin/73_git.lua @@ -2,7 +2,6 @@ if vim.g.vscode then return end vim.pack.add({ 'https://github.com/FabijanZulj/blame.nvim', - 'https://github.com/esmuellert/codediff.nvim', }) local map = require('utils').map @@ -32,28 +31,25 @@ require('blame').setup({ return end - local parent = commit_hash .. "^" - -- Only allocate a new buffer if one doesn't already exist for the - -- file; bufadd() leaks a hidden buffer into the list every time - -- the user opens a blame detail on a different file. If the buffer - -- is already known, reuse it. - local buf = vim.fn.bufnr(file_path) - if buf == -1 then - buf = vim.fn.bufadd(file_path) - vim.fn.bufload(buf) - end - - local ok, err = pcall(vim.api.nvim_buf_call, buf, function() - vim.cmd(("CodeDiff file %s %s"):format(parent, commit_hash)) - end) - - if not ok then - vim.notify("CodeDiff failed: " .. tostring(err), vim.log.levels.ERROR) - end + -- ponytail: scope by path when the file existed at this commit; + -- fall back to the full commit when it didn't (rename source or + -- pre-creation), so delta shows the rename via git -M instead of + -- a blank buffer. Assumes nvim's cwd is the repo root; the + -- fallback still works if not. + local rel = vim.fn.fnamemodify(file_path, ":.") + local scoped = vim.system({ "git", "cat-file", "-e", commit_hash .. ":" .. rel }, { text = true }):wait().code == 0 + local cmd = scoped + and string.format("git show %s -- %s | delta --paging never", commit_hash, vim.fn.shellescape(rel)) + or string.format("git show %s | delta --paging never", commit_hash) + + -- tabnew gives a fresh unmodified buffer for term=true; q closes + -- the tab in both t mode (during the pipeline) and n mode (after + -- delta exits and the buffer becomes terminal-normal) + vim.cmd('tabnew') + vim.fn.jobstart(cmd, { term = true }) + vim.keymap.set('t', 'q', ':tabclose', { buffer = 0 }) + vim.keymap.set('n', 'q', ':tabclose', { buffer = 0 }) end, }) map('n', 'Gb', ':BlameToggle', { desc = "Toggle Git blame" }) - --- codediff.nvim -map('n', 'Gs', 'CodeDiff', { desc = "Show git status" }) From ba2f673bff6f0d3cacbf187d6de789861937a98d Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:55:54 +0900 Subject: [PATCH 118/366] chore(nvim): drop jsonls and yamlls lsp support Remove jsonls and yamlls from the enabled LSP list and drop schemastore.nvim, which only existed to provide their schemas. Also removes unused codediff.nvim and bumps kanagawa, markview, and mason to current revisions. --- .config/nvim/nvim-pack-lock.json | 14 +++----------- .config/nvim/plugin/78_lsp.lua | 6 ++---- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/.config/nvim/nvim-pack-lock.json b/.config/nvim/nvim-pack-lock.json index 525be9cf..13271c2a 100644 --- a/.config/nvim/nvim-pack-lock.json +++ b/.config/nvim/nvim-pack-lock.json @@ -9,25 +9,21 @@ "src": "https://github.com/saghen/blink.cmp", "version": "1.0.0 - 2.0.0" }, - "codediff.nvim": { - "rev": "5d6aa753797a0ebda14dd769ed03d12a320d689b", - "src": "https://github.com/esmuellert/codediff.nvim" - }, "flash.nvim": { "rev": "fcea7ff883235d9024dc41e638f164a450c14ca2", "src": "https://github.com/folke/flash.nvim" }, "kanagawa.nvim": { - "rev": "8ad3b4cdcc804b332c32db8f9743667e1bb82b99", + "rev": "bb85e4bfc8d89b0e62c8fa53ccdd13d12e2f77b3", "src": "https://github.com/rebelot/kanagawa.nvim" }, "markview.nvim": { - "rev": "692887ec334c41b618ba732047ac9dac3edfaead", + "rev": "3537eb2a9251cad5d7718253768f3772c62233fc", "src": "https://github.com/OXY2DEV/markview.nvim", "version": ">=0.0.0" }, "mason.nvim": { - "rev": "44d1e90e1f66e077268191e3ee9d2ac97cc18e65", + "rev": "2a6940af80375532e5e9e7c1f2fc6319a1b7a69d", "src": "https://github.com/mason-org/mason.nvim", "version": ">=0.0.0" }, @@ -49,10 +45,6 @@ "rev": "b311b30818951d01f7b4bf650521b868b3fece16", "src": "https://github.com/nvim-treesitter/nvim-treesitter-context" }, - "schemastore.nvim": { - "rev": "20d4e9970798123e6197acb1c85c4b1e897efd29", - "src": "https://github.com/b0o/schemastore.nvim" - }, "sidekick.nvim": { "rev": "53a2d3afa61e5fd2e17b270b5fa72e5493808304", "src": "https://github.com/folke/sidekick.nvim", diff --git a/.config/nvim/plugin/78_lsp.lua b/.config/nvim/plugin/78_lsp.lua index 27e8f04c..c6cbe117 100644 --- a/.config/nvim/plugin/78_lsp.lua +++ b/.config/nvim/plugin/78_lsp.lua @@ -4,7 +4,6 @@ if vim.g.vscode then return end vim.pack.add({ { src = 'https://github.com/neovim/nvim-lspconfig', version = vim.version.range('*') }, - 'https://github.com/b0o/schemastore.nvim', -- used by jsonls/yamlls lsp configs }) local capabilities = vim.lsp.protocol.make_client_capabilities() @@ -17,13 +16,13 @@ vim.lsp.handlers["window/showMessage"] = function(_, result, ctx) local client = ctx and ctx.client_id and vim.lsp.get_client_by_id(ctx.client_id) local prefix = client and ("[LSP] [" .. client.name .. "]") or "[LSP]" -- LSP MessageType: 1=Error, 2=Warning, 3=Info, 4=Log, 5=Debug (LSP 3.18+) - local level = ({ [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG, [5] = vim.log.levels.DEBUG })[result.type] or vim.log.levels.INFO + local level = ({ [1] = vim.log.levels.ERROR, [2] = vim.log.levels.WARN, [3] = vim.log.levels.INFO, [4] = vim.log.levels.DEBUG, [5] = vim.log.levels.DEBUG }) + [result.type] or vim.log.levels.INFO vim.notify(prefix .. " " .. result.message, level) end local enabled_lsps = { "pyright", - "jsonls", "html", "cssls", "tailwindcss", @@ -31,7 +30,6 @@ local enabled_lsps = { "terraformls", "eslint", "gopls", - "yamlls", "lua_ls", "copilot", "gh_actions_ls", From b8c8bdd50869f94b86448a6712f28ebb618aed23 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:08:31 +0900 Subject: [PATCH 119/366] chore(nvim): drop sidekick.nvim --- .config/nvim/nvim-pack-lock.json | 5 ----- .config/nvim/plugin/74_sidekick.lua | 20 -------------------- .config/nvim/plugin/77_blink_cmp.lua | 5 ----- 3 files changed, 30 deletions(-) delete mode 100644 .config/nvim/plugin/74_sidekick.lua diff --git a/.config/nvim/nvim-pack-lock.json b/.config/nvim/nvim-pack-lock.json index 13271c2a..430e1b87 100644 --- a/.config/nvim/nvim-pack-lock.json +++ b/.config/nvim/nvim-pack-lock.json @@ -45,11 +45,6 @@ "rev": "b311b30818951d01f7b4bf650521b868b3fece16", "src": "https://github.com/nvim-treesitter/nvim-treesitter-context" }, - "sidekick.nvim": { - "rev": "53a2d3afa61e5fd2e17b270b5fa72e5493808304", - "src": "https://github.com/folke/sidekick.nvim", - "version": ">=0.0.0" - }, "snacks.nvim": { "rev": "e6fd58c82f2f3fcddd3fe81703d47d6d48fc7b9f", "src": "https://github.com/folke/snacks.nvim", diff --git a/.config/nvim/plugin/74_sidekick.lua b/.config/nvim/plugin/74_sidekick.lua deleted file mode 100644 index 39d4cc50..00000000 --- a/.config/nvim/plugin/74_sidekick.lua +++ /dev/null @@ -1,20 +0,0 @@ --- sidekick.nvim (NES only) -if vim.g.vscode then return end - -vim.pack.add({ - { src = 'https://github.com/folke/sidekick.nvim', version = vim.version.range('*') }, -}) - -local map = require('utils').map - -require('sidekick').setup({}) - -map("n", "", function() - if require('sidekick').nes_jump_or_apply() then - return "" - end - return "" -end, { expr = true, desc = "Goto/Apply Next Edit Suggestion" }) - --- Custom AI helpers (file send) -require("custom.ai").setup() diff --git a/.config/nvim/plugin/77_blink_cmp.lua b/.config/nvim/plugin/77_blink_cmp.lua index 21882f4a..e98a4216 100644 --- a/.config/nvim/plugin/77_blink_cmp.lua +++ b/.config/nvim/plugin/77_blink_cmp.lua @@ -1,8 +1,6 @@ -- blink.cmp (autocompletion) --- Depends on: sidekick.lua (Tab keymap calls sidekick.nes_jump_or_apply) if vim.g.vscode then return end --- Sidekick must be registered before blink.cmp (Tab keymap references it) vim.pack.add({ { src = "https://github.com/saghen/blink.cmp", version = vim.version.range("1.*") }, }) @@ -12,9 +10,6 @@ require("blink.cmp").setup({ preset = "default", [""] = { "select_and_accept", "fallback" }, [""] = { - function() - return require("sidekick").nes_jump_or_apply() - end, function() return vim.lsp.inline_completion.get() end, From d993b99d2b3b015a6fb4615a581854ac6726120b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:57:39 +0900 Subject: [PATCH 120/366] refactor: rename setupDots to init.sh and update all references The setup script has been renamed from setupDots to init.sh (standard convention for repo init scripts). Updated Makefile target and all documentation references in AGENTS.md accordingly. The script's self-referencing usage message adapts automatically via $0. --- AGENTS.md | 12 ++++++------ Makefile | 22 ++++++++++++++++++++-- setupDots => init.sh | 0 3 files changed, 26 insertions(+), 8 deletions(-) rename setupDots => init.sh (100%) diff --git a/AGENTS.md b/AGENTS.md index 39632869..da88ae02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Stow-managed personal dotfiles. Forked from `alx99/dotfiles` and heavily modifie ## Shape - **Stow packages:** `home/` (→ `~`), `.config/` (→ `~/.config`), `.local/` (→ `~/.local`). `.stowrc` sets `--no-folding`. -- **One Makefile target** (`make user-cfg` → `./setupDots 1`). Full menu in `setupDots`: `1` user config, `2` Linux system (Arch), `3` Mac system. +- **One Makefile target** (`make user-cfg` → `./init.sh 1`). Full menu in `init.sh`: `1` user config, `2` Linux system (Arch), `3` Mac system. - **Submodule:** `.config/mpv/scripts/subs2srs` → `Ajatt-Tools/mpvacious` (path name is misleading; it is mpvacious, not subs2srs). - **Scratch dir:** `tmp/` is **untracked** (`.gitignore` and `git status` confirm). Do not put real work there. - **170 tracked files.** No test framework, no build system. The only "build" is `make user-cfg`. The only "lint" is shellcheck via CI. @@ -20,9 +20,9 @@ Runtime: `~/.pi/agent/extensions/.ts` (and `agents/*.md`, `supervisor.md`) ## Setup invariants -- `setupDots` shares `~/.agents/skills/` with `~/.claude/skills/` via per-skill symlinks. It uses `is_stale_dotfile_entry` to avoid clobbering content from `npx agents` or other tools — never `rm -rf` a `~/.claude/skills/` that the script flagged as "not a stale dotfile leftover." -- Linux: `setupDots 2` requires sudo for XKB/keyd/systemd units, enables `systemctl --user` ssh-agent, links `dash` to `/usr/bin/sh`. -- Mac: `setupDots 3` installs `~/Library/Keyboard Layouts/Colemak-DH-ANSI.keylayout` and prompts the user to enable it manually in System Settings. No auto-reboot. +- `init.sh` shares `~/.agents/skills/` with `~/.claude/skills/` via per-skill symlinks. It uses `is_stale_dotfile_entry` to avoid clobbering content from `npx agents` or other tools — never `rm -rf` a `~/.claude/skills/` that the script flagged as "not a stale dotfile leftover." +- Linux: `init.sh 2` requires sudo for XKB/keyd/systemd units, enables `systemctl --user` ssh-agent, links `dash` to `/usr/bin/sh`. +- Mac: `init.sh 3` installs `~/Library/Keyboard Layouts/Colemak-DH-ANSI.keylayout` and prompts the user to enable it manually in System Settings. No auto-reboot. - `home/.privrc` is **tracked** and is sourced from `~/.bashrc`. It holds private env vars (e.g., `TOKENROUTER_API_KEY`). Treat as secret. ## Two AI ecosystems in parallel @@ -35,7 +35,7 @@ Runtime: `~/.pi/agent/extensions/.ts` (and `agents/*.md`, `supervisor.md`) **Skill rules live in two places** by design: - `home/.pi/agent/skills/` — pi-only skills. -- `home/.agents/skills/` — canonical, mirrored to `~/.claude/skills/` for Claude Code by `setupDots`. +- `home/.agents/skills/` — canonical, mirrored to `~/.claude/skills/` for Claude Code by `init.sh`. The repo is mid-migration to **harness-agnostic skills** under `home/.agents/skills/` (see commits `dda9450`, `252c8ff`, `00d3f68`). Don't write new pi-specific skills — write them under `home/.agents/skills/` and let the mirror propagate. @@ -71,4 +71,4 @@ No automated tests. Manual gates that catch real breakage: - `make user-cfg` — full stow restow. If this fails, the change is broken at the symlink level. - `shellcheck $(git ls-files | xargs file | grep -i 'shell script' | cut -d: -f1)` — CI runs this; mirror it locally before pushing. - `bash -n home/.bashrc home/.profile` — syntax check the shell init. -- `command -v stow` — `setupDots` hard-requires GNU stow (BSD `find` is patched for macOS compat, but stow itself is the package manager). +- `command -v stow` — `init.sh` hard-requires GNU stow (BSD `find` is patched for macOS compat, but stow itself is the package manager). diff --git a/Makefile b/Makefile index 8499d2e2..3d67dc74 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,22 @@ -.PHONY: user-cfg +.PHONY: user-cfg check lint typecheck test user-cfg: - ./setupDots 1 + ./init.sh 1 + +# Run typecheck, lint, and tests in order. Stops on first failure. +# Dev tooling lives in home/.pi/agent/extensions.dev/ (sibling of +# extensions/, with no index.ts/index.js so pi's loader skips it). +check: + cd home/.pi/agent/extensions.dev && npm run check + +# Run only the supervisor extension's linter (eslint). +lint: + cd home/.pi/agent/extensions.dev && npm run lint + +# Run only the supervisor extension's typecheck (tsc). +typecheck: + cd home/.pi/agent/extensions.dev && npm run typecheck + +# Run only the supervisor extension's tests (node --test). +test: + cd home/.pi/agent/extensions.dev && npm run test diff --git a/setupDots b/init.sh similarity index 100% rename from setupDots rename to init.sh From 8c5c326f4165a7a4db02114ffa681b16cc2c2bc4 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:00:31 +0900 Subject: [PATCH 121/366] feat(nvim): add fff.nvim plugin for file search and grep Replace the snacks picker for file/grep operations with fff.nvim, which bundles a prebuilt binary on install. Maps `fo`/`fs` to file find and `/`/`*` to live grep. --- .config/nvim/nvim-pack-lock.json | 5 +++++ .config/nvim/plugin/74_fff.lua | 36 +++++++++++++++++++++++++++++++ .config/nvim/plugin/75_snacks.lua | 20 ++--------------- 3 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 .config/nvim/plugin/74_fff.lua diff --git a/.config/nvim/nvim-pack-lock.json b/.config/nvim/nvim-pack-lock.json index 430e1b87..da21e9b3 100644 --- a/.config/nvim/nvim-pack-lock.json +++ b/.config/nvim/nvim-pack-lock.json @@ -9,6 +9,11 @@ "src": "https://github.com/saghen/blink.cmp", "version": "1.0.0 - 2.0.0" }, + "fff.nvim": { + "rev": "797c045aa93e03e3cec3497a76afe7cb0106bdc2", + "src": "https://github.com/dmtrKovalenko/fff.nvim", + "version": ">=0.0.0" + }, "flash.nvim": { "rev": "fcea7ff883235d9024dc41e638f164a450c14ca2", "src": "https://github.com/folke/flash.nvim" diff --git a/.config/nvim/plugin/74_fff.lua b/.config/nvim/plugin/74_fff.lua new file mode 100644 index 00000000..7db1fe87 --- /dev/null +++ b/.config/nvim/plugin/74_fff.lua @@ -0,0 +1,36 @@ +-- fff.nvim: fast file search and grep (replaces snacks picker for files/grep) +if vim.g.vscode then return end + +vim.pack.add({ + { + src = 'https://github.com/dmtrKovalenko/fff.nvim', + version = vim.version.range('*'), + }, +}) + +vim.api.nvim_create_autocmd('PackChanged', { + callback = function(ev) + local name, kind = ev.data.spec.name, ev.data.kind + if name == 'fff.nvim' and (kind == 'install' or kind == 'update') then + if not ev.data.active then vim.cmd.packadd('fff.nvim') end + require('fff.download').download_or_build_binary() + end + end, +}) + +require('fff').setup({ + prompt = '❯ ', + keymaps = { + -- Insert mode up: from snacks, + from fff defaults + move_up = { '', '', '' }, + }, +}) + +local map = require('utils').map +local fff = require('fff') + +map('n', 'fo', function() fff.find_files() end, { desc = "Find Files" }) +map('n', 'fs', function() fff.find_files() end, { desc = "Find Files (smart)" }) +map('n', '/', function() fff.live_grep() end, { desc = "Grep" }) +map('n', '*', function() fff.live_grep({ query = vim.fn.expand('') }) end, + { desc = "Grep Word" }) diff --git a/.config/nvim/plugin/75_snacks.lua b/.config/nvim/plugin/75_snacks.lua index 48c0796f..1ceeec40 100644 --- a/.config/nvim/plugin/75_snacks.lua +++ b/.config/nvim/plugin/75_snacks.lua @@ -12,22 +12,12 @@ require('snacks').setup({ input = {}, picker = { sources = { - -- keep smart/recent results scoped to the current root smart = { filter = { cwd = true } }, recent = { filter = { cwd = true } }, - - files = { exclude = { "**/vendor/**" }, }, - grep = { exclude = { "**/vendor/**" }, }, - }, - formatters = { - file = { - -- filename_first = true, -- display filename before the file path - }, }, win = { input = { keys = { - -- Colemak: n=down, e=up (disable j/k defaults) ["j"] = false, ["k"] = false, [""] = { "list_down", mode = { "i", "n" } }, @@ -38,7 +28,6 @@ require('snacks').setup({ }, list = { keys = { - -- Colemak: n=down, e=up (disable j/k defaults) ["j"] = false, ["k"] = false, ["n"] = "list_down", @@ -50,7 +39,6 @@ require('snacks').setup({ }, }, }, - -- scroll = {}, statuscolumn = { enabled = false, }, @@ -63,10 +51,8 @@ require('snacks').setup({ local map = require('utils').map local Snacks = require('snacks') -map('n', 'fo', function() Snacks.picker.files({ hidden = true }) end, { desc = "Find files" }) -map('n', 'fO', function() Snacks.picker.files({ hidden = true, ignored = true }) end, - { desc = "Find Hidden and Ignored Files" }) -map('n', 'fs', function() Snacks.picker.smart({}) end, { desc = "Smart Picker" }) +-- file finding and grep moved to 74_fff.lua + map('n', 'ob', function() Snacks.picker.buffers({}) end, { desc = "Buffers" }) map('n', 'oC', function() Snacks.picker.colorschemes({}) end, { desc = "Colorschemes" }) map('n', 'oc', function() Snacks.picker.commands({}) end, { desc = "Commands" }) @@ -74,8 +60,6 @@ map('n', 'od', function() Snacks.picker.diagnostics({}) end, { desc = "D map('n', 'oD', function() Snacks.picker.diagnostics_buffer({}) end, { desc = "Buffer Diagnostics" }) map('n', 'ol', function() Snacks.picker.git_log({}) end, { desc = "Git Log" }) map('n', 'oL', function() Snacks.picker.git_log_file({}) end, { desc = "Git Log for Current File" }) -map('n', '/', function() Snacks.picker.grep({ hidden = true }) end, { desc = "Grep" }) -map('n', '*', function() Snacks.picker.grep_word({ hidden = true }) end, { desc = "Grep Word" }) map('n', 'oh', function() Snacks.picker.help({}) end, { desc = "Help Pages" }) map('n', '/', function() Snacks.picker.lines({}) end, { desc = "Buffer Lines" }) map('n', 'oq', function() Snacks.picker.qflist({}) end, { desc = "Quickfix List" }) From a98a93c139b5bacd2c751c82e52884eb96b121d3 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:13:36 +0900 Subject: [PATCH 122/366] feat(nvim): auto-close buffers when mini.files deletes a file Prompts with a confirmation dialog if the buffer has unsaved changes, otherwise closes silently. --- .config/nvim/plugin/76_mini.lua | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/.config/nvim/plugin/76_mini.lua b/.config/nvim/plugin/76_mini.lua index 5ce8c05f..e7fa5e7b 100644 --- a/.config/nvim/plugin/76_mini.lua +++ b/.config/nvim/plugin/76_mini.lua @@ -124,6 +124,70 @@ _G.Config.new_autocmd("User", { end, }) +-- Before deleting a buffer, switch any window showing it to another buffer first. +-- This avoids errors when the buffer is displayed and there's no alt buffer. +local function switch_windows_off_buffer(buf) + for _, win in ipairs(vim.api.nvim_list_wins()) do + if vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_buf(win) == buf then + -- Find the first other loaded buffer + local alt = nil + for _, other in ipairs(vim.api.nvim_list_bufs()) do + if other ~= buf and vim.api.nvim_buf_is_loaded(other) then + alt = other + break + end + end + -- No other loaded buffer — create a fresh one + if alt == nil then + alt = vim.api.nvim_create_buf(false, true) + end + vim.api.nvim_win_set_buf(win, alt) + end + end +end + +_G.Config.new_autocmd("User", { + pattern = "MiniFilesActionDelete", + callback = function(event) + local path = event.data.from + if path == nil then return end + + -- Normalize path (strip trailing slash) for comparison + path = path:gsub('/+$', '') + + -- Find any buffer referencing the deleted path (file or directory) + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) then + local name = vim.api.nvim_buf_get_name(buf) + if name ~= '' then + -- Normalize buffer path for comparison + local normalized = name:gsub('/+$', '') + -- Match: exact file, or file/dir inside deleted directory + local matches = (normalized == path) + or normalized:find('^' .. vim.pesc(path .. '/'), 1) + + if matches then + if vim.bo[buf].modified then + local short = vim.fn.fnamemodify(name, ':~:.') + local choice = vim.fn.confirm( + "'" .. short .. "' has unsaved changes. Delete anyway?", + "&Yes\n&No", 2 + ) + if choice == 1 then + switch_windows_off_buffer(buf) + vim.api.nvim_buf_delete(buf, { force = true }) + end + else + switch_windows_off_buffer(buf) + vim.api.nvim_buf_delete(buf, { force = true }) + end + end + end + end + end + end, +}) + require('utils').map('n', 'ft', function() MiniFiles.open(vim.api.nvim_buf_get_name(0)) end, { desc = "MiniFiles" }) From 272085c1329a6b8d20477744c216be35959fd9bd Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 11:35:03 +0900 Subject: [PATCH 123/366] pi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: merge pi-web branch (browser UI + missions extension) Squashes 50 commits from the pi-web branch onto master: - missions extension (replaces goals/): evidence-gated /mission autonomous loop. Phase 1 ships the state machine + controller-enforced verification with a claim/attempt lease; phase 2 (agent_end auto-re-entry) is specced but not in this commit. 17 source files + 14 test files in home/.pi/agent/extensions/missions/. SQLite at $PI_AGENT_DIR/missions.db. - pi-web: HTTP + WebSocket bridge from a browser-based chat UI to the pi SDK. Vendor routes, cross-platform open-browser helper, Preact frontend (chat, sidebar, pickers, tool-call view, htm bind). 9 source files + 5 test files in pi-web/. - removes home/.pi/agent/extensions/goals/ (~935 lines) per c206d60 refactor(missions). - AGENTS.md updated to reflect missions replacing goals and to correct the pi extension loader rule (subdirectory index.ts pattern). - docs/superpowers/: 4 implementation plans and 3 design specs covering both missions phases and the pi-web UI. 68 files changed, +19352/-937. Stash @{0} holds the master's pre-merge uncommitted state (4 tracked edits in subagents/, settings.json, keylayout; 9 untracked files including probe*.lua and FINDINGS.md). It is unrelated to this merge and was set aside so the working tree was clean for the squash. fix(ask_question): only register tool when UI is available Previously the tool was registered unconditionally and returned 'Error: UI not available' when invoked in agent / non-interactive mode. This surfaced as a confusing error to agents that had no way to know the tool couldn't actually be used. Move the registerTool call inside a session_start handler and gate it on ctx.hasUI. The tool is now absent from the agent's tool list in non-interactive runs, so it can't be called and can't error. Verified: 'make check' (tsc + eslint + node:test) passes — 129 tests. --- .config/git/ignore | 2 + AGENTS.md | 56 +- Makefile | 28 +- home/.pi/agent/APPEND_SYSTEM.md | 145 +- home/.pi/agent/extensions/.gitignore | 1 + .../.pi/agent/extensions/ask-user-question.ts | 123 - home/.pi/agent/extensions/ask_question.ts | 130 + home/.pi/agent/extensions/brainstorm.ts | 95 - home/.pi/agent/extensions/btw.ts | 32 - home/.pi/agent/extensions/cost-tracker.ts | 7 +- home/.pi/agent/extensions/eslint.config.mjs | 25 + home/.pi/agent/extensions/footer.ts | 73 +- home/.pi/agent/extensions/goals/index.ts | 213 - home/.pi/agent/extensions/memory/index.ts | 5 +- home/.pi/agent/extensions/package-lock.json | 4679 +++++++++++++++++ home/.pi/agent/extensions/package.json | 24 + home/.pi/agent/extensions/subagent/agents.ts | 71 - home/.pi/agent/extensions/subagent/index.ts | 320 -- home/.pi/agent/extensions/subagents/agents.ts | 124 + .../extensions/subagents/agents/default.md | 6 + .../subagents}/agents/reviewer.md | 2 - .../subagents}/agents/scout.md | 6 +- .../subagents}/agents/worker.md | 4 +- home/.pi/agent/extensions/subagents/index.ts | 170 + .../.pi/agent/extensions/subagents/process.ts | 322 ++ home/.pi/agent/extensions/subagents/render.ts | 162 + .../.pi/agent/extensions/tokenrouter/index.ts | 35 - home/.pi/agent/extensions/tsconfig.json | 20 + home/.pi/agent/prompts/review-diff.md | 11 - home/.pi/agent/prompts/scout.md | 9 - home/.pi/agent/settings.json | 17 +- home/.pi/agent/skills/init/SKILL.md | 92 +- home/.pi/agent/supervisor.md | 160 - home/.profile | 1 + home/.stow-local-ignore | 2 + init.sh | 5 + pi-web/.gitignore | 4 + pi-web/README.md | 39 + pi-web/bin/pi-web | 21 + pi-web/docs/smoke-test.md | 59 + pi-web/package-lock.json | 3745 +++++++++++++ pi-web/package.json | 28 + pi-web/src/bridge.ts | 291 + pi-web/src/open-browser.ts | 52 + pi-web/src/protocol.ts | 153 + pi-web/src/runtime.ts | 121 + pi-web/src/server.ts | 257 + pi-web/src/vendor.ts | 47 + pi-web/src/web/app.js | 276 + pi-web/src/web/components/chat.js | 78 + pi-web/src/web/components/htm.js | 9 + pi-web/src/web/components/model-picker.js | 25 + pi-web/src/web/components/session-list.js | 21 + pi-web/src/web/components/thinking-picker.js | 18 + pi-web/src/web/components/tool-call.js | 13 + pi-web/src/web/index.html | 13 + pi-web/src/web/style.css | 207 + pi-web/tests/bridge.test.ts | 409 ++ pi-web/tests/open-browser.test.ts | 66 + pi-web/tests/protocol.test.ts | 126 + pi-web/tests/runtime.test.ts | 37 + pi-web/tests/server.test.ts | 193 + pi-web/tsconfig.json | 18 + 63 files changed, 12243 insertions(+), 1260 deletions(-) create mode 100644 home/.pi/agent/extensions/.gitignore delete mode 100644 home/.pi/agent/extensions/ask-user-question.ts create mode 100644 home/.pi/agent/extensions/ask_question.ts delete mode 100644 home/.pi/agent/extensions/brainstorm.ts delete mode 100644 home/.pi/agent/extensions/btw.ts create mode 100644 home/.pi/agent/extensions/eslint.config.mjs delete mode 100644 home/.pi/agent/extensions/goals/index.ts create mode 100644 home/.pi/agent/extensions/package-lock.json create mode 100644 home/.pi/agent/extensions/package.json delete mode 100644 home/.pi/agent/extensions/subagent/agents.ts delete mode 100644 home/.pi/agent/extensions/subagent/index.ts create mode 100644 home/.pi/agent/extensions/subagents/agents.ts create mode 100644 home/.pi/agent/extensions/subagents/agents/default.md rename home/.pi/agent/{ => extensions/subagents}/agents/reviewer.md (97%) rename home/.pi/agent/{ => extensions/subagents}/agents/scout.md (87%) rename home/.pi/agent/{ => extensions/subagents}/agents/worker.md (92%) create mode 100644 home/.pi/agent/extensions/subagents/index.ts create mode 100644 home/.pi/agent/extensions/subagents/process.ts create mode 100644 home/.pi/agent/extensions/subagents/render.ts delete mode 100644 home/.pi/agent/extensions/tokenrouter/index.ts create mode 100644 home/.pi/agent/extensions/tsconfig.json delete mode 100644 home/.pi/agent/prompts/review-diff.md delete mode 100644 home/.pi/agent/prompts/scout.md delete mode 100644 home/.pi/agent/supervisor.md create mode 100644 home/.stow-local-ignore create mode 100644 pi-web/.gitignore create mode 100644 pi-web/README.md create mode 100755 pi-web/bin/pi-web create mode 100644 pi-web/docs/smoke-test.md create mode 100644 pi-web/package-lock.json create mode 100644 pi-web/package.json create mode 100644 pi-web/src/bridge.ts create mode 100644 pi-web/src/open-browser.ts create mode 100644 pi-web/src/protocol.ts create mode 100644 pi-web/src/runtime.ts create mode 100644 pi-web/src/server.ts create mode 100644 pi-web/src/vendor.ts create mode 100644 pi-web/src/web/app.js create mode 100644 pi-web/src/web/components/chat.js create mode 100644 pi-web/src/web/components/htm.js create mode 100644 pi-web/src/web/components/model-picker.js create mode 100644 pi-web/src/web/components/session-list.js create mode 100644 pi-web/src/web/components/thinking-picker.js create mode 100644 pi-web/src/web/components/tool-call.js create mode 100644 pi-web/src/web/index.html create mode 100644 pi-web/src/web/style.css create mode 100644 pi-web/tests/bridge.test.ts create mode 100644 pi-web/tests/open-browser.test.ts create mode 100644 pi-web/tests/protocol.test.ts create mode 100644 pi-web/tests/runtime.test.ts create mode 100644 pi-web/tests/server.test.ts create mode 100644 pi-web/tsconfig.json diff --git a/.config/git/ignore b/.config/git/ignore index 511cfcff..a48cbe48 100644 --- a/.config/git/ignore +++ b/.config/git/ignore @@ -42,6 +42,8 @@ __pycache__ .claude/ .pi/memory/ .cursor/ +.superpowers/ +.worktrees/ # OS artifacts Desktop.ini diff --git a/AGENTS.md b/AGENTS.md index da88ae02..6912477b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,14 +1,13 @@ # dotfiles -Stow-managed personal dotfiles. Forked from `alx99/dotfiles` and heavily modified — README and `.gitconfig` still reference ALX99 (deliberate, do not "fix" without asking). +Stow-managed personal dotfiles. GitHub: `github.com/alx99/dotfiles`. ## Shape -- **Stow packages:** `home/` (→ `~`), `.config/` (→ `~/.config`), `.local/` (→ `~/.local`). `.stowrc` sets `--no-folding`. +- **Stow packages:** `home/` → `~`, `.config/` → `~/.config`, `.local/` → `~/.local`. `.stowrc` sets `--no-folding`. `.stow-local-ignore` skips `node_modules`. - **One Makefile target** (`make user-cfg` → `./init.sh 1`). Full menu in `init.sh`: `1` user config, `2` Linux system (Arch), `3` Mac system. - **Submodule:** `.config/mpv/scripts/subs2srs` → `Ajatt-Tools/mpvacious` (path name is misleading; it is mpvacious, not subs2srs). -- **Scratch dir:** `tmp/` is **untracked** (`.gitignore` and `git status` confirm). Do not put real work there. -- **170 tracked files.** No test framework, no build system. The only "build" is `make user-cfg`. The only "lint" is shellcheck via CI. +- **182 tracked files.** No test framework, no build system. The only "build" is `make user-cfg`. The only "lint" is shellcheck via CI. ## Symlinks in the repo (not stow-created) @@ -16,36 +15,58 @@ Stow-managed personal dotfiles. Forked from `alx99/dotfiles` and heavily modifie - `home/.bash_profile` → `.profile` — login shells source the POSIX profile. - `home/.codex/AGENTS.md` → `../.claude/CLAUDE.md` — Codex reads Claude's instruction file. -Runtime: `~/.pi/agent/extensions/.ts` (and `agents/*.md`, `supervisor.md`) are symlinks back into the dotfiles. **Pi's extension loader only auto-discovers from `/extensions/`** — placing an extension at `~/.pi/agent//` (one level up) makes it invisible. Same for `agents/`, `prompts/`, `skills/`. See `home/.pi/agent/extensions/subagents/` and the deleted-then-moved history in `git status`. +Runtime: `~/.pi/agent/extensions/.ts` (and `agents/*.md`, `supervisor.md`) are symlinks back into the dotfiles. **Pi's extension loader only auto-discovers from `/extensions/`** — placing an extension at `~/.pi/agent//` (one level up) makes it invisible. See `home/.pi/agent/extensions/` for the canonical set. -## Setup invariants +## Setup invariants (init.sh 1) -- `init.sh` shares `~/.agents/skills/` with `~/.claude/skills/` via per-skill symlinks. It uses `is_stale_dotfile_entry` to avoid clobbering content from `npx agents` or other tools — never `rm -rf` a `~/.claude/skills/` that the script flagged as "not a stale dotfile leftover." +- `init.sh` shares `~/.agents/skills/` with `~/.claude/skills/` via per-skill symlinks. Uses `is_stale_dotfile_entry` to avoid clobbering content from `npx agents` or other tools — never `rm -rf` a `~/.claude/skills/` that the script flagged as "not a stale dotfile leftover." +- User config stow order matters: `.local` first, then `home`, then `.config`. `~/.config` and `~/.agents` must exist before stow runs. Broken dotfile repo symlinks are cleaned up before stow via `remove_broken_symlinks`. +- `home/.stow-local-ignore` ignores `node_modules` to avoid stowing npm install dirs. - Linux: `init.sh 2` requires sudo for XKB/keyd/systemd units, enables `systemctl --user` ssh-agent, links `dash` to `/usr/bin/sh`. - Mac: `init.sh 3` installs `~/Library/Keyboard Layouts/Colemak-DH-ANSI.keylayout` and prompts the user to enable it manually in System Settings. No auto-reboot. -- `home/.privrc` is **tracked** and is sourced from `~/.bashrc`. It holds private env vars (e.g., `TOKENROUTER_API_KEY`). Treat as secret. +- After first `make user-cfg`, run `make pi` to install npm deps for pi extensions (`home/.pi/agent/extensions/`). + +## pi extensions + +Extensions live under `home/.pi/agent/extensions/` (stowed to `~/.pi/agent/extensions/`). Development commands (run from `home/.pi/agent/extensions/`): +- `npm run check` — runs `tsc`, then `eslint`, then `node --test '**/tests/*.test.ts'` +- `make pi` — shortcut for `cd ~/.pi/agent/extensions && npm install` + +**Pi's extension loader discovers `.ts` files directly in `extensions/` AND subdirectory entry points `extensions/*/index.ts`** (and `extensions/*/` with a `package.json` `pi` field). `missions/index.ts`, `goals/`-style, `memory/`, and `subagents/` all load via the subdirectory-`index.ts` pattern. `eslint.config.mjs` ends in `.mjs` so pi's loader skips it. + +Active extensions (each entry point is a file in `home/.pi/agent/extensions/`): +- **`subagents/index.ts`** — spawn_agent tool. `agents/*.md` define roles (default/scout/reviewer/worker), `process.ts` runs the child. No standalone `supervisor.md` file. +- **`missions/index.ts`** — Evidence-gated mission loop (replaces `goals`). `/mission start|status|freeze|stop|resume|show|list` command plus 11 tools (`mission_status`, `mission_add_acceptance_item`, `mission_create_work_item`, `mission_supersede_work_item`, `mission_set_final_verify`, `mission_claim_item`, `mission_heartbeat`, `mission_record_progress`, `mission_submit_item`, `mission_request_decision`, `mission_finish_candidate`). Controller runs frozen verify commands itself via `pi.exec` and records real exit codes; a claim/attempt lease with expiry is reaped on `session_start` so crashes don't deadlock. SQLite at `$PI_AGENT_DIR/missions.db` (missions/items/attempts/events). zod + neverthrow + typebox. Acceptance items are human-frozen at `/mission freeze` and gate completion; work items are agent-owned (created/superseded while running) and do not grant completion. A frozen mission-level final verifier (`mission_set_final_verify`, set during planning) must also pass. Phase 1: state machine + controller-enforced verification; the `agent_end` auto-re-entry controller is phase 2. Design spec: `docs/superpowers/specs/2026-06-20-mission-loop-design.md`. +- **`memory/index.ts`** — memory_save tool + `/memory` and `/memory-capture` commands. Injects memory block into system prompt on each agent start. User must approve each memory_save via UI confirmation. +- **`caffeinate.ts`** — Prevents macOS sleep during agent runs (spawns `/usr/bin/caffeinate` on agent_start, kills on agent_end). +- **`cost-saver.ts`** — Intercepts read tool calls: blocks full-file reads > 50 KB (forces offset/limit), deduplicates repeated reads of unchanged files via SHA-256 hash. +- **`cost-tracker.ts`** — `/analyze-cost` interactive dashboard (day/week/month breakdown by model and tool count). Reads JSONL session logs. +- **`ask_question.ts`** — ask_question tool (multiple choice with auto-added "Ask AI for pros and cons" and "Something else"). +- **`footer.ts`** — Custom footer: left side shows cwd, git branch, model + thinking level; right side shows extension statuses, session tokens (in/out/cache hit rate), context usage bar. + +Subagent protocol: `subagents/index.ts` (top-level loader) → `subagents/agents/*.md` (role definitions: default, scout, reviewer, worker) + `subagents/process.ts` (execution). No standalone `supervisor.md` file. ## Two AI ecosystems in parallel | Tool | Source in repo | Notes | |------|----------------|-------| -| **pi** | `home/.pi/agent/` | Primary. `settings.json` (provider tokenrouter, model `MiniMax-M3`, ponytail full, packages: ponytail, superpowers, pi-fff). `APPEND_SYSTEM.md` injects persona. `supervisor.md` is the goal-driven subagent protocol. | -| **Claude Code** | `home/.claude/` + `home/.agents/skills/` | `settings.json` has hooks (gofmt on .go, macOS notification on Stop), permissions allowlist, plugins (`superpowers@superpowers-marketplace`, `lsp@alx99-personal`). `status` script is the statusline. | +| **pi** | `home/.pi/agent/` | Primary. `settings.json` uses provider `openmodel`, model `deepseek-v4-flash`, packages: `superpowers` + `pi-fff`. `APPEND_SYSTEM.md` injects persona. No local `supervisor.md` — subagent protocol is in the extensions directory. Also configured with `enabledModels` including `tokenrouter/MiniMax-M3` and `openai-codex/gpt-5.5`. | +| **Claude Code** | `home/.claude/` + `home/.agents/skills/` | `settings.json` has hooks (gofmt on .go files, macOS notification on Stop), permissions allowlist, plugins (`superpowers`, `lsp@alx99-personal`). `status` script is the statusline. | | **Codex** | `home/.codex/` | Just the symlink to Claude's CLAUDE.md. No other config. | **Skill rules live in two places** by design: -- `home/.pi/agent/skills/` — pi-only skills. +- `home/.pi/agent/skills/` — pi-only skills (currently only `init/`). - `home/.agents/skills/` — canonical, mirrored to `~/.claude/skills/` for Claude Code by `init.sh`. The repo is mid-migration to **harness-agnostic skills** under `home/.agents/skills/` (see commits `dda9450`, `252c8ff`, `00d3f68`). Don't write new pi-specific skills — write them under `home/.agents/skills/` and let the mirror propagate. -## Active git state (worktree is dirty) +Most listed skills in the available-skills block (firecrawl/*, codebase-design, domain-modeling, brainstorming, etc.) come from the **superpowers package** (`git:github.com/obra/superpowers`), not from local skill files. + +## .profile secrets (not obvious from the file alone) -Staged deletions, untracked additions — the worktree is mid-restructure. Don't run `git checkout` or `git stash` blindly: -- Deleted from `home/.pi/agent/agents/`: `reviewer.md`, `scout.md`, `worker.md`. Replaced (untracked) by `home/.pi/agent/extensions/subagents/agents/{default,reviewer,scout,worker}.md`. -- Deleted from `home/.pi/agent/extensions/`: `ask-user-question.ts`, `brainstorm.ts`, `btw.ts`, `subagent/`, `tokenrouter/`. Replaced (untracked) by `ask_question.ts`, `subagents/`, plus new `goals/{storage,validation,templates,templates}.ts`. -- Modified: `home/.pi/agent/extensions/{footer,goals/index}.ts`, `home/.pi/agent/settings.json`, `home/.profile`, `home/.pi/agent/skills/init/SKILL.md`, `.config/git/ignore`, `misc/keymaps/Colemak-DH-ANSI.keylayout`. -- Untracked: `Dockerfile.cbm` (builds `DeusData/codebase-memory-mcp`), `reboot.py` (Buffalo router reboot — hardcoded creds, **don't commit**), `tmp/soulforge/` (3rd-party scratch), `symbol-layer-proposal.md`. +- `PI_FFF_MODE=override` — replaces pi's built-in find/grep with FFF (`@ff-labs/pi-fff` package). +- `DISABLE_TELEMETRY=1` — disables Claude Code telemetry. +- `NPM_CONFIG_IGNORE_SCRIPTS=true` — npm installs skip lifecycle scripts. ## Colemak-DH and theme colors @@ -72,3 +93,4 @@ No automated tests. Manual gates that catch real breakage: - `shellcheck $(git ls-files | xargs file | grep -i 'shell script' | cut -d: -f1)` — CI runs this; mirror it locally before pushing. - `bash -n home/.bashrc home/.profile` — syntax check the shell init. - `command -v stow` — `init.sh` hard-requires GNU stow (BSD `find` is patched for macOS compat, but stow itself is the package manager). +- `make check` — typecheck, lint, and test the pi extensions. diff --git a/Makefile b/Makefile index 3d67dc74..4baaa90f 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,16 @@ -.PHONY: user-cfg check lint typecheck test +.PHONY: user-cfg pi check user-cfg: ./init.sh 1 -# Run typecheck, lint, and tests in order. Stops on first failure. -# Dev tooling lives in home/.pi/agent/extensions.dev/ (sibling of -# extensions/, with no index.ts/index.js so pi's loader skips it). -check: - cd home/.pi/agent/extensions.dev && npm run check - -# Run only the supervisor extension's linter (eslint). -lint: - cd home/.pi/agent/extensions.dev && npm run lint +# Install npm dependencies for pi extensions in the stow target (~/.pi/agent/extensions/). +# Required after first `make user-cfg` or when package.json changes. +# The source's node_modules/ is a stale artifact — the target is canonical. +pi: + cd ~/.pi/agent/extensions && npm install -# Run only the supervisor extension's typecheck (tsc). -typecheck: - cd home/.pi/agent/extensions.dev && npm run typecheck - -# Run only the supervisor extension's tests (node --test). -test: - cd home/.pi/agent/extensions.dev && npm run test +# Run typecheck + lint + tests for the supervisor extension. Dev tooling +# lives in home/.pi/agent/extensions/; eslint.config.mjs ends in .mjs +# so pi's loader skips it. +check: + cd home/.pi/agent/extensions && npm run check diff --git a/home/.pi/agent/APPEND_SYSTEM.md b/home/.pi/agent/APPEND_SYSTEM.md index 4f44dfef..b206b0e7 100644 --- a/home/.pi/agent/APPEND_SYSTEM.md +++ b/home/.pi/agent/APPEND_SYSTEM.md @@ -1,70 +1,93 @@ - - -- You prioritize correctness, simplicity, and long-term maintainability over cleverness or novelty. -- You default to well-established patterns, standard libraries, and widely adopted practices. -- You avoid premature abstraction; abstractions must be justified by clear, repeated need. -- You are willing to challenge the user when their approach is flawed or suboptimal. - - - +# Persona + +* You prioritize correctness, simplicity, and long-term maintainability over cleverness or novelty. +* You default to well-established patterns, standard libraries, and widely adopted practices. +* You avoid premature abstraction; abstractions must be justified by clear, repeated need. The user owns abstractions — when the user articulates one ("we do this elsewhere," "the pattern is X"), apply it. Do not extract a general rule from a single instance on your own. +* You are willing to challenge the user when their approach is flawed or suboptimal. + +# Decision Framework + When evaluating solutions, prefer the option with: + 1. Fewer moving parts -2. Strong industry precedent +2. Modern APIs and idioms first; industry precedent is the fallback when no modern option exists. 3. Lower cognitive load for future maintainers 4. Clear failure modes and debuggability -- Do not optimize for edge cases unless they are explicitly required. -- Avoid introducing new dependencies unless they provide significant, proven value. - +* Do not optimize for edge cases unless they are explicitly required. +* Avoid introducing new dependencies unless they provide significant, proven value. +* When two options are similar in size, prefer the one correct on edge cases. +* Optimize for fewest concepts, not fewest files. +* Building procedure — stop at the first rung that holds: does it need to exist? (skip if speculative) → does stdlib cover it? → does a native platform feature cover it (e.g. `` over a picker lib, a DB constraint over app code)? → does an installed dependency cover it? → can it be one line? → only then, the minimum code that works. + +# Pushback Rules - Push back when the user: -- Reinvents existing tools, frameworks, or infrastructure -- Introduces unnecessary abstraction or indirection -- Overengineers for hypothetical future needs -- Ignores common best practices or constraints of the language/platform + +* Reinvents existing tools, frameworks, or infrastructure +* Introduces unnecessary abstraction or indirection +* Overengineers for hypothetical future needs +* Ignores common best practices or constraints of the language/platform When pushing back: -- Be direct and specific -- Clearly explain why the approach is problematic -- Provide a concrete, better alternative - - - -- Prefer actionable recommendations over listing many options. -- If multiple approaches are viable, briefly compare and then recommend one. -- Highlight trade-offs explicitly (e.g., simplicity vs flexibility, performance vs readability). -- Make assumptions explicit when required. - - - -- Be concise, but include enough detail to make the reasoning clear. -- Avoid vague statements; use concrete examples where helpful. -- Do not agree by default—agreement must be earned. -- Avoid filler, fluff, and generic “LLM-style” phrasing. - - - -- If information is missing or ambiguous, clarify or state assumptions before proceeding. -- Do not present speculation as fact. -- If something depends on context, say what it depends on. - - - -- Do not invent new patterns, architectures, or terminology without strong justification. -- Do not over-abstract or generalize beyond what the problem requires. -- Favor clarity and explicitness over clever or “smart” solutions. -- Favor correct architecture, design, and extensibility over "quick" solutions, even if it means more upfront work. - - - -- Before reading a file, ask: can I answer this from what I already know? If yes, skip the read. -- Use targeted rg searches to find specific API details — do not read entire files. -- When running shell commands that could produce large output, pipe through head/tail/rg/awk to limit results. Example: `rg pattern ./path | head -20` not bare `rg pattern ./path`. - - - -- Use rg over grep, and fd over find. -- Trust the write tool's response; do not re-read files to verify writes. - - + +* Be direct and specific +* Clearly explain why the approach is problematic +* Provide a concrete, better alternative + +# Output Expectations + +* Prefer actionable recommendations over listing many options. +* If multiple approaches are viable, briefly compare and then recommend one. +* Highlight trade-offs explicitly (e.g., simplicity vs flexibility, performance vs readability). +* Make assumptions explicit when required. +* When the answer is code: lead with the code, then at most a few short lines naming what was skipped and when to add it back. + +# Communication Style + +* Be concise, but include enough detail to make the reasoning clear. +* Avoid vague statements; use concrete examples where helpful. +* Do not agree by default—agreement must be earned. +* Avoid filler, fluff, and generic “LLM-style” phrasing. + +# Uncertainty Handling + +* If information is missing or ambiguous, clarify or state assumptions before proceeding. +* Do not present speculation as fact. +* If something depends on context, say what it depends on. + +# Constraints + +* Do not invent new patterns, architectures, or terminology without strong justification. +* Do not over-abstract or generalize beyond what the problem requires. +* Favor clarity and explicitness over clever or “smart” solutions. +* Favor correct architecture, design, and extensibility over "quick" solutions, even if it means more upfront work. +* Never cut corners on: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, or explicit user requirements. +* Hardware is real, not the spec ideal — clocks drift, sensors read off, peripherals run a few percent fast. Leave calibration knobs, not just less code. +* Non-trivial logic (a branch, a loop, a parser, a money or security path) leaves one runnable check behind: an assert-based `__main__` self-check or one small test. Trivial one-liners need none. + +# Context Efficiency + +* Before reading a file, ask: can I answer this from what I already know? If yes, skip the read. +* The building-decision procedure above governs change work. On Q&A, review, or explanation turns, deprioritize it — answer the question first. + +# Search Tools + +**Search lives in `ffgrep` / `fffind`, never in `bash`. Drift signature: `cmd && grep`, `cmd | rg`, `ls dir/**` to discover, `find . -name ...`, `grep -r "..."`. When you see yourself composing any of these mid-investigation — STOP, split, use the search tools.** + +Why this drifts: when `bash` is already loaded mid-flow, appending `&& grep` feels cheaper than opening a new tool call. That calculation is wrong. `ffgrep` / `fffind` are one tool call each, and the *next* search is faster after them (frecency + git-aware ranking). The drift compounds — pay now or pay more later. + +**Pre-flight on every `bash` command.** If the line contains `grep`, `egrep`, `fgrep`, `rg`, `ag`, `find`, `tree`, `ls **`, or a glob across files, the goal is discovery — use `ffgrep` (content) or `fffind` (path/glob) instead. `bash` stays for: reading a file with a known path (`read` / `cat` / `head`), running scripts, builds, installs, git ops, and searches outside the repo (`cd && rg ...`). + +`ffgrep` = content search. `fffind` = path/filename search. Both are pi-native, frecency-ranked, git-aware. + +* Scope = workspace repo at CWD. `path` MUST be repo-relative; absolute paths error. Outside-repo search → fall back to `cd && rg ...`. +* Smart-case default; force `caseSensitive: true` for exact case. Auto-detects regex vs literal. Multi-word = AND-narrow, not OR. +* `exclude` = comma/array of path prefixes, filenames, globs (`test/,*.lock,*.min.js`). +* `pattern` is fuzzy filename matching; use `path` for globs (`path: "src/**/*.ts"`), `pattern` for concepts (`pattern: "spawn_agent"`). +* `context: N` adds N lines around matches. `cursor` paginates beyond `limit`. +* On 0 exact matches, `ffgrep` falls back to fuzzy path matches prefixed "Maybe you meant this?" — discovery hint, not a result. + +# Tool Use + +* Trust the write tool's response; do not re-read files to verify writes. diff --git a/home/.pi/agent/extensions/.gitignore b/home/.pi/agent/extensions/.gitignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/home/.pi/agent/extensions/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/home/.pi/agent/extensions/ask-user-question.ts b/home/.pi/agent/extensions/ask-user-question.ts deleted file mode 100644 index 04349f8b..00000000 --- a/home/.pi/agent/extensions/ask-user-question.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Ask User Question — multiple choice with automatic follow-up options. - * - * The AI provides a question and 2-5 alternatives. The tool appends "Ask AI for pros and cons" - * and "Something else" as final options. If the user asks for pros/cons, the AI should - * explain the trade-offs and call this tool again with the same question/alternatives. - * If the user picks "Something else", a free-form input prompt is shown. - */ - -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { Type } from "typebox"; -import { Text } from "@earendil-works/pi-tui"; - -const PROS_CONS_OPTION = "Ask AI for pros and cons"; -const OTHER_OPTION = "Something else"; -const NO_ANSWER_MSG = "User declined to answer, await further instructions."; - -interface AskUserQuestionDetails { - question: string; - alternatives: string[]; - answer: string | null; - wasCustom: boolean; -} - -const AskUserQuestionParams = Type.Object({ - question: Type.String({ description: "The question to ask the user" }), - alternatives: Type.Array(Type.String({ description: "One alternative answer option" }), { - minItems: 2, - maxItems: 5, - description: "2 to 5 alternative answer options." - }), -}); - -export default function(pi: ExtensionAPI) { - pi.registerTool({ - name: "ask_user_question", - label: "Ask User Question", - description: - "Ask the user a multiple-choice question. Provide 2-5 alternatives. The tool automatically adds 'Ask AI for pros and cons' and 'Something else'. Use when you need the user to choose between specific options, ask for trade-offs, or provide a custom answer.", - promptSnippet: "Ask the user a multiple-choice question with 2-5 alternatives", - promptGuidelines: [ - "Use ask_user_question when you need the user to pick from specific options, ask for trade-offs, or provide a custom answer.", - "Keep alternatives short and mutually exclusive.", - ], - parameters: AskUserQuestionParams, - - async execute(_toolCallId, params, signal, _onUpdate, ctx) { - if (!ctx.hasUI) { - return makeResult(params, "Error: UI not available (running in non-interactive mode)", null, false); - } - - const options = [...params.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; - const choice = await ctx.ui.select(params.question, options, { signal }); - - if (choice == null) { - return makeResult(params, NO_ANSWER_MSG, null, false); - } - - if (choice === PROS_CONS_OPTION) { - return makeResult( - params, - "User asked for pros and cons. Explain the pros and cons of each alternative, then call ask_user_question again with the same question and alternatives.", - choice, - false, - ); - } - - if (choice === OTHER_OPTION) { - const custom = await ctx.ui.input("Something else", "Type your answer...", { signal }); - if (custom == null) { - return makeResult(params, NO_ANSWER_MSG, null, false); - } - return makeResult(params, `User answered (custom): ${custom}`, custom, true); - } - - return makeResult(params, `User selected: ${choice}`, choice, false); - }, - - renderCall(args, theme, _context) { - const opts = [...args.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; - const optsText = opts.map((o, i) => `${i + 1}. ${o}`).join(", "); - const text = - theme.fg("toolTitle", theme.bold("ask_user_question ")) + - theme.fg("muted", args.question) + - `\n${theme.fg("dim", ` Options: ${optsText}`)}`; - return new Text(text, 0, 0); - }, - - renderResult(result, _options, theme, _context) { - const details = result.details as AskUserQuestionDetails | undefined; - if (!details || details.answer === null) { - return new Text(theme.fg("warning", "Cancelled"), 0, 0); - } - if (details.wasCustom) { - return new Text( - theme.fg("success", "✓ ") + - theme.fg("muted", "(custom) ") + - theme.fg("accent", details.answer), - 0, - 0, - ); - } - return new Text(theme.fg("success", "✓ ") + theme.fg("accent", details.answer), 0, 0); - }, - }); -} - -function makeResult( - params: { question: string; alternatives: string[] }, - text: string, - answer: string | null, - wasCustom: boolean, -) { - return { - content: [{ type: "text" as const, text }], - details: { - question: params.question, - alternatives: params.alternatives, - answer, - wasCustom, - } satisfies AskUserQuestionDetails, - }; -} diff --git a/home/.pi/agent/extensions/ask_question.ts b/home/.pi/agent/extensions/ask_question.ts new file mode 100644 index 00000000..de14806b --- /dev/null +++ b/home/.pi/agent/extensions/ask_question.ts @@ -0,0 +1,130 @@ +/** + * Ask Question — multiple choice with automatic follow-up options. + * + * The AI provides a question and 2-5 alternatives. The tool appends "Ask AI for pros and cons" + * and "Something else" as final options. If the user asks for pros/cons, the AI should + * explain the trade-offs and call this tool again with the same question/alternatives. + * If the user picks "Something else", a free-form input prompt is shown. + * + * The tool is only registered when the session has a UI (TUI or RPC). In agent / non- + * interactive modes the tool is never exposed to the LLM, so it can't be invoked and + * can't produce the "UI not available" error. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import { Text } from "@earendil-works/pi-tui"; + +const PROS_CONS_OPTION = "Ask AI for pros and cons"; +const OTHER_OPTION = "Something else"; +const NO_ANSWER_MSG = "User declined to answer, await further instructions."; + +interface AskQuestionDetails { + question: string; + alternatives: string[]; + answer: string | null; + wasCustom: boolean; +} + +const AskQuestionParams = Type.Object({ + question: Type.String({ description: "The question to ask the user" }), + alternatives: Type.Array(Type.String({ description: "One alternative answer option" }), { + minItems: 2, + maxItems: 5, + description: "2 to 5 alternative answer options." + }), +}); + +export default function(pi: ExtensionAPI) { + // Defer registration to session_start so we can branch on ctx.hasUI. + // In non-interactive runs (CLI scripted, agents driving pi, etc.) the + // tool is simply absent from the agent's tool list. + pi.on("session_start", (_event, ctx) => { + if (!ctx.hasUI) return; + + pi.registerTool({ + name: "ask_question", + label: "Ask Question", + description: + "Ask the user a multiple-choice question. Provide 2-5 alternatives. The tool automatically adds 'Ask AI for pros and cons' and 'Something else'. Use when you need the user to choose between specific options, ask for trade-offs, or provide a custom answer.", + promptSnippet: "Ask the user a multiple-choice question with 2-5 alternatives", + promptGuidelines: [ + "Use ask_question when you need the user to pick from specific options, ask for trade-offs, or provide a custom answer.", + "Keep alternatives short and mutually exclusive.", + ], + parameters: AskQuestionParams, + + async execute(_toolCallId, params, signal, _onUpdate, ctx) { + const options = [...params.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; + const choice = await ctx.ui.select(params.question, options, { signal }); + + if (choice == null) { + return makeResult(params, NO_ANSWER_MSG, null, false); + } + + if (choice === PROS_CONS_OPTION) { + return makeResult( + params, + "User asked for pros and cons. Explain the pros and cons of each alternative, then call ask_question again with the same question and alternatives.", + choice, + false, + ); + } + + if (choice === OTHER_OPTION) { + const custom = await ctx.ui.input("Something else", "Type your answer...", { signal }); + if (custom == null) { + return makeResult(params, NO_ANSWER_MSG, null, false); + } + return makeResult(params, `User answered (custom): ${custom}`, custom, true); + } + + return makeResult(params, `User selected: ${choice}`, choice, false); + }, + + renderCall(args, theme, _context) { + const opts = [...args.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; + const optsText = opts.map((o, i) => `${i + 1}. ${o}`).join(", "); + const text = + theme.fg("toolTitle", theme.bold("ask_question ")) + + theme.fg("muted", args.question) + + `\n${theme.fg("dim", ` Options: ${optsText}`)}`; + return new Text(text, 0, 0); + }, + + renderResult(result, _options, theme, _context) { + const details = result.details as AskQuestionDetails | undefined; + if (!details || details.answer === null) { + return new Text(theme.fg("warning", "Cancelled"), 0, 0); + } + if (details.wasCustom) { + return new Text( + theme.fg("success", "✓ ") + + theme.fg("muted", "(custom) ") + + theme.fg("accent", details.answer), + 0, + 0, + ); + } + return new Text(theme.fg("success", "✓ ") + theme.fg("accent", details.answer), 0, 0); + }, + }); + }); +} + +function makeResult( + params: { question: string; alternatives: string[] }, + text: string, + answer: string | null, + wasCustom: boolean, +) { + return { + content: [{ type: "text" as const, text }], + details: { + question: params.question, + alternatives: params.alternatives, + answer, + wasCustom, + } satisfies AskQuestionDetails, + }; +} diff --git a/home/.pi/agent/extensions/brainstorm.ts b/home/.pi/agent/extensions/brainstorm.ts deleted file mode 100644 index 5332a91f..00000000 --- a/home/.pi/agent/extensions/brainstorm.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Brainstorm Mode - * - * Lightweight developer/CEO collaboration mode. The agent acts as a senior - * developer / technical lead; the user is the CEO/product owner. /brainstorm - * toggles a context-injected mode. Plan persistence, finalize/recommend/ - * implement subcommands and heading validators are intentionally not provided - * — call them out in the conversation and copy/paste the plan when ready. - */ - -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; - -const BRAINSTORM_CONTEXT = `[BRAINSTORM MODE ACTIVE] -You are acting as a senior developer / technical lead. The user is the CEO/product owner. - -Primary job: -Help turn rough business/product direction into a practical technical direction through back-and-forth discussion. - -Operating principles: -- Treat the user as the decision-maker for product/business trade-offs. -- You own technical judgment: feasibility, simplicity, maintainability, risks, and implementation shape. -- Be direct. Push back on vague goals, overengineering, risky shortcuts, or weak assumptions. -- Prefer the smallest coherent slice that delivers value. -- Do not create durable specs, plans, or implementation handoff documents unless explicitly asked. -- Do not start implementation or make durable code/config changes unless the user explicitly asks you to. -- Reversible experiments are allowed only after stating purpose, files affected, expected learning, and rollback plan. - -Tool use: -- Use tools to understand reality before making technical claims. -- Token discipline matters: default to direct reasoning from established context when confidence is adequate. -- If the subagent tool is available, use scout only when broad read-only reconnaissance would materially improve the recommendation. -- If the subagent tool is available, use reviewer only when correctness risk, future compatibility, or deviation from current structure/standard patterns justifies the spend. -- Do not run a multi-agent pipeline; use at most one focused subagent call for a recommendation turn unless the user explicitly asks otherwise. -- Use direct reads for focused follow-up after scout/reviewer returns compressed findings. -- Read-only commands, git status/diff, tests, and targeted diagnostics are encouraged when they improve the recommendation. -- If tool output contradicts assumptions, update the recommendation. - -Conversation style: -- Keep responses concise: usually 3-6 bullets. -- Ask at most one question at a time, and only when it blocks a meaningful recommendation. -- If ambiguity is not blocking, state a reasonable assumption and proceed. -- Recommend one path by default. Mention alternatives only when they materially affect the CEO decision. -- Separate business decisions from technical decisions when useful. -- Avoid generic questionnaires and long checklists. - -Useful response shapes: -- Recommendation: what I would do, why, trade-offs, next decision. -- Pushback: what is risky, why it matters, safer alternative. -- Understanding check: goal, constraints, likely approach, open decision. -- Implementation readiness: files/areas likely touched, risks, verification approach.`; - -export default function (pi: ExtensionAPI) { - let active = false; - - function updateStatus(ctx: ExtensionContext): void { - ctx.ui.setStatus("brainstorm", active ? ctx.ui.theme.fg("warning", "brainstorm") : undefined); - } - - pi.registerFlag("brainstorm", { - description: "Start in brainstorm mode", - type: "boolean", - default: false, - }); - - pi.registerCommand("brainstorm", { - description: "Toggle brainstorm mode (developer/CEO back-and-forth)", - handler: async (args, ctx) => { - const topic = args.trim(); - active = !active; - updateStatus(ctx); - ctx.ui.notify(active ? "Brainstorm mode enabled." : "Brainstorm mode disabled.", "info"); - if (active && topic) { - pi.sendUserMessage(`Brainstorm topic: ${topic}`); - } - }, - }); - - pi.on("session_start", (_event, ctx) => { - if (pi.getFlag("brainstorm") === true) active = true; - updateStatus(ctx); - }); - - pi.on("before_agent_start", async () => { - if (!active) return; - return { - message: { - customType: "brainstorm-context", - content: BRAINSTORM_CONTEXT, - display: false, - }, - }; - }); - - pi.on("session_shutdown", () => { active = false; }); -} diff --git a/home/.pi/agent/extensions/btw.ts b/home/.pi/agent/extensions/btw.ts deleted file mode 100644 index 24c6b00e..00000000 --- a/home/.pi/agent/extensions/btw.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -function buildBtwPrompt(question: string): string { - return `Side question / BTW: ${question} - -Treat this as a lightweight side conversation: -- Answer the side question without derailing or broadening the main task. -- Do not make durable code/config changes unless the user explicitly asks in this side question. -- Prefer read-only inspection. If broad reconnaissance is needed and the subagent tool is available, use the scout subagent once. -- Keep the response concise and call out any uncertainty or follow-up needed.`; -} - -export default function(pi: ExtensionAPI) { - pi.registerCommand("btw", { - description: "Ask a lightweight side question without derailing the main task", - handler: async (args, ctx) => { - const question = args.trim(); - if (!question) { - ctx.ui.notify("Usage: /btw ", "warning"); - return; - } - - const prompt = buildBtwPrompt(question); - if (ctx.isIdle()) { - pi.sendUserMessage(prompt); - } else { - pi.sendUserMessage(prompt, { deliverAs: "followUp" }); - ctx.ui.notify("BTW queued as a follow-up side question.", "info"); - } - }, - }); -} diff --git a/home/.pi/agent/extensions/cost-tracker.ts b/home/.pi/agent/extensions/cost-tracker.ts index 99de4166..d254cfd9 100644 --- a/home/.pi/agent/extensions/cost-tracker.ts +++ b/home/.pi/agent/extensions/cost-tracker.ts @@ -10,7 +10,7 @@ * (just timestamps + tool call counts). */ -import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { getAgentDir, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent"; import { type Component, matchesKey, @@ -218,11 +218,6 @@ function aggregateTools(toolRecords: ToolRecord[], cutoff: number): Record string; - bold: (text: string) => string; -}; - const TABS: Period[] = ["day", "week", "month"]; const TAB_LABELS: Record = { day: "Day", week: "Week", month: "Month" }; diff --git a/home/.pi/agent/extensions/eslint.config.mjs b/home/.pi/agent/extensions/eslint.config.mjs new file mode 100644 index 00000000..85736bba --- /dev/null +++ b/home/.pi/agent/extensions/eslint.config.mjs @@ -0,0 +1,25 @@ +// @ts-check +import js from "@eslint/js"; +import { defineConfig } from "eslint/config"; +import tseslint from "typescript-eslint"; + +export default defineConfig({ + files: ["**/*.ts"], + extends: [js.configs.recommended, tseslint.configs.recommended], + languageOptions: { + parser: tseslint.parser, + }, + rules: { + // typescript-eslint's no-unused-vars drops the base rule's `_` prefix + // convention by default. Restore it so `_context`, `_unused`, etc. work. + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, +}); diff --git a/home/.pi/agent/extensions/footer.ts b/home/.pi/agent/extensions/footer.ts index 82a06f3f..e4222329 100644 --- a/home/.pi/agent/extensions/footer.ts +++ b/home/.pi/agent/extensions/footer.ts @@ -1,8 +1,8 @@ /** * Footer Extension — Full custom footer replacement. * - * Shows on the left: other extension statuses, cwd, git branch - * Shows on the right: (provider) model, thinking level, context bar, session tokens, latest cache hit rate + * Shows on the left: cwd, git branch, model + thinking level, context bar + * Shows on the right: extension statuses, session tokens, latest cache hit rate * * Right-aligned with space padding so stats stay flush to the terminal edge. */ @@ -25,6 +25,18 @@ function shortenCwd(cwd: string): string { return cwd.startsWith(home) ? cwd.replace(home, "~") : cwd; } +/* ─── thinking level color ramp ─── */ + +// Pi ships per-level theme colors. Map thinking level → theme color so +// minimal reads cool/dim and xhigh reads hot, using the theme's palette. +const THINKING_COLOR: Record = { + minimal: "thinkingMinimal", + low: "thinkingLow", + medium: "thinkingMedium", + high: "thinkingHigh", + xhigh: "thinkingXhigh", +}; + /* ─── context bar ─── */ function renderContextBar(usage: { tokens: number | null; contextWindow: number; percent: number | null }, theme: ExtensionContext["ui"]["theme"]): string { @@ -36,7 +48,7 @@ function renderContextBar(usage: { tokens: number | null; contextWindow: number; const filled = Math.round((usage.percent / 100) * width); const empty = width - filled; - let color: string; + let color: "accent" | "warning" | "error"; if (usage.percent < 50) color = "accent"; else if (usage.percent < 80) color = "warning"; else color = "error"; @@ -97,43 +109,47 @@ function setupFooter(ctx: ExtensionContext, pi: ExtensionAPI): () => void { render(width: number): string[] { recheckTokens(); - /* left: other extension statuses + cwd + (branch) */ - const statuses = footerData.getExtensionStatuses(); - let left = theme.fg("dim", shortenCwd(ctx.cwd)); + /* left: cwd, branch, model, context bar */ + const leftParts: string[] = []; + leftParts.push(theme.fg("muted", shortenCwd(ctx.cwd))); const branchName = footerData.getGitBranch(); if (branchName) { - left += theme.fg("dim", ` (${branchName})`); + leftParts.push(theme.fg("dim", "(") + theme.fg("accent", branchName) + theme.fg("dim", ")")); } + const model = ctx.model; + if (model) { + let modelText = theme.fg("text", model.id) + theme.fg("dim", "(") + theme.fg("muted", model.provider) + theme.fg("dim", ")"); + const thinking = pi.getThinkingLevel(); + if (thinking) { + const color = thinking === "off" ? "muted" : THINKING_COLOR[thinking]; + if (color) modelText += theme.fg(color, ` · ${thinking}`); + } + leftParts.push(modelText); + } + + const left = leftParts.join(" "); + + /* right: extension statuses, session tokens, context bar */ + const statuses = footerData.getExtensionStatuses(); const statusParts: string[] = []; for (const [, text] of statuses) { statusParts.push(text); } - if (statusParts.length > 0) { - left = statusParts.join(" ") + " " + left; - } - /* right: model ctx-bar reasoning session-tokens */ const rightParts: string[] = []; - - const model = ctx.model; - if (model) { - rightParts.push(theme.fg("dim", `(${model.provider}) ${model.id}`)); + if (statusParts.length > 0) { + rightParts.push(statusParts.join(" ")); } + rightParts.push(cachedTokens); + const ctxUsage = ctx.getContextUsage(); if (ctxUsage) { rightParts.push(renderContextBar(ctxUsage, theme)); } - const thinking = pi.getThinkingLevel(); - if (thinking && thinking !== "off") { - rightParts.push(theme.fg("warning", thinking)); - } - - rightParts.push(cachedTokens); - const right = rightParts.join(" "); const pad = width - visibleWidth(left) - visibleWidth(right); if (pad > 0) { @@ -150,8 +166,6 @@ function setupFooter(ctx: ExtensionContext, pi: ExtensionAPI): () => void { function buildSessionTokens(ctx: ExtensionContext): string { let input = 0; let output = 0; - let cacheRead = 0; - let cacheWrite = 0; let latestCacheHitRate: number | undefined; for (const e of ctx.sessionManager.getBranch()) { @@ -159,18 +173,15 @@ function buildSessionTokens(ctx: ExtensionContext): string { const usage = e.message.usage; input += usage.input ?? 0; output += usage.output ?? 0; - cacheRead += usage.cacheRead ?? 0; - cacheWrite += usage.cacheWrite ?? 0; const latestPromptTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0); latestCacheHitRate = latestPromptTokens > 0 ? ((usage.cacheRead ?? 0) / latestPromptTokens) * 100 : undefined; } } + // Always show CH (with ??.?% placeholder when unknown) so the right-side + // width is stable and the layout doesn't shift as cache data appears. const sep = ctx.ui.theme.fg("dim", "/"); - let tokens = `${ctx.ui.theme.fg("accent", "↑")}${fmt(input)}${sep}${ctx.ui.theme.fg("accent", "↓")}${fmt(output)}`; - if ((cacheRead > 0 || cacheWrite > 0) && latestCacheHitRate !== undefined) { - tokens += `${sep}${ctx.ui.theme.fg("accent", "CH")}${latestCacheHitRate.toFixed(1)}%`; - } - return tokens; + const chStr = latestCacheHitRate !== undefined ? `${latestCacheHitRate.toFixed(1)}%` : "??.?%"; + return `${ctx.ui.theme.fg("accent", "↑")}${fmt(input)}${sep}${ctx.ui.theme.fg("accent", "↓")}${fmt(output)}${sep}${ctx.ui.theme.fg("accent", "CH")}${chStr}`; } diff --git a/home/.pi/agent/extensions/goals/index.ts b/home/.pi/agent/extensions/goals/index.ts deleted file mode 100644 index 9e106035..00000000 --- a/home/.pi/agent/extensions/goals/index.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Goals — autonomous supervisor for ad-hoc goals. - * - * Commands: - * /goal Start a goal; current session becomes the supervisor - * /goal-status Show current goal's progress (tail of progress.md) - * /goal-cancel Mark current goal as cancelled - * /goals List all goals (past and current) - * - * Protocol: - * The supervisor protocol lives at ~/.pi/agent/supervisor.md. - * When /goal is invoked, the current session's model is told (via a directive - * message) to read the protocol and operate as supervisor. State is written - * to ~/.pi/agent/goals// following the protocol's file conventions. - * - * Lifecycle: - * The active goal id is tracked in a closure variable. The footer status - * badge reflects it. A goal is "active" until final-report.md, stuck.md, or - * cancelled.md is written — the user can run /goal-cancel to mark one as - * cancelled explicitly, or the supervisor writes the other two itself. - * - * Non-goals (v1): - * - Spawning a fresh session via ctx.newSession (current session IS the supervisor) - * - Auto-detecting goal completion (supervisor reports via final-report.md; user checks /goal-status) - * - Backgrounded / detached supervisor runs - */ - -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import { homedir } from "node:os"; -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; - -const GOALS_DIR = path.join(homedir(), ".pi/agent/goals"); - -function shortId(): string { - return Date.now().toString(36) + Math.random().toString(36).slice(2, 6); -} - -async function ensureGoalsDir(): Promise { - await fs.mkdir(GOALS_DIR, { recursive: true }); -} - -async function fileExists(file: string): Promise { - try { - await fs.access(file); - return true; - } catch { - return false; - } -} - -async function readFirstLine(file: string): Promise { - try { - const content = await fs.readFile(file, "utf-8"); - const first = content.split("\n").find((line) => line.trim().length > 0) ?? ""; - return first.replace(/^#+\s*/, "").trim(); - } catch { - return "(not yet written)"; - } -} - -async function readTail(file: string, lines: number): Promise { - try { - const content = await fs.readFile(file, "utf-8"); - return content.split("\n").slice(-lines).join("\n"); - } catch { - return "(not yet written)"; - } -} - -type GoalStatus = "running" | "completed" | "stuck" | "cancelled"; - -async function goalStatus(id: string): Promise { - const dir = path.join(GOALS_DIR, id); - if (await fileExists(path.join(dir, "cancelled.md"))) return "cancelled"; - if (await fileExists(path.join(dir, "final-report.md"))) return "completed"; - if (await fileExists(path.join(dir, "stuck.md"))) return "stuck"; - return "running"; -} - -export default function(pi: ExtensionAPI) { - let activeGoalId: string | undefined; - - function setStatus(ctx: ExtensionContext): void { - if (activeGoalId) { - ctx.ui.setStatus("goal", ctx.ui.theme.fg("warning", `goal:${activeGoalId}`)); - } else { - ctx.ui.setStatus("goal", undefined); - } - } - - pi.on("session_start", (_event, ctx) => { - setStatus(ctx); - }); - - pi.registerCommand("goal", { - description: "Start an autonomous goal: current session becomes the supervisor with no budget caps", - handler: async (args, ctx) => { - await ctx.waitForIdle(); - - let text = args.trim(); - if (!text && ctx.hasUI) { - const input = await ctx.ui.input("What is the goal?", "Describe the goal for the supervisor..."); - text = input?.trim() ?? ""; - } - if (!text) { - ctx.ui.notify("Usage: /goal ", "error"); - return; - } - - await ensureGoalsDir(); - const id = shortId(); - const dir = path.join(GOALS_DIR, id); - await fs.mkdir(dir, { recursive: true }); - await fs.writeFile( - path.join(dir, "goal.md"), - `# Goal\n\n${text}\n\n## Restated\n\n(Supervisor will restate this in concrete terms here.)\n`, - "utf-8", - ); - - activeGoalId = id; - setStatus(ctx); - ctx.ui.notify(`Goal ${id} started. State at ~/.pi/agent/goals/${id}/`, "info"); - - const directive = `You are now the supervisor for a new goal. - -Goal: ${text} - -Goal state directory: ~/.pi/agent/goals/${id}/ - -Required first actions: -1. Read ~/.pi/agent/supervisor.md — the supervisor protocol (read fully). -2. Read ~/.pi/agent/goals/${id}/goal.md and restate the goal concretely. -3. Ask 2-4 clarifying questions via ctx.ui.ask before dispatching anything; write answers to clarifications.md. - -Then operate per the protocol: plan → dispatch via the subagent tool (scout/worker/reviewer) → verify gates yourself → iterate. No budget caps. Mark each item resolved or stuck; do not stop early. When complete, write final-report.md to the goal state directory and give me a one-paragraph summary.`; - - pi.sendUserMessage(directive); - }, - }); - - pi.registerCommand("goal-status", { - description: "Show the current goal's status and latest progress entries", - handler: async (_args, ctx) => { - if (!activeGoalId) { - ctx.ui.notify("No active goal. Use /goal to start one.", "warning"); - return; - } - const dir = path.join(GOALS_DIR, activeGoalId); - const status = await goalStatus(activeGoalId); - const goal = await readFirstLine(path.join(dir, "goal.md")); - const progress = await readTail(path.join(dir, "progress.md"), 12); - - const output = `Goal ${activeGoalId} — ${status}\n${goal}\n\n--- latest progress ---\n${progress}`; - ctx.ui.notify(output, "info"); - }, - }); - - pi.registerCommand("goal-cancel", { - description: "Mark the current goal as cancelled (writes cancelled.md; does not delete state)", - handler: async (_args, ctx) => { - if (!activeGoalId) { - ctx.ui.notify("No active goal to cancel.", "warning"); - return; - } - const id = activeGoalId; - const dir = path.join(GOALS_DIR, id); - await fs.writeFile( - path.join(dir, "cancelled.md"), - `# Cancelled at ${new Date().toISOString()}\n`, - "utf-8", - ); - activeGoalId = undefined; - setStatus(ctx); - ctx.ui.notify(`Goal ${id} marked as cancelled.`, "info"); - }, - }); - - pi.registerCommand("goals", { - description: "List all goals with status (most recent first)", - handler: async (_args, ctx) => { - await ensureGoalsDir(); - const entries = await fs.readdir(GOALS_DIR, { withFileTypes: true }); - const dirs = entries.filter((e) => e.isDirectory()); - if (dirs.length === 0) { - ctx.ui.notify("No goals yet. Use /goal to start one.", "info"); - return; - } - - const lines: string[] = []; - const sorted = await Promise.all( - dirs.map(async (entry) => { - const stat = await fs.stat(path.join(GOALS_DIR, entry.name)); - return { entry, mtime: stat.mtimeMs }; - }), - ); - sorted.sort((a, b) => b.mtime - a.mtime); - - for (const { entry } of sorted) { - const id = entry.name; - const dir = path.join(GOALS_DIR, id); - const stat = await fs.stat(dir); - const goal = await readFirstLine(path.join(dir, "goal.md")); - const status = await goalStatus(id); - const active = id === activeGoalId ? " (active)" : ""; - const date = stat.mtime.toISOString().slice(0, 10); - lines.push(`${date} ${status.padEnd(10)} ${id}${active} — ${goal.slice(0, 60)}`); - } - - ctx.ui.notify(lines.join("\n"), "info"); - }, - }); -} diff --git a/home/.pi/agent/extensions/memory/index.ts b/home/.pi/agent/extensions/memory/index.ts index f44f4bcd..5e4ba7b0 100644 --- a/home/.pi/agent/extensions/memory/index.ts +++ b/home/.pi/agent/extensions/memory/index.ts @@ -1,4 +1,3 @@ -import { StringEnum } from "@earendil-works/pi-ai"; import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { mkdir, readFile, stat, appendFile } from "node:fs/promises"; @@ -87,7 +86,7 @@ function parseCaptureArgs(args: string): { scope?: MemoryScope; note: string } { async function chooseCaptureTarget( pi: ExtensionAPI, - ctx: ExtensionCommandContext, + ctx: ExtensionContext, requestedScope?: MemoryScope, ): Promise<{ scope: MemoryScope; filePath: string } | undefined> { if (requestedScope === "global") return { scope: "global", filePath: GLOBAL_MEMORY_PATH }; @@ -171,7 +170,7 @@ export default function(pi: ExtensionAPI) { "Do not use memory_save for secrets, transient task details, guesses, or information already clearly covered by AGENTS.md.", ], parameters: Type.Object({ - scope: StringEnum(["global", "repo"], { description: "Where to save the memory" }), + scope: Type.Union([Type.Literal("global"), Type.Literal("repo")], { description: "Where to save the memory" }), note: Type.String({ description: "Concise durable memory to save" }), }), diff --git a/home/.pi/agent/extensions/package-lock.json b/home/.pi/agent/extensions/package-lock.json new file mode 100644 index 00000000..9745b4a0 --- /dev/null +++ b/home/.pi/agent/extensions/package-lock.json @@ -0,0 +1,4679 @@ +{ + "name": "extensions", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "extensions", + "version": "1.0.0", + "dependencies": { + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-coding-agent": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", + "execa": "^9.6.1", + "neverthrow": "^8.2.0", + "typebox": "^1.2.17", + "zod": "^4.4.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^26.0.0", + "eslint": "^10.5.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.1" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.22.tgz", + "integrity": "sha512-YofH63shc6YRdXjz80BJkpJW+Bkn0Cuu2dn4Rv7s9G2Idt58tgtzQEWxrR2xVljlVfIBeUjPuULnSVYLke3sUQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@aws-sdk/xml-builder": "^3.972.30", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.48.tgz", + "integrity": "sha512-h6FEC95fbexUd6zxm4PdgS82bTcI2PRtUb2ZwMipb/Xr8bPwtf0G8rBo2jp7NA24Mbx2JA8/WingiYpA9RCCyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.50.tgz", + "integrity": "sha512-lJO3OLpjvz5m/RSBQmsG/CEUGsvCy5ruxKwPQaOCqxqCMuyYT2BZwQUTDZVVwqQ9LrZKuK24JSa6r31hL/tvkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.1.tgz", + "integrity": "sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.55.tgz", + "integrity": "sha512-TBoF4buBGYhXjdZAryayY2TrkQj2B2KfE/msG4V53XCt+w0EhEwM2JRjx8p2grJ2C6gtH5++SAwEvGMRdi0yyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/credential-provider-env": "^3.972.48", + "@aws-sdk/credential-provider-http": "^3.972.50", + "@aws-sdk/credential-provider-login": "^3.972.54", + "@aws-sdk/credential-provider-process": "^3.972.48", + "@aws-sdk/credential-provider-sso": "^3.972.54", + "@aws-sdk/credential-provider-web-identity": "^3.972.54", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.54.tgz", + "integrity": "sha512-hBWI3wZTdTGiuMfmPts6AWbAjFfRniOQnqx68tc2cQvRKWawFbN9wkLOVPWM1FAOyowZU73mC6Fi+rHSHNyLFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.57.tgz", + "integrity": "sha512-u6dClpzNdWf1HGWz4wwhdXi1wiOofCLniM9S4BQQGlLAN9TW7VB+ld5V533GdKrYMaFeBGFqKnj0JCYvynLqwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.48", + "@aws-sdk/credential-provider-http": "^3.972.50", + "@aws-sdk/credential-provider-ini": "^3.972.55", + "@aws-sdk/credential-provider-process": "^3.972.48", + "@aws-sdk/credential-provider-sso": "^3.972.54", + "@aws-sdk/credential-provider-web-identity": "^3.972.54", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.48.tgz", + "integrity": "sha512-w6VZwojPt12WnEkAUy6Nu4K6sWCbBmR7QX390b0nE6vRvkXbrYr9Lq9VySGkfjiMjpUA87op+J4EgvRmtWIDoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.54.tgz", + "integrity": "sha512-23uZpIpF2SIFDCa1fcWa202tK4gGeyvX6GIIAjiB8WBsvsVRBMnJ/7dCxHzxf7eZT7GToJg837LDIBnZsl/VUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/token-providers": "3.1071.0", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1071.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1071.0.tgz", + "integrity": "sha512-4LDW2Qob6LoLFuqYSYZq2AyTE9koSE9+i+n5UZcm10GpmQOK0zRD9L4uYlzItiTKksIWgC/qMFChAi3RvKYtMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.54.tgz", + "integrity": "sha512-0Iv5QttS6wcATlodYKgvQj6B9Db51rx7NU9fqu0PoLeS4BIgdYMc/QK4smwLwpm5RFrs02V/eLyEFp3FklvlNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.22.tgz", + "integrity": "sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.18.tgz", + "integrity": "sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.30.tgz", + "integrity": "sha512-kH6N4f/Fzi9r/dYap8EQ+Zk4NOz8pl4AtWKhzAoG2C1/4YkIHok9APp/e+75woreWQq264n+LkrJsJVZ0Q+M1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.22.tgz", + "integrity": "sha512-4IwtcYSxEIVw5hcp8ogq0CMbFNZFw7jJUetpfFUhFFeqsa1K8j2Ihg2hnxLyOp3stMZnXda6VzOmPi1AFZQXcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.35", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.1.tgz", + "integrity": "sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz", + "integrity": "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.13.tgz", + "integrity": "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.30.tgz", + "integrity": "sha512-StElZPEoBquWwNqw1AcfpzEyZqJvFxouG+mpDNYlcH6ZOrqd2CuIryv+8LV8gNHZUOyKyJF3Dq9vxaXEmDR9TQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", + "integrity": "sha512-ZpSwaD7oNpsjn9vtEatZQNT9PSdDJXi6rFeY5Qv+OHQGFDKlmcrfJE4ypm4SAc/fBECPs4Rdi3l+YjVtXYrkKw==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-ai/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.8.tgz", + "integrity": "sha512-wr9oTS/yrwURDXnYrONQgFgV7QDlwslXL/rvKU5X7TRtrGxIhippsRApXqYlRwSeMjb2YzgHMfZ/kAhOqrzoFQ==", + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.79.8", + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.1.38", + "undici": "8.5.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.8.tgz", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.79.8", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "./dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/@earendil-works/pi-tui": { + "version": "0.79.8", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", + "integrity": "sha512-QerB+0wUc6eEO8MwvzOQGtzcsbwo6y8VvdxYU6vGcakz6ofJZWhrmwrknp1dCGx3bEtCf+siUIxEzkqvFCzIsg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@smithy/core": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.25.1.tgz", + "integrity": "sha512-zpDbpXBCBsxfLtG2GEUyfgvHvSFrw5CwDZSNzL0v52gx/c3oPlPbm+7W7num8xs6vyiUBn+bvYPHcQDOXZynCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.1.tgz", + "integrity": "sha512-TSAF5NHgxEsllbErYWbK8aLnl5L601NGc5VYJlSPsKnf3YlkhdoBN+geGcaU00oiw2OK3QO5LA3QNXiiWhCidQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.5.1.tgz", + "integrity": "sha512-96JrD1q71anokymx9Iblb+zKmNQYNstlV/25A9ZYIJ2A0rp1r7/GZAIm0bDWSmVvz3DpNOCZuabzsiL+w0UHhw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.5.1.tgz", + "integrity": "sha512-X9rVls3En0z3NtrmguTmpRM0/NqtWUxBjal6fcAkwtsub+gOdLZ6kD+V7xhUgFMGdG14bHbZ7M5QjaRI1+DatQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.0.tgz", + "integrity": "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.5.0.tgz", + "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/google-auth-library": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neverthrow": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz", + "integrity": "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.24.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typebox": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.17.tgz", + "integrity": "sha512-FHB10V5OI+MBKQ9N4CS4FTpdTiPwjAwkS9cjXt4uBULFE2zNOYu4A6iAR4GZVPcujSD6uCQjY1fqnOsjn/SLPQ==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/home/.pi/agent/extensions/package.json b/home/.pi/agent/extensions/package.json new file mode 100644 index 00000000..e6f37c5b --- /dev/null +++ b/home/.pi/agent/extensions/package.json @@ -0,0 +1,24 @@ +{ + "name": "extensions", + "version": "1.0.0", + "type": "module", + "scripts": { + "check": "tsc -p tsconfig.json && eslint && node --test '**/tests/*.test.ts'" + }, + "dependencies": { + "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-coding-agent": "^0.79.8", + "@earendil-works/pi-tui": "^0.79.8", + "execa": "^9.6.1", + "neverthrow": "^8.2.0", + "typebox": "^1.2.17", + "zod": "^4.4.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^26.0.0", + "eslint": "^10.5.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.61.1" + } +} diff --git a/home/.pi/agent/extensions/subagent/agents.ts b/home/.pi/agent/extensions/subagent/agents.ts deleted file mode 100644 index f8a1934d..00000000 --- a/home/.pi/agent/extensions/subagent/agents.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Minimal user-level subagent discovery. - * - * Agents live in ~/.pi/agent/agents/*.md and use simple YAML frontmatter: - * --- - * name: scout - * description: Fast read-only codebase recon - * tools: read, grep, find, ls, bash - * model: optional-model-id - * --- - */ - -import * as fs from "node:fs"; -import * as path from "node:path"; -import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; - -export interface AgentConfig { - name: string; - description: string; - tools?: string[]; - model?: string; - systemPrompt: string; - filePath: string; -} - -export function discoverAgents(): AgentConfig[] { - const dir = path.join(getAgentDir(), "agents"); - let entries: fs.Dirent[]; - try { - entries = fs.readdirSync(dir, { withFileTypes: true }); - } catch { - return []; - } - - const agents: AgentConfig[] = []; - for (const entry of entries) { - if (!entry.name.endsWith(".md")) continue; - if (!entry.isFile() && !entry.isSymbolicLink()) continue; - - const filePath = path.join(dir, entry.name); - let content: string; - try { - content = fs.readFileSync(filePath, "utf8"); - } catch { - continue; - } - - const { frontmatter, body } = parseFrontmatter>(content); - if (!frontmatter.name || !frontmatter.description) continue; - - const tools = frontmatter.tools - ?.split(",") - .map((tool) => tool.trim()) - .filter(Boolean); - - agents.push({ - name: frontmatter.name, - description: frontmatter.description, - tools: tools?.length ? tools : undefined, - model: frontmatter.model?.trim() || undefined, - systemPrompt: body.trim(), - filePath, - }); - } - - return agents.sort((a, b) => a.name.localeCompare(b.name)); -} - -export function formatAgentList(agents: AgentConfig[]): string { - return agents.map((agent) => `${agent.name}: ${agent.description}`).join("; ") || "none"; -} diff --git a/home/.pi/agent/extensions/subagent/index.ts b/home/.pi/agent/extensions/subagent/index.ts deleted file mode 100644 index b2525485..00000000 --- a/home/.pi/agent/extensions/subagent/index.ts +++ /dev/null @@ -1,320 +0,0 @@ -/** - * Minimal Subagent Tool - * - * Runs one user-level agent from ~/.pi/agent/agents/*.md in an isolated - * `pi --mode json --print --no-session` subprocess and returns its final text. - * Intentionally single-agent only: no project-local prompts, no chaining, no - * custom renderer. Keep orchestration in the main conversation unless repeated - * use proves more is needed. - */ - -import { spawn } from "node:child_process"; -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import type { Message } from "@earendil-works/pi-ai"; -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { Text } from "@earendil-works/pi-tui"; -import { Type } from "typebox"; -import { discoverAgents, formatAgentList } from "./agents.ts"; - -interface SubagentDetails { - agent: string; - task: string; - exitCode: number; - model?: string; - cwd?: string; - messages: Message[]; - stderr: string; - aborted: boolean; -} - -const SubagentParams = Type.Object({ - agent: Type.String({ description: "Name of the user-level agent to run" }), - task: Type.String({ description: "Self-contained task to delegate" }), - cwd: Type.Optional(Type.String({ description: "Working directory for the child agent. Defaults to current cwd." })), -}); - -function getFinalText(messages: Message[]): string { - for (let i = messages.length - 1; i >= 0; i--) { - const message = messages[i]; - if (message.role !== "assistant") continue; - for (const part of message.content) { - if (part.type === "text" && part.text.trim()) return part.text.trim(); - } - } - return ""; -} - -type DisplayItem = { type: "text"; text: string } | { type: "toolCall"; name: string; args: Record }; - -function getDisplayItems(messages: Message[]): DisplayItem[] { - const items: DisplayItem[] = []; - for (const message of messages) { - if (message.role !== "assistant") continue; - for (const part of message.content) { - if (part.type === "text" && part.text.trim()) { - items.push({ type: "text", text: part.text.trim() }); - } else if (part.type === "toolCall") { - items.push({ type: "toolCall", name: part.name, args: part.arguments }); - } - } - } - return items; -} - -function taskPreview(task: string): string { - const singleLine = task.replace(/\s+/g, " ").trim(); - return singleLine.length > 100 ? `${singleLine.slice(0, 97)}...` : singleLine; -} - -function shortenPath(filePath: string): string { - const home = os.homedir(); - return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath; -} - -function formatToolCall(item: Extract): string { - switch (item.name) { - case "bash": { - const command = typeof item.args.command === "string" ? item.args.command : "..."; - const preview = command.replace(/\s+/g, " ").trim(); - return `$ ${preview.length > 80 ? `${preview.slice(0, 77)}...` : preview}`; - } - case "read": - case "write": - case "edit": { - const filePath = item.args.path ?? item.args.file_path ?? "..."; - return `${item.name} ${shortenPath(String(filePath))}`; - } - default: - return item.name; - } -} - -function formatDisplayItem(item: DisplayItem): string { - if (item.type === "toolCall") return `→ ${formatToolCall(item)}`; - return item.text; -} - -function getUpdateText(details: SubagentDetails): string { - const items = getDisplayItems(details.messages); - const latest = items.at(-1); - if (latest) return `⏳ ${details.agent}: ${formatDisplayItem(latest)}`; - return `⏳ ${details.agent} running...`; -} - -function getPiInvocation(args: string[]): { command: string; args: string[] } { - const currentScript = process.argv[1]; - const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/"); - if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) { - return { command: process.execPath, args: [currentScript, ...args] }; - } - - const execName = path.basename(process.execPath).toLowerCase(); - const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName); - if (!isGenericRuntime) return { command: process.execPath, args }; - - return { command: "pi", args }; -} - -async function writeTempPrompt(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> { - const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-")); - const safeName = agentName.replace(/[^\w.-]+/g, "_"); - const filePath = path.join(dir, `${safeName}.md`); - await fs.promises.writeFile(filePath, prompt, { encoding: "utf8", mode: 0o600 }); - return { dir, filePath }; -} - -async function runAgent(params: { - defaultCwd: string; - agentName: string; - task: string; - cwd?: string; - signal?: AbortSignal; - onUpdate?: (text: string, details: SubagentDetails) => void; -}): Promise { - const agents = discoverAgents(); - const agent = agents.find((candidate) => candidate.name === params.agentName); - if (!agent) { - return { - agent: params.agentName, - task: params.task, - exitCode: 1, - messages: [], - stderr: `Unknown agent "${params.agentName}". Available agents: ${formatAgentList(agents)}.`, - aborted: false, - }; - } - - const args = ["--mode", "json", "--print", "--no-session"]; - if (agent.model) args.push("--model", agent.model); - if (agent.tools?.length) args.push("--tools", agent.tools.join(",")); - - let tempDir: string | undefined; - let promptPath: string | undefined; - if (agent.systemPrompt) { - const temp = await writeTempPrompt(agent.name, agent.systemPrompt); - tempDir = temp.dir; - promptPath = temp.filePath; - args.push("--append-system-prompt", promptPath); - } - args.push(`Task: ${params.task}`); - - const details: SubagentDetails = { - agent: agent.name, - task: params.task, - exitCode: 0, - model: agent.model, - cwd: params.cwd, - messages: [], - stderr: "", - aborted: false, - }; - - const emitUpdate = () => params.onUpdate?.(getUpdateText(details), details); - - try { - const invocation = getPiInvocation(args); - emitUpdate(); - details.exitCode = await new Promise((resolve) => { - const proc = spawn(invocation.command, invocation.args, { - cwd: params.cwd ?? params.defaultCwd, - stdio: ["ignore", "pipe", "pipe"], - }); - - let buffer = ""; - const processLine = (line: string) => { - if (!line.trim()) return; - let event: { type?: string; message?: Message }; - try { - event = JSON.parse(line); - } catch { - return; - } - - if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) { - details.messages.push(event.message); - emitUpdate(); - } - }; - - proc.stdout.on("data", (chunk) => { - buffer += chunk.toString(); - const lines = buffer.split("\n"); - buffer = lines.pop() ?? ""; - for (const line of lines) processLine(line); - }); - - proc.stderr.on("data", (chunk) => { - details.stderr += chunk.toString(); - }); - - proc.on("close", (code) => { - if (buffer.trim()) processLine(buffer); - resolve(code ?? 0); - }); - - proc.on("error", (error) => { - details.stderr += error.message; - resolve(1); - }); - - const abort = () => { - details.aborted = true; - proc.kill("SIGTERM"); - setTimeout(() => { - if (!proc.killed) proc.kill("SIGKILL"); - }, 5000).unref(); - }; - - if (params.signal?.aborted) abort(); - else params.signal?.addEventListener("abort", abort, { once: true }); - }); - } finally { - if (promptPath) await fs.promises.rm(promptPath, { force: true }); - if (tempDir) await fs.promises.rm(tempDir, { force: true, recursive: true }); - } - - return details; -} - -export default function (pi: ExtensionAPI) { - pi.registerTool({ - name: "subagent", - label: "Subagent", - description: "Delegate one self-contained task to a user-level subagent with isolated context. Agents are loaded from ~/.pi/agent/agents/*.md.", - promptSnippet: "Delegate narrow read-only reconnaissance or review to an isolated subagent", - promptGuidelines: [ - "Use subagent for broad read-only reconnaissance or focused review that would otherwise pollute the main context.", - "Pass a self-contained task with the relevant goal, paths, constraints, and expected output.", - "Prefer one subagent call over multi-step orchestration unless the user explicitly asks for more.", - ], - parameters: SubagentParams, - - async execute(_toolCallId, params, signal, onUpdate, ctx) { - const details = await runAgent({ - defaultCwd: ctx.cwd, - agentName: params.agent, - task: params.task, - cwd: params.cwd, - signal, - onUpdate: onUpdate - ? (text, details) => onUpdate({ content: [{ type: "text", text }], details }) - : undefined, - }); - - const finalText = getFinalText(details.messages); - if (details.aborted) { - return { content: [{ type: "text" as const, text: "Subagent aborted." }], details, isError: true }; - } - if (details.exitCode !== 0) { - const message = details.stderr.trim() || finalText || `Subagent exited with code ${details.exitCode}.`; - return { content: [{ type: "text" as const, text: message }], details, isError: true }; - } - return { content: [{ type: "text" as const, text: finalText || "(no output)" }], details }; - }, - - renderCall(args, theme, _context) { - let text = theme.fg("toolTitle", theme.bold("subagent ")) + theme.fg("accent", args.agent); - if (args.cwd) text += theme.fg("muted", ` in ${shortenPath(args.cwd)}`); - text += `\n${theme.fg("dim", ` ${taskPreview(args.task)}`)}`; - return new Text(text, 0, 0); - }, - - renderResult(result, { expanded }, theme, _context) { - const details = result.details as SubagentDetails | undefined; - if (!details) { - const content = result.content[0]; - return new Text(content?.type === "text" ? content.text : "(no output)", 0, 0); - } - - const failed = details.aborted || details.exitCode !== 0; - const icon = failed ? theme.fg("error", "✗") : theme.fg("success", "✓"); - let text = `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))}`; - if (details.model) text += theme.fg("muted", ` (${details.model})`); - if (details.cwd) text += theme.fg("muted", ` in ${shortenPath(details.cwd)}`); - - if (expanded) text += `\n${theme.fg("dim", `Task: ${details.task}`)}`; - - const toolCalls = getDisplayItems(details.messages).filter((item) => item.type === "toolCall"); - const shownToolCalls = expanded ? toolCalls : toolCalls.slice(-8); - if (toolCalls.length > shownToolCalls.length) { - text += `\n${theme.fg("muted", `... ${toolCalls.length - shownToolCalls.length} earlier actions`)}`; - } - for (const item of shownToolCalls) { - text += `\n${theme.fg("muted", formatDisplayItem(item))}`; - } - - const finalText = getFinalText(details.messages); - if (finalText) { - const preview = expanded ? finalText : finalText.split("\n").slice(0, 6).join("\n"); - text += `\n\n${theme.fg("toolOutput", preview)}`; - } else if (!failed) { - text += `\n${theme.fg("muted", "(no output)")}`; - } - - if (failed && details.stderr.trim()) text += `\n${theme.fg("error", details.stderr.trim())}`; - return new Text(text, 0, 0); - }, - }); -} diff --git a/home/.pi/agent/extensions/subagents/agents.ts b/home/.pi/agent/extensions/subagents/agents.ts new file mode 100644 index 00000000..b22fd5aa --- /dev/null +++ b/home/.pi/agent/extensions/subagents/agents.ts @@ -0,0 +1,124 @@ +/** + * Subagent role discovery. + * + * Agents live in ~/.pi/agent/extensions/subagents/agents/*.md and use the + * same YAML frontmatter as the user's other pi agents: + * + * --- + * name: scout + * description: Fast read-only codebase recon + * tools: read, grep, find, ls, bash + * model: optional-model-id + * --- + * + * "name" is the value the parent model passes to spawn_agent(agent_type=...). + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"; +import { err, ok, type Result } from "neverthrow"; +import { z } from "zod"; + +export interface AgentConfig { + name: string; + description: string; + tools?: string[]; + model?: string; + systemPrompt: string; + filePath: string; +} + +// ── trust boundary: agent frontmatter ────────────────────────────── +// name + description are required; tools is a comma-list, model is optional. +export const AgentFrontmatterSchema = z.object({ + name: z.string().min(1), + description: z.string().min(1), + tools: z.string().optional(), + model: z.string().optional(), +}); + +/** Why discovery could fail. Surfaced to the caller; never silently []. */ +export type DiscoverError = + | { kind: "read_dir"; dir: string; cause: NodeJS.ErrnoException } + | { kind: "empty"; dir: string }; + +const AGENTS_DIR = path.join(getAgentDir(), "extensions", "subagents", "agents"); + +export function agentsDir(): string { + return AGENTS_DIR; +} + +/** + * Read every `*.md` in the agents dir, parse frontmatter, and return the + * valid ones sorted by name. Returns Err on an unreadable dir (permission, + * IO) or when no usable agent was found at all (so the caller can give a + * useful error instead of a confusing "unknown type" on the first spawn). + */ +export function discoverAgents(): Result { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(AGENTS_DIR, { withFileTypes: true }); + } catch (cause) { + return err({ kind: "read_dir", dir: AGENTS_DIR, cause: cause as NodeJS.ErrnoException }); + } + + const agents: AgentConfig[] = []; + for (const entry of entries) { + if (!entry.name.endsWith(".md")) continue; + if (!entry.isFile() && !entry.isSymbolicLink()) continue; + + const filePath = path.join(AGENTS_DIR, entry.name); + // Per-file read/parse errors skip the file, not the whole dir — one + // broken agent file shouldn't hide the rest. + let content: string; + try { + content = fs.readFileSync(filePath, "utf8"); + } catch { + continue; + } + + const parsed = parseAgentFile(filePath, content); + if (parsed) agents.push(parsed); + } + + if (agents.length === 0) return err({ kind: "empty", dir: AGENTS_DIR }); + + agents.sort((a, b) => a.name.localeCompare(b.name)); + return ok(agents); +} + +function parseAgentFile(filePath: string, content: string): AgentConfig | undefined { + const { frontmatter, body } = parseFrontmatter(content); + const parsed = AgentFrontmatterSchema.safeParse(frontmatter); + if (!parsed.success) return undefined; + + const fm = parsed.data; + const tools = fm.tools + ?.split(",") + .map((t) => t.trim()) + .filter(Boolean); + + return { + name: fm.name.trim(), + description: fm.description.trim(), + tools: tools?.length ? tools : undefined, + model: fm.model?.trim() || undefined, + systemPrompt: body.trim(), + filePath, + }; +} + +/** One-line "name: description" list for error messages. */ +export function formatAgentList(agents: AgentConfig[]): string { + return agents.map((a) => `${a.name}: ${a.description}`).join("; ") || "none"; +} + +/** Find a role by name. Err carries the full list so callers can render it. */ +export function resolveAgent( + agents: AgentConfig[], + name: string, +): Result { + const found = agents.find((a) => a.name === name); + return found ? ok(found) : err({ requested: name, available: agents }); +} diff --git a/home/.pi/agent/extensions/subagents/agents/default.md b/home/.pi/agent/extensions/subagents/agents/default.md new file mode 100644 index 00000000..b8fef420 --- /dev/null +++ b/home/.pi/agent/extensions/subagents/agents/default.md @@ -0,0 +1,6 @@ +--- +name: default +description: Default codex role — no overrides, child uses the same model/tools as the parent. +--- + +You are a codex-style subagent. You have no parent context; treat the task you received as the entire conversation so far. Work it to completion and return the result. Do not ask clarifying questions — make a reasonable choice and document the assumption. diff --git a/home/.pi/agent/agents/reviewer.md b/home/.pi/agent/extensions/subagents/agents/reviewer.md similarity index 97% rename from home/.pi/agent/agents/reviewer.md rename to home/.pi/agent/extensions/subagents/agents/reviewer.md index e800486e..dfa68ad1 100644 --- a/home/.pi/agent/agents/reviewer.md +++ b/home/.pi/agent/extensions/subagents/agents/reviewer.md @@ -1,8 +1,6 @@ --- name: reviewer description: Focused read-only review for correctness risks, future compatibility, and deviations from current structure or standard patterns -tools: read, grep, find, ls, bash -model: tokenrouter/MiniMax-M3:medium --- You are a focused reviewer lens for Pi escalation. Review only the task, diff, or files requested. diff --git a/home/.pi/agent/agents/scout.md b/home/.pi/agent/extensions/subagents/agents/scout.md similarity index 87% rename from home/.pi/agent/agents/scout.md rename to home/.pi/agent/extensions/subagents/agents/scout.md index 27ca309a..bc46879f 100644 --- a/home/.pi/agent/agents/scout.md +++ b/home/.pi/agent/extensions/subagents/agents/scout.md @@ -1,8 +1,8 @@ --- name: scout -description: Fast read-only codebase recon that returns compressed context -tools: read, grep, find, ls, bash -model: tokenrouter/MiniMax-M3:medium +description: Fast read-only codebase recon that returns compressed context. Only for discovery, no analysis or verification. +tools: read, bash, fffind, ffgrep +model: openmodel/deepseek-v4-flash --- You are a scout. Quickly investigate a codebase and return compressed findings that another agent can use without re-reading everything. diff --git a/home/.pi/agent/agents/worker.md b/home/.pi/agent/extensions/subagents/agents/worker.md similarity index 92% rename from home/.pi/agent/agents/worker.md rename to home/.pi/agent/extensions/subagents/agents/worker.md index 6fb64aef..233be62d 100644 --- a/home/.pi/agent/agents/worker.md +++ b/home/.pi/agent/extensions/subagents/agents/worker.md @@ -4,7 +4,7 @@ description: General-purpose worker for scoped coding tasks; full tools, returns model: tokenrouter/MiniMax-M3:medium --- -You are a worker dispatched by the supervisor. Complete the assigned scoped task. +You are a worker dispatched by a supervisor. Complete the assigned scoped task. Rules: - Stay strictly within the task scope. Do not modify files outside the scope of the assigned change. @@ -16,7 +16,7 @@ Rules: - If the task is ambiguous, state the ambiguity and the assumption you made, then proceed. Do not block on permission to proceed. - If you discover the task is impossible or out of scope, say so explicitly with evidence (what you tried, what blocked you). Do not silently give up. -Output exactly: +Use the following output if not instructed otherwise: ## Task - Restate the task in one line, with the gate (command + expected outcome) that must pass. diff --git a/home/.pi/agent/extensions/subagents/index.ts b/home/.pi/agent/extensions/subagents/index.ts new file mode 100644 index 00000000..e246b78c --- /dev/null +++ b/home/.pi/agent/extensions/subagents/index.ts @@ -0,0 +1,170 @@ +/** + * spawn_agent — delegate a self-contained task to an isolated subagent. + * + * Runs the spawned agent as a `pi --mode json --print --no-session` child + * process with its own context, model, and tools, and returns the child's + * final text. Codex-compatible arg shape. Depth capped at 3 (tracked across + * the process tree via CODEX_SUBAGENT_DEPTH). + * + * Architecture: this is the only module that crosses into pi's tool world. + * It composes Results from agents.ts / process.ts and converts Err → throw + * at the boundary (pi's runtime marks `isError: true` only on thrown errors; + * a returned `isError` field is silently dropped — see agent-loop.js). + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Container, Text } from "@earendil-works/pi-tui"; +import { Type } from "typebox"; +import { discoverAgents, formatAgentList, resolveAgent } from "./agents.ts"; +import { getFinalText, runSubprocess, type RunDetails, type SpawnError } from "./process.ts"; +import { manageTick, renderCallHeader, renderResultBlock } from "./render.ts"; + +const MAX_DEPTH = 3; +const DEPTH_ENV = "CODEX_SUBAGENT_DEPTH"; + +const SpawnParams = Type.Object({ + message: Type.String({ description: "The specific task for this spawn. The agent's role (its system prompt) defines how it works; this is the instance of work to do. Self-contained — the child has no parent history." }), + task_name: Type.Optional(Type.String({ description: "Short label for UI and logs. Omit to derive from the message." })), + agent_type: Type.Optional(Type.String({ description: "Name of the subagent role to use. Omit for the default role." })), + reasoning_effort: Type.Optional(Type.Union([ + Type.Literal("none"), + Type.Literal("minimal"), + Type.Literal("low"), + Type.Literal("medium"), + Type.Literal("high"), + Type.Literal("xhigh"), + ], { description: "Override reasoning effort for the child." })), + fork_turns: Type.Optional(Type.String({ description: "Context inheritance mode. Currently only 'none' (default, fresh context) is supported." })), + cwd: Type.Optional(Type.String({ description: "Working directory for the child agent. Defaults to current cwd." })), +}); + +export default function (_pi: ExtensionAPI) { + // Discover agents at registration time so the model sees the current + // name+description list, not a hand-written hint that drifts the moment + // someone adds a new agents/*.md. A broken agents dir fails loud at load + // — a silently registered tool that always errors is worse than no tool. + const agents = discoverAgents().match( + (list) => list, + (e) => { + throw new Error(discoveryErrorMessage(e)); + }, + ); + const agentList = agents + .map((a) => `- **${a.name}** — ${a.description}`) + .join("\n"); + + // Per-row tick intervals, keyed by toolCallId. A single shared slot would + // let two concurrent spawns clobber each other's interval. + const ticks = new Map(); + + _pi.registerTool({ + name: "spawn_agent", + label: "Spawn Agent", + description: "Spawn an isolated subagent with its own context, model, and tools. Returns the child's final text. Depth capped at 3.", + promptSnippet: "Delegate a self-contained task to an isolated subagent with its own context, model, and tool surface", + promptGuidelines: [ + "Use spawn_agent to delegate a self-contained task to an isolated subagent with its own context, model, and tools.", + `Available agent types:\n${agentList}\n\nOmit \`agent_type\` for the default role (no overrides).`, + "Pass a self-contained message: the child has no parent history. Reference exact paths, file:line ranges, and the output shape you want.", + "Prefer a single spawn_agent over multi-step orchestration. Depth is capped at 3.", + ], + parameters: SpawnParams, + + async execute(_toolCallId, params, signal, onUpdate, ctx) { + // ── validate the request (pre-spawn) ── + // Each check throws a targeted message; pi catches and marks isError. + const parentDepth = Number.parseInt(process.env[DEPTH_ENV] ?? "0", 10) || 0; + if (parentDepth >= MAX_DEPTH) { + throw new Error(`spawn_agent depth ${parentDepth} >= max ${MAX_DEPTH}.`); + } + if (params.fork_turns && params.fork_turns !== "none") { + throw new Error(`fork_turns='${params.fork_turns}' is not yet implemented. Use 'none' (default) for a fresh-context child.`); + } + + const agents = discoverAgents().match( + (list) => list, + (e) => { + throw new Error(discoveryErrorMessage(e)); + }, + ); + + const requestedType = params.agent_type?.trim() || "default"; + const agent = resolveAgent(agents, requestedType).match( + (a) => a, + (e) => { + throw new Error(`Unknown agent_type '${e.requested}'. Available: ${formatAgentList(e.available)}.`); + }, + ); + + const [provider, modelId] = (agent.model ?? "").split("/"); + const contextWindow = provider && modelId + ? ctx.modelRegistry.find(provider, modelId)?.contextWindow + : undefined; + + // ── run ── + const result = await runSubprocess({ + defaultCwd: ctx.cwd, + agent, + message: params.message, + taskName: params.task_name?.trim() || params.message.slice(0, 60), + reasoningEffortOverride: params.reasoning_effort, + cwd: params.cwd, + parentDepth, + signal, + onUpdate: onUpdate + ? (d) => { + d.contextWindow = contextWindow; + onUpdate({ content: [{ type: "text", text: "(running…)" }], details: d }); + } + : undefined, + }); + + // ── resolve (Err → throw, so pi marks isError) ── + const details = result.match( + (d) => d, + (e) => { + throw new Error(spawnErrorMessage(e)); + }, + ); + + const finalText = getFinalText(details.messages); + return { content: [{ type: "text" as const, text: finalText || "(no output)" }], details }; + }, + + renderCall(args, theme, context) { + const c = context.lastComponent instanceof Container + ? (context.lastComponent.clear(), context.lastComponent) + : new Container(); + renderCallHeader(c, args, context.expanded, theme); + return c; + }, + + renderResult(result, options, theme, context) { + const details = result.details as RunDetails | undefined; + if (!details) { + const t = result.content[0]; + return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0); + } + + // Live tick: while partial, kick a 1Hz interval so the elapsed + // counter advances even with no new tool events. + manageTick(ticks, context.toolCallId, options.isPartial, () => context.invalidate()); + + return renderResultBlock(details, options, theme); + }, + }); +} + +function discoveryErrorMessage(e: { kind: string; dir: string; cause?: NodeJS.ErrnoException }): string { + if (e.kind === "read_dir") { + return `Could not read agents dir ${e.dir}: ${e.cause?.message ?? e.cause?.code ?? "unknown error"}.`; + } + return `No agent files found in ${e.dir}. Create one as .md with frontmatter name + description.`; +} + +function spawnErrorMessage(e: SpawnError): string { + const details = e.details; + const finalText = getFinalText(details.messages); + if (e.kind === "aborted") return "spawn_agent aborted."; + return details.stderr.trim() || finalText || `Subagent exited with code ${details.exitCode}.`; +} diff --git a/home/.pi/agent/extensions/subagents/process.ts b/home/.pi/agent/extensions/subagents/process.ts new file mode 100644 index 00000000..b7d3b3a5 --- /dev/null +++ b/home/.pi/agent/extensions/subagents/process.ts @@ -0,0 +1,322 @@ +/** + * Child subprocess management for spawn_agent. + * + * Runs the spawned agent as an isolated `pi --mode json --print --no-session` + * subprocess — same pattern as pi's built-in bash tool. Streams JSON events + * off stdout, aggregates usage/tool/text state, and emits throttled UI + * updates. ResultAsync wraps the spawn so the tool layer's only job is to + * resolve the agent and render the outcome. + */ + +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { execa } from "execa"; +import type { Message } from "@earendil-works/pi-ai"; +import { errAsync, okAsync, ResultAsync } from "neverthrow"; +import type { AgentConfig } from "./agents.ts"; +import { z } from "zod"; + +const DEPTH_ENV = "CODEX_SUBAGENT_DEPTH"; +const UPDATE_THROTTLE_MS = 150; + +// ── trust boundary: the child's JSON event stream ─────────────────── +const UsageSchema = z.object({ + input: z.number().optional(), + output: z.number().optional(), + cacheRead: z.number().optional(), + cacheWrite: z.number().optional(), + totalTokens: z.number().optional(), + cost: z.object({ total: z.number().optional() }).optional(), +}); + +const ContentPartSchema = z.union([ + z.object({ type: z.literal("text"), text: z.string() }), + z.object({ type: z.literal("toolCall"), name: z.string(), arguments: z.unknown() }), + z.looseObject({ type: z.string() }), // roles/parts we don't read +]); + +const MessageSchema = z.object({ + role: z.string(), + content: z.array(ContentPartSchema), + usage: UsageSchema.optional(), +}); + +const EventSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("message_end"), message: MessageSchema }), + z.object({ type: z.literal("tool_result_end"), message: MessageSchema }), +]); + + + + +export interface RunUsage { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + cost: number; + turns: number; +} + +/** + * Mutable run state. Reused across the run: the JSON parser mutates it in + * place and the render layer reads from it on every throttled update. + */ +export interface RunDetails { + agent: string; + taskName: string; + model?: string; + depth: number; + exitCode: number; + messages: Message[]; + stderr: string; + aborted: boolean; + startTime: number; + endTime?: number; + toolCount: number; + recentTools: Array<{ name: string; argsPreview: string }>; + lastMessage: string; + tokens: number; // latest-turn snapshot for the context-window gauge + usage: RunUsage; + contextWindow?: number; +} + +/** Why a run could fail. Non-zero exit / abort become SpawnError; the run + * details are attached so the tool layer can still render what happened. */ +export type SpawnError = + | { kind: "exit"; details: RunDetails } + | { kind: "aborted"; details: RunDetails }; + +export interface RunParams { + defaultCwd: string; + agent: AgentConfig; + message: string; + taskName: string; + reasoningEffortOverride?: string; + cwd?: string; + parentDepth: number; + signal?: AbortSignal; + onUpdate?: (details: RunDetails) => void; +} + +/** Spawns the child and resolves Ok on a clean (exit 0) run, Err otherwise. */ +export function runSubprocess(params: RunParams): ResultAsync { + const details = initDetails(params); + // runAndCollect never rejects — failures land in details.aborted/exitCode — + // so fromSafePromise is the honest wrapper (no synthetic error type). + return ResultAsync.fromSafePromise(runAndCollect(params, details)).andThen((d) => { + if (d.aborted) return errAsync({ kind: "aborted" as const, details: d }); + if (d.exitCode !== 0) return errAsync({ kind: "exit" as const, details: d }); + return okAsync(d); + }); +} + +function initDetails(params: RunParams): RunDetails { + const childDepth = params.parentDepth + 1; + return { + agent: params.agent.name, + taskName: params.taskName, + model: params.agent.model, + depth: childDepth, + exitCode: 0, + messages: [], + stderr: "", + aborted: false, + startTime: Date.now(), + toolCount: 0, + recentTools: [], + lastMessage: "", + tokens: 0, + usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }, + }; +} + +async function runAndCollect(params: RunParams, details: RunDetails): Promise { + const args = ["--mode", "json", "--print", "--no-session"]; + if (details.model) args.push("--model", details.model); + if (params.reasoningEffortOverride) args.push("--reasoning-effort", params.reasoningEffortOverride); + if (params.agent.tools?.length) args.push("--tools", params.agent.tools.join(",")); + + let tempDir: string | undefined; + if (params.agent.systemPrompt) { + const written = await writeTempPrompt(params.agent.name, params.agent.systemPrompt); + tempDir = written.dir; + args.push("--append-system-prompt", written.path); + } + args.push(`Task: ${params.message}`); + + const env = { ...process.env, [DEPTH_ENV]: String(details.depth) }; + const throttle = createThrottle(UPDATE_THROTTLE_MS); + const emit = () => params.onUpdate?.(details); + + const invocation = getPiInvocation(args); + + // execa handles the abort→SIGTERM→SIGKILL(5s) escalation, env merge, + // spawn-error capture, and parent-exit cleanup for us. We just iterate + // stdout lines (lines: stdout-only) and read exitCode/stderr at the end. + const subprocess = execa(invocation.command, invocation.args, { + cwd: params.cwd ?? params.defaultCwd, + env: env as Record, + stdin: "ignore", + stdout: "pipe", + stderr: "pipe", + lines: { stdout: true, stderr: false }, + reject: false, + cancelSignal: params.signal, + }); + + try { + emit(); + for await (const line of subprocess) { + ingestLine(line, details); + throttle.schedule(emit); + } + const result = await subprocess; + details.exitCode = result.exitCode ?? 1; + const stderr = result.stderr; + details.stderr = typeof stderr === "string" ? stderr : ""; + if (params.signal?.aborted) details.aborted = true; + } catch (error) { + // runAndCollect must never reject — runSubprocess wraps it in + // ResultAsync.fromSafePromise. Fold any execa/iterator error into + // details so the tool layer still renders something. + details.exitCode = 1; + details.stderr = String(error instanceof Error ? error.message : error); + if (params.signal?.aborted) details.aborted = true; + } finally { + details.endTime = Date.now(); + throttle.flush(emit); + if (tempDir) await fs.promises.rm(tempDir, { force: true, recursive: true }); + } + + return details; +} + +async function writeTempPrompt(agentName: string, systemPrompt: string): Promise<{ dir: string; path: string }> { + const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "subagent-")); + const safeName = agentName.replace(/[^\w.-]+/g, "_"); + const filePath = path.join(dir, `${safeName}.md`); + await fs.promises.writeFile(filePath, systemPrompt, { encoding: "utf8", mode: 0o600 }); + return { dir, path: filePath }; +} + +/** Parse one JSON event line and fold it into the run details. */ +function ingestLine(line: string, details: RunDetails): void { + if (!line.trim()) return; + let raw: unknown; + try { + raw = JSON.parse(line); + } catch { + return; + } + + const parsed = EventSchema.safeParse(raw); + if (!parsed.success) return; // unrelated event line — skip + const msg = parsed.data.message as Message; + details.messages.push(msg); + if (msg.role !== "assistant") return; + + const u = msg.usage; + if (u) { + details.usage.turns++; + details.usage.input += u.input ?? 0; + details.usage.output += u.output ?? 0; + details.usage.cacheRead += u.cacheRead ?? 0; + details.usage.cacheWrite += u.cacheWrite ?? 0; + details.usage.cost += u.cost?.total ?? 0; + // Latest-turn snapshot — don't sum across turns; each turn re-sends the + // whole conversation, so one assistant message already represents the + // current context size. + details.tokens = u.totalTokens ?? (u.input ?? 0) + (u.output ?? 0) + (u.cacheRead ?? 0) + (u.cacheWrite ?? 0); + } + + for (const part of msg.content) { + if (part.type === "toolCall") { + details.toolCount++; + details.recentTools.push({ name: part.name, argsPreview: argsPreview(part.arguments) }); + } else if (part.type === "text" && part.text.trim()) { + const prose = part.text.split("\n").find((l) => l.trim() && !l.trimStart().startsWith("```")); + if (prose) details.lastMessage = prose.trim(); + } + } +} + +/** Last non-empty assistant text block. Used by the tool layer for the + * return content and by render.ts for the final output preview. */ +export function getFinalText(messages: RunDetails["messages"]): string { + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages[i]; + if (message.role !== "assistant") continue; + for (const part of message.content) { + if (part.type === "text" && part.text.trim()) return part.text.trim(); + } + } + return ""; +} + +/** Pick the runtime: re-exec the current script if it's a real file, else `pi`. */ +function getPiInvocation(args: string[]): { command: string; args: string[] } { + const currentScript = process.argv[1]; + const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/"); + if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) { + return { command: process.execPath, args: [currentScript, ...args] }; + } + + const execName = path.basename(process.execPath).toLowerCase(); + const isGenericRuntime = /^(node|bun)(\.exe)?$/.exec(execName); + if (!isGenericRuntime) return { command: process.execPath, args }; + + return { command: "pi", args }; +} + +// ── small helpers ───────────────────────────────────────────────────── + +function argsPreview(args: unknown): string { + if (!args || typeof args !== "object") return ""; + const a = args as Record; + for (const k of ["path", "file_path", "command", "query", "url", "pattern", "content"]) { + const v = a[k]; + if (typeof v === "string") return v.replace(/\s+/g, " ").trim(); + } + return JSON.stringify(args).replace(/\s+/g, " ").trim(); +} + +// Leading-edge throttle. Loses a trailing update scheduled before fire — same +// trade-off as the original. flush() forces one out so the final state renders. +function createThrottle(intervalMs: number) { + let pending = false; + let lastFire = 0; + let timer: NodeJS.Timeout | undefined; + return { + schedule(fn: () => void) { + pending = true; + const delay = intervalMs - (Date.now() - lastFire); + if (delay <= 0) { + if (timer) clearTimeout(timer); + timer = undefined; + pending = false; + lastFire = Date.now(); + fn(); + } else if (!timer) { + timer = setTimeout(() => { + timer = undefined; + if (pending) { + pending = false; + lastFire = Date.now(); + fn(); + } + }, delay); + } + }, + flush(fn: () => void) { + if (timer) clearTimeout(timer); + timer = undefined; + if (pending) { + pending = false; + lastFire = Date.now(); + fn(); + } + }, + }; +} diff --git a/home/.pi/agent/extensions/subagents/render.ts b/home/.pi/agent/extensions/subagents/render.ts new file mode 100644 index 00000000..e517478d --- /dev/null +++ b/home/.pi/agent/extensions/subagents/render.ts @@ -0,0 +1,162 @@ +/** + * UI rendering for spawn_agent — the tool-call header (renderCall) and the + * streaming/final result block (renderResult). Pure formatting plus a 1Hz + * tick interval to keep the elapsed counter alive between tool events. + */ + +import type { Theme } from "@earendil-works/pi-coding-agent"; +import { Container, Spacer, Text } from "@earendil-works/pi-tui"; +import { getFinalText, type RunDetails } from "./process.ts"; + +const TICK_INTERVAL_MS = 1000; + +// ── formatters ──────────────────────────────────────────────────────── + +export function formatDuration(ms: number): string { + const s = ms / 1000; + return s < 10 ? `${s.toFixed(1)}s` : `${Math.round(s)}s`; +} + +export function formatTokens(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; + return String(n); +} + +export function formatContextUsage(tokens: number, window?: number): string { + if (!window) return formatTokens(tokens); + return `${Math.round((tokens / window) * 100)}%/${formatTokens(window)}`; +} + +export function taskPreview(s: string): string { + const one = s.replace(/\s+/g, " ").trim(); + return one.length > 80 ? `${one.slice(0, 77)}...` : one; +} + +// ── tool-call header ────────────────────────────────────────────────── + +export function renderCallHeader( + c: Container, + args: { message?: string; agent_type?: string; task_name?: string; model?: string; cwd?: string }, + expanded: boolean, + theme: Theme, +): void { + const agentLabel = args.agent_type ? ` ${theme.fg("accent", args.agent_type)}` : ""; + const meta: string[] = []; + if (args.task_name) meta.push(theme.fg("muted", `· ${args.task_name}`)); + if (args.model) meta.push(theme.fg("muted", `· model=${args.model}`)); + if (args.cwd) meta.push(theme.fg("muted", `· cwd=${args.cwd}`)); + c.addChild(new Text(`${theme.fg("toolTitle", theme.bold("spawn_agent"))}${agentLabel} ${meta.join(" ")}`, 0, 0)); + + if (args.message) { + if (!expanded) { + c.addChild(new Text(theme.fg("dim", ` ${taskPreview(args.message)}`), 0, 0)); + } else { + c.addChild(new Spacer(1)); + c.addChild(new Text(theme.fg("text", args.message), 0, 0)); + } + } +} + +// ── result block ────────────────────────────────────────────────────── + +export interface RenderOptions { + expanded: boolean; + isPartial: boolean; +} + +export function renderResultBlock(details: RunDetails, options: RenderOptions, theme: Theme): Container { + const c = new Container(); + const failed = details.aborted || details.exitCode !== 0; + const isRunning = options.isPartial && !failed; + const elapsed = formatDuration((details.endTime ?? Date.now()) - details.startTime); + + const icon = isRunning + ? theme.fg("warning", "⟳") + : failed + ? theme.fg("error", "✗") + : theme.fg("success", "✓"); + + const headerParts = [ + `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))}`, + theme.fg("muted", `[d${details.depth}]`), + theme.fg("muted", `· ${details.taskName}`), + ]; + if (details.model) headerParts.push(theme.fg("muted", `(${details.model})`)); + headerParts.push(theme.fg("dim", `· ${details.toolCount} tools · ${elapsed}${isRunning ? " running" : ""}`)); + c.addChild(new Text(headerParts.join(" "), 0, 0)); + + // Tool log: last 8 collapsed, all expanded; earlier entries summarized. + const tools = details.recentTools; + const visibleCount = options.expanded ? tools.length : Math.min(tools.length, 8); + if (tools.length > visibleCount) { + c.addChild(new Text(theme.fg("dim", `… ${tools.length - visibleCount} earlier actions`), 0, 0)); + } + for (let i = tools.length - visibleCount; i < tools.length; i++) { + const t = tools[i]; + const body = t.argsPreview ? `${t.name}: ${t.argsPreview}` : t.name; + c.addChild(new Text(theme.fg("muted", ` ${body}`), 0, 0)); + } + + // Latest thinking line. Skip when the tool log is empty — nothing to think about yet. + if (details.lastMessage && tools.length > 0) { + c.addChild(new Spacer(1)); + c.addChild(new Text(theme.fg("text", details.lastMessage), 0, 0)); + } + + // Usage line — meaningful only after the first turn, or with a context gauge. + const usageParts: string[] = []; + if (details.usage.input) usageParts.push(theme.fg("dim", `↑${formatTokens(details.usage.input)}`)); + if (details.usage.output) usageParts.push(theme.fg("dim", `↓${formatTokens(details.usage.output)}`)); + if (details.usage.cacheRead) usageParts.push(theme.fg("dim", `R${formatTokens(details.usage.cacheRead)}`)); + if (details.usage.cacheWrite) usageParts.push(theme.fg("dim", `W${formatTokens(details.usage.cacheWrite)}`)); + if (details.usage.cost) usageParts.push(theme.fg("dim", `$${details.usage.cost.toFixed(3)}`)); + if (details.tokens > 0) { + const pct = details.contextWindow ? (details.tokens / details.contextWindow) * 100 : 0; + const color = pct > 90 ? "error" : pct > 70 ? "warning" : "dim"; + usageParts.push(theme.fg(color, formatContextUsage(details.tokens, details.contextWindow))); + } + if (usageParts.length) { + c.addChild(new Spacer(1)); + c.addChild(new Text(usageParts.join(" "), 0, 0)); + } + + // Final output, only when done. 8 lines collapsed, full expanded. + if (!isRunning) { + const finalText = getFinalText(details.messages); + if (finalText) { + c.addChild(new Spacer(1)); + const preview = options.expanded ? finalText : finalText.split("\n").slice(0, 8).join("\n"); + c.addChild(new Text(theme.fg("toolOutput", preview), 0, 0)); + } + } + + if (failed && details.stderr.trim()) { + c.addChild(new Spacer(1)); + c.addChild(new Text(theme.fg("error", details.stderr.trim()), 0, 0)); + } + + return c; +} + +/** Set up (and tear down) a 1Hz invalidation tick while the result is partial. + * `ticks` is keyed by toolCallId so concurrent spawns don't share a slot. */ +export function manageTick( + ticks: Map, + id: string, + isPartial: boolean, + invalidate: () => void, +): void { + if (isPartial) { + if (ticks.has(id)) return; + // .unref() so a missed cleanup (process exits before final renderResult + // fires) can't keep the agent alive indefinitely. + ticks.set(id, setInterval(invalidate, TICK_INTERVAL_MS).unref()!); + } else { + const t = ticks.get(id); + if (t) { + clearInterval(t); + ticks.delete(id); + } + } +} diff --git a/home/.pi/agent/extensions/tokenrouter/index.ts b/home/.pi/agent/extensions/tokenrouter/index.ts deleted file mode 100644 index 8739fb29..00000000 --- a/home/.pi/agent/extensions/tokenrouter/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; - -export default function(pi: ExtensionAPI) { - pi.registerProvider("tokenrouter", { - name: "TokenRouter", - baseUrl: "https://api.tokenrouter.com/v1", - apiKey: "$TOKENROUTER_API_KEY", - api: "openai-completions", - authHeader: true, - - compat: { - supportsDeveloperRole: false, - supportsReasoningEffort: false, - supportsUsageInStreaming: true, - maxTokensField: "max_tokens", - }, - - models: [ - { - id: "MiniMax-M3", - name: "MiniMax M3", - reasoning: true, - input: ["text", "image"], - contextWindow: 1_000_000, - maxTokens: 16_384, - cost: { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, - }, - }, - ], - }); -} diff --git a/home/.pi/agent/extensions/tsconfig.json b/home/.pi/agent/extensions/tsconfig.json new file mode 100644 index 00000000..2339f918 --- /dev/null +++ b/home/.pi/agent/extensions/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2025", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2025"], + "types": ["node"], + "noEmit": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + }, + "include": [ + "**/*.ts" + ], + "exclude": [ + "node_modules" + ]} diff --git a/home/.pi/agent/prompts/review-diff.md b/home/.pi/agent/prompts/review-diff.md deleted file mode 100644 index 0e845c7b..00000000 --- a/home/.pi/agent/prompts/review-diff.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -description: Delegate focused read-only review of the current git diff -argument-hint: "[focus]" ---- -Use the subagent tool with agent "reviewer" to review unstaged changes in the current repository for correctness risks, future compatibility issues, and deviations from current structure or standard patterns. - -The reviewer should inspect `git status --short`, `git diff`, and relevant untracked files. Do not review staged or committed changes unless they are needed to understand the unstaged diff. - -Focus, if supplied: $@ - -Return the reviewer's findings. Do not edit files. diff --git a/home/.pi/agent/prompts/scout.md b/home/.pi/agent/prompts/scout.md deleted file mode 100644 index 0638c2c4..00000000 --- a/home/.pi/agent/prompts/scout.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: Delegate read-only codebase reconnaissance to a scout subagent -argument-hint: "" ---- -Use the subagent tool with agent "scout" to investigate this read-only question: - -$@ - -Return the scout's findings and then give only the shortest useful recommendation for the next step. Do not implement. diff --git a/home/.pi/agent/settings.json b/home/.pi/agent/settings.json index 20b87dea..b0d7108a 100644 --- a/home/.pi/agent/settings.json +++ b/home/.pi/agent/settings.json @@ -5,15 +5,18 @@ "defaultProjectTrust": "always", "collapseChangelog": true, "quietStartup": true, - "lastChangelogVersion": "0.79.3", + "lastChangelogVersion": "0.79.8", "retry": { "maxRetries": 10 }, - "packages": [], + "packages": [ + "git:github.com/obra/superpowers", + "npm:@ff-labs/pi-fff" + ], "enabledModels": [ + "tokenrouter/MiniMax-M3", "openai-codex/gpt-5.5", - "deepseek/deepseek-v4-flash", - "deepseek/deepseek-v4-pro", - "tokenrouter/MiniMax-M3" - ] -} + "openmodel/deepseek-v4-flash" + ], + "theme": "dark" +} \ No newline at end of file diff --git a/home/.pi/agent/skills/init/SKILL.md b/home/.pi/agent/skills/init/SKILL.md index 153cc176..5666df58 100644 --- a/home/.pi/agent/skills/init/SKILL.md +++ b/home/.pi/agent/skills/init/SKILL.md @@ -7,63 +7,71 @@ Create or update AGENTS.md for this repository. ## Objective -Produce a high-signal reference that helps future agents: -- understand stable architecture and patterns -- avoid non-obvious mistakes -- find things quickly +A compact instruction file that helps future agents avoid mistakes and ramp up quickly. Every line must answer: **"Would an agent likely miss this without help?"** If not, leave it out. -Do not restate obvious details. +## Approach ---- +Two phases: **gather** deeply to understand big patterns, then **filter** aggressively so only non-obvious knowledge survives into AGENTS.md. -## Approach +### Gather -- Identify project type from root files -- Extract real commands from scripts/config -- Read a small set of representative files -- Infer patterns from repetition (not single instances) -- Stop once patterns stabilize +Goal: understand patterns well enough to write rules that hold up across the codebase, not just enumerate layout. Don't stop at the surface. ---- +1. Read the project's stated intent (README, top-level docs, architecture notes). +2. Read configs and executable sources (Makefiles, CI workflows, package scripts, lockfiles, lint/format/build configs). These run; prose can lie. +3. Map layout: top-level dirs and what each owns. Enough to know where to look, not exhaustive enumeration. +4. Read source until patterns stabilize. Read at least 70k tokens of content or everything in the repo, whichever hits first. Don't stop at 2–3 spot-checks when the codebase has subsystems, frameworks, or platform branches — read enough that claims survive cross-checks. For bi-platform repos, verify both branches; for plugin systems, both the loader and a representative plugin; for layered architectures, both the boundary and a layer that crosses it. +5. Note the gotchas and non-obvious conventions that took multiple files or cross-referencing to discover. -## Include +### Filter -### Essential Commands -Only what an agent will actually run (build/test/run/lint). -Include flags only if non-obvious. +Apply **What to extract**: would an agent likely miss this without help? If not, leave it out. Directory trees, single-file facts, generic advice — all noise. AGENTS.md is the filtered output, not the gathered context. -### Architecture -- overall structure (monolith, layered, etc.) -- key directories and responsibilities -- control/data flow at a high level +### Updating an existing AGENTS.md -### Patterns & Conventions -Only stable, repeated patterns: -- naming -- structure -- layering -- error handling +- Audit every claim against current state. Delete what doesn't verify. +- **Structural rewrites are fine and often warranted.** Don't preserve the old layout or sectioning for its own sake — reorganize freely when it serves clarity, including a complete format change. +- Preserve verified guidance, reconcile with current codebase, add new claims that survive the filter. -### Gotchas -Non-obvious rules, edge cases, surprising behavior. +Stop gathering when a new file in a known place doesn't change the rule. -### Navigation -Where to start and how to trace features. +## Sources of truth (priority order) -### Testing -Only if non-trivial. +1. **Executable sources** — scripts, Makefiles, CI workflows, package.json scripts, lockfiles. What actually runs. +2. **Configs** — lint, format, typecheck, build configs. +3. **Existing instruction files** — `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/`, `.cursorrules`, `.github/copilot-instructions.md`. Migrate non-obvious rules worth keeping. +4. **Prose docs** — README, CONTRIBUTING. Lowest priority; trust executable over prose when they conflict. ---- +## Verify before writing + +Every fact must be backed by file path, command output, or git state. Enumerate tracked files, map layout, and cross-reference as needed. Read each cited file before claiming what it does. + +**Only tracked files are sources of truth.** Untracked files are transient — do not include claims about them in AGENTS.md and do not cite them as evidence. If a claim can only be verified from an untracked file, drop it. + +Speculation is worse than a gap. Drop uncertain claims. + +## What to extract (high-signal) + +- Exact commands, especially non-obvious ones (single-test, focused verification, required order like `lint → typecheck → test`) +- Monorepo / multi-package boundaries and real entrypoints +- Framework or toolchain quirks: codegen, migrations, build artifacts, env loading, dev servers +- Repo conventions that differ from language/framework defaults +- Testing quirks: fixtures, integration prerequisites, snapshot workflows, required services, flaky/expensive suites +- Non-obvious gotchas that took reading multiple files to infer ## Exclude -- obvious facts from a single file -- full file listings -- CI/CD, infra, deployment -- speculation ---- +- Anything obvious from a single file or filename. +- Full file trees — easily enumerated on demand. +- Generic software advice. +- Long tutorials or exhaustive references. +- Speculation, "probably", "I think". +- Apologetic qualifiers (`(not an X)`, `(note: doesn't…)`). If something doesn't fit, omit it or state what it is. +- Anything the agent would pick up from reading the relevant file. Mentioning the obvious is actively detrimental — it costs tokens and crowds out real gotchas. ## Rules -- Prefer dense, useful information -- No filler, no repetition -- Only include what is clearly supported by the code + +- Each line earns its place. +- When updating, audit existing claims before adding new ones. A stale AGENTS.md is worse than no AGENTS.md. +- Prefer dense bullets over prose. +- Trim if a section is longer than what it documents. diff --git a/home/.pi/agent/supervisor.md b/home/.pi/agent/supervisor.md deleted file mode 100644 index 032dd931..00000000 --- a/home/.pi/agent/supervisor.md +++ /dev/null @@ -1,160 +0,0 @@ -You are the user's autonomous engineering proxy. The user has set a goal -and asked you to drive it to completion. You inherit their full environment: -their context files (AGENTS.md / CLAUDE.md), their skills, their tools, and -their conventions. Operate as if you are them, with two differences: - -1. You have the `subagent` tool to delegate work. -2. You must produce a final report they can review. - -The user has explicitly opted out of budget caps. You do not stop early. -You drive until the goal is verifiably met, every tracked bug is either -resolved or honestly marked STUCK with a reason, and your final report -is written. - -## Files (the goal state directory) - -All state lives in a single directory the user provides at kickoff -(typically `~/.pi/agent/goals//`). - -- `goal.md` — the goal, restated by you in concrete terms. Not copied. -- `clarifications.md` — upfront questions and the user's answers. Append-only thereafter. -- `plan.md` — your working plan. Ordered bug list with file paths, the gate - each bug fails, and dependencies between bugs. -- `verification.md` — the gates you have derived. Each gate is a runnable - command with a captured pass/fail status. Append-only. -- `progress.md` — append-only dated log. Every dispatch, every gate run, - every state transition, every reviewer verdict. Make it greppable. -- `stuck.md` — bugs you could not resolve. One paragraph each: what you - tried, what you observed, what would unblock. -- `final-report.md` — the deliverable. - -## Phase 1: Upfront clarification (do not skip) - -Before dispatching anything, ask the user 2-4 clarifying questions via -`ctx.ui.ask` (or `ctx.ui.question` for multi-select). Write the answers -to `clarifications.md`. - -Bias toward asking now, asking well, then driving. Do not ask -permission-to-proceed questions ("should I continue?", "is this OK?"). -Ask only when requirements are genuinely ambiguous or off-limits items -exist. Cheap questions now save expensive wrong-direction work later. - -Default questions to consider, adapted to context: -- What counts as a bug here? (syntax errors only? behavior? style? perf?) -- Is there an authoritative test command, linter, or CI gate I must honor? -- Any paths or files off-limits? (generated, vendored, secrets, lockfiles) -- Should fixes be committed as I go, or batched at the end? - -If the user declines to answer, record the declined question and your -chosen default in `clarifications.md`, then proceed. - -## Phase 2: Triage and plan - -1. Run every gate you can identify (linters, syntax checks, typecheck, - tests) and capture the output. These gates define "bug" for this goal. -2. Dispatch scout agent(s) — read-only, fast, cheap model — to enumerate - candidate bugs your gate runs may have missed. Triage their output: - real bug, false positive, or out of scope. -3. Write `plan.md` with the ordered list. Group by file or module. Note - dependencies. Note which gate each bug fails. Note the worker's model - and toolset you will use. -4. Tell the user the plan in one short paragraph. Do not wait for approval — - they have opted into autonomy. - -## Phase 3: Iterate (the loop) - -For each bug in plan order, with the freedom to re-order when you learn -something: - -1. **Pre-flight.** Re-read `goal.md`, `plan.md`, and the relevant - `progress.md` entries. State the bug, its gate, and the expected fix - shape in one line in `progress.md`. -2. **Dispatch a worker.** Use the `subagent` tool. Give one bug per - dispatch. Demand evidence: a diff and the exact command output for - the gate. If the bug requires investigation first, dispatch a scout - before the worker. -3. **Independently verify.** Run the gate yourself. Do not trust the - worker's claim. Append the command and its output to `verification.md`. -4. **Independent review** for non-trivial fixes. Dispatch a reviewer - agent to inspect the diff. They must return a verdict: approve, - request-changes, or reject, with a one-line reason. -5. **State transition.** Update `plan.md` and `progress.md`. A bug moves - to `resolved` only if the gate passes *and* any reviewer agrees. - Otherwise back to `fix-in-progress` with a one-line note. -6. **Re-run the full gate set** after each fix. Earlier fixes can - regress. New bugs can surface. Update `plan.md`. - -Append a dated entry to `progress.md` for every dispatch, every gate -run, every reviewer verdict, and every state transition. Format is -yours; make it greppable. - -## Phase 4: Stuck detection - -A budget cap is forbidden. A stuck detection is not. - -If 3 consecutive attempts on the same bug produce no new state — same -gate output, same error, same blocker — mark the bug `STUCK` in -`plan.md`, write a paragraph to `stuck.md` (what you tried, what you -observed, what would unblock), and move to the next bug. Do not retry -the same approach with cosmetic variations. If a genuinely fresh -approach occurs to you, attempt it once and re-enter the 3-attempt -window. - -Stuck is not failure of the goal. The goal is "drive to completion"; -completion means "every bug resolved or honestly stuck with evidence." - -## Delegation: how to use `subagent` - -- **Scout** — read-only, fast, cheap model. "Find all the X", "where is - Y used", "summarize Z". Triage, exploration, inventory. -- **Worker** — full tools, default model. Scoped fixes, code changes, - command runs. One bug per dispatch. Demand evidence, not prose. -- **Reviewer** — read-only or write-light; may re-run commands. - Independent diff review. Verdict required. - -When a worker reports "done" without showing the exact command and its -exact output for the gate, push back: "show me the command, show me the -output." If they cannot produce it, treat the fix as unverified and stay -in `fix-in-progress`. - -## Verification protocol (the load-bearing rule) - -Every bug has an objective gate: a command that can be run, with output -that can be captured. Examples: - -- Linter passes: `shellcheck file.sh` returns 0, output saved -- Syntax valid: `bash -n script.sh` returns 0 -- No broken links: `find ... -xtype l` returns empty -- Test passes: the relevant test command, output section captured -- No regression: the full gate set, re-run, has not grown in failures - -You (the supervisor) run the gates. The worker may run them too, but -you re-run independently. Capture the command and a digest of the -output, not a paraphrase. Never accept "I think it's fixed" — only -"gate X runs cleanly, output below." - -## Output discipline - -- Workers return: file paths, command output (verbatim where short, - digested where long), and diffs. Not summaries. -- You log: dispatch summaries, gate runs with output, state transitions, - reviewer verdicts, surprises. -- The final report (`final-report.md`) contains: bugs found (count, - categories), bugs resolved (with `verification.md` line references), - bugs stuck (with reasons and what would unblock each), and any new - bugs surfaced during the work that the user should know about. - -## What you do not do - -- Do not ask permission-to-proceed questions. The user opted into autonomy. -- Do not stop on a budget. There is no budget. -- Do not skip verification. Every fix must have captured gate output. -- Do not trust worker claims of "done." Re-run gates yourself. -- Do not retry the same failed approach 4+ times. That is a stuck loop, - not persistence. -- Do not modify files outside the active bug's scope. If a fix needs a - refactor, file the refactor as a new bug and move on. -- Do not commit, push, or publish. The user owns VCS. Leave changes in - the working tree unless explicitly told otherwise. -- Do not ask the user mid-flight. If something is truly blocking, write - it to `stuck.md` and continue with other bugs. diff --git a/home/.profile b/home/.profile index 620ad565..98700dbf 100644 --- a/home/.profile +++ b/home/.profile @@ -78,6 +78,7 @@ GH_PAGER="delta" DOCKER_BUILDKIT="1" NPM_CONFIG_IGNORE_SCRIPTS=true DISABLE_TELEMETRY=1 # Disable claude code telemetry +PI_FFF_MODE=override # Replace pi's built-in find/grep with FFF (pi-fff ext) # Compact PS1-themed GNU ls colors. __ls_colors() ( diff --git a/home/.stow-local-ignore b/home/.stow-local-ignore new file mode 100644 index 00000000..7f725274 --- /dev/null +++ b/home/.stow-local-ignore @@ -0,0 +1,2 @@ +# Ignore npm package directories — tens of thousands of files +node_modules diff --git a/init.sh b/init.sh index 05c42932..9d618e2a 100755 --- a/init.sh +++ b/init.sh @@ -138,6 +138,11 @@ user_config() { run_command systemctl --user enable ssh-agent run_command systemctl --user start ssh-agent fi + + # Remind about pi extension deps if missing (stow no longer manages node_modules) + if [ ! -d ~/.pi/agent/extensions/node_modules ]; then + echo -e "${YELLOW}Hint: run 'make pi' to install npm deps for pi extensions${NC}" + fi } linux_system_config() { diff --git a/pi-web/.gitignore b/pi-web/.gitignore new file mode 100644 index 00000000..dd6e803c --- /dev/null +++ b/pi-web/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/pi-web/README.md b/pi-web/README.md new file mode 100644 index 00000000..c3500a24 --- /dev/null +++ b/pi-web/README.md @@ -0,0 +1,39 @@ +# pi-web + +A localhost browser UI for [pi](https://github.com/badlogic/pi-mono), the +coding agent. Drop-in TUI replacement: same sessions, same extensions, +same skills — just a browser tab instead of a terminal. + +## Run it + +```bash +cd pi-web +npm install +npm run dev +``` + +This starts an HTTP server on `http://127.0.0.1:7878` and opens your +browser. The session lives in whatever directory you ran `npm run dev` +from (use `--cwd ` to chat about a different project). + +## What's in scope (v0.1) + +- Chat (streaming text, thinking, tool calls) +- New session, switch session, list prior sessions +- Model picker, thinking-level picker +- Abort +- Persists to the same `~/.pi/agent/sessions/` tree the TUI uses + +## What's not + +Session tree UI (fork/branch), steering/follow-up queue UI, compaction +controls, image attachments, markdown rendering. See +`docs/superpowers/specs/2026-06-21-pi-web-design.md` for the full spec. + +## Layout + +``` +src/ # server + bridge + protocol +src/web/ # static frontend (Preact + HTM, no build step) +tests/ # node:test +``` diff --git a/pi-web/bin/pi-web b/pi-web/bin/pi-web new file mode 100755 index 00000000..8c4958e0 --- /dev/null +++ b/pi-web/bin/pi-web @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Launch pi-web in the current directory. +# +# Usage: +# pi-web # chat about the current directory +# pi-web --cwd ~/projects/foo # chat about a different project +# +# Env: +# PI_WEB_PORT override port (default 7878) +# PI_WEB=0 disable browser auto-open +# PI_WEB_ROOT override install location (default ~/dotfiles/pi-web) +set -euo pipefail + +PI_WEB_ROOT="${PI_WEB_ROOT:-~/dotfiles/pi-web}" + +if [ ! -d "$PI_WEB_ROOT" ]; then + echo "[pi-web] PI_WEB_ROOT not found: $PI_WEB_ROOT" >&2 + exit 1 +fi + +exec "$PI_WEB_ROOT/node_modules/.bin/tsx" "$PI_WEB_ROOT/src/server.ts" "$@" diff --git a/pi-web/docs/smoke-test.md b/pi-web/docs/smoke-test.md new file mode 100644 index 00000000..53774cf2 --- /dev/null +++ b/pi-web/docs/smoke-test.md @@ -0,0 +1,59 @@ +# pi-web smoke test + +Run before merging any change to `pi-web/`. + +## Setup + +```bash +cd pi-web +npm install +npm run typecheck +npm test +``` + +All three must succeed. Tests: 38 across `protocol.test.ts`, +`open-browser.test.ts`, `bridge.test.ts`, `server.test.ts`, +`runtime.test.ts` (one is skipped if no API key is available). + +## Manual end-to-end + +```bash +mkdir -p /tmp/pw-smoke +cd /tmp/pw-smoke +PI_WEB=0 /path/to/pi-web/node_modules/.bin/tsx /path/to/pi-web/src/server.ts --cwd /tmp/pw-smoke +``` + +In another terminal: + +```bash +open http://127.0.0.1:7878/ +``` + +Verify: + +1. Page loads. Sidebar shows Session, Model, Thinking sections. +2. The Model picker is empty if no API key is configured; if a key + exists, the picker lists available models. +3. Type a message and press Enter. The user message appears; an + assistant response streams in (if a key is configured). If no key, + an error toast appears with the SDK's error message. +4. Click `+ New session`. The chat clears. A new session entry + appears in the session list. +5. Click a prior session in the list. The chat shows that session's + messages. +6. Change the model in the picker. The next prompt uses the new + model. +7. Change the thinking level. The next prompt uses the new level. +8. Send a long prompt; press Abort. The "Abort" button works, the + partial response is discarded. +9. Close the browser tab. The server still runs. +10. Press Ctrl-C in the server terminal. Clean exit. + +## What is not tested in v0.1 + +- Streaming deltas rendering (covered in `bridge.test.ts` only at the + event-forwarding level; visual verification is manual). +- Multiple browser tabs in sync. +- Session tree (forks, branches) — not in v0.1. +- Image attachments — not in v0.1. +- Markdown rendering — text is shown as-is. diff --git a/pi-web/package-lock.json b/pi-web/package-lock.json new file mode 100644 index 00000000..e9c9ce67 --- /dev/null +++ b/pi-web/package-lock.json @@ -0,0 +1,3745 @@ +{ + "name": "pi-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pi-web", + "version": "0.1.0", + "dependencies": { + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*", + "htm": "^3.1.1", + "preact": "^10.22.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^22.7.0", + "@types/ws": "^8.5.12", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.22.tgz", + "integrity": "sha512-YofH63shc6YRdXjz80BJkpJW+Bkn0Cuu2dn4Rv7s9G2Idt58tgtzQEWxrR2xVljlVfIBeUjPuULnSVYLke3sUQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@aws-sdk/xml-builder": "^3.972.30", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.48.tgz", + "integrity": "sha512-h6FEC95fbexUd6zxm4PdgS82bTcI2PRtUb2ZwMipb/Xr8bPwtf0G8rBo2jp7NA24Mbx2JA8/WingiYpA9RCCyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.50.tgz", + "integrity": "sha512-lJO3OLpjvz5m/RSBQmsG/CEUGsvCy5ruxKwPQaOCqxqCMuyYT2BZwQUTDZVVwqQ9LrZKuK24JSa6r31hL/tvkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.1.tgz", + "integrity": "sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.55.tgz", + "integrity": "sha512-TBoF4buBGYhXjdZAryayY2TrkQj2B2KfE/msG4V53XCt+w0EhEwM2JRjx8p2grJ2C6gtH5++SAwEvGMRdi0yyw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/credential-provider-env": "^3.972.48", + "@aws-sdk/credential-provider-http": "^3.972.50", + "@aws-sdk/credential-provider-login": "^3.972.54", + "@aws-sdk/credential-provider-process": "^3.972.48", + "@aws-sdk/credential-provider-sso": "^3.972.54", + "@aws-sdk/credential-provider-web-identity": "^3.972.54", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.54.tgz", + "integrity": "sha512-hBWI3wZTdTGiuMfmPts6AWbAjFfRniOQnqx68tc2cQvRKWawFbN9wkLOVPWM1FAOyowZU73mC6Fi+rHSHNyLFw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.57", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.57.tgz", + "integrity": "sha512-u6dClpzNdWf1HGWz4wwhdXi1wiOofCLniM9S4BQQGlLAN9TW7VB+ld5V533GdKrYMaFeBGFqKnj0JCYvynLqwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.48", + "@aws-sdk/credential-provider-http": "^3.972.50", + "@aws-sdk/credential-provider-ini": "^3.972.55", + "@aws-sdk/credential-provider-process": "^3.972.48", + "@aws-sdk/credential-provider-sso": "^3.972.54", + "@aws-sdk/credential-provider-web-identity": "^3.972.54", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.48.tgz", + "integrity": "sha512-w6VZwojPt12WnEkAUy6Nu4K6sWCbBmR7QX390b0nE6vRvkXbrYr9Lq9VySGkfjiMjpUA87op+J4EgvRmtWIDoQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.54.tgz", + "integrity": "sha512-23uZpIpF2SIFDCa1fcWa202tK4gGeyvX6GIIAjiB8WBsvsVRBMnJ/7dCxHzxf7eZT7GToJg837LDIBnZsl/VUg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/token-providers": "3.1071.0", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1071.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1071.0.tgz", + "integrity": "sha512-4LDW2Qob6LoLFuqYSYZq2AyTE9koSE9+i+n5UZcm10GpmQOK0zRD9L4uYlzItiTKksIWgC/qMFChAi3RvKYtMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.54.tgz", + "integrity": "sha512-0Iv5QttS6wcATlodYKgvQj6B9Db51rx7NU9fqu0PoLeS4BIgdYMc/QK4smwLwpm5RFrs02V/eLyEFp3FklvlNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/nested-clients": "^3.997.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.22.tgz", + "integrity": "sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.18.tgz", + "integrity": "sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.30.tgz", + "integrity": "sha512-kH6N4f/Fzi9r/dYap8EQ+Zk4NOz8pl4AtWKhzAoG2C1/4YkIHok9APp/e+75woreWQq264n+LkrJsJVZ0Q+M1Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.22.tgz", + "integrity": "sha512-4IwtcYSxEIVw5hcp8ogq0CMbFNZFw7jJUetpfFUhFFeqsa1K8j2Ihg2hnxLyOp3stMZnXda6VzOmPi1AFZQXcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.35", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.1.tgz", + "integrity": "sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz", + "integrity": "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.13.tgz", + "integrity": "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.30.tgz", + "integrity": "sha512-StElZPEoBquWwNqw1AcfpzEyZqJvFxouG+mpDNYlcH6ZOrqd2CuIryv+8LV8gNHZUOyKyJF3Dq9vxaXEmDR9TQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", + "integrity": "sha512-fHmgNMONwCCE7bQAKbcz76sgm3iQuA7km1mpIc4H5xXd9+zhPh/faULz6ARkgjQE0EufHnfZPJY39+lNf8Sa9g==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.9.tgz", + "integrity": "sha512-8TZ796Zn0NE4vmhxG9hv4ZtJDGJzhqMjlmFg8ZkUKxfqB7LJa4ums2jSJKtnyAZfAamN6VzqzN0A82RNDqv8Ag==", + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.79.9", + "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-tui": "^0.79.9", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.1.38", + "undici": "8.5.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.9.tgz", + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.79.9", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.79.9", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.9.tgz", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/core": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.25.1.tgz", + "integrity": "sha512-zpDbpXBCBsxfLtG2GEUyfgvHvSFrw5CwDZSNzL0v52gx/c3oPlPbm+7W7num8xs6vyiUBn+bvYPHcQDOXZynCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.1.tgz", + "integrity": "sha512-TSAF5NHgxEsllbErYWbK8aLnl5L601NGc5VYJlSPsKnf3YlkhdoBN+geGcaU00oiw2OK3QO5LA3QNXiiWhCidQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.5.1.tgz", + "integrity": "sha512-96JrD1q71anokymx9Iblb+zKmNQYNstlV/25A9ZYIJ2A0rp1r7/GZAIm0bDWSmVvz3DpNOCZuabzsiL+w0UHhw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.5.1.tgz", + "integrity": "sha512-X9rVls3En0z3NtrmguTmpRM0/NqtWUxBjal6fcAkwtsub+gOdLZ6kD+V7xhUgFMGdG14bHbZ7M5QjaRI1+DatQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.25.1", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/anynum": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", + "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "license": "Apache-2.0" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.0.tgz", + "integrity": "sha512-e5y7RCLHKjemsgQ4eqGJtPyr10ILz25HO7flzxhTV8bgvd5yHx98DGtCAtbVW9f2TqnYI/gEVZd+vz7snrdPTw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/strnum": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", + "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.1" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/pi-web/package.json b/pi-web/package.json new file mode 100644 index 00000000..4792c24a --- /dev/null +++ b/pi-web/package.json @@ -0,0 +1,28 @@ +{ + "name": "pi-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "start": "tsx src/server.ts", + "typecheck": "tsc --noEmit", + "test": "node --test 'tests/*.test.ts'" + }, + "dependencies": { + "@earendil-works/pi-coding-agent": "*", + "@earendil-works/pi-ai": "*", + "preact": "^10.22.0", + "htm": "^3.1.1", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^22.7.0", + "@types/ws": "^8.5.12", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/pi-web/src/bridge.ts b/pi-web/src/bridge.ts new file mode 100644 index 00000000..d5ff7b89 --- /dev/null +++ b/pi-web/src/bridge.ts @@ -0,0 +1,291 @@ +// Bridge: AgentSessionRuntime <-> WebSocket fan-out. +// +// On construction: subscribes to runtime.session events; pushes them +// (JSON-serialized) to every connected client sender. +// +// On handleCommand: parses a raw client message, dispatches to the SDK, +// sends a { type: "response", id, ok, error? } back to the originating +// client. The originating client is the one whose send closure is passed +// alongside the raw message (server.ts threads these together). +// +// Session replacement (newSession/switchSession) goes through runtime, +// which mutates runtime.session. The bridge then calls rebind() to +// re-subscribe to the new session. + +import { parseClientCommand, type ClientCommand, type ServerResponse } from "./protocol.ts"; +import type { ModelThinkingLevel, Model } from "@earendil-works/pi-ai"; + +/** Subset of AgentSession the bridge depends on. */ +export interface BridgeSession { + subscribe(cb: (event: unknown) => void): () => void; + prompt(text: string, opts?: { images?: unknown[] }): Promise; + abort(): Promise; + setModel(model: Model): Promise; + setThinkingLevel(level: ModelThinkingLevel): void; + readonly messages: unknown[]; + readonly agent: { + readonly state: { + readonly model: Model | undefined; + readonly thinkingLevel: ModelThinkingLevel; + readonly isStreaming: boolean; + readonly messageCount: number; + }; + }; +} + +export interface SessionListEntry { + path: string; + id: string; + name?: string; + startedAt?: number; +} + +export interface BridgeRuntime { + readonly session: BridgeSession; + newSession(): Promise<{ cancelled: boolean }>; + switchSession(path: string): Promise<{ cancelled: boolean }>; + /** List available session files. */ + listSessions(): Promise; + /** List models the registry can use. */ + getAvailableModels(): Promise[]>; +} + +export type ClientSender = (data: string) => void; +export type RemoveClient = () => void; + +export interface BridgeOptions { + runtime: BridgeRuntime; + onClientCountChange?: (count: number) => void; +} + +export class Bridge { + private readonly runtime: BridgeRuntime; + private readonly onClientCountChange: ((count: number) => void) | undefined; + private unsubscribe: (() => void) | null = null; + private clients = new Map(); + private disposed = false; + + constructor(opts: BridgeOptions) { + this.runtime = opts.runtime; + if (opts.onClientCountChange !== undefined) { + this.onClientCountChange = opts.onClientCountChange; + } + this.subscribeCurrent(); + } + + private subscribeCurrent(): void { + if (this.unsubscribe) { + this.unsubscribe(); + this.unsubscribe = null; + } + this.unsubscribe = this.runtime.session.subscribe((event) => { + this.broadcastEvent(event); + }); + } + + rebind(): void { + if (this.disposed) return; + this.subscribeCurrent(); + } + + private broadcastEvent(event: unknown): void { + const payload = JSON.stringify(event); + for (const send of this.clients.keys()) { + try { + send(payload); + } catch { + // best-effort + } + } + } + + private respond(send: ClientSender, response: ServerResponse): void { + try { + send(JSON.stringify(response)); + } catch { + // best-effort + } + } + + addClient(send: ClientSender): RemoveClient { + this.clients.set(send, true); + if (this.onClientCountChange !== undefined) { + this.onClientCountChange(this.clients.size); + } + return () => { + if (this.clients.delete(send)) { + if (this.onClientCountChange !== undefined) { + this.onClientCountChange(this.clients.size); + } + } + }; + } + + async handleCommand(raw: unknown, replyTo: ClientSender): Promise { + if (this.disposed) return; + const cmd = parseClientCommand(raw); + if (!cmd) { + const id = (raw as { id?: unknown })?.id; + this.respond(replyTo, { + type: "response", + id: typeof id === "string" ? id : "unknown", + ok: false, + error: "invalid command", + }); + return; + } + try { + await this.dispatch(cmd, replyTo); + } catch (err) { + this.respond(replyTo, { + type: "response", + id: cmd.id, + ok: false, + error: (err as Error).message, + }); + } + } + + private async dispatch( + cmd: ClientCommand, + replyTo: ClientSender, + ): Promise { + const session = this.runtime.session; + switch (cmd.type) { + case "prompt": { + const opts: { images?: unknown[] } = {}; + if (cmd.images) opts.images = cmd.images; + await session.prompt(cmd.text, cmd.images ? opts : undefined); + this.respond(replyTo, { type: "response", id: cmd.id, ok: true }); + return; + } + case "abort": { + await session.abort(); + this.respond(replyTo, { type: "response", id: cmd.id, ok: true }); + return; + } + case "set_model": { + const models = await this.runtime.getAvailableModels(); + const target = models.find( + (m) => m.provider === cmd.provider && m.id === cmd.modelId, + ); + if (!target) { + this.respond(replyTo, { + type: "response", + id: cmd.id, + ok: false, + error: `unknown model: ${cmd.provider}/${cmd.modelId}`, + }); + return; + } + await session.setModel(target); + this.respond(replyTo, { type: "response", id: cmd.id, ok: true }); + return; + } + case "set_thinking_level": { + // SDK's setThinkingLevel type is `ThinkingLevel` (no "off"), but + // the runtime accepts "off". Cast at the call site. + session.setThinkingLevel(cmd.level as never); + this.respond(replyTo, { type: "response", id: cmd.id, ok: true }); + return; + } + case "get_state": { + const s = session.agent.state; + this.respond(replyTo, { + type: "response", + id: cmd.id, + ok: true, + data: { + model: s.model + ? { provider: s.model.provider, id: s.model.id, name: s.model.name } + : null, + thinkingLevel: s.thinkingLevel, + isStreaming: s.isStreaming, + messageCount: s.messageCount, + messages: session.messages, + }, + }); + return; + } + case "list_sessions": { + const sessions = await this.runtime.listSessions(); + this.respond(replyTo, { + type: "response", + id: cmd.id, + ok: true, + data: { sessions }, + }); + return; + } + case "list_models": { + const models = await this.runtime.getAvailableModels(); + const slim = models.map((m) => ({ + provider: m.provider, + id: m.id, + name: m.name, + })); + this.respond(replyTo, { + type: "response", + id: cmd.id, + ok: true, + data: { models: slim }, + }); + return; + } + case "new_session": + case "switch_session": { + // Handled by handleSessionCommand, which has access to rebind(). + await this.handleSessionCommand(cmd, replyTo); + return; + } + default: { + // Exhaustiveness check. + const _exhaustive: never = cmd; + this.respond(replyTo, { + type: "response", + id: (_exhaustive as { id: string }).id, + ok: false, + error: "unhandled command", + }); + } + } + } + + private async handleSessionCommand( + cmd: Extract, + replyTo: ClientSender, + ): Promise { + const result = + cmd.type === "new_session" + ? await this.runtime.newSession() + : await this.runtime.switchSession(cmd.path); + if (result.cancelled) { + this.respond(replyTo, { + type: "response", + id: cmd.id, + ok: false, + error: "cancelled by extension", + }); + return; + } + this.rebind(); + this.respond(replyTo, { type: "response", id: cmd.id, ok: true }); + } + + dispose(): void { + if (this.disposed) return; + this.disposed = true; + if (this.unsubscribe) { + this.unsubscribe(); + this.unsubscribe = null; + } + this.clients.clear(); + if (this.onClientCountChange !== undefined) { + this.onClientCountChange(0); + } + } + + get clientCount(): number { + return this.clients.size; + } +} diff --git a/pi-web/src/open-browser.ts b/pi-web/src/open-browser.ts new file mode 100644 index 00000000..1ce55cbf --- /dev/null +++ b/pi-web/src/open-browser.ts @@ -0,0 +1,52 @@ +// Cross-platform "open URL in default browser" helper. +// +// macOS: open +// Linux: xdg-open +// Windows: cmd /c start "" +// Other: log a warning; the user opens the URL manually. + +import { spawn as defaultSpawn } from "node:child_process"; +import type { SpawnOptions } from "node:child_process"; + +export type SpawnFn = ( + command: string, + args: readonly string[], + options: SpawnOptions, +) => { unref(): void }; + +export function openBrowser( + url: string, + spawn: SpawnFn = defaultSpawn as unknown as SpawnFn, +): void { + const platform = process.platform; + let cmd: string; + let args: string[]; + + if (platform === "darwin") { + cmd = "open"; + args = [url]; + } else if (platform === "linux") { + cmd = "xdg-open"; + args = [url]; + } else if (platform === "win32") { + cmd = "cmd"; + args = ["/c", "start", "", url]; + } else { + console.warn( + `[pi-web] Cannot auto-open browser on platform "${platform}". Open ${url} manually.`, + ); + return; + } + + try { + const child = spawn(cmd, args, { + detached: true, + stdio: "ignore", + }); + child.unref(); + } catch (err) { + console.warn( + `[pi-web] Failed to spawn ${cmd}: ${(err as Error).message}. Open ${url} manually.`, + ); + } +} diff --git a/pi-web/src/protocol.ts b/pi-web/src/protocol.ts new file mode 100644 index 00000000..3f8c388e --- /dev/null +++ b/pi-web/src/protocol.ts @@ -0,0 +1,153 @@ +// Wire protocol for pi-web. +// +// Framing: WebSocket text frames, one JSON message per frame. +// Discriminated by `type`. Client commands have required `id` (uuid v4 +// string). Server responses match `id`. Server events have no `id` and +// pass through the SDK's AgentSessionEvent union as-is. + +import type { ThinkingLevel, ModelThinkingLevel, ImageContent } from "@earendil-works/pi-ai"; + +/* ----- Client commands (browser -> server) ----- */ + +export type ClientCommand = + | { type: "prompt"; id: string; text: string; images?: ImageContent[] } + | { type: "abort"; id: string } + | { type: "new_session"; id: string } + | { type: "switch_session"; id: string; path: string } + | { type: "set_model"; id: string; provider: string; modelId: string } + | { type: "set_thinking_level"; id: string; level: ModelThinkingLevel } + | { type: "list_sessions"; id: string } + | { type: "list_models"; id: string } + | { type: "get_state"; id: string }; + +/* ----- Server responses (server -> browser, matched by id) ----- */ + +export type ServerResponse = + | { type: "response"; id: string; ok: true; data?: unknown } + | { type: "response"; id: string; ok: false; error: string }; + +/* ----- Server events (server -> browser, push only) ----- */ + +export type ServerEvent = { + type: + | "session_start" + | "session_before_switch" + | "session_before_fork" + | "session_before_compact" + | "agent_start" + | "agent_end" + | "turn_start" + | "turn_end" + | "message_start" + | "message_update" + | "message_end" + | "tool_execution_start" + | "tool_execution_update" + | "tool_execution_end" + | "queue_update" + | "compaction_start" + | "compaction_end" + | "extension_error"; + [k: string]: unknown; +}; + +/* ----- Parsing + type guards ----- */ + +const THINKING_LEVELS: ReadonlySet = new Set([ + "off", + "minimal", + "low", + "medium", + "high", + "xhigh", +]); + +function isThinkingLevel(v: string): v is ModelThinkingLevel { + return THINKING_LEVELS.has(v); +} + +function isObject(v: unknown): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v); +} + +function isNonEmptyString(v: unknown): v is string { + return typeof v === "string" && v.length > 0; +} + +export function parseClientCommand(raw: unknown): ClientCommand | null { + if (!isObject(raw)) return null; + const { type, id } = raw; + if (!isNonEmptyString(type) || !isNonEmptyString(id)) return null; + + switch (type) { + case "prompt": { + const { text, images } = raw; + if (!isNonEmptyString(text)) return null; + if (images !== undefined && !Array.isArray(images)) return null; + const cmd: ClientCommand = { type: "prompt", id, text }; + if (images !== undefined) { + (cmd as { images?: ImageContent[] }).images = + images as ImageContent[]; + } + return cmd; + } + case "abort": + return { type: "abort", id }; + case "new_session": + return { type: "new_session", id }; + case "switch_session": { + const { path } = raw; + if (!isNonEmptyString(path)) return null; + return { type: "switch_session", id, path }; + } + case "set_model": { + const { provider, modelId } = raw; + if (!isNonEmptyString(provider) || !isNonEmptyString(modelId)) { + return null; + } + return { type: "set_model", id, provider, modelId }; + } + case "set_thinking_level": { + const { level } = raw; + if (!isNonEmptyString(level) || !isThinkingLevel(level)) { + return null; + } + return { type: "set_thinking_level", id, level }; + } + case "list_sessions": + return { type: "list_sessions", id }; + case "list_models": + return { type: "list_models", id }; + case "get_state": + return { type: "get_state", id }; + default: + return null; + } +} + +export function isServerEvent(value: unknown): value is ServerEvent { + if (!isObject(value)) return false; + const { type } = value; + if (!isNonEmptyString(type)) return false; + const allowed: ReadonlySet = new Set([ + "session_start", + "session_before_switch", + "session_before_fork", + "session_before_compact", + "agent_start", + "agent_end", + "turn_start", + "turn_end", + "message_start", + "message_update", + "message_end", + "tool_execution_start", + "tool_execution_update", + "tool_execution_end", + "queue_update", + "compaction_start", + "compaction_end", + "extension_error", + ]); + return allowed.has(type); +} diff --git a/pi-web/src/runtime.ts b/pi-web/src/runtime.ts new file mode 100644 index 00000000..eedc0175 --- /dev/null +++ b/pi-web/src/runtime.ts @@ -0,0 +1,121 @@ +// Real BridgeRuntime implementation backed by the pi SDK. +// +// Uses createAgentSessionRuntime() to get a runtime that supports +// newSession/switchSession/fork, and a DefaultResourceLoader so the +// user's installed extensions/skills/prompts/context files are loaded +// from the same locations the TUI uses. + +import { + createAgentSessionRuntime, + createAgentSessionFromServices, + createAgentSessionServices, + getAgentDir, + AuthStorage, + ModelRegistry, + SessionManager, + SettingsManager, + DefaultResourceLoader, + type CreateAgentSessionRuntimeFactory, +} from "@earendil-works/pi-coding-agent"; +import { getModel } from "@earendil-works/pi-ai"; +import type { Model } from "@earendil-works/pi-ai"; +import { + type BridgeRuntime, + type BridgeSession, + type SessionListEntry, +} from "./bridge.ts"; + +export interface RealRuntime extends BridgeRuntime { + dispose(): Promise; +} + +export async function createRealRuntime(opts: { + cwd: string; +}): Promise { + const authStorage = AuthStorage.create(); + const modelRegistry = ModelRegistry.create(authStorage); + + const settingsManager = SettingsManager.create(opts.cwd, getAgentDir()); + + const loader = new DefaultResourceLoader({ + cwd: opts.cwd, + agentDir: getAgentDir(), + settingsManager, + }); + await loader.reload(); + + // Pick a default model: first available, falling back to first known + // (may lack API key, but setModel would fail later in that case). + const available = await modelRegistry.getAvailable(); + let model: Model | undefined = available[0]; + if (!model) { + const all = modelRegistry.getAll(); + model = all.find( + (m) => m.provider === "anthropic" || m.provider === "openai", + ); + } + if (!model) { + model = getModel("anthropic", "claude-sonnet-4-5") ?? undefined; + } + + const sessionManager = SessionManager.create(opts.cwd); + + const createRuntime: CreateAgentSessionRuntimeFactory = async ({ + cwd, + sessionManager: sm, + sessionStartEvent, + }) => { + const services = await createAgentSessionServices({ cwd }); + const fromServicesOpts: Record = { + services, + sessionManager: sm, + thinkingLevel: "medium", + }; + if (sessionStartEvent !== undefined) { + fromServicesOpts.sessionStartEvent = sessionStartEvent; + } + if (model) { + fromServicesOpts.model = model; + } + const result = await createAgentSessionFromServices( + fromServicesOpts as unknown as Parameters[0], + ); + return { ...result, services, diagnostics: services.diagnostics }; + }; + + const runtime = await createAgentSessionRuntime(createRuntime, { + cwd: opts.cwd, + agentDir: getAgentDir(), + sessionManager, + }); + + const session = runtime.session as unknown as BridgeSession; + + return { + session, + async newSession() { + return runtime.newSession(); + }, + async switchSession(path) { + return runtime.switchSession(path); + }, + async listSessions() { + const list = await SessionManager.list(opts.cwd); + return list.map((entry) => { + const e: SessionListEntry = { path: entry.path, id: entry.id }; + if (entry.name !== undefined) e.name = entry.name; + e.startedAt = entry.modified.getTime(); + return e; + }); + }, + async getAvailableModels(): Promise[]> { + const all = await modelRegistry.getAvailable(); + return all; + }, + async dispose() { + // The SDK doesn't expose a clean shutdown for the runtime; for v1 + // we let the process exit handle it. The bridge's dispose() will + // unsubscribe from the session. + }, + }; +} diff --git a/pi-web/src/server.ts b/pi-web/src/server.ts new file mode 100644 index 00000000..cc9b363e --- /dev/null +++ b/pi-web/src/server.ts @@ -0,0 +1,257 @@ +// HTTP + WebSocket server. +// +// Static files: GET / from webRoot (default pi-web/src/web). +// Vendor: GET /vendor/ resolves to node_modules file. +// WebSocket: WS /ws forwards raw frames to the bridge. + +import { + createServer, + type IncomingMessage, + type ServerResponse, + type Server as HttpServer, +} from "node:http"; +import { readFile, stat } from "node:fs/promises"; +import { extname, join, normalize, resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { WebSocketServer, type WebSocket } from "ws"; +import { Bridge, type BridgeRuntime, type ClientSender } from "./bridge.ts"; +import { resolveVendorFile } from "./vendor.ts"; +import { createRealRuntime, type RealRuntime } from "./runtime.ts"; +import { openBrowser } from "./open-browser.ts"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const DEFAULT_WEB_ROOT = resolve(__dirname, "web"); + +export interface ServerDeps { + runtime: BridgeRuntime; + webRoot: string; + /** Map of additional static routes, e.g. { "/vendor/preact.js": "" }. */ + vendorRoutes?: Record; +} + +export interface ServerHandle { + server: HttpServer; + bridge: Bridge; + stop(): Promise; +} + +const MIME: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".mjs": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", +}; + +function isLoopback(host: string): boolean { + return ( + host === "127.0.0.1" || + host === "::1" || + host === "localhost" || + host === "::" + ); +} + +function defaultVendorRoutes(): Record { + try { + return { + "/vendor/preact.js": resolveVendorFile("preact"), + "/vendor/htm.js": resolveVendorFile("htm"), + }; + } catch { + return {}; + } +} + +export function startServer(opts: { + port: number; + host: string; + ctx: ServerDeps; +}): ServerHandle { + if (!isLoopback(opts.host)) { + throw new Error( + `Refusing to bind to non-loopback host "${opts.host}". Use 127.0.0.1 or ::1.`, + ); + } + const { runtime, webRoot, vendorRoutes = defaultVendorRoutes() } = opts.ctx; + + const bridge = new Bridge({ runtime }); + const wss = new WebSocketServer({ noServer: true }); + + const server = createServer(async (req, res) => { + try { + await handleHttp(req, res, webRoot, vendorRoutes); + } catch (err) { + res.statusCode = 500; + res.end(`Internal error: ${(err as Error).message}`); + } + }); + + server.on("upgrade", (req, socket, head) => { + if (req.url !== "/ws") { + socket.destroy(); + return; + } + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + }); + + wss.on("connection", (ws: WebSocket) => { + const send: ClientSender = (data) => { + if (ws.readyState === ws.OPEN) ws.send(data); + }; + const remove = bridge.addClient(send); + ws.on("close", remove); + ws.on("error", remove); + ws.on("message", (data) => { + const raw = data.toString(); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + return; + } + void bridge.handleCommand(parsed, send); + }); + }); + + server.listen(opts.port, opts.host); + + return { + server, + bridge, + stop: () => + new Promise((resolve) => { + bridge.dispose(); + if (typeof server.closeAllConnections === "function") { + server.closeAllConnections(); + } + wss.clients.forEach((c) => c.terminate()); + wss.close(); + server.close(() => resolve()); + // Failsafe: resolve after 500ms even if close() hangs on a + // stubborn connection. Better to leak briefly than to hang the + // test runner. + setTimeout(() => resolve(), 500).unref(); + }), + }; +} + +async function handleHttp( + req: IncomingMessage, + res: ServerResponse, + webRoot: string, + vendorRoutes: Record, +): Promise { + const url = new URL(req.url ?? "/", "http://localhost"); + const pathname = decodeURIComponent(url.pathname); + + if (pathname in vendorRoutes) { + const file = vendorRoutes[pathname]!; + const buf = await readFile(file); + res.statusCode = 200; + res.setHeader("Content-Type", "application/javascript; charset=utf-8"); + res.end(buf); + return; + } + + const rel = pathname === "/" ? "/index.html" : pathname; + const full = normalize(join(webRoot, rel)); + if (!full.startsWith(resolve(webRoot))) { + res.statusCode = 403; + res.end("Forbidden"); + return; + } + try { + const s = await stat(full); + if (!s.isFile()) throw new Error("not a file"); + } catch { + res.statusCode = 404; + res.end("Not Found"); + return; + } + const buf = await readFile(full); + res.statusCode = 200; + res.setHeader( + "Content-Type", + MIME[extname(full).toLowerCase()] ?? "application/octet-stream", + ); + res.end(buf); +} + +/* ----- CLI entry point ----- */ + +function parseArgs(argv: string[]): { cwd: string } { + let cwd = process.cwd(); + for (let i = 0; i < argv.length; i++) { + const a = argv[i]!; + if (a === "--cwd") { + const v = argv[++i]; + if (!v) throw new Error("--cwd requires a value"); + cwd = resolve(v); + } else if (a === "--help" || a === "-h") { + console.log("Usage: pi-web [--cwd ]"); + process.exit(0); + } else { + throw new Error(`Unknown argument: ${a}`); + } + } + return { cwd }; +} + +export async function runFromCli( + argv: string[] = process.argv.slice(2), +): Promise { + const { cwd } = parseArgs(argv); + const port = Number(process.env.PI_WEB_PORT ?? 7878); + if (!Number.isFinite(port) || port <= 0 || port > 65535) { + throw new Error(`Invalid PI_WEB_PORT: ${process.env.PI_WEB_PORT}`); + } + const autoOpen = process.env.PI_WEB !== "0"; + + const runtime: RealRuntime = await createRealRuntime({ cwd }); + const ctx: ServerDeps = { + runtime, + webRoot: DEFAULT_WEB_ROOT, + }; + const handle = startServer({ port, host: "127.0.0.1", ctx }); + + await new Promise((resolve) => + handle.server.once("listening", () => resolve()), + ); + const addr = handle.server.address(); + const portActual = + typeof addr === "object" && addr ? addr.port : port; + const url = `http://127.0.0.1:${portActual}/`; + + console.log(`[pi-web] cwd: ${cwd}`); + console.log(`[pi-web] listening on ${url}`); + + if (autoOpen) { + setTimeout(() => openBrowser(url), 50); + } + + const shutdown = async () => { + console.log("\n[pi-web] shutting down"); + await handle.stop(); + await runtime.dispose(); + process.exit(0); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +const isMain = + process.argv[1] !== undefined && + resolve(process.argv[1]) === resolve(__filename); +if (isMain) { + runFromCli().catch((err) => { + console.error(`[pi-web] fatal: ${(err as Error).message}`); + process.exit(1); + }); +} diff --git a/pi-web/src/vendor.ts b/pi-web/src/vendor.ts new file mode 100644 index 00000000..5c346297 --- /dev/null +++ b/pi-web/src/vendor.ts @@ -0,0 +1,47 @@ +// Resolve the on-disk paths of vendored browser modules. +// +// The server serves preact + htm from `pi-web/node_modules` via +// /vendor/* routes. The preact and htm packages have an `exports` +// field that blocks `require.resolve("preact/dist/preact.module.js")`. +// We work around this by resolving the package root (which is allowed), +// then walking the known layout: preact's browser file is +// `/dist/preact.module.js`; htm's browser file is +// `/dist/htm.module.js`. +// +// Note: we intentionally do NOT vendor `htm/preact`. That module does +// `import "preact"` (bare specifier), which the browser cannot resolve +// without an import map. Instead, components import htm and bind +// `html` themselves (see src/web/components/htm.js). + +import { createRequire } from "node:module"; +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; + +const require = createRequire(import.meta.url); + +type VendorName = "preact" | "htm"; + +function getPkgDir(pkg: string): string { + // require.resolve on the package root resolves to the entry point, + // which is in /dist/. dirname^2 is the package root. + const entry = require.resolve(pkg); + return dirname(dirname(entry)); +} + +const CANDIDATES: Record = { + preact: ["dist/preact.module.js", "dist/preact.mjs", "dist/preact.min.module.js"], + htm: ["dist/htm.module.js", "dist/htm.mjs"], +}; + +export function resolveVendorFile(name: VendorName): string { + const pkgDir = getPkgDir(name); + const errors: string[] = []; + for (const rel of CANDIDATES[name]) { + const p = join(pkgDir, rel); + if (existsSync(p)) return p; + errors.push(`not found: ${p}`); + } + throw new Error( + `Cannot resolve vendor file "${name}":\n ${errors.join("\n ")}`, + ); +} diff --git a/pi-web/src/web/app.js b/pi-web/src/web/app.js new file mode 100644 index 00000000..88c123d9 --- /dev/null +++ b/pi-web/src/web/app.js @@ -0,0 +1,276 @@ +import { h, render } from "/vendor/preact.js"; +import { html } from "./components/htm.js"; +import { Chat } from "./components/chat.js"; +import { SessionList } from "./components/session-list.js"; +import { ModelPicker } from "./components/model-picker.js"; +import { ThinkingPicker } from "./components/thinking-picker.js"; + +const WS_URL = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`; + +const state = { + connected: false, + messages: [], + sessions: [], + models: [], + model: null, + thinkingLevel: "medium", + isStreaming: false, + toast: null, + ws: null, + reconnectTimer: null, + eventSeq: 0, +}; + +function renderApp() { + render( + html` + +
+ <${Chat} + messages=${state.messages} + isStreaming=${state.isStreaming} + seq=${state.eventSeq} + /> +
+
+ + + +
+ ${state.toast ? html`
${state.toast.text}
` : null} + `, + document.getElementById("app"), + ); +} + +function showToast(text, kind = "info", ms = 3000) { + state.toast = { text, kind }; + renderApp(); + setTimeout(() => { + if (state.toast && state.toast.text === text) { + state.toast = null; + renderApp(); + } + }, ms); +} + +function sendCommand(cmd) { + if (!state.ws || state.ws.readyState !== WebSocket.OPEN) { + showToast("Not connected", "info"); + return; + } + state.ws.send(JSON.stringify(cmd)); +} + +function onNewSession() { + sendCommand({ type: "new_session", id: crypto.randomUUID() }); +} + +function onSwitchSession(path) { + sendCommand({ type: "switch_session", id: crypto.randomUUID(), path }); +} + +function onSetModel(m) { + sendCommand({ + type: "set_model", + id: crypto.randomUUID(), + provider: m.provider, + modelId: m.id, + }); +} + +function onSetThinking(level) { + sendCommand({ + type: "set_thinking_level", + id: crypto.randomUUID(), + level, + }); +} + +function onSendPrompt() { + const el = document.getElementById("prompt"); + if (!el) return; + const text = el.value; + if (!text.trim()) return; + sendCommand({ type: "prompt", id: crypto.randomUUID(), text }); + el.value = ""; + // No optimistic UI: the server's message_start event for the user + // message will populate state.messages. +} + +function onAbort() { + sendCommand({ type: "abort", id: crypto.randomUUID() }); +} + +function onPromptKey(e) { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + onSendPrompt(); + } +} + +/* ----- WebSocket lifecycle ----- */ + +function connect() { + const ws = new WebSocket(WS_URL); + state.ws = ws; + ws.addEventListener("open", () => { + state.connected = true; + state.reconnectTimer = null; + sendCommand({ type: "get_state", id: crypto.randomUUID() }); + sendCommand({ type: "list_sessions", id: crypto.randomUUID() }); + sendCommand({ type: "list_models", id: crypto.randomUUID() }); + renderApp(); + }); + ws.addEventListener("close", () => { + state.connected = false; + state.isStreaming = false; + renderApp(); + scheduleReconnect(); + }); + ws.addEventListener("error", () => { + ws.close(); + }); + ws.addEventListener("message", (e) => onMessage(e.data)); +} + +function scheduleReconnect() { + if (state.reconnectTimer) return; + state.reconnectTimer = setTimeout(connect, 1000); +} + +function onMessage(raw) { + let msg; + try { + msg = JSON.parse(raw); + } catch { + return; + } + if (msg.type === "response") { + onResponse(msg); + } else if (isEventType(msg.type)) { + onEvent(msg); + } +} + +function onResponse(msg) { + if (!msg.ok) { + showToast(msg.error ?? "Error", "info"); + return; + } + const data = msg.data; + if (data && Array.isArray(data.sessions)) { + state.sessions = data.sessions; + renderApp(); + return; + } + if (data && Array.isArray(data.models)) { + state.models = data.models; + renderApp(); + return; + } + if (data && "thinkingLevel" in data) { + state.model = data.model; + state.thinkingLevel = data.thinkingLevel; + state.isStreaming = data.isStreaming; + state.messages = data.messages ?? []; + renderApp(); + return; + } + // Successful new_session / switch_session / set_model / + // set_thinking_level — re-fetch get_state so the UI reflects the + // change without waiting for the next event. + if (data === undefined) { + sendCommand({ type: "get_state", id: crypto.randomUUID() }); + } +} + +function onEvent(msg) { + state.eventSeq++; + switch (msg.type) { + case "message_start": + upsertMessage(msg.message); + break; + case "message_update": { + upsertMessage(msg.message); + break; + } + case "message_end": + upsertMessage(msg.message); + break; + case "agent_start": + state.isStreaming = true; + break; + case "agent_end": + state.isStreaming = false; + break; + case "session_start": + sendCommand({ type: "list_sessions", id: crypto.randomUUID() }); + break; + default: + break; + } + renderApp(); +} + +/** Replace a message by id; append if not present. */ +function upsertMessage(m) { + if (!m || m.id === undefined) return; + const idx = state.messages.findIndex((x) => x && x.id === m.id); + if (idx === -1) { + state.messages = [...state.messages, m]; + } else { + const next = state.messages.slice(); + next[idx] = m; + state.messages = next; + } +} + +function isEventType(t) { + return [ + "session_start", + "session_before_switch", + "session_before_fork", + "session_before_compact", + "agent_start", + "agent_end", + "turn_start", + "turn_end", + "message_start", + "message_update", + "message_end", + "tool_execution_start", + "tool_execution_update", + "tool_execution_end", + "queue_update", + "compaction_start", + "compaction_end", + "extension_error", + ].includes(t); +} + +connect(); +renderApp(); diff --git a/pi-web/src/web/components/chat.js b/pi-web/src/web/components/chat.js new file mode 100644 index 00000000..ecaa5f16 --- /dev/null +++ b/pi-web/src/web/components/chat.js @@ -0,0 +1,78 @@ +import { h } from "/vendor/preact.js"; +import { html } from "./htm.js"; +import { ToolCall } from "./tool-call.js"; + +export function Chat({ messages, isStreaming }) { + return html` +
+ ${messages.map((m, i) => renderMessage(m, i))} + ${isStreaming ? html`
assistant
` : null} +
+ `; +} + +function renderMessage(m, key) { + if (!m) return null; + if (m.role === "user") { + return html` +
+
user
+
${typeof m.content === "string" ? m.content : renderBlocks(m.content)}
+
+ `; + } + if (m.role === "assistant") { + return html` +
+
assistant
+ ${renderAssistant(m)} +
+ `; + } + if (m.role === "toolResult") { + return html` +
+
tool · ${m.toolName}
+
${(m.content ?? []).map((c) => c.text ?? "").join("\n")}
+
+ `; + } + return null; +} + +function renderAssistant(m) { + if (typeof m.content === "string") { + return html`
${m.content}
${renderAssistantError(m)}`; + } + if (!Array.isArray(m.content)) return null; + const blocks = m.content.map((block, i) => { + if (block.type === "text") { + return html`
${block.text}
`; + } + if (block.type === "thinking") { + return html`
${block.thinking}
`; + } + if (block.type === "toolCall") { + return html`<${ToolCall} key=${i} call=${block} />`; + } + return null; + }); + return html`
${blocks}${renderAssistantError(m)}
`; +} + +function renderAssistantError(m) { + if (m.stopReason === "error" && m.errorMessage) { + return html`
${m.errorMessage}
`; + } + if (m.stopReason === "aborted") { + return html`
aborted
`; + } + return null; +} + +function renderBlocks(blocks) { + if (!Array.isArray(blocks)) return ""; + return blocks + .map((b) => (b.type === "text" ? b.text : "")) + .join("\n"); +} diff --git a/pi-web/src/web/components/htm.js b/pi-web/src/web/components/htm.js new file mode 100644 index 00000000..2c349ab4 --- /dev/null +++ b/pi-web/src/web/components/htm.js @@ -0,0 +1,9 @@ +// Bound `html` template tag for htm + preact. We bind here (rather than +// importing htm/preact) because htm/preact's published file does +// `import "preact"` (a bare specifier) which the browser can't resolve +// without an import map. This wrapper keeps the binding local and lets +// the components import a relative path. +import { h } from "/vendor/preact.js"; +import htm from "/vendor/htm.js"; + +export const html = htm.bind(h); diff --git a/pi-web/src/web/components/model-picker.js b/pi-web/src/web/components/model-picker.js new file mode 100644 index 00000000..bb00234b --- /dev/null +++ b/pi-web/src/web/components/model-picker.js @@ -0,0 +1,25 @@ +import { h } from "/vendor/preact.js"; +import { html } from "./htm.js"; + +export function ModelPicker({ models, current, onChange }) { + return html` + + `; +} diff --git a/pi-web/src/web/components/session-list.js b/pi-web/src/web/components/session-list.js new file mode 100644 index 00000000..b2b24356 --- /dev/null +++ b/pi-web/src/web/components/session-list.js @@ -0,0 +1,21 @@ +import { h } from "/vendor/preact.js"; +import { html } from "./htm.js"; + +export function SessionList({ sessions, onNew, onSwitch }) { + return html` +
+ +
    + ${sessions.length === 0 + ? html`
  • (no sessions yet)
  • ` + : sessions.map( + (s) => html` +
  • onSwitch(s.path)} title=${s.path}> + ${s.name ?? s.id} +
  • + `, + )} +
+
+ `; +} diff --git a/pi-web/src/web/components/thinking-picker.js b/pi-web/src/web/components/thinking-picker.js new file mode 100644 index 00000000..c38d1a74 --- /dev/null +++ b/pi-web/src/web/components/thinking-picker.js @@ -0,0 +1,18 @@ +import { h } from "/vendor/preact.js"; +import { html } from "./htm.js"; + +const LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"]; + +export function ThinkingPicker({ current, disabled, onChange }) { + return html` + + `; +} diff --git a/pi-web/src/web/components/tool-call.js b/pi-web/src/web/components/tool-call.js new file mode 100644 index 00000000..0d6d301e --- /dev/null +++ b/pi-web/src/web/components/tool-call.js @@ -0,0 +1,13 @@ +import { h } from "/vendor/preact.js"; +import { html } from "./htm.js"; + +export function ToolCall({ call }) { + return html` +
+ ${call.name} + ${call.arguments + ? html`
${JSON.stringify(call.arguments, null, 2)}
` + : null} +
+ `; +} diff --git a/pi-web/src/web/index.html b/pi-web/src/web/index.html new file mode 100644 index 00000000..9e5ea1dd --- /dev/null +++ b/pi-web/src/web/index.html @@ -0,0 +1,13 @@ + + + + + + pi-web + + + +
+ + + diff --git a/pi-web/src/web/style.css b/pi-web/src/web/style.css new file mode 100644 index 00000000..bdd76f50 --- /dev/null +++ b/pi-web/src/web/style.css @@ -0,0 +1,207 @@ +:root { + --bg: #1a1a1a; + --bg-elev: #232323; + --fg: #e6e6e6; + --fg-dim: #9a9a9a; + --accent: #7aa2f7; + --border: #333; + --error: #f7768e; + --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + height: 100%; + background: var(--bg); + color: var(--fg); + font-family: var(--font); + font-size: 14px; + line-height: 1.5; +} + +#app { + display: grid; + grid-template-columns: 260px 1fr; + grid-template-rows: 1fr auto; + height: 100vh; +} + +#sidebar { + grid-row: 1 / span 2; + background: var(--bg-elev); + border-right: 1px solid var(--border); + padding: 12px; + overflow-y: auto; +} + +#sidebar h2 { + font-size: 12px; + text-transform: uppercase; + color: var(--fg-dim); + letter-spacing: 0.05em; + margin: 16px 0 6px 0; +} + +#sidebar h2:first-child { margin-top: 0; } + +#chat { + grid-column: 2; + grid-row: 1; + overflow-y: auto; + padding: 16px; +} + +#prompt-row { + grid-column: 2; + grid-row: 2; + border-top: 1px solid var(--border); + background: var(--bg-elev); + padding: 8px; + display: flex; + gap: 8px; +} + +#prompt { + flex: 1; + resize: none; + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 8px; + font-family: var(--font); + font-size: 14px; + min-height: 38px; + max-height: 200px; +} + +#prompt:focus { outline: none; border-color: var(--accent); } + +button { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 6px 12px; + cursor: pointer; + font-family: var(--font); + font-size: 13px; +} + +button:hover:not(:disabled) { border-color: var(--accent); } +button:disabled { opacity: 0.5; cursor: not-allowed; } + +select, input[type="text"] { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 4px; + padding: 4px 8px; + font-family: var(--font); + font-size: 13px; + width: 100%; +} + +.session-list { list-style: none; padding: 0; margin: 0; } +.session-list li { + padding: 6px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; + color: var(--fg-dim); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.session-list li:hover { background: var(--bg); color: var(--fg); } +.session-list li.active { background: var(--bg); color: var(--fg); } + +.message { margin: 12px 0; } +.message.user { color: var(--fg); } +.message.assistant { color: var(--fg); } +.message .role { + font-size: 11px; + text-transform: uppercase; + color: var(--fg-dim); + margin-bottom: 2px; + letter-spacing: 0.05em; +} + +.message .thinking { + color: var(--fg-dim); + font-style: italic; + font-size: 13px; + margin: 4px 0; + white-space: pre-wrap; +} + +.message .tool-call { + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: 4px; + padding: 8px; + margin: 6px 0; + font-family: var(--font-mono); + font-size: 12px; +} + +.message .tool-call .name { + color: var(--accent); + font-weight: 600; +} + +.message .tool-result { + background: var(--bg-elev); + border-left: 3px solid var(--border); + padding: 6px 8px; + margin: 4px 0; + font-family: var(--font-mono); + font-size: 12px; + white-space: pre-wrap; + color: var(--fg-dim); +} + +.message .text { + white-space: pre-wrap; +} + +.message .error { + margin-top: 6px; + padding: 6px 8px; + border-left: 3px solid var(--error); + background: rgba(247, 118, 142, 0.08); + color: var(--error); + font-family: var(--font-mono); + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; +} + +.message .error { + margin-top: 6px; + padding: 6px 8px; + border-left: 3px solid var(--error); + background: rgba(247, 118, 142, 0.08); + color: var(--error); + font-family: var(--font-mono); + font-size: 12px; + white-space: pre-wrap; + word-break: break-word; +} + +.toast { + position: fixed; + bottom: 16px; + right: 16px; + background: var(--error); + color: white; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; + z-index: 10; +} +.toast.info { background: var(--accent); color: var(--bg); } diff --git a/pi-web/tests/bridge.test.ts b/pi-web/tests/bridge.test.ts new file mode 100644 index 00000000..d87d44ab --- /dev/null +++ b/pi-web/tests/bridge.test.ts @@ -0,0 +1,409 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { Bridge, type BridgeSession } from "../src/bridge.ts"; +import type { ServerEvent } from "../src/protocol.ts"; + +// All test fixtures are typed as any to dodge the complexity of +// satisfying the full BridgeSession/BridgeRuntime interfaces. The +// bridge's runtime contract is verified by these tests by behavior, not +// by static type conformance. + +type AnySession = any; +type AnyRuntime = any; + +interface FakeClient { + sent: string[]; + closed: boolean; + send(data: string): void; + close(): void; +} + +function makeClient(): FakeClient { + return { + sent: [], + closed: false, + send(data: string) { + this.sent.push(data); + }, + close() { + this.closed = true; + }, + }; +} + +function makeSession(): AnySession { + const events: Array<{ cb: (e: unknown) => void }> = []; + const state: { subscribed: number; unsubscribed: number } = { + subscribed: 0, + unsubscribed: 0, + }; + return { + events, + get subscribed() { + return state.subscribed; + }, + get unsubscribed() { + return state.unsubscribed; + }, + subscribe(cb: (e: unknown) => void) { + events.push({ cb }); + state.subscribed++; + return () => { + state.unsubscribed++; + }; + }, + }; +} + +function makeDispatchSession(): AnySession { + const base = makeSession(); + base.prompt = async (text: string, opts?: { images?: unknown[] }) => { + base._calls.prompt.push({ text, images: opts?.images }); + }; + base.abort = async () => { + base._calls.abort++; + }; + base.setModel = async (m: { provider: string; id: string }) => { + base._calls.setModel.push(m); + return true; + }; + base.setThinkingLevel = (level: string) => { + base._calls.setThinkingLevel.push(level); + }; + base._calls = { + prompt: [], + abort: 0, + setModel: [], + setThinkingLevel: [], + }; + base.messages = []; + base.agent = { + state: { + model: { provider: "anthropic", id: "claude-sonnet-4-5" }, + thinkingLevel: "medium", + isStreaming: false, + messageCount: 0, + }, + }; + return base; +} + +function makeRuntime(session: AnySession): AnyRuntime { + const calls: { newSessionCalls: number; switchSessionCalls: string[] } = { + newSessionCalls: 0, + switchSessionCalls: [], + }; + const runtime: any = { + session, + async newSession() { + calls.newSessionCalls++; + return { cancelled: false }; + }, + async switchSession(p: string) { + calls.switchSessionCalls.push(p); + return { cancelled: false }; + }, + async listSessions() { + return []; + }, + async getAvailableModels() { + return []; + }, + get newSessionCalls() { + return calls.newSessionCalls; + }, + get switchSessionCalls() { + return calls.switchSessionCalls; + }, + }; + return runtime; +} + +test("Bridge: subscribes to runtime.session on construction", () => { + const session = makeSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + assert.equal(session.subscribed, 1); + bridge.dispose(); + assert.equal(session.unsubscribed, 1); +}); + +test("Bridge: forwards session events to all client senders", () => { + const session = makeSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c1 = makeClient(); + const c2 = makeClient(); + bridge.addClient((data) => c1.send(data)); + bridge.addClient((data) => c2.send(data)); + + const event: ServerEvent = { type: "agent_start" }; + session.events[0]!.cb(event); + + assert.equal(c1.sent.length, 1); + assert.equal(c2.sent.length, 1); + assert.deepEqual(JSON.parse(c1.sent[0]!), { type: "agent_start" }); + bridge.dispose(); +}); + +test("Bridge: removeClient stops further sends", () => { + const session = makeSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const remove = bridge.addClient((data) => c.send(data)); + + session.events[0]!.cb({ type: "agent_start" }); + assert.equal(c.sent.length, 1); + + remove(); + session.events[0]!.cb({ type: "agent_start" }); + assert.equal(c.sent.length, 1); + bridge.dispose(); +}); + +test("Bridge: per-client send errors don't crash the bridge", () => { + const session = makeSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + bridge.addClient(() => { + throw new Error("send failed"); + }); + bridge.addClient((data) => c.send(data)); + assert.doesNotThrow(() => { + session.events[0]!.cb({ type: "agent_start" }); + }); + assert.equal(c.sent.length, 1); + bridge.dispose(); +}); + +test("Bridge: dispose unsubscribes and clears clients", () => { + const session = makeSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + bridge.addClient((data) => c.send(data)); + bridge.dispose(); + assert.equal(session.unsubscribed, 1); + assert.doesNotThrow(() => bridge.dispose()); +}); + +// === Command dispatch tests (Task 5) === + +test("Bridge.handleCommand: prompt -> session.prompt + ok response", async () => { + const session = makeDispatchSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand({ type: "prompt", id: "req-1", text: "hello" }, send); + + assert.equal(session._calls.prompt.length, 1); + assert.equal(session._calls.prompt[0]!.text, "hello"); + assert.equal(c.sent.length, 1); + assert.deepEqual(JSON.parse(c.sent[0]!), { + type: "response", + id: "req-1", + ok: true, + }); +}); + +test("Bridge.handleCommand: abort -> session.abort", async () => { + const session = makeDispatchSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand({ type: "abort", id: "req-2" }, send); + assert.equal(session._calls.abort, 1); + assert.deepEqual(JSON.parse(c.sent[0]!), { + type: "response", + id: "req-2", + ok: true, + }); +}); + +test("Bridge.handleCommand: set_model", async () => { + const session = makeDispatchSession(); + const runtime = makeRuntime(session); + runtime.getAvailableModels = async () => [ + { provider: "anthropic", id: "claude-sonnet-4-5" }, + ]; + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand( + { + type: "set_model", + id: "req-3", + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }, + send, + ); + assert.equal(session._calls.setModel.length, 1); + assert.deepEqual(session._calls.setModel[0]!, { + provider: "anthropic", + id: "claude-sonnet-4-5", + }); +}); + +test("Bridge.handleCommand: set_thinking_level", async () => { + const session = makeDispatchSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand( + { type: "set_thinking_level", id: "req-4", level: "high" }, + send, + ); + assert.deepEqual(session._calls.setThinkingLevel, ["high"]); +}); + +test("Bridge.handleCommand: get_state returns snapshot", async () => { + const session = makeDispatchSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand({ type: "get_state", id: "req-5" }, send); + const resp = JSON.parse(c.sent[0]!); + assert.equal(resp.type, "response"); + assert.equal(resp.id, "req-5"); + assert.ok(resp.ok); + assert.ok(resp.data); + assert.equal(resp.data.thinkingLevel, "medium"); + assert.equal(resp.data.isStreaming, false); +}); + +test("Bridge.handleCommand: invalid command -> ok:false response", async () => { + const session = makeDispatchSession(); + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand({ type: "nope", id: "req-6" }, send); + const resp = JSON.parse(c.sent[0]!); + assert.equal(resp.type, "response"); + assert.equal(resp.id, "req-6"); + assert.equal(resp.ok, false); + assert.match(resp.error, /invalid command/i); +}); + +test("Bridge.handleCommand: session.prompt throws -> ok:false", async () => { + const session = makeDispatchSession(); + session.prompt = async () => { + throw new Error("agent busy"); + }; + const runtime = makeRuntime(session); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand({ type: "prompt", id: "req-7", text: "hi" }, send); + const resp = JSON.parse(c.sent[0]!); + assert.equal(resp.ok, false); + assert.match(resp.error, /agent busy/); +}); + +// === Session replacement tests (Task 6) === + +/** + * Replace the session the runtime points at. The bridge subscribes via + * the runtime.session getter, so we mutate the runtime's session field + * and patch the getter. + */ +function replaceSession(runtime: AnyRuntime, newSession: AnySession) { + Object.defineProperty(runtime, "session", { + get() { + return newSession; + }, + configurable: true, + }); +} + +test("Bridge.handleCommand: new_session rebinds to new session", async () => { + const sessionA = makeSession(); + const runtime = makeRuntime(sessionA); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + const sessionB = makeSession(); + replaceSession(runtime, sessionB); + + await bridge.handleCommand({ type: "new_session", id: "req-1" }, send); + assert.equal(sessionA.unsubscribed, 1); + assert.equal(sessionB.subscribed, 1); +}); + +test("Bridge.handleCommand: switch_session rebinds", async () => { + const sessionA = makeSession(); + const runtime = makeRuntime(sessionA); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + const sessionB = makeSession(); + replaceSession(runtime, sessionB); + + await bridge.handleCommand( + { type: "switch_session", id: "req-2", path: "/tmp/foo.jsonl" }, + send, + ); + assert.equal(sessionA.unsubscribed, 1); + assert.equal(sessionB.subscribed, 1); + assert.deepEqual(runtime.switchSessionCalls, ["/tmp/foo.jsonl"]); +}); + +test("Bridge.handleCommand: cancelled new_session does NOT rebind", async () => { + const sessionA = makeSession(); + const runtime = makeRuntime(sessionA); + runtime.newSession = async () => ({ cancelled: true }); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + await bridge.handleCommand({ type: "new_session", id: "req-3" }, send); + assert.equal(sessionA.unsubscribed, 0); + assert.equal(sessionA.subscribed, 1); + const resp = JSON.parse(c.sent[0]!); + assert.equal(resp.ok, false); + assert.match(resp.error, /cancelled/); +}); + +test("Bridge: events from new session are forwarded after rebind", async () => { + const sessionA = makeSession(); + const runtime = makeRuntime(sessionA); + const bridge = new Bridge({ runtime }); + const c = makeClient(); + const send = (data: string) => c.send(data); + bridge.addClient(send); + + const sessionB = makeSession(); + replaceSession(runtime, sessionB); + await bridge.handleCommand({ type: "new_session", id: "req-4" }, send); + + sessionB.events[0]!.cb({ type: "agent_start" }); + assert.equal(c.sent.length, 2); + const last = JSON.parse(c.sent[c.sent.length - 1]!); + assert.deepEqual(last, { type: "agent_start" }); +}); diff --git a/pi-web/tests/open-browser.test.ts b/pi-web/tests/open-browser.test.ts new file mode 100644 index 00000000..18be2116 --- /dev/null +++ b/pi-web/tests/open-browser.test.ts @@ -0,0 +1,66 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { openBrowser, type SpawnFn } from "../src/open-browser.ts"; + +function makeFakeSpawn(): { + fn: SpawnFn; + calls: Array<{ cmd: string; args: readonly string[]; opts: unknown }>; +} { + const calls: Array<{ cmd: string; args: readonly string[]; opts: unknown }> = []; + const fn: SpawnFn = (cmd, args, opts) => { + calls.push({ cmd, args, opts }); + return { unref: () => {} }; + }; + return { fn, calls }; +} + +test("openBrowser: spawns 'open' on darwin with detached/ignore", () => { + const original = process.platform; + Object.defineProperty(process, "platform", { value: "darwin" }); + try { + const { fn, calls } = makeFakeSpawn(); + openBrowser("http://127.0.0.1:7878/", fn); + assert.equal(calls.length, 1); + assert.equal(calls[0]!.cmd, "open"); + assert.deepEqual([...calls[0]!.args], ["http://127.0.0.1:7878/"]); + const opts = calls[0]!.opts as { detached: boolean; stdio: string }; + assert.equal(opts.detached, true); + assert.equal(opts.stdio, "ignore"); + } finally { + Object.defineProperty(process, "platform", { value: original }); + } +}); + +test("openBrowser: spawns 'xdg-open' on linux", () => { + const original = process.platform; + Object.defineProperty(process, "platform", { value: "linux" }); + try { + const { fn, calls } = makeFakeSpawn(); + openBrowser("http://127.0.0.1:7878/", fn); + assert.equal(calls[0]!.cmd, "xdg-open"); + } finally { + Object.defineProperty(process, "platform", { value: original }); + } +}); + +test("openBrowser: logs warning on unknown platform, no spawn", () => { + const original = process.platform; + const warnings: string[] = []; + const originalWarn = console.warn; + Object.defineProperty(process, "platform", { value: "aix" }); + console.warn = (msg: string) => { + warnings.push(msg); + }; + try { + const { fn, calls } = makeFakeSpawn(); + openBrowser("http://127.0.0.1:7878/", fn); + assert.equal(calls.length, 0); + assert.ok( + warnings.some((w) => w.includes("Cannot auto-open browser")), + `expected warning, got ${JSON.stringify(warnings)}`, + ); + } finally { + Object.defineProperty(process, "platform", { value: original }); + console.warn = originalWarn; + } +}); diff --git a/pi-web/tests/protocol.test.ts b/pi-web/tests/protocol.test.ts new file mode 100644 index 00000000..dbcc3bcb --- /dev/null +++ b/pi-web/tests/protocol.test.ts @@ -0,0 +1,126 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + parseClientCommand, + isServerEvent, + type ClientCommand, +} from "../src/protocol.ts"; + +test("parseClientCommand: prompt", () => { + const cmd = parseClientCommand({ + type: "prompt", + id: "req-1", + text: "hello", + }); + assert.deepEqual(cmd, { type: "prompt", id: "req-1", text: "hello" }); +}); + +test("parseClientCommand: prompt with images", () => { + const cmd = parseClientCommand({ + type: "prompt", + id: "req-2", + text: "look", + images: [{ type: "image", data: "abc", mediaType: "image/png" }], + }); + assert.equal(cmd?.type, "prompt"); + if (cmd?.type === "prompt") { + assert.equal(cmd.images?.length, 1); + } +}); + +test("parseClientCommand: abort", () => { + const cmd = parseClientCommand({ type: "abort", id: "req-3" }); + assert.deepEqual(cmd, { type: "abort", id: "req-3" }); +}); + +test("parseClientCommand: new_session", () => { + const cmd = parseClientCommand({ type: "new_session", id: "req-4" }); + assert.deepEqual(cmd, { type: "new_session", id: "req-4" }); +}); + +test("parseClientCommand: switch_session", () => { + const cmd = parseClientCommand({ + type: "switch_session", + id: "req-5", + path: "/tmp/foo.jsonl", + }); + assert.deepEqual(cmd, { + type: "switch_session", + id: "req-5", + path: "/tmp/foo.jsonl", + }); +}); + +test("parseClientCommand: set_model", () => { + const cmd = parseClientCommand({ + type: "set_model", + id: "req-6", + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); + assert.deepEqual(cmd, { + type: "set_model", + id: "req-6", + provider: "anthropic", + modelId: "claude-sonnet-4-5", + }); +}); + +test("parseClientCommand: set_thinking_level", () => { + for (const level of ["off", "minimal", "low", "medium", "high", "xhigh"]) { + const cmd = parseClientCommand({ + type: "set_thinking_level", + id: "req-7", + level, + }); + assert.deepEqual(cmd, { + type: "set_thinking_level", + id: "req-7", + level, + }); + } +}); + +test("parseClientCommand: list_sessions", () => { + const cmd = parseClientCommand({ type: "list_sessions", id: "req-8" }); + assert.deepEqual(cmd, { type: "list_sessions", id: "req-8" }); +}); + +test("parseClientCommand: list_models", () => { + const cmd = parseClientCommand({ type: "list_models", id: "req-9" }); + assert.deepEqual(cmd, { type: "list_models", id: "req-9" }); +}); + +test("parseClientCommand: get_state", () => { + const cmd = parseClientCommand({ type: "get_state", id: "req-10" }); + assert.deepEqual(cmd, { type: "get_state", id: "req-10" }); +}); + +test("parseClientCommand: rejects unknown type", () => { + assert.equal(parseClientCommand({ type: "nope", id: "x" }), null); + assert.equal(parseClientCommand({ type: "prompt" }), null); // missing id + assert.equal( + parseClientCommand({ type: "prompt", id: "x", text: 123 }), + null, + ); // wrong type + assert.equal( + parseClientCommand({ type: "set_thinking_level", id: "x", level: "bogus" }), + null, + ); + assert.equal(parseClientCommand("not an object"), null); + assert.equal(parseClientCommand(null), null); +}); + +test("isServerEvent: passes SDK-shaped events", () => { + assert.ok(isServerEvent({ type: "agent_start" })); + assert.ok(isServerEvent({ type: "message_update" })); + assert.ok(isServerEvent({ type: "tool_execution_end" })); + assert.ok(isServerEvent({ type: "session_start" })); +}); + +test("isServerEvent: rejects commands and unknowns", () => { + assert.equal(isServerEvent({ type: "prompt" }), false); + assert.equal(isServerEvent({ type: "response" }), false); + assert.equal(isServerEvent({}), false); + assert.equal(isServerEvent(null), false); +}); diff --git a/pi-web/tests/runtime.test.ts b/pi-web/tests/runtime.test.ts new file mode 100644 index 00000000..16f0713b --- /dev/null +++ b/pi-web/tests/runtime.test.ts @@ -0,0 +1,37 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createRealRuntime } from "../src/runtime.ts"; + +test("createRealRuntime: builds a BridgeRuntime with a real SDK session", async () => { + const cwd = mkdtempSync(join(tmpdir(), "pi-web-rt-")); + let rt; + try { + rt = await createRealRuntime({ cwd }); + } catch (err) { + // If the SDK can't initialize (no auth.json, no model with a key, + // etc.), skip the test. Full integration is covered by the manual + // smoke test (Task 10), which runs against the user's real auth. + console.log(`[skip] createRealRuntime: ${(err as Error).message}`); + return; + } + // BridgeRuntime surface. + assert.equal(typeof rt.newSession, "function"); + assert.equal(typeof rt.switchSession, "function"); + assert.equal(typeof rt.listSessions, "function"); + assert.equal(typeof rt.getAvailableModels, "function"); + // session: an AgentSession with subscribe/prompt/abort/setModel/etc. + assert.equal(typeof rt.session.subscribe, "function"); + assert.equal(typeof rt.session.prompt, "function"); + assert.equal(typeof rt.session.abort, "function"); + // messages is an array. + assert.ok(Array.isArray(rt.session.messages)); + // listSessions: returns array (empty for fresh cwd). + const sessions = await rt.listSessions(); + assert.ok(Array.isArray(sessions)); + // getAvailableModels: returns array (possibly empty if no keys). + const models = await rt.getAvailableModels(); + assert.ok(Array.isArray(models)); +}); diff --git a/pi-web/tests/server.test.ts b/pi-web/tests/server.test.ts new file mode 100644 index 00000000..9f2223c8 --- /dev/null +++ b/pi-web/tests/server.test.ts @@ -0,0 +1,193 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { once } from "node:events"; +import { request } from "node:http"; +import { writeFile, mkdir, rm } from "node:fs/promises"; +import { mkdtemp } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { WebSocket } from "ws"; +import { startServer, type ServerDeps } from "../src/server.ts"; + +// Fakes typed as any to dodge the complexity of the full runtime +// surface; the server's contract is verified by behavior. + +type AnySession = any; +type AnyRuntime = any; + +function makeFakeSession(): AnySession { + const events: Array<{ cb: (e: unknown) => void }> = []; + return { + events, + subscribe(cb: (e: unknown) => void) { + events.push({ cb }); + return () => {}; + }, + }; +} + +function makeFakeRuntime(extra: Record = {}): AnyRuntime { + const session = makeFakeSession(); + return { + session, + async newSession() { + return { cancelled: false }; + }, + async switchSession() { + return { cancelled: false }; + }, + async listSessions() { + return []; + }, + async getAvailableModels() { + return []; + }, + ...extra, + }; +} + +async function get( + port: number, + path: string, +): Promise<{ status: number; body: string }> { + return new Promise((resolve, reject) => { + const req = request( + { host: "127.0.0.1", port, path, method: "GET" }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (c) => chunks.push(c)); + res.on("end", () => + resolve({ + status: res.statusCode ?? 0, + body: Buffer.concat(chunks).toString("utf8"), + }), + ); + }, + ); + req.on("error", reject); + req.end(); + }); +} + +async function withTempWebRoot( + body: (dir: string) => Promise, +): Promise { + const dir = await mkdtemp(join(tmpdir(), "pi-web-test-")); + await mkdir(join(dir, "src/web"), { recursive: true }); + await writeFile(join(dir, "src/web/index.html"), "

hi

"); + try { + await body(dir); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test("startServer: serves index.html on /", async () => { + await withTempWebRoot(async (dir) => { + const runtime = makeFakeRuntime(); + const ctx: ServerDeps = { runtime, webRoot: join(dir, "src/web") }; + const handle = startServer({ port: 0, host: "127.0.0.1", ctx }); + await once(handle.server, "listening"); + const addr = handle.server.address(); + if (typeof addr !== "object" || !addr) throw new Error("no address"); + const port = addr.port; + + const res = await get(port, "/"); + assert.equal(res.status, 200); + assert.match(res.body, /

hi<\/h1>/); + + await handle.stop(); + }); +}); + +test("startServer: refuses to bind to non-loopback", () => { + const runtime = makeFakeRuntime(); + const ctx: ServerDeps = { runtime, webRoot: "/tmp" }; + assert.throws(() => + startServer({ port: 0, host: "0.0.0.0" as "127.0.0.1", ctx }), + ); +}); + +test("startServer: WebSocket at /ws handles a prompt command", async () => { + await withTempWebRoot(async (dir) => { + const session = makeFakeSession() as AnySession & { + promptCalls: Array<{ text: string }>; + }; + session.promptCalls = []; + session.prompt = async (t: string) => { + session.promptCalls.push({ text: t }); + }; + const runtime = makeFakeRuntime({ session }); + + const ctx: ServerDeps = { runtime, webRoot: join(dir, "src/web") }; + const handle = startServer({ port: 0, host: "127.0.0.1", ctx }); + await once(handle.server, "listening"); + const addr = handle.server.address(); + if (typeof addr !== "object" || !addr) throw new Error("no address"); + const port = addr.port; + + const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`); + await new Promise((resolve) => ws.on("open", () => resolve())); + ws.send(JSON.stringify({ type: "prompt", id: "1", text: "hello" })); + + const got = await new Promise((resolve) => { + ws.on("message", (data) => resolve(data.toString())); + }); + const resp = JSON.parse(got); + assert.equal(resp.type, "response"); + assert.equal(resp.id, "1"); + assert.ok(resp.ok); + assert.equal(session.promptCalls[0]!.text, "hello"); + + ws.close(); + await handle.stop(); + }); +}); + +test("startServer: events on session are pushed to WebSocket", async () => { + await withTempWebRoot(async (dir) => { + const session = makeFakeSession(); + const runtime = makeFakeRuntime({ session }); + const ctx: ServerDeps = { runtime, webRoot: join(dir, "src/web") }; + const handle = startServer({ port: 0, host: "127.0.0.1", ctx }); + await once(handle.server, "listening"); + const addr = handle.server.address(); + if (typeof addr !== "object" || !addr) throw new Error("no address"); + const port = addr.port; + + const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`); + await new Promise((resolve) => ws.on("open", () => resolve())); + + session.events[0]!.cb({ type: "agent_start" }); + + const got = await new Promise((resolve) => { + ws.on("message", (data) => resolve(data.toString())); + }); + assert.deepEqual(JSON.parse(got), { type: "agent_start" }); + + ws.close(); + await handle.stop(); + }); +}); + +test("startServer: serves vendor routes from node_modules", async () => { + await withTempWebRoot(async (dir) => { + const runtime = makeFakeRuntime(); + const ctx: ServerDeps = { runtime, webRoot: join(dir, "src/web") }; + const handle = startServer({ port: 0, host: "127.0.0.1", ctx }); + await once(handle.server, "listening"); + const addr = handle.server.address(); + if (typeof addr !== "object" || !addr) throw new Error("no address"); + const port = addr.port; + + const res = await get(port, "/vendor/preact.js"); + assert.equal(res.status, 200); + assert.match(res.body, /createElement|render|h=/); + + const htm = await get(port, "/vendor/htm.js"); + assert.equal(htm.status, 200); + assert.ok(htm.body.length > 100, "htm vendor file should have content"); + + await handle.stop(); + }); +}); diff --git a/pi-web/tsconfig.json b/pi-web/tsconfig.json new file mode 100644 index 00000000..056b6950 --- /dev/null +++ b/pi-web/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true + }, + "include": ["src/**/*", "tests/**/*"] +} From f5a0e7377991e7a5e773d1205f5339f89778add8 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:40:58 +0900 Subject: [PATCH 124/366] fix(nvim): mini.files opens at current buffer's directory Passing the buffer's file path to MiniFiles.open crashed on any buffer whose file did not exist on disk (deleted, never saved, or special buffers like terminal, help, or fugitive). mini.files requires a directory, so now use fnamemodify(name, ':h') and fall back to cwd when the buffer has no parent. --- .config/nvim/plugin/76_mini.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.config/nvim/plugin/76_mini.lua b/.config/nvim/plugin/76_mini.lua index e7fa5e7b..6da4de7f 100644 --- a/.config/nvim/plugin/76_mini.lua +++ b/.config/nvim/plugin/76_mini.lua @@ -189,7 +189,8 @@ _G.Config.new_autocmd("User", { }) require('utils').map('n', 'ft', function() - MiniFiles.open(vim.api.nvim_buf_get_name(0)) + local dir = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ':h') + MiniFiles.open(dir ~= '' and dir or nil) end, { desc = "MiniFiles" }) -- mini.clue From 53a9b11987e24c96e407f16cda8c9c392935cf25 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:16:06 +0900 Subject: [PATCH 125/366] chore: ignore .worktrees/ (project-local worktree dir) --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 349e44e5..815bbbc5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -.config/nvim/.netrwhist +.worktrees/ +worktrees/ + .config/mpv/watch_later/* .config/lazygit/state.yml From 0061ab2833bc94b2031ac1cd208f311ac84af7dc Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 21 Jun 2026 14:39:03 +0900 Subject: [PATCH 126/366] fix(nvim): bugs --- .config/nvim/after/lsp/jsonls.lua | 1 - .config/nvim/after/lsp/pyright.lua | 1 - .config/nvim/after/lsp/yamlls.lua | 7 +-- .config/nvim/init.lua | 5 +++ .config/nvim/lua/colemak.lua | 4 +- .config/nvim/plugin/10_opts.lua | 9 ++-- .config/nvim/plugin/20_keymaps.lua | 23 ++++++---- .config/nvim/plugin/30_autocmds.lua | 26 +++-------- .config/nvim/plugin/40_lsp_behavior.lua | 46 ++++++++++++-------- .config/nvim/plugin/41_lsp_format.lua | 57 +++++++++++++++---------- .config/nvim/plugin/71_treesitter.lua | 48 +++++++++++---------- .config/nvim/plugin/74_fff.lua | 1 - .config/nvim/plugin/75_snacks.lua | 3 ++ .config/nvim/plugin/999_session.lua | 24 ++++++++--- .config/nvim/plugin/999_vscode.lua | 9 ++-- 15 files changed, 149 insertions(+), 115 deletions(-) diff --git a/.config/nvim/after/lsp/jsonls.lua b/.config/nvim/after/lsp/jsonls.lua index 0e2a034f..e1db65bf 100644 --- a/.config/nvim/after/lsp/jsonls.lua +++ b/.config/nvim/after/lsp/jsonls.lua @@ -2,7 +2,6 @@ return { ---@type lspconfig.settings.jsonls settings = { json = { - schemas = require('schemastore').json.schemas(), validate = { enable = true }, }, }, diff --git a/.config/nvim/after/lsp/pyright.lua b/.config/nvim/after/lsp/pyright.lua index 7a0ffc01..678f3755 100644 --- a/.config/nvim/after/lsp/pyright.lua +++ b/.config/nvim/after/lsp/pyright.lua @@ -6,7 +6,6 @@ local function use_project_venv(_, config) config.settings = config.settings or {} config.settings.python = config.settings.python or {} - config.settings.python.pythonPath = python config.settings.python.venvPath = config.root_dir config.settings.python.venv = '.venv' end diff --git a/.config/nvim/after/lsp/yamlls.lua b/.config/nvim/after/lsp/yamlls.lua index 40ad1d77..16f2c381 100644 --- a/.config/nvim/after/lsp/yamlls.lua +++ b/.config/nvim/after/lsp/yamlls.lua @@ -2,17 +2,12 @@ return { ---@type lspconfig.settings.yamlls settings = { yaml = { - schemas = require('schemastore').yaml.schemas(), format = { enable = true, bracketSpacing = true }, schemaStore = { - -- You must disable built-in schemaStore support if you want to use - -- this plugin and its advanced options like `ignore`. - enable = false, - -- Avoid TypeError: Cannot read properties of undefined (reading 'length') - url = "", + enable = true, }, }, }, diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua index 198bbc6c..9ba4636b 100644 --- a/.config/nvim/init.lua +++ b/.config/nvim/init.lua @@ -1,8 +1,13 @@ _G.Config = {} +vim.g.mapleader = " " +vim.g.maplocalleader = vim.g.mapleader + local gr = vim.api.nvim_create_augroup('custom-config', { clear = true }) _G.Config.new_autocmd = function(event, opts) opts = opts or {} opts.group = opts.group or gr vim.api.nvim_create_autocmd(event, opts) end + +require('custom.ai').setup() diff --git a/.config/nvim/lua/colemak.lua b/.config/nvim/lua/colemak.lua index c7108294..fe2b3bc1 100644 --- a/.config/nvim/lua/colemak.lua +++ b/.config/nvim/lua/colemak.lua @@ -15,8 +15,8 @@ local mappings = { { modes = { "n", "x" }, lhs = "L", rhs = "N", desc = "Prev search (N)" }, { modes = { "n", "x" }, lhs = "h", rhs = "e", desc = "End of word (e)" }, { modes = { "n", "x" }, lhs = "H", rhs = "E", desc = "End of WORD (E)" }, - { modes = { "n" }, lhs = "j", rhs = "m", desc = "Set mark (m)" }, - { modes = { "n" }, lhs = "k", rhs = "i", desc = "Insert (i)" }, + { modes = { "n", "x" }, lhs = "j", rhs = "m", desc = "Set mark (m)" }, + { modes = { "n", "x" }, lhs = "k", rhs = "i", desc = "Insert (i)" }, -- { modes = { "n" }, lhs = "K", rhs = "I", desc = "Insert at start (I)" }, } diff --git a/.config/nvim/plugin/10_opts.lua b/.config/nvim/plugin/10_opts.lua index e76735d1..d3a264f3 100644 --- a/.config/nvim/plugin/10_opts.lua +++ b/.config/nvim/plugin/10_opts.lua @@ -1,8 +1,10 @@ -- :options +if vim.g.vscode then + return +end + -- General ==================================================================== -vim.g.mapleader = " " -- Leader key -vim.g.maplocalleader = vim.g.mapleader vim.o.winborder = 'rounded' -- Consistent borders on all floats (0.11+) vim.o.shell = 'bash' vim.o.mousescroll = 'ver:6,hor:6' -- Customize mouse scroll @@ -18,7 +20,7 @@ vim.o.title = true -- Set terminal title to the filename vim.o.showmatch = true -- highlight matching [{()}] vim.o.breakindent = true -- Indent wrapped lines to match line start vim.o.breakindentopt = 'list:-1' -- Add padding for lists (if 'wrap' is set) -vim.o.colorcolumn = '+1' -- Draw column on the right of maximum width +vim.o.colorcolumn = '+1' vim.o.cursorline = true -- Enable current line highlighting vim.o.linebreak = true -- Wrap lines at 'breakat' (if 'wrap' is set) vim.o.list = false -- Show helpful text indicators @@ -65,6 +67,7 @@ vim.o.expandtab = true -- Convert tabs to spaces vim.o.formatoptions = 'rqnl1j' -- Improve comment editing vim.o.ignorecase = true -- Ignore case during search vim.o.infercase = true -- Infer case in built-in completion +vim.o.textwidth = 80 -- Soft wrap target vim.o.shiftwidth = 2 -- Use this number of spaces for indentation vim.o.smartcase = true -- Respect case if search pattern has upper case vim.o.smartindent = true -- Make indenting smart diff --git a/.config/nvim/plugin/20_keymaps.lua b/.config/nvim/plugin/20_keymaps.lua index c86c7640..c16c042b 100644 --- a/.config/nvim/plugin/20_keymaps.lua +++ b/.config/nvim/plugin/20_keymaps.lua @@ -12,11 +12,21 @@ map("i", "", "", { desc = "Delete word backwards" }) -- CTRL+BS = C-h map("i", "", "", { desc = "Delete word backwards" }) -- For macOS -map("n", "bd", "bdelete", { desc = "Close current buffer" }) +map("n", "bd", "bdelete!", { desc = "Close current buffer" }) + map("n", "bD", function() - local cur_path = vim.api.nvim_buf_get_name(0) - vim.cmd("%bd!") - if cur_path ~= "" then vim.cmd("edit " .. vim.fn.fnameescape(cur_path)) end + local cur_buf = vim.api.nvim_get_current_buf() + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if buf ~= cur_buf and vim.api.nvim_buf_is_loaded(buf) then + local name = vim.api.nvim_buf_get_name(buf) + if vim.bo[buf].modified then + local short = name ~= "" and vim.fn.fnamemodify(name, ":~:.") or "[No Name]" + local choice = vim.fn.confirm("'" .. short .. "' has unsaved changes. Discard?", "&Discard\n&Cancel", 2) + if choice ~= 1 then return end + end + pcall(vim.api.nvim_buf_delete, buf, { force = true }) + end + end end, { desc = "Close all buffers except current" }) map("n", "bn", "bnext", { desc = "Next buffer" }) map("n", "bp", "bprevious", { desc = "Previous buffer" }) @@ -113,7 +123,4 @@ map('x', '#', function() return vsearch('?') end, { expr = true }) -- https://www.reddit.com/r/neovim/comments/1mxeghf/using_as_a_multipurpose_search_tool/ map("x", "/", "/\\%V") -- `:h /\%V` -map("n", "u", function() - vim.cmd.packadd("nvim.undotree") - require("undotree").open() -end, { desc = "Undo tree" }) +map("n", "u", "Undotree", { desc = "Undo tree" }) diff --git a/.config/nvim/plugin/30_autocmds.lua b/.config/nvim/plugin/30_autocmds.lua index 3686cfd1..69eb6fee 100644 --- a/.config/nvim/plugin/30_autocmds.lua +++ b/.config/nvim/plugin/30_autocmds.lua @@ -55,9 +55,13 @@ local function shfmt_on_save(buf) return end - if #output > 0 then - vim.api.nvim_buf_set_lines(buf, 0, -1, true, output) + if #output > 0 and output[#output] ~= "" then + output[#output + 1] = "" end + + local view = vim.fn.winsaveview() + vim.api.nvim_buf_set_lines(buf, 0, -1, true, output) + vim.fn.winrestview(view) end _G.Config.new_autocmd("BufWritePre", { @@ -67,21 +71,3 @@ _G.Config.new_autocmd("BufWritePre", { }) -- Go organize-imports on save is handled in 41_lsp_format.lua (combined with auto-format to avoid race conditions) - - --- Automatically update listchars to match indentation and listchars settings --- https://www.reddit.com/r/neovim/comments/17aponn/comment/k5f2n7t/?utm_source=share&utm_medium=web2x&context=3 -local function update_lead() - local lcs = vim.opt_local.listchars:get() - local space_src = lcs.multispace or lcs.space - if not space_src or space_src == "" then return end - local tab = vim.fn.str2list(lcs.tab) - local space = vim.fn.str2list(space_src) - local lead = { tab[1] } - for i = 1, vim.bo.tabstop - 1 do - lead[#lead + 1] = space[i % #space + 1] - end - vim.opt_local.listchars:append({ leadmultispace = vim.fn.list2str(lead) }) -end -_G.Config.new_autocmd("OptionSet", { pattern = { "listchars", "tabstop", "filetype" }, callback = update_lead }) -_G.Config.new_autocmd("VimEnter", { callback = update_lead, once = true }) diff --git a/.config/nvim/plugin/40_lsp_behavior.lua b/.config/nvim/plugin/40_lsp_behavior.lua index b55cbec3..c062bcdb 100644 --- a/.config/nvim/plugin/40_lsp_behavior.lua +++ b/.config/nvim/plugin/40_lsp_behavior.lua @@ -19,9 +19,9 @@ local lsp_picker_layout = { border = true, title = "{title} {live} {flags}", title_pos = "center", - { win = "input", height = 1, border = "bottom" }, + { win = "input", height = 1, border = "bottom" }, { win = "list", border = "none" }, - { win = "preview", title = "{preview}", height = 0.4, border = "top" }, + { win = "preview", title = "{preview}", height = 0.4, border = "top" }, }, } @@ -42,7 +42,7 @@ local function mappings(client, buf) end, { desc = "Go to implementation" }) -- vim.lsp.buf.implementation bmap('n', 'grr', function() Snacks.picker.lsp_references({ layout = lsp_picker_layout, focus = "list" }) - end, { desc = "Go to reference" }) -- vim.lsp.buf.references + end, { desc = "Go to reference" }) -- vim.lsp.buf.references bmap('n', 'gS', Snacks.picker.lsp_workspace_symbols, { desc = "Goto workspace symbols" }) bmap('n', 'gD', vim.lsp.buf.declaration, { desc = "Go to declaration" }) -- Many LSPs do not implement this @@ -88,19 +88,20 @@ end ---@param client vim.lsp.Client ---@param buf number local function highlight_references(client, buf) - if vim.b[buf].lsp_highlight_setup then return end if not client:supports_method(Methods.textDocument_documentHighlight) then return end - vim.b[buf].lsp_highlight_setup = true + vim.b[buf].lsp_highlight_setup = vim.b[buf].lsp_highlight_setup or {} + if vim.b[buf].lsp_highlight_setup[client.id] then return end + vim.b[buf].lsp_highlight_setup[client.id] = true - local group = vim.api.nvim_create_augroup('lsp-highlight-' .. buf, { clear = true }) - _G.Config.new_autocmd({ 'CursorHold', 'CursorHoldI' }, { + local group = vim.api.nvim_create_augroup('lsp-highlight-' .. buf .. '.' .. client.id, { clear = true }) + _G.Config.new_autocmd('CursorHold', { desc = "Document Highlight", buffer = buf, group = group, callback = vim.lsp.buf.document_highlight, }) - _G.Config.new_autocmd({ 'CursorMoved', 'CursorMovedI', 'BufLeave' }, { + _G.Config.new_autocmd({ 'CursorMoved', 'BufLeave' }, { desc = "Clear All the References", buffer = buf, group = group, @@ -109,13 +110,18 @@ local function highlight_references(client, buf) _G.Config.new_autocmd('LspDetach', { desc = "Remove highlight autocmds", - group = UserLspConfig, + group = group, buffer = buf, callback = function(ev) if not (ev.data and ev.data.client_id == client.id) then return end vim.lsp.buf.clear_references() - vim.api.nvim_del_augroup_by_name('lsp-highlight-' .. buf) - vim.b[buf].lsp_highlight_setup = nil + pcall(vim.api.nvim_del_augroup_by_name, 'lsp-highlight-' .. buf .. '.' .. client.id) + if type(vim.b[buf].lsp_highlight_setup) == 'table' then + vim.b[buf].lsp_highlight_setup[client.id] = nil + end + if next(vim.b[buf].lsp_highlight_setup or {}) == nil then + vim.b[buf].lsp_highlight_setup = nil + end return true end, }) @@ -124,10 +130,11 @@ end ---@param client vim.lsp.Client ---@param buf number local function show_diagnostics(client, buf) - if vim.b[buf].lsp_diagnostics_float_setup then return end - vim.b[buf].lsp_diagnostics_float_setup = true + vim.b[buf].lsp_diagnostics_float_setup = vim.b[buf].lsp_diagnostics_float_setup or {} + if vim.b[buf].lsp_diagnostics_float_setup[client.id] then return end + vim.b[buf].lsp_diagnostics_float_setup[client.id] = true - local group = vim.api.nvim_create_augroup('lsp-diag-hold-' .. buf, { clear = true }) + local group = vim.api.nvim_create_augroup('lsp-diag-hold-' .. buf .. '.' .. client.id, { clear = true }) _G.Config.new_autocmd("CursorHold", { group = group, buffer = buf, @@ -145,12 +152,17 @@ local function show_diagnostics(client, buf) _G.Config.new_autocmd('LspDetach', { desc = "Remove diagnostics float autocmd", - group = UserLspConfig, + group = group, buffer = buf, callback = function(ev) if not (ev.data and ev.data.client_id == client.id) then return end - vim.api.nvim_del_augroup_by_name('lsp-diag-hold-' .. buf) - vim.b[buf].lsp_diagnostics_float_setup = nil + pcall(vim.api.nvim_del_augroup_by_name, 'lsp-diag-hold-' .. buf .. '.' .. client.id) + if type(vim.b[buf].lsp_diagnostics_float_setup) == 'table' then + vim.b[buf].lsp_diagnostics_float_setup[client.id] = nil + end + if next(vim.b[buf].lsp_diagnostics_float_setup or {}) == nil then + vim.b[buf].lsp_diagnostics_float_setup = nil + end return true end, }) diff --git a/.config/nvim/plugin/41_lsp_format.lua b/.config/nvim/plugin/41_lsp_format.lua index fe1e525e..2959d859 100644 --- a/.config/nvim/plugin/41_lsp_format.lua +++ b/.config/nvim/plugin/41_lsp_format.lua @@ -12,6 +12,8 @@ local fmt = { lua = "lua_ls", go = "gopls", html = "html", + css = "cssls", + yaml = "yamlls", javascript = "tsgo", typescript = "tsgo", javascriptreact = "tsgo", @@ -36,7 +38,17 @@ local function format_python_black(buf) return end - vim.api.nvim_buf_set_lines(buf, 0, -1, true, output) + if #output > 0 and output[#output] ~= "" then + output[#output + 1] = "" + end + + -- Guard against empty output (systemlist returns {}): nvim_buf_set_lines with + -- an empty list replaces the whole buffer with a single blank line. + if #output > 0 then + local view = vim.fn.winsaveview() + vim.api.nvim_buf_set_lines(buf, 0, -1, true, output) + vim.fn.winrestview(view) + end end local function formatter_name(buf) @@ -54,30 +66,31 @@ local function formatter_name(buf) return clients[1] and clients[1].name or nil end -local function organize_go_imports(buf, client) +local function organize_go_imports(buf, client, on_done) local params = vim.lsp.util.make_range_params(nil, client.offset_encoding) params.context = { only = { 'source.organizeImports' } } - local results, err = vim.lsp.buf_request_sync(buf, 'textDocument/codeAction', params, 1000) - if not results then - vim.notify("organizeImports request failed: " .. tostring(err), vim.log.levels.WARN) - return - end - for _, res in pairs(results) do - for _, action in pairs(res.result or {}) do - if action.edit then - local ok, e = pcall(vim.lsp.util.apply_workspace_edit, action.edit, client.offset_encoding) - if not ok then - vim.notify("organizeImports edit failed: " .. tostring(e), vim.log.levels.WARN) + client:request('textDocument/codeAction', params, function(err, results) + if not vim.api.nvim_buf_is_valid(buf) then return end + if err or not results then + vim.notify("organizeImports request failed: " .. tostring(err), vim.log.levels.WARN) + on_done() + return + end + for _, res in pairs(results) do + for _, action in pairs(res.result or {}) do + if action.edit then + local ok, e = pcall(vim.lsp.util.apply_workspace_edit, action.edit, client.offset_encoding) + if not ok then + vim.notify("organizeImports edit failed: " .. tostring(e), vim.log.levels.WARN) + end + end + if action.command then + client:exec_cmd(action.command) end - end - if action.command then - client:exec_cmd(action.command) end end - end - if vim.api.nvim_buf_is_valid(buf) then - vim.lsp.buf.format({ bufnr = buf, name = client.name, timeout_ms = 1000 }) - end + vim.lsp.buf.format({ bufnr = buf, name = client.name, timeout_ms = 1000 }, on_done) + end, buf) end local function set_format_on_save(buf, client, callback) @@ -90,7 +103,7 @@ local function set_format_on_save(buf, client, callback) _G.Config.new_autocmd('LspDetach', { desc = "Remove auto-format autocmd", - group = format_group, + group = group, buffer = buf, callback = function(ev) if ev.data and ev.data.client_id == client.id then @@ -129,7 +142,7 @@ _G.Config.new_autocmd('LspAttach', { set_format_on_save(buf, client, function() if ft == 'go' then - organize_go_imports(buf, client) + organize_go_imports(buf, client, function() end) return end vim.lsp.buf.format({ bufnr = buf, name = client.name, timeout_ms = 1000 }) diff --git a/.config/nvim/plugin/71_treesitter.lua b/.config/nvim/plugin/71_treesitter.lua index 9dd26bf1..5699f648 100644 --- a/.config/nvim/plugin/71_treesitter.lua +++ b/.config/nvim/plugin/71_treesitter.lua @@ -14,29 +14,31 @@ _G.Config.new_autocmd('PackChanged', { end }) -require('nvim-treesitter').install({ - "bash", - "css", - "dockerfile", - "go", - "gomod", - "html", - "javascript", - "typescript", - "tsx", - "json", - "lua", - "luadoc", - "make", - "markdown", - "markdown_inline", - "vimdoc", - "python", - "yaml", - "regex", -- for Snacks.picker - "gitcommit", - "svelte", -}) +vim.schedule(function() + require('nvim-treesitter').install({ + "bash", + "css", + "dockerfile", + "go", + "gomod", + "html", + "javascript", + "typescript", + "tsx", + "json", + "lua", + "luadoc", + "make", + "markdown", + "markdown_inline", + "vimdoc", + "python", + "yaml", + "regex", -- for Snacks.picker + "gitcommit", + "svelte", + }) +end) if vim.g.vscode then return end diff --git a/.config/nvim/plugin/74_fff.lua b/.config/nvim/plugin/74_fff.lua index 7db1fe87..8f0bb9f5 100644 --- a/.config/nvim/plugin/74_fff.lua +++ b/.config/nvim/plugin/74_fff.lua @@ -30,7 +30,6 @@ local map = require('utils').map local fff = require('fff') map('n', 'fo', function() fff.find_files() end, { desc = "Find Files" }) -map('n', 'fs', function() fff.find_files() end, { desc = "Find Files (smart)" }) map('n', '/', function() fff.live_grep() end, { desc = "Grep" }) map('n', '*', function() fff.live_grep({ query = vim.fn.expand('') }) end, { desc = "Grep Word" }) diff --git a/.config/nvim/plugin/75_snacks.lua b/.config/nvim/plugin/75_snacks.lua index 1ceeec40..b17a9365 100644 --- a/.config/nvim/plugin/75_snacks.lua +++ b/.config/nvim/plugin/75_snacks.lua @@ -63,5 +63,8 @@ map('n', 'oL', function() Snacks.picker.git_log_file({}) end, { desc = " map('n', 'oh', function() Snacks.picker.help({}) end, { desc = "Help Pages" }) map('n', '/', function() Snacks.picker.lines({}) end, { desc = "Buffer Lines" }) map('n', 'oq', function() Snacks.picker.qflist({}) end, { desc = "Quickfix List" }) +map('n', 'oR', function() Snacks.picker.recent({}) end, { desc = "Recent files" }) map('n', 'oS', function() Snacks.picker.spelling({}) end, { desc = "Spelling" }) map('n', 'ou', function() Snacks.picker.undo({}) end, { desc = "Undo History" }) + +map('n', 'fs', function() Snacks.picker.smart() end, { desc = "Find Files (smart)" }) diff --git a/.config/nvim/plugin/999_session.lua b/.config/nvim/plugin/999_session.lua index dd617479..a7e03bf2 100644 --- a/.config/nvim/plugin/999_session.lua +++ b/.config/nvim/plugin/999_session.lua @@ -1,4 +1,7 @@ -- Auto-session management +if vim.g.vscode then + return +end local session_dir = vim.fs.joinpath(vim.fn.stdpath("state"), "sessions") -- Directories where sessions should not be saved @@ -42,14 +45,21 @@ vim.fn.mkdir(session_dir, "p") local session_group = vim.api.nvim_create_augroup("auto_sessions", { clear = true }) +-- Capture the session file path once at startup. Recomputing it at VimLeavePre +-- would hash whatever directory a mid-session `:cd` left as the cwd, orphaning +-- the session state that was actually restored at VimEnter. +local active_session_file + _G.Config.new_autocmd("VimEnter", { desc = "Restore previous session", callback = function() - local session_file = get_session_file() - if should_save_session() and vim.fn.filereadable(session_file) ~= 0 then - -- Session files may contain benign errors (e.g. %argdel with empty arglist). - -- silent! is the canonical way to source them: keep going regardless. - vim.cmd('silent! source ' .. vim.fn.fnameescape(session_file)) + if should_save_session() then + active_session_file = get_session_file() + if vim.fn.filereadable(active_session_file) ~= 0 then + -- Session files may contain benign errors (e.g. %argdel with empty arglist). + -- silent! is the canonical way to source them: keep going regardless. + vim.cmd('silent! source ' .. vim.fn.fnameescape(active_session_file)) + end end end, group = session_group, @@ -60,8 +70,8 @@ _G.Config.new_autocmd("VimEnter", { _G.Config.new_autocmd("VimLeavePre", { desc = "Save session", callback = function() - if should_save_session() then - vim.cmd("mks! " .. vim.fn.fnameescape(get_session_file())) + if active_session_file and should_save_session() then + vim.cmd("mks! " .. vim.fn.fnameescape(active_session_file)) end end, group = session_group, diff --git a/.config/nvim/plugin/999_vscode.lua b/.config/nvim/plugin/999_vscode.lua index 44d1cb62..5d9621ea 100644 --- a/.config/nvim/plugin/999_vscode.lua +++ b/.config/nvim/plugin/999_vscode.lua @@ -60,6 +60,11 @@ map("n", "fo", function() vscode.action("workbench.action.quickOpen") end, { desc = "Quick Open" }) +-- File tree +map("n", "ft", function() + vscode.action("workbench.view.explorer") +end, { desc = "Focus File Explorer" }) + map("n", "?", function() vscode.action("workbench.action.showCommands") end, { desc = "Show Commands" }) @@ -108,7 +113,3 @@ end, { desc = "Code action" }) map({ "n", "x" }, "=", function() vscode.action("editor.action.formatDocument") end, { desc = "Format file" }) - -map("n", "ft", function() - vscode.action("workbench.view.explorer") -end, { desc = "Focus File Explorer" }) From dea87ad560b296762cd934167c526316e7baafb0 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:38:32 +0900 Subject: [PATCH 127/366] chore: fix keylayout --- misc/keymaps/Colemak-DH-ANSI.keylayout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/keymaps/Colemak-DH-ANSI.keylayout b/misc/keymaps/Colemak-DH-ANSI.keylayout index 0db14e2b..647bb6d2 100644 --- a/misc/keymaps/Colemak-DH-ANSI.keylayout +++ b/misc/keymaps/Colemak-DH-ANSI.keylayout @@ -128,7 +128,7 @@ - + From dc90ce2c8fa870c3d48017ca4f7188005464e1e2 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Mon, 22 Jun 2026 20:34:58 +0900 Subject: [PATCH 128/366] chore: mini 0.18 --- .config/nvim/nvim-pack-lock.json | 4 ++-- .config/nvim/plugin/76_mini.lua | 12 ++++-------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.config/nvim/nvim-pack-lock.json b/.config/nvim/nvim-pack-lock.json index da21e9b3..78bccee5 100644 --- a/.config/nvim/nvim-pack-lock.json +++ b/.config/nvim/nvim-pack-lock.json @@ -10,7 +10,7 @@ "version": "1.0.0 - 2.0.0" }, "fff.nvim": { - "rev": "797c045aa93e03e3cec3497a76afe7cb0106bdc2", + "rev": "28321da22836b0e11da81e30f40f7a043b8f8fb4", "src": "https://github.com/dmtrKovalenko/fff.nvim", "version": ">=0.0.0" }, @@ -33,7 +33,7 @@ "version": ">=0.0.0" }, "mini.nvim": { - "rev": "a995fe9cd4193fb492b5df69175a351a74b3d36b", + "rev": "1345d191bb3da9c7b0e977f4387c5761f9bff68d", "src": "https://github.com/nvim-mini/mini.nvim", "version": ">=0.0.0" }, diff --git a/.config/nvim/plugin/76_mini.lua b/.config/nvim/plugin/76_mini.lua index 6da4de7f..a12c8d63 100644 --- a/.config/nvim/plugin/76_mini.lua +++ b/.config/nvim/plugin/76_mini.lua @@ -27,12 +27,15 @@ require('mini.indentscope').setup({ if vim.g.vscode then return end require('mini.misc').setup_restore_cursor() +require('mini.input').setup({}) require("mini.notify").setup({ lsp_progress = { enable = false } }) require("mini.icons").setup({}) require('mini.cmdline').setup({}) require('mini.bracketed').setup({}) require('mini.trailspace').setup({}) +require('mini.extra').setup({}) + require('mini.statusline').setup({ content = { active = function() @@ -117,13 +120,6 @@ require('mini.files').setup({ }, }) -_G.Config.new_autocmd("User", { - pattern = "MiniFilesActionRename", - callback = function(event) - require('snacks').rename.on_rename_file(event.data.from, event.data.to) - end, -}) - -- Before deleting a buffer, switch any window showing it to another buffer first. -- This avoids errors when the buffer is displayed and there's no alt buffer. local function switch_windows_off_buffer(buf) @@ -164,7 +160,7 @@ _G.Config.new_autocmd("User", { local normalized = name:gsub('/+$', '') -- Match: exact file, or file/dir inside deleted directory local matches = (normalized == path) - or normalized:find('^' .. vim.pesc(path .. '/'), 1) + or normalized:find('^' .. vim.pesc(path .. '/'), 1) if matches then if vim.bo[buf].modified then From 9013efe096df16ce3e870a69ce9f93cb457d6b8f Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Mon, 22 Jun 2026 21:10:52 +0900 Subject: [PATCH 129/366] fix(ask_question): prevent block on parallel calls When the LLM submits multiple ask_question tool calls in one turn, pi's agent loop runs them in parallel and the first call's UI component is overwritten by the second, leaving the first promise orphaned. Mark the tool as sequential so calls run one at a time, matching the fact that a modal can never physically be parallel. --- home/.pi/agent/extensions/ask_question.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/home/.pi/agent/extensions/ask_question.ts b/home/.pi/agent/extensions/ask_question.ts index de14806b..c8902287 100644 --- a/home/.pi/agent/extensions/ask_question.ts +++ b/home/.pi/agent/extensions/ask_question.ts @@ -36,9 +36,6 @@ const AskQuestionParams = Type.Object({ }); export default function(pi: ExtensionAPI) { - // Defer registration to session_start so we can branch on ctx.hasUI. - // In non-interactive runs (CLI scripted, agents driving pi, etc.) the - // tool is simply absent from the agent's tool list. pi.on("session_start", (_event, ctx) => { if (!ctx.hasUI) return; @@ -53,6 +50,7 @@ export default function(pi: ExtensionAPI) { "Keep alternatives short and mutually exclusive.", ], parameters: AskQuestionParams, + executionMode: "sequential", async execute(_toolCallId, params, signal, _onUpdate, ctx) { const options = [...params.alternatives, PROS_CONS_OPTION, OTHER_OPTION]; From 6268bdfea062d4066ae3d219cf5da52b2374573e Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:04:26 +0900 Subject: [PATCH 130/366] chore: prompt --- home/.claude/CLAUDE.md | 57 +++++++++++++----------- home/.pi/agent/APPEND_SYSTEM.md | 79 +++++++++++---------------------- 2 files changed, 56 insertions(+), 80 deletions(-) diff --git a/home/.claude/CLAUDE.md b/home/.claude/CLAUDE.md index ea0584bb..83e59f46 100644 --- a/home/.claude/CLAUDE.md +++ b/home/.claude/CLAUDE.md @@ -1,38 +1,41 @@ - +# Working Principles - +1. **Ask, don't assume.** If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements. When running unattended, pick the most reasonable interpretation, proceed, and record the assumption rather than blocking. +2. **Match solution weight to problem weight.** Implement the simplest solution for simple problems, better solutions for harder problems. Do not over-engineer or add flexibility that isn't needed yet — and do not cut corners on: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, or explicit user requirements. Simple does not mean skipping what's genuinely required; it means no extra, and no less. +3. **Stay in scope, surface smells.** Don't touch unrelated code, but do flag bad code or design smells you discover so we can address them as a separate issue. +4. **Flag uncertainty explicitly.** If you're unsure about something, see point 1. If it helps, run a small, localized, low-risk experiment and bring the hypothesis and results to discuss. Confidence without certainty causes more damage than admitting a gap. +5. **Suggest better ways.** I'm always open to ideas on better ways to do things. Don't hesitate to suggest a better approach, or one with lasting impact over a tactical change. -- The user doesn't like sycophancy. -- Be brief. No preambles, no summaries, no narrating actions, Don't repeat my instructions back to me. -- Don't explain code you just wrote; the user can read it. -- Stop excessive validation; challenge the user's reasoning. +# Decision Framework - +When evaluating solutions, prefer the option with: - +1. Fewer moving parts +2. Modern APIs and idioms first; industry precedent is the fallback when no modern option exists. +3. Lower cognitive load for future maintainers +4. Clear failure modes and debuggability -- The user is a senior developer. No trivial comments, no hand-holding. -- KISS. No over-engineering, no premature abstractions, no "just in case" code. -- Minimize state — it's where bugs hide. Prefer stateless approaches where practical, but don't overcomplicate things to avoid it. -- Always consider the non-happy path. What can go wrong? +* Do not optimize for edge cases unless they are explicitly required. +* Avoid introducing new dependencies unless they provide significant, proven value. +* When two options are similar in size, prefer the one correct on edge cases. +* Optimize for fewest concepts, not fewest files. +* Building procedure — stop at the first rung that holds: does it need to exist? (skip if speculative) → does stdlib cover it? → does a native platform feature cover it (e.g. `` over a picker lib, a DB constraint over app code)? → does an installed dependency cover it? → can it be one line? → only then, the minimum code that works. - +# Output Expectations - +* Prefer actionable recommendations over listing many options. +* If multiple approaches are viable, briefly compare and then recommend one. +* Highlight trade-offs explicitly (e.g. simplicity vs flexibility, performance vs readability). +* When the answer is code: lead with the code, then at most a few short lines naming what was skipped and when to add it back. -- *Take the correct approach, not the easy one.* Technical debt compounds. A shortcut today becomes a refactoring nightmare tomorrow. Always choose the long-term solution. -- Never assume, always verify. Don't trust plans, comments, variable names, or your own intuition. Read the code. Compare the numbers. Document what you find with file:line references. -- "Good enough" is not good enough. If there's a known issue, raise it. Figure it out. Fix it. Don't say "acceptable for now" or "close enough". -- The user makes the decisions. When there's a tradeoff, present the options with evidence and let the user decide. Don't silently pick the easy path. +# Communication Style - +* Be concise, but include enough detail to make the reasoning clear. +* Avoid vague statements; use concrete examples where helpful. +* Do not agree by default — agreement must be earned. +* Avoid filler, fluff, and generic "LLM-style" phrasing. - +# Context Efficiency -The user might not always know what they want, might ask for something ambiguous, or something sub-optimal. -It is your job as an expert to first gather context, and challenge the user's assumptions if needed, before writing any code. -In short, 1) understand the problem, 2) gather context (read code), 3) clarify or challenge the user's request, 4) write the code. - - - - +* Before reading a file, ask: can I answer this from what I already know? If yes, skip the read. +* The building-decision procedure above governs change work. On Q&A, review, or explanation turns, deprioritize it — answer the question first. diff --git a/home/.pi/agent/APPEND_SYSTEM.md b/home/.pi/agent/APPEND_SYSTEM.md index b206b0e7..f2797ad8 100644 --- a/home/.pi/agent/APPEND_SYSTEM.md +++ b/home/.pi/agent/APPEND_SYSTEM.md @@ -1,9 +1,10 @@ -# Persona +# Working Principles -* You prioritize correctness, simplicity, and long-term maintainability over cleverness or novelty. -* You default to well-established patterns, standard libraries, and widely adopted practices. -* You avoid premature abstraction; abstractions must be justified by clear, repeated need. The user owns abstractions — when the user articulates one ("we do this elsewhere," "the pattern is X"), apply it. Do not extract a general rule from a single instance on your own. -* You are willing to challenge the user when their approach is flawed or suboptimal. +1. **Ask, don't assume.** If something is unclear, ask before writing a single line. Never make silent assumptions about intent, architecture, or requirements. When running unattended, pick the most reasonable interpretation, proceed, and record the assumption rather than blocking. +2. **Match solution weight to problem weight.** Implement the simplest solution for simple problems, better solutions for harder problems. Do not over-engineer or add flexibility that isn't needed yet — and do not cut corners on: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, or explicit user requirements. Simple does not mean skipping what's genuinely required; it means no extra, and no less. +3. **Stay in scope, surface smells.** Don't touch unrelated code, but do flag bad code or design smells you discover so we can address them as a separate issue. +4. **Flag uncertainty explicitly.** If you're unsure about something, see point 1. If it helps, run a small, localized, low-risk experiment and bring the hypothesis and results to discuss. Confidence without certainty causes more damage than admitting a gap. +5. **Suggest better ways.** I'm always open to ideas on better ways to do things. Don't hesitate to suggest a better approach, or one with lasting impact over a tactical change. # Decision Framework @@ -20,74 +21,46 @@ When evaluating solutions, prefer the option with: * Optimize for fewest concepts, not fewest files. * Building procedure — stop at the first rung that holds: does it need to exist? (skip if speculative) → does stdlib cover it? → does a native platform feature cover it (e.g. `` over a picker lib, a DB constraint over app code)? → does an installed dependency cover it? → can it be one line? → only then, the minimum code that works. -# Pushback Rules - -Push back when the user: - -* Reinvents existing tools, frameworks, or infrastructure -* Introduces unnecessary abstraction or indirection -* Overengineers for hypothetical future needs -* Ignores common best practices or constraints of the language/platform - -When pushing back: - -* Be direct and specific -* Clearly explain why the approach is problematic -* Provide a concrete, better alternative - # Output Expectations * Prefer actionable recommendations over listing many options. * If multiple approaches are viable, briefly compare and then recommend one. -* Highlight trade-offs explicitly (e.g., simplicity vs flexibility, performance vs readability). -* Make assumptions explicit when required. +* Highlight trade-offs explicitly (e.g. simplicity vs flexibility, performance vs readability). * When the answer is code: lead with the code, then at most a few short lines naming what was skipped and when to add it back. # Communication Style * Be concise, but include enough detail to make the reasoning clear. * Avoid vague statements; use concrete examples where helpful. -* Do not agree by default—agreement must be earned. -* Avoid filler, fluff, and generic “LLM-style” phrasing. - -# Uncertainty Handling - -* If information is missing or ambiguous, clarify or state assumptions before proceeding. -* Do not present speculation as fact. -* If something depends on context, say what it depends on. - -# Constraints - -* Do not invent new patterns, architectures, or terminology without strong justification. -* Do not over-abstract or generalize beyond what the problem requires. -* Favor clarity and explicitness over clever or “smart” solutions. -* Favor correct architecture, design, and extensibility over "quick" solutions, even if it means more upfront work. -* Never cut corners on: input validation at trust boundaries, error handling that prevents data loss, security, accessibility, or explicit user requirements. -* Hardware is real, not the spec ideal — clocks drift, sensors read off, peripherals run a few percent fast. Leave calibration knobs, not just less code. -* Non-trivial logic (a branch, a loop, a parser, a money or security path) leaves one runnable check behind: an assert-based `__main__` self-check or one small test. Trivial one-liners need none. +* Do not agree by default — agreement must be earned. +* Avoid filler, fluff, and generic "LLM-style" phrasing. # Context Efficiency * Before reading a file, ask: can I answer this from what I already know? If yes, skip the read. * The building-decision procedure above governs change work. On Q&A, review, or explanation turns, deprioritize it — answer the question first. -# Search Tools +# Tool Use -**Search lives in `ffgrep` / `fffind`, never in `bash`. Drift signature: `cmd && grep`, `cmd | rg`, `ls dir/**` to discover, `find . -name ...`, `grep -r "..."`. When you see yourself composing any of these mid-investigation — STOP, split, use the search tools.** +* Trust the write tool's response; do not re-read files to verify writes. -Why this drifts: when `bash` is already loaded mid-flow, appending `&& grep` feels cheaper than opening a new tool call. That calculation is wrong. `ffgrep` / `fffind` are one tool call each, and the *next* search is faster after them (frecency + git-aware ranking). The drift compounds — pay now or pay more later. +## grep — content search (repo at CWD) -**Pre-flight on every `bash` command.** If the line contains `grep`, `egrep`, `fgrep`, `rg`, `ag`, `find`, `tree`, `ls **`, or a glob across files, the goal is discovery — use `ffgrep` (content) or `fffind` (path/glob) instead. `bash` stays for: reading a file with a known path (`read` / `cat` / `head`), running scripts, builds, installs, git ops, and searches outside the repo (`cd && rg ...`). +* `path` MUST be repo-relative; absolute paths error. Outside-repo → `cd && rg ...`. +* Smart-case: all-lowercase pattern = case-insensitive; mixed-case or `caseSensitive: true` = exact. +* Use bare identifiers (e.g. `spawn_agent`, not `.*spawn.*`). Wildcard patterns (`.*`, `*`, `.`, `.+`) are **rejected** — use a concrete substring. Regex is auto-detected only when metacharacters are present; don't add anchors unless you mean them. +* Multi-word = AND-narrow (each word narrows), not OR-wide. +* `exclude`: comma/array of prefixes, filenames, globs (`test/,*.min.js`). Leading `!` optional. +* On 0 exact matches it retries fuzzy and prepends "**[0 exact matches. Maybe you meant this?]**" — a discovery hint, **not** an actionable result. Treat 0 exact = 0 results and refine the query. +* `context: N` adds N lines before+after each match. Raise `limit` for broad sweeps. -`ffgrep` = content search. `fffind` = path/filename search. Both are pi-native, frecency-ranked, git-aware. +## find — fuzzy path search -* Scope = workspace repo at CWD. `path` MUST be repo-relative; absolute paths error. Outside-repo search → fall back to `cd && rg ...`. -* Smart-case default; force `caseSensitive: true` for exact case. Auto-detects regex vs literal. Multi-word = AND-narrow, not OR. -* `exclude` = comma/array of path prefixes, filenames, globs (`test/,*.lock,*.min.js`). -* `pattern` is fuzzy filename matching; use `path` for globs (`path: "src/**/*.ts"`), `pattern` for concepts (`pattern: "spawn_agent"`). -* `context: N` adds N lines around matches. `cursor` paginates beyond `limit`. -* On 0 exact matches, `ffgrep` falls back to fuzzy path matches prefixed "Maybe you meant this?" — discovery hint, not a result. +* Matches the **whole repo-relative path**, not just filename: `pattern: "profile"` hits `chrome/browser/profiles/x.cc`. +* `pattern` = fuzzy concept (`"spawn_agent"`); `path` = glob (`"src/**/*.ts"`), prefix (`"src/"`), or bare filename (`"main.rs"`). +* Weak matches cap at **5 samples with a notice** — don't treat a weak top score as exhaustive. Use a glob `path` (e.g. `"**/profile.h"`) when you need exact/exhaustive listing. -# Tool Use +## Both -* Trust the write tool's response; do not re-read files to verify writes. +* First call at session start may block ~15s while the index builds; instant after that. +* Safe to call both in parallel. \ No newline at end of file From 236a17903aa2e4b4c58fe7a258137fc3a04e7885 Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:05:35 +0900 Subject: [PATCH 131/366] fix(pi): keep mac awake indefinitely during agent runs 30-minute timeout could let macOS sleep during long sessions. Process exit handler ensures cleanup even on abrupt shutdown. --- home/.pi/agent/extensions/caffeinate.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/home/.pi/agent/extensions/caffeinate.ts b/home/.pi/agent/extensions/caffeinate.ts index 89bd16fb..2c3cce08 100644 --- a/home/.pi/agent/extensions/caffeinate.ts +++ b/home/.pi/agent/extensions/caffeinate.ts @@ -22,7 +22,7 @@ export default function (pi: ExtensionAPI) { function startCaffeinate() { // Kill any existing instance before starting a new one. killCaffeinate(); - proc = spawn("/usr/bin/caffeinate", ["-t", "1800"], { + proc = spawn("/usr/bin/caffeinate", ["-i"], { stdio: "ignore", detached: false, }); @@ -42,4 +42,6 @@ export default function (pi: ExtensionAPI) { // Safety net: clean up on session shutdown (quit, reload, switch, fork). pi.on("session_shutdown", killCaffeinate); + + process.on("exit", killCaffeinate); } From 267567f4f6108515bff586ce948f537380d33d7e Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:07:46 +0900 Subject: [PATCH 132/366] docs(commit): clarify git status output interpretation Make the staged-check command return explicit human-readable output instead of relying on exit codes. Document what each column of git status -s means. --- home/.agents/skills/commit/SKILL.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/home/.agents/skills/commit/SKILL.md b/home/.agents/skills/commit/SKILL.md index 3097cb11..c3d2e04d 100644 --- a/home/.agents/skills/commit/SKILL.md +++ b/home/.agents/skills/commit/SKILL.md @@ -5,12 +5,15 @@ description: Create a git commit. Use whenever you or the user wants to create a ## Context -- Are there staged files? Run `git diff --cached --quiet` — silent output means no staged files. +- To check if anything is staged: `git diff --cached --quiet && echo "nothing staged" || echo "HAS STAGED CHANGES"` - Use the description the user provided when invoking this skill. If none was given, assume "staged changes". ### `git status -s` output -Run `git status -s` to see the staged changes. +Run `git status -s` to see the staged changes. The first column shows the staging area (index), the second shows the working tree: + - `M ` (first column) = staged + - ` M` (second column) = unstaged but modified + - `??` = untracked ## Your task From 5e231bb160541299f58da0b1edf2df15842944fb Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:36:17 +0900 Subject: [PATCH 133/366] chore: pi --- home/.pi/agent/extensions/package-lock.json | 47 +++--- home/.pi/agent/extensions/package.json | 6 +- home/.pi/agent/extensions/subagents/index.ts | 72 +++++---- .../.pi/agent/extensions/subagents/process.ts | 22 ++- home/.pi/agent/extensions/subagents/render.ts | 24 ++- .../subagents/tests/process.test.ts | 142 ++++++++++++++++++ pi-web/src/runtime.ts | 4 +- 7 files changed, 241 insertions(+), 76 deletions(-) create mode 100644 home/.pi/agent/extensions/subagents/tests/process.test.ts diff --git a/home/.pi/agent/extensions/package-lock.json b/home/.pi/agent/extensions/package-lock.json index 9745b4a0..7a95b72b 100644 --- a/home/.pi/agent/extensions/package-lock.json +++ b/home/.pi/agent/extensions/package-lock.json @@ -8,9 +8,9 @@ "name": "extensions", "version": "1.0.0", "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-coding-agent": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-coding-agent": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "execa": "^9.6.1", "neverthrow": "^8.2.0", "typebox": "^1.2.17", @@ -503,9 +503,9 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", - "integrity": "sha512-ZpSwaD7oNpsjn9vtEatZQNT9PSdDJXi6rFeY5Qv+OHQGFDKlmcrfJE4ypm4SAc/fBECPs4Rdi3l+YjVtXYrkKw==", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", + "integrity": "sha512-5GNKfdrRJ4uZ5Zd9iudoXggi/BbUcKnD/xfRHtdR+7q4vWqPvfx8auFuaT+ewGBVI8K4wj87eigFQ/iCSuy9RQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -534,15 +534,15 @@ "license": "MIT" }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.8.tgz", - "integrity": "sha512-wr9oTS/yrwURDXnYrONQgFgV7QDlwslXL/rvKU5X7TRtrGxIhippsRApXqYlRwSeMjb2YzgHMfZ/kAhOqrzoFQ==", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.2.tgz", + "integrity": "sha512-m9v7OUit0s9LklWfh61ca/XY5INjUzjtYtNZwy3cNvyjOLk3IpBgghP8aAp0iH35rLaiRwuuWiJ8t88ODMWY+A==", "hasShrinkwrap": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.8", - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-agent-core": "^0.80.2", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -1005,11 +1005,11 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.8.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", + "@earendil-works/pi-ai": "^0.80.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -1019,8 +1019,8 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.8.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -1036,15 +1036,15 @@ "typebox": "1.1.38" }, "bin": { - "pi-ai": "./dist/cli.js" + "pi-ai": "dist/cli.js" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -2367,9 +2367,9 @@ } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.79.8", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.8.tgz", - "integrity": "sha512-QerB+0wUc6eEO8MwvzOQGtzcsbwo6y8VvdxYU6vGcakz6ofJZWhrmwrknp1dCGx3bEtCf+siUIxEzkqvFCzIsg==", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", + "integrity": "sha512-OvOAMIbXiC9OSse17YMiXIsI9AS5XM/ZV8N/k+UzdlRpPILDQYmLElevgGW92kkXR8qHBClIdzhCjuzlBGvphA==", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -3816,7 +3816,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" diff --git a/home/.pi/agent/extensions/package.json b/home/.pi/agent/extensions/package.json index e6f37c5b..2a2af97d 100644 --- a/home/.pi/agent/extensions/package.json +++ b/home/.pi/agent/extensions/package.json @@ -6,9 +6,9 @@ "check": "tsc -p tsconfig.json && eslint && node --test '**/tests/*.test.ts'" }, "dependencies": { - "@earendil-works/pi-ai": "^0.79.8", - "@earendil-works/pi-coding-agent": "^0.79.8", - "@earendil-works/pi-tui": "^0.79.8", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-coding-agent": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "execa": "^9.6.1", "neverthrow": "^8.2.0", "typebox": "^1.2.17", diff --git a/home/.pi/agent/extensions/subagents/index.ts b/home/.pi/agent/extensions/subagents/index.ts index e246b78c..5ce75346 100644 --- a/home/.pi/agent/extensions/subagents/index.ts +++ b/home/.pi/agent/extensions/subagents/index.ts @@ -4,7 +4,7 @@ * Runs the spawned agent as a `pi --mode json --print --no-session` child * process with its own context, model, and tools, and returns the child's * final text. Codex-compatible arg shape. Depth capped at 3 (tracked across - * the process tree via CODEX_SUBAGENT_DEPTH). + * the process tree via PI_SUBAGENT_DEPTH). * * Architecture: this is the only module that crosses into pi's tool world. * It composes Results from agents.ts / process.ts and converts Err → throw @@ -16,27 +16,10 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Container, Text } from "@earendil-works/pi-tui"; import { Type } from "typebox"; import { discoverAgents, formatAgentList, resolveAgent } from "./agents.ts"; -import { getFinalText, runSubprocess, type RunDetails, type SpawnError } from "./process.ts"; +import { DEPTH_ENV, getFinalText, runSubprocess, type RunDetails, type SpawnError } from "./process.ts"; import { manageTick, renderCallHeader, renderResultBlock } from "./render.ts"; const MAX_DEPTH = 3; -const DEPTH_ENV = "CODEX_SUBAGENT_DEPTH"; - -const SpawnParams = Type.Object({ - message: Type.String({ description: "The specific task for this spawn. The agent's role (its system prompt) defines how it works; this is the instance of work to do. Self-contained — the child has no parent history." }), - task_name: Type.Optional(Type.String({ description: "Short label for UI and logs. Omit to derive from the message." })), - agent_type: Type.Optional(Type.String({ description: "Name of the subagent role to use. Omit for the default role." })), - reasoning_effort: Type.Optional(Type.Union([ - Type.Literal("none"), - Type.Literal("minimal"), - Type.Literal("low"), - Type.Literal("medium"), - Type.Literal("high"), - Type.Literal("xhigh"), - ], { description: "Override reasoning effort for the child." })), - fork_turns: Type.Optional(Type.String({ description: "Context inheritance mode. Currently only 'none' (default, fresh context) is supported." })), - cwd: Type.Optional(Type.String({ description: "Working directory for the child agent. Defaults to current cwd." })), -}); export default function (_pi: ExtensionAPI) { // Discover agents at registration time so the model sees the current @@ -53,6 +36,28 @@ export default function (_pi: ExtensionAPI) { .map((a) => `- **${a.name}** — ${a.description}`) .join("\n"); + // Build agent_type as an enum of the discovered names so the model gets a + // hard constraint instead of free text that round-trips as an error. + const agentLiterals = agents.map((a) => Type.Literal(a.name)); + const agentTypeSchema = agentLiterals.length + ? Type.Union(agentLiterals, { description: "Name of the subagent role to use. Omit for the default role." }) + : Type.String({ description: "Name of the subagent role to use. Omit for the default role." }); + + const SpawnParams = Type.Object({ + message: Type.String({ description: "The specific task for this spawn. The agent's role (its system prompt) defines how it works; this is the instance of work to do. Self-contained — the child has no parent history." }), + task_name: Type.Optional(Type.String({ description: "Short label for UI and logs. Omit to derive from the message." })), + agent_type: Type.Optional(agentTypeSchema), + reasoning_effort: Type.Optional(Type.Union([ + Type.Literal("none"), + Type.Literal("minimal"), + Type.Literal("low"), + Type.Literal("medium"), + Type.Literal("high"), + Type.Literal("xhigh"), + ], { description: "Override reasoning effort for the child." })), + cwd: Type.Optional(Type.String({ description: "Working directory for the child agent. Defaults to current cwd." })), + }); + // Per-row tick intervals, keyed by toolCallId. A single shared slot would // let two concurrent spawns clobber each other's interval. const ticks = new Map(); @@ -75,19 +80,9 @@ export default function (_pi: ExtensionAPI) { // Each check throws a targeted message; pi catches and marks isError. const parentDepth = Number.parseInt(process.env[DEPTH_ENV] ?? "0", 10) || 0; if (parentDepth >= MAX_DEPTH) { - throw new Error(`spawn_agent depth ${parentDepth} >= max ${MAX_DEPTH}.`); - } - if (params.fork_turns && params.fork_turns !== "none") { - throw new Error(`fork_turns='${params.fork_turns}' is not yet implemented. Use 'none' (default) for a fresh-context child.`); + throw new Error(`spawn_agent capped at depth ${MAX_DEPTH}. Do not spawn a further child — perform this work in your current session instead.`); } - const agents = discoverAgents().match( - (list) => list, - (e) => { - throw new Error(discoveryErrorMessage(e)); - }, - ); - const requestedType = params.agent_type?.trim() || "default"; const agent = resolveAgent(agents, requestedType).match( (a) => a, @@ -106,7 +101,7 @@ export default function (_pi: ExtensionAPI) { defaultCwd: ctx.cwd, agent, message: params.message, - taskName: params.task_name?.trim() || params.message.slice(0, 60), + taskName: params.task_name?.trim() || clipAtWord(params.message, 60), reasoningEffortOverride: params.reasoning_effort, cwd: params.cwd, parentDepth, @@ -164,7 +159,18 @@ function discoveryErrorMessage(e: { kind: string; dir: string; cause?: NodeJS.Er function spawnErrorMessage(e: SpawnError): string { const details = e.details; - const finalText = getFinalText(details.messages); if (e.kind === "aborted") return "spawn_agent aborted."; - return details.stderr.trim() || finalText || `Subagent exited with code ${details.exitCode}.`; + const code = `Subagent exited with code ${details.exitCode}.`; + const stderr = details.stderr.trim(); + const finalText = getFinalText(details.messages); + const body = stderr ? (finalText ? `${stderr}\n${finalText}` : stderr) : finalText; + return body ? `${code} ${body}` : code; +} + +function clipAtWord(s: string, max: number): string { + const one = s.replace(/\s+/g, " ").trim(); + if (one.length <= max) return one; + const cut = one.slice(0, max); + const lastSpace = cut.lastIndexOf(" "); + return (lastSpace > max * 0.5 ? one.slice(0, lastSpace) : cut) + "…"; } diff --git a/home/.pi/agent/extensions/subagents/process.ts b/home/.pi/agent/extensions/subagents/process.ts index b7d3b3a5..1407c7d4 100644 --- a/home/.pi/agent/extensions/subagents/process.ts +++ b/home/.pi/agent/extensions/subagents/process.ts @@ -17,8 +17,9 @@ import { errAsync, okAsync, ResultAsync } from "neverthrow"; import type { AgentConfig } from "./agents.ts"; import { z } from "zod"; -const DEPTH_ENV = "CODEX_SUBAGENT_DEPTH"; +export const DEPTH_ENV = "PI_SUBAGENT_DEPTH"; const UPDATE_THROTTLE_MS = 150; +const MAX_RECENT_TOOLS = 50; // ── trust boundary: the child's JSON event stream ─────────────────── const UsageSchema = z.object({ @@ -42,10 +43,10 @@ const MessageSchema = z.object({ usage: UsageSchema.optional(), }); -const EventSchema = z.discriminatedUnion("type", [ - z.object({ type: z.literal("message_end"), message: MessageSchema }), - z.object({ type: z.literal("tool_result_end"), message: MessageSchema }), -]); +const EventSchema = z.object({ + type: z.literal("message_end"), + message: MessageSchema, +}); @@ -148,7 +149,7 @@ async function runAndCollect(params: RunParams, details: RunDetails): Promise params.onUpdate?.(details); + const emit = () => params.onUpdate?.(toUpdateSnapshot(details)); const invocation = getPiInvocation(args); @@ -193,6 +194,10 @@ async function runAndCollect(params: RunParams, details: RunDetails): Promise { const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "subagent-")); const safeName = agentName.replace(/[^\w.-]+/g, "_"); @@ -202,7 +207,7 @@ async function writeTempPrompt(agentName: string, systemPrompt: string): Promise } /** Parse one JSON event line and fold it into the run details. */ -function ingestLine(line: string, details: RunDetails): void { +export function ingestLine(line: string, details: RunDetails): void { if (!line.trim()) return; let raw: unknown; try { @@ -235,6 +240,7 @@ function ingestLine(line: string, details: RunDetails): void { if (part.type === "toolCall") { details.toolCount++; details.recentTools.push({ name: part.name, argsPreview: argsPreview(part.arguments) }); + if (details.recentTools.length > MAX_RECENT_TOOLS) details.recentTools.shift(); } else if (part.type === "text" && part.text.trim()) { const prose = part.text.split("\n").find((l) => l.trim() && !l.trimStart().startsWith("```")); if (prose) details.lastMessage = prose.trim(); @@ -272,7 +278,7 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } { // ── small helpers ───────────────────────────────────────────────────── -function argsPreview(args: unknown): string { +export function argsPreview(args: unknown): string { if (!args || typeof args !== "object") return ""; const a = args as Record; for (const k of ["path", "file_path", "command", "query", "url", "pattern", "content"]) { diff --git a/home/.pi/agent/extensions/subagents/render.ts b/home/.pi/agent/extensions/subagents/render.ts index e517478d..cc509127 100644 --- a/home/.pi/agent/extensions/subagents/render.ts +++ b/home/.pi/agent/extensions/subagents/render.ts @@ -6,7 +6,7 @@ import type { Theme } from "@earendil-works/pi-coding-agent"; import { Container, Spacer, Text } from "@earendil-works/pi-tui"; -import { getFinalText, type RunDetails } from "./process.ts"; +import { DEPTH_ENV, getFinalText, type RunDetails } from "./process.ts"; const TICK_INTERVAL_MS = 1000; @@ -33,17 +33,23 @@ export function taskPreview(s: string): string { return one.length > 80 ? `${one.slice(0, 77)}...` : one; } +function clipLine(s: string, max: number): string { + return s.length > max ? `${s.slice(0, max - 1)}…` : s; +} + // ── tool-call header ────────────────────────────────────────────────── export function renderCallHeader( c: Container, - args: { message?: string; agent_type?: string; task_name?: string; model?: string; cwd?: string }, + args: { message?: string; agent_type?: string; task_name?: string; reasoning_effort?: string; model?: string; cwd?: string }, expanded: boolean, theme: Theme, ): void { const agentLabel = args.agent_type ? ` ${theme.fg("accent", args.agent_type)}` : ""; - const meta: string[] = []; + const parentDepth = Number.parseInt(process.env[DEPTH_ENV] ?? "0", 10) || 0; + const meta: string[] = [theme.fg("muted", `[d${parentDepth + 1}]`)]; if (args.task_name) meta.push(theme.fg("muted", `· ${args.task_name}`)); + if (args.reasoning_effort) meta.push(theme.fg("muted", `· effort=${args.reasoning_effort}`)); if (args.model) meta.push(theme.fg("muted", `· model=${args.model}`)); if (args.cwd) meta.push(theme.fg("muted", `· cwd=${args.cwd}`)); c.addChild(new Text(`${theme.fg("toolTitle", theme.bold("spawn_agent"))}${agentLabel} ${meta.join(" ")}`, 0, 0)); @@ -89,13 +95,19 @@ export function renderResultBlock(details: RunDetails, options: RenderOptions, t // Tool log: last 8 collapsed, all expanded; earlier entries summarized. const tools = details.recentTools; const visibleCount = options.expanded ? tools.length : Math.min(tools.length, 8); - if (tools.length > visibleCount) { - c.addChild(new Text(theme.fg("dim", `… ${tools.length - visibleCount} earlier actions`), 0, 0)); + if (details.toolCount > visibleCount) { + c.addChild(new Text(theme.fg("dim", `… ${details.toolCount - visibleCount} earlier actions`), 0, 0)); } for (let i = tools.length - visibleCount; i < tools.length; i++) { const t = tools[i]; const body = t.argsPreview ? `${t.name}: ${t.argsPreview}` : t.name; - c.addChild(new Text(theme.fg("muted", ` ${body}`), 0, 0)); + c.addChild(new Text(theme.fg("muted", ` ${clipLine(body, 100)}`), 0, 0)); + } + + // Waiting indicator for the long first-turn gap before any tool event. + if (isRunning && details.toolCount === 0 && !details.lastMessage) { + c.addChild(new Spacer(1)); + c.addChild(new Text(theme.fg("dim", "waiting for first response…"), 0, 0)); } // Latest thinking line. Skip when the tool log is empty — nothing to think about yet. diff --git a/home/.pi/agent/extensions/subagents/tests/process.test.ts b/home/.pi/agent/extensions/subagents/tests/process.test.ts new file mode 100644 index 00000000..b29c2875 --- /dev/null +++ b/home/.pi/agent/extensions/subagents/tests/process.test.ts @@ -0,0 +1,142 @@ +import { test } from "node:test"; +import * as assert from "node:assert/strict"; +import type { Message } from "@earendil-works/pi-ai"; +import { argsPreview, getFinalText, ingestLine, type RunDetails } from "../process.ts"; + +function fresh(): RunDetails { + return { + agent: "test", + taskName: "test", + depth: 1, + exitCode: 0, + messages: [], + stderr: "", + aborted: false, + startTime: 0, + toolCount: 0, + recentTools: [], + lastMessage: "", + tokens: 0, + usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 }, + }; +} + +function msgEnd(message: unknown): string { + return JSON.stringify({ type: "message_end", message }); +} + +const assistantWithUsage = (content: unknown, usage?: unknown): unknown => ({ + role: "assistant", + content, + ...(usage ? { usage } : {}), +}); + +const msgs = (xs: unknown[]): Message[] => xs as unknown as Message[]; + +test("getFinalText returns empty for no assistant messages", () => { + assert.equal(getFinalText([]), ""); + assert.equal(getFinalText(msgs([{ role: "user", content: [{ type: "text", text: "hi" }] }])), ""); +}); + +test("getFinalText returns last assistant text block, trimming whitespace", () => { + assert.equal( + getFinalText(msgs([ + { role: "assistant", content: [{ type: "text", text: " first " }] }, + { role: "toolResult", content: [{ type: "text", text: "output" }] }, + { role: "assistant", content: [{ type: "text", text: " final answer " }] }, + ])), + "final answer", + ); +}); + +test("getFinalText skips empty text parts", () => { + assert.equal( + getFinalText(msgs([{ role: "assistant", content: [{ type: "text", text: " " }, { type: "text", text: "real" }] }])), + "real", + ); +}); + +test("ingestLine ignores non-JSON and unrelated event types", () => { + const d = fresh(); + ingestLine("not json", d); + ingestLine(JSON.stringify({ type: "turn_start" }), d); + ingestLine("", d); + assert.deepEqual(d.messages, []); + assert.equal(d.toolCount, 0); +}); + +test("ingestLine folds an assistant message_end into usage/tools/lastMessage", () => { + const d = fresh(); + ingestLine( + msgEnd( + assistantWithUsage( + [ + { type: "text", text: "Let me check.\n```code\nx\n```\nDone now" }, + { type: "toolCall", name: "read", arguments: { path: "/a/b.ts" } }, + ], + { input: 10, output: 5, cacheRead: 2, cost: { total: 0.01 } }, + ), + ), + d, + ); + assert.equal(d.messages.length, 1); + assert.equal(d.toolCount, 1); + assert.deepEqual(d.recentTools, [{ name: "read", argsPreview: "/a/b.ts" }]); + assert.equal(d.usage.input, 10); + assert.equal(d.usage.output, 5); + assert.equal(d.usage.cacheRead, 2); + assert.equal(d.usage.cost, 0.01); + assert.equal(d.usage.turns, 1); + assert.equal(d.tokens, 17); + assert.equal(d.lastMessage, "Let me check."); +}); + +test("ingestLine: toolResult arrives via message_end (not tool_result_end)", () => { + const d = fresh(); + ingestLine(msgEnd({ role: "toolResult", content: [{ type: "text", text: "result" }] }), d); + assert.equal(d.messages.length, 1); + assert.equal(d.toolCount, 0); + assert.equal(d.lastMessage, ""); +}); + +test("ingestLine ignores a tool_result_end event — pins the wire format", () => { + const d = fresh(); + ingestLine(JSON.stringify({ type: "tool_result_end", message: { role: "toolResult", content: [] } }), d); + assert.deepEqual(d.messages, []); +}); + +test("ingestLine: lastMessage skips blank lines and fence delimiters", () => { + const d = fresh(); + ingestLine( + msgEnd(assistantWithUsage([{ type: "text", text: "```ts\nconst x = 1;\n```\nReal prose" }])), + d, + ); + assert.equal(d.lastMessage, "const x = 1;"); +}); + +test("recentTools is capped to a rolling window of the most recent calls", () => { + const d = fresh(); + for (let i = 0; i < 60; i++) { + ingestLine( + msgEnd(assistantWithUsage([{ type: "toolCall", name: "bash", arguments: { command: `cmd ${i}` } }])), + d, + ); + } + assert.equal(d.toolCount, 60); + assert.ok(d.recentTools.length <= 50); + assert.equal(d.recentTools[0].argsPreview, "cmd 10"); + assert.equal(d.recentTools.at(-1)?.argsPreview, "cmd 59"); +}); + +test("argsPreview picks known keys", () => { + assert.equal(argsPreview({ path: "/x" }), "/x"); + assert.equal(argsPreview({ command: "ls -la" }), "ls -la"); + assert.equal(argsPreview({ query: "hi there" }), "hi there"); +}); + +test("argsPreview falls back to compact JSON for unknown shapes", () => { + assert.equal(argsPreview({ foo: "bar", n: 1 }), '{"foo":"bar","n":1}'); + assert.equal(argsPreview(undefined), ""); + assert.equal(argsPreview("str"), ""); + assert.equal(argsPreview(null), ""); +}); diff --git a/pi-web/src/runtime.ts b/pi-web/src/runtime.ts index eedc0175..0b473eab 100644 --- a/pi-web/src/runtime.ts +++ b/pi-web/src/runtime.ts @@ -17,7 +17,7 @@ import { DefaultResourceLoader, type CreateAgentSessionRuntimeFactory, } from "@earendil-works/pi-coding-agent"; -import { getModel } from "@earendil-works/pi-ai"; +import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all"; import type { Model } from "@earendil-works/pi-ai"; import { type BridgeRuntime, @@ -55,7 +55,7 @@ export async function createRealRuntime(opts: { ); } if (!model) { - model = getModel("anthropic", "claude-sonnet-4-5") ?? undefined; + model = getBuiltinModel("anthropic", "claude-sonnet-4-5"); } const sessionManager = SessionManager.create(opts.cwd); From 50409c81017b4c17bbb6b5bf9f6812cf8417b16a Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:46:55 +0900 Subject: [PATCH 134/366] chore: better skills --- home/.agents/skills/commit/SKILL.md | 4 ++-- home/.agents/skills/create-pr/SKILL.md | 2 +- home/.agents/skills/go-code/SKILL.md | 9 ++------- home/.agents/skills/go-testing/SKILL.md | 2 +- home/.agents/skills/risk-review/SKILL.md | 1 - 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/home/.agents/skills/commit/SKILL.md b/home/.agents/skills/commit/SKILL.md index c3d2e04d..cb24ea37 100644 --- a/home/.agents/skills/commit/SKILL.md +++ b/home/.agents/skills/commit/SKILL.md @@ -23,8 +23,8 @@ Create a single git commit using **Conventional Commits** format: (): ``` -- **type**: `feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `perf`, `ci`, `build` -- **scope**: optional, the area of the codebase (e.g. `nvim`, `tmux`, `shell`, `backend`) +- **type**: `feat` (new feature), `fix` (bug fix only), `refactor` (restructure without behavior change), `chore` (general random things — build config, deps, minor tweaks, misc), `docs` (documentation), `test` (testing), `perf` (performance), `ci` (CI/CD pipeline, workflows, actions, infra), `build` (build system, tooling) +- **scope**: optional, the BROAD area of the codebase (e.g. `nvim`, `tmux`, `shell`, `backend`, `frontend`), not individual features - **summary**: imperative, lowercase, no period, max 50 chars (hard limit 72) **Litmus test**: a new contributor should understand the problem, why it matters, and the impact without opening files or reading the diff. Avoid code identifiers, filenames, and function names in the summary unless they ARE the user-facing impact. diff --git a/home/.agents/skills/create-pr/SKILL.md b/home/.agents/skills/create-pr/SKILL.md index aac90f3f..54da70c4 100644 --- a/home/.agents/skills/create-pr/SKILL.md +++ b/home/.agents/skills/create-pr/SKILL.md @@ -41,4 +41,4 @@ The user may have provided a description when invoking this skill. Use it; other ### Body - Fill in the PR template if one exists. Remove sections that don't apply. -- Write like a human would — no exhaustive bullet points, be brief and assume the reader knows the codebase. +- No exhaustive bullet points, be brief and assume the reader knows the codebase. diff --git a/home/.agents/skills/go-code/SKILL.md b/home/.agents/skills/go-code/SKILL.md index ffd20dca..01d9728a 100644 --- a/home/.agents/skills/go-code/SKILL.md +++ b/home/.agents/skills/go-code/SKILL.md @@ -7,7 +7,7 @@ description: Use ALWAYS when writing, editing, or reviewing ANY Go code — no e ## Overview -Go best practices for clean, idiomatic, maintainable code. Core principle: **Clear > Clever**. +Go best practices for clean, idiomatic, maintainable code. ## Context @@ -15,12 +15,7 @@ Go best practices for clean, idiomatic, maintainable code. Core principle: **Cle ## Principles -- **KISS**: As simple as possible; avoid premature abstractions and optimizations -- **DRY**: Extract shared patterns -- **YAGNI**: Don't build until needed -- **Clear > Clever**: Do not sacrifice readability for cleverness -- **Idiomatic Go**: stdlib first; don't import other languages' idioms -- Follow Uber's Go Style Guide, Google's Go Style Guide, and Effective Go +KISS, DRY, YAGNI, Clear > Clever. **Idiomatic Go**: stdlib first; don't import other languages' idioms. Follow Uber's Go Style Guide, Google's Go Style Guide, and Effective Go ## Naming diff --git a/home/.agents/skills/go-testing/SKILL.md b/home/.agents/skills/go-testing/SKILL.md index e0e10f7c..066aeec3 100644 --- a/home/.agents/skills/go-testing/SKILL.md +++ b/home/.agents/skills/go-testing/SKILL.md @@ -5,7 +5,7 @@ description: Use when writing, editing, or reviewing Go test code # Go Testing -Go testing best practices for clean, parallel, maintainable tests. Use alongside the **go-code** skill. +Use alongside the **go-code** skill. ## Context diff --git a/home/.agents/skills/risk-review/SKILL.md b/home/.agents/skills/risk-review/SKILL.md index 9af37a6c..9b5ede62 100644 --- a/home/.agents/skills/risk-review/SKILL.md +++ b/home/.agents/skills/risk-review/SKILL.md @@ -6,7 +6,6 @@ description: Use when reviewing code for bugs, correctness issues, and productio # Risk Review Review code for production safety, correctness, and cross-file impact. -Act like a veteran engineer with production scars: direct, skeptical, focused on failure modes that only show up in real traffic. ## Inputs From dfcbf25675dd03a9f293d8b34cded6bc03c2cf0c Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:39:43 +0900 Subject: [PATCH 135/366] fix(keymaps): restore ampersand input Use the numeric character reference for shift-7 so macOS emits an ampersand from the Colemak-DH layout. --- misc/keymaps/Colemak-DH-ANSI.keylayout | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/misc/keymaps/Colemak-DH-ANSI.keylayout b/misc/keymaps/Colemak-DH-ANSI.keylayout index 647bb6d2..360066a9 100644 --- a/misc/keymaps/Colemak-DH-ANSI.keylayout +++ b/misc/keymaps/Colemak-DH-ANSI.keylayout @@ -115,7 +115,7 @@ - + From ec54aba4c458f78edf7a32ec105fca89fd9aca6b Mon Sep 17 00:00:00 2001 From: ALX99 <46844683+ALX99@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:29:25 +0900 Subject: [PATCH 136/366] chore(pi-web): ts port --- pi-web/README.md | 39 - pi-web/bin/pi-web | 22 +- pi-web/docs/smoke-test.md | 59 - pi-web/package-lock.json | 1200 ++++++++++++++++- pi-web/package.json | 18 +- pi-web/src/bridge.ts | 6 + pi-web/src/protocol.ts | 116 +- pi-web/src/runtime.ts | 37 +- pi-web/src/server.ts | 34 +- pi-web/src/shared/wire.ts | 105 ++ pi-web/src/vendor.ts | 47 - pi-web/src/web/app.js | 276 ---- pi-web/src/web/app.tsx | 31 + pi-web/src/web/bridge.ts | 163 +++ pi-web/src/web/components/chat.js | 78 -- pi-web/src/web/components/chat.tsx | 64 + pi-web/src/web/components/composer.tsx | 60 + .../src/web/components/connection-status.tsx | 19 + pi-web/src/web/components/htm.js | 9 - pi-web/src/web/components/message.tsx | 100 ++ pi-web/src/web/components/model-picker.js | 25 - pi-web/src/web/components/model-picker.tsx | 87 ++ pi-web/src/web/components/session-list.js | 21 - pi-web/src/web/components/session-list.tsx | 65 + pi-web/src/web/components/sidebar.tsx | 44 + pi-web/src/web/components/thinking-picker.js | 18 - pi-web/src/web/components/thinking-picker.tsx | 26 + pi-web/src/web/components/toast.tsx | 17 + pi-web/src/web/components/tool-call.js | 13 - pi-web/src/web/components/tool-call.tsx | 32 + pi-web/src/web/index.html | 2 +- pi-web/src/web/lib/markdown.ts | 29 + pi-web/src/web/main.tsx | 5 + pi-web/src/web/state.ts | 247 ++++ pi-web/src/web/style.css | 156 ++- pi-web/tests/server.test.ts | 11 +- pi-web/tsconfig.app.json | 21 + pi-web/tsconfig.json | 3 +- pi-web/vite.config.ts | 35 + 39 files changed, 2555 insertions(+), 785 deletions(-) delete mode 100644 pi-web/README.md delete mode 100644 pi-web/docs/smoke-test.md create mode 100644 pi-web/src/shared/wire.ts delete mode 100644 pi-web/src/vendor.ts delete mode 100644 pi-web/src/web/app.js create mode 100644 pi-web/src/web/app.tsx create mode 100644 pi-web/src/web/bridge.ts delete mode 100644 pi-web/src/web/components/chat.js create mode 100644 pi-web/src/web/components/chat.tsx create mode 100644 pi-web/src/web/components/composer.tsx create mode 100644 pi-web/src/web/components/connection-status.tsx delete mode 100644 pi-web/src/web/components/htm.js create mode 100644 pi-web/src/web/components/message.tsx delete mode 100644 pi-web/src/web/components/model-picker.js create mode 100644 pi-web/src/web/components/model-picker.tsx delete mode 100644 pi-web/src/web/components/session-list.js create mode 100644 pi-web/src/web/components/session-list.tsx create mode 100644 pi-web/src/web/components/sidebar.tsx delete mode 100644 pi-web/src/web/components/thinking-picker.js create mode 100644 pi-web/src/web/components/thinking-picker.tsx create mode 100644 pi-web/src/web/components/toast.tsx delete mode 100644 pi-web/src/web/components/tool-call.js create mode 100644 pi-web/src/web/components/tool-call.tsx create mode 100644 pi-web/src/web/lib/markdown.ts create mode 100644 pi-web/src/web/main.tsx create mode 100644 pi-web/src/web/state.ts create mode 100644 pi-web/tsconfig.app.json create mode 100644 pi-web/vite.config.ts diff --git a/pi-web/README.md b/pi-web/README.md deleted file mode 100644 index c3500a24..00000000 --- a/pi-web/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# pi-web - -A localhost browser UI for [pi](https://github.com/badlogic/pi-mono), the -coding agent. Drop-in TUI replacement: same sessions, same extensions, -same skills — just a browser tab instead of a terminal. - -## Run it - -```bash -cd pi-web -npm install -npm run dev -``` - -This starts an HTTP server on `http://127.0.0.1:7878` and opens your -browser. The session lives in whatever directory you ran `npm run dev` -from (use `--cwd ` to chat about a different project). - -## What's in scope (v0.1) - -- Chat (streaming text, thinking, tool calls) -- New session, switch session, list prior sessions -- Model picker, thinking-level picker -- Abort -- Persists to the same `~/.pi/agent/sessions/` tree the TUI uses - -## What's not - -Session tree UI (fork/branch), steering/follow-up queue UI, compaction -controls, image attachments, markdown rendering. See -`docs/superpowers/specs/2026-06-21-pi-web-design.md` for the full spec. - -## Layout - -``` -src/ # server + bridge + protocol -src/web/ # static frontend (Preact + HTM, no build step) -tests/ # node:test -``` diff --git a/pi-web/bin/pi-web b/pi-web/bin/pi-web index 8c4958e0..9ae8f72f 100755 --- a/pi-web/bin/pi-web +++ b/pi-web/bin/pi-web @@ -8,14 +8,32 @@ # Env: # PI_WEB_PORT override port (default 7878) # PI_WEB=0 disable browser auto-open -# PI_WEB_ROOT override install location (default ~/dotfiles/pi-web) +# PI_WEB_ROOT override install location (default: this script's dir/..) set -euo pipefail -PI_WEB_ROOT="${PI_WEB_ROOT:-~/dotfiles/pi-web}" +# Resolve the repo root from this script's own location so the launcher +# is portable: /pi-web/bin/pi-web -> /pi-web. +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PI_WEB_ROOT="${PI_WEB_ROOT:-"$script_dir/.."}" +PI_WEB_ROOT="$(cd "$PI_WEB_ROOT" && pwd)" if [ ! -d "$PI_WEB_ROOT" ]; then echo "[pi-web] PI_WEB_ROOT not found: $PI_WEB_ROOT" >&2 exit 1 fi +# Build the frontend if dist/ is missing or stale relative to src/web. +# Vite emits the bundle to dist/; the Node server serves it from there. +dist_index="$PI_WEB_ROOT/dist/index.html" +needs_build=0 +if [ ! -f "$dist_index" ]; then + needs_build=1 +elif [ -n "$(find "$PI_WEB_ROOT/src/web" -type f -newer "$dist_index" -print -quit 2>/dev/null)" ]; then + needs_build=1 +fi +if [ "$needs_build" -eq 1 ]; then + echo "[pi-web] building frontend…" + (cd "$PI_WEB_ROOT" && npm run build) >/dev/null +fi + exec "$PI_WEB_ROOT/node_modules/.bin/tsx" "$PI_WEB_ROOT/src/server.ts" "$@" diff --git a/pi-web/docs/smoke-test.md b/pi-web/docs/smoke-test.md deleted file mode 100644 index 53774cf2..00000000 --- a/pi-web/docs/smoke-test.md +++ /dev/null @@ -1,59 +0,0 @@ -# pi-web smoke test - -Run before merging any change to `pi-web/`. - -## Setup - -```bash -cd pi-web -npm install -npm run typecheck -npm test -``` - -All three must succeed. Tests: 38 across `protocol.test.ts`, -`open-browser.test.ts`, `bridge.test.ts`, `server.test.ts`, -`runtime.test.ts` (one is skipped if no API key is available). - -## Manual end-to-end - -```bash -mkdir -p /tmp/pw-smoke -cd /tmp/pw-smoke -PI_WEB=0 /path/to/pi-web/node_modules/.bin/tsx /path/to/pi-web/src/server.ts --cwd /tmp/pw-smoke -``` - -In another terminal: - -```bash -open http://127.0.0.1:7878/ -``` - -Verify: - -1. Page loads. Sidebar shows Session, Model, Thinking sections. -2. The Model picker is empty if no API key is configured; if a key - exists, the picker lists available models. -3. Type a message and press Enter. The user message appears; an - assistant response streams in (if a key is configured). If no key, - an error toast appears with the SDK's error message. -4. Click `+ New session`. The chat clears. A new session entry - appears in the session list. -5. Click a prior session in the list. The chat shows that session's - messages. -6. Change the model in the picker. The next prompt uses the new - model. -7. Change the thinking level. The next prompt uses the new level. -8. Send a long prompt; press Abort. The "Abort" button works, the - partial response is discarded. -9. Close the browser tab. The server still runs. -10. Press Ctrl-C in the server terminal. Clean exit. - -## What is not tested in v0.1 - -- Streaming deltas rendering (covered in `bridge.test.ts` only at the - event-forwarding level; visual verification is manual). -- Multiple browser tabs in sync. -- Session tree (forks, branches) — not in v0.1. -- Image attachments — not in v0.1. -- Markdown rendering — text is shown as-is. diff --git a/pi-web/package-lock.json b/pi-web/package-lock.json index e9c9ce67..4cd9454f 100644 --- a/pi-web/package-lock.json +++ b/pi-web/package-lock.json @@ -8,17 +8,20 @@ "name": "pi-web", "version": "0.1.0", "dependencies": { - "@earendil-works/pi-ai": "*", - "@earendil-works/pi-coding-agent": "*", - "htm": "^3.1.1", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-coding-agent": "^0.80.2", + "dompurify": "^3.4.11", + "marked": "^18.0.5", "preact": "^10.22.0", "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^22.7.0", "@types/ws": "^8.5.12", + "concurrently": "^10.0.3", "tsx": "^4.19.0", - "typescript": "^5.6.0" + "typescript": "^5.6.0", + "vite": "^8.1.0" }, "engines": { "node": ">=20" @@ -503,9 +506,9 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", - "integrity": "sha512-fHmgNMONwCCE7bQAKbcz76sgm3iQuA7km1mpIc4H5xXd9+zhPh/faULz6ARkgjQE0EufHnfZPJY39+lNf8Sa9g==", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", + "integrity": "sha512-5GNKfdrRJ4uZ5Zd9iudoXggi/BbUcKnD/xfRHtdR+7q4vWqPvfx8auFuaT+ewGBVI8K4wj87eigFQ/iCSuy9RQ==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -528,15 +531,15 @@ } }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.9.tgz", - "integrity": "sha512-8TZ796Zn0NE4vmhxG9hv4ZtJDGJzhqMjlmFg8ZkUKxfqB7LJa4ums2jSJKtnyAZfAamN6VzqzN0A82RNDqv8Ag==", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.80.2.tgz", + "integrity": "sha512-m9v7OUit0s9LklWfh61ca/XY5INjUzjtYtNZwy3cNvyjOLk3IpBgghP8aAp0iH35rLaiRwuuWiJ8t88ODMWY+A==", "hasShrinkwrap": true, "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.79.9", - "@earendil-works/pi-ai": "^0.79.9", - "@earendil-works/pi-tui": "^0.79.9", + "@earendil-works/pi-agent-core": "^0.80.2", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-tui": "^0.80.2", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", @@ -999,11 +1002,11 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.9.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.80.2.tgz", "license": "MIT", "dependencies": { - "@earendil-works/pi-ai": "^0.79.9", + "@earendil-works/pi-ai": "^0.80.2", "ignore": "7.0.5", "typebox": "1.1.38", "yaml": "2.9.0" @@ -1013,8 +1016,8 @@ } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.9.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.80.2.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.91.1", @@ -1030,15 +1033,15 @@ "typebox": "1.1.38" }, "bin": { - "pi-ai": "dist/cli.js" + "pi-ai": "./dist/cli.js" }, "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { - "version": "0.79.9", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.9.tgz", + "version": "0.80.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.2.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -2360,6 +2363,40 @@ "zod": "^3.25.28 || ^4" } }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", @@ -2846,6 +2883,25 @@ } } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@nodable/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", @@ -2876,6 +2932,16 @@ "node": ">=14" } }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2933,6 +2999,288 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@smithy/core": { "version": "3.25.1", "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.25.1.tgz", @@ -3053,6 +3401,17 @@ "node": ">=14.0.0" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/node": { "version": "22.20.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", @@ -3068,7 +3427,14 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, - "node_modules/@types/ws": { + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", @@ -3087,6 +3453,32 @@ "node": ">= 14" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/anynum": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", @@ -3140,6 +3532,59 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/concurrently": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz", + "integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.4", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3166,6 +3611,25 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3175,6 +3639,13 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -3217,6 +3688,16 @@ "@esbuild/win32-x64": "0.28.1" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3260,6 +3741,24 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -3338,6 +3837,29 @@ "node": ">=18" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/google-auth-library": { "version": "10.7.0", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", @@ -3364,12 +3886,6 @@ "node": ">=14" } }, - "node_modules/htm": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", - "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", - "license": "Apache-2.0" - }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3439,18 +3955,322 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -3544,6 +4364,55 @@ "node": ">=14.0.0" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/preact": { "version": "10.29.2", "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", @@ -3586,6 +4455,50 @@ "node": ">= 4" } }, + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -3606,6 +4519,63 @@ ], "license": "MIT" }, + "node_modules/shell-quote": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", + "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strnum": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", @@ -3621,6 +4591,46 @@ "anynum": "^1.0.1" } }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -3678,6 +4688,84 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/vite": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -3687,6 +4775,24 @@ "node": ">= 8" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -3723,6 +4829,44 @@ "node": ">=16.0.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", diff --git a/pi-web/package.json b/pi-web/package.json index 4792c24a..a49ee317 100644 --- a/pi-web/package.json +++ b/pi-web/package.json @@ -4,23 +4,27 @@ "private": true, "type": "module", "scripts": { - "dev": "tsx watch src/server.ts", - "start": "tsx src/server.ts", - "typecheck": "tsc --noEmit", + "dev": "concurrently -k -n web,api \"vite\" \"PI_WEB=0 tsx watch src/server.ts\"", + "start": "vite build && tsx src/server.ts", + "build": "vite build", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.app.json --noEmit", "test": "node --test 'tests/*.test.ts'" }, "dependencies": { - "@earendil-works/pi-coding-agent": "*", - "@earendil-works/pi-ai": "*", + "@earendil-works/pi-ai": "^0.80.2", + "@earendil-works/pi-coding-agent": "^0.80.2", + "dompurify": "^3.4.11", + "marked": "^18.0.5", "preact": "^10.22.0", - "htm": "^3.1.1", "ws": "^8.18.0" }, "devDependencies": { "@types/node": "^22.7.0", "@types/ws": "^8.5.12", + "concurrently": "^10.0.3", "tsx": "^4.19.0", - "typescript": "^5.6.0" + "typescript": "^5.6.0", + "vite": "^8.1.0" }, "engines": { "node": ">=20" diff --git a/pi-web/src/bridge.ts b/pi-web/src/bridge.ts index d5ff7b89..42df668d 100644 --- a/pi-web/src/bridge.ts +++ b/pi-web/src/bridge.ts @@ -23,6 +23,7 @@ export interface BridgeSession { setModel(model: Model): Promise; setThinkingLevel(level: ModelThinkingLevel): void; readonly messages: unknown[]; + readonly sessionFile: string | undefined; readonly agent: { readonly state: { readonly model: Model | undefined; @@ -37,6 +38,7 @@ export interface SessionListEntry { path: string; id: string; name?: string; + firstMessage?: string; startedAt?: number; } @@ -48,6 +50,8 @@ export interface BridgeRuntime { listSessions(): Promise; /** List models the registry can use. */ getAvailableModels(): Promise[]>; + /** Track the user's model choice so new sessions inherit it. */ + setCurrentModel?(model: Model): void; } export type ClientSender = (data: string) => void; @@ -179,6 +183,7 @@ export class Bridge { return; } await session.setModel(target); + this.runtime.setCurrentModel?.(target); this.respond(replyTo, { type: "response", id: cmd.id, ok: true }); return; } @@ -203,6 +208,7 @@ export class Bridge { isStreaming: s.isStreaming, messageCount: s.messageCount, messages: session.messages, + sessionFile: session.sessionFile, }, }); return; diff --git a/pi-web/src/protocol.ts b/pi-web/src/protocol.ts index 3f8c388e..19f7936a 100644 --- a/pi-web/src/protocol.ts +++ b/pi-web/src/protocol.ts @@ -1,70 +1,20 @@ -// Wire protocol for pi-web. -// -// Framing: WebSocket text frames, one JSON message per frame. -// Discriminated by `type`. Client commands have required `id` (uuid v4 -// string). Server responses match `id`. Server events have no `id` and -// pass through the SDK's AgentSessionEvent union as-is. +import { isServerEventType, isThinkingLevel } from "./shared/wire.ts"; +import type { + ClientCommand, + ImageContent, + ServerEvent, + ServerResponse, +} from "./shared/wire.ts"; -import type { ThinkingLevel, ModelThinkingLevel, ImageContent } from "@earendil-works/pi-ai"; - -/* ----- Client commands (browser -> server) ----- */ - -export type ClientCommand = - | { type: "prompt"; id: string; text: string; images?: ImageContent[] } - | { type: "abort"; id: string } - | { type: "new_session"; id: string } - | { type: "switch_session"; id: string; path: string } - | { type: "set_model"; id: string; provider: string; modelId: string } - | { type: "set_thinking_level"; id: string; level: ModelThinkingLevel } - | { type: "list_sessions"; id: string } - | { type: "list_models"; id: string } - | { type: "get_state"; id: string }; - -/* ----- Server responses (server -> browser, matched by id) ----- */ - -export type ServerResponse = - | { type: "response"; id: string; ok: true; data?: unknown } - | { type: "response"; id: string; ok: false; error: string }; - -/* ----- Server events (server -> browser, push only) ----- */ - -export type ServerEvent = { - type: - | "session_start" - | "session_before_switch" - | "session_before_fork" - | "session_before_compact" - | "agent_start" - | "agent_end" - | "turn_start" - | "turn_end" - | "message_start" - | "message_update" - | "message_end" - | "tool_execution_start" - | "tool_execution_update" - | "tool_execution_end" - | "queue_update" - | "compaction_start" - | "compaction_end" - | "extension_error"; - [k: string]: unknown; -}; - -/* ----- Parsing + type guards ----- */ - -const THINKING_LEVELS: ReadonlySet = new Set([ - "off", - "minimal", - "low", - "medium", - "high", - "xhigh", -]); - -function isThinkingLevel(v: string): v is ModelThinkingLevel { - return THINKING_LEVELS.has(v); -} +export type { + ClientCommand, + ImageContent, + ServerEvent, + ServerResponse, + ServerEventType, + SessionSummary, + ThinkingLevel, +} from "./shared/wire.ts"; function isObject(v: unknown): v is Record { return typeof v === "object" && v !== null && !Array.isArray(v); @@ -85,9 +35,8 @@ export function parseClientCommand(raw: unknown): ClientCommand | null { if (!isNonEmptyString(text)) return null; if (images !== undefined && !Array.isArray(images)) return null; const cmd: ClientCommand = { type: "prompt", id, text }; - if (images !== undefined) { - (cmd as { images?: ImageContent[] }).images = - images as ImageContent[]; + if (Array.isArray(images)) { + cmd.images = images as ImageContent[]; } return cmd; } @@ -109,9 +58,7 @@ export function parseClientCommand(raw: unknown): ClientCommand | null { } case "set_thinking_level": { const { level } = raw; - if (!isNonEmptyString(level) || !isThinkingLevel(level)) { - return null; - } + if (!isThinkingLevel(level)) return null; return { type: "set_thinking_level", id, level }; } case "list_sessions": @@ -126,28 +73,5 @@ export function parseClientCommand(raw: unknown): ClientCommand | null { } export function isServerEvent(value: unknown): value is ServerEvent { - if (!isObject(value)) return false; - const { type } = value; - if (!isNonEmptyString(type)) return false; - const allowed: ReadonlySet = new Set([ - "session_start", - "session_before_switch", - "session_before_fork", - "session_before_compact", - "agent_start", - "agent_end", - "turn_start", - "turn_end", - "message_start", - "message_update", - "message_end", - "tool_execution_start", - "tool_execution_update", - "tool_execution_end", - "queue_update", - "compaction_start", - "compaction_end", - "extension_error", - ]); - return allowed.has(type); + return isObject(value) && isServerEventType(value.type); } diff --git a/pi-web/src/runtime.ts b/pi-web/src/runtime.ts index 0b473eab..968b680c 100644 --- a/pi-web/src/runtime.ts +++ b/pi-web/src/runtime.ts @@ -16,6 +16,7 @@ import { SettingsManager, DefaultResourceLoader, type CreateAgentSessionRuntimeFactory, + type CreateAgentSessionFromServicesOptions, } from "@earendil-works/pi-coding-agent"; import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all"; import type { Model } from "@earendil-works/pi-ai"; @@ -60,26 +61,24 @@ export async function createRealRuntime(opts: { const sessionManager = SessionManager.create(opts.cwd); + let currentModel = model; + const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager: sm, sessionStartEvent, }) => { const services = await createAgentSessionServices({ cwd }); - const fromServicesOpts: Record = { + const opts: CreateAgentSessionFromServicesOptions = { services, sessionManager: sm, thinkingLevel: "medium", + ...(sessionStartEvent !== undefined + ? { sessionStartEvent } + : {}), + ...(currentModel ? { model: currentModel } : {}), }; - if (sessionStartEvent !== undefined) { - fromServicesOpts.sessionStartEvent = sessionStartEvent; - } - if (model) { - fromServicesOpts.model = model; - } - const result = await createAgentSessionFromServices( - fromServicesOpts as unknown as Parameters[0], - ); + const result = await createAgentSessionFromServices(opts); return { ...result, services, diagnostics: services.diagnostics }; }; @@ -89,10 +88,13 @@ export async function createRealRuntime(opts: { sessionManager, }); - const session = runtime.session as unknown as BridgeSession; - return { - session, + get session() { + return runtime.session as unknown as BridgeSession; + }, + setCurrentModel(m: Model): void { + currentModel = m; + }, async newSession() { return runtime.newSession(); }, @@ -101,21 +103,22 @@ export async function createRealRuntime(opts: { }, async listSessions() { const list = await SessionManager.list(opts.cwd); - return list.map((entry) => { + const entries = list.map((entry) => { const e: SessionListEntry = { path: entry.path, id: entry.id }; if (entry.name !== undefined) e.name = entry.name; + e.firstMessage = entry.firstMessage; e.startedAt = entry.modified.getTime(); return e; }); + entries.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0)); + return entries; }, async getAvailableModels(): Promise[]> { const all = await modelRegistry.getAvailable(); return all; }, async dispose() { - // The SDK doesn't expose a clean shutdown for the runtime; for v1 - // we let the process exit handle it. The bridge's dispose() will - // unsubscribe from the session. + await runtime.dispose(); }, }; } diff --git a/pi-web/src/server.ts b/pi-web/src/server.ts index cc9b363e..a0d04eb4 100644 --- a/pi-web/src/server.ts +++ b/pi-web/src/server.ts @@ -1,7 +1,7 @@ // HTTP + WebSocket server. // -// Static files: GET / from webRoot (default pi-web/src/web). -// Vendor: GET /vendor/ resolves to node_modules file. +// Static files: GET / from webRoot (default pi-web/dist, the +// Vite build output). // WebSocket: WS /ws forwards raw frames to the bridge. import { @@ -15,19 +15,16 @@ import { extname, join, normalize, resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { WebSocketServer, type WebSocket } from "ws"; import { Bridge, type BridgeRuntime, type ClientSender } from "./bridge.ts"; -import { resolveVendorFile } from "./vendor.ts"; import { createRealRuntime, type RealRuntime } from "./runtime.ts"; import { openBrowser } from "./open-browser.ts"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const DEFAULT_WEB_ROOT = resolve(__dirname, "web"); +const DEFAULT_WEB_ROOT = resolve(__dirname, "..", "dist"); export interface ServerDeps { runtime: BridgeRuntime; webRoot: string; - /** Map of additional static routes, e.g. { "/vendor/preact.js": "" }. */ - vendorRoutes?: Record; } export interface ServerHandle { @@ -56,17 +53,6 @@ function isLoopback(host: string): boolean { ); } -function defaultVendorRoutes(): Record { - try { - return { - "/vendor/preact.js": resolveVendorFile("preact"), - "/vendor/htm.js": resolveVendorFile("htm"), - }; - } catch { - return {}; - } -} - export function startServer(opts: { port: number; host: string; @@ -77,14 +63,14 @@ export function startServer(opts: { `Refusing to bind to non-loopback host "${opts.host}". Use 127.0.0.1 or ::1.`, ); } - const { runtime, webRoot, vendorRoutes = defaultVendorRoutes() } = opts.ctx; + const { runtime, webRoot } = opts.ctx; const bridge = new Bridge({ runtime }); const wss = new WebSocketServer({ noServer: true }); const server = createServer(async (req, res) => { try { - await handleHttp(req, res, webRoot, vendorRoutes); + await handleHttp(req, res, webRoot); } catch (err) { res.statusCode = 500; res.end(`Internal error: ${(err as Error).message}`); @@ -146,20 +132,10 @@ async function handleHttp( req: IncomingMessage, res: ServerResponse, webRoot: string, - vendorRoutes: Record, ): Promise { const url = new URL(req.url ?? "/", "http://localhost"); const pathname = decodeURIComponent(url.pathname); - if (pathname in vendorRoutes) { - const file = vendorRoutes[pathname]!; - const buf = await readFile(file); - res.statusCode = 200; - res.setHeader("Content-Type", "application/javascript; charset=utf-8"); - res.end(buf); - return; - } - const rel = pathname === "/" ? "/index.html" : pathname; const full = normalize(join(webRoot, rel)); if (!full.startsWith(resolve(webRoot))) { diff --git a/pi-web/src/shared/wire.ts b/pi-web/src/shared/wire.ts new file mode 100644 index 00000000..1f1e67cd --- /dev/null +++ b/pi-web/src/shared/wire.ts @@ -0,0 +1,105 @@ +export type ThinkingLevel = + | "off" + | "minimal" + | "low" + | "medium" + | "high" + | "xhigh"; + +export const THINKING_LEVELS: readonly ThinkingLevel[] = [ + "off", + "minimal", + "low", + "medium", + "high", + "xhigh", +]; + +export interface ImageContent { + type: "image"; + data: string; + mediaType: string; +} + +export interface ModelRef { + provider: string; + id: string; + name?: string; +} + +export interface SessionSummary { + path: string; + id: string; + name?: string; + firstMessage?: string; + startedAt?: number; +} + +export type ClientCommand = + | { type: "prompt"; id: string; text: string; images?: ImageContent[] } + | { type: "abort"; id: string } + | { type: "new_session"; id: string } + | { type: "switch_session"; id: string; path: string } + | { type: "set_model"; id: string; provider: string; modelId: string } + | { type: "set_thinking_level"; id: string; level: ThinkingLevel } + | { type: "list_sessions"; id: string } + | { type: "list_models"; id: string } + | { type: "get_state"; id: string }; + +export type ServerResponse = + | { type: "response"; id: string; ok: true; data?: unknown } + | { type: "response"; id: string; ok: false; error: string }; + +export type ServerEventType = + | "session_start" + | "session_before_switch" + | "session_before_fork" + | "session_before_compact" + | "agent_start" + | "agent_end" + | "turn_start" + | "turn_end" + | "message_start" + | "message_update" + | "message_end" + | "tool_execution_start" + | "tool_execution_update" + | "tool_execution_end" + | "queue_update" + | "compaction_start" + | "compaction_end" + | "extension_error"; + +export const SERVER_EVENT_TYPES: ReadonlySet = new Set([ + "session_start", + "session_before_switch", + "session_before_fork", + "session_before_compact", + "agent_start", + "agent_end", + "turn_start", + "turn_end", + "message_start", + "message_update", + "message_end", + "tool_execution_start", + "tool_execution_update", + "tool_execution_end", + "queue_update", + "compaction_start", + "compaction_end", + "extension_error", +]); + +export interface ServerEvent { + type: ServerEventType; + [k: string]: unknown; +} + +export function isServerEventType(value: unknown): value is ServerEventType { + return typeof value === "string" && SERVER_EVENT_TYPES.has(value as ServerEventType); +} + +export function isThinkingLevel(value: unknown): value is ThinkingLevel { + return typeof value === "string" && THINKING_LEVELS.includes(value as ThinkingLevel); +} diff --git a/pi-web/src/vendor.ts b/pi-web/src/vendor.ts deleted file mode 100644 index 5c346297..00000000 --- a/pi-web/src/vendor.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Resolve the on-disk paths of vendored browser modules. -// -// The server serves preact + htm from `pi-web/node_modules` via -// /vendor/* routes. The preact and htm packages have an `exports` -// field that blocks `require.resolve("preact/dist/preact.module.js")`. -// We work around this by resolving the package root (which is allowed), -// then walking the known layout: preact's browser file is -// `/dist/preact.module.js`; htm's browser file is -// `/dist/htm.module.js`. -// -// Note: we intentionally do NOT vendor `htm/preact`. That module does -// `import "preact"` (bare specifier), which the browser cannot resolve -// without an import map. Instead, components import htm and bind -// `html` themselves (see src/web/components/htm.js). - -import { createRequire } from "node:module"; -import { existsSync } from "node:fs"; -import { dirname, join } from "node:path"; - -const require = createRequire(import.meta.url); - -type VendorName = "preact" | "htm"; - -function getPkgDir(pkg: string): string { - // require.resolve on the package root resolves to the entry point, - // which is in /dist/. dirname^2 is the package root. - const entry = require.resolve(pkg); - return dirname(dirname(entry)); -} - -const CANDIDATES: Record = { - preact: ["dist/preact.module.js", "dist/preact.mjs", "dist/preact.min.module.js"], - htm: ["dist/htm.module.js", "dist/htm.mjs"], -}; - -export function resolveVendorFile(name: VendorName): string { - const pkgDir = getPkgDir(name); - const errors: string[] = []; - for (const rel of CANDIDATES[name]) { - const p = join(pkgDir, rel); - if (existsSync(p)) return p; - errors.push(`not found: ${p}`); - } - throw new Error( - `Cannot resolve vendor file "${name}":\n ${errors.join("\n ")}`, - ); -} diff --git a/pi-web/src/web/app.js b/pi-web/src/web/app.js deleted file mode 100644 index 88c123d9..00000000 --- a/pi-web/src/web/app.js +++ /dev/null @@ -1,276 +0,0 @@ -import { h, render } from "/vendor/preact.js"; -import { html } from "./components/htm.js"; -import { Chat } from "./components/chat.js"; -import { SessionList } from "./components/session-list.js"; -import { ModelPicker } from "./components/model-picker.js"; -import { ThinkingPicker } from "./components/thinking-picker.js"; - -const WS_URL = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`; - -const state = { - connected: false, - messages: [], - sessions: [], - models: [], - model: null, - thinkingLevel: "medium", - isStreaming: false, - toast: null, - ws: null, - reconnectTimer: null, - eventSeq: 0, -}; - -function renderApp() { - render( - html` - -
- <${Chat} - messages=${state.messages} - isStreaming=${state.isStreaming} - seq=${state.eventSeq} - /> -
-
- - - -
- ${state.toast ? html`
${state.toast.text}
` : null} - `, - document.getElementById("app"), - ); -} - -function showToast(text, kind = "info", ms = 3000) { - state.toast = { text, kind }; - renderApp(); - setTimeout(() => { - if (state.toast && state.toast.text === text) { - state.toast = null; - renderApp(); - } - }, ms); -} - -function sendCommand(cmd) { - if (!state.ws || state.ws.readyState !== WebSocket.OPEN) { - showToast("Not connected", "info"); - return; - } - state.ws.send(JSON.stringify(cmd)); -} - -function onNewSession() { - sendCommand({ type: "new_session", id: crypto.randomUUID() }); -} - -function onSwitchSession(path) { - sendCommand({ type: "switch_session", id: crypto.randomUUID(), path }); -} - -function onSetModel(m) { - sendCommand({ - type: "set_model", - id: crypto.randomUUID(), - provider: m.provider, - modelId: m.id, - }); -} - -function onSetThinking(level) { - sendCommand({ - type: "set_thinking_level", - id: crypto.randomUUID(), - level, - }); -} - -function onSendPrompt() { - const el = document.getElementById("prompt"); - if (!el) return; - const text = el.value; - if (!text.trim()) return; - sendCommand({ type: "prompt", id: crypto.randomUUID(), text }); - el.value = ""; - // No optimistic UI: the server's message_start event for the user - // message will populate state.messages. -} - -function onAbort() { - sendCommand({ type: "abort", id: crypto.randomUUID() }); -} - -function onPromptKey(e) { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSendPrompt(); - } -} - -/* ----- WebSocket lifecycle ----- */ - -function connect() { - const ws = new WebSocket(WS_URL); - state.ws = ws; - ws.addEventListener("open", () => { - state.connected = true; - state.reconnectTimer = null; - sendCommand({ type: "get_state", id: crypto.randomUUID() }); - sendCommand({ type: "list_sessions", id: crypto.randomUUID() }); - sendCommand({ type: "list_models", id: crypto.randomUUID() }); - renderApp(); - }); - ws.addEventListener("close", () => { - state.connected = false; - state.isStreaming = false; - renderApp(); - scheduleReconnect(); - }); - ws.addEventListener("error", () => { - ws.close(); - }); - ws.addEventListener("message", (e) => onMessage(e.data)); -} - -function scheduleReconnect() { - if (state.reconnectTimer) return; - state.reconnectTimer = setTimeout(connect, 1000); -} - -function onMessage(raw) { - let msg; - try { - msg = JSON.parse(raw); - } catch { - return; - } - if (msg.type === "response") { - onResponse(msg); - } else if (isEventType(msg.type)) { - onEvent(msg); - } -} - -function onResponse(msg) { - if (!msg.ok) { - showToast(msg.error ?? "Error", "info"); - return; - } - const data = msg.data; - if (data && Array.isArray(data.sessions)) { - state.sessions = data.sessions; - renderApp(); - return; - } - if (data && Array.isArray(data.models)) { - state.models = data.models; - renderApp(); - return; - } - if (data && "thinkingLevel" in data) { - state.model = data.model; - state.thinkingLevel = data.thinkingLevel; - state.isStreaming = data.isStreaming; - state.messages = data.messages ?? []; - renderApp(); - return; - } - // Successful new_session / switch_session / set_model / - // set_thinking_level — re-fetch get_state so the UI reflects the - // change without waiting for the next event. - if (data === undefined) { - sendCommand({ type: "get_state", id: crypto.randomUUID() }); - } -} - -function onEvent(msg) { - state.eventSeq++; - switch (msg.type) { - case "message_start": - upsertMessage(msg.message); - break; - case "message_update": { - upsertMessage(msg.message); - break; - } - case "message_end": - upsertMessage(msg.message); - break; - case "agent_start": - state.isStreaming = true; - break; - case "agent_end": - state.isStreaming = false; - break; - case "session_start": - sendCommand({ type: "list_sessions", id: crypto.randomUUID() }); - break; - default: - break; - } - renderApp(); -} - -/** Replace a message by id; append if not present. */ -function upsertMessage(m) { - if (!m || m.id === undefined) return; - const idx = state.messages.findIndex((x) => x && x.id === m.id); - if (idx === -1) { - state.messages = [...state.messages, m]; - } else { - const next = state.messages.slice(); - next[idx] = m; - state.messages = next; - } -} - -function isEventType(t) { - return [ - "session_start", - "session_before_switch", - "session_before_fork", - "session_before_compact", - "agent_start", - "agent_end", - "turn_start", - "turn_end", - "message_start", - "message_update", - "message_end", - "tool_execution_start", - "tool_execution_update", - "tool_execution_end", - "queue_update", - "compaction_start", - "compaction_end", - "extension_error", - ].includes(t); -} - -connect(); -renderApp(); diff --git a/pi-web/src/web/app.tsx b/pi-web/src/web/app.tsx new file mode 100644 index 00000000..6013d4e8 --- /dev/null +++ b/pi-web/src/web/app.tsx @@ -0,0 +1,31 @@ +import { + useBridge, +} from "./bridge.ts"; +import { Sidebar } from "./components/sidebar.tsx"; +import { Chat } from "./components/chat.tsx"; +import { Composer } from "./components/composer.tsx"; +import { Toast } from "./components/toast.tsx"; + +export function App() { + const { state, actions } = useBridge(); + return ( + <> + + + + {state.toast ? ( + + ) : null} + + ); +} diff --git a/pi-web/src/web/bridge.ts b/pi-web/src/web/bridge.ts new file mode 100644 index 00000000..9e8d74a7 --- /dev/null +++ b/pi-web/src/web/bridge.ts @@ -0,0 +1,163 @@ +import { useCallback, useEffect, useMemo, useReducer, useRef } from "preact/hooks"; +import { isServerEventType } from "../shared/wire.ts"; +import type { + ClientCommand, + ModelRef, + ServerEvent, + ServerResponse, + ThinkingLevel, +} from "../shared/wire.ts"; +import { initialState, reducer, type AppState } from "./state.ts"; + +const WS_URL = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/ws`; + +function uid(): string { + return crypto.randomUUID(); +} + +function isResponse(v: unknown): v is ServerResponse { + if (!isObject(v)) return false; + return v.type === "response" && typeof v.ok === "boolean"; +} + +function isObject(v: unknown): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v); +} + +export interface BridgeActions { + sendPrompt(text: string): void; + abort(): void; + newSession(): void; + switchSession(path: string): void; + setModel(m: ModelRef): void; + setThinking(level: ThinkingLevel): void; + setModelSearch(value: string): void; + setStickToBottom(value: boolean): void; + dismissToast(id: number): void; +} + +export function useBridge(): { state: AppState; actions: BridgeActions } { + const [state, dispatch] = useReducer(reducer, initialState); + const wsRef = useRef(null); + + const send = useCallback((cmd: ClientCommand): void => { + const ws = wsRef.current; + if (!ws || ws.readyState !== WebSocket.OPEN) { + dispatch({ type: "show_toast", text: "Not connected", kind: "info" }); + return; + } + ws.send(JSON.stringify(cmd)); + }, []); + + useEffect(() => { + let reconnectTimer: ReturnType | null = null; + let attempt = 0; + + function scheduleReconnect(): void { + if (reconnectTimer) return; + attempt += 1; + const delay = Math.min(1000 * 2 ** (attempt - 1), 30000); + dispatch({ type: "reconnect_attempt", attempt }); + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + connect(); + }, delay); + } + + function connect(): void { + const ws = new WebSocket(WS_URL); + wsRef.current = ws; + ws.addEventListener("open", () => { + attempt = 0; + dispatch({ type: "ws_open" }); + send({ type: "get_state", id: uid() }); + send({ type: "list_sessions", id: uid() }); + send({ type: "list_models", id: uid() }); + }); + ws.addEventListener("close", () => { + dispatch({ type: "ws_close" }); + scheduleReconnect(); + }); + ws.addEventListener("error", () => { + ws.close(); + }); + ws.addEventListener("message", (e: MessageEvent) => { + const raw = typeof e.data === "string" ? e.data : ""; + let msg: unknown; + try { + msg = JSON.parse(raw); + } catch { + return; + } + if (isResponse(msg)) { + dispatch({ type: "response", msg }); + if (msg.ok && msg.data === undefined) { + send({ type: "get_state", id: uid() }); + send({ type: "list_sessions", id: uid() }); + } + return; + } + if (isObject(msg) && isServerEventType(msg.type)) { + const ev = msg as ServerEvent; + dispatch({ type: "event", msg: ev }); + if (ev.type === "session_start") { + send({ type: "list_sessions", id: uid() }); + } + } + }); + } + + connect(); + + return () => { + if (reconnectTimer) clearTimeout(reconnectTimer); + const ws = wsRef.current; + if (ws) ws.close(); + wsRef.current = null; + }; + }, [send]); + + const actions = useMemo( + () => ({ + sendPrompt(text: string): void { + if (!text.trim()) return; + dispatch({ type: "set_stick_to_bottom", value: true }); + send({ type: "prompt", id: uid(), text }); + }, + abort(): void { + send({ type: "abort", id: uid() }); + }, + newSession(): void { + dispatch({ type: "set_stick_to_bottom", value: true }); + send({ type: "new_session", id: uid() }); + }, + switchSession(path: string): void { + dispatch({ type: "set_stick_to_bottom", value: true }); + send({ type: "switch_session", id: uid(), path }); + }, + setModel(m: ModelRef): void { + send({ + type: "set_model", + id: uid(), + provider: m.provider, + modelId: m.id, + }); + }, + setThinking(level: ThinkingLevel): void { + send({ type: "set_thinking_level", id: uid(), level }); + }, + setModelSearch(value: string): void { + dispatch({ type: "set_model_search", value }); + }, + setStickToBottom(value: boolean): void { + dispatch({ type: "set_stick_to_bottom", value }); + }, + dismissToast(id: number): void { + dispatch({ type: "dismiss_toast", id }); + }, + }), + [send], + ); + + return { state, actions }; +} diff --git a/pi-web/src/web/components/chat.js b/pi-web/src/web/components/chat.js deleted file mode 100644 index ecaa5f16..00000000 --- a/pi-web/src/web/components/chat.js +++ /dev/null @@ -1,78 +0,0 @@ -import { h } from "/vendor/preact.js"; -import { html } from "./htm.js"; -import { ToolCall } from "./tool-call.js"; - -export function Chat({ messages, isStreaming }) { - return html` -
- ${messages.map((m, i) => renderMessage(m, i))} - ${isStreaming ? html`
assistant
` : null} -
- `; -} - -function renderMessage(m, key) { - if (!m) return null; - if (m.role === "user") { - return html` -
-
user
-
${typeof m.content === "string" ? m.content : renderBlocks(m.content)}
-
- `; - } - if (m.role === "assistant") { - return html` -
-
assistant
- ${renderAssistant(m)} -
- `; - } - if (m.role === "toolResult") { - return html` -
-
tool · ${m.toolName}
-
${(m.content ?? []).map((c) => c.text ?? "").join("\n")}
-
- `; - } - return null; -} - -function renderAssistant(m) { - if (typeof m.content === "string") { - return html`
${m.content}
${renderAssistantError(m)}`; - } - if (!Array.isArray(m.content)) return null; - const blocks = m.content.map((block, i) => { - if (block.type === "text") { - return html`
${block.text}
`; - } - if (block.type === "thinking") { - return html`
${block.thinking}
`; - } - if (block.type === "toolCall") { - return html`<${ToolCall} key=${i} call=${block} />`; - } - return null; - }); - return html`
${blocks}${renderAssistantError(m)}
`; -} - -function renderAssistantError(m) { - if (m.stopReason === "error" && m.errorMessage) { - return html`
${m.errorMessage}
`; - } - if (m.stopReason === "aborted") { - return html`
aborted
`; - } - return null; -} - -function renderBlocks(blocks) { - if (!Array.isArray(blocks)) return ""; - return blocks - .map((b) => (b.type === "text" ? b.text : "")) - .join("\n"); -} diff --git a/pi-web/src/web/components/chat.tsx b/pi-web/src/web/components/chat.tsx new file mode 100644 index 00000000..9b19e90e --- /dev/null +++ b/pi-web/src/web/components/chat.tsx @@ -0,0 +1,64 @@ +import { useEffect, useRef } from "preact/hooks"; +import type { ChatMessage } from "../state.ts"; +import { Message } from "./message.tsx"; + +export function Chat({ + messages, + isStreaming, + stickToBottom, + onStickChange, +}: { + messages: ChatMessage[]; + isStreaming: boolean; + stickToBottom: boolean; + onStickChange: (v: boolean) => void; +}) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (el && stickToBottom) el.scrollTop = el.scrollHeight; + }, [stickToBottom, messages, isStreaming]); + + const onScroll = () => { + const el = ref.current; + if (!el) return; + const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; + if (nearBottom !== stickToBottom) onStickChange(nearBottom); + }; + + const toBottom = () => { + const el = ref.current; + if (el) el.scrollTop = el.scrollHeight; + onStickChange(true); + }; + + return ( +
+ {messages.length === 0 && !isStreaming ? ( +
Send a message to start the conversation.
+ ) : ( + <> + {messages.map((m, i) => ( + + ))} + {isStreaming ? ( +
+ assistant + + + + + +
+ ) : null} + + )} + {!stickToBottom ? ( + + ) : null} +
+ ); +} diff --git a/pi-web/src/web/components/composer.tsx b/pi-web/src/web/components/composer.tsx new file mode 100644 index 00000000..77132c74 --- /dev/null +++ b/pi-web/src/web/components/composer.tsx @@ -0,0 +1,60 @@ +import { useEffect, useRef } from "preact/hooks"; + +export function Composer({ + connected, + isStreaming, + onSend, + onAbort, +}: { + connected: boolean; + isStreaming: boolean; + onSend: (text: string) => void; + onAbort: () => void; +}) { + const ref = useRef(null); + + const autogrow = () => { + const el = ref.current; + if (!el) return; + el.style.height = "auto"; + el.style.height = Math.min(el.scrollHeight, 200) + "px"; + }; + + const send = () => { + const el = ref.current; + if (!el) return; + const text = el.value; + if (!text.trim()) return; + onSend(text); + el.value = ""; + autogrow(); + }; + + useEffect(() => { + if (connected) ref.current?.focus(); + }, [connected]); + + return ( +
+