@vasilvestre/opencode-dream-memory 0.0.0-stage → 0.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/.opencode/plugins/dream-memory.js +83 -0
- package/LICENSE +21 -0
- package/README.md +206 -2
- package/dream/agents/dream-deep.md +107 -0
- package/dream/agents/dream-light.md +54 -0
- package/dream/dream +149 -0
- package/dream/dream-corpus +71 -0
- package/dream/dream-status +41 -0
- package/dream/systemd/dream.service +11 -0
- package/dream/systemd/dream.timer +10 -0
- package/install.sh +119 -0
- package/package.json +38 -4
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// dream-memory — OpenCode plugin (v2).
|
|
2
|
+
//
|
|
3
|
+
// Injects shared memory plus the memory of the current project into the system
|
|
4
|
+
// prompt on every agent-loop LLM request, so it survives context compaction.
|
|
5
|
+
// AGENTS.md does not: it is summarized away when a session gets long.
|
|
6
|
+
//
|
|
7
|
+
// Files are read on each request, never cached, so a nightly rewrite shows up in
|
|
8
|
+
// the next turn. A missing or unreadable file contributes nothing: this hook
|
|
9
|
+
// runs on every request, and throwing here would break all of them.
|
|
10
|
+
//
|
|
11
|
+
// Layout under $DREAM_HOME (default ~/dreams):
|
|
12
|
+
// MEMORY.md facts true in every project
|
|
13
|
+
// projects/<name>/MEMORY.md facts about one project
|
|
14
|
+
//
|
|
15
|
+
// <name> is the base name of the project directory, the same tag dream-corpus
|
|
16
|
+
// writes into the corpus and dream-deep uses as a file name.
|
|
17
|
+
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
const HOME = process.env.DREAM_HOME || path.join(os.homedir(), 'dreams');
|
|
23
|
+
|
|
24
|
+
// Cut points, not writer budgets: dream-deep is told to stay below them (see
|
|
25
|
+
// dream/agents/dream-deep.md). Shared memory is injected into every session,
|
|
26
|
+
// so its share is the smaller one.
|
|
27
|
+
const ROOT_MAX = 1000;
|
|
28
|
+
const PROJECT_MAX = 3000;
|
|
29
|
+
|
|
30
|
+
const HEADER =
|
|
31
|
+
'Cross-session memory, written by a nightly pass from earlier sessions and possibly stale. ' +
|
|
32
|
+
'Trust the repository and the live system over this block.';
|
|
33
|
+
|
|
34
|
+
// Drop the title, cut on a line boundary, and say what was dropped. A hard byte
|
|
35
|
+
// cut would drop a fact without telling the model.
|
|
36
|
+
function read(file, max, label) {
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
text = fs.readFileSync(file, 'utf8');
|
|
40
|
+
} catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
const body = text
|
|
44
|
+
.split('\n')
|
|
45
|
+
.filter((line) => !line.startsWith('# '))
|
|
46
|
+
.join('\n')
|
|
47
|
+
.trim();
|
|
48
|
+
if (!body) return '';
|
|
49
|
+
if (body.length <= max) return body;
|
|
50
|
+
const cut = body.lastIndexOf('\n', max);
|
|
51
|
+
const kept = cut > 0 ? cut : max;
|
|
52
|
+
return (
|
|
53
|
+
`${body.slice(0, kept)}\n` +
|
|
54
|
+
`[${label} truncated: ${body.length - kept} characters dropped, ` +
|
|
55
|
+
`prune ${path.relative(HOME, file)} to fit]`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function memoryBlock(directory) {
|
|
60
|
+
const parts = [read(path.join(HOME, 'MEMORY.md'), ROOT_MAX, 'memory')];
|
|
61
|
+
const name = path.basename(directory || '');
|
|
62
|
+
if (name && name !== '.' && name !== '..' && !name.startsWith('.')) {
|
|
63
|
+
const body = read(path.join(HOME, 'projects', name, 'MEMORY.md'), PROJECT_MAX, `${name} memory`);
|
|
64
|
+
if (body) parts.push(`Project memory (${name}):\n${body}`);
|
|
65
|
+
}
|
|
66
|
+
const body = parts.filter(Boolean).join('\n\n');
|
|
67
|
+
return body ? `${HEADER}\n\n${body}` : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
id: 'vasilvestre.opencode-dream-memory',
|
|
72
|
+
async setup(ctx) {
|
|
73
|
+
// OpenCode creates one plugin instance per project location, so the project
|
|
74
|
+
// is known before a session exists. Worktrees share their canonical path.
|
|
75
|
+
const location = ctx.location ?? {};
|
|
76
|
+
const directory =
|
|
77
|
+
location.project?.canonical || location.project?.directory || location.directory;
|
|
78
|
+
await ctx.session.hook('context', (event) => {
|
|
79
|
+
const block = memoryBlock(directory);
|
|
80
|
+
if (block) event.system.push({ type: 'text', text: block });
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vasilvestre
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,207 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-dream-memory
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two pieces that give an OpenCode agent memory across sessions:
|
|
4
|
+
|
|
5
|
+
1. **A plugin** that injects memory files into the system prompt of every
|
|
6
|
+
request, so the memory survives context compaction.
|
|
7
|
+
2. **A nightly job** that writes those files from past sessions.
|
|
8
|
+
|
|
9
|
+
`AGENTS.md` does not survive compaction. When a session gets long, OpenCode
|
|
10
|
+
summarizes the conversation and the instructions file goes with it. A plugin
|
|
11
|
+
hook runs on every agent-loop request, so the same block is re-sent each turn.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Requirements: `opencode` and `jq` on `PATH`, `curl` or `wget` to download, and
|
|
16
|
+
systemd for the timer.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -fsSL https://raw.githubusercontent.com/vasilvestre/opencode-dream-memory/main/install.sh | sh
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
One command installs both pieces. The plugin lands in
|
|
23
|
+
`~/.config/opencode/plugins`, the job in `$BIN_DIR` (default `~/.local/bin`)
|
|
24
|
+
next to its two agents and two systemd user units. Then restart the service to
|
|
25
|
+
load the plugin:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
opencode service restart
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Do **not** also list the plugin file path in `plugins` in `opencode.jsonc`:
|
|
32
|
+
OpenCode warns that a configured plugin path must be a directory, and a path
|
|
33
|
+
entry placed next to a discovered copy loads the plugin twice. Two instances
|
|
34
|
+
with the same id make OpenCode drop every plugin.
|
|
35
|
+
|
|
36
|
+
The installer leaves the nightly timer disabled on purpose. `dream` spends two
|
|
37
|
+
agent runs per invocation, and a one-line install cannot ask a question, so a
|
|
38
|
+
job that spends money must not start by itself. Enable it once a manual run
|
|
39
|
+
looks right, with `-s --` passing the flag to the script:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
curl -fsSL https://raw.githubusercontent.com/vasilvestre/opencode-dream-memory/main/install.sh | sh -s -- --enable
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The installer also creates `~/dreams` with `MEMORY.md`, an empty `projects/`
|
|
46
|
+
tree, `DREAMS.md`, an empty project list and a commented `dream-models` file. It
|
|
47
|
+
rewrites the unit's `ExecStart` to the chosen `BIN_DIR`, and it never overwrites
|
|
48
|
+
`MEMORY.md`, `DREAMS.md`, `dream-projects`, `dream-models` or a project memory
|
|
49
|
+
file. An existing symlink at
|
|
50
|
+
`~/.config/opencode/plugins/dream-memory.js` stays as it is, so a clone that
|
|
51
|
+
symlinks its own copy keeps working.
|
|
52
|
+
|
|
53
|
+
The deep pass leaves an audit trail: `dreaming/deep/<date>.md` lists each
|
|
54
|
+
promoted fact with the light-report line that supports it, so a fact stays
|
|
55
|
+
checkable after the corpus is gone.
|
|
56
|
+
|
|
57
|
+
If `opencode` is not on the systemd default `PATH`, write its directory to
|
|
58
|
+
`~/.config/dream.env`, which the unit reads when present:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
echo 'PATH=/opt/opencode/bin:/usr/local/bin:/usr/bin:/bin' > ~/.config/dream.env
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### From a clone instead
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/vasilvestre/opencode-dream-memory
|
|
68
|
+
cd opencode-dream-memory
|
|
69
|
+
sh install.sh # install, timer stays disabled
|
|
70
|
+
sh install.sh --enable # install, then enable and start the 04:30 timer
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The clone itself is what to symlink when you want the plugin to track edits:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
ln -sf "$PWD/.opencode/plugins/dream-memory.js" ~/.config/opencode/plugins/dream-memory.js
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Memory layout
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
~/dreams/
|
|
83
|
+
MEMORY.md shared facts, injected in every session
|
|
84
|
+
projects/homelab/MEMORY.md one project's facts, injected there only
|
|
85
|
+
projects/<name>/MEMORY.md
|
|
86
|
+
DREAMS.md the diary
|
|
87
|
+
dreaming/{light,deep}/<date>.md staging and audit reports
|
|
88
|
+
.dreams/ corpus and cursor, gitignored
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Only `.dreams/` is gitignored. `MEMORY.md`, `projects/` and `dreaming/` stay
|
|
92
|
+
tracked, so a `$DREAM_HOME` git repo keeps a history of every promotion.
|
|
93
|
+
|
|
94
|
+
## What the plugin injects
|
|
95
|
+
|
|
96
|
+
Two files, read fresh on every request:
|
|
97
|
+
|
|
98
|
+
| File | Content | Cut at |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `~/dreams/MEMORY.md` | facts true in every project | 1 000 characters |
|
|
101
|
+
| `~/dreams/projects/<name>/MEMORY.md` | facts about the current project | 3 000 characters |
|
|
102
|
+
|
|
103
|
+
`<name>` is the base name of the project directory: `~/homelab` reads
|
|
104
|
+
`projects/homelab/MEMORY.md`. The corpus uses the same tag, so a candidate
|
|
105
|
+
lands in the file of the project it came from. OpenCode creates one plugin
|
|
106
|
+
instance per project location, so the hook knows which project it serves; a
|
|
107
|
+
worktree uses the name of its main repository.
|
|
108
|
+
|
|
109
|
+
The block opens with a header saying the content comes from a nightly pass and
|
|
110
|
+
may be stale. Top-level titles are dropped. A part that is missing, empty or
|
|
111
|
+
unreadable contributes nothing: the hook runs on every request, so it fails
|
|
112
|
+
silent instead of breaking the session. Move the whole tree with `DREAM_HOME`.
|
|
113
|
+
|
|
114
|
+
Nothing is injected when both parts are empty.
|
|
115
|
+
|
|
116
|
+
## Privacy
|
|
117
|
+
|
|
118
|
+
The corpus is built from your real sessions. Each message contributes up to 400
|
|
119
|
+
characters, and the whole corpus is sent to whichever model `dream` runs.
|
|
120
|
+
|
|
121
|
+
- Inspect `~/dreams/.dreams/session-corpus/<date>.txt` before enabling the
|
|
122
|
+
timer, and again after any session where you pasted something sensitive.
|
|
123
|
+
- The corpus skips the dream's own sessions, so the job cannot feed on its own
|
|
124
|
+
output. It also redacts labelled secrets (`API_KEY=`, `token:`, `password=`,
|
|
125
|
+
`secret=`, `bearer ...`). A bare key with no label still gets through, so this
|
|
126
|
+
is a net, not a guarantee.
|
|
127
|
+
- `opencode session export --sanitize` is not an option: it replaces every
|
|
128
|
+
message with `[redacted:text:...]`, which makes the corpus useless.
|
|
129
|
+
- For a private setup, set a local model in `~/.config/opencode/dream-models`
|
|
130
|
+
(see below), or set `DREAM_HOME` on an encrypted volume and skip the timer.
|
|
131
|
+
- The dream job is instructed never to write secrets into a memory file, and the
|
|
132
|
+
plugin injects those files into every request. A secret that reaches one is
|
|
133
|
+
re-sent on every turn.
|
|
134
|
+
- Before the deep pass, `dream` commits `$DREAM_HOME` when it is a git repo, so
|
|
135
|
+
a bad rewrite is one `git revert` away. The corpus is excluded from that
|
|
136
|
+
commit even when your `.gitignore` forgets it.
|
|
137
|
+
|
|
138
|
+
### Which model runs each stage
|
|
139
|
+
|
|
140
|
+
`dream` reads `~/.config/opencode/dream-models`, one line per stage. The first
|
|
141
|
+
word is the stage, the rest is a list of models:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
light opencode-go/deepseek-v4.1-flash
|
|
145
|
+
deep opencode-go/deepseek-v4-pro
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The script tries the models left to right and keeps the first that runs, so a
|
|
149
|
+
fallback after the first choice is fine. A stage with no line uses the script's
|
|
150
|
+
own defaults: `deepseek-v4.1-flash` for `light`, which reads the raw session
|
|
151
|
+
text, and `deepseek-v4-pro` for `deep`, which only sees staged candidates. Both
|
|
152
|
+
are paid models on purpose. A free model may train on its input, and a nightly
|
|
153
|
+
job that silently falls back to one sends your session text there.
|
|
154
|
+
|
|
155
|
+
Do not set the model in `dream/agents/*.md` for the job: `dream` passes `-m`,
|
|
156
|
+
and that flag beats the agent's pinned model. Override the file path with
|
|
157
|
+
`DREAM_MODELS`.
|
|
158
|
+
|
|
159
|
+
## Limits
|
|
160
|
+
|
|
161
|
+
- Each part is cut on its own: 1 000 characters for shared memory, 3 000 for a
|
|
162
|
+
project file. Past that, the tail is dropped on a line boundary and the block
|
|
163
|
+
says how much was dropped.
|
|
164
|
+
- `dream-deep` is told to keep the files under 900 and 2 800 characters, below
|
|
165
|
+
the cuts. It deletes older entries to stay there, because a memory file that
|
|
166
|
+
only grows eventually loses its own tail.
|
|
167
|
+
- One project's memory cannot squeeze out another's: the files share no budget.
|
|
168
|
+
- A run takes a lock on `$DREAM_HOME/.dreams/run.lock`. A second run exits at
|
|
169
|
+
once instead of interleaving writes, so the 04:30 timer cannot collide with a
|
|
170
|
+
manual run.
|
|
171
|
+
- The files are read on each request, never cached, so a nightly rewrite shows
|
|
172
|
+
up in the next turn at the cost of two small reads.
|
|
173
|
+
|
|
174
|
+
## Commands
|
|
175
|
+
|
|
176
|
+
| Command | What it does |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `dream` | builds the corpus, then the light and deep passes |
|
|
179
|
+
| `dream corpus` | only the corpus, no model, no cost |
|
|
180
|
+
| `dream status` | one screen of state, read-only |
|
|
181
|
+
| `dream reset` | deletes memory, diary, staging and corpus under `DREAM_HOME` |
|
|
182
|
+
|
|
183
|
+
`dream reset` asks for confirmation; add `--yes` to skip it. It never touches
|
|
184
|
+
your projects, and it refuses a `DREAM_HOME` that does not look like a dream
|
|
185
|
+
home. When `$DREAM_HOME` is a git repo, the preimage commits keep the old
|
|
186
|
+
content one `git checkout` away.
|
|
187
|
+
|
|
188
|
+
`dream-corpus` and `dream-status` stay installed as standalone commands; they do
|
|
189
|
+
the same thing as the two subcommands.
|
|
190
|
+
|
|
191
|
+
## Test
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm test
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The plugin checks: the hook registers, missing files inject nothing, shared
|
|
198
|
+
memory alone injects one block, the current project's file is appended after it
|
|
199
|
+
while another project's stays out, an oversized file is truncated without the
|
|
200
|
+
whole block growing, and titles are dropped. Then the corpus checks: the dream's
|
|
201
|
+
own sessions stay out, a labelled secret is redacted, a torn session export is
|
|
202
|
+
skipped instead of fatal, and a git data directory gets a preimage commit before
|
|
203
|
+
the deep pass.
|
|
204
|
+
|
|
205
|
+
## Licence
|
|
206
|
+
|
|
207
|
+
MIT.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Promotes staged memory candidates and writes the dream diary
|
|
3
|
+
mode: primary
|
|
4
|
+
model: opencode-go/deepseek-v4-pro
|
|
5
|
+
permissions:
|
|
6
|
+
- action: "*"
|
|
7
|
+
resource: "*"
|
|
8
|
+
effect: deny
|
|
9
|
+
- action: read
|
|
10
|
+
resource: "*"
|
|
11
|
+
effect: allow
|
|
12
|
+
- action: glob
|
|
13
|
+
resource: "*"
|
|
14
|
+
effect: allow
|
|
15
|
+
- action: grep
|
|
16
|
+
resource: "*"
|
|
17
|
+
effect: allow
|
|
18
|
+
- action: edit
|
|
19
|
+
resource: "MEMORY.md"
|
|
20
|
+
effect: allow
|
|
21
|
+
- action: edit
|
|
22
|
+
resource: "projects/*/MEMORY.md"
|
|
23
|
+
effect: allow
|
|
24
|
+
- action: edit
|
|
25
|
+
resource: "DREAMS.md"
|
|
26
|
+
effect: allow
|
|
27
|
+
- action: edit
|
|
28
|
+
resource: "dreaming/deep/*"
|
|
29
|
+
effect: allow
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
You are the deep-sleep pass of a session-memory system. You turn staged
|
|
33
|
+
candidates into durable memory, then write the diary.
|
|
34
|
+
|
|
35
|
+
Input: `dreaming/light/<today>.md`.
|
|
36
|
+
|
|
37
|
+
## 1. Promote
|
|
38
|
+
|
|
39
|
+
Promote a candidate only when it is durable: a preference or rule the user
|
|
40
|
+
stated, a correction they made, a verified fact about their machines, or the
|
|
41
|
+
cause of a failure likely to repeat.
|
|
42
|
+
|
|
43
|
+
Two destinations:
|
|
44
|
+
|
|
45
|
+
- `MEMORY.md` holds facts that are true in every project: how the user wants
|
|
46
|
+
work done, their general preferences. Keep it under **900 characters**.
|
|
47
|
+
- `projects/<project>/MEMORY.md` holds facts about one project. The
|
|
48
|
+
`project:` line of the candidate gives the name: `project: homelab` means
|
|
49
|
+
`projects/homelab/MEMORY.md`. Keep each file under **2 800 characters**.
|
|
50
|
+
|
|
51
|
+
The plugin injects shared memory into every session, and the current project's
|
|
52
|
+
file only. It cuts shared memory at 1 000 characters and a project file at
|
|
53
|
+
3 000, so a longer file loses its tail silently at 04:30. When an addition
|
|
54
|
+
would exceed a budget, delete or merge older lines in that same file until it
|
|
55
|
+
fits. Deleting a stale fact is part of this pass, not an exception to it.
|
|
56
|
+
|
|
57
|
+
- Add or update the entry in place. Never duplicate one.
|
|
58
|
+
- Remove entries this candidate makes false. A wrong memory is worse than a
|
|
59
|
+
missing one.
|
|
60
|
+
- Keep each file curated: short lines, one fact each, no chronology.
|
|
61
|
+
- Write a fact about a machine, a repository or a setup into that project's
|
|
62
|
+
file, not into shared memory.
|
|
63
|
+
- If shared memory holds a fact about one project, move it into that project's
|
|
64
|
+
file and delete it from shared memory.
|
|
65
|
+
- Never write a secret, a token, or a password.
|
|
66
|
+
- If a project file does not exist, create it with a `# <project>` title. Never
|
|
67
|
+
create one for a project that has no candidate today.
|
|
68
|
+
|
|
69
|
+
## 2. Report
|
|
70
|
+
|
|
71
|
+
Write `dreaming/deep/<today>.md`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
# Deep Sleep
|
|
75
|
+
|
|
76
|
+
- Ranked <n> candidate(s) for durable promotion.
|
|
77
|
+
- Promoted <n> candidate(s) into memory.
|
|
78
|
+
- Removed <n> stale entr(y|ies).
|
|
79
|
+
- Pruned <n> candidate(s) as one-off.
|
|
80
|
+
- Changed: MEMORY.md, projects/<name>/MEMORY.md.
|
|
81
|
+
|
|
82
|
+
## Sources
|
|
83
|
+
|
|
84
|
+
- <short fact promoted> (dreaming/light/<today>.md:<line>)
|
|
85
|
+
- <another fact> (dreaming/light/<today>.md:<line>)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
List one line per promoted fact, with the line of the light report that
|
|
89
|
+
supports it. The corpus is deleted eventually; this report is what lets a
|
|
90
|
+
reader check a fact afterwards. This is an audit trail, not memory: never add
|
|
91
|
+
these references to a memory file.
|
|
92
|
+
|
|
93
|
+
## 3. Dream
|
|
94
|
+
|
|
95
|
+
Append one entry to `DREAMS.md`, above the existing entries, after the
|
|
96
|
+
`<!-- opencode:dreaming:diary:start -->` marker.
|
|
97
|
+
|
|
98
|
+
If an entry dated today already exists, rewrite that entry instead of adding a
|
|
99
|
+
second one for the same day.
|
|
100
|
+
|
|
101
|
+
The entry is dated (`*October 21, 2026*`), separated by `---`, and written as
|
|
102
|
+
first-person reflection on the day: what was tried, what resisted, what
|
|
103
|
+
changed. It is a diary, not a report: images are welcome, code is not. Between
|
|
104
|
+
120 and 200 words. No secrets, no file dumps, no task list.
|
|
105
|
+
|
|
106
|
+
If nothing happened today, still write two sentences saying so, in the same
|
|
107
|
+
voice.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Extracts staged memory candidates from a session corpus
|
|
3
|
+
mode: primary
|
|
4
|
+
model: opencode-go/deepseek-v4.1-flash
|
|
5
|
+
permissions:
|
|
6
|
+
- action: "*"
|
|
7
|
+
resource: "*"
|
|
8
|
+
effect: deny
|
|
9
|
+
- action: read
|
|
10
|
+
resource: "*"
|
|
11
|
+
effect: allow
|
|
12
|
+
- action: glob
|
|
13
|
+
resource: "*"
|
|
14
|
+
effect: allow
|
|
15
|
+
- action: grep
|
|
16
|
+
resource: "*"
|
|
17
|
+
effect: allow
|
|
18
|
+
- action: edit
|
|
19
|
+
resource: "dreaming/light/*"
|
|
20
|
+
effect: allow
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
You are the light-sleep pass of a session-memory system. You read what the user
|
|
24
|
+
did today and stage candidate memories. You do not promote anything.
|
|
25
|
+
|
|
26
|
+
Input: `.dreams/session-corpus/<today>.txt`. Each line is one message of one
|
|
27
|
+
session, numbered from 1, prefixed with the project it came from
|
|
28
|
+
(`[homelab] User: ...`).
|
|
29
|
+
|
|
30
|
+
Output: `dreaming/light/<today>.md`, starting with `# Light Sleep`.
|
|
31
|
+
|
|
32
|
+
For each candidate, write:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
- Candidate: <one sentence, under 200 characters>
|
|
36
|
+
- project: <the tag from the corpus>
|
|
37
|
+
- confidence: <0.00 to 1.00>
|
|
38
|
+
- evidence: .dreams/session-corpus/<today>.txt:<line>
|
|
39
|
+
- recalls: 0
|
|
40
|
+
- status: staged
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
|
|
45
|
+
- At most 15 candidates. Keep the ones a future session would actually need.
|
|
46
|
+
- Prefer what concerns the user: their preferences, corrections, decisions,
|
|
47
|
+
recurring problems, and verified facts about their machines.
|
|
48
|
+
- Ignore project documentation that already lives in the repository.
|
|
49
|
+
- Never record secrets, tokens, passwords, API keys, or file contents that
|
|
50
|
+
look like credentials. If the corpus holds one, do not quote it.
|
|
51
|
+
- Skip anything already written in `MEMORY.md`, in `projects/<project>/MEMORY.md`
|
|
52
|
+
or in a previous light file.
|
|
53
|
+
- If the corpus is empty or holds nothing durable, write only the heading and
|
|
54
|
+
the line `- No candidates today.`
|
package/dream/dream
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Nightly session-memory pass.
|
|
3
|
+
#
|
|
4
|
+
# Usage: dream corpus, then the light and deep passes (two agent runs)
|
|
5
|
+
# dream corpus corpus only, no model, no cost
|
|
6
|
+
# dream status one screen of state, read-only
|
|
7
|
+
# dream reset delete memory, diary, staging and corpus (asks first)
|
|
8
|
+
#
|
|
9
|
+
# Everything is written under $DREAM_HOME (default ~/dreams), never in the
|
|
10
|
+
# projects themselves: this is the agent's memory of the user, not project docs.
|
|
11
|
+
# Shared facts land in MEMORY.md, project facts in projects/<name>/MEMORY.md.
|
|
12
|
+
set -eu
|
|
13
|
+
|
|
14
|
+
# Resolve next to this script, so a custom BIN_DIR works without a PATH.
|
|
15
|
+
here=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
16
|
+
root="${DREAM_HOME:-$HOME/dreams}"
|
|
17
|
+
|
|
18
|
+
# One entry point. The standalone scripts stay installed and delegated to.
|
|
19
|
+
case "${1:-}" in
|
|
20
|
+
status) exec "$here/dream-status" ;;
|
|
21
|
+
corpus) exec "$here/dream-corpus" ;;
|
|
22
|
+
reset)
|
|
23
|
+
# Only $DREAM_HOME, and only when it looks like a dream home: a mistyped
|
|
24
|
+
# DREAM_HOME must not delete something else.
|
|
25
|
+
case $root in ''|/|"$HOME"|"$HOME/"|.) echo "dream: refusing to reset '$root'" >&2; exit 1 ;; esac
|
|
26
|
+
if [ -e "$root" ] && [ ! -d "$root/.dreams" ] && [ ! -d "$root/dreaming" ]; then
|
|
27
|
+
echo "dream: $root does not look like a dream home, refusing" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
if [ "${2:-}" != "--yes" ]; then
|
|
31
|
+
printf 'Delete every memory, diary and corpus under %s? [y/N] ' "$root" >&2
|
|
32
|
+
read -r answer || answer=
|
|
33
|
+
case $answer in y|Y|yes) ;; *) echo "dream: reset cancelled" >&2; exit 1 ;; esac
|
|
34
|
+
fi
|
|
35
|
+
rm -rf "$root/MEMORY.md" "$root/DREAMS.md" "$root/dreaming" "$root/projects" "$root/.dreams"
|
|
36
|
+
mkdir -p "$root/dreaming/light" "$root/dreaming/deep" "$root/projects" "$root/.dreams/session-corpus"
|
|
37
|
+
cat > "$root/MEMORY.md" <<'EOF'
|
|
38
|
+
# Shared memory
|
|
39
|
+
|
|
40
|
+
Facts true in every project, one per line. Project facts live in
|
|
41
|
+
`projects/<project>/MEMORY.md`.
|
|
42
|
+
EOF
|
|
43
|
+
cat > "$root/DREAMS.md" <<'EOF'
|
|
44
|
+
# Dream Diary
|
|
45
|
+
|
|
46
|
+
One entry per night, newest first. Durable facts live in [MEMORY.md](MEMORY.md).
|
|
47
|
+
|
|
48
|
+
<!-- opencode:dreaming:diary:start -->
|
|
49
|
+
EOF
|
|
50
|
+
echo "dream: reset done, $root is empty" >&2
|
|
51
|
+
exit 0
|
|
52
|
+
;;
|
|
53
|
+
'') ;;
|
|
54
|
+
*) echo "dream: unknown command '$1' (try: dream, dream status, dream corpus, dream reset)" >&2; exit 2 ;;
|
|
55
|
+
esac
|
|
56
|
+
|
|
57
|
+
list="${DREAM_PROJECTS:-$HOME/.config/opencode/dream-projects}"
|
|
58
|
+
day=$(date +%F)
|
|
59
|
+
# One run at a time: two overlapping passes interleave writes and commits, and
|
|
60
|
+
# the 04:30 timer can fire while a manual run still spends its two models.
|
|
61
|
+
mkdir -p "$root/.dreams"
|
|
62
|
+
if command -v flock >/dev/null 2>&1; then
|
|
63
|
+
exec 9>"$root/.dreams/run.lock"
|
|
64
|
+
flock -n 9 || { echo "dream: another run is in progress" >&2; exit 0; }
|
|
65
|
+
fi
|
|
66
|
+
command -v opencode >/dev/null || { echo "dream: opencode not on PATH" >&2; exit 1; }
|
|
67
|
+
[ -r "$list" ] || { echo "dream: no project list at $list" >&2; exit 1; }
|
|
68
|
+
|
|
69
|
+
corpus_bin="${DREAM_CORPUS_BIN:-$here/dream-corpus}"
|
|
70
|
+
"$corpus_bin"
|
|
71
|
+
|
|
72
|
+
corpus="$root/.dreams/session-corpus/$day.txt"
|
|
73
|
+
[ -s "$corpus" ] || { echo "dream: empty corpus, nothing to do" >&2; exit 0; }
|
|
74
|
+
|
|
75
|
+
mkdir -p "$root/dreaming/light" "$root/dreaming/deep"
|
|
76
|
+
cd "$root"
|
|
77
|
+
|
|
78
|
+
# One memory file per tracked project, so the deep pass only ever edits files
|
|
79
|
+
# that exist. Named after the project directory, the same tag dream-corpus
|
|
80
|
+
# writes into the corpus.
|
|
81
|
+
while IFS= read -r project; do
|
|
82
|
+
case $project in ''|\#*) continue ;; esac
|
|
83
|
+
name=$(basename "$project")
|
|
84
|
+
case $name in ''|.|..) continue ;; esac
|
|
85
|
+
memory="$root/projects/$name/MEMORY.md"
|
|
86
|
+
[ -f "$memory" ] && continue
|
|
87
|
+
mkdir -p "$(dirname "$memory")"
|
|
88
|
+
printf '# %s\n' "$name" > "$memory"
|
|
89
|
+
done < "$list"
|
|
90
|
+
|
|
91
|
+
# Per-stage model choice: "<stage> <model> [<model> ...]", first that works wins.
|
|
92
|
+
# The light pass reads raw session text, so this is where a local or paid
|
|
93
|
+
# provider matters. Missing file or missing stage falls back to the paid
|
|
94
|
+
# defaults below. Never fall back to a free model: sending the corpus to a
|
|
95
|
+
# provider that trains on its input is a downgrade nobody asked for.
|
|
96
|
+
models="${DREAM_MODELS:-$HOME/.config/opencode/dream-models}"
|
|
97
|
+
|
|
98
|
+
default_models() {
|
|
99
|
+
case $1 in
|
|
100
|
+
deep) echo "opencode-go/deepseek-v4-pro opencode-go/deepseek-v4.1-flash" ;;
|
|
101
|
+
*) echo "opencode-go/deepseek-v4.1-flash opencode-go/deepseek-v4-pro" ;;
|
|
102
|
+
esac
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
models_for() {
|
|
106
|
+
found=
|
|
107
|
+
if [ -r "$models" ]; then
|
|
108
|
+
found=$(awk -v stage="$1" '$1 == stage { sub(/^[[:space:]]*[^[:space:]]+[[:space:]]+/, ""); print; exit }' "$models")
|
|
109
|
+
fi
|
|
110
|
+
printf '%s\n' "${found:-$(default_models "$1")}"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# --agent does not carry the model: a session keeps its own selected model, so
|
|
114
|
+
# pass -m explicitly, and fall back down the stage's list.
|
|
115
|
+
run_agent() {
|
|
116
|
+
agent=$1; prompt=$2; title=$3
|
|
117
|
+
for model in $(models_for "${agent#dream-}"); do
|
|
118
|
+
echo "dream: $agent on $model" >&2
|
|
119
|
+
if opencode run --agent "$agent" -m "$model" --title "$title" "$prompt"; then
|
|
120
|
+
return 0
|
|
121
|
+
fi
|
|
122
|
+
echo "dream: $model unavailable, trying the next one" >&2
|
|
123
|
+
done
|
|
124
|
+
echo "dream: $agent failed on every model" >&2
|
|
125
|
+
return 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
echo "dream: light"
|
|
129
|
+
run_agent dream-light \
|
|
130
|
+
"Stage today's candidates from .dreams/session-corpus/$day.txt." "light $day"
|
|
131
|
+
|
|
132
|
+
# Preimage: a bad promotion then stays one `git checkout` away. Only when the
|
|
133
|
+
# data directory is a repo; install.sh does not create one. The corpus is
|
|
134
|
+
# excluded even when a .gitignore forgets it: session text must not enter git.
|
|
135
|
+
if git -C "$root" rev-parse --git-dir >/dev/null 2>&1; then
|
|
136
|
+
git -C "$root" add -A -- . ':!.dreams' >/dev/null 2>&1 || true
|
|
137
|
+
if ! git -C "$root" diff --cached --quiet; then
|
|
138
|
+
git -C "$root" commit -qm "dream: before deep $day" ||
|
|
139
|
+
echo "dream: preimage commit failed, check the git identity in $root" >&2
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
echo "dream: deep"
|
|
144
|
+
if run_agent dream-deep \
|
|
145
|
+
"Promote today's candidates and write the diary entry for $day." "deep $day"; then
|
|
146
|
+
# Advance the cursor only after both passes succeeded, so a failed run is
|
|
147
|
+
# retried on the same slice instead of losing it.
|
|
148
|
+
date +%s%3N > "$root/.dreams/last-run"
|
|
149
|
+
fi
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Build the dream corpus: one line per message, for each session touched in the
|
|
3
|
+
# last 24 hours, in every listed project. Deterministic: no model, no cost.
|
|
4
|
+
# The dream's own runs are skipped: their output must not become tomorrow's
|
|
5
|
+
# source. Labelled secrets are redacted on the way in.
|
|
6
|
+
# Output: $DREAM_HOME/.dreams/session-corpus/YYYY-MM-DD.txt (default ~/dreams)
|
|
7
|
+
set -eu
|
|
8
|
+
|
|
9
|
+
root="${DREAM_HOME:-$HOME/dreams}"
|
|
10
|
+
list="${DREAM_PROJECTS:-$HOME/.config/opencode/dream-projects}"
|
|
11
|
+
day=$(date +%F)
|
|
12
|
+
now=$(( $(date +%s) * 1000 ))
|
|
13
|
+
# Incremental: start where the last successful run stopped, never earlier than
|
|
14
|
+
# 24h ago. Without this, a session spanning midnight lands in two corpora.
|
|
15
|
+
state="$root/.dreams/last-run"
|
|
16
|
+
last=0
|
|
17
|
+
[ -r "$state" ] && last=$(cat "$state" 2>/dev/null || true)
|
|
18
|
+
case $last in ''|*[!0-9]*) last=0 ;; esac
|
|
19
|
+
cutoff=$(( now - 86400000 ))
|
|
20
|
+
[ "$last" -gt "$cutoff" ] && cutoff=$last
|
|
21
|
+
command -v opencode >/dev/null || { echo "dream-corpus: opencode not on PATH" >&2; exit 1; }
|
|
22
|
+
|
|
23
|
+
out="$root/.dreams/session-corpus/$day.txt"
|
|
24
|
+
mkdir -p "$(dirname "$out")"
|
|
25
|
+
# truncate on the first run of the day, append afterwards so a later run keeps
|
|
26
|
+
# the earlier slice. shortcut: a retry after a failed run re-appends the same
|
|
27
|
+
# messages; dedupe with sort -u if that ever shows up.
|
|
28
|
+
[ -f "$out" ] || : > "$out"
|
|
29
|
+
|
|
30
|
+
# One line per message. A labelled secret never leaves the machine; a bare key
|
|
31
|
+
# with no label still gets through, so this is a net, not a guarantee.
|
|
32
|
+
filter='
|
|
33
|
+
def clean:
|
|
34
|
+
gsub("\n"; " ")
|
|
35
|
+
| gsub("(?i)(?<k>(api[_-]?key|token|password|passwd|secret|bearer)[[:space:]]*[:=][[:space:]]*)[^[:space:]]+"; "\(.k)[redacted]")
|
|
36
|
+
| .[0:400];
|
|
37
|
+
.messages[] |
|
|
38
|
+
if .type == "user" then
|
|
39
|
+
"[" + $name + "] User: " + (.text // "" | clean)
|
|
40
|
+
elif .type == "assistant" then
|
|
41
|
+
(.content[]? | select(.type == "text") | .text | clean | "[" + $name + "] Assistant: " + .)
|
|
42
|
+
else empty end
|
|
43
|
+
'
|
|
44
|
+
|
|
45
|
+
while IFS= read -r project; do
|
|
46
|
+
case $project in ''|\#*) continue ;; esac
|
|
47
|
+
[ -d "$project/.git" ] || { echo "dream-corpus: skip $project (not a git repo)" >&2; continue; }
|
|
48
|
+
name=$(basename "$project")
|
|
49
|
+
cd "$project"
|
|
50
|
+
|
|
51
|
+
before=$(wc -l < "$out")
|
|
52
|
+
opencode session list --format json --max-count 100 2>/dev/null |
|
|
53
|
+
jq -r --argjson cutoff "$cutoff" '.[] | select(.updated >= $cutoff) | .id' |
|
|
54
|
+
while IFS= read -r sid; do
|
|
55
|
+
[ -n "$sid" ] || continue
|
|
56
|
+
tmp=$(mktemp)
|
|
57
|
+
if opencode session export "$sid" > "$tmp" 2>/dev/null; then
|
|
58
|
+
# Never learn from the dream's own runs: they are output, not source.
|
|
59
|
+
agent=$(jq -r '.info.agent // ""' "$tmp" 2>/dev/null) || agent=
|
|
60
|
+
case $agent in
|
|
61
|
+
dream-*) echo "dream-corpus: skip $sid (own $agent run)" >&2 ;;
|
|
62
|
+
*) jq -r --arg name "$name" "$filter" "$tmp" >> "$out" ||
|
|
63
|
+
echo "dream-corpus: skip $sid (unreadable export)" >&2 ;;
|
|
64
|
+
esac
|
|
65
|
+
fi
|
|
66
|
+
rm -f "$tmp"
|
|
67
|
+
done
|
|
68
|
+
echo "dream-corpus: $name +$(( $(wc -l < "$out") - before )) lignes"
|
|
69
|
+
done < "$list"
|
|
70
|
+
|
|
71
|
+
echo "dream-corpus: $out ($(wc -l < "$out") lignes)"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# One screen of state for the dream job. Read-only, no model, no cost.
|
|
3
|
+
set -eu
|
|
4
|
+
|
|
5
|
+
root="${DREAM_HOME:-$HOME/dreams}"
|
|
6
|
+
list="${DREAM_PROJECTS:-$HOME/.config/opencode/dream-projects}"
|
|
7
|
+
day=$(date +%F)
|
|
8
|
+
|
|
9
|
+
timer=$(systemctl --user is-active dream.timer 2>/dev/null || true)
|
|
10
|
+
[ -n "$timer" ] || timer=inactive
|
|
11
|
+
next=$(systemctl --user list-timers dream.timer --no-legend 2>/dev/null | awk 'NR==1 {print $1" "$2" "$3}' || true)
|
|
12
|
+
echo "root: $root"
|
|
13
|
+
echo "timer: $timer${next:+ (next $next)}"
|
|
14
|
+
|
|
15
|
+
corpus_dir="$root/.dreams/session-corpus"
|
|
16
|
+
n_corpus=$(find "$corpus_dir" -name '*.txt' 2>/dev/null | wc -l)
|
|
17
|
+
last_corpus=$(ls -1 "$corpus_dir" 2>/dev/null | tail -1 || true)
|
|
18
|
+
last_lines=0
|
|
19
|
+
[ -n "$last_corpus" ] && last_lines=$(wc -l < "$corpus_dir/$last_corpus")
|
|
20
|
+
|
|
21
|
+
n_light=$(find "$root/dreaming/light" -name '*.md' 2>/dev/null | wc -l)
|
|
22
|
+
n_deep=$(find "$root/dreaming/deep" -name '*.md' 2>/dev/null | wc -l)
|
|
23
|
+
n_mem=$(grep -c '.' "$root/MEMORY.md" 2>/dev/null || true); [ -n "$n_mem" ] || n_mem=0
|
|
24
|
+
mem_files=$(find "$root/projects" -name MEMORY.md 2>/dev/null | sort)
|
|
25
|
+
n_proj=$(printf '%s\n' "$mem_files" | grep -c . 2>/dev/null || true); [ -n "$n_proj" ] || n_proj=0
|
|
26
|
+
n_dreams=$(grep -c '^\*[A-Z]' "$root/DREAMS.md" 2>/dev/null || true); [ -n "$n_dreams" ] || n_dreams=0
|
|
27
|
+
today=$([ -f "$root/dreaming/light/$day.md" ] && echo oui || echo non)
|
|
28
|
+
|
|
29
|
+
echo "corpus: $n_corpus jour(s), dernier ${last_corpus:-aucun} ($last_lines lignes)"
|
|
30
|
+
echo "light: $n_light fichier(s), aujourd'hui: $today"
|
|
31
|
+
echo "deep: $n_deep fichier(s)"
|
|
32
|
+
echo "MEMORY: $n_mem ligne(s) partagées, $n_proj projet(s)"
|
|
33
|
+
mem_report() { [ -f "$1" ] && printf ' %6s %s\n' "$(wc -m < "$1" | tr -d ' ')" "${1#"$root"/}"; }
|
|
34
|
+
mem_report "$root/MEMORY.md"
|
|
35
|
+
while IFS= read -r f; do mem_report "$f"; done <<EOF
|
|
36
|
+
$mem_files
|
|
37
|
+
EOF
|
|
38
|
+
echo "DREAMS: $n_dreams entrée(s)"
|
|
39
|
+
echo
|
|
40
|
+
echo "projets suivis:"
|
|
41
|
+
sed -e 's/#.*//' -e '/^[[:space:]]*$/d' "$list" 2>/dev/null | sed 's/^/ /' || echo " (aucun)"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Consolidate OpenCode sessions into a memory file
|
|
3
|
+
|
|
4
|
+
[Service]
|
|
5
|
+
Type=oneshot
|
|
6
|
+
# Portable defaults. If `opencode` lives outside them, put its directory in
|
|
7
|
+
# ~/.config/dream.env as PATH=/your/bin:/usr/local/bin:/usr/bin:/bin
|
|
8
|
+
Environment="PATH=%h/.local/bin:/usr/local/bin:/usr/bin:/bin"
|
|
9
|
+
EnvironmentFile=-%h/.config/dream.env
|
|
10
|
+
# install.sh rewrites the line below to the chosen BIN_DIR.
|
|
11
|
+
ExecStart=%h/.local/bin/dream
|
package/install.sh
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Install the dream job and its plugin: corpus builder, light/deep agents,
|
|
3
|
+
# systemd units, memory home.
|
|
4
|
+
#
|
|
5
|
+
# curl -fsSL https://raw.githubusercontent.com/vasilvestre/opencode-dream-memory/main/install.sh | sh
|
|
6
|
+
# sh install.sh # same thing from a clone, timer disabled
|
|
7
|
+
# sh install.sh --enable # install, then enable and start the nightly timer
|
|
8
|
+
#
|
|
9
|
+
# Never overwrites MEMORY.md, DREAMS.md or dream-projects: those hold your data
|
|
10
|
+
# and your choices. Safe to re-run after a pull.
|
|
11
|
+
set -eu
|
|
12
|
+
|
|
13
|
+
ENABLE=0
|
|
14
|
+
[ "${1:-}" = "--enable" ] && ENABLE=1
|
|
15
|
+
|
|
16
|
+
BIN="${BIN_DIR:-$HOME/.local/bin}"
|
|
17
|
+
AGENTS="${AGENTS_DIR:-$HOME/.config/opencode/agents}"
|
|
18
|
+
PLUGINS="${PLUGINS_DIR:-$HOME/.config/opencode/plugins}"
|
|
19
|
+
UNITS="${UNITS_DIR:-$HOME/.config/systemd/user}"
|
|
20
|
+
HOME_DREAM="${DREAM_HOME:-$HOME/dreams}"
|
|
21
|
+
LIST="$HOME/.config/opencode/dream-projects"
|
|
22
|
+
MODELS="$HOME/.config/opencode/dream-models"
|
|
23
|
+
|
|
24
|
+
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
25
|
+
|
|
26
|
+
command -v opencode >/dev/null || { echo "install: opencode is not on PATH" >&2; exit 1; }
|
|
27
|
+
command -v jq >/dev/null || { echo "install: jq is not on PATH" >&2; exit 1; }
|
|
28
|
+
|
|
29
|
+
# A piped install has no dream/ next to the script, so fetch the archive.
|
|
30
|
+
if [ ! -d "$ROOT/dream" ]; then
|
|
31
|
+
url=https://github.com/vasilvestre/opencode-dream-memory/archive/refs/heads/main.tar.gz
|
|
32
|
+
tmp=$(mktemp -d)
|
|
33
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
34
|
+
echo "install: downloading $url"
|
|
35
|
+
if command -v curl >/dev/null 2>&1; then
|
|
36
|
+
curl -fsSL "$url" | tar xz -C "$tmp"
|
|
37
|
+
elif command -v wget >/dev/null 2>&1; then
|
|
38
|
+
wget -qO- "$url" | tar xz -C "$tmp"
|
|
39
|
+
else
|
|
40
|
+
echo "install: curl or wget is required to download the archive" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
ROOT=$tmp/opencode-dream-memory-main
|
|
44
|
+
fi
|
|
45
|
+
SRC=$ROOT/dream
|
|
46
|
+
|
|
47
|
+
mkdir -p "$BIN" "$AGENTS" "$UNITS" "$PLUGINS" "$(dirname "$LIST")" "$HOME_DREAM/dreaming/light" "$HOME_DREAM/dreaming/deep" "$HOME_DREAM/.dreams/session-corpus" "$HOME_DREAM/projects"
|
|
48
|
+
|
|
49
|
+
for f in dream dream-corpus dream-status; do
|
|
50
|
+
install -m 755 "$SRC/$f" "$BIN/$f"
|
|
51
|
+
done
|
|
52
|
+
for f in dream-light.md dream-deep.md; do
|
|
53
|
+
install -m 644 "$SRC/agents/$f" "$AGENTS/$f"
|
|
54
|
+
done
|
|
55
|
+
# One copy only: a second file with the same id makes OpenCode drop every plugin.
|
|
56
|
+
# An existing symlink stays, so a developer keeps editing the file they cloned.
|
|
57
|
+
[ -L "$PLUGINS/dream-memory.js" ] ||
|
|
58
|
+
install -m 644 "$ROOT/.opencode/plugins/dream-memory.js" "$PLUGINS/dream-memory.js"
|
|
59
|
+
for f in dream.service dream.timer; do
|
|
60
|
+
install -m 644 "$SRC/systemd/$f" "$UNITS/$f"
|
|
61
|
+
done
|
|
62
|
+
# The unit must point at the chosen BIN_DIR, which systemd cannot guess.
|
|
63
|
+
sed -i "s|^ExecStart=.*|ExecStart=$BIN/dream|" "$UNITS/dream.service"
|
|
64
|
+
|
|
65
|
+
[ -f "$HOME_DREAM/MEMORY.md" ] || cat > "$HOME_DREAM/MEMORY.md" <<'EOF'
|
|
66
|
+
# Shared memory
|
|
67
|
+
|
|
68
|
+
Facts true in every project, one per line. Project facts live in
|
|
69
|
+
`projects/<project>/MEMORY.md`.
|
|
70
|
+
EOF
|
|
71
|
+
|
|
72
|
+
[ -f "$HOME_DREAM/DREAMS.md" ] || cat > "$HOME_DREAM/DREAMS.md" <<'EOF'
|
|
73
|
+
# Dream Diary
|
|
74
|
+
|
|
75
|
+
One entry per night, newest first. Durable facts live in [MEMORY.md](MEMORY.md).
|
|
76
|
+
|
|
77
|
+
<!-- opencode:dreaming:diary:start -->
|
|
78
|
+
EOF
|
|
79
|
+
|
|
80
|
+
[ -f "$HOME_DREAM/.gitignore" ] || printf '.dreams/\n' > "$HOME_DREAM/.gitignore"
|
|
81
|
+
|
|
82
|
+
if [ ! -f "$LIST" ]; then
|
|
83
|
+
cat > "$LIST" <<'EOF'
|
|
84
|
+
# Projects the dream job consolidates, one absolute path per line.
|
|
85
|
+
# A project opts in by having a MEMORY.md file at its root.
|
|
86
|
+
# Add your projects below, for example /home/you/code/my-project
|
|
87
|
+
EOF
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [ ! -f "$MODELS" ]; then
|
|
91
|
+
cat > "$MODELS" <<'EOF'
|
|
92
|
+
# Model per dream stage: <stage> <model> [<model> ...]. The script tries them
|
|
93
|
+
# left to right and keeps the first that runs.
|
|
94
|
+
# The light pass reads raw session text; the deep pass reads staged candidates.
|
|
95
|
+
# With no matching line, the script uses its own paid defaults.
|
|
96
|
+
#
|
|
97
|
+
# light opencode-go/deepseek-v4.1-flash
|
|
98
|
+
# deep opencode-go/deepseek-v4-pro
|
|
99
|
+
EOF
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
103
|
+
|
|
104
|
+
echo "installed:"
|
|
105
|
+
echo " $BIN/dream{,-corpus,-status}"
|
|
106
|
+
echo " $AGENTS/dream-{light,deep}.md"
|
|
107
|
+
echo " $PLUGINS/dream-memory.js"
|
|
108
|
+
echo " $UNITS/dream.{service,timer}"
|
|
109
|
+
echo " $HOME_DREAM (MEMORY.md, projects/, dreaming/, DREAMS.md)"
|
|
110
|
+
|
|
111
|
+
if [ "$ENABLE" -eq 1 ]; then
|
|
112
|
+
systemctl --user enable --now dream.timer
|
|
113
|
+
echo "timer: enabled (daily 04:30)"
|
|
114
|
+
elif systemctl --user is-enabled dream.timer >/dev/null 2>&1; then
|
|
115
|
+
echo "timer: already enabled (daily 04:30)"
|
|
116
|
+
else
|
|
117
|
+
echo "timer: disabled. Enable it when the output looks right:"
|
|
118
|
+
echo " systemctl --user enable --now dream.timer"
|
|
119
|
+
fi
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vasilvestre/opencode-dream-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Injects shared and per-project memory files into every OpenCode request, so they survive context compaction.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"opencode-plugin",
|
|
7
|
+
"opencode",
|
|
8
|
+
"memory",
|
|
9
|
+
"context",
|
|
10
|
+
"compaction"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "vasilvestre",
|
|
15
|
+
"url": "https://github.com/vasilvestre"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/vasilvestre/opencode-dream-memory",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/vasilvestre/opencode-dream-memory.git"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./.opencode/plugins/dream-memory.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./.opencode/plugins/dream-memory.js",
|
|
26
|
+
"./plugin": "./.opencode/plugins/dream-memory.js"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node scripts/check-plugin.mjs && sh scripts/check-corpus.sh"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
".opencode/",
|
|
33
|
+
"dream/",
|
|
34
|
+
"install.sh",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|