mirror-mind 0.39.4 → 0.39.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/log-assistant-reply.sh +36 -4
- package/.claude/hooks/log-session-end.sh +36 -4
- package/.claude/hooks/log-user-prompt.sh +36 -4
- package/.claude/hooks/mirror-inject.sh +36 -4
- package/.claude/hooks/session-start.sh +36 -4
- package/.pi/extensions/mirror-logger.ts +1 -1
- package/docs/releases/index.md +1 -0
- package/docs/releases/v0.39.2.md +10 -3
- package/docs/releases/v0.39.5.md +146 -0
- package/package.json +1 -1
- package/plugins/mirror-mind/.claude-plugin/plugin.json +2 -2
- package/plugins/mirror-mind/agy/pre-invocation.sh +45 -4
- package/plugins/mirror-mind/agy/session-start.sh +45 -4
- package/plugins/mirror-mind/agy/stop.sh +45 -4
- package/plugins/mirror-mind/hooks/log-assistant-reply.sh +36 -4
- package/plugins/mirror-mind/hooks/log-session-end.sh +36 -4
- package/plugins/mirror-mind/hooks/log-user-prompt.sh +36 -4
- package/plugins/mirror-mind/hooks/mirror-inject.sh +36 -4
- package/plugins/mirror-mind/hooks/session-start.sh +36 -4
- package/plugins/mirror-mind/mcp_config.json +1 -1
- package/plugins/mirror-mind/plugin.json +2 -2
- package/ts/src/frontDoor/runtimeRoute.ts +2 -8
- package/ts/src/guards/claudePlugin.ts +22 -1
- package/ts/src/runtime/diagnose.ts +25 -6
|
@@ -31,8 +31,13 @@ env_value() {
|
|
|
31
31
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
32
32
|
printf '%s' "$value"
|
|
33
33
|
}
|
|
34
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
35
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
36
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
37
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
38
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
34
39
|
note() {
|
|
35
|
-
local home user
|
|
40
|
+
local home user seen today
|
|
36
41
|
home="$(env_value MIRROR_HOME)"
|
|
37
42
|
user="$(env_value MIRROR_USER)"
|
|
38
43
|
if [ -n "$home" ]; then
|
|
@@ -43,6 +48,11 @@ note() {
|
|
|
43
48
|
else
|
|
44
49
|
return 0
|
|
45
50
|
fi
|
|
51
|
+
if [ "${2:-}" = daily ]; then
|
|
52
|
+
today="$(date -u +%Y-%m-%d)"
|
|
53
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
54
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
55
|
+
fi
|
|
46
56
|
mkdir -p "$home" 2>/dev/null \
|
|
47
57
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
48
58
|
}
|
|
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
|
|
|
58
68
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
59
69
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
60
70
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
#
|
|
72
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
73
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
74
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
75
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
76
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
77
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
78
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
79
|
+
NODE=""
|
|
80
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
81
|
+
refused=""
|
|
82
|
+
case "$MIRROR_NODE" in
|
|
83
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
84
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
85
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
86
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
87
|
+
else NODE="$MIRROR_NODE"
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
*) refused="relative" ;;
|
|
91
|
+
esac
|
|
92
|
+
if [ -n "$refused" ]; then
|
|
93
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
94
|
+
fi
|
|
64
95
|
fi
|
|
96
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
65
97
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
66
98
|
[ -n "$NODE" ] && break
|
|
67
99
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -31,8 +31,13 @@ env_value() {
|
|
|
31
31
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
32
32
|
printf '%s' "$value"
|
|
33
33
|
}
|
|
34
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
35
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
36
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
37
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
38
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
34
39
|
note() {
|
|
35
|
-
local home user
|
|
40
|
+
local home user seen today
|
|
36
41
|
home="$(env_value MIRROR_HOME)"
|
|
37
42
|
user="$(env_value MIRROR_USER)"
|
|
38
43
|
if [ -n "$home" ]; then
|
|
@@ -43,6 +48,11 @@ note() {
|
|
|
43
48
|
else
|
|
44
49
|
return 0
|
|
45
50
|
fi
|
|
51
|
+
if [ "${2:-}" = daily ]; then
|
|
52
|
+
today="$(date -u +%Y-%m-%d)"
|
|
53
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
54
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
55
|
+
fi
|
|
46
56
|
mkdir -p "$home" 2>/dev/null \
|
|
47
57
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
48
58
|
}
|
|
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
|
|
|
58
68
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
59
69
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
60
70
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
#
|
|
72
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
73
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
74
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
75
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
76
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
77
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
78
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
79
|
+
NODE=""
|
|
80
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
81
|
+
refused=""
|
|
82
|
+
case "$MIRROR_NODE" in
|
|
83
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
84
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
85
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
86
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
87
|
+
else NODE="$MIRROR_NODE"
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
*) refused="relative" ;;
|
|
91
|
+
esac
|
|
92
|
+
if [ -n "$refused" ]; then
|
|
93
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
94
|
+
fi
|
|
64
95
|
fi
|
|
96
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
65
97
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
66
98
|
[ -n "$NODE" ] && break
|
|
67
99
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -31,8 +31,13 @@ env_value() {
|
|
|
31
31
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
32
32
|
printf '%s' "$value"
|
|
33
33
|
}
|
|
34
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
35
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
36
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
37
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
38
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
34
39
|
note() {
|
|
35
|
-
local home user
|
|
40
|
+
local home user seen today
|
|
36
41
|
home="$(env_value MIRROR_HOME)"
|
|
37
42
|
user="$(env_value MIRROR_USER)"
|
|
38
43
|
if [ -n "$home" ]; then
|
|
@@ -43,6 +48,11 @@ note() {
|
|
|
43
48
|
else
|
|
44
49
|
return 0
|
|
45
50
|
fi
|
|
51
|
+
if [ "${2:-}" = daily ]; then
|
|
52
|
+
today="$(date -u +%Y-%m-%d)"
|
|
53
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
54
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
55
|
+
fi
|
|
46
56
|
mkdir -p "$home" 2>/dev/null \
|
|
47
57
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
48
58
|
}
|
|
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
|
|
|
58
68
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
59
69
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
60
70
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
#
|
|
72
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
73
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
74
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
75
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
76
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
77
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
78
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
79
|
+
NODE=""
|
|
80
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
81
|
+
refused=""
|
|
82
|
+
case "$MIRROR_NODE" in
|
|
83
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
84
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
85
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
86
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
87
|
+
else NODE="$MIRROR_NODE"
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
*) refused="relative" ;;
|
|
91
|
+
esac
|
|
92
|
+
if [ -n "$refused" ]; then
|
|
93
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
94
|
+
fi
|
|
64
95
|
fi
|
|
96
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
65
97
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
66
98
|
[ -n "$NODE" ] && break
|
|
67
99
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -31,8 +31,13 @@ env_value() {
|
|
|
31
31
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
32
32
|
printf '%s' "$value"
|
|
33
33
|
}
|
|
34
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
35
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
36
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
37
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
38
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
34
39
|
note() {
|
|
35
|
-
local home user
|
|
40
|
+
local home user seen today
|
|
36
41
|
home="$(env_value MIRROR_HOME)"
|
|
37
42
|
user="$(env_value MIRROR_USER)"
|
|
38
43
|
if [ -n "$home" ]; then
|
|
@@ -43,6 +48,11 @@ note() {
|
|
|
43
48
|
else
|
|
44
49
|
return 0
|
|
45
50
|
fi
|
|
51
|
+
if [ "${2:-}" = daily ]; then
|
|
52
|
+
today="$(date -u +%Y-%m-%d)"
|
|
53
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
54
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
55
|
+
fi
|
|
46
56
|
mkdir -p "$home" 2>/dev/null \
|
|
47
57
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
48
58
|
}
|
|
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
|
|
|
58
68
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
59
69
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
60
70
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
#
|
|
72
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
73
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
74
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
75
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
76
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
77
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
78
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
79
|
+
NODE=""
|
|
80
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
81
|
+
refused=""
|
|
82
|
+
case "$MIRROR_NODE" in
|
|
83
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
84
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
85
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
86
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
87
|
+
else NODE="$MIRROR_NODE"
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
*) refused="relative" ;;
|
|
91
|
+
esac
|
|
92
|
+
if [ -n "$refused" ]; then
|
|
93
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
94
|
+
fi
|
|
64
95
|
fi
|
|
96
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
65
97
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
66
98
|
[ -n "$NODE" ] && break
|
|
67
99
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -31,8 +31,13 @@ env_value() {
|
|
|
31
31
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
32
32
|
printf '%s' "$value"
|
|
33
33
|
}
|
|
34
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
35
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
36
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
37
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
38
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
34
39
|
note() {
|
|
35
|
-
local home user
|
|
40
|
+
local home user seen today
|
|
36
41
|
home="$(env_value MIRROR_HOME)"
|
|
37
42
|
user="$(env_value MIRROR_USER)"
|
|
38
43
|
if [ -n "$home" ]; then
|
|
@@ -43,6 +48,11 @@ note() {
|
|
|
43
48
|
else
|
|
44
49
|
return 0
|
|
45
50
|
fi
|
|
51
|
+
if [ "${2:-}" = daily ]; then
|
|
52
|
+
today="$(date -u +%Y-%m-%d)"
|
|
53
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
54
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
55
|
+
fi
|
|
46
56
|
mkdir -p "$home" 2>/dev/null \
|
|
47
57
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
48
58
|
}
|
|
@@ -58,10 +68,32 @@ ENTRY="$REPO_ROOT/bin/mirror-hook.js"
|
|
|
58
68
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
59
69
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
60
70
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
#
|
|
72
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
73
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
74
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
75
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
76
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
77
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
78
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
79
|
+
NODE=""
|
|
80
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
81
|
+
refused=""
|
|
82
|
+
case "$MIRROR_NODE" in
|
|
83
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
84
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
85
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
86
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
87
|
+
else NODE="$MIRROR_NODE"
|
|
88
|
+
fi
|
|
89
|
+
;;
|
|
90
|
+
*) refused="relative" ;;
|
|
91
|
+
esac
|
|
92
|
+
if [ -n "$refused" ]; then
|
|
93
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
94
|
+
fi
|
|
64
95
|
fi
|
|
96
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
65
97
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
66
98
|
[ -n "$NODE" ] && break
|
|
67
99
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -235,7 +235,7 @@ export interface NodeResolution {
|
|
|
235
235
|
* then `NODE_CANDIDATES`. Not `process.execPath`: Pi may run on a Node older
|
|
236
236
|
* than the core needs. On Windows the PATH is the last place looked.
|
|
237
237
|
*
|
|
238
|
-
* A relative `MIRROR_NODE` is refused
|
|
238
|
+
* A relative `MIRROR_NODE` is refused, as every reader refuses one (CR192): it
|
|
239
239
|
* would resolve against whatever project Pi was opened in.
|
|
240
240
|
*
|
|
241
241
|
* The absolute candidates make "nothing anywhere" a fact about the host, so
|
package/docs/releases/index.md
CHANGED
|
@@ -12,6 +12,7 @@ Future release notes should use the structure defined in [Release Notes](../proc
|
|
|
12
12
|
|
|
13
13
|
## Releases
|
|
14
14
|
|
|
15
|
+
- [v0.39.5 — The Node You Named, or Why Not](v0.39.5.md) — patch release of CR192, from PR #47's thread (@rodrigoimmaginario): the hook wrappers, the plugin's MCP entry, and `runtime diagnose` use `MIRROR_NODE` only as an absolute path to an executable file, as the Pi extension does; a relative value is never run, and a refused one is named with its reason, once a day in `hooks.log` and on the MCP entry's stderr; the Windows installer's drive-letter path counts as absolute, in CI under Git Bash and on the installer. No migration.
|
|
15
16
|
- [v0.39.4 — The Reply Finds Its Prompt](v0.39.4.md) — patch release of PR #51 (@alannascimento1), CR166 and CR196: on Claude Code, a prompt typed while a turn runs and a journey entered in plain words no longer lose the reply; `nothing_waiting` in `hooks.log` means a lost reply again. No schema change, no migration.
|
|
16
17
|
- [v0.39.3 — Windows Gets Its Hooks](v0.39.3.md) — patch release of PR #53, PR #50, and PR #54 (@rodrigoimmaginario): the plugin's hook wrappers and MCP locator run the package's own bin when npm's global entry is a shell shim, so Claude Code hooks and the MCP server work on a Windows package install; `runtime diagnose` no longer counts its own verdict as a front-door error (CR139); the docs point a Windows reader to the installer in its own repository. No migration.
|
|
17
18
|
- [v0.39.2 — Input, Read the Platform's Way](v0.39.2.md) — patch release of PR #49 and PR #48 (@rodrigoimmaginario), CR194, and CR193: `runtime diagnose` stops warning every Windows install about Node; a PowerShell byte-order mark is not stored; stdin is decoded by its mark, UTF-8 or UTF-16, and `identity set` and `journey update` gain `--file`, the route around PowerShell 5.1's ASCII pipe; `package-lock.json` names the package at its version, and the release doctor checks it. No migration.
|
package/docs/releases/v0.39.2.md
CHANGED
|
@@ -89,11 +89,18 @@ Validated on 2026-11-03 against a scratch copy of the maintainer's database: UTF
|
|
|
89
89
|
with a mark, and UTF-16LE as PowerShell 5.1 writes it, through `--file` and through stdin;
|
|
90
90
|
the refusals writing nothing; the one-liner pipe unchanged.
|
|
91
91
|
|
|
92
|
+
**Post-release Windows validation, 2026-11-03.** Rodrigo Immaginario ran v0.39.2 through
|
|
93
|
+
npm's `mirror.ps1` shim on Windows PowerShell 5.1.26100. `--file` preserved both unmarked
|
|
94
|
+
UTF-8 and UTF-16LE. The measurement also corrected the pipe documentation: for an
|
|
95
|
+
editor-saved unmarked UTF-8 file, UTF-8 `$OutputEncoding` is not enough because
|
|
96
|
+
`Get-Content` has already decoded the file through the ANSI code page. The complete pipe
|
|
97
|
+
route is `Get-Content -Encoding UTF8` plus UTF-8 `$OutputEncoding`. Evidence:
|
|
98
|
+
[PR #49](https://github.com/mirror-mind-ai/mirror/pull/49#issuecomment-6610137363).
|
|
99
|
+
|
|
92
100
|
## Conscious Exclusions
|
|
93
101
|
|
|
94
|
-
- Windows PowerShell 5.1 itself
|
|
95
|
-
|
|
96
|
-
the reporter.
|
|
102
|
+
- At publication, Windows PowerShell 5.1 itself had not been run on the maintainer's side;
|
|
103
|
+
the post-release validation above closed that measurement gap.
|
|
97
104
|
- Detecting text the ASCII pipe damaged: not possible without guessing.
|
|
98
105
|
- The hook wrappers and the MCP locator still accept a relative `MIRROR_NODE`
|
|
99
106
|
([CR192](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr192-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-node.md),
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
---
|
|
2
|
+
digest: >
|
|
3
|
+
v0.39.5 is a patch release of one Change Request, CR192, from PR #47's thread with
|
|
4
|
+
Rodrigo Immaginario. The hook wrappers, the plugin's MCP entry, and `runtime diagnose`
|
|
5
|
+
now use `MIRROR_NODE` only when it is an absolute path to an executable file, the rule
|
|
6
|
+
the Pi extension has followed since v0.39.1. A relative value could make a hook run a
|
|
7
|
+
file from the project the runtime was opened in, and a value that no longer pointed at
|
|
8
|
+
Node was dropped without a word. Now neither is used, and the hooks and the MCP entry
|
|
9
|
+
name the refused value with its reason. The Windows installer's drive-letter path counts
|
|
10
|
+
as absolute, proven in CI under Git Bash and on the installer itself. No schema change,
|
|
11
|
+
no migration, and no model or prompt change.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# v0.39.5 — The Node You Named, or Why Not
|
|
15
|
+
|
|
16
|
+
**Date:** 2026-11-05
|
|
17
|
+
|
|
18
|
+
## Highlights
|
|
19
|
+
|
|
20
|
+
- **A relative `MIRROR_NODE` is never run**
|
|
21
|
+
([CR192](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr192-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-node.md)).
|
|
22
|
+
A runtime starts its hooks and its MCP server from the project it was opened in, and the
|
|
23
|
+
hook wrappers and the MCP entry kept any `MIRROR_NODE` that passed `[ -x ]` there: with
|
|
24
|
+
`MIRROR_NODE=bin/node`, the project's own `bin/node` ran on every hook. Every reader now
|
|
25
|
+
follows the Pi extension's rule: an absolute path to an executable file, or nothing.
|
|
26
|
+
- **A value that is not used is named, with its reason.** A value that named no usable
|
|
27
|
+
Node used to be dropped without a word. Now the hook wrappers write
|
|
28
|
+
`MIRROR_NODE=bin/node is relative; searching the PATH and the usual places instead` to
|
|
29
|
+
`<mirror home>/hooks.log`, once a UTC day for the same value, and the MCP entry writes
|
|
30
|
+
the same message to its stderr. The reasons are the Pi extension's: `relative`,
|
|
31
|
+
`a directory`, `missing`, and `not executable`. The search then goes on through the
|
|
32
|
+
PATH and the usual places, as before.
|
|
33
|
+
- **The Windows installer's form counts as absolute.** Under Git Bash the installer's
|
|
34
|
+
`MIRROR_NODE` is a drive-letter path with backslashes, and a POSIX-only test would have
|
|
35
|
+
refused it on every Windows hook; @rodrigoimmaginario named that trap before the plan
|
|
36
|
+
was written. One shell pattern now classifies exactly as Node's `path.win32.isAbsolute`:
|
|
37
|
+
`C:\…`, `C:/…`, and `\\server\share\…` are absolute, and a drive-relative `C:node.exe`
|
|
38
|
+
is refused as `relative`.
|
|
39
|
+
- **Proven on Windows twice.** The Windows CI job runs a real wrapper under Git Bash, with
|
|
40
|
+
a Node under `Program Files\nodejs` named in its Windows form. And before this tag,
|
|
41
|
+
Rodrigo ran the installer against `main`: its `MIRROR_NODE`, under a user folder with a
|
|
42
|
+
space and accents, ran the hooks and the MCP server with no refusal line, and
|
|
43
|
+
`C:node.exe` gave exactly one.
|
|
44
|
+
- No schema change, no migration, and no model or prompt change.
|
|
45
|
+
|
|
46
|
+
## Where We Started
|
|
47
|
+
|
|
48
|
+
v0.39.1 made the Pi extension strict about `MIRROR_NODE`
|
|
49
|
+
([CR191](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr191-the-pi-extension-cannot-find-node-where-the-hook-wrappers-can.md)):
|
|
50
|
+
an absolute path to an executable file, or a warning with the reason and a search without
|
|
51
|
+
it. The rest of the family was not strict. The hook wrappers and the plugin's MCP locator
|
|
52
|
+
kept any value that passed `[ -x ]`, a test a relative path passes against the project a
|
|
53
|
+
hook was started in; a value that failed it was dropped, and whoever had set it to pin a
|
|
54
|
+
Node learned nothing. `REFERENCE.md` described one trust class for every reader, and only
|
|
55
|
+
one reader held to it. CR191's debt review left the family's rule as its own decision,
|
|
56
|
+
captured as CR192 on 2026-11-02.
|
|
57
|
+
|
|
58
|
+
On PR #47 the next day, Rodrigo Immaginario named the trap in the obvious fix: under Git
|
|
59
|
+
Bash the Windows installer's `MIRROR_NODE` is a drive-letter path, it passes `[ -x ]`
|
|
60
|
+
today, and a `/*` test for "absolute" would refuse it, so every hook on a Windows install
|
|
61
|
+
would log a refusal. He also asked that PR #53, which changed the same generator, locator,
|
|
62
|
+
and tests, merge first. It did, in v0.39.3, and the plan started from its blocks.
|
|
63
|
+
|
|
64
|
+
## What Changed
|
|
65
|
+
|
|
66
|
+
**One rule, one pattern.** For the shell readers, "absolute" is one `case` pattern,
|
|
67
|
+
`/*|\\*|[A-Za-z]:[\\/]*`, which matched `path.win32.isAbsolute` on 15 values in bash,
|
|
68
|
+
dash, ksh, `/bin/sh`, and zsh as `sh`. `scripts/generate_hook_wrappers.sh` gives it, with
|
|
69
|
+
the logging helpers and the Node search, to all three wrapper forms as shared fragments,
|
|
70
|
+
and the 13 wrappers are regenerated. The MCP locator in `ts/src/guards/claudePlugin.ts`
|
|
71
|
+
carries the same text as `ABSOLUTE_PATH_CASE`, and a test fails if the two drift. The Node
|
|
72
|
+
readers keep their platform's `path.isAbsolute`, so on Windows every reader draws the same
|
|
73
|
+
line.
|
|
74
|
+
|
|
75
|
+
**A refusal that cannot forge a line.** Line breaks are removed from a value before it is
|
|
76
|
+
logged: in review, a value with a line break in it wrote a second `hooks.log` line, as
|
|
77
|
+
`claude:stop`, that `runtime diagnose` counted. The locator prints with `printf`, because
|
|
78
|
+
dash's `echo` turns the `\n` of a Windows path into a newline, and names `$MIRROR_NODE`
|
|
79
|
+
once, because Grok pastes a set variable's value into the MCP arguments it loads.
|
|
80
|
+
|
|
81
|
+
**`runtime diagnose`.** Its Node check counts `MIRROR_NODE` only under the rule, through
|
|
82
|
+
`isExecutableFile()`, which the front door's runtime route now shares. It adds no finding
|
|
83
|
+
of its own: the hooks' refusal line reaches it as `hook_failures_recorded`, for seven
|
|
84
|
+
days.
|
|
85
|
+
|
|
86
|
+
**Tests and CI.** `mirrorNodeRule.test.ts` holds the pattern equal across the three
|
|
87
|
+
wrapper forms and the locator, runs the classification table through bash and, where
|
|
88
|
+
installed, dash, and runs a real wrapper with a Node under `Program Files/nodejs` and
|
|
89
|
+
with `C:node.exe`; the Windows job runs it under Git Bash. `hooks.test.ts` and
|
|
90
|
+
`mcpEntry.test.ts` cover each reason, a planted `bin/node` that never runs, the daily
|
|
91
|
+
rule across two fixed days, a forged line break, and a server that starts past a
|
|
92
|
+
relative value.
|
|
93
|
+
|
|
94
|
+
**Docs.** The rule is stated once, in full, in the
|
|
95
|
+
[configuration reference](../reference/configuration.md#hook-node-resolution);
|
|
96
|
+
`REFERENCE.md`'s row points there. Troubleshooting shows the refusal line and the fix, and
|
|
97
|
+
says `runtime diagnose` keeps counting old lines for seven days after the fix.
|
|
98
|
+
|
|
99
|
+
Validated by the Navigator in a scratch home: two runs of the plugin's prompt hook with
|
|
100
|
+
`MIRROR_NODE=bin/node` wrote one line,
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
2026-11-04T21:40:16Z claude:user-prompt: MIRROR_NODE=bin/node is relative; searching the PATH and the usual places instead
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
the project's `bin/node` never ran, the prompt was logged, and `runtime diagnose`
|
|
107
|
+
reported `hook_failures_recorded`. CI passed on every job, the Git Bash step included,
|
|
108
|
+
and Rodrigo's installer run, 21 of 21 on the installer's contract, was the gate before
|
|
109
|
+
the tag.
|
|
110
|
+
|
|
111
|
+
**Also in this release.** Troubleshooting's PowerShell 5.1 section, from Rodrigo's PR #49
|
|
112
|
+
run: `Get-Content` decodes an editor-saved UTF-8 file without a mark through the ANSI code
|
|
113
|
+
page before the pipe runs, so a UTF-8 `$OutputEncoding` alone does not save it. The pipe
|
|
114
|
+
route now adds `Get-Content -Encoding UTF8`, and `--file` stays the safe one.
|
|
115
|
+
|
|
116
|
+
## Conscious Exclusions
|
|
117
|
+
|
|
118
|
+
- `MIRROR_BIN` keeps its behavior: a relative value is still honored, and one that names
|
|
119
|
+
nothing is still dropped without a word.
|
|
120
|
+
[CR197](../project/refinement/rs010-cv22-oracle-and-port-hygiene/cr197-the-hook-wrappers-and-the-mcp-locator-accept-a-relative-mirror-bin.md)
|
|
121
|
+
(captured) would give it this rule, keeping accepted the forward-slash `C:/…` form a
|
|
122
|
+
Windows machine sets for the Claude Code forwarders.
|
|
123
|
+
- `runtime diagnose` does not name a refused value. It counts the hooks' line under
|
|
124
|
+
`hook_failures_recorded`, and the value and its reason are in `hooks.log`; Rodrigo
|
|
125
|
+
suggests saying it in diagnose too. A finding of its own was left out at planning.
|
|
126
|
+
- Whitespace: the Pi extension trims the value and the shell readers do not, so
|
|
127
|
+
` /opt/node` is accepted by Pi and refused as `relative` by the hooks. Nothing writes
|
|
128
|
+
such a value.
|
|
129
|
+
- Converting MSYS `/c/…` values, and rotating `hooks.log`.
|
|
130
|
+
- Where Codex and Antigravity show an MCP server's stderr is not verified; the docs say
|
|
131
|
+
only what each CLI's own help shows.
|
|
132
|
+
|
|
133
|
+
## Upgrading
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
mirror runtime update
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
If you set `MIRROR_NODE` by hand, make it the absolute path of the `node` binary: a
|
|
140
|
+
relative one is now refused, with one line a day in `hooks.log`.
|
|
141
|
+
|
|
142
|
+
## Next Horizon
|
|
143
|
+
|
|
144
|
+
CR197, which would bring `MIRROR_BIN` under this rule with its pattern; Rodrigo's
|
|
145
|
+
suggestion that `runtime diagnose` name a refused value, not yet decided; and the open
|
|
146
|
+
RS010 backlog.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirror-mind",
|
|
3
|
-
"version": "0.39.
|
|
3
|
+
"version": "0.39.5",
|
|
4
4
|
"description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Mirror Mind"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"command": "/bin/sh",
|
|
11
11
|
"args": [
|
|
12
12
|
"-c",
|
|
13
|
-
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
13
|
+
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; mmnode=\"$MIRROR_NODE\"; mmwhy=\"\"; [ -n \"$mmnode\" ] && case \"$mmnode\" in /*|\\\\*|[A-Za-z]:[\\\\/]*) if [ -d \"$mmnode\" ]; then mmwhy=\"a directory\"; elif [ ! -e \"$mmnode\" ]; then mmwhy=\"missing\"; elif [ ! -f \"$mmnode\" ] || [ ! -x \"$mmnode\" ]; then mmwhy=\"not executable\"; fi ;; *) mmwhy=\"relative\" ;; esac; [ -n \"$mmwhy\" ] && { printf 'mirror-mind: MIRROR_NODE=%s is %s; searching the PATH and the usual places instead\\n' \"$(printf '%s' \"$mmnode\" | tr -d '\\r\\n')\" \"$mmwhy\" >&2; mmnode=\"\"; }; [ -n \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
14
14
|
]
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -35,8 +35,13 @@ env_value() {
|
|
|
35
35
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
36
36
|
printf '%s' "$value"
|
|
37
37
|
}
|
|
38
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
39
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
40
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
41
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
42
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
38
43
|
note() {
|
|
39
|
-
local home user
|
|
44
|
+
local home user seen today
|
|
40
45
|
home="$(env_value MIRROR_HOME)"
|
|
41
46
|
user="$(env_value MIRROR_USER)"
|
|
42
47
|
if [ -n "$home" ]; then
|
|
@@ -47,6 +52,11 @@ note() {
|
|
|
47
52
|
else
|
|
48
53
|
return 0
|
|
49
54
|
fi
|
|
55
|
+
if [ "${2:-}" = daily ]; then
|
|
56
|
+
today="$(date -u +%Y-%m-%d)"
|
|
57
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
58
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
59
|
+
fi
|
|
50
60
|
mkdir -p "$home" 2>/dev/null \
|
|
51
61
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
52
62
|
}
|
|
@@ -75,10 +85,41 @@ if [ -z "$ENTRY" ]; then
|
|
|
75
85
|
exit 0
|
|
76
86
|
fi
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
# Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
|
|
89
|
+
# nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
|
|
90
|
+
# hooks could end in `|| true` because they always found their interpreter;
|
|
91
|
+
# the same silence here would hide a new failure — no logging, no inject, no
|
|
92
|
+
# close tail — that a user would meet weeks later as "Mirror stopped
|
|
93
|
+
# remembering". So an unresolvable Node is RECORDED, never swallowed. The
|
|
94
|
+
# directory the entry was found in is searched first: an npm global bin holds
|
|
95
|
+
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
96
|
+
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
97
|
+
#
|
|
98
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
99
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
100
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
101
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
102
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
103
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
104
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
105
|
+
NODE=""
|
|
106
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
107
|
+
refused=""
|
|
108
|
+
case "$MIRROR_NODE" in
|
|
109
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
110
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
111
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
112
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
113
|
+
else NODE="$MIRROR_NODE"
|
|
114
|
+
fi
|
|
115
|
+
;;
|
|
116
|
+
*) refused="relative" ;;
|
|
117
|
+
esac
|
|
118
|
+
if [ -n "$refused" ]; then
|
|
119
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
120
|
+
fi
|
|
81
121
|
fi
|
|
122
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
82
123
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
83
124
|
[ -n "$NODE" ] && break
|
|
84
125
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -35,8 +35,13 @@ env_value() {
|
|
|
35
35
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
36
36
|
printf '%s' "$value"
|
|
37
37
|
}
|
|
38
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
39
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
40
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
41
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
42
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
38
43
|
note() {
|
|
39
|
-
local home user
|
|
44
|
+
local home user seen today
|
|
40
45
|
home="$(env_value MIRROR_HOME)"
|
|
41
46
|
user="$(env_value MIRROR_USER)"
|
|
42
47
|
if [ -n "$home" ]; then
|
|
@@ -47,6 +52,11 @@ note() {
|
|
|
47
52
|
else
|
|
48
53
|
return 0
|
|
49
54
|
fi
|
|
55
|
+
if [ "${2:-}" = daily ]; then
|
|
56
|
+
today="$(date -u +%Y-%m-%d)"
|
|
57
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
58
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
59
|
+
fi
|
|
50
60
|
mkdir -p "$home" 2>/dev/null \
|
|
51
61
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
52
62
|
}
|
|
@@ -75,10 +85,41 @@ if [ -z "$ENTRY" ]; then
|
|
|
75
85
|
exit 0
|
|
76
86
|
fi
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
# Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
|
|
89
|
+
# nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
|
|
90
|
+
# hooks could end in `|| true` because they always found their interpreter;
|
|
91
|
+
# the same silence here would hide a new failure — no logging, no inject, no
|
|
92
|
+
# close tail — that a user would meet weeks later as "Mirror stopped
|
|
93
|
+
# remembering". So an unresolvable Node is RECORDED, never swallowed. The
|
|
94
|
+
# directory the entry was found in is searched first: an npm global bin holds
|
|
95
|
+
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
96
|
+
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
97
|
+
#
|
|
98
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
99
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
100
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
101
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
102
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
103
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
104
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
105
|
+
NODE=""
|
|
106
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
107
|
+
refused=""
|
|
108
|
+
case "$MIRROR_NODE" in
|
|
109
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
110
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
111
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
112
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
113
|
+
else NODE="$MIRROR_NODE"
|
|
114
|
+
fi
|
|
115
|
+
;;
|
|
116
|
+
*) refused="relative" ;;
|
|
117
|
+
esac
|
|
118
|
+
if [ -n "$refused" ]; then
|
|
119
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
120
|
+
fi
|
|
81
121
|
fi
|
|
122
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
82
123
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
83
124
|
[ -n "$NODE" ] && break
|
|
84
125
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -35,8 +35,13 @@ env_value() {
|
|
|
35
35
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
36
36
|
printf '%s' "$value"
|
|
37
37
|
}
|
|
38
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
39
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
40
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
41
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
42
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
38
43
|
note() {
|
|
39
|
-
local home user
|
|
44
|
+
local home user seen today
|
|
40
45
|
home="$(env_value MIRROR_HOME)"
|
|
41
46
|
user="$(env_value MIRROR_USER)"
|
|
42
47
|
if [ -n "$home" ]; then
|
|
@@ -47,6 +52,11 @@ note() {
|
|
|
47
52
|
else
|
|
48
53
|
return 0
|
|
49
54
|
fi
|
|
55
|
+
if [ "${2:-}" = daily ]; then
|
|
56
|
+
today="$(date -u +%Y-%m-%d)"
|
|
57
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
58
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
59
|
+
fi
|
|
50
60
|
mkdir -p "$home" 2>/dev/null \
|
|
51
61
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
52
62
|
}
|
|
@@ -75,10 +85,41 @@ if [ -z "$ENTRY" ]; then
|
|
|
75
85
|
exit 0
|
|
76
86
|
fi
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
# Find Node. /usr/bin/python3 exists on every macOS; node usually lives under
|
|
89
|
+
# nvm or Homebrew and is NOT on the PATH of a GUI-launched runtime. The Python
|
|
90
|
+
# hooks could end in `|| true` because they always found their interpreter;
|
|
91
|
+
# the same silence here would hide a new failure — no logging, no inject, no
|
|
92
|
+
# close tail — that a user would meet weeks later as "Mirror stopped
|
|
93
|
+
# remembering". So an unresolvable Node is RECORDED, never swallowed. The
|
|
94
|
+
# directory the entry was found in is searched first: an npm global bin holds
|
|
95
|
+
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
96
|
+
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
97
|
+
#
|
|
98
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
99
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
100
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
101
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
102
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
103
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
104
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
105
|
+
NODE=""
|
|
106
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
107
|
+
refused=""
|
|
108
|
+
case "$MIRROR_NODE" in
|
|
109
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
110
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
111
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
112
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
113
|
+
else NODE="$MIRROR_NODE"
|
|
114
|
+
fi
|
|
115
|
+
;;
|
|
116
|
+
*) refused="relative" ;;
|
|
117
|
+
esac
|
|
118
|
+
if [ -n "$refused" ]; then
|
|
119
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
120
|
+
fi
|
|
81
121
|
fi
|
|
122
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
82
123
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
83
124
|
[ -n "$NODE" ] && break
|
|
84
125
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -30,8 +30,13 @@ env_value() {
|
|
|
30
30
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
31
31
|
printf '%s' "$value"
|
|
32
32
|
}
|
|
33
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
34
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
35
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
36
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
37
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
33
38
|
note() {
|
|
34
|
-
local home user
|
|
39
|
+
local home user seen today
|
|
35
40
|
home="$(env_value MIRROR_HOME)"
|
|
36
41
|
user="$(env_value MIRROR_USER)"
|
|
37
42
|
if [ -n "$home" ]; then
|
|
@@ -42,6 +47,11 @@ note() {
|
|
|
42
47
|
else
|
|
43
48
|
return 0
|
|
44
49
|
fi
|
|
50
|
+
if [ "${2:-}" = daily ]; then
|
|
51
|
+
today="$(date -u +%Y-%m-%d)"
|
|
52
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
53
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
54
|
+
fi
|
|
45
55
|
mkdir -p "$home" 2>/dev/null \
|
|
46
56
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
47
57
|
}
|
|
@@ -79,10 +89,32 @@ fi
|
|
|
79
89
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
80
90
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
81
91
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
#
|
|
93
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
94
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
95
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
96
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
97
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
98
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
99
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
100
|
+
NODE=""
|
|
101
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
102
|
+
refused=""
|
|
103
|
+
case "$MIRROR_NODE" in
|
|
104
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
105
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
106
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
107
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
108
|
+
else NODE="$MIRROR_NODE"
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
*) refused="relative" ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ -n "$refused" ]; then
|
|
114
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
115
|
+
fi
|
|
85
116
|
fi
|
|
117
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
86
118
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
87
119
|
[ -n "$NODE" ] && break
|
|
88
120
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -30,8 +30,13 @@ env_value() {
|
|
|
30
30
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
31
31
|
printf '%s' "$value"
|
|
32
32
|
}
|
|
33
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
34
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
35
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
36
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
37
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
33
38
|
note() {
|
|
34
|
-
local home user
|
|
39
|
+
local home user seen today
|
|
35
40
|
home="$(env_value MIRROR_HOME)"
|
|
36
41
|
user="$(env_value MIRROR_USER)"
|
|
37
42
|
if [ -n "$home" ]; then
|
|
@@ -42,6 +47,11 @@ note() {
|
|
|
42
47
|
else
|
|
43
48
|
return 0
|
|
44
49
|
fi
|
|
50
|
+
if [ "${2:-}" = daily ]; then
|
|
51
|
+
today="$(date -u +%Y-%m-%d)"
|
|
52
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
53
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
54
|
+
fi
|
|
45
55
|
mkdir -p "$home" 2>/dev/null \
|
|
46
56
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
47
57
|
}
|
|
@@ -79,10 +89,32 @@ fi
|
|
|
79
89
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
80
90
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
81
91
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
#
|
|
93
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
94
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
95
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
96
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
97
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
98
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
99
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
100
|
+
NODE=""
|
|
101
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
102
|
+
refused=""
|
|
103
|
+
case "$MIRROR_NODE" in
|
|
104
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
105
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
106
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
107
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
108
|
+
else NODE="$MIRROR_NODE"
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
*) refused="relative" ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ -n "$refused" ]; then
|
|
114
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
115
|
+
fi
|
|
85
116
|
fi
|
|
117
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
86
118
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
87
119
|
[ -n "$NODE" ] && break
|
|
88
120
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -30,8 +30,13 @@ env_value() {
|
|
|
30
30
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
31
31
|
printf '%s' "$value"
|
|
32
32
|
}
|
|
33
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
34
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
35
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
36
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
37
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
33
38
|
note() {
|
|
34
|
-
local home user
|
|
39
|
+
local home user seen today
|
|
35
40
|
home="$(env_value MIRROR_HOME)"
|
|
36
41
|
user="$(env_value MIRROR_USER)"
|
|
37
42
|
if [ -n "$home" ]; then
|
|
@@ -42,6 +47,11 @@ note() {
|
|
|
42
47
|
else
|
|
43
48
|
return 0
|
|
44
49
|
fi
|
|
50
|
+
if [ "${2:-}" = daily ]; then
|
|
51
|
+
today="$(date -u +%Y-%m-%d)"
|
|
52
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
53
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
54
|
+
fi
|
|
45
55
|
mkdir -p "$home" 2>/dev/null \
|
|
46
56
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
47
57
|
}
|
|
@@ -79,10 +89,32 @@ fi
|
|
|
79
89
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
80
90
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
81
91
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
#
|
|
93
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
94
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
95
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
96
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
97
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
98
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
99
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
100
|
+
NODE=""
|
|
101
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
102
|
+
refused=""
|
|
103
|
+
case "$MIRROR_NODE" in
|
|
104
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
105
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
106
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
107
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
108
|
+
else NODE="$MIRROR_NODE"
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
*) refused="relative" ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ -n "$refused" ]; then
|
|
114
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
115
|
+
fi
|
|
85
116
|
fi
|
|
117
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
86
118
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
87
119
|
[ -n "$NODE" ] && break
|
|
88
120
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -30,8 +30,13 @@ env_value() {
|
|
|
30
30
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
31
31
|
printf '%s' "$value"
|
|
32
32
|
}
|
|
33
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
34
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
35
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
36
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
37
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
33
38
|
note() {
|
|
34
|
-
local home user
|
|
39
|
+
local home user seen today
|
|
35
40
|
home="$(env_value MIRROR_HOME)"
|
|
36
41
|
user="$(env_value MIRROR_USER)"
|
|
37
42
|
if [ -n "$home" ]; then
|
|
@@ -42,6 +47,11 @@ note() {
|
|
|
42
47
|
else
|
|
43
48
|
return 0
|
|
44
49
|
fi
|
|
50
|
+
if [ "${2:-}" = daily ]; then
|
|
51
|
+
today="$(date -u +%Y-%m-%d)"
|
|
52
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
53
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
54
|
+
fi
|
|
45
55
|
mkdir -p "$home" 2>/dev/null \
|
|
46
56
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
47
57
|
}
|
|
@@ -79,10 +89,32 @@ fi
|
|
|
79
89
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
80
90
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
81
91
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
#
|
|
93
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
94
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
95
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
96
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
97
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
98
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
99
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
100
|
+
NODE=""
|
|
101
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
102
|
+
refused=""
|
|
103
|
+
case "$MIRROR_NODE" in
|
|
104
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
105
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
106
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
107
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
108
|
+
else NODE="$MIRROR_NODE"
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
*) refused="relative" ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ -n "$refused" ]; then
|
|
114
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
115
|
+
fi
|
|
85
116
|
fi
|
|
117
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
86
118
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
87
119
|
[ -n "$NODE" ] && break
|
|
88
120
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -30,8 +30,13 @@ env_value() {
|
|
|
30
30
|
[ -n "$value" ] || value="$(env_file_value "$1" "${XDG_CONFIG_HOME:-$HOME/.config}/mirror/env")"
|
|
31
31
|
printf '%s' "$value"
|
|
32
32
|
}
|
|
33
|
+
# `note <line> daily` writes at most once a day (UTC): for a setting to fix
|
|
34
|
+
# rather than a hook that failed, so the line stays inside diagnose's seven-day
|
|
35
|
+
# window while the setting stays wrong, without one per hook per turn (CR192).
|
|
36
|
+
# The log is read into a variable, not piped to `grep -q`: under pipefail, a
|
|
37
|
+
# reader that stops early can fail the pipeline and the line be written twice.
|
|
33
38
|
note() {
|
|
34
|
-
local home user
|
|
39
|
+
local home user seen today
|
|
35
40
|
home="$(env_value MIRROR_HOME)"
|
|
36
41
|
user="$(env_value MIRROR_USER)"
|
|
37
42
|
if [ -n "$home" ]; then
|
|
@@ -42,6 +47,11 @@ note() {
|
|
|
42
47
|
else
|
|
43
48
|
return 0
|
|
44
49
|
fi
|
|
50
|
+
if [ "${2:-}" = daily ]; then
|
|
51
|
+
today="$(date -u +%Y-%m-%d)"
|
|
52
|
+
seen="$(grep -F -- ": $1" "$home/hooks.log" 2>/dev/null)"
|
|
53
|
+
case "$seen" in "$today"T*|*$'\n'"$today"T*) return 0 ;; esac
|
|
54
|
+
fi
|
|
45
55
|
mkdir -p "$home" 2>/dev/null \
|
|
46
56
|
&& printf '%s %s: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$HOOK" "$1" >> "$home/hooks.log" 2>/dev/null
|
|
47
57
|
}
|
|
@@ -79,10 +89,32 @@ fi
|
|
|
79
89
|
# directory the entry was found in is searched first: an npm global bin holds
|
|
80
90
|
# the `node` that installed the package beside `mirror-hook`, while a system
|
|
81
91
|
# `/usr/local/bin/node` can be years older (CI's Ubuntu runner has one).
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
#
|
|
93
|
+
# MIRROR_NODE is used only as an absolute path to an executable file, as every
|
|
94
|
+
# reader uses it (CR192): a relative one would resolve against the project the
|
|
95
|
+
# runtime spawned this hook from. Anything else is noted with its reason, in
|
|
96
|
+
# the Pi extension's words, once a day, and the search goes on without it.
|
|
97
|
+
# "Absolute" is the MCP locator's pattern too, and it classifies exactly as
|
|
98
|
+
# path.win32.isAbsolute: under Git Bash the Windows installer writes
|
|
99
|
+
# C:\Program Files\nodejs\node.exe (PR #47).
|
|
100
|
+
NODE=""
|
|
101
|
+
if [ -n "${MIRROR_NODE:-}" ]; then
|
|
102
|
+
refused=""
|
|
103
|
+
case "$MIRROR_NODE" in
|
|
104
|
+
/*|\\*|[A-Za-z]:[\\/]*)
|
|
105
|
+
if [ -d "$MIRROR_NODE" ]; then refused="a directory"
|
|
106
|
+
elif [ ! -e "$MIRROR_NODE" ]; then refused="missing"
|
|
107
|
+
elif [ ! -f "$MIRROR_NODE" ] || [ ! -x "$MIRROR_NODE" ]; then refused="not executable"
|
|
108
|
+
else NODE="$MIRROR_NODE"
|
|
109
|
+
fi
|
|
110
|
+
;;
|
|
111
|
+
*) refused="relative" ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ -n "$refused" ]; then
|
|
114
|
+
note "MIRROR_NODE=${MIRROR_NODE//[$'\r\n']/} is $refused; searching the PATH and the usual places instead" daily
|
|
115
|
+
fi
|
|
85
116
|
fi
|
|
117
|
+
[ -n "$NODE" ] || NODE="$(command -v node 2>/dev/null || true)"
|
|
86
118
|
for candidate in "$(dirname "$ENTRY")/node" "$HOME/.nvm/current/bin/node" /opt/homebrew/bin/node /usr/local/bin/node; do
|
|
87
119
|
[ -n "$NODE" ] && break
|
|
88
120
|
[ -x "$candidate" ] && NODE="$candidate"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"command": "/bin/sh",
|
|
5
5
|
"args": [
|
|
6
6
|
"-c",
|
|
7
|
-
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
7
|
+
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; mmnode=\"$MIRROR_NODE\"; mmwhy=\"\"; [ -n \"$mmnode\" ] && case \"$mmnode\" in /*|\\\\*|[A-Za-z]:[\\\\/]*) if [ -d \"$mmnode\" ]; then mmwhy=\"a directory\"; elif [ ! -e \"$mmnode\" ]; then mmwhy=\"missing\"; elif [ ! -f \"$mmnode\" ] || [ ! -x \"$mmnode\" ]; then mmwhy=\"not executable\"; fi ;; *) mmwhy=\"relative\" ;; esac; [ -n \"$mmwhy\" ] && { printf 'mirror-mind: MIRROR_NODE=%s is %s; searching the PATH and the usual places instead\\n' \"$(printf '%s' \"$mmnode\" | tr -d '\\r\\n')\" \"$mmwhy\" >&2; mmnode=\"\"; }; [ -n \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
8
8
|
]
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mirror-mind",
|
|
3
3
|
"description": "Mirror Mind — local-first memory and identity for agentic runtimes.",
|
|
4
|
-
"version": "0.39.
|
|
4
|
+
"version": "0.39.5",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"mirror-mind": {
|
|
7
7
|
"command": "/bin/sh",
|
|
8
8
|
"args": [
|
|
9
9
|
"-c",
|
|
10
|
-
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; mmnode=\"$MIRROR_NODE\"; [ -n \"$mmnode\" ] && [ -x \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
10
|
+
"mmbin=\"\"; [ -n \"$MIRROR_BIN\" ] && [ -x \"$MIRROR_BIN/mirror\" ] && mmbin=\"$MIRROR_BIN/mirror\"; [ -n \"$mmbin\" ] || mmbin=\"$(command -v mirror 2>/dev/null)\"; for mmdir in \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmbin\" ] && break; [ -x \"$mmdir/mirror\" ] && mmbin=\"$mmdir/mirror\"; done; [ -n \"$mmbin\" ] || { echo \"mirror-mind: mirror not found on PATH; the MCP server cannot start. Install mirror-mind (npm install -g mirror-mind) or set MIRROR_BIN to its bin directory.\" >&2; exit 1; }; mmjs=\"$(dirname \"$mmbin\")/node_modules/mirror-mind/bin/mirror.js\"; [ -f \"$mmjs\" ] && mmbin=\"$mmjs\"; mmnode=\"$MIRROR_NODE\"; mmwhy=\"\"; [ -n \"$mmnode\" ] && case \"$mmnode\" in /*|\\\\*|[A-Za-z]:[\\\\/]*) if [ -d \"$mmnode\" ]; then mmwhy=\"a directory\"; elif [ ! -e \"$mmnode\" ]; then mmwhy=\"missing\"; elif [ ! -f \"$mmnode\" ] || [ ! -x \"$mmnode\" ]; then mmwhy=\"not executable\"; fi ;; *) mmwhy=\"relative\" ;; esac; [ -n \"$mmwhy\" ] && { printf 'mirror-mind: MIRROR_NODE=%s is %s; searching the PATH and the usual places instead\\n' \"$(printf '%s' \"$mmnode\" | tr -d '\\r\\n')\" \"$mmwhy\" >&2; mmnode=\"\"; }; [ -n \"$mmnode\" ] || mmnode=\"$(command -v node 2>/dev/null)\"; for mmdir in \"$(dirname \"$mmbin\")\" \"$HOME/.nvm/current/bin\" /opt/homebrew/bin /usr/local/bin; do [ -n \"$mmnode\" ] && break; [ -x \"$mmdir/node\" ] && mmnode=\"$mmdir/node\"; done; [ -n \"$mmnode\" ] || { echo \"mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE.\" >&2; exit 1; }; exec \"$mmnode\" \"$mmbin\" mcp"
|
|
11
11
|
]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
extractionReasoningFindings,
|
|
30
30
|
hookFailureFindings,
|
|
31
31
|
hookNodeFindings,
|
|
32
|
+
isExecutableFile,
|
|
32
33
|
openRouterKeyFindings,
|
|
33
34
|
pluginWiringFindings,
|
|
34
35
|
probeModelPins,
|
|
@@ -345,14 +346,7 @@ export async function runRuntimeReadRoute(
|
|
|
345
346
|
// and nothing else on this report would say so.
|
|
346
347
|
...openRouterKeyFindings(env),
|
|
347
348
|
...hookFailureFindings(report),
|
|
348
|
-
...hookNodeFindings(env,
|
|
349
|
-
try {
|
|
350
|
-
accessSync(path, constants.X_OK);
|
|
351
|
-
return true;
|
|
352
|
-
} catch {
|
|
353
|
-
return false;
|
|
354
|
-
}
|
|
355
|
-
}),
|
|
349
|
+
...hookNodeFindings(env, isExecutableFile),
|
|
356
350
|
// CV21.DS4.TS6: the plugin wiring. Codex's versioned cache goes stale
|
|
357
351
|
// after an npm update; Claude Code's link can dangle. Absence is not
|
|
358
352
|
// drift, so an unwired machine stays green.
|
|
@@ -81,6 +81,18 @@ export function readVersion(repoRoot: string): string {
|
|
|
81
81
|
*/
|
|
82
82
|
export const GLOBAL_BIN_DIRS = ['"$HOME/.nvm/current/bin"', "/opt/homebrew/bin", "/usr/local/bin"];
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* What the shell readers of `MIRROR_NODE` call an absolute path (CR192): one
|
|
86
|
+
* `case` pattern, written into the hook wrappers by
|
|
87
|
+
* `scripts/generate_hook_wrappers.sh` and into the locator below, and held
|
|
88
|
+
* equal by `mirrorNodeRule.test.ts`. It classifies exactly as
|
|
89
|
+
* `path.win32.isAbsolute`, on every platform: under Git Bash the Windows
|
|
90
|
+
* installer writes `C:\Program Files\nodejs\node.exe`, which a POSIX-only
|
|
91
|
+
* pattern would refuse on every hook (PR #47). On POSIX a drive or backslash
|
|
92
|
+
* value names no file, and is refused as missing.
|
|
93
|
+
*/
|
|
94
|
+
export const ABSOLUTE_PATH_CASE = "/*|\\\\*|[A-Za-z]:[\\\\/]*";
|
|
95
|
+
|
|
84
96
|
/**
|
|
85
97
|
* The MCP entry's locator (CV21.DS2.US6).
|
|
86
98
|
*
|
|
@@ -93,6 +105,12 @@ export const GLOBAL_BIN_DIRS = ['"$HOME/.nvm/current/bin"', "/opt/homebrew/bin",
|
|
|
93
105
|
* launch) still starts the server, and one with no `mirror` anywhere gets one
|
|
94
106
|
* stderr line naming the install step, and exit 1.
|
|
95
107
|
*
|
|
108
|
+
* `MIRROR_NODE` is used only as an absolute path to an executable file, as
|
|
109
|
+
* every reader uses it (CR192); anything else gets one stderr line with the
|
|
110
|
+
* reason, in the Pi extension's words, and the search goes on without it. The
|
|
111
|
+
* value is read once, into `mmnode`, and printed with `printf` and its line
|
|
112
|
+
* breaks removed: dash's `echo` turns the `\n` of `\nodejs` into a newline.
|
|
113
|
+
*
|
|
96
114
|
* Written for what three runtimes do to the string (the US6 spike):
|
|
97
115
|
*
|
|
98
116
|
* * POSIX `sh`: `/bin/sh` is dash on Linux;
|
|
@@ -117,7 +135,10 @@ export const MCP_LOCATOR = [
|
|
|
117
135
|
'mmjs="$(dirname "$mmbin")/node_modules/mirror-mind/bin/mirror.js"',
|
|
118
136
|
'[ -f "$mmjs" ] && mmbin="$mmjs"',
|
|
119
137
|
'mmnode="$MIRROR_NODE"',
|
|
120
|
-
'
|
|
138
|
+
'mmwhy=""',
|
|
139
|
+
`[ -n "$mmnode" ] && case "$mmnode" in ${ABSOLUTE_PATH_CASE}) if [ -d "$mmnode" ]; then mmwhy="a directory"; elif [ ! -e "$mmnode" ]; then mmwhy="missing"; elif [ ! -f "$mmnode" ] || [ ! -x "$mmnode" ]; then mmwhy="not executable"; fi ;; *) mmwhy="relative" ;; esac`,
|
|
140
|
+
`[ -n "$mmwhy" ] && { printf 'mirror-mind: MIRROR_NODE=%s is %s; searching the PATH and the usual places instead\\n' "$(printf '%s' "$mmnode" | tr -d '\\r\\n')" "$mmwhy" >&2; mmnode=""; }`,
|
|
141
|
+
'[ -n "$mmnode" ] || mmnode="$(command -v node 2>/dev/null)"',
|
|
121
142
|
`for mmdir in "$(dirname "$mmbin")" ${GLOBAL_BIN_DIRS.join(" ")}; do [ -n "$mmnode" ] && break; [ -x "$mmdir/node" ] && mmnode="$mmdir/node"; done`,
|
|
122
143
|
'[ -n "$mmnode" ] || { echo "mirror-mind: node not found on PATH; the MCP server cannot start. Set MIRROR_NODE." >&2; exit 1; }',
|
|
123
144
|
'exec "$mmnode" "$mmbin" mcp',
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// replay substrate or reports inconclusive, exactly as the oracle reports
|
|
25
25
|
// inconclusive without a key; the live fetch belongs to DS8.
|
|
26
26
|
|
|
27
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
27
|
+
import { accessSync, constants, existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
28
28
|
import { basename, dirname, join, posix, win32 } from "node:path";
|
|
29
29
|
import { openDatabaseReadOnly } from "#db/database.ts";
|
|
30
30
|
import {
|
|
@@ -842,6 +842,22 @@ export const HOOK_NODE_CANDIDATES: readonly string[] = [
|
|
|
842
842
|
"/usr/local/bin/node",
|
|
843
843
|
];
|
|
844
844
|
|
|
845
|
+
/**
|
|
846
|
+
* An executable that is not a directory: what every reader of `MIRROR_NODE`
|
|
847
|
+
* accepts (CR192), and what `hookNodeFindings` is handed to probe with.
|
|
848
|
+
* `X_OK` alone passes a directory, and on Windows it is only an existence
|
|
849
|
+
* check, so the directory is ruled out first.
|
|
850
|
+
*/
|
|
851
|
+
export function isExecutableFile(path: string): boolean {
|
|
852
|
+
try {
|
|
853
|
+
if (statSync(path).isDirectory()) return false;
|
|
854
|
+
accessSync(path, constants.X_OK);
|
|
855
|
+
return true;
|
|
856
|
+
} catch {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
845
861
|
/** How far back `hookFailureFindings` looks. Longer than the front door's day,
|
|
846
862
|
* because a hook failure is the kind a user notices weeks later. */
|
|
847
863
|
const HOOK_FAILURE_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -915,17 +931,20 @@ export function hookNodeFindings(
|
|
|
915
931
|
isExecutable: (path: string) => boolean,
|
|
916
932
|
platform: NodeJS.Platform = process.platform,
|
|
917
933
|
): DriftFinding[] {
|
|
918
|
-
const explicit = env.MIRROR_NODE ?? "";
|
|
919
|
-
if (explicit && isExecutable(explicit)) return [];
|
|
920
|
-
const candidates = HOOK_NODE_CANDIDATES.map((candidate) =>
|
|
921
|
-
candidate.replace("$HOME", env.HOME ?? ""),
|
|
922
|
-
);
|
|
923
934
|
// The platform's PATH separator and its executable name: `;` and `node.exe`
|
|
924
935
|
// on Windows, where splitting on `:` cut `C:\Program Files\nodejs` into `C`
|
|
925
936
|
// and a path and the finding fired on every install that had Node (PR #49).
|
|
926
937
|
// `platform` is a parameter so the test runs both branches on every host:
|
|
927
938
|
// CI's Windows job runs two files and this one is not among them.
|
|
928
939
|
const paths = platform === "win32" ? win32 : posix;
|
|
940
|
+
// MIRROR_NODE counts only as every hook reads it: an absolute path to an
|
|
941
|
+
// executable file (CR192). A relative one would resolve against this
|
|
942
|
+
// terminal's cwd, and no hook would use it.
|
|
943
|
+
const explicit = env.MIRROR_NODE ?? "";
|
|
944
|
+
if (explicit && paths.isAbsolute(explicit) && isExecutable(explicit)) return [];
|
|
945
|
+
const candidates = HOOK_NODE_CANDIDATES.map((candidate) =>
|
|
946
|
+
candidate.replace("$HOME", env.HOME ?? ""),
|
|
947
|
+
);
|
|
929
948
|
const name = platform === "win32" ? "node.exe" : "node";
|
|
930
949
|
const onPath = (env.PATH ?? "")
|
|
931
950
|
.split(paths.delimiter)
|