opencode-deja 0.19.2 → 0.19.3
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 +3 -2
- package/index.js +86 -12
- package/lib.js +20 -3
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# opencode-deja
|
|
2
2
|
|
|
3
3
|
opencode remembers its own sessions. This plugin answers the other question:
|
|
4
|
-
what you did in
|
|
5
|
-
|
|
4
|
+
what you did in the twenty-one other coding agents on this machine — Claude Code,
|
|
5
|
+
Codex, Cursor, Gemini and Zed among them — including the months before you
|
|
6
|
+
installed anything.
|
|
6
7
|
|
|
7
8
|
It runs [deja](https://github.com/vshulcz/deja-vu), a local Go binary that
|
|
8
9
|
indexes the transcripts those agents already wrote to disk. No LLM, no
|
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { join } from "node:path"
|
|
|
14
14
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs"
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
+
argv,
|
|
17
18
|
clampLimit,
|
|
18
19
|
cliPluginPath,
|
|
19
20
|
configPaths,
|
|
@@ -30,6 +31,11 @@ const WINDOWS = process.platform === "win32"
|
|
|
30
31
|
const PLATFORM = WINDOWS ? "windows" : process.platform
|
|
31
32
|
const ARCH = process.arch === "x64" ? "amd64" : process.arch
|
|
32
33
|
const EXE = WINDOWS ? "deja.exe" : "deja"
|
|
34
|
+
// How many turns of a session will ask again after being answered with
|
|
35
|
+
// nothing. Three covers the moments that pass — a locked index, a call that
|
|
36
|
+
// timed out, an upgrade replacing the binary — without turning a store that
|
|
37
|
+
// genuinely holds nothing into a shell-out on every turn.
|
|
38
|
+
const emptyRetries = 3
|
|
33
39
|
|
|
34
40
|
// The tool helper is a peer of the host, not of us: it is identity plus a zod
|
|
35
41
|
// re-export. Without it the hooks still run and only the model-facing tools are
|
|
@@ -143,6 +149,15 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
143
149
|
|
|
144
150
|
const digests = new Map()
|
|
145
151
|
const told = new Set()
|
|
152
|
+
// How many times this session has been answered with nothing. An empty
|
|
153
|
+
// answer is cached so the plugin does not shell out every turn, but the
|
|
154
|
+
// reasons for it are not alike: no history is permanent, while a locked
|
|
155
|
+
// index, a timed-out call or a binary being replaced mid-`deja update` are
|
|
156
|
+
// over by the next turn. Cached alike, one bad moment cost the session all
|
|
157
|
+
// of its memory — every later turn read the emptiness rather than asking
|
|
158
|
+
// again. Counted, so a store that really is empty is still only asked a few
|
|
159
|
+
// times.
|
|
160
|
+
const empties = new Map()
|
|
146
161
|
|
|
147
162
|
const hooks = {}
|
|
148
163
|
|
|
@@ -166,7 +181,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
166
181
|
},
|
|
167
182
|
async execute(args) {
|
|
168
183
|
const limit = String(clampLimit(args.limit))
|
|
169
|
-
return answer(await ask(
|
|
184
|
+
return answer(await ask(argv("search", ["--json", "--limit", limit], args.query)))
|
|
170
185
|
},
|
|
171
186
|
}),
|
|
172
187
|
deja_session: tool({
|
|
@@ -176,7 +191,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
176
191
|
query: schema.string().describe("A query, or a session id prefix returned by deja_recall."),
|
|
177
192
|
},
|
|
178
193
|
async execute(args) {
|
|
179
|
-
return answer(await ask(
|
|
194
|
+
return answer(await ask(argv("ctx", [], args.query)))
|
|
180
195
|
},
|
|
181
196
|
}),
|
|
182
197
|
deja_blame: tool({
|
|
@@ -186,7 +201,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
186
201
|
path: schema.string().describe("Path to the file, absolute or relative to the project."),
|
|
187
202
|
},
|
|
188
203
|
async execute(args) {
|
|
189
|
-
return answer(await ask(
|
|
204
|
+
return answer(await ask(argv("blame", ["--json"], args.path)))
|
|
190
205
|
},
|
|
191
206
|
}),
|
|
192
207
|
deja_fix: tool({
|
|
@@ -196,7 +211,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
196
211
|
error: schema.string().describe("The failing output, copied as it was printed."),
|
|
197
212
|
},
|
|
198
213
|
async execute(args) {
|
|
199
|
-
return answer(await ask(
|
|
214
|
+
return answer(await ask(argv("fix", [], args.error)))
|
|
200
215
|
},
|
|
201
216
|
}),
|
|
202
217
|
deja_how: tool({
|
|
@@ -206,7 +221,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
206
221
|
what: schema.string().describe("The thing to run: a tool, a task, a script name."),
|
|
207
222
|
},
|
|
208
223
|
async execute(args) {
|
|
209
|
-
return answer(await ask(
|
|
224
|
+
return answer(await ask(argv("how", [], args.what)))
|
|
210
225
|
},
|
|
211
226
|
}),
|
|
212
227
|
deja_remember: tool({
|
|
@@ -216,7 +231,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
216
231
|
text: schema.string().describe("The decision, in one or two sentences, with the reason it was taken."),
|
|
217
232
|
},
|
|
218
233
|
async execute(args) {
|
|
219
|
-
const written = await ask(
|
|
234
|
+
const written = await ask(argv("remember", [], args.text))
|
|
220
235
|
if (!installed) return MISSING
|
|
221
236
|
return written || "deja did not record that."
|
|
222
237
|
},
|
|
@@ -250,9 +265,13 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
250
265
|
output.system.push(context)
|
|
251
266
|
return
|
|
252
267
|
}
|
|
253
|
-
// Nothing recalled:
|
|
254
|
-
//
|
|
255
|
-
// drops the empty answer so the
|
|
268
|
+
// Nothing recalled: this machine has no history yet, the first index is
|
|
269
|
+
// still building, or the call did not get through. The build is the only
|
|
270
|
+
// one worth saying out loud, and saying it drops the empty answer so the
|
|
271
|
+
// next turn asks again.
|
|
272
|
+
const asks = (empties.get(key) || 0) + 1
|
|
273
|
+
empties.set(key, asks)
|
|
274
|
+
if (asks < emptyRetries) digests.delete(key)
|
|
256
275
|
if (told.has(key)) return
|
|
257
276
|
const status = await ask(["warmup-status"], undefined, 5000)
|
|
258
277
|
if (!status) return
|
|
@@ -269,11 +288,15 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
269
288
|
// Per-prompt recall, the relevance pass Claude Code gets on
|
|
270
289
|
// UserPromptSubmit: the digest above is ranked by the project, this is ranked
|
|
271
290
|
// by what the user just asked. Silent when nothing matches.
|
|
272
|
-
hooks["experimental.chat.messages.transform"] = async (
|
|
291
|
+
hooks["experimental.chat.messages.transform"] = async (input, output) => {
|
|
273
292
|
try {
|
|
274
|
-
const { parts, prompt } = lastUserText(output?.messages)
|
|
293
|
+
const { parts, prompt, sessionID } = lastUserText(output?.messages)
|
|
275
294
|
if (!prompt) return
|
|
276
|
-
|
|
295
|
+
// The session id lets recall skip what it already showed this session.
|
|
296
|
+
// Without it every message re-injects the same block — measured on a real
|
|
297
|
+
// store, half of all injections were a word-for-word repeat.
|
|
298
|
+
const key = input?.sessionID || sessionID || ""
|
|
299
|
+
const raw = await ask(["hook-prompt"], JSON.stringify({ prompt, session_id: key, cwd }))
|
|
277
300
|
if (!raw) return
|
|
278
301
|
const extra = JSON.parse(raw)?.hookSpecificOutput?.additionalContext
|
|
279
302
|
if (!extra) return
|
|
@@ -293,5 +316,56 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
293
316
|
}
|
|
294
317
|
}
|
|
295
318
|
|
|
319
|
+
// A spawned agent gets none of the above: the system prompt was built for the
|
|
320
|
+
// session that spawned it, and the per-prompt pass fires on what the user
|
|
321
|
+
// typed, which a subagent never does. Its instructions are the one thing that
|
|
322
|
+
// reaches it, so recall goes in there.
|
|
323
|
+
hooks["tool.execute.before"] = async (input, output) => {
|
|
324
|
+
try {
|
|
325
|
+
if (input?.tool !== "task") return
|
|
326
|
+
const args = output?.args
|
|
327
|
+
if (!args?.prompt) return
|
|
328
|
+
const payload = {
|
|
329
|
+
hook_event_name: "PreToolUse",
|
|
330
|
+
tool_name: "Task",
|
|
331
|
+
tool_input: { prompt: args.prompt },
|
|
332
|
+
session_id: input.sessionID || "",
|
|
333
|
+
cwd,
|
|
334
|
+
}
|
|
335
|
+
const raw = await ask(["hook-tool"], JSON.stringify(payload))
|
|
336
|
+
if (!raw) return
|
|
337
|
+
const next = JSON.parse(raw)?.hookSpecificOutput?.updatedInput?.prompt
|
|
338
|
+
if (next) args.prompt = next
|
|
339
|
+
} catch {
|
|
340
|
+
// memory is optional: never break a spawn over it
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// The moment a command fails is the one an agent never thinks to ask about,
|
|
345
|
+
// and tool.execute.after is the only seam opencode gives for it. The hook
|
|
346
|
+
// returns nothing to inject, so the line is folded into the tool output,
|
|
347
|
+
// which the next request carries as the tool result.
|
|
348
|
+
hooks["tool.execute.after"] = async (input, output) => {
|
|
349
|
+
try {
|
|
350
|
+
if (input?.tool !== "bash") return
|
|
351
|
+
const text = output?.output
|
|
352
|
+
if (!text) return
|
|
353
|
+
const payload = {
|
|
354
|
+
hook_event_name: "PostToolUse",
|
|
355
|
+
tool_name: "bash",
|
|
356
|
+
tool_input: { command: input?.args?.command || "" },
|
|
357
|
+
tool_response: { output: text },
|
|
358
|
+
session_id: input.sessionID || "",
|
|
359
|
+
cwd,
|
|
360
|
+
}
|
|
361
|
+
const raw = await ask(["hook-tool-after"], JSON.stringify(payload))
|
|
362
|
+
if (!raw) return
|
|
363
|
+
const extra = JSON.parse(raw)?.hookSpecificOutput?.additionalContext
|
|
364
|
+
if (extra) output.output = text + "\n\n" + extra
|
|
365
|
+
} catch {
|
|
366
|
+
// memory is optional: never break a tool call over it
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
296
370
|
return hooks
|
|
297
371
|
}
|
package/lib.js
CHANGED
|
@@ -29,11 +29,12 @@ export function lastUserText(messages) {
|
|
|
29
29
|
const list = Array.isArray(messages) ? messages : []
|
|
30
30
|
for (let i = list.length - 1; i >= 0; i--) {
|
|
31
31
|
if (list[i]?.info?.role !== "user") continue
|
|
32
|
+
const sessionID = list[i].info?.sessionID || ""
|
|
32
33
|
const parts = (list[i].parts || []).filter((p) => p?.type === "text" && p.text)
|
|
33
|
-
if (!parts.length) return { parts: [], prompt: "" }
|
|
34
|
-
return { parts, prompt: parts.map((p) => p.text).join("\n").trim() }
|
|
34
|
+
if (!parts.length) return { parts: [], prompt: "", sessionID }
|
|
35
|
+
return { parts, prompt: parts.map((p) => p.text).join("\n").trim(), sessionID }
|
|
35
36
|
}
|
|
36
|
-
return { parts: [], prompt: "" }
|
|
37
|
+
return { parts: [], prompt: "", sessionID: "" }
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
// cliPluginPath is where `deja install opencode-auto` writes its own plugin.
|
|
@@ -118,6 +119,22 @@ export function contributions(wiring, config = {}) {
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// argv builds the call for a query somebody typed, rather than handing the text
|
|
123
|
+
// to deja as it stands.
|
|
124
|
+
//
|
|
125
|
+
// A query that starts with a dash is read as a flag: `deja search --no-verify`
|
|
126
|
+
// exits on an unknown flag, run() turns a failed call into "", and the model is
|
|
127
|
+
// told the history holds nothing about --no-verify while the sessions that
|
|
128
|
+
// discuss it sit right there. A wrong answer, where an error would at least be
|
|
129
|
+
// visible. `--` ends the flags.
|
|
130
|
+
//
|
|
131
|
+
// Sent only when the query needs it, so a deja too old to know the terminator
|
|
132
|
+
// on this subcommand still answers every ordinary query.
|
|
133
|
+
export function argv(cmd, flags, text) {
|
|
134
|
+
const arg = String(text)
|
|
135
|
+
return arg.startsWith("-") ? [cmd, ...flags, "--", arg] : [cmd, ...flags, arg]
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
// clampLimit keeps a model that asks for a hundred sessions from spending the
|
|
122
139
|
// window on a tail nobody reads.
|
|
123
140
|
export function clampLimit(value, fallback = 5) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-deja",
|
|
3
|
-
"version": "0.19.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.19.3",
|
|
4
|
+
"description": "deja-vu memory for opencode: the session history of twenty-one other coding agents, searchable and recalled on every request — a local index, no LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@opencode-ai/plugin": ">=1.0.0",
|
|
40
|
-
"@vshulcz/deja-vu": "^0.19.
|
|
40
|
+
"@vshulcz/deja-vu": "^0.19.3"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"test": "node --test test/*.test.js"
|