@vshulcz/pi-deja 0.22.2 → 0.23.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 +1 -1
- package/index.ts +52 -8
- package/lib.mjs +44 -6
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
English | [中文](https://github.com/vshulcz/deja-vu/blob/main/extensions/pi/docs/zh.md)
|
|
4
4
|
|
|
5
5
|
pi remembers its own sessions. This extension answers the other question: what
|
|
6
|
-
was done in the forty-
|
|
6
|
+
was done in the forty-two other coding agents on this machine — Claude Code,
|
|
7
7
|
Codex, Cursor, Gemini, OpenClaw and Hermes among them — including the months
|
|
8
8
|
before pi was installed.
|
|
9
9
|
|
package/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// deja-vu for pi: recall from the coding sessions already on this machine at
|
|
2
2
|
// session start and on every prompt, and /deja to search them by hand.
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { accessSync, constants, existsSync } from "node:fs";
|
|
4
|
+
import { accessSync, closeSync, constants, existsSync, openSync, readSync } from "node:fs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
8
|
|
|
9
|
-
import { argv, contextText, installerExtensionPaths, sessionKey } from "./lib.mjs";
|
|
9
|
+
import { argv, branchHoldsRecall, contextText, installerExtensionPaths, sessionKey } from "./lib.mjs";
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
|
|
@@ -72,7 +72,7 @@ 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 (installerExtensionPaths(homedir()).some((p) => existsSync(p))) return;
|
|
75
|
+
if (installerExtensionPaths(homedir(), process.env).some((p) => existsSync(p))) return;
|
|
76
76
|
} catch {}
|
|
77
77
|
|
|
78
78
|
// Once per session, not once per process: /new, a resume and a fork keep the
|
|
@@ -124,16 +124,58 @@ export default function (pi: any) {
|
|
|
124
124
|
},
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
// The session a fork was made from, named on its header by path, when the
|
|
128
|
+
// fork holds a copy of its turns: a fork at the first message copies
|
|
129
|
+
// nothing, and its source is no part of it. The id is on that file's own
|
|
130
|
+
// header; pi names the file <time>_<id>.jsonl.
|
|
131
|
+
const parents: Record<string, string> = {};
|
|
132
|
+
const parentID = (ctx: any): string => {
|
|
133
|
+
if (session in parents) return parents[session];
|
|
134
|
+
let id = "";
|
|
135
|
+
try {
|
|
136
|
+
const m = ctx.sessionManager;
|
|
137
|
+
const path = String((m.getHeader() || {}).parentSession || "");
|
|
138
|
+
const copied = (m.getBranch() || []).some((e: any) => e && e.type === "message");
|
|
139
|
+
if (path && copied) {
|
|
140
|
+
try {
|
|
141
|
+
const fd = openSync(path, "r");
|
|
142
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
143
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
144
|
+
closeSync(fd);
|
|
145
|
+
id = String(JSON.parse(buf.subarray(0, n).toString("utf8").split("\n", 1)[0]).id || "");
|
|
146
|
+
} catch {}
|
|
147
|
+
if (!id) {
|
|
148
|
+
const name = basename(path).replace(/\.jsonl$/, "");
|
|
149
|
+
id = name.slice(name.lastIndexOf("_") + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
if (session) parents[session] = id;
|
|
154
|
+
return id;
|
|
155
|
+
};
|
|
156
|
+
|
|
127
157
|
pi.on("before_agent_start", async (event: any, ctx: any) => {
|
|
128
158
|
try {
|
|
129
159
|
const key = sessionKey(event, ctx);
|
|
130
160
|
if (key) session = key;
|
|
161
|
+
// A resumed session holds the digest it opened with, and pi sends it to
|
|
162
|
+
// the model again with the rest of the session; a fork holds a copy of
|
|
163
|
+
// its source's. Asking for another put a second copy in front of the
|
|
164
|
+
// model, and in a fork one that led with the source itself.
|
|
165
|
+
const parent = parentID(ctx);
|
|
166
|
+
try {
|
|
167
|
+
if (!injected.has(session) && branchHoldsRecall(ctx.sessionManager.getBranch())) {
|
|
168
|
+
injected.add(session);
|
|
169
|
+
const stats = run(["statusline"], "");
|
|
170
|
+
if (stats) ctx.ui.setStatus("deja", stats);
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
131
173
|
if (!injected.has(session)) {
|
|
132
174
|
// The session goes with it: hook-context marks the one starting as
|
|
133
175
|
// live, which keeps it out of its own MCP recall on this first turn
|
|
134
176
|
// (#4394, as #4246 and #4273 did for Hermes and opencode).
|
|
135
177
|
const { context: digest, receipt } = contextText(
|
|
136
|
-
run(["hook-context"], JSON.stringify({ session_id: session, cwd: process.cwd() })),
|
|
178
|
+
run(["hook-context"], JSON.stringify({ session_id: session, parent_session_id: parent, cwd: process.cwd() })),
|
|
137
179
|
);
|
|
138
180
|
if (digest) {
|
|
139
181
|
injected.add(session);
|
|
@@ -145,7 +187,9 @@ export default function (pi: any) {
|
|
|
145
187
|
// quiet reminder that memory is on rather than a one-off toast.
|
|
146
188
|
const stats = run(["statusline"], "");
|
|
147
189
|
if (stats) ctx.ui.setStatus("deja", stats);
|
|
148
|
-
|
|
190
|
+
// Marked, so a resume can tell the digest from a prompt's recall
|
|
191
|
+
// (#5314).
|
|
192
|
+
return { message: { customType: "deja-recall", content: digest, display: false, details: { digest: true } } };
|
|
149
193
|
}
|
|
150
194
|
// Nothing to recall: either there is no history yet, or the first
|
|
151
195
|
// index is still building. Only the second is worth saying, once.
|
|
@@ -155,7 +199,7 @@ export default function (pi: any) {
|
|
|
155
199
|
}
|
|
156
200
|
return;
|
|
157
201
|
}
|
|
158
|
-
const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: session }));
|
|
202
|
+
const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: session, parent_session_id: parent }));
|
|
159
203
|
if (!raw) return;
|
|
160
204
|
const resp = JSON.parse(raw);
|
|
161
205
|
if (resp && resp.systemMessage) ctx.ui.notify(resp.systemMessage, "info");
|
|
@@ -251,7 +295,7 @@ export default function (pi: any) {
|
|
|
251
295
|
// on a switch ctx may already name the next session.
|
|
252
296
|
pi.on("session_shutdown", async (_event: any) => {
|
|
253
297
|
try {
|
|
254
|
-
if (session) run(["hook-session-end"], JSON.stringify({ session_id: session }), 2000);
|
|
298
|
+
if (session) run(["hook-session-end"], JSON.stringify({ session_id: session, cwd: process.cwd() }), 2000);
|
|
255
299
|
} catch {}
|
|
256
300
|
});
|
|
257
301
|
}
|
package/lib.mjs
CHANGED
|
@@ -7,19 +7,22 @@ import { join } from "node:path"
|
|
|
7
7
|
// installerExtensionPath is where `deja install pi-auto` writes its own
|
|
8
8
|
// extension. pi loads that file and this package side by side, and both
|
|
9
9
|
// would inject recall and register /deja, so this package stands down when
|
|
10
|
-
// the installer's copy is present. Mirrors PiConfigDir() in the installer
|
|
11
|
-
//
|
|
12
|
-
export function installerExtensionPath(home) {
|
|
13
|
-
|
|
10
|
+
// the installer's copy is present. Mirrors PiConfigDir() in the installer:
|
|
11
|
+
// ~/.pi/agent, or PI_CODING_AGENT_DIR when pi was told to live elsewhere.
|
|
12
|
+
export function installerExtensionPath(home, env = {}) {
|
|
13
|
+
let dir = String(env.PI_CODING_AGENT_DIR || "")
|
|
14
|
+
if (dir === "~") dir = home || ""
|
|
15
|
+
else if (dir.startsWith("~/") || dir.startsWith("~\\")) dir = join(home || "", dir.slice(2))
|
|
16
|
+
return join(dir || join(home || "", ".pi", "agent"), "extensions", "deja.ts")
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
// installerExtensionPaths are every place `deja install` writes an extension
|
|
17
20
|
// this package would duplicate. omp runs the same package and the installer
|
|
18
21
|
// writes it under ~/.omp/agent/extensions/deja/index.js, so checking pi's path
|
|
19
22
|
// alone left both running side by side (#3180).
|
|
20
|
-
export function installerExtensionPaths(home) {
|
|
23
|
+
export function installerExtensionPaths(home, env = {}) {
|
|
21
24
|
return [
|
|
22
|
-
installerExtensionPath(home),
|
|
25
|
+
installerExtensionPath(home, env),
|
|
23
26
|
join(home || "", ".omp", "agent", "extensions", "deja", "index.js"),
|
|
24
27
|
]
|
|
25
28
|
}
|
|
@@ -64,3 +67,38 @@ export function sessionKey(event, ctx) {
|
|
|
64
67
|
} catch {}
|
|
65
68
|
return (event && (event.sessionId || event.session_id)) || (ctx && ctx.session && ctx.session.id) || ""
|
|
66
69
|
}
|
|
70
|
+
|
|
71
|
+
// branchHoldsRecall reports whether what pi sends the model from this branch
|
|
72
|
+
// holds a recall block this package injected: what follows the last
|
|
73
|
+
// compaction, and the entries it kept from before it, firstKeptEntryId on.
|
|
74
|
+
// pi keeps the block as a custom_message and sends it again on a resume, and
|
|
75
|
+
// a fork copies it from its source.
|
|
76
|
+
//
|
|
77
|
+
// What counts is the start digest, not any recall: a prompt's recall goes
|
|
78
|
+
// under the same customType, and a compaction that kept a later one but not
|
|
79
|
+
// the digest left a resumed session with no digest (#5314). The digest
|
|
80
|
+
// carries details.digest; a branch with no marked digest anywhere was written
|
|
81
|
+
// before the mark, and any block of ours still counts there.
|
|
82
|
+
export function branchHoldsRecall(branch) {
|
|
83
|
+
const entries = Array.isArray(branch) ? branch : []
|
|
84
|
+
const marked = entries.some(isDigest)
|
|
85
|
+
const counts = (e) => (marked ? isDigest(e) : e.type === "custom_message" && e.customType === "deja-recall")
|
|
86
|
+
let keep = ""
|
|
87
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
88
|
+
const e = entries[i] || {}
|
|
89
|
+
if (keep === "" && e.type === "compaction") {
|
|
90
|
+
keep = String(e.firstKeptEntryId || "")
|
|
91
|
+
if (!keep) return false
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (counts(e)) return true
|
|
95
|
+
if (keep && e.id === keep) return false
|
|
96
|
+
}
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// isDigest reports whether a branch entry is the start digest this package
|
|
101
|
+
// sent, which it marks with details.digest.
|
|
102
|
+
function isDigest(e) {
|
|
103
|
+
return !!e && e.type === "custom_message" && e.customType === "deja-recall" && !!(e.details && e.details.digest)
|
|
104
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vshulcz/pi-deja",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "deja-vu memory for pi: the coding sessions already on this machine — pi, Claude Code, Codex, Cursor and
|
|
3
|
+
"version": "0.23.1",
|
|
4
|
+
"description": "deja-vu memory for pi: the coding sessions already on this machine — pi, Claude Code, Codex, Cursor and 39 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",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"node": ">=18"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@vshulcz/deja-vu": "^0.
|
|
48
|
+
"@vshulcz/deja-vu": "^0.23.1"
|
|
49
49
|
},
|
|
50
50
|
"pi": {
|
|
51
51
|
"extensions": [
|