Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions plugins/claude-code/hooks/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,68 @@ _json_encode_str() {
return 0
}

# _resolve_symlinks <path>
# Follow a symlink chain to its target without `readlink -f`, which BSD
# readlink (macOS) does not support. Relative link targets resolve against
# the link's directory; a hop cap guards against cycles. The directory part
# is canonicalized with `pwd -P` so `..` segments and directory symlinks
# collapse the way GNU `readlink -f` would collapse them.
_resolve_symlinks() {
local target="$1" link dir hops=0
while [ -L "$target" ] && [ "$hops" -lt 40 ]; do
link=$(readlink "$target" 2>/dev/null) || break
case "$link" in
/*) target="$link" ;;
*) target="$(dirname "$target")/$link" ;;
esac
hops=$((hops + 1))
done
dir=$(cd "$(dirname "$target")" 2>/dev/null && pwd -P) || { printf '%s' "$target"; return 0; }
printf '%s/%s' "$dir" "${target##*/}"
}

# Resolve the installed memsearch version from its dist-info directory name.
# Callers already spend one CLI start reading config; asking the CLI for
# `--version` spawns a second Python interpreter (~0.3s warm, several seconds
# cold) purely to render a status string. Prints nothing when no dist-info is
# discoverable (uvx, editable installs) so callers can fall back to the CLI.
_installed_version_from_dist_info() {
local bin real candidate
bin=$(command -v memsearch 2>/dev/null) || return 0
[ -n "$bin" ] || return 0
real=$(_resolve_symlinks "$bin")
for candidate in "${real%/bin/memsearch}"/lib/python*/site-packages/memsearch-*.dist-info; do
[ -d "$candidate" ] || continue
candidate=${candidate##*/memsearch-}
candidate=${candidate%.dist-info}
# Require a version-shaped string so a sibling distribution whose name
# starts with "memsearch-" cannot be mistaken for the package itself.
case "$candidate" in
[0-9]*) printf '%s' "$candidate"; return 0 ;;
esac
done
}

# Latest memsearch version on PyPI, cached for 24h. Keeps the update hint
# current without making every session start wait on a network round trip: an
# unreachable PyPI otherwise costs the full curl timeout on every start.
# Prints nothing when the lookup fails.
_pypi_latest_version() {
local cache="$HOME/.memsearch/.pypi-latest" latest json
# A cache file younger than a day is authoritative even when it is empty: an
# empty file records a lookup that failed, so an offline machine stops
# re-paying the curl timeout on every session start.
if [ -n "$(find "$cache" -mtime -1 2>/dev/null)" ]; then
cat "$cache" 2>/dev/null || true
return 0
fi
json=$(curl -s --max-time 2 https://pypi.org/pypi/memsearch/json 2>/dev/null || true)
latest=$(_json_val "$json" "info.version" "")
mkdir -p "$(dirname "$cache")" 2>/dev/null || true
printf '%s' "$latest" > "$cache" 2>/dev/null || true
printf '%s' "$latest"
}

# Return a concise user-facing warning when the persisted index state says
# search may be stale. Detailed diagnosis stays in the memory-config skill.
index_state_warning() {
Expand Down
11 changes: 6 additions & 5 deletions plugins/claude-code/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ if [ -n "$MEMSEARCH_CMD" ]; then
MILVUS_URI=$($MEMSEARCH_CMD config get milvus.uri 2>/dev/null || echo "")
fi
# "memsearch, version 0.1.10" → "0.1.10"
VERSION=$($MEMSEARCH_CMD --version 2>/dev/null | sed 's/.*version //' || echo "")
VERSION=$(_installed_version_from_dist_info)
[ -n "$VERSION" ] || VERSION=$($MEMSEARCH_CMD --version 2>/dev/null | sed 's/.*version //' || echo "")
fi

# Determine required API key for the configured provider
Expand All @@ -77,17 +78,17 @@ if [ -n "$REQUIRED_KEY" ] && [ -z "${!REQUIRED_KEY:-}" ]; then
fi
fi

# Check PyPI for newer version (2s timeout, non-blocking on failure)
# Check PyPI for a newer version. The lookup is cached, so a slow or
# unreachable index does not delay every session start.
UPDATE_HINT=""
if [ -n "$VERSION" ]; then
_PYPI_JSON=$(curl -s --max-time 2 https://pypi.org/pypi/memsearch/json 2>/dev/null || true)
LATEST=$(_json_val "$_PYPI_JSON" "info.version" "")
LATEST=$(_pypi_latest_version)
if [ -n "$LATEST" ] && [ "$LATEST" != "$VERSION" ]; then
# Detect install method to suggest the right upgrade command.
_MS_BIN=$(command -v memsearch 2>/dev/null || echo "")
_MS_REAL=""
if [ -n "$_MS_BIN" ]; then
_MS_REAL=$(readlink -f "$_MS_BIN" 2>/dev/null || python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$_MS_BIN" 2>/dev/null || echo "$_MS_BIN")
_MS_REAL=$(_resolve_symlinks "$_MS_BIN")
fi
if [[ "$MEMSEARCH_CMD" == *"uvx"* ]]; then
UPGRADE_CMD="uvx --upgrade --from 'memsearch[onnx]' memsearch --version"
Expand Down
62 changes: 62 additions & 0 deletions plugins/codex/hooks/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,68 @@ _json_encode_str() {
return 0
}

# _resolve_symlinks <path>
# Follow a symlink chain to its target without `readlink -f`, which BSD
# readlink (macOS) does not support. Relative link targets resolve against
# the link's directory; a hop cap guards against cycles. The directory part
# is canonicalized with `pwd -P` so `..` segments and directory symlinks
# collapse the way GNU `readlink -f` would collapse them.
_resolve_symlinks() {
local target="$1" link dir hops=0
while [ -L "$target" ] && [ "$hops" -lt 40 ]; do
link=$(readlink "$target" 2>/dev/null) || break
case "$link" in
/*) target="$link" ;;
*) target="$(dirname "$target")/$link" ;;
esac
hops=$((hops + 1))
done
dir=$(cd "$(dirname "$target")" 2>/dev/null && pwd -P) || { printf '%s' "$target"; return 0; }
printf '%s/%s' "$dir" "${target##*/}"
}

# Resolve the installed memsearch version from its dist-info directory name.
# Callers already spend one CLI start reading config; asking the CLI for
# `--version` spawns a second Python interpreter (~0.3s warm, several seconds
# cold) purely to render a status string. Prints nothing when no dist-info is
# discoverable (uvx, editable installs) so callers can fall back to the CLI.
_installed_version_from_dist_info() {
local bin real candidate
bin=$(command -v memsearch 2>/dev/null) || return 0
[ -n "$bin" ] || return 0
real=$(_resolve_symlinks "$bin")
for candidate in "${real%/bin/memsearch}"/lib/python*/site-packages/memsearch-*.dist-info; do
[ -d "$candidate" ] || continue
candidate=${candidate##*/memsearch-}
candidate=${candidate%.dist-info}
# Require a version-shaped string so a sibling distribution whose name
# starts with "memsearch-" cannot be mistaken for the package itself.
case "$candidate" in
[0-9]*) printf '%s' "$candidate"; return 0 ;;
esac
done
}

# Latest memsearch version on PyPI, cached for 24h. Keeps the update hint
# current without making every session start wait on a network round trip: an
# unreachable PyPI otherwise costs the full curl timeout on every start.
# Prints nothing when the lookup fails.
_pypi_latest_version() {
local cache="$HOME/.memsearch/.pypi-latest" latest json
# A cache file younger than a day is authoritative even when it is empty: an
# empty file records a lookup that failed, so an offline machine stops
# re-paying the curl timeout on every session start.
if [ -n "$(find "$cache" -mtime -1 2>/dev/null)" ]; then
cat "$cache" 2>/dev/null || true
return 0
fi
json=$(curl -s --max-time 2 https://pypi.org/pypi/memsearch/json 2>/dev/null || true)
latest=$(_json_val "$json" "info.version" "")
mkdir -p "$(dirname "$cache")" 2>/dev/null || true
printf '%s' "$latest" > "$cache" 2>/dev/null || true
printf '%s' "$latest"
}

# Return a concise user-facing warning when the persisted index state says
# search may be stale. Detailed diagnosis stays in the memory-config skill.
index_state_warning() {
Expand Down
11 changes: 6 additions & 5 deletions plugins/codex/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ if memsearch_available; then
MODEL=$(_memsearch config get embedding.model 2>/dev/null || echo "")
MILVUS_URI=$(_memsearch config get milvus.uri 2>/dev/null || echo "")
# "memsearch, version 0.1.10" → "0.1.10"
VERSION=$(_memsearch --version 2>/dev/null | sed 's/.*version //' || echo "")
VERSION=$(_installed_version_from_dist_info)
[ -n "$VERSION" ] || VERSION=$(_memsearch --version 2>/dev/null | sed 's/.*version //' || echo "")
fi

# Determine required API key for the configured provider
Expand Down Expand Up @@ -61,17 +62,17 @@ if [ -n "$REQUIRED_KEY" ] && [ -z "${!REQUIRED_KEY:-}" ]; then
fi
fi

# Check PyPI for newer version (2s timeout, non-blocking on failure)
# Check PyPI for a newer version. The lookup is cached, so a slow or
# unreachable index does not delay every session start.
UPDATE_HINT=""
if [ -n "$VERSION" ]; then
_PYPI_JSON=$(curl -s --max-time 2 https://pypi.org/pypi/memsearch/json 2>/dev/null || true)
LATEST=$(_json_val "$_PYPI_JSON" "info.version" "")
LATEST=$(_pypi_latest_version)
if [ -n "$LATEST" ] && [ "$LATEST" != "$VERSION" ]; then
# Detect install method to suggest the right upgrade command.
_MS_BIN=$(command -v memsearch 2>/dev/null || echo "")
_MS_REAL=""
if [ -n "$_MS_BIN" ]; then
_MS_REAL=$(readlink -f "$_MS_BIN" 2>/dev/null || python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$_MS_BIN" 2>/dev/null || echo "$_MS_BIN")
_MS_REAL=$(_resolve_symlinks "$_MS_BIN")
fi
if [ "${MEMSEARCH_CMD[0]:-}" = "uvx" ]; then
UPGRADE_CMD="uvx --upgrade --from 'memsearch[onnx]' memsearch --version"
Expand Down
Loading
Loading