opencode-deja 0.1.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/LICENSE +21 -0
- package/README.md +70 -0
- package/index.js +260 -0
- package/lib.js +43 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vladislav Shulcz
|
|
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
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# opencode-deja
|
|
2
|
+
|
|
3
|
+
opencode remembers its own sessions. This plugin answers the other question:
|
|
4
|
+
what you did in Claude Code, Codex, Cursor, Gemini, Zed and fifteen more agents
|
|
5
|
+
on this machine — including the months before you installed anything.
|
|
6
|
+
|
|
7
|
+
It runs [deja](https://github.com/vshulcz/deja-vu), a local Go binary that
|
|
8
|
+
indexes the transcripts those agents already wrote to disk. No LLM, no
|
|
9
|
+
embeddings, nothing leaves the machine.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"$schema": "https://opencode.ai/config.json",
|
|
16
|
+
"plugin": ["opencode-deja"]
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
opencode installs the package on start. The deja binary comes with it, but a
|
|
21
|
+
deja you installed yourself always wins — see [Which binary](#which-binary).
|
|
22
|
+
|
|
23
|
+
## What you get
|
|
24
|
+
|
|
25
|
+
Six tools the model can call:
|
|
26
|
+
|
|
27
|
+
| Tool | Answers |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `deja_recall` | past sessions matching an error, function, file or flag |
|
|
30
|
+
| `deja_session` | the full digest of one past session — what was tried and decided |
|
|
31
|
+
| `deja_blame` | the sessions that discussed a file, before you edit it |
|
|
32
|
+
| `deja_fix` | what this machine ran after that same error, where it stayed fixed |
|
|
33
|
+
| `deja_how` | the real build/test/deploy invocation, with the flags actually used |
|
|
34
|
+
| `deja_remember` | store one durable decision |
|
|
35
|
+
|
|
36
|
+
And recall nobody has to ask for:
|
|
37
|
+
|
|
38
|
+
- the project's recent sessions are pushed onto the system prompt once per
|
|
39
|
+
session, with a one-time toast so you know memory arrived;
|
|
40
|
+
- each prompt gets a relevance pass of its own — silent when nothing matches;
|
|
41
|
+
- before compaction the working transcript is indexed, so the session survives
|
|
42
|
+
the window collapsing.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"plugin": [["opencode-deja", { "autoRecall": false, "tools": true }]]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `autoRecall` (default `true`) — the three hooks above. Turn off to keep only
|
|
53
|
+
the tools.
|
|
54
|
+
- `tools` (default `true`) — the six tools. Turn off if you already reach deja
|
|
55
|
+
through its MCP server and do not want them twice.
|
|
56
|
+
- `bin` — path to a specific deja binary.
|
|
57
|
+
|
|
58
|
+
## Which binary
|
|
59
|
+
|
|
60
|
+
In order: `bin` from the options, `DEJA_BIN`, a `deja` on `PATH`, the usual
|
|
61
|
+
install locations (`~/.local/bin`, `/usr/local/bin`, `/opt/homebrew/bin`), and
|
|
62
|
+
only then the copy npm installed with this package. Your own `deja update` or
|
|
63
|
+
`brew upgrade deja` wins over whatever version this package shipped with.
|
|
64
|
+
|
|
65
|
+
Without deja anywhere, the tools say so instead of reporting an empty history,
|
|
66
|
+
and the hooks stay quiet.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// opencode-deja — the sessions you already have, inside opencode.
|
|
2
|
+
//
|
|
3
|
+
// opencode remembers its own sessions. This plugin answers the other question:
|
|
4
|
+
// what you did in Claude Code, Codex, Cursor, Gemini and sixteen more agents on
|
|
5
|
+
// this machine, including months before deja existed. The index is deja's; this
|
|
6
|
+
// file is the seam: six tools the model can call, plus recall that arrives
|
|
7
|
+
// without being asked for.
|
|
8
|
+
|
|
9
|
+
import { createRequire } from "node:module"
|
|
10
|
+
import { execFile } from "node:child_process"
|
|
11
|
+
import { promisify } from "node:util"
|
|
12
|
+
import { homedir } from "node:os"
|
|
13
|
+
import { join } from "node:path"
|
|
14
|
+
import { accessSync, constants } from "node:fs"
|
|
15
|
+
|
|
16
|
+
import { clampLimit, contextText, lastUserText } from "./lib.js"
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url)
|
|
19
|
+
const run_ = promisify(execFile)
|
|
20
|
+
|
|
21
|
+
const WINDOWS = process.platform === "win32"
|
|
22
|
+
const PLATFORM = WINDOWS ? "windows" : process.platform
|
|
23
|
+
const ARCH = process.arch === "x64" ? "amd64" : process.arch
|
|
24
|
+
const EXE = WINDOWS ? "deja.exe" : "deja"
|
|
25
|
+
|
|
26
|
+
// The tool helper is a peer of the host, not of us: it is identity plus a zod
|
|
27
|
+
// re-export. Without it the hooks still run and only the model-facing tools are
|
|
28
|
+
// skipped — a missing peer must not take the plugin down.
|
|
29
|
+
let tool = null
|
|
30
|
+
try {
|
|
31
|
+
;({ tool } = await import("@opencode-ai/plugin"))
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
// wellKnown lists the places a user's own install lands. PATH is the normal
|
|
35
|
+
// answer; these cover the case where opencode was started from a launcher with
|
|
36
|
+
// a PATH that never sourced a shell profile.
|
|
37
|
+
function wellKnown() {
|
|
38
|
+
const home = homedir()
|
|
39
|
+
if (WINDOWS) {
|
|
40
|
+
const local = process.env.LOCALAPPDATA || join(home, "AppData", "Local")
|
|
41
|
+
return [join(local, "deja", "bin", EXE), join(home, ".local", "bin", EXE)]
|
|
42
|
+
}
|
|
43
|
+
return [
|
|
44
|
+
join(home, ".local", "bin", EXE),
|
|
45
|
+
"/usr/local/bin/deja",
|
|
46
|
+
"/opt/homebrew/bin/deja",
|
|
47
|
+
"/usr/bin/deja",
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// resolveDeja picks the binary in the order a user would expect: what they
|
|
52
|
+
// pointed at, then the deja they installed themselves and keep current with
|
|
53
|
+
// `deja update` or brew, and only last the copy npm brought along with this
|
|
54
|
+
// plugin. Pinning them to our bundled copy would freeze their memory at
|
|
55
|
+
// whatever version this package was released against.
|
|
56
|
+
function resolveDeja(setting) {
|
|
57
|
+
const candidates = [setting, process.env.DEJA_BIN, EXE, ...wellKnown()]
|
|
58
|
+
try {
|
|
59
|
+
candidates.push(require.resolve(`@vshulcz/deja-vu-${PLATFORM}-${ARCH}/bin/${EXE}`))
|
|
60
|
+
} catch {}
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
if (!candidate) continue
|
|
63
|
+
if (candidate.includes("/") || candidate.includes("\\")) {
|
|
64
|
+
try {
|
|
65
|
+
accessSync(candidate, constants.X_OK)
|
|
66
|
+
} catch {
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return candidate
|
|
71
|
+
}
|
|
72
|
+
// Nothing to check further; keep the plain name so a failure names the thing
|
|
73
|
+
// that is missing rather than a path nobody chose.
|
|
74
|
+
return EXE
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// deja is asked for text, never for control flow: memory is optional
|
|
78
|
+
// everywhere, and a throw here would end someone's turn.
|
|
79
|
+
async function run(bin, args, input, cwd, timeout = 20000) {
|
|
80
|
+
try {
|
|
81
|
+
const child = run_(bin, args, {
|
|
82
|
+
encoding: "utf8",
|
|
83
|
+
timeout,
|
|
84
|
+
cwd,
|
|
85
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
86
|
+
})
|
|
87
|
+
if (input !== undefined) {
|
|
88
|
+
child.child.stdin.end(input)
|
|
89
|
+
}
|
|
90
|
+
const { stdout } = await child
|
|
91
|
+
return stdout.trim()
|
|
92
|
+
} catch {
|
|
93
|
+
return ""
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const NOTHING = "Nothing in this machine's history matches that."
|
|
98
|
+
const MISSING =
|
|
99
|
+
"deja is not installed on this machine, so there is no history to search. " +
|
|
100
|
+
"Install it with: curl -fsSL https://raw.githubusercontent.com/vshulcz/deja-vu/main/install.sh | sh"
|
|
101
|
+
|
|
102
|
+
export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
103
|
+
const config = options || {}
|
|
104
|
+
const bin = resolveDeja(typeof config.bin === "string" ? config.bin : "")
|
|
105
|
+
const cwd = directory || process.cwd()
|
|
106
|
+
// Every call runs in the session's own directory: deja ranks by project.
|
|
107
|
+
const ask = (args, input, timeout) => run(bin, args, input, cwd, timeout)
|
|
108
|
+
|
|
109
|
+
// Whether the binary answers at all is asked once. Without it every tool
|
|
110
|
+
// would report an empty history to someone who simply never installed deja,
|
|
111
|
+
// which reads as "you have no past" rather than "nothing is here to read it".
|
|
112
|
+
const installed = (await ask(["version"], undefined, 5000)) !== ""
|
|
113
|
+
const answer = (text) => (installed ? text || NOTHING : MISSING)
|
|
114
|
+
|
|
115
|
+
const digests = new Map()
|
|
116
|
+
const told = new Set()
|
|
117
|
+
|
|
118
|
+
const hooks = {}
|
|
119
|
+
|
|
120
|
+
if (config.tools !== false && tool) {
|
|
121
|
+
const schema = tool.schema
|
|
122
|
+
hooks.tool = {
|
|
123
|
+
deja_recall: tool({
|
|
124
|
+
description:
|
|
125
|
+
"Search this machine's own past AI coding sessions — every agent used on it, including months before deja was installed. Use before debugging an error or re-implementing anything that may already exist. Match on the most specific token available: an exact error string, function name, file path or flag.",
|
|
126
|
+
args: {
|
|
127
|
+
query: schema.string().describe("Specific tokens to match. Several words are ANDed."),
|
|
128
|
+
limit: schema.number().optional().describe("How many sessions to return. Default 5."),
|
|
129
|
+
},
|
|
130
|
+
async execute(args) {
|
|
131
|
+
const limit = String(clampLimit(args.limit))
|
|
132
|
+
return answer(await ask(["search", "--json", "--limit", limit, String(args.query)]))
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
deja_session: tool({
|
|
136
|
+
description:
|
|
137
|
+
"A full digest of the single best-matching past session — what was tried, what was decided, what it cost. Use after deja_recall when the reasoning behind an earlier decision matters, not just that it happened.",
|
|
138
|
+
args: {
|
|
139
|
+
query: schema.string().describe("A query, or a session id prefix returned by deja_recall."),
|
|
140
|
+
},
|
|
141
|
+
async execute(args) {
|
|
142
|
+
return answer(await ask(["ctx", String(args.query)]))
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
deja_blame: tool({
|
|
146
|
+
description:
|
|
147
|
+
"The past sessions that discussed a file, so you know why it is shaped the way it is before editing, refactoring or deleting it. Session history, not git authorship.",
|
|
148
|
+
args: {
|
|
149
|
+
path: schema.string().describe("Path to the file, absolute or relative to the project."),
|
|
150
|
+
},
|
|
151
|
+
async execute(args) {
|
|
152
|
+
return answer(await ask(["blame", String(args.path), "--json"]))
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
155
|
+
deja_fix: tool({
|
|
156
|
+
description:
|
|
157
|
+
"What this machine ran after that same error before, in the sessions where the error did not come back. Paste the failing output verbatim rather than a paraphrase — the match is on the error's own words.",
|
|
158
|
+
args: {
|
|
159
|
+
error: schema.string().describe("The failing output, copied as it was printed."),
|
|
160
|
+
},
|
|
161
|
+
async execute(args) {
|
|
162
|
+
return answer(await ask(["fix", String(args.error)]))
|
|
163
|
+
},
|
|
164
|
+
}),
|
|
165
|
+
deja_how: tool({
|
|
166
|
+
description:
|
|
167
|
+
"The real invocation this machine uses for a build, test, deploy or script, with the flags it actually ran, ordered by how many sessions ran it. A guessed command is plausible and fails on this setup.",
|
|
168
|
+
args: {
|
|
169
|
+
what: schema.string().describe("The thing to run: a tool, a task, a script name."),
|
|
170
|
+
},
|
|
171
|
+
async execute(args) {
|
|
172
|
+
return answer(await ask(["how", String(args.what)]))
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
deja_remember: tool({
|
|
176
|
+
description:
|
|
177
|
+
"Store one durable decision once it is settled, as a single self-contained fact that will make sense months later. Not transcripts, not a summary of the conversation, and not anything already obvious from the code.",
|
|
178
|
+
args: {
|
|
179
|
+
text: schema.string().describe("The decision, in one or two sentences, with the reason it was taken."),
|
|
180
|
+
},
|
|
181
|
+
async execute(args) {
|
|
182
|
+
const written = await ask(["remember", String(args.text)])
|
|
183
|
+
if (!installed) return MISSING
|
|
184
|
+
return written || "deja did not record that."
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (config.autoRecall === false) return hooks
|
|
191
|
+
|
|
192
|
+
// opencode has no session-start hook. The system prompt is assembled on
|
|
193
|
+
// every request, so the session digest is fetched once and pushed there —
|
|
194
|
+
// the same place Claude Code's SessionStart output lands.
|
|
195
|
+
hooks["experimental.chat.system.transform"] = async (input, output) => {
|
|
196
|
+
try {
|
|
197
|
+
const key = input?.sessionID || "default"
|
|
198
|
+
if (!digests.has(key)) {
|
|
199
|
+
const { context, receipt } = contextText(await ask(["hook-context"], undefined, 30000))
|
|
200
|
+
digests.set(key, context)
|
|
201
|
+
// The receipt is the only sign the user gets that memory arrived. Once
|
|
202
|
+
// per session: repeating it every turn is wallpaper. The hook's own
|
|
203
|
+
// output is model context, so the toast is the only channel for it.
|
|
204
|
+
if (receipt && !told.has(key)) {
|
|
205
|
+
told.add(key)
|
|
206
|
+
await client.tui.showToast({
|
|
207
|
+
body: { message: receipt, variant: "info", duration: 6000 },
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const context = digests.get(key)
|
|
212
|
+
if (context) {
|
|
213
|
+
output.system.push(context)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
// Nothing recalled: either this machine has no history yet, or the first
|
|
217
|
+
// index is still building. Only the second is worth saying, and saying it
|
|
218
|
+
// drops the empty answer so the next turn asks again.
|
|
219
|
+
if (told.has(key)) return
|
|
220
|
+
const status = await ask(["warmup-status"], undefined, 5000)
|
|
221
|
+
if (!status) return
|
|
222
|
+
told.add(key)
|
|
223
|
+
digests.delete(key)
|
|
224
|
+
await client.tui.showToast({
|
|
225
|
+
body: { message: status, variant: "info", duration: 6000 },
|
|
226
|
+
})
|
|
227
|
+
} catch {
|
|
228
|
+
// memory is optional: never break the session over it
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Per-prompt recall, the relevance pass Claude Code gets on
|
|
233
|
+
// UserPromptSubmit: the digest above is ranked by the project, this is ranked
|
|
234
|
+
// by what the user just asked. Silent when nothing matches.
|
|
235
|
+
hooks["experimental.chat.messages.transform"] = async (_input, output) => {
|
|
236
|
+
try {
|
|
237
|
+
const { parts, prompt } = lastUserText(output?.messages)
|
|
238
|
+
if (!prompt) return
|
|
239
|
+
const raw = await ask(["hook-prompt"], JSON.stringify({ prompt, cwd }))
|
|
240
|
+
if (!raw) return
|
|
241
|
+
const extra = JSON.parse(raw)?.hookSpecificOutput?.additionalContext
|
|
242
|
+
if (!extra) return
|
|
243
|
+
parts[parts.length - 1].text += "\n\n" + extra
|
|
244
|
+
} catch {
|
|
245
|
+
// memory is optional: never break the session over it
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Compaction is about to throw the working transcript away. Index what
|
|
250
|
+
// exists now, so the session survives in memory after the window collapses.
|
|
251
|
+
hooks["experimental.session.compacting"] = async () => {
|
|
252
|
+
try {
|
|
253
|
+
await ask(["hook-precompact"], undefined, 60000)
|
|
254
|
+
} catch {
|
|
255
|
+
// memory is optional: never break a compaction over it
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return hooks
|
|
260
|
+
}
|
package/lib.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The pure half of the plugin: everything that reads deja's output or
|
|
2
|
+
// opencode's message shape, with no process and no host. Kept apart from
|
|
3
|
+
// index.js because opencode loads every function a plugin module exports —
|
|
4
|
+
// a helper exported next to the plugin gets called as one.
|
|
5
|
+
|
|
6
|
+
// contextText pulls the recall out of whatever hook-context printed. deja
|
|
7
|
+
// answers in the Claude Code hook shape; older builds and `--plain` print bare
|
|
8
|
+
// text, so both are accepted.
|
|
9
|
+
export function contextText(raw) {
|
|
10
|
+
const text = String(raw || "").trim()
|
|
11
|
+
if (!text) return { context: "", receipt: "" }
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(text)
|
|
14
|
+
return {
|
|
15
|
+
context: parsed?.hookSpecificOutput?.additionalContext || "",
|
|
16
|
+
receipt: parsed?.systemMessage || "",
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
return { context: text, receipt: "" }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// lastUserText is what the person typed on this turn, which is what the
|
|
24
|
+
// per-prompt recall is ranked against. Parts a plugin appended earlier are
|
|
25
|
+
// part of the same message, so the text is joined and matched whole.
|
|
26
|
+
export function lastUserText(messages) {
|
|
27
|
+
const list = Array.isArray(messages) ? messages : []
|
|
28
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
29
|
+
if (list[i]?.info?.role !== "user") continue
|
|
30
|
+
const parts = (list[i].parts || []).filter((p) => p?.type === "text" && p.text)
|
|
31
|
+
if (!parts.length) return { parts: [], prompt: "" }
|
|
32
|
+
return { parts, prompt: parts.map((p) => p.text).join("\n").trim() }
|
|
33
|
+
}
|
|
34
|
+
return { parts: [], prompt: "" }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// clampLimit keeps a model that asks for a hundred sessions from spending the
|
|
38
|
+
// window on a tail nobody reads.
|
|
39
|
+
export function clampLimit(value, fallback = 5) {
|
|
40
|
+
const asked = Number(value)
|
|
41
|
+
if (!Number.isFinite(asked)) return fallback
|
|
42
|
+
return Math.min(20, Math.max(1, Math.trunc(asked)))
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-deja",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Brings the session history of nineteen other coding agents into opencode: recall, session digest and per-file history tools over a local index, plus automatic recall on every request.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./package.json": "./package.json"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"lib.js",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/vshulcz/deja-vu.git",
|
|
21
|
+
"directory": "extensions/opencode"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/vshulcz/deja-vu/tree/main/extensions/opencode",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/vshulcz/deja-vu/issues"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"opencode",
|
|
29
|
+
"opencode-plugin",
|
|
30
|
+
"memory",
|
|
31
|
+
"recall",
|
|
32
|
+
"session-history",
|
|
33
|
+
"agent"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@opencode-ai/plugin": ">=1.0.0",
|
|
40
|
+
"@vshulcz/deja-vu": "^0.18.0"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test test/*.test.js"
|
|
44
|
+
}
|
|
45
|
+
}
|