opencode-claude-memory 1.1.0 β†’ 1.3.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/README.md CHANGED
@@ -1,69 +1,31 @@
1
1
  <div align="center">
2
2
 
3
- # 🧠 opencode-claude-memory
3
+ # 🧠 Claude Code-compatible memory for OpenCode
4
4
 
5
- **A 1:1 replica of [Claude Code's memory system](https://github.com/anthropics/claude-code) for OpenCode**
5
+ **Make OpenCode and Claude Code share the same memory β€” zero config, local-first, and no migration required.**
6
6
 
7
- *Ported from the original source β€” same paths, same format, same tools, same prompts. Zero drift.*
8
-
9
- Claude Code writes memories β†’ OpenCode reads them. OpenCode writes memories β†’ Claude Code reads them.
7
+ Claude Code writes memory β†’ OpenCode reads it. OpenCode writes memory β†’ Claude Code reads it.
10
8
 
11
9
  [![npm version](https://img.shields.io/npm/v/opencode-claude-memory.svg?style=flat-square)](https://www.npmjs.com/package/opencode-claude-memory)
12
10
  [![npm downloads](https://img.shields.io/npm/dm/opencode-claude-memory.svg?style=flat-square)](https://www.npmjs.com/package/opencode-claude-memory)
13
11
  [![License](https://img.shields.io/npm/l/opencode-claude-memory.svg?style=flat-square)](https://github.com/kuitos/opencode-claude-memory/blob/main/LICENSE)
14
12
 
15
- [Features](#-features) β€’ [Quick Start](#-quick-start) β€’ [How It Works](#-how-it-works) β€’ [Configuration](#%EF%B8%8F-configuration) β€’ [Tools Reference](#-tools-reference)
13
+ [Quick Start](#-quick-start) β€’ [Why this exists](#-why-this-exists) β€’ [What makes this different](#-what-makes-this-different) β€’ [How it works](#-how-it-works) β€’ [Who this is for](#-who-this-is-for) β€’ [FAQ](#-faq)
16
14
 
17
15
  </div>
18
16
 
19
17
  ---
20
18
 
21
- ## ✨ Features
22
-
23
- <table>
24
- <tr>
25
- <td width="50%">
26
-
27
- ### πŸ”„ Claude Code Compatible
28
- Shares the exact same `~/.claude/projects/<project>/memory/` directory β€” bidirectional sync out of the box
29
-
30
- </td>
31
- <td width="50%">
32
-
33
- ### πŸ› οΈ 5 Memory Tools
34
- `memory_save`, `memory_delete`, `memory_list`, `memory_search`, `memory_read`
35
-
36
- </td>
37
- </tr>
38
- <tr>
39
- <td width="50%">
40
-
41
- ### ⚑ Auto-Extraction
42
- Drop-in `opencode` wrapper that extracts memories in the background after each session
43
-
44
- </td>
45
- <td width="50%">
46
-
47
- ### πŸ’‰ System Prompt Injection
48
- Existing memories are automatically injected into every conversation
49
-
50
- </td>
51
- </tr>
52
- <tr>
53
- <td width="50%">
54
-
55
- ### πŸ“ 4 Memory Types
56
- `user`, `feedback`, `project`, `reference` β€” same taxonomy as Claude Code
57
-
58
- </td>
59
- <td width="50%">
60
-
61
- ### 🌳 Git Worktree Aware
62
- Worktrees of the same repo share the same memory directory
19
+ ## ✨ At a glance
63
20
 
64
- </td>
65
- </tr>
66
- </table>
21
+ - **Claude Code-compatible memory**
22
+ Uses Claude Code’s existing memory paths, file format, and taxonomy.
23
+ - **Zero config**
24
+ Install + enable plugin, then keep using `opencode` as usual.
25
+ - **Local-first, no migration**
26
+ Memory stays as local Markdown files in the same directory Claude Code already uses.
27
+ - **Auto-dream consolidation**
28
+ Periodically runs a background memory consolidation pass (Claude-style auto-dream gating).
67
29
 
68
30
  ## πŸš€ Quick Start
69
31
 
@@ -71,11 +33,13 @@ Worktrees of the same repo share the same memory directory
71
33
 
72
34
  ```bash
73
35
  npm install -g opencode-claude-memory
36
+ opencode-memory install # one-time: installs shell hook
74
37
  ```
75
38
 
76
39
  This installs:
77
40
  - The **plugin** β€” memory tools + system prompt injection
78
- - An `opencode` **wrapper** β€” auto-extracts memories after each session
41
+ - The `opencode-memory` **CLI** β€” wraps opencode with automatic memory extraction + auto-dream consolidation
42
+ - A **shell hook** β€” defines an `opencode()` function in your `.zshrc`/`.bashrc` that delegates to `opencode-memory`
79
43
 
80
44
  ### 2. Configure
81
45
 
@@ -89,150 +53,157 @@ This installs:
89
53
  ### 3. Use
90
54
 
91
55
  ```bash
92
- opencode # just use it as usual
56
+ opencode
93
57
  ```
94
58
 
95
- The AI agent can now use memory tools:
59
+ That’s it. Memory extraction runs in the background after each session, and auto-dream consolidation is checked with time/session gates.
96
60
 
97
- - **"Remember that I prefer terse responses"** β†’ saves a `feedback` memory
98
- - **"What do you remember about me?"** β†’ reads from memory
99
- - **"Forget the memory about my role"** β†’ deletes a memory
100
-
101
- When you exit, memories are extracted in the background β€” zero blocking.
102
-
103
- <details>
104
- <summary>πŸ—‘οΈ Uninstall</summary>
61
+ To uninstall:
105
62
 
106
63
  ```bash
64
+ opencode-memory uninstall # removes shell hook from .zshrc/.bashrc
107
65
  npm uninstall -g opencode-claude-memory
108
66
  ```
109
67
 
110
- This removes the wrapper and the plugin. Your saved memories in `~/.claude/projects/` are **not** deleted.
68
+ This removes the shell hook, the CLI, and the plugin. Your saved memories in `~/.claude/projects/` are **not** deleted.
69
+
70
+ ## πŸ’‘ Why this exists
71
+
72
+ If you use both Claude Code and OpenCode on the same repository, memory often ends up in separate silos.
73
+
74
+ This project solves that by making OpenCode read and write memory in Claude Code’s existing structure, so your context carries over naturally between both tools.
75
+
76
+ ## 🧩 What makes this different
111
77
 
112
- </details>
78
+ Most memory plugins introduce a new storage model or migration step.
113
79
 
114
- ## πŸ’‘ How It Works
80
+ This one is a **compatibility layer**, not a new memory system:
81
+
82
+ - same memory directory conventions as Claude Code
83
+ - same Markdown + frontmatter format
84
+ - same memory taxonomy (`user`, `feedback`, `project`, `reference`)
85
+ - same project/worktree resolution behavior
86
+
87
+ The outcome: **shared context across Claude Code and OpenCode without maintaining two memory systems.**
88
+
89
+ ## βš™οΈ How it works
115
90
 
116
91
  ```mermaid
117
92
  graph LR
118
- A[You run opencode] --> B[Wrapper finds real binary]
119
- B --> C[Runs opencode normally]
120
- C --> D[You exit]
121
- D --> E[Get latest session ID]
122
- E --> F[Fork session + extract memories]
123
- F --> G[Memories saved to ~/.claude/projects/]
93
+ A[You run opencode] --> B[Shell hook calls opencode-memory]
94
+ B --> C[opencode-memory finds real binary]
95
+ C --> D[Runs opencode normally]
96
+ D --> E[You exit]
97
+ E --> F[Extract memories if needed]
98
+ F --> G[Evaluate auto-dream gate]
99
+ G --> H[Consolidate memories if gate passes]
100
+ H --> I[Memories saved to ~/.claude/projects/]
124
101
  ```
125
102
 
126
- The wrapper is a drop-in replacement that:
103
+ The shell hook defines an `opencode()` function that delegates to `opencode-memory`:
127
104
 
128
- 1. Scans `PATH` to find the real `opencode` binary (skipping itself)
129
- 2. Runs it with all your arguments
130
- 3. After you exit, forks the session with a memory extraction prompt
131
- 4. Extraction runs **in the background** β€” you're never blocked
105
+ 1. Shell function intercepts `opencode` command (higher priority than PATH)
106
+ 2. `opencode-memory` finds the real `opencode` binary in PATH
107
+ 3. Runs it with all your arguments
108
+ 4. After you exit, it checks whether the session already wrote memory files
109
+ 5. If needed, it forks the session with a memory extraction prompt
110
+ 6. It evaluates the auto-dream gate (default: at least 24h since last consolidation and 5 touched sessions)
111
+ 7. If the gate passes, it runs a background consolidation pass to merge/prune memories
112
+ 8. Maintenance runs **in the background** unless `OPENCODE_MEMORY_FOREGROUND=1`
132
113
 
133
- ### What "1:1 Replica" Means
114
+ ### Compatibility details
134
115
 
135
- Every core component is ported directly from [Claude Code's source](https://github.com/anthropics/claude-code):
116
+ The implementation ports core logic from Claude Code for path hashing, git-root/worktree handling, memory format, and memory prompting behavior, so both tools can operate on the same files safely.
136
117
 
137
- | Component | Source |
138
- |---|---|
139
- | `sanitizePath()` + `djb2Hash()` | `utils/sessionStoragePortable.ts` |
140
- | `findGitRoot()` + worktree resolution | `utils/git.ts` |
141
- | Memory types & frontmatter format | `commands/memory.ts` |
142
- | System prompt (types, when to save/skip) | `commands/memory.ts` |
143
- | Extraction prompt (post-session) | Claude Code's memory extraction agent |
118
+ ## πŸ‘₯ Who this is for
144
119
 
145
- This ensures:
146
- - `~/.claude/projects/<sanitized>/memory/` paths are **byte-identical** to Claude Code's output
147
- - Git worktrees resolve to the same canonical root
148
- - Memory files are interchangeable β€” no migration needed
120
+ - You use **both Claude Code and OpenCode**.
121
+ - You want **one shared memory context** across both tools.
122
+ - You prefer **file-based, local-first memory** you can inspect in Git/worktrees.
123
+ - You don’t want migration overhead or lock-in.
149
124
 
150
- ## βš™οΈ Configuration
125
+ ## ❓ FAQ
151
126
 
152
- ### Environment Variables
153
-
154
- | Variable | Default | Description |
155
- |---|---|---|
156
- | `OPENCODE_MEMORY_EXTRACT` | `1` | Set to `0` to disable auto-extraction |
157
- | `OPENCODE_MEMORY_FOREGROUND` | `0` | Set to `1` to run extraction in foreground (debugging) |
158
- | `OPENCODE_MEMORY_MODEL` | *(default)* | Override model for extraction |
159
- | `OPENCODE_MEMORY_AGENT` | *(default)* | Override agent for extraction |
160
-
161
- ### Logs
127
+ ### Is this a new memory system?
162
128
 
163
- Extraction logs are written to `$TMPDIR/opencode-memory-logs/extract-*.log`.
129
+ No. It is a compatibility layer that lets OpenCode use Claude Code-compatible memory layout and conventions.
164
130
 
165
- ### Concurrency Safety
131
+ ### Do I need to migrate existing memory?
166
132
 
167
- A file lock prevents multiple extractions from running simultaneously on the same project. Stale locks (from crashed processes) are automatically cleaned up.
133
+ No migration required. If you already have Claude Code memory files, OpenCode can work with them directly.
168
134
 
169
- ## πŸ“ Memory Format
135
+ ### Where is data stored?
170
136
 
171
- Each memory is a Markdown file with YAML frontmatter:
137
+ In local files under Claude-style project memory directories (for example, under `~/.claude/projects/<project>/memory/`).
172
138
 
173
- ```markdown
174
- ---
175
- name: User prefers terse responses
176
- description: User wants concise answers without trailing summaries
177
- type: feedback
178
- ---
139
+ ### Why file-based memory?
179
140
 
180
- Skip post-action summaries. User reads diffs directly.
141
+ File-based memory is transparent, local-first, easy to inspect/diff/back up, and works naturally with existing developer workflows.
181
142
 
182
- **Why:** User explicitly requested terse output style.
183
- **How to apply:** Don't summarize changes at the end of responses.
184
- ```
143
+ ### Can I disable auto extraction?
185
144
 
186
- ### Memory Types
145
+ Yes. Set `OPENCODE_MEMORY_EXTRACT=0`.
187
146
 
188
- | Type | Description |
189
- |---|---|
190
- | `user` | User's role, expertise, preferences |
191
- | `feedback` | Guidance on how to work (corrections and confirmations) |
192
- | `project` | Ongoing work context not derivable from code |
193
- | `reference` | Pointers to external resources |
147
+ ### Can I disable auto-dream?
194
148
 
195
- <details>
196
- <summary>πŸ“„ Index file (MEMORY.md)</summary>
149
+ Yes. Set `OPENCODE_MEMORY_AUTODREAM=0`. You can also tune gates with:
150
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_HOURS`
151
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS`
197
152
 
198
- `MEMORY.md` is an auto-managed index (not content storage). Each entry is one line:
153
+ ## πŸ”§ Configuration
199
154
 
200
- ```markdown
201
- - [User prefers terse responses](feedback_terse_responses.md) β€” Skip summaries, user reads diffs
202
- - [User is a data scientist](user_role.md) β€” Focus on observability/logging context
203
- ```
155
+ ### Environment variables
204
156
 
205
- </details>
157
+ - `OPENCODE_MEMORY_EXTRACT` (default `1`): set `0` to disable automatic memory extraction
158
+ - `OPENCODE_MEMORY_FOREGROUND` (default `0`): set `1` to run maintenance in foreground
159
+ - `OPENCODE_MEMORY_MODEL`: override model used for extraction
160
+ - `OPENCODE_MEMORY_AGENT`: override agent used for extraction
161
+ - `OPENCODE_MEMORY_AUTODREAM` (default `1`): set `0` to disable auto-dream consolidation
162
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_HOURS` (default `24`): min hours between consolidation runs
163
+ - `OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS` (default `5`): min touched sessions since last consolidation
164
+ - `OPENCODE_MEMORY_AUTODREAM_MODEL`: override model used for auto-dream
165
+ - `OPENCODE_MEMORY_AUTODREAM_AGENT`: override agent used for auto-dream
206
166
 
207
- ## πŸ”§ Tools Reference
167
+ ### Logs
208
168
 
209
- ### `memory_save`
169
+ Logs are written to `$TMPDIR/opencode-memory-logs/`:
170
+ - `extract-*.log`: automatic memory extraction
171
+ - `dream-*.log`: auto-dream consolidation
210
172
 
211
- Save or update a memory.
173
+ ### Concurrency safety
212
174
 
213
- | Parameter | Type | Required | Description |
214
- |---|---|---|---|
215
- | `file_name` | string | βœ… | File name slug (e.g., `user_role`) |
216
- | `name` | string | βœ… | Short title |
217
- | `description` | string | βœ… | One-line description for relevance matching |
218
- | `type` | enum | βœ… | `user`, `feedback`, `project`, or `reference` |
219
- | `content` | string | βœ… | Memory content |
175
+ Lock files prevent concurrent extraction/consolidation runs per project root. Stale locks are cleaned up automatically.
220
176
 
221
- ### `memory_delete`
177
+ ## πŸ“ Memory format
222
178
 
223
- Delete a memory by file name.
179
+ Each memory is a Markdown file with YAML frontmatter:
224
180
 
225
- ### `memory_list`
181
+ ```markdown
182
+ ---
183
+ name: User prefers terse responses
184
+ description: User wants concise answers without trailing summaries
185
+ type: feedback
186
+ ---
226
187
 
227
- List all memories with their metadata.
188
+ Skip post-action summaries. User reads diffs directly.
228
189
 
229
- ### `memory_search`
190
+ **Why:** User explicitly requested terse output style.
191
+ **How to apply:** Don't summarize changes at the end of responses.
192
+ ```
230
193
 
231
- Search memories by keyword across name, description, and content.
194
+ Supported memory types:
195
+ - `user`
196
+ - `feedback`
197
+ - `project`
198
+ - `reference`
232
199
 
233
- ### `memory_read`
200
+ ## πŸ”§ Tools reference
234
201
 
235
- Read the full content of a specific memory file.
202
+ - `memory_save`: save/update a memory
203
+ - `memory_delete`: delete a memory by filename
204
+ - `memory_list`: list memory metadata
205
+ - `memory_search`: search by keyword
206
+ - `memory_read`: read full memory content
236
207
 
237
208
  ## πŸ“„ License
238
209
 
@@ -0,0 +1,643 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # opencode-memory β€” Wrapper for OpenCode with post-session memory maintenance.
4
+ #
5
+ # Installs a shell hook (function) that intercepts the `opencode` command,
6
+ # then wraps the real binary with post-session extraction and auto-dream.
7
+ #
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 post-session memory maintenance
12
+ #
13
+ # How it works:
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. Optionally runs memory extraction (conversation -> memories)
19
+ # 6. Optionally runs auto-dream consolidation (memory pruning/merge)
20
+ #
21
+ # Requirements:
22
+ # - Real `opencode` CLI reachable in PATH
23
+ # - `jq` for auto-dream gate/session counting
24
+ # - The opencode-memory plugin installed (provides memory_* tools)
25
+ #
26
+ # Environment variables:
27
+ # OPENCODE_MEMORY_EXTRACT=0 β€” Disable post-session extraction
28
+ # OPENCODE_MEMORY_FOREGROUND=1 β€” Run maintenance in foreground (debug)
29
+ # OPENCODE_MEMORY_MODEL=... β€” Extraction model override
30
+ # OPENCODE_MEMORY_AGENT=... β€” Extraction agent override
31
+ # OPENCODE_MEMORY_AUTODREAM=0 β€” Disable auto-dream consolidation
32
+ # OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=24 β€” Min hours between auto-dream runs
33
+ # OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=5 β€” Min touched sessions since last consolidation
34
+ # OPENCODE_MEMORY_AUTODREAM_MODEL=... β€” Auto-dream model override
35
+ # OPENCODE_MEMORY_AUTODREAM_AGENT=... β€” Auto-dream agent override
36
+ # OPENCODE_MEMORY_DIR=... β€” Override working directory for opencode
37
+ #
38
+
39
+ set -euo pipefail
40
+
41
+ # ============================================================================
42
+ # Shell Hook Management
43
+ # ============================================================================
44
+
45
+ HOOK_START_MARKER='# >>> opencode-memory auto-initialization >>>'
46
+ HOOK_END_MARKER='# <<< opencode-memory auto-initialization <<<'
47
+
48
+ detect_shell_rc() {
49
+ local shell_name
50
+ shell_name="$(basename "${SHELL:-}")"
51
+
52
+ case "$shell_name" in
53
+ zsh)
54
+ echo "$HOME/.zshrc"
55
+ ;;
56
+ bash)
57
+ echo "$HOME/.bashrc"
58
+ ;;
59
+ *)
60
+ if [ -f "$HOME/.zshrc" ]; then
61
+ echo "$HOME/.zshrc"
62
+ elif [ -f "$HOME/.bashrc" ]; then
63
+ echo "$HOME/.bashrc"
64
+ else
65
+ echo "$HOME/.zshrc"
66
+ fi
67
+ ;;
68
+ esac
69
+ }
70
+
71
+ install_hook() {
72
+ local rc_file
73
+ rc_file=$(detect_shell_rc)
74
+
75
+ if grep -qF "$HOOK_START_MARKER" "$rc_file" 2>/dev/null; then
76
+ echo "[opencode-memory] Hook already installed in $rc_file"
77
+ return 0
78
+ fi
79
+
80
+ cat >> "$rc_file" << 'HOOK'
81
+
82
+ # >>> opencode-memory auto-initialization >>>
83
+ opencode() {
84
+ command opencode-memory "$@"
85
+ }
86
+ # <<< opencode-memory auto-initialization <<<
87
+ HOOK
88
+
89
+ echo "[opencode-memory] Shell hook installed in $rc_file"
90
+ echo "[opencode-memory] Restart your shell or run: source $rc_file"
91
+ }
92
+
93
+ remove_hook_from_rc() {
94
+ local rc_file="$1"
95
+ local tmp_file
96
+ tmp_file=$(mktemp)
97
+
98
+ awk -v start="$HOOK_START_MARKER" -v end="$HOOK_END_MARKER" '
99
+ $0 == start { skip=1; next }
100
+ $0 == end { skip=0; next }
101
+ !skip
102
+ ' "$rc_file" > "$tmp_file"
103
+
104
+ mv "$tmp_file" "$rc_file"
105
+ }
106
+
107
+ uninstall_hook() {
108
+ local removed=0
109
+ local rc_file
110
+
111
+ for rc_file in "$HOME/.zshrc" "$HOME/.bashrc"; do
112
+ [ -f "$rc_file" ] || continue
113
+
114
+ if grep -qF "$HOOK_START_MARKER" "$rc_file" 2>/dev/null; then
115
+ remove_hook_from_rc "$rc_file"
116
+ echo "[opencode-memory] Shell hook removed from $rc_file"
117
+ removed=1
118
+ fi
119
+ done
120
+
121
+ if [ "$removed" -eq 0 ]; then
122
+ rc_file=$(detect_shell_rc)
123
+ echo "[opencode-memory] Hook not found in $rc_file"
124
+ return 0
125
+ fi
126
+
127
+ echo "[opencode-memory] Restart your shell or run: source <your rc file>"
128
+ }
129
+
130
+ # Handle subcommands before any opencode resolution
131
+ case "${1:-}" in
132
+ install)
133
+ install_hook
134
+ exit 0
135
+ ;;
136
+ uninstall)
137
+ uninstall_hook
138
+ exit 0
139
+ ;;
140
+ esac
141
+
142
+ # ============================================================================
143
+ # Resolve the real opencode binary
144
+ # ============================================================================
145
+
146
+ find_real_opencode() {
147
+ # Since this script is named `opencode-memory` (not `opencode`),
148
+ # `command -v opencode` finds the real binary without ambiguity.
149
+ local real
150
+ real=$(command -v opencode 2>/dev/null) || true
151
+ if [ -z "$real" ] || [ ! -x "$real" ]; then
152
+ echo "[opencode-memory] ERROR: Cannot find opencode binary in PATH" >&2
153
+ echo "[opencode-memory] Make sure opencode is installed: https://opencode.ai" >&2
154
+ exit 1
155
+ fi
156
+ echo "$real"
157
+ }
158
+
159
+ REAL_OPENCODE="$(find_real_opencode)"
160
+
161
+ # ============================================================================
162
+ # Configuration
163
+ # ============================================================================
164
+
165
+ EXTRACT_ENABLED="${OPENCODE_MEMORY_EXTRACT:-1}"
166
+ FOREGROUND="${OPENCODE_MEMORY_FOREGROUND:-0}"
167
+ EXTRACT_MODEL="${OPENCODE_MEMORY_MODEL:-}"
168
+ EXTRACT_AGENT="${OPENCODE_MEMORY_AGENT:-}"
169
+
170
+ AUTODREAM_ENABLED="${OPENCODE_MEMORY_AUTODREAM:-1}"
171
+ AUTODREAM_MIN_HOURS="${OPENCODE_MEMORY_AUTODREAM_MIN_HOURS:-24}"
172
+ AUTODREAM_MIN_SESSIONS="${OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS:-5}"
173
+ AUTODREAM_SCAN_LIMIT="${OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT:-200}"
174
+ AUTODREAM_MODEL="${OPENCODE_MEMORY_AUTODREAM_MODEL:-$EXTRACT_MODEL}"
175
+ AUTODREAM_AGENT="${OPENCODE_MEMORY_AUTODREAM_AGENT:-$EXTRACT_AGENT}"
176
+ AUTODREAM_STALE_LOCK_SECS=$((60 * 60))
177
+
178
+ WORKING_DIR="${OPENCODE_MEMORY_DIR:-$(pwd)}"
179
+
180
+ # Scope lock files at project root granularity (not per-subdirectory).
181
+ PROJECT_SCOPE_DIR="$WORKING_DIR"
182
+ if git -C "$WORKING_DIR" rev-parse --show-toplevel >/dev/null 2>&1; then
183
+ PROJECT_SCOPE_DIR="$(git -C "$WORKING_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$WORKING_DIR")"
184
+ fi
185
+
186
+ PROJECT_KEY="$(printf '%s' "$PROJECT_SCOPE_DIR" | cksum | awk '{print $1}')"
187
+
188
+ # Lock files (prevent concurrent work on the same project)
189
+ LOCK_DIR="${TMPDIR:-/tmp}/opencode-memory-locks"
190
+ mkdir -p "$LOCK_DIR"
191
+ EXTRACT_LOCK_FILE="$LOCK_DIR/${PROJECT_KEY}.extract.lock"
192
+
193
+ STATE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/opencode-memory"
194
+ mkdir -p "$STATE_DIR"
195
+ CONSOLIDATION_LOCK_FILE="$STATE_DIR/${PROJECT_KEY}.consolidate-lock"
196
+
197
+ # Logs
198
+ LOG_DIR="${TMPDIR:-/tmp}/opencode-memory-logs"
199
+ mkdir -p "$LOG_DIR"
200
+ TASK_LOG_PREFIX="$(date +%Y%m%d-%H%M%S)-${PROJECT_KEY}"
201
+ EXTRACT_LOG_FILE="$LOG_DIR/extract-${TASK_LOG_PREFIX}.log"
202
+ AUTODREAM_LOG_FILE="$LOG_DIR/dream-${TASK_LOG_PREFIX}.log"
203
+
204
+ # ============================================================================
205
+ # Prompts
206
+ # ============================================================================
207
+
208
+ # Adapted from Claude Code's extraction prompt, simplified for OpenCode's
209
+ # headless run mode. The model sees the full conversation context via --fork.
210
+ EXTRACT_PROMPT='You are now acting as the memory extraction subagent. Review the entire conversation above and extract any information worth remembering for future sessions.
211
+
212
+ ## What to save
213
+
214
+ Use the `memory_save` tool to persist memories. There are four types:
215
+
216
+ 1. **user** β€” Who the user is: role, expertise, preferences, communication style. Helps tailor future interactions.
217
+ 2. **feedback** β€” Guidance on how to work: corrections ("don'\''t do X"), confirmations ("yes, keep doing that"), approach preferences. Include *why* so edge cases can be judged.
218
+ 3. **project** β€” Ongoing work context: goals, deadlines, initiatives, decisions, bugs. NOT derivable from code/git. Convert relative dates to absolute.
219
+ 4. **reference** β€” Pointers to external resources: URLs, tool names, where to find information outside the codebase.
220
+
221
+ ## What NOT to save
222
+
223
+ - Code patterns, architecture, file structure β€” derivable from the codebase
224
+ - Git history, recent changes β€” use `git log`/`git blame`
225
+ - Debugging solutions β€” the fix is in the code
226
+ - Anything already in AGENTS.md / project config files
227
+ - Ephemeral task details or current conversation context
228
+ - Information that was already saved in a previous extraction
229
+
230
+ ## How to save
231
+
232
+ For each memory worth saving, call `memory_save` with:
233
+ - `file_name`: descriptive slug (e.g., `user_role`, `feedback_testing_approach`)
234
+ - `name`: short title
235
+ - `description`: one-line description (used for relevance matching in future sessions)
236
+ - `type`: one of user, feedback, project, reference
237
+ - `content`: the memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines.
238
+
239
+ ## Instructions
240
+
241
+ 1. Analyze the conversation for memorable information
242
+ 2. Check existing memories first (use `memory_list`) to avoid duplicates β€” update existing ones if needed
243
+ 3. Save each distinct memory as a separate entry
244
+ 4. If the conversation was trivial (e.g., just "hello" or a quick lookup), save nothing β€” that'\''s fine
245
+ 5. Be selective: 0-3 memories per session is typical. Quality over quantity.
246
+ 6. Do NOT save a memory about the extraction process itself.'
247
+
248
+ # Periodic memory consolidation inspired by Claude Code auto-dream.
249
+ AUTODREAM_PROMPT="$(cat <<'EOF'
250
+ You are performing an auto-dream memory consolidation pass.
251
+
252
+ Goal: tighten and de-duplicate memory files so future sessions can orient faster.
253
+
254
+ ## Available tools
255
+ - memory_list
256
+ - memory_search
257
+ - memory_read
258
+ - memory_save
259
+ - memory_delete
260
+
261
+ ## Phase 1 β€” Orient
262
+ 1. Use memory_list to inspect current memory inventory.
263
+ 2. Identify overlapping or stale entries that can be merged/updated/deleted.
264
+
265
+ ## Phase 2 β€” Consolidate
266
+ 1. Merge duplicates into a single stronger memory using memory_save.
267
+ 2. Rewrite vague descriptions so retrieval is easier and more precise.
268
+ 3. For feedback/project entries, ensure content is structured as:
269
+ - main rule/fact
270
+ - **Why:**
271
+ - **How to apply:**
272
+
273
+ ## Phase 3 β€” Prune
274
+ 1. Delete memories that are clearly obsolete, contradictory, or low-value.
275
+ 2. Keep total memory set concise and high signal.
276
+
277
+ ## Guardrails
278
+ - Do NOT invent facts.
279
+ - If confidence is low, keep existing memory instead of guessing.
280
+ - If memory quality is already strong, make no changes and explicitly say so.
281
+
282
+ Return a short summary of what you updated, merged, or removed.
283
+ EOF
284
+ )"
285
+
286
+ # ============================================================================
287
+ # Helper Functions
288
+ # ============================================================================
289
+
290
+ log() {
291
+ echo "[opencode-memory] $*" >&2
292
+ }
293
+
294
+ is_positive_int() {
295
+ [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -gt 0 ]
296
+ }
297
+
298
+ if ! is_positive_int "$AUTODREAM_MIN_HOURS"; then
299
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_HOURS=$AUTODREAM_MIN_HOURS, using default 24"
300
+ AUTODREAM_MIN_HOURS=24
301
+ fi
302
+ if ! is_positive_int "$AUTODREAM_MIN_SESSIONS"; then
303
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_MIN_SESSIONS=$AUTODREAM_MIN_SESSIONS, using default 5"
304
+ AUTODREAM_MIN_SESSIONS=5
305
+ fi
306
+ if ! is_positive_int "$AUTODREAM_SCAN_LIMIT"; then
307
+ log "Invalid OPENCODE_MEMORY_AUTODREAM_SCAN_LIMIT=$AUTODREAM_SCAN_LIMIT, using default 200"
308
+ AUTODREAM_SCAN_LIMIT=200
309
+ fi
310
+
311
+ has_new_memories() {
312
+ # Check if any memory file was modified during the session.
313
+ local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
314
+ if [ ! -d "$mem_base" ]; then
315
+ return 1
316
+ fi
317
+
318
+ local newer_files
319
+ newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1)
320
+ [ -n "$newer_files" ]
321
+ }
322
+
323
+ cleanup_timestamp() {
324
+ rm -f "$TIMESTAMP_FILE"
325
+ }
326
+
327
+ get_session_list_json() {
328
+ local limit="$1"
329
+ local output
330
+
331
+ if output=$("$REAL_OPENCODE" session list --format json -n "$limit" 2>/dev/null); then
332
+ echo "$output"
333
+ return 0
334
+ fi
335
+
336
+ if output=$("$REAL_OPENCODE" session list --format json 2>/dev/null); then
337
+ echo "$output"
338
+ return 0
339
+ fi
340
+
341
+ return 1
342
+ }
343
+
344
+ get_latest_session_id() {
345
+ local session_json
346
+ session_json=$(get_session_list_json 1) || return 1
347
+
348
+ # Parse with jq if available, fallback to grep.
349
+ if command -v jq &>/dev/null; then
350
+ echo "$session_json" | jq -r '.[0].id // empty'
351
+ else
352
+ echo "$session_json" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'
353
+ fi
354
+ }
355
+
356
+ file_mtime_secs() {
357
+ local file="$1"
358
+ if [ ! -f "$file" ]; then
359
+ echo 0
360
+ return 0
361
+ fi
362
+
363
+ if stat -c %Y "$file" >/dev/null 2>&1; then
364
+ stat -c %Y "$file"
365
+ return 0
366
+ fi
367
+
368
+ stat -f %m "$file"
369
+ }
370
+
371
+ # Claude-style lock/mtime semantics:
372
+ # - lock file CONTENT (PID) = current holder
373
+ # - lock file MTIME = last successful consolidation timestamp
374
+ read_last_consolidated_at_secs() {
375
+ file_mtime_secs "$CONSOLIDATION_LOCK_FILE"
376
+ }
377
+
378
+ set_file_mtime_secs() {
379
+ local file="$1"
380
+ local secs="$2"
381
+
382
+ if command -v python3 >/dev/null 2>&1; then
383
+ python3 - "$file" "$secs" <<'PY'
384
+ import os
385
+ import sys
386
+
387
+ path = sys.argv[1]
388
+ secs = int(sys.argv[2])
389
+ os.utime(path, (secs, secs))
390
+ PY
391
+ return 0
392
+ fi
393
+
394
+ # Best effort fallback; may not be portable across all environments.
395
+ touch -d "@$secs" "$file" >/dev/null 2>&1 || true
396
+ }
397
+
398
+ acquire_simple_lock() {
399
+ local lock_file="$1"
400
+ local lock_name="$2"
401
+
402
+ if [ -f "$lock_file" ]; then
403
+ local lock_pid
404
+ lock_pid=$(cat "$lock_file" 2>/dev/null || true)
405
+ if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
406
+ log "Another $lock_name is already running (PID $lock_pid), skipping"
407
+ return 1
408
+ fi
409
+ rm -f "$lock_file"
410
+ fi
411
+
412
+ echo $$ > "$lock_file"
413
+ return 0
414
+ }
415
+
416
+ release_simple_lock() {
417
+ local lock_file="$1"
418
+ rm -f "$lock_file"
419
+ }
420
+
421
+ count_sessions_touched_since_ms() {
422
+ local since_ms="$1"
423
+ local exclude_session_id="$2"
424
+
425
+ if ! command -v jq >/dev/null 2>&1; then
426
+ echo ""
427
+ return 1
428
+ fi
429
+
430
+ local session_json
431
+ session_json=$(get_session_list_json "$AUTODREAM_SCAN_LIMIT") || {
432
+ echo ""
433
+ return 1
434
+ }
435
+
436
+ echo "$session_json" | jq -r --argjson since "$since_ms" --arg exclude "$exclude_session_id" '
437
+ [ .[]
438
+ | select((.id // "") != $exclude)
439
+ | select(((.time.updated // .time.created // 0) | tonumber) > $since)
440
+ ] | length
441
+ '
442
+ }
443
+
444
+ CONSOLIDATION_PRIOR_MTIME=0
445
+
446
+ try_acquire_consolidation_lock() {
447
+ local path="$CONSOLIDATION_LOCK_FILE"
448
+ local now mtime holder age
449
+
450
+ # Returns prior mtime via CONSOLIDATION_PRIOR_MTIME so callers can rollback
451
+ # on failure (preserving last successful consolidation time semantics).
452
+
453
+ now=$(date +%s)
454
+ mtime=0
455
+ holder=""
456
+
457
+ if [ -f "$path" ]; then
458
+ mtime=$(file_mtime_secs "$path")
459
+ holder=$(cat "$path" 2>/dev/null || true)
460
+ age=$((now - mtime))
461
+
462
+ if [ "$age" -lt "$AUTODREAM_STALE_LOCK_SECS" ] && [ -n "$holder" ] && kill -0 "$holder" 2>/dev/null; then
463
+ log "Auto-dream lock held by live PID $holder (${age}s old), skipping"
464
+ return 1
465
+ fi
466
+ fi
467
+
468
+ mkdir -p "$(dirname "$path")"
469
+ echo $$ > "$path"
470
+
471
+ local verify
472
+ verify=$(cat "$path" 2>/dev/null || true)
473
+ if [ "$verify" != "$$" ]; then
474
+ return 1
475
+ fi
476
+
477
+ CONSOLIDATION_PRIOR_MTIME="$mtime"
478
+ return 0
479
+ }
480
+
481
+ rollback_consolidation_lock() {
482
+ local prior_mtime="$1"
483
+ local path="$CONSOLIDATION_LOCK_FILE"
484
+
485
+ if [ "$prior_mtime" -eq 0 ]; then
486
+ rm -f "$path"
487
+ return 0
488
+ fi
489
+
490
+ : > "$path"
491
+ set_file_mtime_secs "$path" "$prior_mtime"
492
+ }
493
+
494
+ run_extraction_if_needed() {
495
+ local session_id="$1"
496
+ local memory_written_during_session="$2"
497
+
498
+ if [ "$EXTRACT_ENABLED" = "0" ]; then
499
+ return 0
500
+ fi
501
+
502
+ if [ "$memory_written_during_session" = "1" ]; then
503
+ log "Main agent already wrote memories during session, skipping extraction"
504
+ return 0
505
+ fi
506
+
507
+ if ! acquire_simple_lock "$EXTRACT_LOCK_FILE" "memory extraction"; then
508
+ return 0
509
+ fi
510
+
511
+ log "Extracting memories from session $session_id..."
512
+ log "Extraction log: $EXTRACT_LOG_FILE"
513
+
514
+ local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
515
+ if [ -n "$EXTRACT_MODEL" ]; then
516
+ cmd+=(-m "$EXTRACT_MODEL")
517
+ fi
518
+ if [ -n "$EXTRACT_AGENT" ]; then
519
+ cmd+=(--agent "$EXTRACT_AGENT")
520
+ fi
521
+ cmd+=("$EXTRACT_PROMPT")
522
+
523
+ if "${cmd[@]}" >> "$EXTRACT_LOG_FILE" 2>&1; then
524
+ log "Memory extraction completed successfully"
525
+ else
526
+ local code=$?
527
+ log "Memory extraction failed (exit code $code). Check $EXTRACT_LOG_FILE for details"
528
+ fi
529
+
530
+ release_simple_lock "$EXTRACT_LOCK_FILE"
531
+ }
532
+
533
+ run_autodream_if_needed() {
534
+ local session_id="$1"
535
+
536
+ if [ "$AUTODREAM_ENABLED" = "0" ]; then
537
+ return 0
538
+ fi
539
+
540
+ if ! command -v jq >/dev/null 2>&1; then
541
+ log "jq not found, skipping auto-dream (requires JSON session parsing)"
542
+ return 0
543
+ fi
544
+
545
+ local last_at now hours_since since_ms touched_count
546
+ last_at=$(read_last_consolidated_at_secs)
547
+ now=$(date +%s)
548
+
549
+ hours_since=$(((now - last_at) / 3600))
550
+ if [ "$hours_since" -lt "$AUTODREAM_MIN_HOURS" ]; then
551
+ return 0
552
+ fi
553
+
554
+ since_ms=$((last_at * 1000))
555
+ touched_count=$(count_sessions_touched_since_ms "$since_ms" "$session_id" || true)
556
+ if [ -z "$touched_count" ]; then
557
+ log "Unable to inspect touched sessions, skipping auto-dream"
558
+ return 0
559
+ fi
560
+
561
+ if [ "$touched_count" -lt "$AUTODREAM_MIN_SESSIONS" ]; then
562
+ return 0
563
+ fi
564
+
565
+ if ! try_acquire_consolidation_lock; then
566
+ return 0
567
+ fi
568
+
569
+ log "Auto-dream firing (${hours_since}h since last consolidation, ${touched_count} sessions touched)"
570
+ log "Auto-dream log: $AUTODREAM_LOG_FILE"
571
+
572
+ local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
573
+ if [ -n "$AUTODREAM_MODEL" ]; then
574
+ cmd+=(-m "$AUTODREAM_MODEL")
575
+ fi
576
+ if [ -n "$AUTODREAM_AGENT" ]; then
577
+ cmd+=(--agent "$AUTODREAM_AGENT")
578
+ fi
579
+ cmd+=("$AUTODREAM_PROMPT")
580
+
581
+ if "${cmd[@]}" >> "$AUTODREAM_LOG_FILE" 2>&1; then
582
+ log "Auto-dream consolidation completed successfully"
583
+ # Keep lock mtime at "now" to represent last consolidated timestamp.
584
+ else
585
+ local code=$?
586
+ log "Auto-dream consolidation failed (exit code $code). Rolling back gate timestamp"
587
+ rollback_consolidation_lock "$CONSOLIDATION_PRIOR_MTIME"
588
+ return 0
589
+ fi
590
+ }
591
+
592
+ run_post_session_tasks() {
593
+ local session_id="$1"
594
+ local memory_written_during_session="$2"
595
+
596
+ run_extraction_if_needed "$session_id" "$memory_written_during_session"
597
+ run_autodream_if_needed "$session_id"
598
+ }
599
+
600
+ # ============================================================================
601
+ # Main
602
+ # ============================================================================
603
+
604
+ # Step 0: Create timestamp marker before running opencode
605
+ TIMESTAMP_FILE=$(mktemp)
606
+
607
+ # Step 1: Run the real opencode with all original arguments, capture exit code
608
+ opencode_exit=0
609
+ "$REAL_OPENCODE" "$@" || opencode_exit=$?
610
+
611
+ # Step 2: If no maintenance task is enabled, exit early
612
+ if [ "$EXTRACT_ENABLED" = "0" ] && [ "$AUTODREAM_ENABLED" = "0" ]; then
613
+ cleanup_timestamp
614
+ exit $opencode_exit
615
+ fi
616
+
617
+ # Step 3: Get the most recent session ID
618
+ session_id=$(get_latest_session_id || true)
619
+ if [ -z "$session_id" ]; then
620
+ log "No session found, skipping post-session memory maintenance"
621
+ cleanup_timestamp
622
+ exit $opencode_exit
623
+ fi
624
+
625
+ # Step 4: Check whether main session already wrote memory files
626
+ memory_written_during_session=0
627
+ if has_new_memories; then
628
+ memory_written_during_session=1
629
+ fi
630
+
631
+ # Timestamp file is no longer needed after the check above.
632
+ cleanup_timestamp
633
+
634
+ # Step 5: Run tasks (foreground for debug, background by default)
635
+ if [ "$FOREGROUND" = "1" ]; then
636
+ run_post_session_tasks "$session_id" "$memory_written_during_session"
637
+ else
638
+ run_post_session_tasks "$session_id" "$memory_written_during_session" &
639
+ disown
640
+ log "Post-session memory maintenance started in background (PID $!)"
641
+ fi
642
+
643
+ exit $opencode_exit
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "opencode-claude-memory",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
- "description": "Cross-session memory plugin for OpenCode β€” Claude Code compatible, persistent, file-based memory",
5
+ "description": "Claude Code-compatible memory compatibility layer for OpenCode β€” zero config, local-first, no migration",
6
6
  "main": "src/index.ts",
7
7
  "bin": {
8
- "opencode": "./bin/opencode"
8
+ "opencode-memory": "./bin/opencode-memory"
9
9
  },
10
10
  "exports": {
11
11
  ".": "./src/index.ts"
package/bin/opencode DELETED
@@ -1,261 +0,0 @@
1
- #!/usr/bin/env bash
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.
5
- #
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.
8
- #
9
- # Usage:
10
- # opencode [any opencode args...]
11
- #
12
- # 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
18
- #
19
- # Requirements:
20
- # - Real `opencode` CLI reachable in PATH (after this wrapper)
21
- # - `jq` for JSON parsing
22
- # - The opencode-memory plugin installed (provides memory_save tool)
23
- #
24
- # Environment variables:
25
- # OPENCODE_MEMORY_EXTRACT=0 β€” Disable auto-extraction
26
- # OPENCODE_MEMORY_FOREGROUND=1 β€” Run extraction in foreground (for debugging)
27
- # OPENCODE_MEMORY_MODEL=... β€” Override model for extraction (e.g., "anthropic/claude-sonnet-4-20250514")
28
- # OPENCODE_MEMORY_AGENT=... β€” Override agent for extraction
29
- # OPENCODE_MEMORY_DIR=... β€” Override working directory for opencode
30
- #
31
-
32
- set -euo pipefail
33
-
34
- # ============================================================================
35
- # Resolve the real opencode binary (skip this wrapper)
36
- # ============================================================================
37
-
38
- SELF="$(cd "$(dirname "$0")" && pwd -P)/$(basename "$0")"
39
-
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
47
- fi
48
- 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
52
- }
53
-
54
- REAL_OPENCODE="$(find_real_opencode)"
55
-
56
- # ============================================================================
57
- # Configuration
58
- # ============================================================================
59
-
60
- EXTRACT_ENABLED="${OPENCODE_MEMORY_EXTRACT:-1}"
61
- FOREGROUND="${OPENCODE_MEMORY_FOREGROUND:-0}"
62
- EXTRACT_MODEL="${OPENCODE_MEMORY_MODEL:-}"
63
- EXTRACT_AGENT="${OPENCODE_MEMORY_AGENT:-}"
64
- WORKING_DIR="${OPENCODE_MEMORY_DIR:-$(pwd)}"
65
-
66
- # Lock file to prevent concurrent extractions on the same project
67
- LOCK_DIR="${TMPDIR:-/tmp}/opencode-memory-locks"
68
- mkdir -p "$LOCK_DIR"
69
- LOCK_FILE="$LOCK_DIR/$(echo "$WORKING_DIR" | sed 's/[^a-zA-Z0-9]/-/g').lock"
70
-
71
- # Log file for background extraction
72
- LOG_DIR="${TMPDIR:-/tmp}/opencode-memory-logs"
73
- mkdir -p "$LOG_DIR"
74
- LOG_FILE="$LOG_DIR/extract-$(date +%Y%m%d-%H%M%S).log"
75
-
76
- # ============================================================================
77
- # Extraction Prompt
78
- # ============================================================================
79
-
80
- # Adapted from Claude Code's extraction prompt, simplified for OpenCode's
81
- # headless run mode. The model sees the full conversation context via --fork.
82
- EXTRACT_PROMPT='You are now acting as the memory extraction subagent. Review the entire conversation above and extract any information worth remembering for future sessions.
83
-
84
- ## What to save
85
-
86
- Use the `memory_save` tool to persist memories. There are four types:
87
-
88
- 1. **user** β€” Who the user is: role, expertise, preferences, communication style. Helps tailor future interactions.
89
- 2. **feedback** β€” Guidance on how to work: corrections ("don'\''t do X"), confirmations ("yes, keep doing that"), approach preferences. Include *why* so edge cases can be judged.
90
- 3. **project** β€” Ongoing work context: goals, deadlines, initiatives, decisions, bugs. NOT derivable from code/git. Convert relative dates to absolute.
91
- 4. **reference** β€” Pointers to external resources: URLs, tool names, where to find information outside the codebase.
92
-
93
- ## What NOT to save
94
-
95
- - Code patterns, architecture, file structure β€” derivable from the codebase
96
- - Git history, recent changes β€” use `git log`/`git blame`
97
- - Debugging solutions β€” the fix is in the code
98
- - Anything already in AGENTS.md / project config files
99
- - Ephemeral task details or current conversation context
100
- - Information that was already saved in a previous extraction
101
-
102
- ## How to save
103
-
104
- For each memory worth saving, call `memory_save` with:
105
- - `file_name`: descriptive slug (e.g., `user_role`, `feedback_testing_approach`)
106
- - `name`: short title
107
- - `description`: one-line description (used for relevance matching in future sessions)
108
- - `type`: one of user, feedback, project, reference
109
- - `content`: the memory content. For feedback/project types, structure as: rule/fact, then **Why:** and **How to apply:** lines.
110
-
111
- ## Instructions
112
-
113
- 1. Analyze the conversation for memorable information
114
- 2. Check existing memories first (use `memory_list`) to avoid duplicates β€” update existing ones if needed
115
- 3. Save each distinct memory as a separate entry
116
- 4. If the conversation was trivial (e.g., just "hello" or a quick lookup), save nothing β€” that'\''s fine
117
- 5. Be selective: 0-3 memories per session is typical. Quality over quantity.
118
- 6. Do NOT save a memory about the extraction process itself.'
119
-
120
- # ============================================================================
121
- # Helper Functions
122
- # ============================================================================
123
-
124
- log() {
125
- echo "[opencode-memory] $*" >&2
126
- }
127
-
128
- has_new_memories() {
129
- # Check if any memory file was modified during the session
130
- # Checks all projects' memory directories for files newer than the timestamp marker
131
- local mem_base="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/projects"
132
-
133
- if [ ! -d "$mem_base" ]; then
134
- return 1
135
- fi
136
-
137
- # Find any .md file under projects/*/memory/ newer than our timestamp
138
- local newer_files
139
- newer_files=$(find "$mem_base" -path "*/memory/*.md" -newer "$TIMESTAMP_FILE" 2>/dev/null | head -1)
140
-
141
- [ -n "$newer_files" ]
142
- }
143
-
144
- get_latest_session_id() {
145
- local session_json
146
- session_json=$("$REAL_OPENCODE" session list --format json -n 1 2>/dev/null) || return 1
147
-
148
- # Parse with jq if available, fallback to grep
149
- if command -v jq &>/dev/null; then
150
- echo "$session_json" | jq -r '.[0].id // empty'
151
- else
152
- # Rough fallback: extract first "id" value
153
- echo "$session_json" | grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/'
154
- fi
155
- }
156
-
157
- acquire_lock() {
158
- if [ -f "$LOCK_FILE" ]; then
159
- local lock_pid
160
- lock_pid=$(cat "$LOCK_FILE" 2>/dev/null || true)
161
- if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
162
- log "Another extraction is already running (PID $lock_pid), skipping"
163
- return 1
164
- fi
165
- # Stale lock β€” remove it
166
- rm -f "$LOCK_FILE"
167
- fi
168
- echo $$ > "$LOCK_FILE"
169
- return 0
170
- }
171
-
172
- release_lock() {
173
- rm -f "$LOCK_FILE"
174
- }
175
-
176
- cleanup_timestamp() {
177
- rm -f "$TIMESTAMP_FILE"
178
- }
179
-
180
- run_extraction() {
181
- local session_id="$1"
182
-
183
- log "Extracting memories from session $session_id..."
184
- log "Log file: $LOG_FILE"
185
-
186
- # Build the opencode run command
187
- local cmd=("$REAL_OPENCODE" run -s "$session_id" --fork)
188
-
189
- if [ -n "$EXTRACT_MODEL" ]; then
190
- cmd+=(-m "$EXTRACT_MODEL")
191
- fi
192
-
193
- if [ -n "$EXTRACT_AGENT" ]; then
194
- cmd+=(--agent "$EXTRACT_AGENT")
195
- fi
196
-
197
- cmd+=("$EXTRACT_PROMPT")
198
-
199
- # Execute
200
- if "${cmd[@]}" >> "$LOG_FILE" 2>&1; then
201
- log "Memory extraction completed successfully"
202
- else
203
- log "Memory extraction failed (exit code $?). Check $LOG_FILE for details"
204
- fi
205
-
206
- release_lock
207
- }
208
-
209
- # ============================================================================
210
- # Main
211
- # ============================================================================
212
-
213
- # Step 0: Create timestamp marker before running opencode
214
- TIMESTAMP_FILE=$(mktemp)
215
-
216
- # Step 1: Run the real opencode with all original arguments, capture exit code
217
- opencode_exit=0
218
- "$REAL_OPENCODE" "$@" || opencode_exit=$?
219
-
220
- # Step 2: Check if extraction is enabled
221
- if [ "$EXTRACT_ENABLED" = "0" ]; then
222
- cleanup_timestamp
223
- exit $opencode_exit
224
- fi
225
-
226
- # Step 3: Get the most recent session ID
227
- session_id=$(get_latest_session_id)
228
-
229
- if [ -z "$session_id" ]; then
230
- log "No session found, skipping memory extraction"
231
- cleanup_timestamp
232
- exit $opencode_exit
233
- fi
234
-
235
- # Step 3.5: Check if memories were already written during the session
236
- if has_new_memories; then
237
- log "Main agent already wrote memories during session, skipping extraction"
238
- cleanup_timestamp
239
- exit $opencode_exit
240
- fi
241
-
242
- # Step 4: Acquire lock (prevent concurrent extractions)
243
- if ! acquire_lock; then
244
- cleanup_timestamp
245
- exit $opencode_exit
246
- fi
247
-
248
- # Step 5: Run extraction
249
- if [ "$FOREGROUND" = "1" ]; then
250
- # Foreground mode (for debugging)
251
- run_extraction "$session_id"
252
- cleanup_timestamp
253
- else
254
- # Background mode (default) β€” user isn't blocked
255
- run_extraction "$session_id" &
256
- disown
257
- log "Memory extraction started in background (PID $!)"
258
- cleanup_timestamp
259
- fi
260
-
261
- exit $opencode_exit