@vshulcz/pi-deja 0.19.4 → 0.20.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.
Files changed (4) hide show
  1. package/README.md +8 -2
  2. package/index.ts +80 -2
  3. package/lib.mjs +21 -2
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @vshulcz/pi-deja
2
2
 
3
3
  pi remembers its own sessions. This extension answers the other question: what
4
- was done in the twenty-two other coding agents on this machine — Claude Code,
4
+ was done in the twenty-four other coding agents on this machine — Claude Code,
5
5
  Codex, Cursor, Gemini, OpenClaw and Hermes among them — including the months
6
6
  before pi was installed.
7
7
 
@@ -21,7 +21,7 @@ The deja binary comes with the package; a deja you installed yourself
21
21
  `deja install pi-auto` wires pi too, and is the shorter path if you have the
22
22
  CLI: it adds deja's MCP server and writes an extension of its own. When that
23
23
  extension is present this package stands down, so having both never injects
24
- twice.
24
+ twice — and the same for omp, whose extension the installer writes elsewhere.
25
25
 
26
26
  omp loads `pi.extensions` manifests and emits the same `before_agent_start`
27
27
  event, so `pi install`'s omp counterpart picks this package up as well.
@@ -33,6 +33,12 @@ event, so `pi install`'s omp counterpart picks this package up as well.
33
33
  - **Every prompt** (`before_agent_start`): the prompt is matched against the
34
34
  index and, when a past session answers it, that session goes to the model
35
35
  with the prompt. Silence is the common case.
36
+ - **After a failed command** (`tool_result`): the repair this machine used the
37
+ last time that error came up goes in beside the error, in the same turn.
38
+ - **Before an edit** (`tool_result` on a read): what earlier sessions decided
39
+ about the file the agent just opened.
40
+ - **After a compaction** (`session_compact`): the list of what this session was
41
+ already shown is cleared, so the recall it lost can come back.
36
42
  - **`/deja <query>`**: search the history by hand.
37
43
 
38
44
  ## Which binary
package/index.ts CHANGED
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
6
6
  import { homedir } from "node:os";
7
7
  import { join } from "node:path";
8
8
 
9
- import { argv, contextText, installerExtensionPath, sessionKey } from "./lib.mjs";
9
+ import { argv, contextText, installerExtensionPaths, sessionKey } from "./lib.mjs";
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
 
@@ -72,11 +72,16 @@ export default function (pi: any) {
72
72
  // it already does everything below, and two copies would inject the
73
73
  // recall twice and register /deja twice.
74
74
  try {
75
- if (existsSync(installerExtensionPath(homedir()))) return;
75
+ if (installerExtensionPaths(homedir()).some((p) => existsSync(p))) return;
76
76
  } catch {}
77
77
 
78
78
  let injected = false;
79
79
  let toldBuilding = false;
80
+ // The tool and compaction events carry no session id of their own, and
81
+ // recall dedupes per session: without one it repeats itself and forgets
82
+ // nothing. The prompt handler passes through first and pi keeps the id on
83
+ // the session manager, so it is read there and held.
84
+ let session = "";
80
85
 
81
86
  // pi keeps a footer status line: while the first index builds, that is
82
87
  // where the user can see it happening instead of wondering why recall is
@@ -141,6 +146,7 @@ export default function (pi: any) {
141
146
  return;
142
147
  }
143
148
  const key = sessionKey(event, ctx);
149
+ if (key) session = key;
144
150
  const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: key }));
145
151
  if (!raw) return;
146
152
  const resp = JSON.parse(raw);
@@ -152,4 +158,76 @@ export default function (pi: any) {
152
158
  // memory is optional: never break the session over it
153
159
  }
154
160
  });
161
+
162
+ // The other half of the point of action, and the half this package was
163
+ // missing: a command has just failed, and this machine has fixed that same
164
+ // error before. pi hands tool_result the content the model is about to read
165
+ // and uses what the handler returns, so the repair goes in beside the error
166
+ // in the same turn. The installer's own extension has carried this since
167
+ // #3112/#3113; anyone who installed from npm had neither it nor the
168
+ // compaction handler below (#3180).
169
+ const repaired: Record<string, string> = {};
170
+ pi.on("tool_result", async (event: any, ctx: any) => {
171
+ try {
172
+ if (!event) return;
173
+ const key = sessionKey(event, ctx);
174
+ if (key) session = key;
175
+ // A file the agent just opened is the step before it changes that file,
176
+ // and pi has no handler that runs earlier whose return the model reads.
177
+ // So the file's own history goes out here: what was decided about it,
178
+ // from the sessions that decided it. deja answers once per session per
179
+ // fact, so re-reading the same file stays quiet.
180
+ if (event.toolName === "read") {
181
+ const path = String((event.input && (event.input.path || event.input.file_path)) || "");
182
+ if (!path) return;
183
+ const parts = Array.isArray(event.content) ? event.content : [];
184
+ const id = "read:" + String(event.toolCallId || "");
185
+ if (!(id in repaired)) {
186
+ repaired[id] = run(["hook-tool", "--plain"], JSON.stringify({
187
+ tool_name: "read",
188
+ tool_input: { file_path: path },
189
+ session_id: session,
190
+ cwd: process.cwd(),
191
+ }));
192
+ }
193
+ const note = repaired[id];
194
+ if (!note) return;
195
+ return { content: parts.concat([{ type: "text", text: note }]) };
196
+ }
197
+ if (!event.isError) return;
198
+ if (event.toolName !== "bash") return;
199
+ const parts = Array.isArray(event.content) ? event.content : [];
200
+ const output = parts
201
+ .filter((p: any) => p && p.type === "text" && typeof p.text === "string")
202
+ .map((p: any) => p.text)
203
+ .join("\n");
204
+ if (!output.trim()) return;
205
+ const id = String(event.toolCallId || "");
206
+ if (!(id in repaired)) {
207
+ repaired[id] = run(["hook-tool-after", "--plain"], JSON.stringify({
208
+ tool_name: "bash",
209
+ tool_response: output,
210
+ session_id: session,
211
+ cwd: process.cwd(),
212
+ }));
213
+ }
214
+ const line = repaired[id];
215
+ if (!line) return;
216
+ return { content: parts.concat([{ type: "text", text: line }]) };
217
+ } catch {
218
+ // never turn a failed command into a failed session
219
+ }
220
+ });
221
+
222
+ // Compaction throws away the blocks this session was shown, and the list
223
+ // that stops them repeating outlives them. session_compact fires once the
224
+ // summary has replaced the history, which is exactly when the session should
225
+ // be allowed to see those blocks again.
226
+ pi.on("session_compact", async (_event: any, ctx: any) => {
227
+ try {
228
+ const key = sessionKey(_event, ctx);
229
+ if (key) session = key;
230
+ run(["hook-precompact"], JSON.stringify({ session_id: session }));
231
+ } catch {}
232
+ });
155
233
  }
package/lib.mjs CHANGED
@@ -13,6 +13,17 @@ export function installerExtensionPath(home) {
13
13
  return join(home || "", ".pi", "agent", "extensions", "deja.ts")
14
14
  }
15
15
 
16
+ // installerExtensionPaths are every place `deja install` writes an extension
17
+ // this package would duplicate. omp runs the same package and the installer
18
+ // writes it under ~/.omp/agent/extensions/deja/index.js, so checking pi's path
19
+ // alone left both running side by side (#3180).
20
+ export function installerExtensionPaths(home) {
21
+ return [
22
+ installerExtensionPath(home),
23
+ join(home || "", ".omp", "agent", "extensions", "deja", "index.js"),
24
+ ]
25
+ }
26
+
16
27
  // contextText pulls the recall out of whatever hook-context printed. deja
17
28
  // answers in the Claude Code hook shape; older builds and `--plain` print
18
29
  // bare text, so both are accepted.
@@ -40,8 +51,16 @@ export function argv(cmd, flags, text) {
40
51
  }
41
52
 
42
53
  // sessionKey is what per-prompt recall dedups on: a hit shown once in a
43
- // session is not shown again. Without one it repeats itself every message.
44
- // Same order as the extension `deja install pi-auto` writes.
54
+ // session is not shown again. Without one it repeats itself every message —
55
+ // which it did, because pi keeps the id on ctx.sessionManager and nowhere the
56
+ // event or ctx.session shapes below look (#3179). The manager first, the same
57
+ // order as the extension `deja install pi-auto` writes; the rest for hosts
58
+ // that put an id on the event.
45
59
  export function sessionKey(event, ctx) {
60
+ try {
61
+ const m = ctx && ctx.sessionManager
62
+ const id = m && (typeof m.getSessionId === "function" ? m.getSessionId() : m.sessionId)
63
+ if (id) return String(id)
64
+ } catch {}
46
65
  return (event && (event.sessionId || event.session_id)) || (ctx && ctx.session && ctx.session.id) || ""
47
66
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vshulcz/pi-deja",
3
- "version": "0.19.4",
4
- "description": "deja-vu memory for pi: the coding sessions already on this machine — pi, Claude Code, Codex, Cursor and 19 more — recalled at session start and on every prompt, searchable with /deja. A local index, no LLM.",
3
+ "version": "0.20.0",
4
+ "description": "deja-vu memory for pi: the coding sessions already on this machine — pi, Claude Code, Codex, Cursor and 21 more — recalled at session start and on every prompt, searchable with /deja. A local index, no LLM.",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "index.ts",
@@ -33,7 +33,7 @@
33
33
  "node": ">=18"
34
34
  },
35
35
  "dependencies": {
36
- "@vshulcz/deja-vu": "^0.19.4"
36
+ "@vshulcz/deja-vu": "^0.20.0"
37
37
  },
38
38
  "pi": {
39
39
  "extensions": [