|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | # |
3 | | -# opencode (memory wrapper) — Drop-in replacement that wraps the real opencode |
4 | | -# binary, then automatically extracts and saves memories after the session ends. |
| 3 | +# opencode-memory — Wrapper for OpenCode with automatic memory extraction. |
5 | 4 | # |
6 | | -# Install by placing this earlier in PATH than the real opencode binary. |
7 | | -# The script finds the real opencode by scanning PATH and skipping itself. |
| 5 | +# Installs a shell hook (function) that intercepts the `opencode` command, |
| 6 | +# then wraps the real binary with post-session memory extraction. |
8 | 7 | # |
9 | | -# Usage: |
10 | | -# opencode [any opencode args...] |
| 8 | +# Subcommands: |
| 9 | +# opencode-memory install — Install shell hook to ~/.zshrc or ~/.bashrc |
| 10 | +# opencode-memory uninstall — Remove shell hook |
| 11 | +# opencode-memory [args...] — Run opencode with memory extraction |
11 | 12 | # |
12 | 13 | # How it works: |
13 | | -# 1. Finds the real `opencode` binary (skipping this wrapper in PATH) |
14 | | -# 2. Runs it normally with all your arguments |
15 | | -# 3. After you exit, finds the most recent session |
16 | | -# 4. Forks that session and sends a memory extraction prompt |
17 | | -# 5. The extraction runs in the background so you're not blocked |
| 14 | +# 1. Shell hook defines `opencode()` function that delegates to `opencode-memory` |
| 15 | +# 2. `opencode-memory` finds the real `opencode` binary in PATH |
| 16 | +# 3. Runs it normally with all your arguments |
| 17 | +# 4. After you exit, finds the most recent session |
| 18 | +# 5. Forks that session and sends a memory extraction prompt |
| 19 | +# 6. The extraction runs in the background so you're not blocked |
18 | 20 | # |
19 | 21 | # Requirements: |
20 | | -# - Real `opencode` CLI reachable in PATH (after this wrapper) |
| 22 | +# - Real `opencode` CLI reachable in PATH |
21 | 23 | # - `jq` for JSON parsing |
22 | 24 | # - The opencode-memory plugin installed (provides memory_save tool) |
23 | 25 | # |
|
32 | 34 | set -euo pipefail |
33 | 35 |
|
34 | 36 | # ============================================================================ |
35 | | -# Resolve the real opencode binary (skip this wrapper) |
| 37 | +# Shell Hook Management |
36 | 38 | # ============================================================================ |
37 | 39 |
|
38 | | -SELF="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")" |
| 40 | +HOOK_START_MARKER='# >>> opencode-memory auto-initialization >>>' |
| 41 | +HOOK_END_MARKER='# <<< opencode-memory auto-initialization <<<' |
| 42 | + |
| 43 | +detect_shell_rc() { |
| 44 | + local shell_name |
| 45 | + shell_name="$(basename "${SHELL:-}")" |
| 46 | + |
| 47 | + case "$shell_name" in |
| 48 | + zsh) |
| 49 | + echo "$HOME/.zshrc" |
| 50 | + ;; |
| 51 | + bash) |
| 52 | + echo "$HOME/.bashrc" |
| 53 | + ;; |
| 54 | + *) |
| 55 | + if [ -f "$HOME/.zshrc" ]; then |
| 56 | + echo "$HOME/.zshrc" |
| 57 | + elif [ -f "$HOME/.bashrc" ]; then |
| 58 | + echo "$HOME/.bashrc" |
| 59 | + else |
| 60 | + echo "$HOME/.zshrc" |
| 61 | + fi |
| 62 | + ;; |
| 63 | + esac |
| 64 | +} |
39 | 65 |
|
40 | | -find_real_opencode() { |
41 | | - local IFS=':' |
42 | | - for dir in $PATH; do |
43 | | - local candidate="$dir/opencode" |
44 | | - if [ -x "$candidate" ] && [ "$(cd "$(dirname "$candidate")" && pwd -P)/$(basename "$candidate")" != "$SELF" ]; then |
45 | | - echo "$candidate" |
46 | | - return 0 |
| 66 | +install_hook() { |
| 67 | + local rc_file |
| 68 | + rc_file=$(detect_shell_rc) |
| 69 | + |
| 70 | + if grep -qF "$HOOK_START_MARKER" "$rc_file" 2>/dev/null; then |
| 71 | + echo "[opencode-memory] Hook already installed in $rc_file" |
| 72 | + return 0 |
| 73 | + fi |
| 74 | + |
| 75 | + cat >> "$rc_file" << 'HOOK' |
| 76 | +
|
| 77 | +# >>> opencode-memory auto-initialization >>> |
| 78 | +opencode() { |
| 79 | + command opencode-memory "$@" |
| 80 | +} |
| 81 | +# <<< opencode-memory auto-initialization <<< |
| 82 | +HOOK |
| 83 | + |
| 84 | + echo "[opencode-memory] Shell hook installed in $rc_file" |
| 85 | + echo "[opencode-memory] Restart your shell or run: source $rc_file" |
| 86 | +} |
| 87 | + |
| 88 | +remove_hook_from_rc() { |
| 89 | + local rc_file="$1" |
| 90 | + local tmp_file |
| 91 | + tmp_file=$(mktemp) |
| 92 | + |
| 93 | + awk -v start="$HOOK_START_MARKER" -v end="$HOOK_END_MARKER" ' |
| 94 | + $0 == start { skip=1; next } |
| 95 | + $0 == end { skip=0; next } |
| 96 | + !skip |
| 97 | + ' "$rc_file" > "$tmp_file" |
| 98 | + |
| 99 | + mv "$tmp_file" "$rc_file" |
| 100 | +} |
| 101 | + |
| 102 | +uninstall_hook() { |
| 103 | + local removed=0 |
| 104 | + local rc_file |
| 105 | + |
| 106 | + for rc_file in "$HOME/.zshrc" "$HOME/.bashrc"; do |
| 107 | + [ -f "$rc_file" ] || continue |
| 108 | + |
| 109 | + if grep -qF "$HOOK_START_MARKER" "$rc_file" 2>/dev/null; then |
| 110 | + remove_hook_from_rc "$rc_file" |
| 111 | + echo "[opencode-memory] Shell hook removed from $rc_file" |
| 112 | + removed=1 |
47 | 113 | fi |
48 | 114 | done |
49 | | - echo "[opencode-memory] ERROR: Cannot find real opencode binary in PATH" >&2 |
50 | | - echo "[opencode-memory] Make sure opencode is installed and this wrapper is placed earlier in PATH" >&2 |
51 | | - exit 1 |
| 115 | + |
| 116 | + if [ "$removed" -eq 0 ]; then |
| 117 | + rc_file=$(detect_shell_rc) |
| 118 | + echo "[opencode-memory] Hook not found in $rc_file" |
| 119 | + return 0 |
| 120 | + fi |
| 121 | + |
| 122 | + echo "[opencode-memory] Restart your shell or run: source <your rc file>" |
| 123 | +} |
| 124 | + |
| 125 | +# Handle subcommands before any opencode resolution |
| 126 | +case "${1:-}" in |
| 127 | + install) |
| 128 | + install_hook |
| 129 | + exit 0 |
| 130 | + ;; |
| 131 | + uninstall) |
| 132 | + uninstall_hook |
| 133 | + exit 0 |
| 134 | + ;; |
| 135 | +esac |
| 136 | + |
| 137 | +# ============================================================================ |
| 138 | +# Resolve the real opencode binary |
| 139 | +# ============================================================================ |
| 140 | + |
| 141 | +find_real_opencode() { |
| 142 | + # Since this script is named `opencode-memory` (not `opencode`), |
| 143 | + # `command -v opencode` finds the real binary without ambiguity. |
| 144 | + local real |
| 145 | + real=$(command -v opencode 2>/dev/null) || true |
| 146 | + if [ -z "$real" ] || [ ! -x "$real" ]; then |
| 147 | + echo "[opencode-memory] ERROR: Cannot find opencode binary in PATH" >&2 |
| 148 | + echo "[opencode-memory] Make sure opencode is installed: https://opencode.ai" >&2 |
| 149 | + exit 1 |
| 150 | + fi |
| 151 | + echo "$real" |
52 | 152 | } |
53 | 153 |
|
54 | 154 | REAL_OPENCODE="$(find_real_opencode)" |
@@ -129,15 +229,15 @@ has_new_memories() { |
129 | 229 | # Check if any memory file was modified during the session |
130 | 230 | # Checks all projects' memory directories for files newer than the timestamp marker |
131 | 231 | local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects" |
132 | | - |
| 232 | + |
133 | 233 | if [ ! -d "$mem_base" ]; then |
134 | 234 | return 1 |
135 | 235 | fi |
136 | | - |
| 236 | + |
137 | 237 | # Find any .md file under projects/*/memory/ newer than our timestamp |
138 | 238 | local newer_files |
139 | 239 | newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1) |
140 | | - |
| 240 | + |
141 | 241 | [ -n "$newer_files" ] |
142 | 242 | } |
143 | 243 |
|
|
0 commit comments