mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 14:38:59 +02:00
Two things the first cut got wrong.
Memorable's own installer registers the same UserPromptSubmit hook, under its
own name and outside gstack's table. `memorable start`, `memorable setup` and
`memorable install-hooks` all do it, and that is the documented way to install
the CLI, so on most machines it is already there before gstack is asked.
Registering ours beside it ran the same command twice on every prompt: context
injected twice, and the session captured twice against the user's own
extraction allowance. `enable` now looks for it and refuses, naming the entry
and the file it lives in; `status` says who registered it rather than reporting
none. Matched on the command rather than on a tag, for the reason the hook
table already gives: Claude Code rewrites settings and private tags do not
survive it.
The removal instruction says to delete the entry by hand because Memorable has
no command that removes its own hook. `uninstall-hooks` is not a command in
0.5.18; it answers "unknown command".
The README said "Memorable, not gstack, owns the captured data and any network
access", which answers the question by pointing away from it. It now carries a
per-command table of exactly what leaves the machine, in the shape the adopted
gbrain section uses, and it is explicit that the hook makes no network call of
its own, that every row is the third-party CLI acting under its own consent,
and that `gstack-egress` will therefore not show any of it. Under it, the split
between what gstack pin-tests (the gating and the wiring) and what is
Memorable's claim (storage, sending, and what disable and forget erase).
The CHANGELOG entry is removed. This file has never carried an [Unreleased]
heading; every entry is a version and a date, written at release. The text is
in the pull request for whoever cuts the next one.
Three tests added: enable refuses and touches neither consent nor settings when
Memorable already holds the hook, status names that registration, and a foreign
UserPromptSubmit hook is not mistaken for Memorable's.
(cherry picked from commit e0899afa8c)
178 lines
5.7 KiB
Bash
Executable File
178 lines
5.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Opt-in wiring between gstack's Claude hook manager and Memorable.
|
|
set -u
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
SETTINGS_HOOK="$SCRIPT_DIR/gstack-settings-hook"
|
|
SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json}"
|
|
MEMORABLE_HOOK="$ROOT_DIR/hosts/claude/hooks/memorable-user-prompt-hook"
|
|
HOOK_SOURCE="gstack-memorable"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: gstack-memorable <enable|disable|status>
|
|
|
|
enable Enable Memorable, then register its Claude UserPromptSubmit hook
|
|
disable Remove the gstack hook, then disable Memorable
|
|
status Report Memorable CLI availability and hook registration
|
|
EOF
|
|
}
|
|
|
|
resolve_memorable() {
|
|
if [ -n "${MEMORABLE_BIN:-}" ]; then
|
|
[ -f "$MEMORABLE_BIN" ] && [ -x "$MEMORABLE_BIN" ] || return 1
|
|
printf '%s\n' "$MEMORABLE_BIN"
|
|
return 0
|
|
fi
|
|
|
|
if [ -n "${HOME:-}" ] && [ -f "$HOME/.memorable/bin/memorable" ] && [ -x "$HOME/.memorable/bin/memorable" ]; then
|
|
printf '%s\n' "$HOME/.memorable/bin/memorable"
|
|
return 0
|
|
fi
|
|
|
|
command -v memorable 2>/dev/null
|
|
}
|
|
|
|
require_memorable() {
|
|
MEMORABLE_CLI="$(resolve_memorable 2>/dev/null)" || {
|
|
echo "gstack-memorable: Memorable CLI not found; install memorable-cli or set MEMORABLE_BIN." >&2
|
|
return 1
|
|
}
|
|
[ -n "$MEMORABLE_CLI" ] || return 1
|
|
}
|
|
|
|
# Memorable's own installer registers the SAME UserPromptSubmit hook, under
|
|
# its own name and outside gstack's table. `memorable start`, `memorable setup`
|
|
# and `memorable install-hooks` all do it, and that is the documented way to
|
|
# install the CLI — so on most machines it is already there before gstack is
|
|
# asked. Registering ours beside it runs the same command twice on every
|
|
# prompt: context injected twice, and the session captured twice against the
|
|
# user's own extraction allowance.
|
|
#
|
|
# Matched on the command, not on a tag, for the same reason the hook table in
|
|
# gstack-settings-hook matches on command: Claude Code rewrites settings and
|
|
# private tags do not survive it.
|
|
memorable_own_hook() {
|
|
[ -f "$SETTINGS_FILE" ] || return 1
|
|
GSTACK_SETTINGS_PATH="$SETTINGS_FILE" bun -e '
|
|
const fs = require("fs");
|
|
let s = {};
|
|
try { s = JSON.parse(fs.readFileSync(process.env.GSTACK_SETTINGS_PATH, "utf8")); } catch { process.exit(1); }
|
|
const groups = (s.hooks && s.hooks.UserPromptSubmit) || [];
|
|
const ours = process.env.GSTACK_MEMORABLE_HOOK || "";
|
|
for (const g of groups) {
|
|
for (const h of (g.hooks || [])) {
|
|
const c = String(h.command || "");
|
|
if (c === ours || c.includes("memorable-user-prompt-hook")) continue;
|
|
if (/memorable/i.test(c) && /hook\s+user-prompt/.test(c)) { console.log(c); process.exit(0); }
|
|
}
|
|
}
|
|
process.exit(1);
|
|
' 2>/dev/null
|
|
}
|
|
|
|
hook_present() {
|
|
[ -x "$SETTINGS_HOOK" ] || return 1
|
|
if "$SETTINGS_HOOK" list-sources 2>/dev/null |
|
|
awk -F '\t' -v source="$HOOK_SOURCE" '
|
|
$1 == "UserPromptSubmit" && $2 == source { found = 1 }
|
|
END { exit(found ? 0 : 1) }
|
|
'; then
|
|
return 0
|
|
fi
|
|
|
|
# Claude Code may strip the private source tag when it rewrites settings.
|
|
# A no-op diff still proves that the canonical command itself is present.
|
|
"$SETTINGS_HOOK" diff-event \
|
|
--event UserPromptSubmit \
|
|
--command "$MEMORABLE_HOOK" \
|
|
--source "$HOOK_SOURCE" 2>/dev/null |
|
|
awk '
|
|
/^--- BEFORE$/ { section = 1; saw_before = 1; next }
|
|
/^--- AFTER$/ { section = 2; saw_after = 1; next }
|
|
section == 1 { before = before $0 "\n" }
|
|
section == 2 { after = after $0 "\n" }
|
|
END { exit(saw_before && saw_after && before == after ? 0 : 1) }
|
|
'
|
|
}
|
|
|
|
enable_memorable() {
|
|
local existing
|
|
require_memorable || return 1
|
|
[ -x "$SETTINGS_HOOK" ] || {
|
|
echo "gstack-memorable: missing hook manager: $SETTINGS_HOOK" >&2
|
|
return 1
|
|
}
|
|
[ -x "$MEMORABLE_HOOK" ] || {
|
|
echo "gstack-memorable: missing executable hook: $MEMORABLE_HOOK" >&2
|
|
return 1
|
|
}
|
|
|
|
existing="$(GSTACK_MEMORABLE_HOOK="$MEMORABLE_HOOK" memorable_own_hook)" && {
|
|
cat >&2 <<EOF
|
|
gstack-memorable: Memorable already registers this hook itself:
|
|
$existing
|
|
|
|
Registering gstack's as well would run it twice on every prompt: injected
|
|
twice, and the session captured twice against your extraction allowance.
|
|
|
|
Keep the one you have, or hand it to gstack: delete that entry from
|
|
$SETTINGS_FILE
|
|
and run this again. Memorable has no command to remove its own hook.
|
|
EOF
|
|
return 1
|
|
}
|
|
|
|
"$MEMORABLE_CLI" enable || return $?
|
|
"$SETTINGS_HOOK" ensure-event \
|
|
--event UserPromptSubmit \
|
|
--command "$MEMORABLE_HOOK" \
|
|
--source "$HOOK_SOURCE"
|
|
}
|
|
|
|
disable_memorable() {
|
|
local hook_rc=0 cli_rc=0
|
|
|
|
if [ -x "$SETTINGS_HOOK" ]; then
|
|
"$SETTINGS_HOOK" remove-source --source "$HOOK_SOURCE" || hook_rc=$?
|
|
else
|
|
echo "gstack-memorable: missing hook manager: $SETTINGS_HOOK" >&2
|
|
hook_rc=1
|
|
fi
|
|
|
|
if require_memorable; then
|
|
"$MEMORABLE_CLI" disable || cli_rc=$?
|
|
else
|
|
cli_rc=1
|
|
fi
|
|
|
|
[ "$hook_rc" -eq 0 ] && [ "$cli_rc" -eq 0 ]
|
|
}
|
|
|
|
status_memorable() {
|
|
local existing
|
|
if MEMORABLE_CLI="$(resolve_memorable 2>/dev/null)" && [ -n "$MEMORABLE_CLI" ]; then
|
|
printf 'Memorable CLI: available (%s)\n' "$MEMORABLE_CLI"
|
|
else
|
|
echo "Memorable CLI: unavailable"
|
|
fi
|
|
|
|
if hook_present; then
|
|
echo "Claude UserPromptSubmit hook: registered by gstack"
|
|
elif existing="$(GSTACK_MEMORABLE_HOOK="$MEMORABLE_HOOK" memorable_own_hook)"; then
|
|
printf 'Claude UserPromptSubmit hook: registered by Memorable itself (%s)\n' "$existing"
|
|
echo " gstack is not managing it; 'gstack-memorable enable' would double it."
|
|
else
|
|
echo "Claude UserPromptSubmit hook: not registered"
|
|
fi
|
|
}
|
|
|
|
case "${1:-}" in
|
|
enable) enable_memorable ;;
|
|
disable) disable_memorable ;;
|
|
status) status_memorable ;;
|
|
-h|--help|help) usage ;;
|
|
*) usage >&2; exit 1 ;;
|
|
esac
|