@adverant/nexus-memory-skill 2.3.3 → 2.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.
- package/SKILL.md +19 -0
- package/hooks/auto-ingest.sh +333 -0
- package/hooks/auto-recall.sh +61 -0
- package/hooks/ingest-notify.sh +316 -0
- package/hooks/upload-document.sh +77 -0
- package/install.sh +35 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -30,6 +30,20 @@ When properly configured, Nexus Memory provides **fully automatic** memory manag
|
|
|
30
30
|
- Captures: topics discussed, decisions made, problems solved
|
|
31
31
|
- Stored with `event_type: "episode"` for easy retrieval
|
|
32
32
|
|
|
33
|
+
### Auto-Ingest (File Detection) - NEW in v2.4.0
|
|
34
|
+
- **Automatic file ingestion** when Claude reads files or user mentions file paths
|
|
35
|
+
- Files are processed in the background (non-blocking)
|
|
36
|
+
- Full document storage in PostgreSQL + GraphRAG for semantic search
|
|
37
|
+
- **Triggers**:
|
|
38
|
+
- When Claude uses the Read tool to access a file
|
|
39
|
+
- When user mentions file paths in prompts (e.g., `/path/to/file.pdf`)
|
|
40
|
+
- **Excluded directories**: node_modules, .git, __pycache__, dist, build, vendor
|
|
41
|
+
- **Deduplication**: Files only re-ingested if modified since last ingestion
|
|
42
|
+
- **Status notifications**:
|
|
43
|
+
- Context injection shows "Files queued for ingestion..."
|
|
44
|
+
- Next prompt shows "✅ file.pdf - now searchable"
|
|
45
|
+
- Stale documents (>30 days) flagged for re-ingestion
|
|
46
|
+
|
|
33
47
|
### Configuration
|
|
34
48
|
|
|
35
49
|
The automatic features are controlled by `~/.claude/settings.json`:
|
|
@@ -42,6 +56,7 @@ The automatic features are controlled by `~/.claude/settings.json`:
|
|
|
42
56
|
"matcher": "",
|
|
43
57
|
"hooks": [
|
|
44
58
|
{"type": "command", "command": "~/.claude/hooks/auto-recall.sh"},
|
|
59
|
+
{"type": "command", "command": "~/.claude/hooks/auto-ingest.sh"},
|
|
45
60
|
{"type": "command", "command": "~/.claude/hooks/store-memory.sh"}
|
|
46
61
|
]
|
|
47
62
|
}
|
|
@@ -50,6 +65,7 @@ The automatic features are controlled by `~/.claude/settings.json`:
|
|
|
50
65
|
{
|
|
51
66
|
"matcher": "",
|
|
52
67
|
"hooks": [
|
|
68
|
+
{"type": "command", "command": "~/.claude/hooks/auto-ingest.sh"},
|
|
53
69
|
{"type": "command", "command": "~/.claude/hooks/store-memory.sh"},
|
|
54
70
|
{"type": "command", "command": "~/.claude/hooks/episode-summary.sh"}
|
|
55
71
|
]
|
|
@@ -68,6 +84,9 @@ The automatic features are controlled by `~/.claude/settings.json`:
|
|
|
68
84
|
| `NEXUS_RECALL_LIMIT` | `5` | Number of memories to auto-recall |
|
|
69
85
|
| `NEXUS_EPISODE_THRESHOLD` | `10` | Tool count before episode summary |
|
|
70
86
|
| `NEXUS_VERBOSE` | `0` | Set to `1` for debug output |
|
|
87
|
+
| `NEXUS_AUTO_INGEST` | `1` | Enable automatic file ingestion |
|
|
88
|
+
| `NEXUS_FRESHNESS_DAYS` | `30` | Days before document flagged as stale |
|
|
89
|
+
| `NEXUS_INGEST_POLL_INTERVAL` | `10` | Job status poll interval (seconds) |
|
|
71
90
|
|
|
72
91
|
---
|
|
73
92
|
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Nexus Memory - Auto Ingest Hook
|
|
4
|
+
# Automatically detects files from Read tool usage or prompt mentions and queues
|
|
5
|
+
# them for background ingestion into GraphRAG.
|
|
6
|
+
#
|
|
7
|
+
# Triggers:
|
|
8
|
+
# - PostToolUse: When Claude uses the Read tool to access a file
|
|
9
|
+
# - UserPromptSubmit: When user mentions file paths in their prompt
|
|
10
|
+
#
|
|
11
|
+
# Features:
|
|
12
|
+
# - Automatic file path detection from tool input and prompts
|
|
13
|
+
# - Deduplication via mtime-based freshness tracking
|
|
14
|
+
# - Background ingestion (non-blocking)
|
|
15
|
+
# - Excluded directory filtering (node_modules, .git, etc.)
|
|
16
|
+
# - Context injection for immediate user feedback
|
|
17
|
+
#
|
|
18
|
+
# Environment Variables:
|
|
19
|
+
# NEXUS_API_KEY - API key for authentication (REQUIRED)
|
|
20
|
+
# NEXUS_AUTO_INGEST - Enable/disable auto-ingestion (default: 1)
|
|
21
|
+
# NEXUS_INGEST_VERBOSE - Enable debug output (default: 0)
|
|
22
|
+
# NEXUS_FRESHNESS_DAYS - Days before re-ingesting a file (default: 30)
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
set -o pipefail
|
|
26
|
+
|
|
27
|
+
# Configuration
|
|
28
|
+
NEXUS_API_KEY="${NEXUS_API_KEY:-}"
|
|
29
|
+
AUTO_INGEST="${NEXUS_AUTO_INGEST:-1}"
|
|
30
|
+
VERBOSE="${NEXUS_INGEST_VERBOSE:-${NEXUS_VERBOSE:-0}}"
|
|
31
|
+
FRESHNESS_DAYS="${NEXUS_FRESHNESS_DAYS:-30}"
|
|
32
|
+
|
|
33
|
+
# State directory
|
|
34
|
+
STATE_DIR="${HOME}/.claude/session-env/auto-ingest"
|
|
35
|
+
INGESTED_FILES="${STATE_DIR}/ingested_files.json"
|
|
36
|
+
PENDING_JOBS="${STATE_DIR}/pending_jobs.json"
|
|
37
|
+
NOTIFIER_PID="${STATE_DIR}/notifier.pid"
|
|
38
|
+
|
|
39
|
+
# Excluded directories (skip files in these paths)
|
|
40
|
+
EXCLUDED_DIRS=(
|
|
41
|
+
"node_modules"
|
|
42
|
+
".git"
|
|
43
|
+
"__pycache__"
|
|
44
|
+
"dist"
|
|
45
|
+
"build"
|
|
46
|
+
"vendor"
|
|
47
|
+
".venv"
|
|
48
|
+
"venv"
|
|
49
|
+
".next"
|
|
50
|
+
".nuxt"
|
|
51
|
+
"coverage"
|
|
52
|
+
".cache"
|
|
53
|
+
".npm"
|
|
54
|
+
".yarn"
|
|
55
|
+
"bower_components"
|
|
56
|
+
"target"
|
|
57
|
+
"out"
|
|
58
|
+
".gradle"
|
|
59
|
+
".idea"
|
|
60
|
+
".vscode"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Logging functions
|
|
64
|
+
log() {
|
|
65
|
+
if [[ "$VERBOSE" == "1" ]]; then
|
|
66
|
+
echo "[auto-ingest] $1" >&2
|
|
67
|
+
fi
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log_error() {
|
|
71
|
+
echo "[auto-ingest] ERROR: $1" >&2
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Skip if auto-ingest is disabled
|
|
75
|
+
if [[ "$AUTO_INGEST" != "1" ]]; then
|
|
76
|
+
log "Auto-ingest disabled (NEXUS_AUTO_INGEST=$AUTO_INGEST)"
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Skip if no API key (silently - don't block conversation)
|
|
81
|
+
if [[ -z "$NEXUS_API_KEY" ]]; then
|
|
82
|
+
log "NEXUS_API_KEY not set, skipping auto-ingest"
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Check dependencies silently
|
|
87
|
+
if ! command -v jq &> /dev/null; then
|
|
88
|
+
log "jq not installed, skipping auto-ingest"
|
|
89
|
+
exit 0
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Initialize state directory
|
|
93
|
+
init_state() {
|
|
94
|
+
mkdir -p "$STATE_DIR"
|
|
95
|
+
[[ -f "$INGESTED_FILES" ]] || echo "{}" > "$INGESTED_FILES"
|
|
96
|
+
[[ -f "$PENDING_JOBS" ]] || echo "[]" > "$PENDING_JOBS"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Check if file is in an excluded directory
|
|
100
|
+
is_excluded() {
|
|
101
|
+
local file_path="$1"
|
|
102
|
+
|
|
103
|
+
for dir in "${EXCLUDED_DIRS[@]}"; do
|
|
104
|
+
if [[ "$file_path" == *"/$dir/"* ]] || [[ "$file_path" == *"/$dir" ]]; then
|
|
105
|
+
log "Skipping excluded directory: $dir"
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
|
|
110
|
+
return 1
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# Get file modification time (cross-platform)
|
|
114
|
+
get_mtime() {
|
|
115
|
+
local file_path="$1"
|
|
116
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
117
|
+
stat -f %m "$file_path" 2>/dev/null
|
|
118
|
+
else
|
|
119
|
+
stat -c %Y "$file_path" 2>/dev/null
|
|
120
|
+
fi
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Check if file needs ingestion (not already ingested or modified since)
|
|
124
|
+
needs_ingestion() {
|
|
125
|
+
local file_path="$1"
|
|
126
|
+
|
|
127
|
+
# File must exist
|
|
128
|
+
if [[ ! -f "$file_path" ]]; then
|
|
129
|
+
log "File not found: $file_path"
|
|
130
|
+
return 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
# Check if already pending
|
|
134
|
+
local is_pending=$(jq -r --arg p "$file_path" '[.[] | select(.path == $p)] | length' "$PENDING_JOBS" 2>/dev/null || echo "0")
|
|
135
|
+
if [[ "$is_pending" -gt 0 ]]; then
|
|
136
|
+
log "File already pending: $file_path"
|
|
137
|
+
return 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# Get current mtime
|
|
141
|
+
local current_mtime=$(get_mtime "$file_path")
|
|
142
|
+
if [[ -z "$current_mtime" ]]; then
|
|
143
|
+
log "Cannot get mtime for: $file_path"
|
|
144
|
+
return 1
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Check ingested files index
|
|
148
|
+
local cached=$(jq -r --arg p "$file_path" '.[$p] // empty' "$INGESTED_FILES" 2>/dev/null)
|
|
149
|
+
|
|
150
|
+
if [[ -z "$cached" ]]; then
|
|
151
|
+
log "File never ingested: $file_path"
|
|
152
|
+
return 0 # Never ingested, needs ingestion
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
local cached_mtime=$(echo "$cached" | jq -r '.mtime // 0')
|
|
156
|
+
local cached_status=$(echo "$cached" | jq -r '.status // "unknown"')
|
|
157
|
+
|
|
158
|
+
# Re-ingest if: modified since last ingest, or previous attempt failed
|
|
159
|
+
if [[ "$current_mtime" != "$cached_mtime" ]]; then
|
|
160
|
+
log "File modified since last ingest: $file_path (old: $cached_mtime, new: $current_mtime)"
|
|
161
|
+
return 0
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
if [[ "$cached_status" == "failed" ]]; then
|
|
165
|
+
log "Previous ingestion failed, retrying: $file_path"
|
|
166
|
+
return 0
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
log "File unchanged and already ingested: $file_path"
|
|
170
|
+
return 1
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Extract file paths from Read tool input
|
|
174
|
+
extract_read_tool_path() {
|
|
175
|
+
local input="$1"
|
|
176
|
+
|
|
177
|
+
# Check if this is a Read tool PostToolUse event
|
|
178
|
+
local tool_name=$(echo "$input" | jq -r '.tool_name // empty' 2>/dev/null)
|
|
179
|
+
|
|
180
|
+
if [[ "$tool_name" == "Read" ]]; then
|
|
181
|
+
# Extract file_path from tool_input
|
|
182
|
+
local file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
|
|
183
|
+
if [[ -n "$file_path" ]] && [[ "$file_path" != "null" ]]; then
|
|
184
|
+
echo "$file_path"
|
|
185
|
+
fi
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Extract file paths from prompt text using regex
|
|
190
|
+
extract_prompt_paths() {
|
|
191
|
+
local prompt="$1"
|
|
192
|
+
|
|
193
|
+
# Match absolute paths: /foo/bar/file.ext
|
|
194
|
+
# Match home paths: ~/project/file.ext
|
|
195
|
+
# Match relative paths: ./foo/bar.ts, ../src/index.js
|
|
196
|
+
# Require file extension to reduce false positives
|
|
197
|
+
echo "$prompt" | grep -oE '(~|\.{0,2})?/[A-Za-z0-9_./+-]+\.[A-Za-z0-9]+' 2>/dev/null | while read -r path; do
|
|
198
|
+
# Expand ~ to home directory
|
|
199
|
+
if [[ "$path" == ~* ]]; then
|
|
200
|
+
path="${path/#\~/$HOME}"
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# Resolve relative paths
|
|
204
|
+
if [[ "$path" == ./* ]] || [[ "$path" == ../* ]]; then
|
|
205
|
+
path=$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
# Only output if file exists
|
|
209
|
+
if [[ -f "$path" ]]; then
|
|
210
|
+
echo "$path"
|
|
211
|
+
fi
|
|
212
|
+
done | sort -u
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Queue file for background ingestion
|
|
216
|
+
queue_for_ingestion() {
|
|
217
|
+
local file_path="$1"
|
|
218
|
+
local file_name=$(basename "$file_path")
|
|
219
|
+
|
|
220
|
+
log "Queueing for ingestion: $file_path"
|
|
221
|
+
|
|
222
|
+
# Call upload-document.sh in background mode
|
|
223
|
+
local upload_script="${HOME}/.claude/hooks/upload-document.sh"
|
|
224
|
+
|
|
225
|
+
if [[ ! -x "$upload_script" ]]; then
|
|
226
|
+
log_error "upload-document.sh not found or not executable"
|
|
227
|
+
return 1
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# Run in background (fire-and-forget)
|
|
231
|
+
(
|
|
232
|
+
"$upload_script" "$file_path" --background 2>/dev/null
|
|
233
|
+
) &
|
|
234
|
+
disown 2>/dev/null || true
|
|
235
|
+
|
|
236
|
+
return 0
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Output context injection for user feedback
|
|
240
|
+
emit_context() {
|
|
241
|
+
local files_queued="$1"
|
|
242
|
+
local count=$(echo "$files_queued" | wc -l | tr -d ' ')
|
|
243
|
+
|
|
244
|
+
if [[ -n "$files_queued" ]] && [[ "$count" -gt 0 ]]; then
|
|
245
|
+
echo ""
|
|
246
|
+
echo "<nexus-auto-ingest>"
|
|
247
|
+
echo "## Files Queued for Memory Ingestion"
|
|
248
|
+
echo "$files_queued" | while read -r path; do
|
|
249
|
+
if [[ -n "$path" ]]; then
|
|
250
|
+
echo "- $(basename "$path") - processing in background..."
|
|
251
|
+
fi
|
|
252
|
+
done
|
|
253
|
+
echo ""
|
|
254
|
+
echo "Files will be searchable via recall after processing completes."
|
|
255
|
+
echo "</nexus-auto-ingest>"
|
|
256
|
+
fi
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Main execution
|
|
260
|
+
main() {
|
|
261
|
+
init_state
|
|
262
|
+
|
|
263
|
+
# Read input from stdin
|
|
264
|
+
INPUT=$(cat)
|
|
265
|
+
|
|
266
|
+
if [[ -z "$INPUT" ]]; then
|
|
267
|
+
log "No input provided"
|
|
268
|
+
exit 0
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
log "Received input: ${INPUT:0:200}..."
|
|
272
|
+
|
|
273
|
+
# Collect file paths to process
|
|
274
|
+
FILES_TO_INGEST=""
|
|
275
|
+
|
|
276
|
+
# 1. Check for Read tool usage (PostToolUse)
|
|
277
|
+
local read_path=$(extract_read_tool_path "$INPUT")
|
|
278
|
+
if [[ -n "$read_path" ]]; then
|
|
279
|
+
log "Detected Read tool file: $read_path"
|
|
280
|
+
|
|
281
|
+
if ! is_excluded "$read_path" && needs_ingestion "$read_path"; then
|
|
282
|
+
FILES_TO_INGEST="$read_path"
|
|
283
|
+
fi
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# 2. Check for file paths in prompt (UserPromptSubmit)
|
|
287
|
+
local prompt=$(echo "$INPUT" | jq -r '.prompt // .content // empty' 2>/dev/null)
|
|
288
|
+
if [[ -n "$prompt" ]] && [[ "$prompt" != "null" ]]; then
|
|
289
|
+
log "Scanning prompt for file paths..."
|
|
290
|
+
|
|
291
|
+
while IFS= read -r path; do
|
|
292
|
+
if [[ -n "$path" ]]; then
|
|
293
|
+
log "Detected prompt file: $path"
|
|
294
|
+
|
|
295
|
+
if ! is_excluded "$path" && needs_ingestion "$path"; then
|
|
296
|
+
if [[ -n "$FILES_TO_INGEST" ]]; then
|
|
297
|
+
FILES_TO_INGEST="${FILES_TO_INGEST}"$'\n'"${path}"
|
|
298
|
+
else
|
|
299
|
+
FILES_TO_INGEST="$path"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
fi
|
|
303
|
+
done < <(extract_prompt_paths "$prompt")
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# Remove duplicates
|
|
307
|
+
if [[ -n "$FILES_TO_INGEST" ]]; then
|
|
308
|
+
FILES_TO_INGEST=$(echo "$FILES_TO_INGEST" | sort -u)
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# Queue files for ingestion
|
|
312
|
+
QUEUED_FILES=""
|
|
313
|
+
if [[ -n "$FILES_TO_INGEST" ]]; then
|
|
314
|
+
while IFS= read -r file_path; do
|
|
315
|
+
if [[ -n "$file_path" ]]; then
|
|
316
|
+
if queue_for_ingestion "$file_path"; then
|
|
317
|
+
if [[ -n "$QUEUED_FILES" ]]; then
|
|
318
|
+
QUEUED_FILES="${QUEUED_FILES}"$'\n'"${file_path}"
|
|
319
|
+
else
|
|
320
|
+
QUEUED_FILES="$file_path"
|
|
321
|
+
fi
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
done <<< "$FILES_TO_INGEST"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# Emit context for user feedback
|
|
328
|
+
emit_context "$QUEUED_FILES"
|
|
329
|
+
|
|
330
|
+
exit 0
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
main
|
package/hooks/auto-recall.sh
CHANGED
|
@@ -332,6 +332,67 @@ if [[ "$FOLLOWUP_COUNT" -gt 0 ]] && [[ "$FOLLOWUP_COUNT" != "null" ]] && [[ "$IN
|
|
|
332
332
|
echo ""
|
|
333
333
|
fi
|
|
334
334
|
|
|
335
|
+
# === AUTO-INGEST STATUS SECTION ===
|
|
336
|
+
AUTO_INGEST_STATE="${HOME}/.claude/session-env/auto-ingest"
|
|
337
|
+
|
|
338
|
+
# Show recently completed ingestions (cleared after display)
|
|
339
|
+
if [[ -f "${AUTO_INGEST_STATE}/recent_completions.json" ]]; then
|
|
340
|
+
COMPLETIONS=$(cat "${AUTO_INGEST_STATE}/recent_completions.json" 2>/dev/null || echo "[]")
|
|
341
|
+
COMPLETION_COUNT=$(echo "$COMPLETIONS" | jq 'length' 2>/dev/null || echo "0")
|
|
342
|
+
|
|
343
|
+
if [[ "$COMPLETION_COUNT" -gt 0 ]] && [[ "$COMPLETION_COUNT" != "0" ]]; then
|
|
344
|
+
echo "## Recently Ingested Files (Ready for Recall)"
|
|
345
|
+
echo "$COMPLETIONS" | jq -r '.[] |
|
|
346
|
+
if .status == "completed" then
|
|
347
|
+
"- ✅ \(.path | split("/") | .[-1]) - now searchable\(if .entityCount > 0 then " (\(.entityCount) entities)" else "" end)"
|
|
348
|
+
else
|
|
349
|
+
"- ❌ \(.path | split("/") | .[-1]) - ingestion failed"
|
|
350
|
+
end
|
|
351
|
+
' 2>/dev/null
|
|
352
|
+
echo ""
|
|
353
|
+
# Clear after display
|
|
354
|
+
echo "[]" > "${AUTO_INGEST_STATE}/recent_completions.json"
|
|
355
|
+
fi
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Show pending ingestions
|
|
359
|
+
if [[ -f "${AUTO_INGEST_STATE}/pending_jobs.json" ]]; then
|
|
360
|
+
PENDING=$(cat "${AUTO_INGEST_STATE}/pending_jobs.json" 2>/dev/null || echo "[]")
|
|
361
|
+
PENDING_COUNT=$(echo "$PENDING" | jq 'length' 2>/dev/null || echo "0")
|
|
362
|
+
|
|
363
|
+
if [[ "$PENDING_COUNT" -gt 0 ]] && [[ "$PENDING_COUNT" != "0" ]]; then
|
|
364
|
+
echo "## Files Currently Being Processed"
|
|
365
|
+
echo "$PENDING" | jq -r '.[] | "- ⏳ \(.path | split("/") | .[-1]) - processing..."' 2>/dev/null
|
|
366
|
+
echo ""
|
|
367
|
+
fi
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
# Document freshness alerts (files ingested > 30 days ago)
|
|
371
|
+
FRESHNESS_DAYS="${NEXUS_FRESHNESS_DAYS:-30}"
|
|
372
|
+
if [[ -f "${AUTO_INGEST_STATE}/ingested_files.json" ]]; then
|
|
373
|
+
STALE_THRESHOLD=$((FRESHNESS_DAYS * 24 * 60 * 60)) # Convert days to seconds
|
|
374
|
+
NOW=$(date +%s)
|
|
375
|
+
|
|
376
|
+
# Find stale documents (completed but old)
|
|
377
|
+
STALE_DOCS=$(jq --argjson now "$NOW" --argjson thresh "$STALE_THRESHOLD" '
|
|
378
|
+
to_entries | map(select(
|
|
379
|
+
(.value.status == "completed") and
|
|
380
|
+
(.value.ingestedAt != null) and
|
|
381
|
+
(($now - (.value.ingestedAt | if type == "string" then (. | split("T")[0] | strptime("%Y-%m-%d") | mktime) else 0 end)) > $thresh)
|
|
382
|
+
)) | .[0:3]
|
|
383
|
+
' "${AUTO_INGEST_STATE}/ingested_files.json" 2>/dev/null || echo "[]")
|
|
384
|
+
|
|
385
|
+
STALE_COUNT=$(echo "$STALE_DOCS" | jq 'length' 2>/dev/null || echo "0")
|
|
386
|
+
|
|
387
|
+
if [[ "$STALE_COUNT" -gt 0 ]] && [[ "$STALE_COUNT" != "0" ]]; then
|
|
388
|
+
echo "## Document Freshness Alerts"
|
|
389
|
+
echo "$STALE_DOCS" | jq -r --argjson days "$FRESHNESS_DAYS" '.[] |
|
|
390
|
+
"- ⚠️ \(.key | split("/") | .[-1]) - ingested >\($days) days ago, may be outdated"
|
|
391
|
+
' 2>/dev/null
|
|
392
|
+
echo ""
|
|
393
|
+
fi
|
|
394
|
+
fi
|
|
395
|
+
|
|
335
396
|
echo "</nexus-memory-context>"
|
|
336
397
|
|
|
337
398
|
exit 0
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# Nexus Memory - Ingest Notify Daemon
|
|
4
|
+
# Background daemon that polls pending ingestion jobs and updates state files.
|
|
5
|
+
# Started by auto-ingest.sh when files are queued for ingestion.
|
|
6
|
+
#
|
|
7
|
+
# Features:
|
|
8
|
+
# - Polls job status every 10 seconds
|
|
9
|
+
# - Updates pending_jobs.json with current status
|
|
10
|
+
# - Moves completed jobs to recent_completions.json
|
|
11
|
+
# - Self-terminates when no pending jobs remain
|
|
12
|
+
# - Prevents duplicate daemon instances
|
|
13
|
+
#
|
|
14
|
+
# Environment Variables:
|
|
15
|
+
# NEXUS_API_KEY - API key for authentication (REQUIRED)
|
|
16
|
+
# NEXUS_API_URL - API endpoint (default: https://api.adverant.ai)
|
|
17
|
+
# NEXUS_INGEST_POLL_INTERVAL - Poll interval in seconds (default: 10)
|
|
18
|
+
# NEXUS_INGEST_VERBOSE - Enable debug output (default: 0)
|
|
19
|
+
#
|
|
20
|
+
|
|
21
|
+
set -o pipefail
|
|
22
|
+
|
|
23
|
+
# Configuration
|
|
24
|
+
NEXUS_API_KEY="${NEXUS_API_KEY:-}"
|
|
25
|
+
NEXUS_API_URL="${NEXUS_API_URL:-https://api.adverant.ai}"
|
|
26
|
+
POLL_INTERVAL="${NEXUS_INGEST_POLL_INTERVAL:-10}"
|
|
27
|
+
VERBOSE="${NEXUS_INGEST_VERBOSE:-${NEXUS_VERBOSE:-0}}"
|
|
28
|
+
|
|
29
|
+
# State files
|
|
30
|
+
STATE_DIR="${HOME}/.claude/session-env/auto-ingest"
|
|
31
|
+
INGESTED_FILES="${STATE_DIR}/ingested_files.json"
|
|
32
|
+
PENDING_JOBS="${STATE_DIR}/pending_jobs.json"
|
|
33
|
+
RECENT_COMPLETIONS="${STATE_DIR}/recent_completions.json"
|
|
34
|
+
PID_FILE="${STATE_DIR}/notifier.pid"
|
|
35
|
+
|
|
36
|
+
# Logging functions
|
|
37
|
+
log() {
|
|
38
|
+
if [[ "$VERBOSE" == "1" ]]; then
|
|
39
|
+
echo "[ingest-notify] $1" >&2
|
|
40
|
+
fi
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
log_error() {
|
|
44
|
+
echo "[ingest-notify] ERROR: $1" >&2
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Check if another instance is running
|
|
48
|
+
check_existing_instance() {
|
|
49
|
+
if [[ -f "$PID_FILE" ]]; then
|
|
50
|
+
local existing_pid=$(cat "$PID_FILE" 2>/dev/null)
|
|
51
|
+
if [[ -n "$existing_pid" ]] && kill -0 "$existing_pid" 2>/dev/null; then
|
|
52
|
+
log "Another notifier instance is running (PID: $existing_pid)"
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
# Stale PID file, remove it
|
|
56
|
+
rm -f "$PID_FILE"
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
# Initialize state files
|
|
61
|
+
init_state() {
|
|
62
|
+
mkdir -p "$STATE_DIR"
|
|
63
|
+
[[ -f "$INGESTED_FILES" ]] || echo "{}" > "$INGESTED_FILES"
|
|
64
|
+
[[ -f "$PENDING_JOBS" ]] || echo "[]" > "$PENDING_JOBS"
|
|
65
|
+
[[ -f "$RECENT_COMPLETIONS" ]] || echo "[]" > "$RECENT_COMPLETIONS"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Get file modification time (cross-platform)
|
|
69
|
+
get_mtime() {
|
|
70
|
+
local file_path="$1"
|
|
71
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
72
|
+
stat -f %m "$file_path" 2>/dev/null || echo "0"
|
|
73
|
+
else
|
|
74
|
+
stat -c %Y "$file_path" 2>/dev/null || echo "0"
|
|
75
|
+
fi
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Update ingested files index
|
|
79
|
+
update_ingested_file() {
|
|
80
|
+
local file_path="$1"
|
|
81
|
+
local job_id="$2"
|
|
82
|
+
local status="$3"
|
|
83
|
+
local entity_count="${4:-0}"
|
|
84
|
+
|
|
85
|
+
local mtime=$(get_mtime "$file_path")
|
|
86
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
87
|
+
|
|
88
|
+
# Atomic update using temp file
|
|
89
|
+
jq --arg p "$file_path" \
|
|
90
|
+
--arg j "$job_id" \
|
|
91
|
+
--arg s "$status" \
|
|
92
|
+
--arg m "$mtime" \
|
|
93
|
+
--arg t "$now" \
|
|
94
|
+
--argjson e "$entity_count" \
|
|
95
|
+
'.[$p] = {
|
|
96
|
+
jobId: $j,
|
|
97
|
+
status: $s,
|
|
98
|
+
mtime: ($m | tonumber),
|
|
99
|
+
ingestedAt: $t,
|
|
100
|
+
entityCount: $e
|
|
101
|
+
}' "$INGESTED_FILES" > "${INGESTED_FILES}.tmp" 2>/dev/null \
|
|
102
|
+
&& mv "${INGESTED_FILES}.tmp" "$INGESTED_FILES"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Add to recent completions
|
|
106
|
+
add_completion() {
|
|
107
|
+
local file_path="$1"
|
|
108
|
+
local job_id="$2"
|
|
109
|
+
local entity_count="${3:-0}"
|
|
110
|
+
local status="${4:-completed}"
|
|
111
|
+
|
|
112
|
+
local now=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
113
|
+
|
|
114
|
+
# Read existing completions
|
|
115
|
+
local existing=$(cat "$RECENT_COMPLETIONS" 2>/dev/null || echo "[]")
|
|
116
|
+
|
|
117
|
+
# Append new completion
|
|
118
|
+
echo "$existing" | jq --arg p "$file_path" \
|
|
119
|
+
--arg j "$job_id" \
|
|
120
|
+
--arg t "$now" \
|
|
121
|
+
--arg s "$status" \
|
|
122
|
+
--argjson e "$entity_count" \
|
|
123
|
+
'. + [{
|
|
124
|
+
path: $p,
|
|
125
|
+
jobId: $j,
|
|
126
|
+
completedAt: $t,
|
|
127
|
+
status: $s,
|
|
128
|
+
entityCount: $e
|
|
129
|
+
}]' > "${RECENT_COMPLETIONS}.tmp" 2>/dev/null \
|
|
130
|
+
&& mv "${RECENT_COMPLETIONS}.tmp" "$RECENT_COMPLETIONS"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Check status of a single job
|
|
134
|
+
check_job_status() {
|
|
135
|
+
local job_id="$1"
|
|
136
|
+
|
|
137
|
+
local response=$(curl -s "$NEXUS_API_URL/fileprocess/api/jobs/$job_id" \
|
|
138
|
+
-H "Authorization: Bearer $NEXUS_API_KEY" \
|
|
139
|
+
--max-time 10 2>/dev/null)
|
|
140
|
+
|
|
141
|
+
if [[ -z "$response" ]]; then
|
|
142
|
+
echo '{"status": "unknown", "error": "No response from API"}'
|
|
143
|
+
return 1
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
# Extract status from response (handles nested .job.status structure)
|
|
147
|
+
local status=$(echo "$response" | jq -r '.job.status // .status // "unknown"' 2>/dev/null)
|
|
148
|
+
local error=$(echo "$response" | jq -r '.job.errorMessage // .error // null' 2>/dev/null)
|
|
149
|
+
local entity_count=$(echo "$response" | jq -r '.job.metadata.entities // [] | length' 2>/dev/null || echo "0")
|
|
150
|
+
|
|
151
|
+
jq -n --arg s "$status" --arg e "$error" --argjson c "$entity_count" \
|
|
152
|
+
'{status: $s, error: $e, entityCount: $c}'
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Poll all pending jobs
|
|
156
|
+
poll_pending_jobs() {
|
|
157
|
+
local pending=$(cat "$PENDING_JOBS" 2>/dev/null || echo "[]")
|
|
158
|
+
local pending_count=$(echo "$pending" | jq 'length' 2>/dev/null || echo "0")
|
|
159
|
+
|
|
160
|
+
if [[ "$pending_count" == "0" ]]; then
|
|
161
|
+
log "No pending jobs"
|
|
162
|
+
return 0
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
log "Polling $pending_count pending jobs..."
|
|
166
|
+
|
|
167
|
+
# Build new pending list (jobs still in progress)
|
|
168
|
+
local still_pending="[]"
|
|
169
|
+
|
|
170
|
+
# Process each pending job
|
|
171
|
+
echo "$pending" | jq -c '.[]' 2>/dev/null | while read -r job; do
|
|
172
|
+
local job_id=$(echo "$job" | jq -r '.jobId')
|
|
173
|
+
local file_path=$(echo "$job" | jq -r '.path')
|
|
174
|
+
local file_name=$(basename "$file_path")
|
|
175
|
+
|
|
176
|
+
log "Checking job $job_id for $file_name"
|
|
177
|
+
|
|
178
|
+
# Get job status
|
|
179
|
+
local status_json=$(check_job_status "$job_id")
|
|
180
|
+
local status=$(echo "$status_json" | jq -r '.status')
|
|
181
|
+
local entity_count=$(echo "$status_json" | jq -r '.entityCount // 0')
|
|
182
|
+
|
|
183
|
+
case "$status" in
|
|
184
|
+
"completed"|"finished"|"success")
|
|
185
|
+
log "Job completed: $file_name ($entity_count entities)"
|
|
186
|
+
update_ingested_file "$file_path" "$job_id" "completed" "$entity_count"
|
|
187
|
+
add_completion "$file_path" "$job_id" "$entity_count" "completed"
|
|
188
|
+
;;
|
|
189
|
+
|
|
190
|
+
"failed"|"error"|"cancelled")
|
|
191
|
+
local error=$(echo "$status_json" | jq -r '.error // "Unknown error"')
|
|
192
|
+
log "Job failed: $file_name - $error"
|
|
193
|
+
update_ingested_file "$file_path" "$job_id" "failed" "0"
|
|
194
|
+
add_completion "$file_path" "$job_id" "0" "failed"
|
|
195
|
+
;;
|
|
196
|
+
|
|
197
|
+
"queued"|"pending"|"processing"|"active"|"triaging"|"routing")
|
|
198
|
+
log "Job still processing: $file_name ($status)"
|
|
199
|
+
# Keep in pending list - will be rebuilt after loop
|
|
200
|
+
;;
|
|
201
|
+
|
|
202
|
+
*)
|
|
203
|
+
log "Unknown status for $file_name: $status"
|
|
204
|
+
# Keep in pending list
|
|
205
|
+
;;
|
|
206
|
+
esac
|
|
207
|
+
done
|
|
208
|
+
|
|
209
|
+
# Rebuild pending jobs list (only keep non-completed jobs)
|
|
210
|
+
local updated_pending=$(cat "$PENDING_JOBS" 2>/dev/null || echo "[]")
|
|
211
|
+
local new_pending="[]"
|
|
212
|
+
|
|
213
|
+
echo "$updated_pending" | jq -c '.[]' 2>/dev/null | while read -r job; do
|
|
214
|
+
local job_id=$(echo "$job" | jq -r '.jobId')
|
|
215
|
+
local file_path=$(echo "$job" | jq -r '.path')
|
|
216
|
+
|
|
217
|
+
# Check if this job is completed in ingested_files
|
|
218
|
+
local ingested_status=$(jq -r --arg p "$file_path" '.[$p].status // "pending"' "$INGESTED_FILES" 2>/dev/null)
|
|
219
|
+
|
|
220
|
+
if [[ "$ingested_status" != "completed" ]] && [[ "$ingested_status" != "failed" ]]; then
|
|
221
|
+
# Still pending, keep it
|
|
222
|
+
new_pending=$(echo "$new_pending" | jq --argjson job "$job" '. + [$job]')
|
|
223
|
+
fi
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
# Calculate remaining pending jobs
|
|
227
|
+
local final_pending=$(cat "$PENDING_JOBS" 2>/dev/null || echo "[]")
|
|
228
|
+
local remaining="[]"
|
|
229
|
+
|
|
230
|
+
while IFS= read -r job; do
|
|
231
|
+
if [[ -n "$job" ]]; then
|
|
232
|
+
local job_id=$(echo "$job" | jq -r '.jobId')
|
|
233
|
+
local file_path=$(echo "$job" | jq -r '.path')
|
|
234
|
+
|
|
235
|
+
# Check completion status
|
|
236
|
+
local ingested_status=$(jq -r --arg p "$file_path" '.[$p].status // "pending"' "$INGESTED_FILES" 2>/dev/null)
|
|
237
|
+
|
|
238
|
+
if [[ "$ingested_status" != "completed" ]] && [[ "$ingested_status" != "failed" ]]; then
|
|
239
|
+
remaining=$(echo "$remaining" | jq --argjson j "$job" '. + [$j]')
|
|
240
|
+
fi
|
|
241
|
+
fi
|
|
242
|
+
done < <(echo "$final_pending" | jq -c '.[]' 2>/dev/null)
|
|
243
|
+
|
|
244
|
+
echo "$remaining" > "$PENDING_JOBS"
|
|
245
|
+
|
|
246
|
+
local remaining_count=$(echo "$remaining" | jq 'length' 2>/dev/null || echo "0")
|
|
247
|
+
log "Remaining pending jobs: $remaining_count"
|
|
248
|
+
|
|
249
|
+
return "$remaining_count"
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# Cleanup on exit
|
|
253
|
+
cleanup() {
|
|
254
|
+
rm -f "$PID_FILE"
|
|
255
|
+
log "Notifier daemon stopped"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Main daemon loop
|
|
259
|
+
run_daemon() {
|
|
260
|
+
log "Starting notifier daemon (PID: $$)"
|
|
261
|
+
echo $$ > "$PID_FILE"
|
|
262
|
+
|
|
263
|
+
trap cleanup EXIT
|
|
264
|
+
|
|
265
|
+
local max_iterations=360 # 1 hour max (360 * 10s = 3600s)
|
|
266
|
+
local iteration=0
|
|
267
|
+
|
|
268
|
+
while [[ $iteration -lt $max_iterations ]]; do
|
|
269
|
+
poll_pending_jobs
|
|
270
|
+
local remaining=$?
|
|
271
|
+
|
|
272
|
+
# Exit if no more pending jobs
|
|
273
|
+
local pending_count=$(jq 'length' "$PENDING_JOBS" 2>/dev/null || echo "0")
|
|
274
|
+
if [[ "$pending_count" == "0" ]]; then
|
|
275
|
+
log "All jobs completed, exiting daemon"
|
|
276
|
+
break
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# Wait before next poll
|
|
280
|
+
sleep "$POLL_INTERVAL"
|
|
281
|
+
((iteration++))
|
|
282
|
+
done
|
|
283
|
+
|
|
284
|
+
log "Daemon loop ended after $iteration iterations"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# Main entry point
|
|
288
|
+
main() {
|
|
289
|
+
# Skip if no API key
|
|
290
|
+
if [[ -z "$NEXUS_API_KEY" ]]; then
|
|
291
|
+
log "NEXUS_API_KEY not set, exiting"
|
|
292
|
+
exit 0
|
|
293
|
+
fi
|
|
294
|
+
|
|
295
|
+
# Check dependencies
|
|
296
|
+
if ! command -v jq &> /dev/null; then
|
|
297
|
+
log_error "jq not installed"
|
|
298
|
+
exit 1
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
if ! command -v curl &> /dev/null; then
|
|
302
|
+
log_error "curl not installed"
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# Check for existing instance
|
|
307
|
+
check_existing_instance
|
|
308
|
+
|
|
309
|
+
# Initialize state
|
|
310
|
+
init_state
|
|
311
|
+
|
|
312
|
+
# Run daemon
|
|
313
|
+
run_daemon
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
main
|
package/hooks/upload-document.sh
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
# --tags=a,b,c Add custom tags for recall (comma-separated)
|
|
38
38
|
# --no-entities Skip entity extraction to knowledge graph
|
|
39
39
|
# --prefer-speed Use faster OCR (may reduce accuracy for scanned docs)
|
|
40
|
+
# --background Fire-and-forget mode for auto-ingestion (non-blocking)
|
|
40
41
|
#
|
|
41
42
|
# Environment Variables:
|
|
42
43
|
# NEXUS_API_KEY - API key for authentication (REQUIRED)
|
|
@@ -158,6 +159,7 @@ BATCH_MODE=0
|
|
|
158
159
|
CUSTOM_TAGS=""
|
|
159
160
|
EXTRACT_ENTITIES=1
|
|
160
161
|
PREFER_SPEED=0
|
|
162
|
+
BACKGROUND_MODE=0
|
|
161
163
|
|
|
162
164
|
while [[ $# -gt 0 ]]; do
|
|
163
165
|
case $1 in
|
|
@@ -185,6 +187,10 @@ while [[ $# -gt 0 ]]; do
|
|
|
185
187
|
PREFER_SPEED=1
|
|
186
188
|
shift
|
|
187
189
|
;;
|
|
190
|
+
--background)
|
|
191
|
+
BACKGROUND_MODE=1
|
|
192
|
+
shift
|
|
193
|
+
;;
|
|
188
194
|
--help|-h)
|
|
189
195
|
print_usage
|
|
190
196
|
exit 0
|
|
@@ -493,6 +499,77 @@ wait_for_job() {
|
|
|
493
499
|
# MAIN EXECUTION
|
|
494
500
|
# ============================================================================
|
|
495
501
|
|
|
502
|
+
# Background mode: Fire-and-forget for auto-ingestion
|
|
503
|
+
if [[ "$BACKGROUND_MODE" == "1" ]]; then
|
|
504
|
+
# Only process first file in background mode
|
|
505
|
+
FILE_PATH="${FILES[0]}"
|
|
506
|
+
FILE_NAME=$(basename "$FILE_PATH")
|
|
507
|
+
|
|
508
|
+
log "Background mode: uploading $FILE_NAME"
|
|
509
|
+
|
|
510
|
+
# Run upload in background subshell
|
|
511
|
+
(
|
|
512
|
+
# Suppress output in background mode
|
|
513
|
+
UPLOAD_OUTPUT=$(upload_file "$FILE_PATH" 2>&1)
|
|
514
|
+
UPLOAD_EXIT_CODE=$?
|
|
515
|
+
|
|
516
|
+
if [[ $UPLOAD_EXIT_CODE -eq 0 ]]; then
|
|
517
|
+
# Extract job ID from output
|
|
518
|
+
JOB_ID=$(echo "$UPLOAD_OUTPUT" | grep -E '^[a-f0-9-]+$' | tail -1)
|
|
519
|
+
if [[ -z "$JOB_ID" ]]; then
|
|
520
|
+
JOB_ID=$(echo "$UPLOAD_OUTPUT" | grep "Job ID:" | sed 's/Job ID: //' | tr -d ' ')
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
if [[ -n "$JOB_ID" ]]; then
|
|
524
|
+
# Update state files for auto-ingest tracking
|
|
525
|
+
STATE_DIR="${HOME}/.claude/session-env/auto-ingest"
|
|
526
|
+
PENDING_FILE="${STATE_DIR}/pending_jobs.json"
|
|
527
|
+
INGESTED_FILE="${STATE_DIR}/ingested_files.json"
|
|
528
|
+
|
|
529
|
+
mkdir -p "$STATE_DIR"
|
|
530
|
+
[[ -f "$PENDING_FILE" ]] || echo "[]" > "$PENDING_FILE"
|
|
531
|
+
[[ -f "$INGESTED_FILE" ]] || echo "{}" > "$INGESTED_FILE"
|
|
532
|
+
|
|
533
|
+
# Add to pending jobs
|
|
534
|
+
NOW=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
535
|
+
jq --arg j "$JOB_ID" --arg p "$FILE_PATH" --arg t "$NOW" \
|
|
536
|
+
'. + [{jobId: $j, path: $p, startTime: $t}]' "$PENDING_FILE" > "${PENDING_FILE}.tmp" 2>/dev/null \
|
|
537
|
+
&& mv "${PENDING_FILE}.tmp" "$PENDING_FILE"
|
|
538
|
+
|
|
539
|
+
# Mark as processing in ingested files
|
|
540
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
541
|
+
MTIME=$(stat -f %m "$FILE_PATH" 2>/dev/null || echo "0")
|
|
542
|
+
else
|
|
543
|
+
MTIME=$(stat -c %Y "$FILE_PATH" 2>/dev/null || echo "0")
|
|
544
|
+
fi
|
|
545
|
+
|
|
546
|
+
jq --arg p "$FILE_PATH" --arg j "$JOB_ID" --arg m "$MTIME" --arg t "$NOW" \
|
|
547
|
+
'.[$p] = {jobId: $j, status: "processing", mtime: ($m | tonumber), ingestedAt: $t}' \
|
|
548
|
+
"$INGESTED_FILE" > "${INGESTED_FILE}.tmp" 2>/dev/null \
|
|
549
|
+
&& mv "${INGESTED_FILE}.tmp" "$INGESTED_FILE"
|
|
550
|
+
|
|
551
|
+
# Start notifier daemon if not running
|
|
552
|
+
NOTIFIER_SCRIPT="${HOME}/.claude/hooks/ingest-notify.sh"
|
|
553
|
+
PID_FILE="${STATE_DIR}/notifier.pid"
|
|
554
|
+
|
|
555
|
+
if [[ -x "$NOTIFIER_SCRIPT" ]]; then
|
|
556
|
+
if [[ ! -f "$PID_FILE" ]] || ! kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then
|
|
557
|
+
"$NOTIFIER_SCRIPT" &
|
|
558
|
+
fi
|
|
559
|
+
fi
|
|
560
|
+
|
|
561
|
+
log "Background upload queued: $FILE_NAME (job: $JOB_ID)"
|
|
562
|
+
fi
|
|
563
|
+
else
|
|
564
|
+
log "Background upload failed: $FILE_NAME"
|
|
565
|
+
fi
|
|
566
|
+
) &
|
|
567
|
+
disown 2>/dev/null || true
|
|
568
|
+
|
|
569
|
+
# Exit immediately (non-blocking)
|
|
570
|
+
exit 0
|
|
571
|
+
fi
|
|
572
|
+
|
|
496
573
|
# Track all job IDs for batch mode
|
|
497
574
|
JOB_IDS=()
|
|
498
575
|
FILE_NAMES=()
|
package/install.sh
CHANGED
|
@@ -116,6 +116,29 @@ install_files() {
|
|
|
116
116
|
chmod +x ~/.claude/hooks/upload-document.sh
|
|
117
117
|
fi
|
|
118
118
|
|
|
119
|
+
# Copy auto-ingest hooks (v2.4.0+)
|
|
120
|
+
if [ -f "$source_dir/hooks/auto-ingest.sh" ]; then
|
|
121
|
+
cp "$source_dir/hooks/auto-ingest.sh" ~/.claude/hooks/
|
|
122
|
+
chmod +x ~/.claude/hooks/auto-ingest.sh
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
if [ -f "$source_dir/hooks/ingest-notify.sh" ]; then
|
|
126
|
+
cp "$source_dir/hooks/ingest-notify.sh" ~/.claude/hooks/
|
|
127
|
+
chmod +x ~/.claude/hooks/ingest-notify.sh
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Copy API key helper if it exists
|
|
131
|
+
if [ -f "$source_dir/hooks/api-key-helper.sh" ]; then
|
|
132
|
+
cp "$source_dir/hooks/api-key-helper.sh" ~/.claude/hooks/
|
|
133
|
+
chmod +x ~/.claude/hooks/api-key-helper.sh
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Copy bead-sync hook if it exists
|
|
137
|
+
if [ -f "$source_dir/hooks/bead-sync.sh" ]; then
|
|
138
|
+
cp "$source_dir/hooks/bead-sync.sh" ~/.claude/hooks/
|
|
139
|
+
chmod +x ~/.claude/hooks/bead-sync.sh
|
|
140
|
+
fi
|
|
141
|
+
|
|
119
142
|
log_success "Files installed successfully"
|
|
120
143
|
}
|
|
121
144
|
|
|
@@ -125,7 +148,7 @@ configure_settings() {
|
|
|
125
148
|
|
|
126
149
|
local settings_file=~/.claude/settings.json
|
|
127
150
|
|
|
128
|
-
# Create settings.json with automatic memory hooks
|
|
151
|
+
# Create settings.json with automatic memory hooks (including auto-ingest)
|
|
129
152
|
cat > "$settings_file" << 'EOF'
|
|
130
153
|
{
|
|
131
154
|
"hooks": {
|
|
@@ -137,6 +160,10 @@ configure_settings() {
|
|
|
137
160
|
"type": "command",
|
|
138
161
|
"command": "~/.claude/hooks/auto-recall.sh"
|
|
139
162
|
},
|
|
163
|
+
{
|
|
164
|
+
"type": "command",
|
|
165
|
+
"command": "~/.claude/hooks/auto-ingest.sh"
|
|
166
|
+
},
|
|
140
167
|
{
|
|
141
168
|
"type": "command",
|
|
142
169
|
"command": "~/.claude/hooks/store-memory.sh"
|
|
@@ -148,6 +175,10 @@ configure_settings() {
|
|
|
148
175
|
{
|
|
149
176
|
"matcher": "",
|
|
150
177
|
"hooks": [
|
|
178
|
+
{
|
|
179
|
+
"type": "command",
|
|
180
|
+
"command": "~/.claude/hooks/auto-ingest.sh"
|
|
181
|
+
},
|
|
151
182
|
{
|
|
152
183
|
"type": "command",
|
|
153
184
|
"command": "~/.claude/hooks/store-memory.sh"
|
|
@@ -310,6 +341,7 @@ print_success() {
|
|
|
310
341
|
echo "Automatic Memory Features Enabled:"
|
|
311
342
|
echo " - Auto-recall: Relevant memories fetched on every prompt"
|
|
312
343
|
echo " - Auto-store: Every prompt and tool use captured"
|
|
344
|
+
echo " - Auto-ingest: Files read by Claude auto-ingested to memory"
|
|
313
345
|
echo " - Episode summaries: Generated every 10 tool uses"
|
|
314
346
|
echo ""
|
|
315
347
|
echo "Manual Commands:"
|
|
@@ -319,6 +351,8 @@ print_success() {
|
|
|
319
351
|
echo "Environment Variables:"
|
|
320
352
|
echo " NEXUS_VERBOSE=1 Enable debug output"
|
|
321
353
|
echo " NEXUS_RECALL_LIMIT=10 Number of memories to auto-recall"
|
|
354
|
+
echo " NEXUS_AUTO_INGEST=0 Disable auto-ingestion of files"
|
|
355
|
+
echo " NEXUS_FRESHNESS_DAYS=30 Days before document flagged as stale"
|
|
322
356
|
echo ""
|
|
323
357
|
echo "Documentation: https://github.com/adverant/nexus-memory-skill"
|
|
324
358
|
echo ""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adverant/nexus-memory-skill",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Claude Code skill for persistent memory via Nexus GraphRAG - store and recall memories across all sessions and projects",
|
|
5
5
|
"main": "SKILL.md",
|
|
6
6
|
"type": "module",
|