shrinker-ai 0.3.2 → 0.4.0

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.
@@ -13,7 +13,9 @@ SKIP_PROFILE=0
13
13
  SKIP_AGENT_RULES=0
14
14
  COPILOT_ONLY=0
15
15
  CLAUDE_ONLY=0
16
+ TRACK_UNCOVERED=""
16
17
  PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
18
+ CONFIG_PATH="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
17
19
  STEP="${SHRINKER_STEP_OFFSET:-0}"
18
20
 
19
21
  print_message() {
@@ -34,6 +36,8 @@ while [[ $# -gt 0 ]]; do
34
36
  --skip-link) SKIP_LINK=1 ;;
35
37
  --enable-profile-routing) ENABLE_PROFILE_ROUTING=1 ;;
36
38
  --skip-profile) SKIP_PROFILE=1 ;;
39
+ --enable-uncovered-tracking) TRACK_UNCOVERED=1 ;;
40
+ --disable-uncovered-tracking) TRACK_UNCOVERED=0 ;;
37
41
  --skip-agent-rules) SKIP_AGENT_RULES=1 ;;
38
42
  --copilot-only) COPILOT_ONLY=1 ;;
39
43
  --claude-only) CLAUDE_ONLY=1 ;;
@@ -46,6 +50,39 @@ done
46
50
  if (( ENABLE_PROFILE_ROUTING == 1 && SKIP_PROFILE == 1 )); then print_message "❌" "Use either --enable-profile-routing or --skip-profile, not both." >&2; exit 1; fi
47
51
  if (( COPILOT_ONLY == 1 && CLAUDE_ONLY == 1 )); then print_message "❌" "Use either --copilot-only or --claude-only, not both." >&2; exit 1; fi
48
52
 
53
+ # Prompt only on a TTY; non-interactive installs keep tracking on and leave the shell profile alone.
54
+ prompt_yes_no() {
55
+ local question="$1"
56
+ local default_answer="$2"
57
+ local reply
58
+ local hint="[y/N]"
59
+ [[ "$default_answer" == "1" ]] && hint="[Y/n]"
60
+ printf ' %s %s ' "$question" "$hint" > /dev/tty
61
+ IFS= read -r reply < /dev/tty || reply=""
62
+ case "$(printf '%s' "$reply" | tr '[:upper:]' '[:lower:]')" in
63
+ y|yes) echo 1 ;;
64
+ n|no) echo 0 ;;
65
+ *) echo "$default_answer" ;;
66
+ esac
67
+ }
68
+
69
+ interactive=0
70
+ [[ -t 0 && -r /dev/tty ]] && interactive=1
71
+
72
+ if [[ -z "$TRACK_UNCOVERED" ]]; then
73
+ if (( interactive )); then
74
+ print_message "❓" "Track uncovered commands? Records which unfiltered commands cost the most tokens."
75
+ TRACK_UNCOVERED="$(prompt_yes_no "Enable uncovered-command tracking?" 1)"
76
+ else
77
+ TRACK_UNCOVERED=1
78
+ fi
79
+ fi
80
+
81
+ if (( interactive && SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 0 )); then
82
+ print_message "❓" "Route wrapped commands through shrinker automatically? Adds a source line to $PROFILE_PATH."
83
+ ENABLE_PROFILE_ROUTING="$(prompt_yes_no "Enable automatic shell routing?" 0)"
84
+ fi
85
+
49
86
  if (( LOCAL == 0 )); then
50
87
  package_spec="$PACKAGE_NAME"
51
88
  [[ -z "$VERSION" ]] || package_spec="$PACKAGE_NAME@$VERSION"
@@ -60,6 +97,7 @@ if (( LOCAL == 0 )); then
60
97
  $( (( SKIP_AGENT_RULES )) && echo --skip-agent-rules ) \
61
98
  $( (( COPILOT_ONLY )) && echo --copilot-only ) \
62
99
  $( (( CLAUDE_ONLY )) && echo --claude-only ) \
100
+ $( (( TRACK_UNCOVERED )) && echo --enable-uncovered-tracking || echo --disable-uncovered-tracking ) \
63
101
  --profile-path "$PROFILE_PATH"
64
102
  if [[ -n "${BASH_SOURCE[0]:-}" && "${BASH_SOURCE[0]}" != "$0" ]]; then
65
103
  return 0
@@ -123,6 +161,20 @@ add_profile_integration() {
123
161
  fi
124
162
  }
125
163
 
164
+ set_config_value() {
165
+ local key="$1"
166
+ local value="$2"
167
+ mkdir -p "$(dirname "$CONFIG_PATH")"
168
+ chmod 700 "$(dirname "$CONFIG_PATH")" 2>/dev/null || true
169
+ touch "$CONFIG_PATH"
170
+ if grep -Eq "^[[:space:]]*$key[[:space:]]*=" "$CONFIG_PATH"; then
171
+ grep -Ev "^[[:space:]]*$key[[:space:]]*=" "$CONFIG_PATH" > "$CONFIG_PATH.tmp" || true
172
+ mv "$CONFIG_PATH.tmp" "$CONFIG_PATH"
173
+ fi
174
+ printf '%s=%s\n' "$key" "$value" >> "$CONFIG_PATH"
175
+ print_message "⚙️" "Set $key=$value in: $CONFIG_PATH"
176
+ }
177
+
126
178
  print_message "📦" "Installing shrinker from: $REPO_ROOT"
127
179
  pushd "$REPO_ROOT" >/dev/null
128
180
  if (( SKIP_NPM_INSTALL == 0 )); then
@@ -131,7 +183,7 @@ if (( SKIP_NPM_INSTALL == 0 )); then
131
183
  fi
132
184
  if (( SKIP_BUILD == 0 )); then
133
185
  print_message "🏗️" "Building shrinker..."
134
- npm run build --silent
186
+ SHRINKER_BUILD_QUIET=1 npm run build --silent
135
187
  fi
136
188
  if (( SKIP_LINK == 0 )); then
137
189
  print_message "🔗" "Linking shrinker globally..."
@@ -139,6 +191,7 @@ if (( SKIP_LINK == 0 )); then
139
191
  fi
140
192
  popd >/dev/null
141
193
 
194
+ set_config_value "SHRINKER_TRACK_UNCOVERED" "$TRACK_UNCOVERED"
142
195
  if (( SKIP_PROFILE == 0 && ENABLE_PROFILE_ROUTING == 1 )); then add_profile_integration "$PROFILE_PATH"; fi
143
196
  if (( SKIP_AGENT_RULES == 0 )); then
144
197
  [[ -f "$TEMPLATE_PATH" ]] || { print_message "❌" "Agent rules template not found: $TEMPLATE_PATH" >&2; exit 1; }
@@ -1,98 +1,165 @@
1
- typeset -gA SHRINK_RULES
2
- SHRINK_RULES=(
3
- git "status diff log show reflog branch tag stash"
4
- npm "test t install i ci ls list"
5
- docker "ps logs images compose"
6
- kubectl "get describe logs"
7
- gh "pr issue run"
8
- rg "*"
9
- find "*"
10
- tail "*"
11
- cat "*"
12
- ls "*"
13
- )
14
-
15
- typeset -gA SHRINK_OPTION_VALUE_FLAGS
16
- SHRINK_OPTION_VALUE_FLAGS=(
17
- git "-C -c --git-dir --work-tree --namespace"
18
- npm "--prefix --cache --registry --workspace --userconfig -w -C"
19
- docker "-H --host --context --config"
20
- kubectl "-n --namespace -o --output --context --kubeconfig --cluster --user"
21
- gh "-R --repo"
22
- )
23
-
24
- _shrink_get_subcommand() {
25
- local cmd="$1"
26
- shift
27
-
28
- local -a args
29
- args=("$@")
30
-
31
- local -a value_flags
32
- value_flags=(${=SHRINK_OPTION_VALUE_FLAGS[$cmd]})
33
-
34
- local i part
35
- i=1
36
- while (( i <= ${#args[@]} )); do
37
- part="${args[$i]}"
38
- if [[ -z "$part" ]]; then
39
- (( i++ ))
40
- continue
41
- fi
42
-
43
- if (( ${value_flags[(Ie)$part]} > 0 )); then
44
- (( i += 2 ))
45
- continue
46
- fi
47
-
48
- if [[ "$part" == -* ]]; then
49
- (( i++ ))
50
- continue
51
- fi
52
-
53
- print -r -- "${part:l}"
54
- return 0
55
- done
56
-
57
- print -r -- ""
58
- }
59
-
60
- _shrink_should_route() {
61
- local cmd="$1"
62
- shift
63
-
64
- local rules="${SHRINK_RULES[$cmd]}"
65
- [[ -z "$rules" ]] && return 1
66
- [[ "$rules" == *"*"* ]] && return 0
67
-
68
- local subcommand
69
- subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
70
- [[ -z "$subcommand" ]] && return 1
71
-
72
- local -a allowlist
73
- allowlist=(${=rules})
74
- (( ${allowlist[(Ie)$subcommand]} > 0 ))
75
- }
76
-
77
- _shrink_invoke_or_native() {
78
- local cmd="$1"
79
- shift
80
-
81
- if _shrink_should_route "$cmd" "$@" && command -v shrinker >/dev/null 2>&1; then
82
- command shrinker "$cmd" "$@"
83
- return $?
84
- fi
85
-
86
- command "$cmd" "$@"
87
- }
88
-
89
- git() { _shrink_invoke_or_native git "$@"; }
90
- npm() { _shrink_invoke_or_native npm "$@"; }
91
- docker() { _shrink_invoke_or_native docker "$@"; }
92
- kubectl() { _shrink_invoke_or_native kubectl "$@"; }
93
- gh() { _shrink_invoke_or_native gh "$@"; }
94
- rg() { _shrink_invoke_or_native rg "$@"; }
95
- find() { _shrink_invoke_or_native find "$@"; }
96
- tail() { _shrink_invoke_or_native tail "$@"; }
97
- cat() { _shrink_invoke_or_native cat "$@"; }
98
- ls() { _shrink_invoke_or_native ls "$@"; }
1
+ typeset -gA SHRINK_RULES
2
+ SHRINK_RULES=(
3
+ git "status diff log show reflog branch tag stash"
4
+ npm "test t install i ci ls list"
5
+ docker "ps logs images compose"
6
+ kubectl "get describe logs"
7
+ gh "pr issue run"
8
+ rg "*"
9
+ find "*"
10
+ tail "*"
11
+ cat "*"
12
+ ls "*"
13
+ )
14
+
15
+ typeset -gA SHRINK_OPTION_VALUE_FLAGS
16
+ SHRINK_OPTION_VALUE_FLAGS=(
17
+ git "-C -c --git-dir --work-tree --namespace"
18
+ npm "--prefix --cache --registry --workspace --userconfig -w -C"
19
+ docker "-H --host --context --config"
20
+ kubectl "-n --namespace -o --output --context --kubeconfig --cluster --user"
21
+ gh "-R --repo"
22
+ )
23
+
24
+ # Resolved once at load so wrapped commands never pay for reading the config file.
25
+ typeset -g SHRINK_TRACK_UNCOVERED_DEFAULT=""
26
+ _shrink_load_config() {
27
+ local config="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
28
+ [[ -r "$config" ]] || return 0
29
+ local value
30
+ value="$(sed -n 's/^[[:space:]]*SHRINKER_TRACK_UNCOVERED[[:space:]]*=[[:space:]]*\([^#[:space:]]*\).*/\1/p' "$config" | tail -n 1)"
31
+ SHRINK_TRACK_UNCOVERED_DEFAULT="$value"
32
+ }
33
+ _shrink_load_config
34
+
35
+ _shrink_uncovered_enabled() {
36
+ local value="${SHRINKER_TRACK_UNCOVERED:-$SHRINK_TRACK_UNCOVERED_DEFAULT}"
37
+ case "${value:l}" in
38
+ 1|true|yes) return 0 ;;
39
+ *) return 1 ;;
40
+ esac
41
+ }
42
+
43
+ _shrink_get_subcommand() {
44
+ local cmd="$1"
45
+ shift
46
+
47
+ local -a args
48
+ args=("$@")
49
+
50
+ local -a value_flags
51
+ value_flags=(${=SHRINK_OPTION_VALUE_FLAGS[$cmd]})
52
+
53
+ local i part
54
+ i=1
55
+ while (( i <= ${#args[@]} )); do
56
+ part="${args[$i]}"
57
+ if [[ -z "$part" ]]; then
58
+ (( i++ ))
59
+ continue
60
+ fi
61
+
62
+ if (( ${value_flags[(Ie)$part]} > 0 )); then
63
+ (( i += 2 ))
64
+ continue
65
+ fi
66
+
67
+ if [[ "$part" == -* ]]; then
68
+ (( i++ ))
69
+ continue
70
+ fi
71
+
72
+ print -r -- "${part:l}"
73
+ return 0
74
+ done
75
+
76
+ print -r -- ""
77
+ }
78
+
79
+ _shrink_should_route() {
80
+ local cmd="$1"
81
+ shift
82
+
83
+ local rules="${SHRINK_RULES[$cmd]}"
84
+ [[ -z "$rules" ]] && return 1
85
+ [[ "$rules" == *"*"* ]] && return 0
86
+
87
+ local subcommand
88
+ subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
89
+ [[ -z "$subcommand" ]] && return 1
90
+
91
+ local -a allowlist
92
+ allowlist=(${=rules})
93
+ (( ${allowlist[(Ie)$subcommand]} > 0 ))
94
+ }
95
+
96
+ _shrink_track_uncovered() {
97
+ local cmd="$1"
98
+ local bytes="$2"
99
+ local status_code="$3"
100
+ shift 3
101
+
102
+ _shrink_uncovered_enabled || return 0
103
+ command -v shrinker >/dev/null 2>&1 || return 0
104
+
105
+ local subcommand
106
+ subcommand="$(_shrink_get_subcommand "$cmd" "$@")"
107
+
108
+ local -a track_args
109
+ track_args=(track --executable "$cmd" --bytes "$bytes" --exit-code "$status_code")
110
+ [[ -n "$subcommand" ]] && track_args+=(--subcommand "$subcommand")
111
+
112
+ (command shrinker "${track_args[@]}" >/dev/null 2>&1 &)
113
+ }
114
+
115
+ _shrink_run_native_tracked() {
116
+ local cmd="$1"
117
+ shift
118
+
119
+ if ! _shrink_uncovered_enabled; then
120
+ command "$cmd" "$@"
121
+ return $?
122
+ fi
123
+
124
+ # Only measure output volume when stdout is redirected (the agent case). Interactive
125
+ # terminals keep the native command untouched so pagers and prompts still work.
126
+ if [[ -t 1 ]]; then
127
+ command "$cmd" "$@"
128
+ local status_code=$?
129
+ _shrink_track_uncovered "$cmd" 0 "$status_code" "$@"
130
+ return $status_code
131
+ fi
132
+
133
+ local capture
134
+ capture="$(mktemp -t shrinker-track)" || { command "$cmd" "$@"; return $?; }
135
+ command "$cmd" "$@" | tee "$capture"
136
+ local status_code=${pipestatus[1]}
137
+ local bytes
138
+ bytes="$(wc -c < "$capture" | tr -d ' ')"
139
+ rm -f "$capture"
140
+ _shrink_track_uncovered "$cmd" "${bytes:-0}" "$status_code" "$@"
141
+ return $status_code
142
+ }
143
+
144
+ _shrink_invoke_or_native() {
145
+ local cmd="$1"
146
+ shift
147
+
148
+ if _shrink_should_route "$cmd" "$@" && command -v shrinker >/dev/null 2>&1; then
149
+ command shrinker "$cmd" "$@"
150
+ return $?
151
+ fi
152
+
153
+ _shrink_run_native_tracked "$cmd" "$@"
154
+ }
155
+
156
+ git() { _shrink_invoke_or_native git "$@"; }
157
+ npm() { _shrink_invoke_or_native npm "$@"; }
158
+ docker() { _shrink_invoke_or_native docker "$@"; }
159
+ kubectl() { _shrink_invoke_or_native kubectl "$@"; }
160
+ gh() { _shrink_invoke_or_native gh "$@"; }
161
+ rg() { _shrink_invoke_or_native rg "$@"; }
162
+ find() { _shrink_invoke_or_native find "$@"; }
163
+ tail() { _shrink_invoke_or_native tail "$@"; }
164
+ cat() { _shrink_invoke_or_native cat "$@"; }
165
+ ls() { _shrink_invoke_or_native ls "$@"; }
@@ -7,7 +7,11 @@ SKIP_UNLINK=0
7
7
  SKIP_AGENT_RULES=0
8
8
  COPILOT_ONLY=0
9
9
  CLAUDE_ONLY=0
10
+ PURGE_DATA=0
11
+ DASHBOARD_PORT=4317
10
12
  PROFILE_PATH="${PROFILE_PATH:-$HOME/.zshrc}"
13
+ CONFIG_PATH="${SHRINKER_CONFIG_PATH:-$HOME/.shrinker/config}"
14
+ DATA_DIR="$(dirname "$CONFIG_PATH")"
11
15
  UNINSTALL_STEP=0
12
16
 
13
17
  print_step() {
@@ -25,6 +29,8 @@ while [[ $# -gt 0 ]]; do
25
29
  --profile-path) PROFILE_PATH="${2:-}"; shift ;;
26
30
  --install-dir) INSTALL_DIR="${2:-}"; shift ;;
27
31
  --remove-install-dir) REMOVE_INSTALL_DIR=1 ;;
32
+ --purge-data) PURGE_DATA=1 ;;
33
+ --port) DASHBOARD_PORT="${2:-}"; shift ;;
28
34
  *) echo "Unknown option: $1" >&2; exit 1 ;;
29
35
  esac
30
36
  shift
@@ -48,6 +54,38 @@ remove_profile_integration() {
48
54
  mv "$profile_file.tmp" "$profile_file"
49
55
  }
50
56
 
57
+ # The dashboard runs as a detached daemon, so unlinking the package never stops it.
58
+ stop_dashboard_server() {
59
+ local url="http://127.0.0.1:$DASHBOARD_PORT"
60
+ if command -v curl >/dev/null 2>&1; then
61
+ curl -fsS -m 3 -X POST "$url/__shrinker_shutdown" -o /dev/null 2>/dev/null || true
62
+ sleep 1
63
+ fi
64
+
65
+ local pid
66
+ pid="$(lsof -nP -iTCP:"$DASHBOARD_PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1 || true)"
67
+ if [[ -n "$pid" ]]; then
68
+ kill "$pid" 2>/dev/null || true
69
+ sleep 1
70
+ pid="$(lsof -nP -iTCP:"$DASHBOARD_PORT" -sTCP:LISTEN -t 2>/dev/null | head -n 1 || true)"
71
+ [[ -n "$pid" ]] && kill -9 "$pid" 2>/dev/null || true
72
+ fi
73
+ }
74
+
75
+ # Only drop installer-managed keys so hand-written settings survive.
76
+ remove_managed_config() {
77
+ [[ -f "$CONFIG_PATH" ]] || return 0
78
+ grep -Ev '^[[:space:]]*SHRINKER_TRACK_UNCOVERED[[:space:]]*=' "$CONFIG_PATH" > "$CONFIG_PATH.tmp" || true
79
+ if [[ -s "$CONFIG_PATH.tmp" ]]; then
80
+ mv "$CONFIG_PATH.tmp" "$CONFIG_PATH"
81
+ else
82
+ rm -f "$CONFIG_PATH.tmp" "$CONFIG_PATH"
83
+ fi
84
+ }
85
+
86
+ print_step "🛑" "Stopping the dashboard server on port $DASHBOARD_PORT..."
87
+ stop_dashboard_server
88
+
51
89
  if (( SKIP_UNLINK == 0 )); then
52
90
  print_step "🔗" "Unlinking shrinker globally..."
53
91
  npm unlink --silent --global shrinker-ai
@@ -62,6 +100,18 @@ if (( SKIP_AGENT_RULES == 0 )); then
62
100
  else
63
101
  print_step "⏭️" "Skipped managed agent rules."
64
102
  fi
103
+
104
+ print_step "🔧" "Removing managed settings..."
105
+ remove_managed_config
106
+ rm -f "$DATA_DIR/dashboard.html"
107
+
108
+ if (( PURGE_DATA == 1 )); then
109
+ rm -rf "$DATA_DIR"
110
+ print_step "🧹" "Removed local data: $DATA_DIR"
111
+ elif [[ -d "$DATA_DIR" ]]; then
112
+ print_step "💾" "Kept saved stats in $DATA_DIR (use --purge-data to delete)."
113
+ fi
114
+
65
115
  print_step "✅" "Uninstall complete."
66
116
 
67
117
  if (( REMOVE_INSTALL_DIR == 1 )) && [[ -d "$INSTALL_DIR" ]]; then