mirror of
https://github.com/lukaszraczylo/claude-adam.git
synced 2026-06-05 22:49:28 +00:00
6d8ff37cb2
Bug fixes (HIGH):
- adam-observe.mjs: errorFingerprint no longer false-positives when
toolResponse.is_error === false; ERROR_RE only used as fallback when
is_error is undefined.
- adam-observe.mjs: resetSessionLocal now clears tool_window so retry_loop
cannot fire on the first tool of a new session by matching prior session.
- adam-archive.mjs: ts dedup uses Map<ts, count> instead of Set<ts>; two
journal entries sharing a millisecond are no longer both archived when
only one is referenced in source_entries.
- adam-nudge.mjs: only counts proposal filenames matching
/^\d{4}-\d{2}-\d{3}-/ pattern; README/notes in proposals/ no longer bump.
- skills/adam-self-improvement/SKILL.md: contradiction_flag veto now applied
at apply time (carry-over from earlier review).
Test isolation:
- adam/tests/run-tests.sh: ALWAYS runs against an isolated $HOME under
mktemp -d. Previously truncated live ~/.claude/adam/journal.jsonl on
every run — destructive on production state.
Conciseness:
- agents/adam.md: -19 LOC (cuts: vestigial cursor sentence, duplicate
not-do bullets, blast-radius bullet collapse, Inputs paths delegate to
SKILL.md, win-cluster-vs-struggle-cluster commentary already enforced
by cluster-key separation, # Overlap section spec compressed).
- skills/adam-self-improvement/SKILL.md: -4 LOC (framing paragraph, dead
catch-all bullet for non-eligible types).
Auto-prune script DELETED:
- The cumulative-count primitive cannot distinguish "never used" from
"used before tracking began"; mtime gate is meaningless for installed
files. Auto-prune deferred to v0.4 with a per-key lastSeen schema.
Cross-platform:
- macOS (BSD coreutils) and Linux (Alpine, glibc + musl) verified.
- All scripts use portable forms (stat -f || stat -c, mktemp -d -t).
- README documents platform support explicitly.
DX overhaul:
- install.sh: hardened — supports `curl | bash` via auto-clone,
--version=vX.Y.Z pinning, --yes / --dry-run flags, jq-based
settings.json merge with diff prompt and backup, conservative file
copy that detects local mtime drift and writes <file>.adam-new
instead of clobbering, idempotent across re-runs.
- adam-uninstall.sh: NEW. Soft-archives ~/.claude/adam/ to .bak.<ts>/
by default; --purge to delete; --yes for non-interactive; jq-based
settings.json cleanup with diff prompt.
- README.md: curl one-liner install + version-pinned variant at top,
What's New section through v0.3.1, upgrade-safe data files callout,
uninstaller documentation, platform support note, expanded rubric
showing skill_edit gate.
Test count: 27 passed, 0 failed (was 27 — no regression).
217 lines
8.1 KiB
Bash
Executable File
217 lines
8.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ADAM installer — pure bash + git + curl + jq.
|
|
# Idempotent. Safe for upgrades. Supports `curl | bash` via auto-clone.
|
|
#
|
|
# Usage:
|
|
# ./install.sh # local install from cwd
|
|
# curl -fsSL <raw>/install.sh | bash
|
|
# VERSION=v0.3.0 ./install.sh # pin a tag
|
|
# ./install.sh --yes # skip settings.json prompt
|
|
# ./install.sh --dry-run # show actions, write nothing
|
|
|
|
set -euo pipefail
|
|
|
|
REPO_GIT="https://github.com/lukaszraczylo/claude-adam.git"
|
|
DEST="${HOME}/.claude"
|
|
ASSUME_YES=0
|
|
DRY_RUN=0
|
|
VERSION="${VERSION:-${BRANCH:-}}" # env var pin; empty = latest tag
|
|
|
|
log() { printf ' %s\n' "$*"; }
|
|
warn() { printf ' ! %s\n' "$*" >&2; }
|
|
die() { printf ' ! %s\n' "$*" >&2; exit 1; }
|
|
run() { if [ "$DRY_RUN" = 1 ]; then printf ' [dry-run] %s\n' "$*"; else eval "$@"; fi; }
|
|
|
|
# --------------------------------------------------------------------- args
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--yes|-y) ASSUME_YES=1 ;;
|
|
--dry-run|-n) DRY_RUN=1 ;;
|
|
--version=*) VERSION="${arg#--version=}" ;;
|
|
--help|-h) sed -n '2,12p' "$0"; exit 0 ;;
|
|
*) die "unknown arg: $arg (try --help)" ;;
|
|
esac
|
|
done
|
|
|
|
# --------------------------------------------------------------------- prereqs
|
|
need() { command -v "$1" >/dev/null 2>&1 || die "missing: $1 — $2"; }
|
|
need git "install: brew install git || apt install git"
|
|
need curl "install: brew install curl || apt install curl"
|
|
need jq "install: brew install jq || apt install jq"
|
|
command -v node >/dev/null 2>&1 || warn "node not found — hooks need node 18+; install: brew install node"
|
|
|
|
# --------------------------------------------------------------------- locate source
|
|
# If invoked via `curl | bash`, $0 is bash and there are no local files.
|
|
PIPED=0
|
|
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
|
|
if [ ! -f "$SCRIPT_PATH" ] || [ "$SCRIPT_PATH" = "bash" ] || [ "$SCRIPT_PATH" = "-" ]; then
|
|
PIPED=1
|
|
elif [ ! -d "$(dirname "$SCRIPT_PATH")/hooks" ]; then
|
|
PIPED=1
|
|
fi
|
|
|
|
CLEANUP_TMP=""
|
|
cleanup() { [ -n "$CLEANUP_TMP" ] && rm -rf "$CLEANUP_TMP" 2>/dev/null || true; }
|
|
trap cleanup EXIT INT TERM
|
|
|
|
if [ "$PIPED" = 1 ]; then
|
|
log "running via curl|bash — cloning repo to tmp"
|
|
CLEANUP_TMP="$(mktemp -d -t claude-adam-install.XXXXXX)"
|
|
REF="$VERSION"
|
|
if [ -z "$REF" ]; then
|
|
# latest semver tag from remote (no local clone needed)
|
|
REF="$(git ls-remote --tags --refs "$REPO_GIT" \
|
|
| awk -F/ '{print $NF}' \
|
|
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
|
| sort -V | tail -1)"
|
|
[ -z "$REF" ] && REF="main"
|
|
fi
|
|
log "fetching $REF"
|
|
run "git clone --quiet --depth=1 --branch=\"$REF\" \"$REPO_GIT\" \"$CLEANUP_TMP\""
|
|
SRC="$CLEANUP_TMP"
|
|
else
|
|
SRC="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
|
fi
|
|
|
|
log "ADAM installer"
|
|
log " source: $SRC"
|
|
log " dest: $DEST"
|
|
log " mode: $([ "$DRY_RUN" = 1 ] && echo dry-run || echo apply)$([ "$ASSUME_YES" = 1 ] && echo ' --yes' || true)"
|
|
log ""
|
|
|
|
[ -d "$DEST" ] || die "$DEST does not exist. Install Claude Code first: https://claude.com/claude-code"
|
|
|
|
# --------------------------------------------------------------------- dirs
|
|
DIRS=(
|
|
"hooks" "agents" "skills/adam-self-improvement" "commands"
|
|
"adam/proposals" "adam/applied" "adam/rejected" "adam/trash"
|
|
"adam/journal" "adam/scripts" "adam/tests/fixtures"
|
|
)
|
|
for d in "${DIRS[@]}"; do run "mkdir -p \"$DEST/$d\""; done
|
|
|
|
# .gitkeep markers so the layout survives `git init` for users who VCS ~/.claude
|
|
for d in adam/proposals adam/applied adam/rejected adam/trash adam/journal; do
|
|
[ -e "$DEST/$d/.gitkeep" ] || run ": > \"$DEST/$d/.gitkeep\""
|
|
done
|
|
|
|
# --------------------------------------------------------------------- file copy
|
|
# Conservative: if dest exists and differs from src AND user-modified after install,
|
|
# write to <file>.adam-new and warn instead of clobbering.
|
|
copy_file() {
|
|
local src="$1" dst="$2"
|
|
[ -f "$src" ] || die "missing source file: $src"
|
|
if [ -f "$dst" ] && ! cmp -s "$src" "$dst"; then
|
|
if [ -f "$DEST/adam/.install-marker" ] \
|
|
&& [ "$(stat -f %m "$dst" 2>/dev/null || stat -c %Y "$dst")" \
|
|
-gt "$(stat -f %m "$DEST/adam/.install-marker" 2>/dev/null || stat -c %Y "$DEST/adam/.install-marker")" ]; then
|
|
warn "modified locally, NOT overwriting: $dst"
|
|
warn " new version written to: $dst.adam-new (review and merge manually)"
|
|
run "cp \"$src\" \"$dst.adam-new\""
|
|
return
|
|
fi
|
|
fi
|
|
run "cp \"$src\" \"$dst\""
|
|
log " copied: ${dst#$HOME/}"
|
|
}
|
|
|
|
# Hooks
|
|
copy_file "$SRC/hooks/adam-observe.mjs" "$DEST/hooks/adam-observe.mjs"
|
|
copy_file "$SRC/hooks/adam-nudge.mjs" "$DEST/hooks/adam-nudge.mjs"
|
|
# Agent / skill / command
|
|
copy_file "$SRC/agents/adam.md" "$DEST/agents/adam.md"
|
|
copy_file "$SRC/skills/adam-self-improvement/SKILL.md" "$DEST/skills/adam-self-improvement/SKILL.md"
|
|
copy_file "$SRC/commands/reflect.md" "$DEST/commands/reflect.md"
|
|
# Adam internals
|
|
copy_file "$SRC/adam/scripts/adam-archive.mjs" "$DEST/adam/scripts/adam-archive.mjs"
|
|
copy_file "$SRC/adam/tests/run-tests.sh" "$DEST/adam/tests/run-tests.sh"
|
|
copy_file "$SRC/adam/tests/fixtures/seed-corrections.jsonl" "$DEST/adam/tests/fixtures/seed-corrections.jsonl"
|
|
|
|
# Preserve user data — never overwrite
|
|
[ -f "$DEST/adam/journal.jsonl" ] || run ": > \"$DEST/adam/journal.jsonl\""
|
|
[ -f "$DEST/adam/state.json" ] || run "echo '{\"tool_window\":[]}' > \"$DEST/adam/state.json\""
|
|
[ -f "$DEST/adam/usage.json" ] || run "echo '{}' > \"$DEST/adam/usage.json\""
|
|
|
|
# install marker — used by future runs to detect local mtime drift
|
|
run "touch \"$DEST/adam/.install-marker\""
|
|
|
|
# --------------------------------------------------------------------- settings.json
|
|
SETTINGS="$DEST/settings.json"
|
|
EXAMPLE="$SRC/settings.json.example"
|
|
[ -f "$EXAMPLE" ] || die "missing $EXAMPLE"
|
|
|
|
# Build target settings via jq merge (preserves all user keys/hooks).
|
|
TMP_NEW="$(mktemp -t adam-settings.XXXXXX)"
|
|
TMP_DIFF="$(mktemp -t adam-settings-diff.XXXXXX)"
|
|
cleanup_full() { cleanup; rm -f "$TMP_NEW" "$TMP_DIFF" 2>/dev/null || true; }
|
|
trap cleanup_full EXIT INT TERM
|
|
|
|
if [ -f "$SETTINGS" ]; then
|
|
jq --slurpfile add "$EXAMPLE" '
|
|
. as $cur
|
|
| ($add[0].hooks // {}) as $new
|
|
| .hooks = (
|
|
($cur.hooks // {}) as $cur_hooks
|
|
| reduce ($new | keys[]) as $k ($cur_hooks;
|
|
.[$k] = (
|
|
((.[$k] // []) + $new[$k])
|
|
| unique_by(tojson)
|
|
)
|
|
)
|
|
)
|
|
' "$SETTINGS" > "$TMP_NEW"
|
|
else
|
|
jq 'del(._comment)' "$EXAMPLE" > "$TMP_NEW"
|
|
fi
|
|
|
|
if [ -f "$SETTINGS" ] && cmp -s "$SETTINGS" "$TMP_NEW"; then
|
|
log "settings.json already wired — no changes"
|
|
else
|
|
log ""
|
|
log "settings.json changes proposed:"
|
|
if [ -f "$SETTINGS" ]; then
|
|
diff -u "$SETTINGS" "$TMP_NEW" > "$TMP_DIFF" || true
|
|
else
|
|
diff -u /dev/null "$TMP_NEW" > "$TMP_DIFF" || true
|
|
fi
|
|
sed 's/^/ /' "$TMP_DIFF"
|
|
log ""
|
|
if [ "$ASSUME_YES" = 1 ]; then
|
|
REPLY=y
|
|
else
|
|
printf ' apply settings.json changes? [y/N] '
|
|
read -r REPLY </dev/tty || REPLY=n
|
|
fi
|
|
case "$REPLY" in
|
|
y|Y|yes|YES)
|
|
[ -f "$SETTINGS" ] && run "cp \"$SETTINGS\" \"$SETTINGS.adam-bak.$(date +%s)\""
|
|
run "mv \"$TMP_NEW\" \"$SETTINGS\""
|
|
log " settings.json updated (backup at *.adam-bak.<ts> if pre-existing)"
|
|
;;
|
|
*)
|
|
log " skipped — wire entries from $EXAMPLE manually"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# --------------------------------------------------------------------- summary
|
|
log ""
|
|
log "installed:"
|
|
log " hooks/adam-observe.mjs, hooks/adam-nudge.mjs"
|
|
log " agents/adam.md"
|
|
log " skills/adam-self-improvement/SKILL.md"
|
|
log " commands/reflect.md"
|
|
log " adam/scripts/adam-archive.mjs"
|
|
log " adam/tests/run-tests.sh"
|
|
log ""
|
|
log "preserved (if existed):"
|
|
log " adam/journal.jsonl, adam/state.json, adam/usage.json"
|
|
log ""
|
|
log "next:"
|
|
log " 1. bash $DEST/adam/tests/run-tests.sh # expect: all passed"
|
|
log " 2. start a fresh Claude Code session"
|
|
log " 3. run /reflect to invoke the analyst"
|
|
log ""
|
|
log "ADAM is dormant until you run /reflect."
|
|
log "journal: $DEST/adam/journal.jsonl"
|
|
log "proposals: $DEST/adam/proposals/"
|