@memnexus-ai/cli 1.7.162 → 1.7.164

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.
@@ -0,0 +1,225 @@
1
+ #!/bin/bash
2
+ # capture-session-end.sh — Automatic memory capture on session activity
3
+ #
4
+ # Installed at ~/.memnexus/hooks/ by `mx mcp install` for end-user lifecycle
5
+ # hook integration with Claude Code.
6
+ #
7
+ # Creates a session-level summary memory capturing what was accomplished, key
8
+ # decisions, and next steps. This is the "wrap-up" signal — a holistic view of
9
+ # the session that individual turn-level captures would miss.
10
+ #
11
+ # Fires on: Stop (fires on every turn completion, but saves only when enough
12
+ # new work has accumulated since the last save — debounced at >100
13
+ # new transcript lines)
14
+ # Input: JSON on stdin with transcript_path, session_id
15
+ # Output: none (side-effect only — saves memory via mx CLI)
16
+ # Exit: always 0 (never blocks)
17
+
18
+ # Fail-open: any error -> allow session to end normally
19
+ trap 'exit 0' ERR
20
+
21
+ # ── 1. Check required tools ──────────────────────────────────────────────
22
+ command -v jq >/dev/null 2>&1 || exit 0
23
+ command -v mx >/dev/null 2>&1 || exit 0
24
+
25
+ # ── 2. Parse hook input ──────────────────────────────────────────────────
26
+ INPUT=$(cat)
27
+
28
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty' 2>/dev/null)
29
+ _SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
30
+ STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
31
+
32
+ # Skip if already in a hook loop
33
+ if [[ "$STOP_HOOK_ACTIVE" == "true" ]]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Skip if no transcript
38
+ if [[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]; then
39
+ exit 0
40
+ fi
41
+
42
+ # Validate transcript path — resolve symlinks
43
+ REAL_TRANSCRIPT=$(realpath "$TRANSCRIPT_PATH" 2>/dev/null) || exit 0
44
+ if [[ ! -f "$REAL_TRANSCRIPT" ]]; then
45
+ exit 0
46
+ fi
47
+ TRANSCRIPT_PATH="$REAL_TRANSCRIPT"
48
+
49
+ # ── 3. Derive project name and debounce state file ───────────────────────
50
+ # Derive a stable project identifier from the transcript path or cwd.
51
+ # Use a hash of the project path so the state file is unique per project.
52
+ PROJECT_DIR=$(dirname "$TRANSCRIPT_PATH" 2>/dev/null)
53
+ if [[ -z "$PROJECT_DIR" || "$PROJECT_DIR" == "." ]]; then
54
+ PROJECT_DIR=$(pwd)
55
+ fi
56
+ PROJECT_NAME=$(basename "$PROJECT_DIR" 2>/dev/null || echo "unknown")
57
+
58
+ # Hash the project directory path for a unique, collision-free state filename
59
+ PROJECT_HASH=$(echo "$PROJECT_DIR" | md5sum 2>/dev/null | awk '{print $1}' || \
60
+ echo "$PROJECT_DIR" | sha256sum 2>/dev/null | awk '{print $1}' || \
61
+ echo "$PROJECT_DIR" | cksum | awk '{print $1}')
62
+ STATE_DIR="${XDG_RUNTIME_DIR:-${HOME}/.memnexus/state}"
63
+ mkdir -p "$STATE_DIR" 2>/dev/null
64
+ STATE_FILE="${STATE_DIR}/.mx-session-capture-${PROJECT_HASH}"
65
+
66
+ # ── 4. Debounce — only save when significant new work has accumulated ─────
67
+ CURRENT_LINES=$(wc -l < "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
68
+
69
+ LAST_SAVED_LINES=0
70
+ if [[ -f "$STATE_FILE" ]]; then
71
+ LAST_SAVED_LINES=$(cat "$STATE_FILE" 2>/dev/null || echo "0")
72
+ fi
73
+
74
+ LINES_SINCE_SAVE=$((CURRENT_LINES - LAST_SAVED_LINES))
75
+
76
+ # Only save if significant work has happened (>100 transcript lines since last save)
77
+ # This prevents saving on every trivial stop event
78
+ if [[ "$LINES_SINCE_SAVE" -lt 100 ]]; then
79
+ exit 0
80
+ fi
81
+
82
+ # ── 5. Extract session summary data ──────────────────────────────────────
83
+
84
+ tmpdir=$(mktemp -d)
85
+ trap 'rm -rf "$tmpdir"; exit 0' EXIT ERR
86
+
87
+ # Only look at lines since last save to avoid re-summarising old content
88
+ OFFSET=$((LAST_SAVED_LINES + 1))
89
+ if [[ "$LAST_SAVED_LINES" -gt 0 ]]; then
90
+ SEGMENT=$(tail -n +"${OFFSET}" "$TRANSCRIPT_PATH")
91
+ else
92
+ SEGMENT=$(cat "$TRANSCRIPT_PATH")
93
+ fi
94
+
95
+ # Files modified in this segment
96
+ FILES_MODIFIED=$(echo "$SEGMENT" | jq -r '
97
+ select(.type == "assistant") |
98
+ .message.content[]? |
99
+ select(.type == "tool_use" and (.name == "Write" or .name == "Edit")) |
100
+ .input.file_path // empty
101
+ ' 2>/dev/null | grep -v '^$' | sort -u | head -20)
102
+
103
+ # Tool call count
104
+ TOOL_COUNT=$(echo "$SEGMENT" | jq -r '
105
+ select(.type == "assistant") |
106
+ .message.content[]? |
107
+ select(.type == "tool_use") | .name
108
+ ' 2>/dev/null | wc -l | tr -d ' ')
109
+
110
+ # Skip if segment was trivial — update state so we don't recheck same lines
111
+ if [[ "$TOOL_COUNT" -lt 5 && -z "$FILES_MODIFIED" ]]; then
112
+ echo "$CURRENT_LINES" > "$STATE_FILE" 2>/dev/null || true
113
+ exit 0
114
+ fi
115
+
116
+ # Git activity in this segment
117
+ BASH_COMMANDS=$(echo "$SEGMENT" | jq -r '
118
+ select(.type == "assistant") |
119
+ .message.content[]? |
120
+ select(.type == "tool_use" and .name == "Bash") |
121
+ .input.command // empty
122
+ ' 2>/dev/null)
123
+
124
+ GIT_COMMITS=$(echo "$BASH_COMMANDS" | grep -cE 'git commit' 2>/dev/null || echo "0")
125
+ PR_REFS=$(echo "$BASH_COMMANDS" | grep -oE 'gh pr (create|merge) [^\n]*' 2>/dev/null | head -5)
126
+
127
+ # User messages (intent)
128
+ USER_MESSAGES=$(echo "$SEGMENT" | jq -r '
129
+ select(.type == "user") |
130
+ .message.content |
131
+ if type == "array" then
132
+ [.[] | select(.type == "text") | .text] | join("\n")
133
+ elif type == "string" then .
134
+ else empty end
135
+ ' 2>/dev/null | head -c 2000)
136
+
137
+ # Recent assistant conclusions
138
+ RECENT_TEXT=$(echo "$SEGMENT" | tail -50 | jq -r '
139
+ select(.type == "assistant") |
140
+ .message.content |
141
+ if type == "array" then
142
+ [.[] | select(.type == "text") | .text] | join("\n")
143
+ elif type == "string" then .
144
+ else empty end
145
+ ' 2>/dev/null | tail -c 1500)
146
+
147
+ # PR and issue references
148
+ GH_REFS=$(echo "$SEGMENT" | jq -r '
149
+ select(.type == "user" or .type == "assistant") |
150
+ .message.content |
151
+ if type == "array" then
152
+ [.[] | select(.type == "text") | .text] | join("\n")
153
+ elif type == "string" then .
154
+ else empty end
155
+ ' 2>/dev/null | grep -oE '(PR |#)[0-9]+' | sort -u | head -10 | tr '\n' ', ' | sed 's/,$//')
156
+
157
+ # ── 5b. Sanitize sensitive data ──────────────────────────────────────────
158
+ sanitize() {
159
+ sed -E \
160
+ -e 's/(api[_-]?key|token|secret|password|bearer|authorization)[[:space:]]*[:=][[:space:]]*[^[:space:],\"'"'"']+/\1=<REDACTED>/gi' \
161
+ -e 's/cmk_live_[A-Za-z0-9_-]+/cmk_live_<REDACTED>/g' \
162
+ -e 's/sk-[A-Za-z0-9_-]{20,}/sk-<REDACTED>/g' \
163
+ -e 's/ghp_[A-Za-z0-9]{36}/ghp_<REDACTED>/g' \
164
+ -e 's/gho_[A-Za-z0-9]{36}/gho_<REDACTED>/g' \
165
+ -e 's/-----BEGIN [A-Z ]*PRIVATE KEY-----/-----BEGIN REDACTED KEY-----/g'
166
+ }
167
+
168
+ USER_MESSAGES=$(echo "$USER_MESSAGES" | sanitize)
169
+ BASH_COMMANDS=$(echo "$BASH_COMMANDS" | sanitize)
170
+ RECENT_TEXT=$(echo "$RECENT_TEXT" | sanitize)
171
+
172
+ # ── 6. Build session summary ─────────────────────────────────────────────
173
+ GIT_BRANCH=$(git -C "$PROJECT_DIR" branch --show-current 2>/dev/null || \
174
+ git branch --show-current 2>/dev/null || echo "unknown")
175
+
176
+ MOD_LIST=""
177
+ if [[ -n "$FILES_MODIFIED" ]]; then
178
+ MOD_LIST=$(echo "$FILES_MODIFIED" | sed 's/^/- /')
179
+ fi
180
+
181
+ CONTENT="[Auto-Captured] Session activity — ${PROJECT_NAME}
182
+
183
+ Branch: ${GIT_BRANCH}
184
+ Commits: ${GIT_COMMITS}
185
+ Tool calls: ${TOOL_COUNT}
186
+ GitHub refs: ${GH_REFS:-none}
187
+
188
+ Files modified:
189
+ ${MOD_LIST:-None}
190
+
191
+ What was worked on:
192
+ $(echo "$USER_MESSAGES" | head -c 1200)
193
+
194
+ Outcomes:
195
+ $(echo "$RECENT_TEXT" | head -c 1200)"
196
+
197
+ # Append PR activity if any
198
+ if [[ -n "$PR_REFS" ]]; then
199
+ CONTENT="${CONTENT}
200
+
201
+ PR activity:
202
+ ${PR_REFS}"
203
+ fi
204
+
205
+ # Truncate to ~5000 chars
206
+ CONTENT=$(echo "$CONTENT" | head -c 5000)
207
+
208
+ # ── 7. Save memory (with timeout — never block session exit) ─────────────
209
+ TIMEOUT_CMD=""
210
+ if command -v timeout >/dev/null 2>&1; then
211
+ TIMEOUT_CMD="timeout 10"
212
+ elif command -v gtimeout >/dev/null 2>&1; then
213
+ TIMEOUT_CMD="gtimeout 10"
214
+ fi
215
+
216
+ $TIMEOUT_CMD mx memories create \
217
+ --conversation-id "NEW" \
218
+ --content "$CONTENT" \
219
+ --topics "auto-captured,session-summary" \
220
+ >/dev/null 2>&1 || true
221
+
222
+ # ── 8. Update debounce state ─────────────────────────────────────────────
223
+ echo "$CURRENT_LINES" > "$STATE_FILE" 2>/dev/null || true
224
+
225
+ exit 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memnexus-ai/cli",
3
- "version": "1.7.162",
3
+ "version": "1.7.164",
4
4
  "description": "Command-line interface for MemNexus Core API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "files": [
23
23
  "dist/",
24
24
  "bin/",
25
+ "hooks/",
25
26
  "README.md",
26
27
  "CHANGELOG.md",
27
28
  "USAGE.md"