mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 23:37:53 +01:00
Bins, careful/freeze hooks, setup, upgrade migrations, browse/src, design, ios-qa daemon, lib and scripts resolve the state root through bin/gstack-state-root.sh (bash) or lib/state-root.ts (TS). Bins source the twin and stop with a reinstall message when it is missing; hooks source it and never spawn gstack-paths. browse/src/config.ts and lib/cso/state.ts delegate to resolveStateRoot. Analytics writers and readers move together so the usage log stays one file. gstack-uninstall deletes state only at ~/.gstack, refuses (exit 2) when it resolves to /, $HOME or an ancestor, the checkout or the git root, and leaves any other resolved root in place with the removal command. Fixtures that copy single bins now copy the twin.
414 lines
21 KiB
Bash
Executable File
414 lines
21 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-memorable — enable | disable | status for the Memorable recall bridge
|
|
# (hosts/claude/hooks/memorable-user-prompt-hook, a Claude Code UserPromptSubmit
|
|
# hook that hands each prompt to the third-party `memorable` CLI under gstack's
|
|
# consent key, receipts and trust envelope).
|
|
#
|
|
# Two independent facts make up the bridge's state, and this CLI is the only
|
|
# writer of both:
|
|
#
|
|
# registration (settings.json) gate (config.yaml memorable_recall)
|
|
# NONE ──enable──▶ GSTACK ──┐ off ──enable──▶ on
|
|
# ▲ │ Claude Code strips ▲ │
|
|
# └──── disable ──────────┘ the tag: still └─── disable ────┘
|
|
# (identity) GSTACK by identity
|
|
# VENDOR-OWN: `memorable install-hooks` registered its own hook. enable
|
|
# refuses (two entries would run the hook twice per prompt).
|
|
# Mismatches are reported by `status`, never silently repaired:
|
|
# gate on + NONE -> "gate on, no hook" (enable to fix)
|
|
# gate off + GSTACK -> "hook is inert" (disable removes it)
|
|
#
|
|
# What each verb hands to the vendor binary: nothing. enable/disable/status
|
|
# never execute `memorable`; they only check that it exists. The vendor's
|
|
# own consent (`memorable enable` / `disable` / `forget`) is yours to run.
|
|
#
|
|
# Style: `set -uo pipefail` WITHOUT -e (like bin/gstack-verify-gate). Every
|
|
# external call is checked explicitly with `|| return N`, so a failure is
|
|
# reported where it happens and partial states are never reported as success.
|
|
#
|
|
# Exit codes: 0 ok · 1 refused / usage · 3 settings.json unparseable ·
|
|
# 4 unexpected settings shape · 5 could not acquire the lock
|
|
# (the hook manager's own codes, passed through).
|
|
# Heredoc delivery guard (see bin/gstack-settings-hook for the rationale).
|
|
BASH_COMPAT=50
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
GSTACK_CONFIG="$SCRIPT_DIR/gstack-config"
|
|
. "$(dirname "$0")/gstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the gstack state root: $(dirname "$0")/gstack-state-root.sh is missing. fix: reinstall with ./setup or /gstack-upgrade (docs/state-root.md)" >&2; exit 1; }
|
|
gstack_state_root_select
|
|
STATE_DIR="$_gstack_sr_root"
|
|
SETTINGS_FILE="${GSTACK_SETTINGS_FILE:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json}"
|
|
|
|
HOOK_SOURCE="gstack-memorable"
|
|
CONFIG_KEY="memorable_recall"
|
|
SINK="memorable-recall"
|
|
HOOK_REL="hosts/claude/hooks/memorable-user-prompt-hook"
|
|
RESOLUTION_ORDER="GSTACK_MEMORABLE_BIN, MEMORABLE_BIN, ~/.memorable/bin/memorable, PATH"
|
|
# JavaScript RegExp (applied by gstack-settings-hook list-items to items no
|
|
# KNOWN_HOOKS row owns). Matches the vendor installer's own registration,
|
|
# verified against memorable-cli 0.5.18: "<HOME>/.memorable/bin/memorable" hook user-prompt
|
|
VENDOR_OWN_RE='[Mm]emorable.*hook\s+user-prompt'
|
|
|
|
# Canonical install root — the hook command MUST point at the stable install,
|
|
# never at the tree this CLI happens to run from (setup's phantom-hooks rule).
|
|
# Copied from setup:2481-2489; TODO D24 extracts a shared helper.
|
|
CANONICAL_GSTACK_ROOT="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills/gstack"
|
|
if [ ! -x "$CANONICAL_GSTACK_ROOT/bin/gstack-session-update" ] \
|
|
&& [ -x "$HOME/.claude/skills/gstack/bin/gstack-session-update" ]; then
|
|
CANONICAL_GSTACK_ROOT="$HOME/.claude/skills/gstack"
|
|
fi
|
|
HOOK_CMD_PATH="$CANONICAL_GSTACK_ROOT/$HOOK_REL"
|
|
# Mutations go through the CANONICAL hook manager so the code that registers
|
|
# is the code that will run. Every verb falls back to this tree's copy when the
|
|
# canonical one is missing (enable cannot get past compat_check then; disable
|
|
# and status must still work against a half-removed install).
|
|
SETTINGS_HOOK="$CANONICAL_GSTACK_ROOT/bin/gstack-settings-hook"
|
|
[ -x "$SETTINGS_HOOK" ] || SETTINGS_HOOK="$SCRIPT_DIR/gstack-settings-hook"
|
|
EGRESS_BIN="$CANONICAL_GSTACK_ROOT/bin/gstack-egress"
|
|
[ -x "$EGRESS_BIN" ] || EGRESS_BIN="$SCRIPT_DIR/gstack-egress"
|
|
|
|
# Platform detection copied from setup:76-79 (TODO D24). Windows support for
|
|
# this bridge is deferred whole (no process groups to contain the vendor).
|
|
IS_WINDOWS=0
|
|
case "${GSTACK_MEMORABLE_TEST_UNAME:-$(uname -s)}" in
|
|
MINGW*|MSYS*|CYGWIN*|Windows_NT) IS_WINDOWS=1 ;;
|
|
esac
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: gstack-memorable <enable|disable|status>
|
|
|
|
enable Register gstack's Memorable UserPromptSubmit hook (canonical path,
|
|
timeout 5) and set memorable_recall=on. Never runs \`memorable enable\`.
|
|
disable Set memorable_recall=off, remove gstack's hook entry (by identity,
|
|
tag or no tag), verify both. Never runs \`memorable disable\`.
|
|
status Read-only: vendor CLI, gate, registration, receipts, recent errors.
|
|
|
|
Vendor CLI resolution: $RESOLUTION_ORDER.
|
|
USAGE
|
|
}
|
|
|
|
_err() { printf 'gstack-memorable: %s\n' "$*" >&2; }
|
|
|
|
# ─── lock: one lifecycle transition at a time ────────────────────────────
|
|
LOCK_DIR="$STATE_DIR/locks/memorable-bridge.lock"
|
|
LOCK_STALE_S=30 # a holder older than this is a crashed writer
|
|
LOCK_TRIES=50 # x LOCK_SLEEP = the 5 s give-up
|
|
LOCK_SLEEP=0.1
|
|
LOCK_HELD=0
|
|
_lock_release() {
|
|
[ "$LOCK_HELD" -eq 1 ] || return 0
|
|
if [ "$(cat "$LOCK_DIR/owner" 2>/dev/null)" = "$$" ]; then rm -rf "$LOCK_DIR"; fi
|
|
LOCK_HELD=0
|
|
}
|
|
_lock_acquire() {
|
|
mkdir -p "$STATE_DIR/locks" 2>/dev/null || { _err "cannot create $STATE_DIR/locks (state directory not writable; nothing can be recorded there)"; return 5; }
|
|
local tries=0 mtime now stale judged moved owner_pid
|
|
while ! mkdir "$LOCK_DIR" 2>/dev/null; do
|
|
tries=$((tries + 1))
|
|
# Staleness from the directory's own mtime (set atomically by the holder's
|
|
# mkdir), never from a file written after it: a contender that looks in
|
|
# the gap between mkdir and bookkeeping must wait, not reclaim. GNU stat
|
|
# first, BSD stat second, garbage -> no takeover (same idiom as
|
|
# bin/gstack-settings-hook).
|
|
mtime="$(stat -c %Y "$LOCK_DIR" 2>/dev/null || stat -f %m "$LOCK_DIR" 2>/dev/null || echo "")"
|
|
case "$mtime" in *[!0-9]*|"") mtime="" ;; esac
|
|
now="$(date +%s)"
|
|
# A holder whose recorded pid is still alive is slow, not crashed: wait.
|
|
owner_pid="$(cat "$LOCK_DIR/owner" 2>/dev/null || echo "")"
|
|
case "$owner_pid" in *[!0-9]*|"") owner_pid="" ;; esac
|
|
if [ -n "$owner_pid" ] && kill -0 "$owner_pid" 2>/dev/null; then mtime=""; fi
|
|
if [ -n "$mtime" ] && [ $((now - mtime)) -gt "$LOCK_STALE_S" ]; then
|
|
# Atomic rename: exactly one contender reclaims a stale lock; the loser
|
|
# loops and re-contends against the winner's fresh mkdir. The inode
|
|
# check closes the gap between judging and renaming: a contender that
|
|
# judged the OLD directory stale must not carry off the FRESH one a
|
|
# faster contender just created in its place. A rename that fails
|
|
# (locks dir not writable by this user) falls through to the give-up
|
|
# counter below instead of spinning.
|
|
judged="$(stat -c %i "$LOCK_DIR" 2>/dev/null || stat -f %i "$LOCK_DIR" 2>/dev/null || echo "")"
|
|
stale="$LOCK_DIR.stale.$$-$RANDOM"
|
|
if mv "$LOCK_DIR" "$stale" 2>/dev/null; then
|
|
moved="$(stat -c %i "$stale" 2>/dev/null || stat -f %i "$stale" 2>/dev/null || echo "")"
|
|
if [ -n "$judged" ] && [ "$moved" = "$judged" ]; then
|
|
rm -rf "$stale" 2>/dev/null || true
|
|
else
|
|
# Not the directory we judged: a fresh holder's lock. Put it back.
|
|
mv "$stale" "$LOCK_DIR" 2>/dev/null || _err "lock bookkeeping: could not restore a fresh lock moved aside at $stale"
|
|
fi
|
|
continue
|
|
fi
|
|
fi
|
|
if [ "$tries" -ge "$LOCK_TRIES" ]; then _err "another gstack-memorable is running (lock $LOCK_DIR; stale but not reclaimable if older than ${LOCK_STALE_S}s); try again"; return 5; fi
|
|
sleep "$LOCK_SLEEP"
|
|
done
|
|
printf '%s\n' "$$" > "$LOCK_DIR/owner"
|
|
LOCK_HELD=1
|
|
trap _lock_release EXIT
|
|
}
|
|
|
|
# ─── probes (read-only) ──────────────────────────────────────────────────
|
|
resolve_memorable() {
|
|
local override="${GSTACK_MEMORABLE_BIN:-${MEMORABLE_BIN:-}}"
|
|
if [ -n "$override" ]; then
|
|
override="${override%\"}"; override="${override#\"}"
|
|
case "$override" in
|
|
/*) [ -f "$override" ] && [ -x "$override" ] && { printf '%s\n' "$override"; return 0; } ;;
|
|
*) command -v "$override" 2>/dev/null && return 0 ;;
|
|
esac
|
|
return 1 # an explicit override that does not resolve is an error, never a fall-through
|
|
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
|
|
}
|
|
|
|
# Gate value or "unknown" (gstack-config missing/failed).
|
|
gate_value() {
|
|
local v
|
|
v="$("$GSTACK_CONFIG" get "$CONFIG_KEY" 2>/dev/null)" || { echo unknown; return 0; }
|
|
printf '%s\n' "${v:-off}"
|
|
}
|
|
|
|
# Registration state via the hook manager's identity view. Sets:
|
|
# REG_STATE none | gstack | vendor | both | unparseable | shape | unreadable
|
|
# REG_GSTACK newline-separated JSON string literals of gstack-owned commands
|
|
# REG_VENDOR newline-separated JSON string literals of the vendor's own commands
|
|
REG_STATE=""; REG_GSTACK=""; REG_VENDOR=""
|
|
registration_state() {
|
|
local rc
|
|
REG_GSTACK="$("$SETTINGS_HOOK" list-items --event UserPromptSubmit --owned-by "$HOOK_SOURCE" 2>/dev/null)"; rc=$?
|
|
case "$rc" in
|
|
0) ;;
|
|
3) REG_STATE="unparseable"; return 0 ;;
|
|
4) REG_STATE="shape"; return 0 ;;
|
|
*) REG_STATE="unreadable"; return 0 ;;
|
|
esac
|
|
REG_VENDOR="$("$SETTINGS_HOOK" list-items --event UserPromptSubmit --command-regex "$VENDOR_OWN_RE" 2>/dev/null)"; rc=$?
|
|
[ "$rc" -eq 0 ] || { REG_STATE="unreadable"; return 0; }
|
|
if [ -n "$REG_GSTACK" ] && [ -n "$REG_VENDOR" ]; then REG_STATE="both"
|
|
elif [ -n "$REG_GSTACK" ]; then REG_STATE="gstack"
|
|
elif [ -n "$REG_VENDOR" ]; then REG_STATE="vendor"
|
|
else REG_STATE="none"; fi
|
|
}
|
|
_reg_exit_code() {
|
|
case "$REG_STATE" in unparseable) echo 3 ;; shape) echo 4 ;; *) echo 1 ;; esac
|
|
}
|
|
_reg_problem_text() {
|
|
case "$REG_STATE" in
|
|
unparseable) echo "$SETTINGS_FILE is not valid JSON (fix or restore it; see gstack-settings-hook rollback)" ;;
|
|
shape) echo "$SETTINGS_FILE has an unexpected shape under hooks.UserPromptSubmit (not an array)" ;;
|
|
unreadable) echo "the hook manager could not read $SETTINGS_FILE" ;;
|
|
esac
|
|
}
|
|
|
|
# The canonical install must carry THIS bridge: a worktree CLI registering an
|
|
# older hook at the stable path would run code without the gate or receipts.
|
|
compat_check() {
|
|
[ -x "$HOOK_CMD_PATH" ] || { _err "no stable install carries the bridge hook at $HOOK_CMD_PATH; run ./setup (or /gstack-upgrade) first"; return 1; }
|
|
[ -f "$HOOK_CMD_PATH.ts" ] || { _err "the stable install at $CANONICAL_GSTACK_ROOT predates this bridge (no memorable-user-prompt-hook.ts); run ./setup first"; return 1; }
|
|
local here there
|
|
here="$(cat "$ROOT_DIR/VERSION" 2>/dev/null)"; there="$(cat "$CANONICAL_GSTACK_ROOT/VERSION" 2>/dev/null)"
|
|
if [ -n "$here" ] && [ "$here" != "$there" ]; then
|
|
_err "the stable install at $CANONICAL_GSTACK_ROOT is version '${there:-unknown}' but this tree is '$here'; run ./setup so the registered hook is the code that will run"
|
|
return 1
|
|
fi
|
|
# Captured, not piped: under pipefail the probe's own non-zero exit would
|
|
# mask a matching grep and let an old hook manager through.
|
|
local probe
|
|
probe="$("$SETTINGS_HOOK" list-items 2>&1)" || true
|
|
if printf '%s' "$probe" | grep -q "Unknown action"; then
|
|
_err "the stable install's hook manager does not know list-items; run ./setup first"; return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# ─── enable ──────────────────────────────────────────────────────────────
|
|
enable_bridge() {
|
|
local vendor prior_gate ensure_out ensure_rc verb
|
|
_lock_acquire || return $?
|
|
[ -x "$SETTINGS_HOOK" ] || { _err "missing hook manager: $SETTINGS_HOOK"; return 1; }
|
|
[ -x "$GSTACK_CONFIG" ] || { _err "missing $GSTACK_CONFIG"; return 1; }
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then
|
|
_err "Windows is not supported by the Memorable bridge yet (no way to contain the vendor process); tracked in TODOS.md: Windows support for the Memorable bridge (D21)"
|
|
return 1
|
|
fi
|
|
vendor="$(resolve_memorable)" || { _err "Memorable CLI not found (checked $RESOLUTION_ORDER). Install it yourself: npm i -g memorable-cli. gstack never installs it."; return 1; }
|
|
compat_check || return 1
|
|
|
|
prior_gate="$(gate_value)"
|
|
registration_state
|
|
case "$REG_STATE" in
|
|
unparseable|shape|unreadable) _err "cannot read the current registration: $(_reg_problem_text)"; return "$(_reg_exit_code)" ;;
|
|
vendor|both)
|
|
cat >&2 <<REFUSE
|
|
gstack-memorable: Memorable already registers this hook itself:
|
|
$(printf '%s\n' "$REG_VENDOR" | sed 's/^/ /')
|
|
|
|
Registering gstack's as well would run the hook twice on every prompt:
|
|
injected twice, and the session captured twice against your 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 that removes its own hook.
|
|
REFUSE
|
|
return 1 ;;
|
|
esac
|
|
|
|
ensure_out="$("$SETTINGS_HOOK" ensure-event --event UserPromptSubmit --command "$HOOK_CMD_PATH" --source "$HOOK_SOURCE" --timeout 5 2>&1)"; ensure_rc=$?
|
|
if [ "$ensure_rc" -ne 0 ]; then
|
|
_err "settings hook update failed: $(printf '%s\n' "$ensure_out" | head -1): run $SETTINGS_HOOK manually (nothing changed; the gate is still '$prior_gate')"
|
|
return "$ensure_rc"
|
|
fi
|
|
case "$ensure_out" in
|
|
*unchanged*) verb="unchanged" ;;
|
|
*re-pointed*) verb="re-pointed" ;;
|
|
*) verb="registered" ;;
|
|
esac
|
|
|
|
if ! "$GSTACK_CONFIG" set "$CONFIG_KEY" on >/dev/null 2>&1; then
|
|
# Restore the CAPTURED prior state, never an assumed one: a registration
|
|
# that predates this run stays; the gate goes back to what it was.
|
|
if [ "$verb" = "registered" ] && [ "$REG_STATE" = "none" ]; then
|
|
"$SETTINGS_HOOK" remove-source --source "$HOOK_SOURCE" >/dev/null 2>&1 || true
|
|
fi
|
|
case "$prior_gate" in on|off) "$GSTACK_CONFIG" set "$CONFIG_KEY" "$prior_gate" >/dev/null 2>&1 || true ;; esac
|
|
_err "could not record consent (gstack-config set $CONFIG_KEY on failed); a registration made by this run was removed, a pre-existing one was kept; gate is '$prior_gate'"
|
|
return 1
|
|
fi
|
|
|
|
cat <<DONE
|
|
gstack-memorable: enabled.
|
|
hook: $verb ($HOOK_CMD_PATH, timeout 5 s, source $HOOK_SOURCE)
|
|
consent: $CONFIG_KEY=on (gstack's gate; revoke: gstack-memorable disable)
|
|
vendor: $vendor
|
|
|
|
What gstack hands to that binary on every prompt: Claude Code's UserPromptSubmit
|
|
JSON (session_id, cwd, transcript_path, prompt), unless it carries a HIGH-tier
|
|
credential shape or the repo's trust policy is deny/read-only. The binary runs
|
|
with your privileges in an allowlisted environment and its own process group.
|
|
Each hand-off is receipted first: gstack-egress list --sink $SINK
|
|
What the binary then sends is Memorable's claim, not gstack's.
|
|
|
|
Claude Code picks up the new hook automatically within a few seconds; if it does
|
|
not fire, restart the session. Verify with: gstack-memorable status
|
|
Memorable's own capture consent is separate and yours to run or inspect:
|
|
memorable status | memorable enable | memorable disable | memorable forget
|
|
DONE
|
|
}
|
|
|
|
# ─── disable ─────────────────────────────────────────────────────────────
|
|
disable_bridge() {
|
|
local gate_rc=0 remove_rc=0 remove_out="" gate_after
|
|
_lock_acquire || return $?
|
|
# Gate first: the hook reads it on every prompt, so consent is revoked
|
|
# immediately even if the registration removal below fails. A missing
|
|
# gstack-config (half-removed install) is reported, and the removal still
|
|
# runs: the hook fails closed without gstack-config, the entry must still go.
|
|
if [ -x "$GSTACK_CONFIG" ]; then
|
|
"$GSTACK_CONFIG" set "$CONFIG_KEY" off >/dev/null 2>&1 || gate_rc=$?
|
|
else
|
|
_err "missing $GSTACK_CONFIG"; gate_rc=1
|
|
fi
|
|
if [ -x "$SETTINGS_HOOK" ]; then
|
|
remove_out="$("$SETTINGS_HOOK" remove-source --source "$HOOK_SOURCE" 2>&1)" || remove_rc=$?
|
|
else
|
|
_err "missing hook manager: $SETTINGS_HOOK"; remove_rc=1
|
|
fi
|
|
# Verify BOTH resulting states; report each, never a blended "done".
|
|
gate_after="$(gate_value)"
|
|
registration_state
|
|
local ok=0
|
|
if [ "$gate_rc" -eq 0 ] && [ "$gate_after" = "off" ]; then
|
|
echo "consent: $CONFIG_KEY=off"
|
|
else
|
|
_err "consent: could not set $CONFIG_KEY=off (gstack-config exit $gate_rc, value now '$gate_after')"; ok=1
|
|
fi
|
|
case "$REG_STATE" in
|
|
none|vendor)
|
|
if [ "$remove_rc" -eq 0 ]; then echo "hook: removed (${remove_out##*OK: })"
|
|
else echo "hook: no gstack entry remains (the hook manager exited $remove_rc: $(printf '%s\n' "$remove_out" | head -1))"; fi ;;
|
|
gstack|both)
|
|
_err "hook: a gstack-owned entry survived in $SETTINGS_FILE:"; printf '%s\n' "$REG_GSTACK" | sed 's/^/ /' >&2; ok=1 ;;
|
|
*) _err "hook: cannot verify removal: $(_reg_problem_text)"; ok=$(_reg_exit_code) ;;
|
|
esac
|
|
[ "$remove_rc" -eq 0 ] || { [ "$remove_rc" -ge 3 ] && ok=$remove_rc; }
|
|
if resolve_memorable >/dev/null 2>&1; then
|
|
echo "Memorable's own consent is unchanged; to stop or erase capture: memorable disable | memorable forget"
|
|
else
|
|
echo "Memorable CLI not found: nothing of the vendor's to revoke here (gstack's hook entry is gone)"
|
|
fi
|
|
echo "In-flight prompts that already passed the gate complete; the next prompt is off."
|
|
return "$ok"
|
|
}
|
|
|
|
# ─── status (read-only; never executes the vendor) ───────────────────────
|
|
status_bridge() {
|
|
local vendor gate n
|
|
if ! command -v bun >/dev/null 2>&1; then
|
|
echo "bun: missing (the hook manager and the hook itself need bun; install bun first)"
|
|
fi
|
|
if vendor="$(resolve_memorable)"; then
|
|
echo "Memorable CLI: available ($vendor); tested against the memorable-cli 0.5.18 hook contract"
|
|
else
|
|
echo "Memorable CLI: not found (checked $RESOLUTION_ORDER)"
|
|
fi
|
|
gate="$(gate_value)"
|
|
echo "memorable_recall: $gate"
|
|
registration_state
|
|
case "$REG_STATE" in
|
|
none) echo "Claude UserPromptSubmit hook: not registered" ;;
|
|
gstack) echo "Claude UserPromptSubmit hook: registered by gstack"; printf '%s\n' "$REG_GSTACK" | sed 's/^/ /' ;;
|
|
vendor) echo "Claude UserPromptSubmit hook: registered by Memorable itself"; printf '%s\n' "$REG_VENDOR" | sed 's/^/ /'
|
|
echo " gstack is not managing it; 'gstack-memorable enable' would refuse (it would double the hook)." ;;
|
|
both) echo "Claude UserPromptSubmit hook: registered by BOTH gstack and Memorable (the hook runs twice per prompt; remove one)"
|
|
printf '%s\n' "$REG_GSTACK" "$REG_VENDOR" | sed 's/^/ /' ;;
|
|
*) echo "Claude UserPromptSubmit hook: unknown ($(_reg_problem_text))" ;;
|
|
esac
|
|
if [ "$gate" = "on" ] && [ "$REG_STATE" = "none" ]; then echo "mismatch: gate on, no hook registered (run: gstack-memorable enable)"; fi
|
|
if [ "$gate" != "on" ] && { [ "$REG_STATE" = "gstack" ] || [ "$REG_STATE" = "both" ]; }; then echo "mismatch: hook registered but gate is '$gate' (hook is inert; run: gstack-memorable disable to remove it)"; fi
|
|
if [ "$IS_WINDOWS" -eq 1 ]; then echo "platform: Windows is not supported by this bridge yet (TODOS.md: Windows support for the Memorable bridge, D21)"; fi
|
|
if [ -x "$EGRESS_BIN" ] && command -v bun >/dev/null 2>&1; then
|
|
# Count the filtered array, not a formatting artefact of the pretty-printed
|
|
# JSON; a failed query is reported as unknown, never as an empty history.
|
|
local egress_json
|
|
if egress_json="$("$EGRESS_BIN" list --sink "$SINK" --json 2>/dev/null)"; then
|
|
n="$(printf '%s' "$egress_json" | bun -e 'const a=JSON.parse(require("fs").readFileSync(0,"utf8")||"[]");console.log(Array.isArray(a)?a.length:0)' 2>/dev/null)"
|
|
case "$n" in *[!0-9]*|"") n="unknown (could not parse gstack-egress output)" ;; esac
|
|
else
|
|
n="unknown (gstack-egress list failed; run it yourself)"
|
|
fi
|
|
echo "receipts: $n for sink $SINK (gstack-egress list --sink $SINK)"
|
|
# Same root lib/egress-receipt.ts resolveEgressHome uses (the shared state-root chain).
|
|
local ledger size
|
|
ledger="$STATE_DIR/security/egress.jsonl"
|
|
if [ -f "$ledger" ]; then
|
|
size="$(wc -c < "$ledger" | tr -d ' ')"
|
|
if [ "${size:-0}" -gt 26214400 ]; then
|
|
echo "ledger: $ledger ($((size / 1048576)) MiB; above the 25 MiB warning, rotation is a filed TODO: this sink appends two lines per prompt)"
|
|
else
|
|
echo "ledger: $ledger ($(( (size + 1023) / 1024 )) KiB; this sink appends two lines per prompt)"
|
|
fi
|
|
fi
|
|
fi
|
|
if [ -f "$STATE_DIR/hook-errors.log" ]; then
|
|
n="$(grep -c 'memorable-user-prompt-hook' "$STATE_DIR/hook-errors.log" 2>/dev/null || true)"
|
|
if [ "${n:-0}" -gt 0 ]; then
|
|
echo "recent hook errors ($STATE_DIR/hook-errors.log):"
|
|
grep 'memorable-user-prompt-hook' "$STATE_DIR/hook-errors.log" | tail -3 | sed 's/^/ /'
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
case "${1:-}" in
|
|
enable) enable_bridge ;;
|
|
disable) disable_bridge ;;
|
|
status) status_bridge ;;
|
|
-h|--help|help) usage ;;
|
|
*) usage >&2; exit 1 ;;
|
|
esac
|