@vshulcz/pi-deja 0.22.2 → 0.23.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.
- package/index.ts +49 -7
- package/lib.mjs +30 -6
- package/package.json +2 -2
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);
|
|
@@ -155,7 +197,7 @@ export default function (pi: any) {
|
|
|
155
197
|
}
|
|
156
198
|
return;
|
|
157
199
|
}
|
|
158
|
-
const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: session }));
|
|
200
|
+
const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: session, parent_session_id: parent }));
|
|
159
201
|
if (!raw) return;
|
|
160
202
|
const resp = JSON.parse(raw);
|
|
161
203
|
if (resp && resp.systemMessage) ctx.ui.notify(resp.systemMessage, "info");
|
|
@@ -251,7 +293,7 @@ export default function (pi: any) {
|
|
|
251
293
|
// on a switch ctx may already name the next session.
|
|
252
294
|
pi.on("session_shutdown", async (_event: any) => {
|
|
253
295
|
try {
|
|
254
|
-
if (session) run(["hook-session-end"], JSON.stringify({ session_id: session }), 2000);
|
|
296
|
+
if (session) run(["hook-session-end"], JSON.stringify({ session_id: session, cwd: process.cwd() }), 2000);
|
|
255
297
|
} catch {}
|
|
256
298
|
});
|
|
257
299
|
}
|
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,24 @@ 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
|
+
export function branchHoldsRecall(branch) {
|
|
77
|
+
const entries = Array.isArray(branch) ? branch : []
|
|
78
|
+
let keep = ""
|
|
79
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
80
|
+
const e = entries[i] || {}
|
|
81
|
+
if (keep === "" && e.type === "compaction") {
|
|
82
|
+
keep = String(e.firstKeptEntryId || "")
|
|
83
|
+
if (!keep) return false
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
if (e.type === "custom_message" && e.customType === "deja-recall") return true
|
|
87
|
+
if (keep && e.id === keep) return false
|
|
88
|
+
}
|
|
89
|
+
return false
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vshulcz/pi-deja",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "deja-vu memory for pi: the coding sessions already on this machine — pi, Claude Code, Codex, Cursor and 38 more — recalled at session start and on every prompt, searchable with /deja. A local index, no LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"node": ">=18"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@vshulcz/deja-vu": "^0.
|
|
48
|
+
"@vshulcz/deja-vu": "^0.23.0"
|
|
49
49
|
},
|
|
50
50
|
"pi": {
|
|
51
51
|
"extensions": [
|