opencode-deja 0.21.7 → 0.22.1
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 -3
- package/index.js +37 -19
- package/lib.js +33 -13
- package/package.json +3 -3
- package/server.js +6 -7
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
English | [中文](https://github.com/vshulcz/deja-vu/blob/main/extensions/opencode/docs/zh.md)
|
|
4
4
|
|
|
5
5
|
opencode remembers its own sessions. This plugin answers the other question:
|
|
6
|
-
what you did in the
|
|
6
|
+
what you did in the forty other coding agents on this machine — Claude Code,
|
|
7
7
|
Codex, Cursor, Gemini and Zed among them — including the months before you
|
|
8
8
|
installed anything.
|
|
9
9
|
|
|
10
10
|
It runs [deja](https://github.com/vshulcz/deja-vu), a local Go binary that
|
|
11
11
|
indexes the transcripts those agents already wrote to disk. No LLM, no
|
|
12
|
-
embeddings, no network path
|
|
12
|
+
embeddings, no network path besides a daily release check, which DEJA_OFFLINE=1 turns off.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -92,7 +92,7 @@ On 2.x:
|
|
|
92
92
|
In order: `bin` from the options, `DEJA_BIN`, a `deja` on `PATH`, the usual
|
|
93
93
|
install locations (`~/.local/bin`, `/usr/local/bin`, `/opt/homebrew/bin`), and
|
|
94
94
|
only then the copy npm installed with this package. Your own `deja update` or
|
|
95
|
-
`brew upgrade deja` wins over whatever version this package shipped with.
|
|
95
|
+
`brew upgrade deja-vu` wins over whatever version this package shipped with.
|
|
96
96
|
|
|
97
97
|
Without deja anywhere, the tools say so instead of reporting an empty history,
|
|
98
98
|
and the hooks stay quiet.
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// opencode-deja — the sessions you already have, inside opencode.
|
|
2
2
|
//
|
|
3
3
|
// opencode remembers its own sessions. This plugin answers the other question:
|
|
4
|
-
// what you did in Claude Code, Codex, Cursor, Gemini and thirty-
|
|
4
|
+
// what you did in Claude Code, Codex, Cursor, Gemini and thirty-seven more agents on
|
|
5
5
|
// this machine, including months before deja existed. The index is deja's; this
|
|
6
6
|
// file is the seam: six tools the model can call, plus recall that arrives
|
|
7
7
|
// without being asked for.
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
configPaths,
|
|
24
24
|
contextText,
|
|
25
25
|
contributions,
|
|
26
|
-
lastUserText,
|
|
27
26
|
mcpWired,
|
|
28
27
|
TOOL_SPECS,
|
|
28
|
+
userTurns,
|
|
29
29
|
zodTools,
|
|
30
30
|
} from "./lib.js"
|
|
31
31
|
|
|
@@ -172,6 +172,12 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
172
172
|
// session ended, so without ending them here the stamp sat out its whole
|
|
173
173
|
// window and the next session's MCP recall left a finished one out (#4546).
|
|
174
174
|
const live = new Set()
|
|
175
|
+
// The per-prompt recall each user message was given, by message.
|
|
176
|
+
const recalled = new Map()
|
|
177
|
+
const remember = (key, extra) => {
|
|
178
|
+
recalled.set(key, extra)
|
|
179
|
+
if (recalled.size > 1000) recalled.delete(recalled.keys().next().value)
|
|
180
|
+
}
|
|
175
181
|
const endSession = (id) => ask(["hook-session-end"], JSON.stringify({ session_id: id }), 5000)
|
|
176
182
|
// The session that spawned each one, asked of opencode once. A task
|
|
177
183
|
// sub-agent's digest and recall led with its parent, which is live and
|
|
@@ -206,7 +212,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
206
212
|
const specs = {
|
|
207
213
|
deja_recall: {
|
|
208
214
|
description:
|
|
209
|
-
"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, and before
|
|
215
|
+
"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, and before the first edit in a task and before calling a change done or ready to merge. Match on the most specific token available: an exact error string, function name, file path or flag.",
|
|
210
216
|
args: {
|
|
211
217
|
query: { type: "string", description: "Specific tokens to match. Several words are ANDed." },
|
|
212
218
|
limit: { type: "number", optional: true, description: "How many sessions to return. Default 5." },
|
|
@@ -348,22 +354,34 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
|
|
|
348
354
|
// the recovery packet would count as delivered to a turn that never saw it.
|
|
349
355
|
const owner = (output?.messages || []).find((m) => m?.info?.sessionID)?.info?.sessionID
|
|
350
356
|
if (owner && compacting.has(owner)) return
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
357
|
+
const users = userTurns(output?.messages)
|
|
358
|
+
const last = users[users.length - 1]
|
|
359
|
+
if (!last) return
|
|
360
|
+
// Asked once per message. opencode builds every model call afresh from
|
|
361
|
+
// its store, so text added to a message here lasts one call: once the
|
|
362
|
+
// agent ran a tool the recall was gone, and deja, having shown it, would
|
|
363
|
+
// not send it again (#4786).
|
|
364
|
+
if (!recalled.has(last.key)) {
|
|
365
|
+
// A turn with no text, an image alone, still goes to hook-prompt: that
|
|
366
|
+
// call is what stamps the session live again after session.idle ended
|
|
367
|
+
// it, and without it the turn's MCP recall could return the session
|
|
368
|
+
// to itself (#4573).
|
|
369
|
+
// The session id lets recall skip what it already showed this
|
|
370
|
+
// session. Without it every message re-injects the same block —
|
|
371
|
+
// measured on a real store, half of all injections were a
|
|
372
|
+
// word-for-word repeat.
|
|
373
|
+
const key = input?.sessionID || last.sessionID || ""
|
|
374
|
+
if (key) live.add(key)
|
|
375
|
+
const raw = await ask(["hook-prompt"], JSON.stringify({ prompt: last.prompt, session_id: key, parent_session_id: await parentOf(key), cwd }))
|
|
376
|
+
remember(last.key, last.prompt && raw ? JSON.parse(raw)?.hookSpecificOutput?.additionalContext || "" : "")
|
|
377
|
+
}
|
|
378
|
+
// Every message gets back what it was given, in every call, the way
|
|
379
|
+
// Claude Code keeps a prompt hook's text in its transcript.
|
|
380
|
+
for (const u of users) {
|
|
381
|
+
const extra = recalled.get(u.key)
|
|
382
|
+
const tail = u.parts[u.parts.length - 1]
|
|
383
|
+
if (extra && tail && !tail.text.endsWith(extra)) tail.text += "\n\n" + extra
|
|
384
|
+
}
|
|
367
385
|
} catch {
|
|
368
386
|
// memory is optional: never break the session over it
|
|
369
387
|
}
|
package/lib.js
CHANGED
|
@@ -22,19 +22,21 @@ export function contextText(raw) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
25
|
+
// userTurns is every user message in a request, oldest first, each with a key
|
|
26
|
+
// that stays the same from one request to the next: opencode's message id, or
|
|
27
|
+
// where a shape carries none, the message's place among the user messages and
|
|
28
|
+
// its text.
|
|
29
|
+
export function userTurns(messages) {
|
|
29
30
|
const list = Array.isArray(messages) ? messages : []
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
const out = []
|
|
32
|
+
for (const m of list) {
|
|
33
|
+
if (m?.info?.role !== "user") continue
|
|
34
|
+
const sessionID = m.info?.sessionID || ""
|
|
35
|
+
const parts = (m.parts || []).filter((p) => p?.type === "text" && p.text)
|
|
36
|
+
const prompt = parts.map((p) => p.text).join("\n").trim()
|
|
37
|
+
out.push({ key: m.info?.id || sessionID + "#" + out.length + "#" + prompt, parts, prompt, sessionID })
|
|
36
38
|
}
|
|
37
|
-
return
|
|
39
|
+
return out
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
// cliPluginPath is where `deja install opencode-auto` writes its own plugin.
|
|
@@ -195,13 +197,13 @@ export function turnEnded(event) {
|
|
|
195
197
|
return ""
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
// v1Messages shows 2.x's request messages in the 1.x shape
|
|
200
|
+
// v1Messages shows 2.x's request messages in the 1.x shape userTurns reads.
|
|
199
201
|
// The parts are 2.x's own content objects, so text appended to one lands in
|
|
200
202
|
// the request.
|
|
201
203
|
export function v1Messages(messages, sessionID) {
|
|
202
204
|
const list = Array.isArray(messages) ? messages : []
|
|
203
205
|
return list.map((m) => ({
|
|
204
|
-
info: { role: m?.role, sessionID: sessionID || "" },
|
|
206
|
+
info: { id: m?.id, role: m?.role, sessionID: sessionID || "" },
|
|
205
207
|
parts: Array.isArray(m?.content) ? m.content : [],
|
|
206
208
|
}))
|
|
207
209
|
}
|
|
@@ -220,6 +222,24 @@ export function resultText(result) {
|
|
|
220
222
|
return typeof r.output === "string" ? r.output : ""
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
// v1Client is the slice of the 1.x client the plugin reads, over 2.x's ctx. A
|
|
226
|
+
// 2.x server plugin has no channel to the TUI, so the toasts go nowhere. The
|
|
227
|
+
// session lookup is what names a sub-agent's parent: without it the
|
|
228
|
+
// sub-agent's digest and recall led with the parent that spawned it (#4548).
|
|
229
|
+
// 1.x answers through client.session.get, 2.x through ctx.session.get, whose
|
|
230
|
+
// SessionInfo carries parentID.
|
|
231
|
+
export function v1Client(ctx) {
|
|
232
|
+
return {
|
|
233
|
+
tui: { showToast: async () => {} },
|
|
234
|
+
session: {
|
|
235
|
+
get: async ({ path } = {}) => {
|
|
236
|
+
if (!path?.id || typeof ctx?.session?.get !== "function") return { data: {} }
|
|
237
|
+
return { data: (await ctx.session.get({ sessionID: path.id })) || {} }
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
223
243
|
// v1ToolName is the 1.x name for a 2.x tool, where the two differ in a way the
|
|
224
244
|
// hooks look at: the spawn tool and the shell.
|
|
225
245
|
export function v1ToolName(name) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-deja",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "deja-vu memory for opencode: the session history of
|
|
3
|
+
"version": "0.22.1",
|
|
4
|
+
"description": "deja-vu memory for opencode: the session history of forty 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": {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@opencode-ai/plugin": ">=1.0.0",
|
|
55
|
-
"@vshulcz/deja-vu": "^0.
|
|
55
|
+
"@vshulcz/deja-vu": "^0.22.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"test": "node --test test/*.test.js"
|
package/server.js
CHANGED
|
@@ -15,14 +15,11 @@
|
|
|
15
15
|
// a turn on `event` "subscribe" and dispose on the cleanup setup returns.
|
|
16
16
|
|
|
17
17
|
import { DejaPlugin } from "./index.js"
|
|
18
|
-
import { jsonSchema, resultText, TOOL_SPECS, turnEnded, v1Messages, v1ToolName } from "./lib.js"
|
|
19
|
-
|
|
20
|
-
// A 2.x server plugin has no channel to the TUI, so the toasts go nowhere.
|
|
21
|
-
const noToast = { tui: { showToast: async () => {} } }
|
|
18
|
+
import { jsonSchema, resultText, TOOL_SPECS, turnEnded, v1Client, v1Messages, v1ToolName } from "./lib.js"
|
|
22
19
|
|
|
23
20
|
async function setup(ctx) {
|
|
24
21
|
const directory = ctx.location?.directory || process.cwd()
|
|
25
|
-
const hooks = await DejaPlugin({ client:
|
|
22
|
+
const hooks = await DejaPlugin({ client: v1Client(ctx), directory }, ctx.options || {})
|
|
26
23
|
|
|
27
24
|
const specs = hooks[TOOL_SPECS]
|
|
28
25
|
if (specs) {
|
|
@@ -66,8 +63,10 @@ async function setup(ctx) {
|
|
|
66
63
|
|
|
67
64
|
const compacting = hooks["experimental.session.compacting"]
|
|
68
65
|
if (compacting) {
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
// The 1.x hook reads the session from its input; called with none, 2.x
|
|
67
|
+
// sent hook-precompact an empty id and nothing was read or forgotten.
|
|
68
|
+
await ctx.session.hook("compaction", async (event) => {
|
|
69
|
+
await compacting({ sessionID: event?.sessionID || "" })
|
|
71
70
|
})
|
|
72
71
|
}
|
|
73
72
|
|