opencode-deja 0.1.0 → 0.1.2

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.
Files changed (4) hide show
  1. package/README.md +9 -2
  2. package/index.js +41 -4
  3. package/lib.js +84 -0
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -20,6 +20,12 @@ embeddings, nothing leaves the machine.
20
20
  opencode installs the package on start. The deja binary comes with it, but a
21
21
  deja you installed yourself always wins — see [Which binary](#which-binary).
22
22
 
23
+ `deja install --auto` wires opencode too, and is the shorter path if you have
24
+ the CLI: it adds deja's MCP server and writes a plugin of its own. Having both
25
+ is fine. This package reads what the installer left — the MCP entry in
26
+ `opencode.json`, the plugin file in `plugins/deja.js` — and contributes only
27
+ what is missing, so nothing is registered or recalled twice.
28
+
23
29
  ## What you get
24
30
 
25
31
  Six tools the model can call:
@@ -51,8 +57,9 @@ And recall nobody has to ask for:
51
57
 
52
58
  - `autoRecall` (default `true`) — the three hooks above. Turn off to keep only
53
59
  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.
60
+ - `tools` (default `true`) — the six tools. They are skipped on their own when
61
+ `deja install` already wired the MCP server; set this to `false` to drop them
62
+ when you reach deja some other way.
56
63
  - `bin` — path to a specific deja binary.
57
64
 
58
65
  ## Which binary
package/index.js CHANGED
@@ -11,9 +11,17 @@ import { execFile } from "node:child_process"
11
11
  import { promisify } from "node:util"
12
12
  import { homedir } from "node:os"
13
13
  import { join } from "node:path"
14
- import { accessSync, constants } from "node:fs"
14
+ import { accessSync, constants, existsSync, readFileSync } from "node:fs"
15
15
 
16
- import { clampLimit, contextText, lastUserText } from "./lib.js"
16
+ import {
17
+ clampLimit,
18
+ cliPluginPath,
19
+ configPaths,
20
+ contextText,
21
+ contributions,
22
+ lastUserText,
23
+ mcpWired,
24
+ } from "./lib.js"
17
25
 
18
26
  const require = createRequire(import.meta.url)
19
27
  const run_ = promisify(execFile)
@@ -99,6 +107,27 @@ const MISSING =
99
107
  "deja is not installed on this machine, so there is no history to search. " +
100
108
  "Install it with: curl -fsSL https://raw.githubusercontent.com/vshulcz/deja-vu/main/install.sh | sh"
101
109
 
110
+ // installerWiring reads what `deja install` left on disk for opencode: the MCP
111
+ // server in the config, and the plugin file `--auto` writes. Both reads are
112
+ // wrapped — a config we cannot read means "not wired", so the worst case is
113
+ // this package doing the work twice rather than not at all.
114
+ function installerWiring() {
115
+ const wiring = { mcp: false, recall: false }
116
+ try {
117
+ wiring.recall = existsSync(cliPluginPath(process.env, homedir()))
118
+ } catch {}
119
+ try {
120
+ for (const path of configPaths(process.env, homedir())) {
121
+ if (!existsSync(path)) continue
122
+ if (mcpWired(readFileSync(path, "utf8"))) {
123
+ wiring.mcp = true
124
+ break
125
+ }
126
+ }
127
+ } catch {}
128
+ return wiring
129
+ }
130
+
102
131
  export const DejaPlugin = async ({ client, directory }, options = {}) => {
103
132
  const config = options || {}
104
133
  const bin = resolveDeja(typeof config.bin === "string" ? config.bin : "")
@@ -117,7 +146,15 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
117
146
 
118
147
  const hooks = {}
119
148
 
120
- if (config.tools !== false && tool) {
149
+ // What `deja install` already wired for opencode is not repeated here. It
150
+ // writes an MCP server (the same six answers, under their own names) and,
151
+ // with --auto, a plugin file of its own that opencode loads beside this
152
+ // package — verified with `opencode debug config`, which resolves both into
153
+ // one plugin list. Whatever the installer wrote wins, because that is the
154
+ // copy `deja install` keeps current; this package fills the gaps.
155
+ const adds = contributions(installerWiring(), config)
156
+
157
+ if (adds.tools && tool) {
121
158
  const schema = tool.schema
122
159
  hooks.tool = {
123
160
  deja_recall: tool({
@@ -187,7 +224,7 @@ export const DejaPlugin = async ({ client, directory }, options = {}) => {
187
224
  }
188
225
  }
189
226
 
190
- if (config.autoRecall === false) return hooks
227
+ if (!adds.recall) return hooks
191
228
 
192
229
  // opencode has no session-start hook. The system prompt is assembled on
193
230
  // every request, so the session digest is fetched once and pushed there —
package/lib.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { join } from "node:path"
2
+
1
3
  // The pure half of the plugin: everything that reads deja's output or
2
4
  // opencode's message shape, with no process and no host. Kept apart from
3
5
  // index.js because opencode loads every function a plugin module exports —
@@ -34,6 +36,88 @@ export function lastUserText(messages) {
34
36
  return { parts: [], prompt: "" }
35
37
  }
36
38
 
39
+ // cliPluginPath is where `deja install opencode-auto` writes its own plugin.
40
+ // opencode loads that file and this package side by side — both are entries in
41
+ // the resolved `plugin` list — and both push recall onto the system prompt, so
42
+ // whoever finds the other has to stand down. Mirrors opencodeConfigHome() in
43
+ // the installer: XDG_CONFIG_HOME wins, else ~/.config.
44
+ export function cliPluginPath(env, home) {
45
+ const base = (env && env.XDG_CONFIG_HOME) || join(home || "", ".config")
46
+ return join(base, "opencode", "plugins", "deja.js")
47
+ }
48
+
49
+ // configPaths are the two names opencode accepts for the global config, in the
50
+ // order `deja install opencode` looks for them.
51
+ export function configPaths(env, home) {
52
+ const base = (env && env.XDG_CONFIG_HOME) || join(home || "", ".config")
53
+ return [join(base, "opencode", "opencode.json"), join(base, "opencode", "opencode.jsonc")]
54
+ }
55
+
56
+ // mcpWired reports whether that config already runs deja as an MCP server,
57
+ // which is what `deja install opencode` writes. The tools this package
58
+ // registers do the same six things, so when the server is there they are a
59
+ // second copy of it in the model's tool list.
60
+ //
61
+ // The file is JSONC — opencode ships it with comments — so comments come out
62
+ // before the parse. A config this cannot read is treated as not wired: losing
63
+ // the tools on a file we misread is worse than listing them twice.
64
+ export function mcpWired(text) {
65
+ try {
66
+ const config = JSON.parse(stripJSONComments(String(text || "")))
67
+ return Boolean(config && config.mcp && config.mcp.deja)
68
+ } catch {
69
+ return false
70
+ }
71
+ }
72
+
73
+ // stripJSONComments removes // and /* */ outside strings. Small on purpose: it
74
+ // only has to survive the file our own installer writes.
75
+ export function stripJSONComments(text) {
76
+ let out = ""
77
+ let inString = false
78
+ for (let i = 0; i < text.length; i++) {
79
+ const c = text[i]
80
+ if (inString) {
81
+ out += c
82
+ if (c === "\\") {
83
+ out += text[++i] || ""
84
+ } else if (c === '"') {
85
+ inString = false
86
+ }
87
+ continue
88
+ }
89
+ if (c === '"') {
90
+ inString = true
91
+ out += c
92
+ continue
93
+ }
94
+ if (c === "/" && text[i + 1] === "/") {
95
+ while (i < text.length && text[i] !== "\n") i++
96
+ out += "\n"
97
+ continue
98
+ }
99
+ if (c === "/" && text[i + 1] === "*") {
100
+ i += 2
101
+ while (i + 1 < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++
102
+ i++
103
+ continue
104
+ }
105
+ out += c
106
+ }
107
+ return out
108
+ }
109
+
110
+ // contributions decides what this package adds, given what `deja install`
111
+ // already wired and what the user turned off in their config. It is the whole
112
+ // rule in one place: fill the gaps, never repeat the installer.
113
+ export function contributions(wiring, config = {}) {
114
+ const wired = wiring || {}
115
+ return {
116
+ tools: config.tools !== false && !wired.mcp,
117
+ recall: config.autoRecall !== false && !wired.recall,
118
+ }
119
+ }
120
+
37
121
  // clampLimit keeps a model that asks for a hundred sessions from spending the
38
122
  // window on a tail nobody reads.
39
123
  export function clampLimit(value, fallback = 5) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-deja",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
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
5
  "type": "module",
6
6
  "main": "index.js",