Merge apkovl-source: the actual apkovl deployment tree used to build adaptiveos-v6.iso
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
export FX_SOURCES_DIR=/root/shell-project/contrib/fx/sources.d
|
||||
. /root/shell-project/src/f.sh
|
||||
. /root/shell-project/contrib/fx/fx.sh
|
||||
. /root/shell-project/contrib/heal/heal.sh
|
||||
cd /root/shell-project 2>/dev/null
|
||||
|
||||
# --- Persistent learning state -------------------------------------------
|
||||
# f()/fx() key learned fixes in a file called "p" in the current directory.
|
||||
# On a plain live-CD boot that file lives on tmpfs and every reboot forgets
|
||||
# everything the shell has ever learned.
|
||||
#
|
||||
# If a second, writable disk is attached (anything that isn't the boot
|
||||
# CD), use it as a raw flat store for "p" -- no filesystem, no mkfs. This
|
||||
# live image never mounts modloop-virt into /lib/modules, so modprobe has
|
||||
# nothing to load and mkfs.ext4/mount fail with "Invalid argument"
|
||||
# regardless of e2fsprogs being installed; dd/tr are busybox builtins and
|
||||
# need no kernel module at all, so they work unconditionally. Learned
|
||||
# fixes are restored from the disk's first 1MB at boot, and a background
|
||||
# loop flushes "p" back to it every few seconds. No such disk -> "p"
|
||||
# stays on tmpfs exactly as before.
|
||||
STATE_DEV=""
|
||||
for d in /dev/vda /dev/vdb /dev/vdc /dev/sda /dev/sdb /dev/sdc /dev/xvda /dev/xvdb; do
|
||||
[ -b "$d" ] && { STATE_DEV="$d"; break; }
|
||||
done
|
||||
|
||||
if [ -n "$STATE_DEV" ]; then
|
||||
dd if="$STATE_DEV" bs=1M count=1 2>/dev/null | tr -d '\0' > /root/shell-project/p
|
||||
# p.trace (fx()'s "which source suggested this fix" record) lives in
|
||||
# the SECOND 1MB block of the same raw device via skip=1/seek=1 --
|
||||
# a distinct region, not appended after p, so growth of one file can
|
||||
# never overwrite the other's flat 1MB slot. Requires STATE_DEV to be
|
||||
# at least 2MB; a device only large enough for p's block silently
|
||||
# leaves p.trace unpersisted rather than corrupting p.
|
||||
dd if="$STATE_DEV" bs=1M skip=1 count=1 2>/dev/null | tr -d '\0' > /root/shell-project/p.trace
|
||||
(
|
||||
while true; do
|
||||
sleep 3
|
||||
[ -f /root/shell-project/p ] && dd if=/root/shell-project/p of="$STATE_DEV" bs=1M count=1 conv=notrunc 2>/dev/null
|
||||
[ -f /root/shell-project/p.trace ] && dd if=/root/shell-project/p.trace of="$STATE_DEV" bs=1M seek=1 count=1 conv=notrunc 2>/dev/null
|
||||
done
|
||||
) &
|
||||
echo "AdaptiveOS: persistent learning state active on $STATE_DEV -- learned fixes AND fix provenance survive reboot"
|
||||
fi
|
||||
|
||||
echo '========================================='
|
||||
echo ' Welcome to AdaptiveOS'
|
||||
echo ' Self-healing shell is live: f(), fx(), heal()'
|
||||
echo ' Try: fx some-command-that-might-fail'
|
||||
echo ' heal() does the same, plus logs a MISS to'
|
||||
echo ' /var/log/glyphos-heal.log if nothing (not'
|
||||
echo ' even the manual prompt) fixes it'
|
||||
echo '========================================='
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# One-time (per boot) install of git and fzf -- needs network.
|
||||
# thefuck is deliberately NOT auto-installed: it needs python3/py3-pip
|
||||
# plus a C build toolchain for musl (no prebuilt wheels for some of its
|
||||
# deps), which this minimal image does not carry, and which would be
|
||||
# painfully slow to compile under emulation. Install it by hand if you
|
||||
# want it: apk add python3 py3-pip gcc musl-dev python3-dev &&
|
||||
# pip3 install --break-system-packages thefuck
|
||||
if [ ! -f /var/adaptive-setup-done ]; then
|
||||
echo 'AdaptiveOS: bringing up network and installing git + fzf...'
|
||||
ip link set eth0 up 2>/dev/null
|
||||
ip addr add 10.0.2.15/24 dev eth0 2>/dev/null
|
||||
ip route add default via 10.0.2.2 2>/dev/null
|
||||
echo 'nameserver 10.0.2.3' > /etc/resolv.conf
|
||||
if timeout 5 ping -c1 -W3 1.1.1.1 >/dev/null 2>&1; then
|
||||
grep -q 'dl-cdn.alpinelinux.org/alpine/v3.19/main' /etc/apk/repositories || echo 'http://dl-cdn.alpinelinux.org/alpine/v3.19/main' >> /etc/apk/repositories
|
||||
grep -q 'dl-cdn.alpinelinux.org/alpine/v3.19/community' /etc/apk/repositories || echo 'http://dl-cdn.alpinelinux.org/alpine/v3.19/community' >> /etc/apk/repositories
|
||||
: > /tmp/adaptive-setup.log
|
||||
timeout 60 apk update -q >>/tmp/adaptive-setup.log 2>&1
|
||||
timeout 90 apk add -q git fzf >>/tmp/adaptive-setup.log 2>&1
|
||||
touch /var/adaptive-setup-done
|
||||
echo 'AdaptiveOS: git + fzf installed. (log: /tmp/adaptive-setup.log)'
|
||||
else
|
||||
echo 'AdaptiveOS: no network reachable -- skipping git/fzf install'
|
||||
touch /var/adaptive-setup-done
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,3 @@
|
||||
python=apk add --no-cache python3 && ln -sf /usr/bin/python3 /usr/bin/python
|
||||
pip=apk add --no-cache py3-pip
|
||||
python3=apk add --no-cache python3
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/bin/sh
|
||||
# fx: automates f()'s "enter a fix" prompt by gathering candidate fixes
|
||||
# from a directory of pluggable sources and letting you pick one (or
|
||||
# auto-picking the top one) before piping it into f's stdin.
|
||||
#
|
||||
# Zero changes to f.sh: this only decides what gets typed at the
|
||||
# "[LEARN] ... Enter a fix" prompt, using the stdin f() already reads.
|
||||
#
|
||||
# Usage:
|
||||
# . src/f.sh
|
||||
# . contrib/fx/fx.sh
|
||||
# fx some-command --that --might --fail
|
||||
#
|
||||
# Env vars:
|
||||
# FX_SOURCES_DIR the sources directory. Defaults to ./contrib/fx/sources.d
|
||||
# (i.e. source this file from the repo root). There is no
|
||||
# portable way for a *sourced* POSIX shell file to learn
|
||||
# its own path -- $0 is the enclosing shell's name, and
|
||||
# bash's BASH_SOURCE has no equivalent in dash/ash, so
|
||||
# this does not try to be clever about it. Anything
|
||||
# other than "source from repo root" (a different cwd,
|
||||
# a temp dir a la bootstrap.sh) must set this explicitly
|
||||
# *before* sourcing this file.
|
||||
# FX_AUTO if set, auto-pick the top candidate instead of
|
||||
# showing a picker
|
||||
# FX_NO_PROBE if set, skip fx's own diagnostic run of the command
|
||||
# (cheaper, but the 30-selfdiag.sh source gets no
|
||||
# output to read and will have no opinion)
|
||||
# FX_TRACE_FILE where chosen fixes' source labels are recorded, one
|
||||
# "cmd=source-file" line per accepted fix. Defaults to
|
||||
# ./p.trace, alongside f()'s own ./p. Written whenever
|
||||
# a non-blank candidate is handed to f() -- same
|
||||
# "wrote intent, not confirmed success" semantics as
|
||||
# p itself (see f.sh). This exists because f() alone
|
||||
# has no notion of *where* a fix came from: once a
|
||||
# fix is in p, a manually-typed fix, a locally-known
|
||||
# fix (10-known.sh), and an LLM-guessed fix
|
||||
# (70/80-*.sh) are byte-for-byte indistinguishable.
|
||||
# 80-remote-api.sh's own docstring is explicit that
|
||||
# LLM-sourced fixes must stay visually distinguishable
|
||||
# from verified ones -- without this file, that
|
||||
# distinction is silently lost the moment a fix is
|
||||
# accepted, and permanently lost if p is disk-persisted
|
||||
# across a reboot (see adaptive-loop.sh). p.trace is
|
||||
# what a caller checks before trusting an auto-applied
|
||||
# fix it didn't personally review.
|
||||
#
|
||||
# Source contract (see sources.d/*.sh):
|
||||
# - any executable file in $FX_SOURCES_DIR
|
||||
# - invoked as: sourcefile "$CMD"
|
||||
# - may read $FX_OUTPUT (captured stdout+stderr of one real attempt,
|
||||
# empty if FX_NO_PROBE was set)
|
||||
# - prints zero or more candidate fix lines to stdout, one per line
|
||||
# - non-zero exit or no output = "no opinion", silently skipped
|
||||
# - filename prefix (NN-name) sets display/consideration order
|
||||
#
|
||||
# Adding a source: drop a new executable file in sources.d/ that follows
|
||||
# the contract above. Nothing else to register or edit.
|
||||
|
||||
# Resolved to an absolute path *now*, at source time -- a relative
|
||||
# default would silently re-resolve against whatever cwd happens to be
|
||||
# active later when fx() is actually called, which is almost never the
|
||||
# repo root in practice.
|
||||
FX_SOURCES_DIR="${FX_SOURCES_DIR:-$(pwd)/contrib/fx/sources.d}"
|
||||
FX_TRACE_FILE="${FX_TRACE_FILE:-p.trace}"
|
||||
|
||||
fx() {
|
||||
if [ ! -d "$FX_SOURCES_DIR" ]; then
|
||||
echo "fx: FX_SOURCES_DIR '$FX_SOURCES_DIR' not found -- no sources will fire." >&2
|
||||
echo "fx: set FX_SOURCES_DIR before sourcing fx.sh if not running from the repo root." >&2
|
||||
fi
|
||||
cmd="$*"
|
||||
|
||||
FX_OUTPUT=""
|
||||
if [ -z "$FX_NO_PROBE" ]; then
|
||||
FX_OUTPUT=$("$@" 2>&1)
|
||||
if [ $? -eq 0 ]; then
|
||||
printf '%s\n' "$FX_OUTPUT"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
export FX_OUTPUT
|
||||
|
||||
# Under FX_AUTO only the first candidate is ever used (head -1
|
||||
# below), so gathering every source unconditionally would mean
|
||||
# always paying for the 25-30s local-LLM/remote-API tiers even
|
||||
# when a free source already answered and that answer is
|
||||
# guaranteed to win -- exactly the cost the NN- numbering claims to
|
||||
# avoid. Stop at the first source that produces anything once
|
||||
# FX_AUTO is set. Without it (the interactive picker), keep
|
||||
# gathering everything: a later, better-verified candidate
|
||||
# shouldn't be hidden behind an earlier weak match when a human is
|
||||
# the one choosing.
|
||||
candidates=$(
|
||||
for src in "$FX_SOURCES_DIR"/*; do
|
||||
[ -x "$src" ] || continue
|
||||
label="${src##*/}"
|
||||
out=$("$src" "$cmd" 2>/dev/null)
|
||||
if [ -n "$out" ]; then
|
||||
printf '%s\n' "$out" | while IFS= read -r line; do
|
||||
[ -n "$line" ] && printf '%s\t%s\n' "$label" "$line"
|
||||
done
|
||||
[ -n "$FX_AUTO" ] && break
|
||||
fi
|
||||
done | awk -F'\t' '
|
||||
{
|
||||
label = $1
|
||||
text = substr($0, length(label) + 2)
|
||||
if (!seen[text]++) print "[" label "] " text
|
||||
}
|
||||
'
|
||||
)
|
||||
|
||||
# chosen_line keeps the "[source-label] fix text" form intact so
|
||||
# provenance can be recovered after picking -- stripping the label
|
||||
# too early (as this used to do in each branch below) throws away
|
||||
# the one piece of information that lets a later reader tell an
|
||||
# LLM-guessed fix apart from a locally-verified one.
|
||||
chosen_line=""
|
||||
if [ -n "$candidates" ]; then
|
||||
if [ -n "$FX_AUTO" ]; then
|
||||
chosen_line=$(printf '%s\n' "$candidates" | head -1)
|
||||
elif command -v fzf >/dev/null 2>&1; then
|
||||
chosen_line=$(printf '%s\n' "$candidates" | fzf --prompt="fix for: $cmd > " --height=40%)
|
||||
else
|
||||
i=0
|
||||
printf '%s\n' "$candidates" | while IFS= read -r line; do
|
||||
i=$((i+1)); printf '%d) %s\n' "$i" "$line"
|
||||
done
|
||||
# "blank to give up", not "type your own": fx already
|
||||
# committed f's stdin to this pipe, so a blank answer here
|
||||
# can't fall through to a real terminal read the way f()'s
|
||||
# own bare prompt does -- it just pipes an empty line into
|
||||
# f(), same as when zero candidates were found at all.
|
||||
printf 'pick a number (blank to give up): '
|
||||
read -r n
|
||||
[ -n "$n" ] && chosen_line=$(printf '%s\n' "$candidates" | sed -n "${n}p")
|
||||
fi
|
||||
fi
|
||||
|
||||
chosen=$(printf '%s\n' "$chosen_line" | sed 's/^\[[^]]*\] //')
|
||||
chosen_source=$(printf '%s\n' "$chosen_line" | sed -n 's/^\[\([^]]*\)\].*/\1/p')
|
||||
|
||||
# Same "record intent, not confirmed success" timing as f()'s own
|
||||
# write to p: logged as soon as a non-blank fix is handed off, not
|
||||
# gated on the retry inside f() actually succeeding.
|
||||
if [ -n "$chosen" ] && [ -n "$chosen_source" ]; then
|
||||
printf '%s=%s\n' "$cmd" "$chosen_source" >> "$FX_TRACE_FILE"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$chosen" | f "$@"
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Fuzzy-known source: surfaces fixes already taught for a command that
|
||||
# *starts the same way* as this one, since f()'s own p lookup only ever
|
||||
# matches the exact, literal string.
|
||||
#
|
||||
# Matches only against the KEY (left of "="), not the whole line -- an
|
||||
# earlier version used `grep -F "$first_word " p`, which also matched
|
||||
# inside a fix's *value*. Proven wrong live: with p containing
|
||||
# `gti --version=gti(){ command git "$@"; }`, querying "git push" (never
|
||||
# taught) matched that line anyway, because "git " appears inside the
|
||||
# value text, not because the key is related.
|
||||
[ -f p ] || exit 0
|
||||
cmd="$1"
|
||||
first_word=$(printf '%s' "$cmd" | awk '{print $1}')
|
||||
[ -n "$first_word" ] || exit 0
|
||||
awk -v fw="$first_word " '
|
||||
substr($0, 1, length(fw)) == fw {
|
||||
line = $0
|
||||
sub(/^[^=]*=/, "", line)
|
||||
if (!seen[line]++) print line
|
||||
}
|
||||
' p 2>/dev/null
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
# Built-in fixes source: a small, curated, read-only fixes list shipped
|
||||
# inside the image itself (contrib/fx/builtin-fixes.txt) -- zero network,
|
||||
# zero API key, zero secrets, works on a completely offline boot. Same
|
||||
# "cmd=fix" line format as the learned p file and 90-team-shared.sh, exact
|
||||
# match on the full attempted command line (not fuzzy -- these are curated,
|
||||
# not guesses, so they should either match precisely or stay out of the way).
|
||||
#
|
||||
# Numbered 15: after the user's own learned-prefix fixes (10-known.sh),
|
||||
# since something the user already solved themselves should always win
|
||||
# over our shipped default. Before history/self-diag/pathfuzzy (20-40),
|
||||
# since a verified curated fix beats a blind guess.
|
||||
BUILTIN_FIXES="${FX_BUILTIN_FIXES:-/root/shell-project/contrib/fx/builtin-fixes.txt}"
|
||||
[ -f "$BUILTIN_FIXES" ] || exit 0
|
||||
cmd="$1"
|
||||
[ -n "$cmd" ] || exit 0
|
||||
grep -F "${cmd}=" "$BUILTIN_FIXES" 2>/dev/null | cut -d= -f2- | awk '!seen[$0]++'
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# History source: suggests the closest-looking command you've actually
|
||||
# typed and moved on from before (thefuck's history.py idea). Reads
|
||||
# HISTFILE directly since a separate process doesn't inherit the parent
|
||||
# shell's in-memory history.
|
||||
#
|
||||
# Single awk pass over the tail of the file: on weak/emulated hardware
|
||||
# every avoided fork+exec matters (measured: an unoptimized PATH walk in
|
||||
# 40-pathfuzzy.sh took >30s on this box before being fixed; pipeline
|
||||
# depth has the same tax, just smaller per-hop) -- a five-process
|
||||
# tail|grep|grep|awk|tail chain does the same job as one awk script
|
||||
# reading the file once.
|
||||
cmd="$1"
|
||||
first_word=$(printf '%s' "$cmd" | awk '{print $1}')
|
||||
[ -n "$first_word" ] || exit 0
|
||||
hf="${HISTFILE:-$HOME/.bash_history}"
|
||||
[ -r "$hf" ] || exit 0
|
||||
|
||||
tail -n 300 "$hf" 2>/dev/null | awk -v first="$first_word" -v full="$cmd" '
|
||||
index($0, first " ") == 1 && $0 != full && !seen[$0]++ { buf[++n] = $0 }
|
||||
END {
|
||||
start = (n > 5) ? n - 4 : 1
|
||||
for (i = start; i <= n; i++) print buf[i]
|
||||
}
|
||||
'
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
# Self-diagnosis source: many CLIs print their own corrected invocation
|
||||
# in their error output (git's --set-upstream hint, apt's "did you
|
||||
# mean", etc.).
|
||||
#
|
||||
# IMPORTANT: this must NOT cache the literal suggested line -- that
|
||||
# reintroduces the exact-key staleness problem (e.g. "git push" learned
|
||||
# once on branch A silently misapplies its captured --set-upstream on
|
||||
# branch B, and B never actually gets pushed). Instead it emits a
|
||||
# function that shadows the whole program and re-derives the suggestion
|
||||
# from *live* output on every call, the same trick proven to generalize
|
||||
# correctly across branches/arguments in practice. One taught fix this
|
||||
# way covers every future self-diagnosing failure from that program, not
|
||||
# just the one instance that happened to trigger the teaching prompt.
|
||||
#
|
||||
# The emitted function runs on every future call to that program for
|
||||
# the rest of the session, so its own internal extraction is one awk
|
||||
# pass, not a grep|sed|tail chain -- three fewer forks on every failure
|
||||
# it ever handles, not just this first one.
|
||||
cmd="$1"
|
||||
prog=$(printf '%s' "$cmd" | awk '{print $1}')
|
||||
[ -n "$prog" ] || exit 0
|
||||
[ -n "$FX_OUTPUT" ] || exit 0
|
||||
|
||||
# Literal prefix comparison (substr/index), never a regex built from
|
||||
# $prog: proven broken live for program names containing ERE
|
||||
# metacharacters -- e.g. "g++ foo.cpp" produced the pattern
|
||||
# "^[[:space:]]*g++[[:space:]]", a malformed stacked-quantifier regex,
|
||||
# and this source silently found nothing even when $FX_OUTPUT clearly
|
||||
# started a line with "g++ ". Same fix applied to the emitted function
|
||||
# below, since it runs the identical check on every future call.
|
||||
printf '%s\n' "$FX_OUTPUT" | awk -v p="$prog" '
|
||||
{ line = $0; sub(/^[ \t]*/, "", line); if (substr(line, 1, length(p) + 1) == p " ") { found = 1; exit } }
|
||||
END { exit !found }
|
||||
' || exit 0
|
||||
|
||||
cat <<FIX
|
||||
${prog}(){ out=\$(command ${prog} "\$@" 2>&1); ec=\$?; if [ \$ec -ne 0 ]; then sug=\$(printf '%s\\n' "\$out" | awk -v p="${prog}" '{ line=\$0; sub(/^[ \\t]*/,"",line); if (substr(line,1,length(p)+1)==p" ") s=line } END{print s}'); if [ -n "\$sug" ]; then eval "command \$sug"; return \$?; fi; fi; printf '%s\\n' "\$out"; return \$ec; }
|
||||
FIX
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# PATH-fuzzy source: if the first word isn't a known command, alias, or
|
||||
# function, offer a function wrapping the closest-spelled real command
|
||||
# as a candidate -- as a fix you approve through the normal f() flow,
|
||||
# not a silently auto-installed hook. A function, not an alias: aliases
|
||||
# only expand in interactive shells (need `shopt -s expand_aliases`
|
||||
# otherwise), so an alias-based fix silently does nothing when f/fx run
|
||||
# inside a script.
|
||||
#
|
||||
# Pure POSIX: walks $PATH directly instead of `compgen -c`, which is a
|
||||
# bash-only builtin and does not exist under busybox ash -- this source
|
||||
# used to be dead weight on exactly the minimal/embedded shells this
|
||||
# project targets. Tested against busybox ash + busybox awk directly,
|
||||
# not just bash + gawk.
|
||||
#
|
||||
# Uses Damerau-Levenshtein-lite (edit distance + adjacent transposition)
|
||||
# so the canonical "gti" -> "git" typo scores 1, not 2 -- plain Hamming
|
||||
# or substitution-only distance ties it with unrelated 3-letter commands
|
||||
# like "ftp" and can pick the wrong one.
|
||||
cmd="$1"
|
||||
first_word=$(printf '%s' "$cmd" | awk '{print $1}')
|
||||
[ -n "$first_word" ] || exit 0
|
||||
command -v "$first_word" >/dev/null 2>&1 && exit 0
|
||||
|
||||
IFS=:
|
||||
for d in $PATH; do
|
||||
ls "$d" 2>/dev/null
|
||||
done | sort -u | awk -v target="$first_word" '
|
||||
function min3(a, b, c) { return (a < b ? (a < c ? a : c) : (b < c ? b : c)) }
|
||||
function distance(a, b, n, m, i, j, ca, cb, cost, tmp) {
|
||||
n = length(a); m = length(b)
|
||||
if (n == 0) return m
|
||||
if (m == 0) return n
|
||||
for (j = 0; j <= m; j++) d[0, j] = j
|
||||
for (i = 1; i <= n; i++) {
|
||||
d[i, 0] = i
|
||||
ca = substr(a, i, 1)
|
||||
for (j = 1; j <= m; j++) {
|
||||
cb = substr(b, j, 1)
|
||||
cost = (ca == cb) ? 0 : 1
|
||||
tmp = min3(d[i-1, j] + 1, d[i, j-1] + 1, d[i-1, j-1] + cost)
|
||||
if (i > 1 && j > 1 && ca == substr(b, j-1, 1) && substr(a, i-1, 1) == cb) {
|
||||
tmp = (tmp < d[i-2, j-2] + 1) ? tmp : d[i-2, j-2] + 1
|
||||
}
|
||||
d[i, j] = tmp
|
||||
}
|
||||
}
|
||||
return d[n, m]
|
||||
}
|
||||
{
|
||||
lendiff = length($0) - length(target)
|
||||
if (lendiff > 2 || lendiff < -2) next
|
||||
dd = distance(target, $0)
|
||||
if (dd <= 2 && dd > 0 && (best == "" || dd < bestd)) { best = $0; bestd = dd }
|
||||
}
|
||||
END { if (best != "") print target "(){ command " best " \"$@\"; }" }
|
||||
'
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# thefuck bridge: only fires if thefuck is installed. Note that thefuck
|
||||
# --yes decides *and executes* its own suggestion, so by the time this
|
||||
# candidate is shown it may have already run once via thefuck -- treat
|
||||
# it as informational, not a dry-run.
|
||||
#
|
||||
# Wrapped in `timeout`: thefuck has no fast-fail path when no rule
|
||||
# matches -- fuzz-testing this source found it hanging past 3s on a
|
||||
# majority of unmatched inputs (well beyond its own ~325ms best-case
|
||||
# startup cost measured earlier). Every other source in this directory
|
||||
# either runs in single-digit milliseconds or is itself already
|
||||
# timeout-wrapped (70/80's HTTP calls); this one wasn't, so a single
|
||||
# bad input could block the whole fx() pipeline indefinitely. Capped
|
||||
# at 5s -- generous enough for a real correction (~2.8s measured
|
||||
# earlier for the git-push case) without being unbounded.
|
||||
command -v thefuck >/dev/null 2>&1 || exit 0
|
||||
command -v timeout >/dev/null 2>&1 || exit 0
|
||||
cmd="$1"
|
||||
[ -n "$cmd" ] || exit 0
|
||||
timeout 5 thefuck --yes "$cmd" 2>/dev/null | tr -d '\342\200\213' | tail -1
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/bin/sh
|
||||
# Last-resort source: queries a local LLM inference server (Ollama or
|
||||
# LM Studio, both common on dev machines) for a fix, only when nothing
|
||||
# cheaper matched. Silent no-op if no local server is reachable -- this
|
||||
# never touches the network, never calls a paid API, and costs nothing
|
||||
# when you don't have a local model running.
|
||||
#
|
||||
# Numbered 70, after every deterministic/heuristic source (10-50) and
|
||||
# before the team-shared source (90): cheap, verified candidates should
|
||||
# always win first. fx.sh's FX_AUTO picks the *first* candidate in
|
||||
# source order, so this only gets auto-selected when it's the only
|
||||
# opinion offered at all -- still review it, this is a guess, not a
|
||||
# fact. Every other default source is either a cached human decision or
|
||||
# a deterministic rule; this is the one source capable of confidently
|
||||
# suggesting something wrong.
|
||||
#
|
||||
# Needs python3 (uses urllib, no extra dependency beyond stdlib) --
|
||||
# silently no-ops without it rather than ship a fragile hand-rolled
|
||||
# JSON encoder in shell.
|
||||
|
||||
cmd="$1"
|
||||
[ -n "$cmd" ] || exit 0
|
||||
command -v python3 >/dev/null 2>&1 || exit 0
|
||||
|
||||
OLLAMA_URL="${FX_LLM_OLLAMA_URL:-http://127.0.0.1:11434}"
|
||||
LMSTUDIO_URL="${FX_LLM_LMSTUDIO_URL:-http://127.0.0.1:1234}"
|
||||
|
||||
python3 - "$cmd" "$FX_OUTPUT" "$OLLAMA_URL" "$LMSTUDIO_URL" <<'PYEOF'
|
||||
import sys, json, urllib.request, urllib.error
|
||||
|
||||
cmd, output, ollama_url, lmstudio_url = sys.argv[1:5]
|
||||
|
||||
PROMPT = f"""You are suggesting a fix for a failed shell command, to be taught to a self-healing shell function called f().
|
||||
Rules:
|
||||
- f() will eval your fix ONCE as a precondition, then re-run the ORIGINAL command verbatim. Your fix never replaces the command; it only sets up state so the original command then succeeds.
|
||||
- If the fix should generalize beyond this one invocation, define a shell function with the same name as the failing program, wrapping the real binary via `command <prog> "$@"`, and only special-case the failure inside it.
|
||||
- Respond with EXACTLY ONE LINE of POSIX shell and nothing else: no explanation, no markdown, no code fences, no commentary.
|
||||
|
||||
Failing command: {cmd}
|
||||
|
||||
Its output:
|
||||
{output}
|
||||
"""
|
||||
|
||||
def http_json(url, payload, timeout):
|
||||
req = urllib.request.Request(
|
||||
url, data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"}, method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
def http_get_json(url, timeout):
|
||||
with urllib.request.urlopen(url, timeout=timeout) as r:
|
||||
return json.loads(r.read().decode())
|
||||
|
||||
def try_ollama():
|
||||
tags = http_get_json(ollama_url.rstrip("/") + "/api/tags", 2)
|
||||
models = tags.get("models") or []
|
||||
if not models:
|
||||
return None
|
||||
model = models[0].get("name")
|
||||
if not model:
|
||||
return None
|
||||
resp = http_json(
|
||||
ollama_url.rstrip("/") + "/api/generate",
|
||||
{"model": model, "prompt": PROMPT, "stream": False},
|
||||
25,
|
||||
)
|
||||
return (resp.get("response") or "").strip()
|
||||
|
||||
def try_lmstudio():
|
||||
models = http_get_json(lmstudio_url.rstrip("/") + "/v1/models", 2)
|
||||
data = models.get("data") or []
|
||||
if not data:
|
||||
return None
|
||||
model = data[0].get("id")
|
||||
if not model:
|
||||
return None
|
||||
resp = http_json(
|
||||
lmstudio_url.rstrip("/") + "/v1/chat/completions",
|
||||
{
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": PROMPT}],
|
||||
"temperature": 0,
|
||||
},
|
||||
25,
|
||||
)
|
||||
choices = resp.get("choices") or []
|
||||
if not choices:
|
||||
return None
|
||||
return (choices[0].get("message", {}).get("content") or "").strip()
|
||||
|
||||
for attempt in (try_ollama, try_lmstudio):
|
||||
try:
|
||||
result = attempt()
|
||||
except (urllib.error.URLError, TimeoutError, OSError, ValueError, KeyError):
|
||||
result = None
|
||||
if result:
|
||||
# Enforce the one-line contract even if the model ignores instructions.
|
||||
first_line = result.splitlines()[0].strip()
|
||||
first_line = first_line.strip("`")
|
||||
if first_line:
|
||||
print(first_line)
|
||||
break
|
||||
PYEOF
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/bin/sh
|
||||
# Remote-API fallback source: queries any OpenAI-compatible chat
|
||||
# completions endpoint (OpenAI itself, OpenRouter, Groq, Together,
|
||||
# Anthropic's OpenAI-compatibility layer, a company-internal proxy,
|
||||
# etc.) for a fix, only when nothing cheaper -- including the free
|
||||
# local model in 70-local-llm.sh -- already matched.
|
||||
#
|
||||
# Off by default. Requires FX_REMOTE_API_URL, FX_REMOTE_API_KEY, and
|
||||
# FX_REMOTE_API_MODEL to all be set explicitly; no default model is
|
||||
# assumed, since guessing one could silently pick something you don't
|
||||
# want to pay for. Silently no-ops if any of the three is missing.
|
||||
#
|
||||
# FX_REMOTE_API_URL is the exact URL to POST to -- no "/chat/completions"
|
||||
# auto-appended. An earlier version guessed that suffix onto whatever
|
||||
# base URL was given, which works for OpenAI/OpenRouter/Groq's actual
|
||||
# convention but broke, silently, against a real third-party API that
|
||||
# used a different path (a local FastAPI service with a plain "/chat"
|
||||
# endpoint, not "/chat/.../completions" -- confirmed live: the guessed
|
||||
# URL 404'd, the real one worked). Silent failure on a wrong guess is
|
||||
# worse than requiring the full URL up front.
|
||||
#
|
||||
# Numbered 80: after the free local-LLM source (70) and before
|
||||
# 90-team-shared.sh, which is a human-curated source and gets the final
|
||||
# word. This is the most expensive tier in latency and real dollar
|
||||
# cost, so it should only ever be the last thing tried.
|
||||
#
|
||||
# TRUST WARNING, sharper than every other source here: this is the one
|
||||
# tier where the text you'd be eval'ing was not authored by you, a tool
|
||||
# you run, or a URL you personally chose to trust -- it's a third-party
|
||||
# service's best guess. fx.sh already labels every candidate with its
|
||||
# source filename in the picker ("[80-remote-api.sh] ..."), so this is
|
||||
# never visually indistinguishable from a self-diagnosed or locally-
|
||||
# verified fix. Read that label before accepting.
|
||||
|
||||
cmd="$1"
|
||||
[ -n "$cmd" ] || exit 0
|
||||
command -v python3 >/dev/null 2>&1 || exit 0
|
||||
|
||||
[ -n "$FX_REMOTE_API_URL" ] || exit 0
|
||||
[ -n "$FX_REMOTE_API_KEY" ] || exit 0
|
||||
[ -n "$FX_REMOTE_API_MODEL" ] || exit 0
|
||||
|
||||
# Some providers want "Authorization: Bearer <key>" (OpenAI, OpenRouter,
|
||||
# Groq, ...), others want a bare custom header (Anthropic's native API
|
||||
# uses "x-api-key: <key>" with no prefix). Both are configurable so this
|
||||
# isn't hardcoded to one vendor's auth scheme.
|
||||
AUTH_HEADER="${FX_REMOTE_API_AUTH_HEADER:-Authorization}"
|
||||
AUTH_PREFIX="${FX_REMOTE_API_AUTH_PREFIX-Bearer }"
|
||||
|
||||
python3 - "$cmd" "$FX_OUTPUT" "$FX_REMOTE_API_URL" "$FX_REMOTE_API_KEY" "$FX_REMOTE_API_MODEL" "$AUTH_HEADER" "$AUTH_PREFIX" <<'PYEOF'
|
||||
import sys, json, urllib.request, urllib.error
|
||||
|
||||
cmd, output, url, key, model, auth_header, auth_prefix = sys.argv[1:8]
|
||||
|
||||
PROMPT = f"""You are suggesting a fix for a failed shell command, to be taught to a self-healing shell function called f().
|
||||
Rules:
|
||||
- f() will eval your fix ONCE as a precondition, then re-run the ORIGINAL command verbatim. Your fix never replaces the command; it only sets up state so the original command then succeeds.
|
||||
- If the fix should generalize beyond this one invocation, define a shell function with the same name as the failing program, wrapping the real binary via `command <prog> "$@"`, and only special-case the failure inside it.
|
||||
- Respond with EXACTLY ONE LINE of POSIX shell and nothing else: no explanation, no markdown, no code fences, no commentary.
|
||||
|
||||
Failing command: {cmd}
|
||||
|
||||
Its output:
|
||||
{output}
|
||||
"""
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": PROMPT}],
|
||||
"temperature": 0,
|
||||
"max_tokens": 200,
|
||||
}
|
||||
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=json.dumps(payload).encode(),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
auth_header: f"{auth_prefix}{key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
resp = json.loads(r.read().decode())
|
||||
except (urllib.error.URLError, TimeoutError, OSError, ValueError):
|
||||
sys.exit(0)
|
||||
|
||||
choices = resp.get("choices") or []
|
||||
if not choices:
|
||||
sys.exit(0)
|
||||
content = (choices[0].get("message", {}).get("content") or "").strip()
|
||||
if not content:
|
||||
sys.exit(0)
|
||||
|
||||
# Enforce the one-line contract even if the model ignores instructions.
|
||||
first_line = content.splitlines()[0].strip().strip("`")
|
||||
if first_line:
|
||||
print(first_line)
|
||||
PYEOF
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
# EXAMPLE custom source -- a template, not a real dependency. This is
|
||||
# the entire extension mechanism demonstrated: any executable dropped
|
||||
# into sources.d/ following the contract in fx.sh is picked up
|
||||
# automatically, no registration step anywhere else.
|
||||
#
|
||||
# Pulls a team-shared p-format corrections file and offers exact-key
|
||||
# matches. Off by default (no-ops unless FX_TEAM_URL is set) because a
|
||||
# match here becomes eval'd code the moment you select it -- point this
|
||||
# at a URL you trust the same way you'd trust an rc file, never a
|
||||
# random link.
|
||||
[ -n "$FX_TEAM_URL" ] || exit 0
|
||||
cmd="$1"
|
||||
[ -n "$cmd" ] || exit 0
|
||||
curl -fsSL "$FX_TEAM_URL" 2>/dev/null | grep -F "${cmd}=" | cut -d= -f2-
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
# heal(): thin observability wrapper around fx(). fx()/f() (see
|
||||
# ../fx/fx.sh, ../../src/f.sh) already do the actual healing across every
|
||||
# tier plus the manual "[LEARN] ... Enter a fix" fallback -- this adds
|
||||
# nothing to that logic. It only appends one structured line when every
|
||||
# tier AND the manual prompt come up empty, so an external caller or
|
||||
# orchestrator has something to grep for instead of only a bare nonzero
|
||||
# exit code.
|
||||
#
|
||||
# Usage:
|
||||
# . src/f.sh
|
||||
# . contrib/fx/fx.sh
|
||||
# . contrib/heal/heal.sh
|
||||
# heal some-command-that-might-fail --with args
|
||||
HEAL_LOG="${HEAL_LOG:-/var/log/glyphos-heal.log}"
|
||||
|
||||
heal() {
|
||||
if fx "$@"; then
|
||||
return 0
|
||||
fi
|
||||
ec=$?
|
||||
ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown-time)
|
||||
printf '%s MISS cmd="%s"\n' "$ts" "$*" >> "$HEAL_LOG" 2>/dev/null
|
||||
return "$ec"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/bin/sh
|
||||
# Self-healing shell loop: run a command, and on failure ask for a fix,
|
||||
# persist it keyed by the exact command line, then retry. On future
|
||||
# calls with the same command, the learned fix is applied proactively
|
||||
# before the command itself runs.
|
||||
#
|
||||
# State lives in ./p as "command=fix" lines, one per learned command.
|
||||
# Copy p to another machine to transfer everything this shell has learned.
|
||||
#
|
||||
# Usage:
|
||||
# . f.sh
|
||||
# f some-command-that-might-fail --with args
|
||||
|
||||
f() {
|
||||
FIX=""
|
||||
if [ -f p ]; then
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
"$*="*) FIX="${line#"$*="}" ;;
|
||||
esac
|
||||
done < p
|
||||
fi
|
||||
|
||||
until { eval "$FIX" 2>/dev/null; "$@"; }; do
|
||||
printf '\n[LEARN] "%s" failed. Enter a fix (blank to give up): ' "$*" >&2
|
||||
read -r n
|
||||
[ -n "$n" ] || return 1
|
||||
# printf, not echo: POSIX leaves it implementation-defined whether
|
||||
# echo interprets backslash escapes in its argument, and dash/
|
||||
# busybox ash both do by default (bash's echo doesn't, unless
|
||||
# -e). A taught fix containing a literal "\n" -- e.g. any fix
|
||||
# that itself uses printf '%s\n' internally, which is common and
|
||||
# idiomatic -- got silently corrupted into a real embedded
|
||||
# newline on write, breaking p's one-fix-per-line format the
|
||||
# moment that fix was replayed. Confirmed live: identical fix
|
||||
# text taught under bash was fine; the same text taught under
|
||||
# `sh` (dash) truncated mid-line on replay. printf's %s never
|
||||
# interprets escapes in the substituted value, under any shell.
|
||||
printf '%s=%s\n' "$*" "$n" >> p
|
||||
FIX="$n"
|
||||
done
|
||||
}
|
||||
Reference in New Issue
Block a user