Skip to content

Commit 9c01a5f

Browse files
authored
feat: replace PATH-based wrapper with shell hook (Janus pattern) (#5)
1 parent 7d8fe45 commit 9c01a5f

4 files changed

Lines changed: 163 additions & 38 deletions

File tree

AGENTS.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ OpenCode plugin that replicates Claude Code's persistent memory system. TypeScri
66

77
```
88
.
9-
├── bin/opencode # Bash wrapper: post-session memory extraction via --fork
9+
├── bin/opencode-memory # Bash wrapper: shell hook install + post-session memory extraction via --fork
1010
├── src/
1111
│ ├── index.ts # Plugin entry: MemoryPlugin export, 5 tools + system prompt hook
1212
│ ├── memory.ts # CRUD: save/delete/list/search/read + MEMORY.md index management
@@ -26,7 +26,8 @@ OpenCode plugin that replicates Claude Code's persistent memory system. TypeScri
2626
| Fix path resolution or worktree sharing | `src/paths.ts``getMemoryDir()`, `findCanonicalGitRoot()` |
2727
| Modify what the agent sees about memory | `src/prompt.ts``buildMemorySystemPrompt()` |
2828
| Change which memories are auto-recalled | `src/recall.ts``recallRelevantMemories()` |
29-
| Fix post-session extraction | `bin/opencode` — bash wrapper |
29+
| Fix post-session extraction | `bin/opencode-memory` — bash wrapper |
30+
| Fix shell hook install/uninstall | `bin/opencode-memory``install`/`uninstall` subcommands |
3031

3132
## Critical Coupling
3233

@@ -91,5 +92,6 @@ git push origin main
9192

9293
- Memory directory is `~/.claude/projects/<sanitizePath(canonicalGitRoot)>/memory/` — shared with Claude Code bidirectionally
9394
- `sanitizePath()` + `djb2Hash()` are exact copies from Claude Code source to guarantee byte-identical paths
94-
- The bash wrapper (`bin/opencode`) uses `mktemp` timestamp comparison to detect if the main agent already wrote memories — if so, extraction is skipped
95+
- The bash wrapper (`bin/opencode-memory`) uses `mktemp` timestamp comparison to detect if the main agent already wrote memories — if so, extraction is skipped
96+
- Shell hook is installed via `opencode-memory install`, which writes an `opencode()` function to `~/.zshrc` or `~/.bashrc` — shell functions take priority over PATH binaries
9597
- `package-lock.json` is gitignored (Bun runtime, not npm)

README.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,13 @@ Claude Code writes memory → OpenCode reads it. OpenCode writes memory → Clau
3131

3232
```bash
3333
npm install -g opencode-claude-memory
34+
opencode-memory install # one-time: installs shell hook
3435
```
3536

3637
This installs:
37-
- the **plugin** (memory tools + system prompt injection)
38-
- an `opencode` **wrapper** (auto-extracts memories after each session)
38+
- The **plugin** — memory tools + system prompt injection
39+
- The `opencode-memory` **CLI** — wraps opencode with post-session memory extraction
40+
- A **shell hook** — defines an `opencode()` function in your `.zshrc`/`.bashrc` that delegates to `opencode-memory`
3941

4042
### 2. Configure
4143

@@ -54,6 +56,15 @@ opencode
5456

5557
That’s it. Memory extraction runs in the background after each session.
5658

59+
To uninstall:
60+
61+
```bash
62+
opencode-memory uninstall # removes shell hook from .zshrc/.bashrc
63+
npm uninstall -g opencode-claude-memory
64+
```
65+
66+
This removes the shell hook, the CLI, and the plugin. Your saved memories in `~/.claude/projects/` are **not** deleted.
67+
5768
## 💡 Why this exists
5869

5970
If you use both Claude Code and OpenCode on the same repository, memory often ends up in separate silos.
@@ -75,11 +86,23 @@ The outcome: **shared context across Claude Code and OpenCode without maintainin
7586

7687
## ⚙️ How it works
7788

78-
1. You run `opencode` (wrapper).
79-
2. Wrapper finds and launches the real OpenCode binary.
80-
3. You use OpenCode normally.
81-
4. After exit, memory extraction runs in the background.
82-
5. Memories are saved to Claude-compatible paths under `~/.claude/projects/`.
89+
```mermaid
90+
graph LR
91+
A[You run opencode] --> B[Shell hook calls opencode-memory]
92+
B --> C[opencode-memory finds real binary]
93+
C --> D[Runs opencode normally]
94+
D --> E[You exit]
95+
E --> F[Fork session + extract memories]
96+
F --> G[Memories saved to ~/.claude/projects/]
97+
```
98+
99+
The shell hook defines an `opencode()` function that delegates to `opencode-memory`:
100+
101+
1. Shell function intercepts `opencode` command (higher priority than PATH)
102+
2. `opencode-memory` finds the real `opencode` binary in PATH
103+
3. Runs it with all your arguments
104+
4. After you exit, forks the session with a memory extraction prompt
105+
5. Extraction runs **in the background** — you're never blocked
83106

84107
### Compatibility details
85108

bin/opencode renamed to bin/opencode-memory

Lines changed: 127 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
#!/usr/bin/env bash
22
#
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.
54
#
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.
87
#
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
1112
#
1213
# 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
1820
#
1921
# Requirements:
20-
# - Real `opencode` CLI reachable in PATH (after this wrapper)
22+
# - Real `opencode` CLI reachable in PATH
2123
# - `jq` for JSON parsing
2224
# - The opencode-memory plugin installed (provides memory_save tool)
2325
#
@@ -32,23 +34,121 @@
3234
set -euo pipefail
3335

3436
# ============================================================================
35-
# Resolve the real opencode binary (skip this wrapper)
37+
# Shell Hook Management
3638
# ============================================================================
3739

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+
}
3965

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
47113
fi
48114
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"
52152
}
53153

54154
REAL_OPENCODE="$(find_real_opencode)"
@@ -129,15 +229,15 @@ has_new_memories() {
129229
# Check if any memory file was modified during the session
130230
# Checks all projects' memory directories for files newer than the timestamp marker
131231
local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
132-
232+
133233
if [ ! -d "$mem_base" ]; then
134234
return 1
135235
fi
136-
236+
137237
# Find any .md file under projects/*/memory/ newer than our timestamp
138238
local newer_files
139239
newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1)
140-
240+
141241
[ -n "$newer_files" ]
142242
}
143243

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "Claude Code-compatible memory compatibility layer for OpenCode — zero config, local-first, no migration",
66
"main": "src/index.ts",
77
"bin": {
8-
"opencode": "./bin/opencode"
8+
"opencode-memory": "./bin/opencode-memory"
99
},
1010
"exports": {
1111
".": "./src/index.ts"

0 commit comments

Comments
 (0)