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 +119 -148
- package/bin/opencode-memory +643 -0
- package/package.json +3 -3
- package/bin/opencode +0 -261
package/README.md
CHANGED
|
@@ -1,69 +1,31 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# π§
|
|
3
|
+
# π§ Claude Code-compatible memory for OpenCode
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Make OpenCode and Claude Code share the same memory β zero config, local-first, and no migration required.**
|
|
6
6
|
|
|
7
|
-
|
|
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
|
[](https://www.npmjs.com/package/opencode-claude-memory)
|
|
12
10
|
[](https://www.npmjs.com/package/opencode-claude-memory)
|
|
13
11
|
[](https://github.com/kuitos/opencode-claude-memory/blob/main/LICENSE)
|
|
14
12
|
|
|
15
|
-
[
|
|
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
|
-
## β¨
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
-
|
|
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
|
|
56
|
+
opencode
|
|
93
57
|
```
|
|
94
58
|
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
78
|
+
Most memory plugins introduce a new storage model or migration step.
|
|
113
79
|
|
|
114
|
-
|
|
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[
|
|
119
|
-
B --> C[
|
|
120
|
-
C --> D[
|
|
121
|
-
D --> E[
|
|
122
|
-
E --> F[
|
|
123
|
-
F --> G[
|
|
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
|
|
103
|
+
The shell hook defines an `opencode()` function that delegates to `opencode-memory`:
|
|
127
104
|
|
|
128
|
-
1.
|
|
129
|
-
2.
|
|
130
|
-
3.
|
|
131
|
-
4.
|
|
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
|
-
###
|
|
114
|
+
### Compatibility details
|
|
134
115
|
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
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
|
-
##
|
|
125
|
+
## β FAQ
|
|
151
126
|
|
|
152
|
-
###
|
|
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
|
-
|
|
129
|
+
No. It is a compatibility layer that lets OpenCode use Claude Code-compatible memory layout and conventions.
|
|
164
130
|
|
|
165
|
-
###
|
|
131
|
+
### Do I need to migrate existing memory?
|
|
166
132
|
|
|
167
|
-
|
|
133
|
+
No migration required. If you already have Claude Code memory files, OpenCode can work with them directly.
|
|
168
134
|
|
|
169
|
-
|
|
135
|
+
### Where is data stored?
|
|
170
136
|
|
|
171
|
-
|
|
137
|
+
In local files under Claude-style project memory directories (for example, under `~/.claude/projects/<project>/memory/`).
|
|
172
138
|
|
|
173
|
-
|
|
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
|
-
|
|
141
|
+
File-based memory is transparent, local-first, easy to inspect/diff/back up, and works naturally with existing developer workflows.
|
|
181
142
|
|
|
182
|
-
|
|
183
|
-
**How to apply:** Don't summarize changes at the end of responses.
|
|
184
|
-
```
|
|
143
|
+
### Can I disable auto extraction?
|
|
185
144
|
|
|
186
|
-
|
|
145
|
+
Yes. Set `OPENCODE_MEMORY_EXTRACT=0`.
|
|
187
146
|
|
|
188
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
153
|
+
## π§ Configuration
|
|
199
154
|
|
|
200
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
### Logs
|
|
208
168
|
|
|
209
|
-
|
|
169
|
+
Logs are written to `$TMPDIR/opencode-memory-logs/`:
|
|
170
|
+
- `extract-*.log`: automatic memory extraction
|
|
171
|
+
- `dream-*.log`: auto-dream consolidation
|
|
210
172
|
|
|
211
|
-
|
|
173
|
+
### Concurrency safety
|
|
212
174
|
|
|
213
|
-
|
|
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
|
-
|
|
177
|
+
## π Memory format
|
|
222
178
|
|
|
223
|
-
|
|
179
|
+
Each memory is a Markdown file with YAML frontmatter:
|
|
224
180
|
|
|
225
|
-
|
|
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
|
-
|
|
188
|
+
Skip post-action summaries. User reads diffs directly.
|
|
228
189
|
|
|
229
|
-
|
|
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
|
-
|
|
194
|
+
Supported memory types:
|
|
195
|
+
- `user`
|
|
196
|
+
- `feedback`
|
|
197
|
+
- `project`
|
|
198
|
+
- `reference`
|
|
232
199
|
|
|
233
|
-
|
|
200
|
+
## π§ Tools reference
|
|
234
201
|
|
|
235
|
-
|
|
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.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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
|