@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.
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +52 -0
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/memories.d.ts.map +1 -1
- package/dist/commands/memories.js +11 -2
- package/dist/commands/memories.js.map +1 -1
- package/dist/lib/setup/hooks-writer.d.ts +51 -0
- package/dist/lib/setup/hooks-writer.d.ts.map +1 -0
- package/dist/lib/setup/hooks-writer.js +276 -0
- package/dist/lib/setup/hooks-writer.js.map +1 -0
- package/dist/lib/setup/index.d.ts +2 -0
- package/dist/lib/setup/index.d.ts.map +1 -1
- package/dist/lib/setup/index.js +4 -1
- package/dist/lib/setup/index.js.map +1 -1
- package/hooks/capture-precompact.sh +218 -0
- package/hooks/capture-session-end.sh +225 -0
- package/package.json +2 -1
|
@@ -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.
|
|
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"
|