@adverant/nexus-memory-skill 1.3.0 → 2.1.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.
@@ -0,0 +1,596 @@
1
+ #!/bin/bash
2
+ #
3
+ # Nexus Memory - Beads Sync Hook
4
+ # Bidirectional sync between local bd (beads) and Nexus GraphRAG
5
+ #
6
+ # Attribution:
7
+ # Beads (bd) is created by Steve Yegge
8
+ # Repository: https://github.com/steveyegge/beads
9
+ # License: See the beads repository for license terms.
10
+ #
11
+ # This hook provides GraphRAG sync capabilities for the beads issue tracker,
12
+ # enabling cross-device sync and semantic search of issues/tasks.
13
+ #
14
+ # Usage:
15
+ # bead-sync.sh install # Download bd binary if not present
16
+ # bead-sync.sh sync # Full bidirectional sync
17
+ # bead-sync.sh push # Push local beads to GraphRAG
18
+ # bead-sync.sh pull # Pull beads from GraphRAG to local
19
+ # bead-sync.sh sync-latest # Sync only recently changed beads
20
+ # bead-sync.sh query "text" # Query beads from GraphRAG
21
+ #
22
+ # Environment Variables:
23
+ # NEXUS_API_KEY - API key for authentication (REQUIRED)
24
+ # NEXUS_API_URL - API endpoint (default: https://api.adverant.ai)
25
+ # NEXUS_VERBOSE - Set to 1 for debug output
26
+ # BD_VERSION - Beads version to install (default: latest)
27
+ #
28
+
29
+ set -o pipefail
30
+
31
+ # Configuration
32
+ NEXUS_API_KEY="${NEXUS_API_KEY:-}"
33
+ NEXUS_API_URL="${NEXUS_API_URL:-https://api.adverant.ai}"
34
+ VERBOSE="${NEXUS_VERBOSE:-0}"
35
+ BD_VERSION="${BD_VERSION:-latest}"
36
+
37
+ # Paths
38
+ BD_BIN="${HOME}/.local/bin/bd"
39
+ STATE_DIR="${HOME}/.claude/session-env"
40
+ LAST_BEAD_SYNC="${STATE_DIR}/last_bead_sync"
41
+ LAST_BEAD_ID="${STATE_DIR}/last_bead_id"
42
+
43
+ # Logging
44
+ log() {
45
+ if [[ "$VERBOSE" == "1" ]]; then
46
+ echo "[bead-sync] $1" >&2
47
+ fi
48
+ }
49
+
50
+ log_error() {
51
+ echo "[bead-sync] ERROR: $1" >&2
52
+ }
53
+
54
+ log_info() {
55
+ echo "[bead-sync] $1" >&2
56
+ }
57
+
58
+ # Ensure state directory exists
59
+ mkdir -p "$STATE_DIR"
60
+ mkdir -p "$(dirname "$BD_BIN")"
61
+
62
+ # Check for API key
63
+ check_api_key() {
64
+ if [[ -z "$NEXUS_API_KEY" ]]; then
65
+ log_error "NEXUS_API_KEY environment variable is required but not set."
66
+ log_error "Get your API key from: https://dashboard.adverant.ai/dashboard/api-keys"
67
+ return 1
68
+ fi
69
+ return 0
70
+ }
71
+
72
+ # Check dependencies
73
+ check_deps() {
74
+ if ! command -v jq &> /dev/null; then
75
+ log_error "jq is required but not installed. Install with: brew install jq"
76
+ return 1
77
+ fi
78
+ if ! command -v curl &> /dev/null; then
79
+ log_error "curl is required but not installed."
80
+ return 1
81
+ fi
82
+ return 0
83
+ }
84
+
85
+ # Detect platform for binary download
86
+ detect_platform() {
87
+ local os=$(uname -s | tr '[:upper:]' '[:lower:]')
88
+ local arch=$(uname -m)
89
+
90
+ # Normalize architecture
91
+ case "$arch" in
92
+ x86_64) arch="amd64" ;;
93
+ aarch64) arch="arm64" ;;
94
+ arm64) arch="arm64" ;;
95
+ esac
96
+
97
+ echo "${os}_${arch}"
98
+ }
99
+
100
+ # Install bd binary if not present
101
+ install_bd() {
102
+ # Check if already installed and working
103
+ if [[ -f "$BD_BIN" ]] && [[ -x "$BD_BIN" ]]; then
104
+ if "$BD_BIN" --version &>/dev/null; then
105
+ log "bd binary already installed and working at $BD_BIN"
106
+ "$BD_BIN" --version 2>/dev/null || true
107
+ return 0
108
+ else
109
+ log "bd binary exists but not working, reinstalling..."
110
+ rm -f "$BD_BIN"
111
+ fi
112
+ fi
113
+
114
+ log_info "Installing beads (bd) binary..."
115
+
116
+ local platform=$(detect_platform)
117
+ local download_url
118
+ local temp_dir=$(mktemp -d)
119
+
120
+ if [[ "$BD_VERSION" == "latest" ]]; then
121
+ # Get latest release URL from GitHub API
122
+ # The releases are .tar.gz archives, not raw binaries
123
+ download_url=$(curl -s "https://api.github.com/repos/steveyegge/beads/releases/latest" | \
124
+ jq -r ".assets[] | select(.name | test(\"${platform}\")) | select(.name | endswith(\".tar.gz\")) | .browser_download_url" | head -1)
125
+ else
126
+ download_url="https://github.com/steveyegge/beads/releases/download/${BD_VERSION}/beads_${BD_VERSION}_${platform}.tar.gz"
127
+ fi
128
+
129
+ if [[ -z "$download_url" ]] || [[ "$download_url" == "null" ]]; then
130
+ log_error "Could not find bd binary for platform: $platform"
131
+ log_error "Try installing manually: go install github.com/steveyegge/beads/cmd/bd@latest"
132
+ rm -rf "$temp_dir"
133
+ return 1
134
+ fi
135
+
136
+ log "Downloading from: $download_url"
137
+
138
+ # Download and extract the tarball
139
+ local tarball="${temp_dir}/beads.tar.gz"
140
+ if curl -sL "$download_url" -o "$tarball"; then
141
+ # Extract to temp directory
142
+ if tar -xzf "$tarball" -C "$temp_dir" 2>/dev/null; then
143
+ # Find the bd binary in the extracted files
144
+ local extracted_bd=$(find "$temp_dir" -name "bd" -type f -perm +111 2>/dev/null | head -1)
145
+ if [[ -z "$extracted_bd" ]]; then
146
+ # Try without execute permission check
147
+ extracted_bd=$(find "$temp_dir" -name "bd" -type f 2>/dev/null | head -1)
148
+ fi
149
+
150
+ if [[ -n "$extracted_bd" ]] && [[ -f "$extracted_bd" ]]; then
151
+ cp "$extracted_bd" "$BD_BIN"
152
+ chmod +x "$BD_BIN"
153
+ rm -rf "$temp_dir"
154
+
155
+ if "$BD_BIN" --version &>/dev/null; then
156
+ log_info "Successfully installed bd to $BD_BIN"
157
+ "$BD_BIN" --version 2>/dev/null || true
158
+ return 0
159
+ else
160
+ log_error "bd binary installed but not executable"
161
+ rm -f "$BD_BIN"
162
+ return 1
163
+ fi
164
+ else
165
+ log_error "Could not find bd binary in archive"
166
+ log "Archive contents:"
167
+ tar -tzf "$tarball" 2>/dev/null | head -10 >&2
168
+ rm -rf "$temp_dir"
169
+ return 1
170
+ fi
171
+ else
172
+ log_error "Failed to extract tarball"
173
+ rm -rf "$temp_dir"
174
+ return 1
175
+ fi
176
+ else
177
+ log_error "Failed to download bd binary"
178
+ rm -rf "$temp_dir"
179
+ return 1
180
+ fi
181
+ }
182
+
183
+ # Check if bd is available
184
+ ensure_bd() {
185
+ if [[ ! -x "$BD_BIN" ]] && ! command -v bd &> /dev/null; then
186
+ install_bd || return 1
187
+ fi
188
+ return 0
189
+ }
190
+
191
+ # Get the bd command (prefer local install)
192
+ get_bd_cmd() {
193
+ if [[ -x "$BD_BIN" ]]; then
194
+ echo "$BD_BIN"
195
+ elif command -v bd &> /dev/null; then
196
+ echo "bd"
197
+ else
198
+ echo ""
199
+ fi
200
+ }
201
+
202
+ # Get current project info
203
+ get_project_info() {
204
+ local project_dir=$(pwd)
205
+ local project_name=$(basename "$project_dir")
206
+ local git_remote=$(git remote get-url origin 2>/dev/null || echo "local")
207
+
208
+ echo "{\"dir\": \"$project_dir\", \"name\": \"$project_name\", \"remote\": \"$git_remote\"}"
209
+ }
210
+
211
+ # Push a single bead to GraphRAG
212
+ push_bead_to_graphrag() {
213
+ local bead_json="$1"
214
+ local project_info="$2"
215
+
216
+ local bead_id=$(echo "$bead_json" | jq -r '.id // .ID // empty')
217
+ local title=$(echo "$bead_json" | jq -r '.title // .Title // empty')
218
+ local description=$(echo "$bead_json" | jq -r '.description // .Description // ""')
219
+ local status=$(echo "$bead_json" | jq -r '.status // .Status // "open"')
220
+ local priority=$(echo "$bead_json" | jq -r '.priority // .Priority // "P2"')
221
+ local issue_type=$(echo "$bead_json" | jq -r '.issueType // .type // "task"')
222
+ local created_at=$(echo "$bead_json" | jq -r '.createdAt // .created_at // empty')
223
+ local deps=$(echo "$bead_json" | jq -c '.dependencies // []')
224
+
225
+ local project_name=$(echo "$project_info" | jq -r '.name')
226
+ local project_dir=$(echo "$project_info" | jq -r '.dir')
227
+
228
+ if [[ -z "$bead_id" ]]; then
229
+ log "Skipping bead with no ID"
230
+ return 0
231
+ fi
232
+
233
+ log "Pushing bead $bead_id: $title"
234
+
235
+ # Build content for memory storage
236
+ local content="[Bead $bead_id] $title
237
+
238
+ Status: $status
239
+ Priority: $priority
240
+ Type: $issue_type
241
+ Project: $project_name
242
+
243
+ $description"
244
+
245
+ # Build tags array
246
+ local tags=$(jq -n \
247
+ --arg id "$bead_id" \
248
+ --arg status "$status" \
249
+ --arg priority "$priority" \
250
+ --arg project "$project_name" \
251
+ --arg type "$issue_type" \
252
+ '["bead", "type:bead", ("bead-id:" + $id), ("status:" + $status), ("priority:" + $priority), ("project:" + $project), ("issue-type:" + $type)]')
253
+
254
+ # Build metadata
255
+ local metadata=$(jq -n \
256
+ --arg bead_id "$bead_id" \
257
+ --arg title "$title" \
258
+ --arg status "$status" \
259
+ --arg priority "$priority" \
260
+ --arg issue_type "$issue_type" \
261
+ --arg project "$project_name" \
262
+ --arg project_dir "$project_dir" \
263
+ --arg created_at "$created_at" \
264
+ --argjson deps "$deps" \
265
+ '{
266
+ bead_id: $bead_id,
267
+ title: $title,
268
+ status: $status,
269
+ priority: $priority,
270
+ issue_type: $issue_type,
271
+ project: $project,
272
+ project_dir: $project_dir,
273
+ created_at: $created_at,
274
+ dependencies: $deps
275
+ }')
276
+
277
+ # Build full payload
278
+ local payload=$(jq -n \
279
+ --arg content "$content" \
280
+ --argjson tags "$tags" \
281
+ --argjson metadata "$metadata" \
282
+ --arg bead_id "$bead_id" \
283
+ '{
284
+ content: $content,
285
+ event_type: "bead",
286
+ tags: $tags,
287
+ metadata: $metadata,
288
+ extract_entities: true,
289
+ entity_types: ["bead", "task", "feature", "bug", "project", "file", "function"],
290
+ domain: "code",
291
+ create_relationships: true,
292
+ episodic: {
293
+ type: "bead",
294
+ interaction_id: $bead_id
295
+ }
296
+ }')
297
+
298
+ # Send to GraphRAG
299
+ local response=$(curl -s -X POST "${NEXUS_API_URL}/api/memory/store" \
300
+ -H "Authorization: Bearer ${NEXUS_API_KEY}" \
301
+ -H "Content-Type: application/json" \
302
+ -d "$payload" \
303
+ --max-time 10)
304
+
305
+ local success=$(echo "$response" | jq -r '.success // false')
306
+
307
+ if [[ "$success" == "true" ]]; then
308
+ log "Successfully pushed bead $bead_id"
309
+ echo "$bead_id" > "$LAST_BEAD_ID"
310
+ return 0
311
+ else
312
+ log_error "Failed to push bead $bead_id: $(echo "$response" | jq -r '.message // .error // "unknown error"')"
313
+ return 1
314
+ fi
315
+ }
316
+
317
+ # Push all local beads to GraphRAG
318
+ push_beads() {
319
+ check_api_key || return 1
320
+ ensure_bd || return 1
321
+
322
+ local bd_cmd=$(get_bd_cmd)
323
+ if [[ -z "$bd_cmd" ]]; then
324
+ log_error "bd command not available"
325
+ return 1
326
+ fi
327
+
328
+ # Check if beads is initialized in this repo
329
+ if [[ ! -d ".beads" ]] && ! "$bd_cmd" list &>/dev/null; then
330
+ log "No beads found in this repository. Run 'bd init' to initialize."
331
+ return 0
332
+ fi
333
+
334
+ local project_info=$(get_project_info)
335
+ local beads=$("$bd_cmd" list --json 2>/dev/null || echo "[]")
336
+ local count=$(echo "$beads" | jq 'length')
337
+
338
+ log_info "Pushing $count beads to GraphRAG..."
339
+
340
+ local pushed=0
341
+ local failed=0
342
+
343
+ echo "$beads" | jq -c '.[]' 2>/dev/null | while read -r bead; do
344
+ if push_bead_to_graphrag "$bead" "$project_info"; then
345
+ ((pushed++)) || true
346
+ else
347
+ ((failed++)) || true
348
+ fi
349
+ done
350
+
351
+ # Record sync timestamp
352
+ date -u +"%Y-%m-%dT%H:%M:%SZ" > "$LAST_BEAD_SYNC"
353
+
354
+ log_info "Push complete. Pushed: $pushed, Failed: $failed"
355
+ return 0
356
+ }
357
+
358
+ # Pull beads from GraphRAG and create locally if missing
359
+ pull_beads() {
360
+ check_api_key || return 1
361
+ ensure_bd || return 1
362
+
363
+ local bd_cmd=$(get_bd_cmd)
364
+ if [[ -z "$bd_cmd" ]]; then
365
+ log_error "bd command not available"
366
+ return 1
367
+ fi
368
+
369
+ local project_info=$(get_project_info)
370
+ local project_name=$(echo "$project_info" | jq -r '.name')
371
+
372
+ log_info "Pulling beads from GraphRAG for project: $project_name"
373
+
374
+ # Query GraphRAG for beads from this project
375
+ local query_payload=$(jq -n \
376
+ --arg project "$project_name" \
377
+ '{
378
+ query: "beads tasks issues",
379
+ filters: {
380
+ tags: ["type:bead", ("project:" + $project)]
381
+ },
382
+ limit: 100,
383
+ includeEpisodic: true
384
+ }')
385
+
386
+ local response=$(curl -s -X POST "${NEXUS_API_URL}/api/retrieve/enhanced" \
387
+ -H "Authorization: Bearer ${NEXUS_API_KEY}" \
388
+ -H "Content-Type: application/json" \
389
+ -d "$query_payload" \
390
+ --max-time 15)
391
+
392
+ local memories=$(echo "$response" | jq -c '.data.memories // .memories // []')
393
+ local count=$(echo "$memories" | jq 'length')
394
+
395
+ log "Found $count beads in GraphRAG"
396
+
397
+ if [[ "$count" == "0" ]]; then
398
+ log "No beads to pull from GraphRAG"
399
+ return 0
400
+ fi
401
+
402
+ # Check if beads is initialized
403
+ if [[ ! -d ".beads" ]]; then
404
+ log "Initializing beads in this repository..."
405
+ "$bd_cmd" init 2>/dev/null || true
406
+ fi
407
+
408
+ local created=0
409
+ local skipped=0
410
+
411
+ # For each bead in GraphRAG, check if it exists locally
412
+ echo "$memories" | jq -c '.[]' 2>/dev/null | while read -r memory; do
413
+ local bead_id=$(echo "$memory" | jq -r '.metadata.bead_id // empty')
414
+ local title=$(echo "$memory" | jq -r '.metadata.title // empty')
415
+ local status=$(echo "$memory" | jq -r '.metadata.status // "open"')
416
+ local priority=$(echo "$memory" | jq -r '.metadata.priority // "P2"')
417
+
418
+ if [[ -z "$bead_id" ]]; then
419
+ continue
420
+ fi
421
+
422
+ # Check if bead exists locally
423
+ if "$bd_cmd" show "$bead_id" &>/dev/null; then
424
+ log "Bead $bead_id already exists locally, skipping"
425
+ ((skipped++)) || true
426
+ continue
427
+ fi
428
+
429
+ # Create bead locally
430
+ log "Creating bead $bead_id: $title"
431
+
432
+ # Note: bd create may not support --id flag, this is a best-effort attempt
433
+ # The actual bd CLI might have different flags
434
+ if "$bd_cmd" create "$title" --priority "$priority" 2>/dev/null; then
435
+ ((created++)) || true
436
+ else
437
+ log "Could not create bead locally (may already exist with different ID)"
438
+ fi
439
+ done
440
+
441
+ log_info "Pull complete. Created: $created, Skipped: $skipped"
442
+ return 0
443
+ }
444
+
445
+ # Full bidirectional sync
446
+ sync_bidirectional() {
447
+ log_info "Starting bidirectional sync..."
448
+
449
+ push_beads
450
+ pull_beads
451
+
452
+ log_info "Bidirectional sync complete"
453
+ }
454
+
455
+ # Sync only recently changed beads (since last sync)
456
+ sync_latest() {
457
+ check_api_key || return 1
458
+ ensure_bd || return 1
459
+
460
+ local bd_cmd=$(get_bd_cmd)
461
+ if [[ -z "$bd_cmd" ]]; then
462
+ log_error "bd command not available"
463
+ return 1
464
+ fi
465
+
466
+ local last_sync=""
467
+ if [[ -f "$LAST_BEAD_SYNC" ]]; then
468
+ last_sync=$(cat "$LAST_BEAD_SYNC")
469
+ fi
470
+
471
+ log "Syncing beads changed since: ${last_sync:-never}"
472
+
473
+ # For now, just do a full push (bd doesn't have --since flag)
474
+ # In the future, we could track which beads have changed
475
+ push_beads
476
+ }
477
+
478
+ # Query beads from GraphRAG
479
+ query_beads() {
480
+ local query_text="$1"
481
+
482
+ check_api_key || return 1
483
+
484
+ if [[ -z "$query_text" ]]; then
485
+ query_text="beads tasks issues"
486
+ fi
487
+
488
+ local project_info=$(get_project_info)
489
+ local project_name=$(echo "$project_info" | jq -r '.name')
490
+
491
+ log "Querying GraphRAG for: $query_text"
492
+
493
+ local query_payload=$(jq -n \
494
+ --arg query "$query_text" \
495
+ --arg project "$project_name" \
496
+ '{
497
+ query: $query,
498
+ filters: {
499
+ tags: ["type:bead"]
500
+ },
501
+ limit: 20,
502
+ includeEpisodic: true
503
+ }')
504
+
505
+ local response=$(curl -s -X POST "${NEXUS_API_URL}/api/retrieve/enhanced" \
506
+ -H "Authorization: Bearer ${NEXUS_API_KEY}" \
507
+ -H "Content-Type: application/json" \
508
+ -d "$query_payload" \
509
+ --max-time 15)
510
+
511
+ # Pretty print results
512
+ echo "$response" | jq -r '.data.memories // .memories // [] | .[] |
513
+ "[\(.metadata.bead_id // "?")] \(.metadata.title // .content[0:50]) (\(.metadata.status // "?"))"'
514
+ }
515
+
516
+ # Show help
517
+ show_help() {
518
+ cat << 'EOF'
519
+ Beads Sync - Bidirectional sync between local bd and Nexus GraphRAG
520
+
521
+ Attribution:
522
+ Beads (bd) is created by Steve Yegge
523
+ https://github.com/steveyegge/beads
524
+
525
+ Usage:
526
+ bead-sync.sh <command> [args]
527
+
528
+ Commands:
529
+ install Download and install bd binary if not present
530
+ sync Full bidirectional sync (push local, pull remote)
531
+ push Push all local beads to GraphRAG
532
+ pull Pull beads from GraphRAG to local repository
533
+ sync-latest Sync only recently changed beads
534
+ query "text" Search for beads in GraphRAG
535
+ help Show this help message
536
+
537
+ Environment Variables:
538
+ NEXUS_API_KEY API key for authentication (required)
539
+ NEXUS_API_URL API endpoint (default: https://api.adverant.ai)
540
+ NEXUS_VERBOSE Set to 1 for debug output
541
+ BD_VERSION Beads version to install (default: latest)
542
+
543
+ Examples:
544
+ # Install bd binary
545
+ bead-sync.sh install
546
+
547
+ # Full sync
548
+ bead-sync.sh sync
549
+
550
+ # Push local beads to GraphRAG
551
+ bead-sync.sh push
552
+
553
+ # Search for authentication-related beads
554
+ bead-sync.sh query "authentication login"
555
+ EOF
556
+ }
557
+
558
+ # Main command dispatcher
559
+ main() {
560
+ local command="${1:-help}"
561
+ shift || true
562
+
563
+ check_deps || exit 1
564
+
565
+ case "$command" in
566
+ install)
567
+ install_bd
568
+ ;;
569
+ sync)
570
+ sync_bidirectional
571
+ ;;
572
+ push)
573
+ push_beads
574
+ ;;
575
+ pull)
576
+ pull_beads
577
+ ;;
578
+ sync-latest)
579
+ sync_latest
580
+ ;;
581
+ query)
582
+ query_beads "$1"
583
+ ;;
584
+ help|--help|-h)
585
+ show_help
586
+ ;;
587
+ *)
588
+ log_error "Unknown command: $command"
589
+ show_help
590
+ exit 1
591
+ ;;
592
+ esac
593
+ }
594
+
595
+ # Run main
596
+ main "$@"