@vshulcz/pi-deja 0.19.4
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/LICENSE +21 -0
- package/README.md +49 -0
- package/index.ts +155 -0
- package/lib.mjs +47 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vladislav Shulcz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# @vshulcz/pi-deja
|
|
2
|
+
|
|
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,
|
|
5
|
+
Codex, Cursor, Gemini, OpenClaw and Hermes among them — including the months
|
|
6
|
+
before pi was installed.
|
|
7
|
+
|
|
8
|
+
It runs [deja](https://github.com/vshulcz/deja-vu), a local Go binary that
|
|
9
|
+
indexes the transcripts those agents already wrote to disk. No LLM, no
|
|
10
|
+
embeddings, nothing leaves the machine.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pi install npm:@vshulcz/pi-deja
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The deja binary comes with the package; a deja you installed yourself
|
|
19
|
+
(`brew install deja-vu`) always wins — see [Which binary](#which-binary).
|
|
20
|
+
|
|
21
|
+
`deja install pi-auto` wires pi too, and is the shorter path if you have the
|
|
22
|
+
CLI: it adds deja's MCP server and writes an extension of its own. When that
|
|
23
|
+
extension is present this package stands down, so having both never injects
|
|
24
|
+
twice.
|
|
25
|
+
|
|
26
|
+
omp loads `pi.extensions` manifests and emits the same `before_agent_start`
|
|
27
|
+
event, so `pi install`'s omp counterpart picks this package up as well.
|
|
28
|
+
|
|
29
|
+
## What it does
|
|
30
|
+
|
|
31
|
+
- **Session start**: a digest of what earlier sessions in this project decided
|
|
32
|
+
goes to the model before your first prompt; the receipt shows in the footer.
|
|
33
|
+
- **Every prompt** (`before_agent_start`): the prompt is matched against the
|
|
34
|
+
index and, when a past session answers it, that session goes to the model
|
|
35
|
+
with the prompt. Silence is the common case.
|
|
36
|
+
- **`/deja <query>`**: search the history by hand.
|
|
37
|
+
|
|
38
|
+
## Which binary
|
|
39
|
+
|
|
40
|
+
In order: `DEJA_BIN`, `deja` on `PATH`, the usual install locations, and last
|
|
41
|
+
the copy bundled through `@vshulcz/deja-vu`.
|
|
42
|
+
|
|
43
|
+
## Privacy
|
|
44
|
+
|
|
45
|
+
deja reads session files where the agents wrote them and builds a local index
|
|
46
|
+
under `~/.cache/deja` (or `DEJA_INDEX_DIR`). Keys and tokens are stripped as
|
|
47
|
+
the index is built. Nothing is uploaded.
|
|
48
|
+
|
|
49
|
+
Part of [deja-vu](https://github.com/vshulcz/deja-vu). MIT.
|
package/index.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// deja-vu for pi: recall from the coding sessions already on this machine at
|
|
2
|
+
// session start and on every prompt, and /deja to search them by hand.
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
import { argv, contextText, installerExtensionPath, sessionKey } from "./lib.mjs";
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
|
|
13
|
+
const WINDOWS = process.platform === "win32";
|
|
14
|
+
const PLATFORM = WINDOWS ? "windows" : process.platform;
|
|
15
|
+
const ARCH = process.arch === "x64" ? "amd64" : process.arch;
|
|
16
|
+
const EXE = WINDOWS ? "deja.exe" : "deja";
|
|
17
|
+
|
|
18
|
+
// wellKnown lists the places a user's own install lands, for a pi started
|
|
19
|
+
// from a launcher whose PATH never sourced a shell profile.
|
|
20
|
+
function wellKnown(): string[] {
|
|
21
|
+
const home = homedir();
|
|
22
|
+
if (WINDOWS) {
|
|
23
|
+
const local = process.env.LOCALAPPDATA || join(home, "AppData", "Local");
|
|
24
|
+
return [join(local, "deja", "bin", EXE), join(home, ".local", "bin", EXE)];
|
|
25
|
+
}
|
|
26
|
+
return [join(home, ".local", "bin", EXE), "/usr/local/bin/deja", "/opt/homebrew/bin/deja", "/usr/bin/deja"];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// resolveDeja: what the user pointed at, then the deja they installed and keep
|
|
30
|
+
// current, and only last the copy npm brought with this package — pinning
|
|
31
|
+
// them to our bundled copy would freeze their memory at whatever version this
|
|
32
|
+
// package was released against.
|
|
33
|
+
function resolveDeja(): string {
|
|
34
|
+
const candidates: string[] = [process.env.DEJA_BIN || "", EXE, ...wellKnown()];
|
|
35
|
+
try {
|
|
36
|
+
candidates.push(require.resolve(`@vshulcz/deja-vu-${PLATFORM}-${ARCH}/bin/${EXE}`));
|
|
37
|
+
} catch {}
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
if (!candidate) continue;
|
|
40
|
+
if (candidate.includes("/") || candidate.includes("\\")) {
|
|
41
|
+
try {
|
|
42
|
+
accessSync(candidate, constants.X_OK);
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
return EXE;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const DEJA = resolveDeja();
|
|
53
|
+
|
|
54
|
+
// deja is asked for text, never for control flow: memory is optional
|
|
55
|
+
// everywhere, and a throw here would end someone's turn.
|
|
56
|
+
function run(args: string[], input: string, timeout = 10000): string {
|
|
57
|
+
try {
|
|
58
|
+
return execFileSync(DEJA, args, {
|
|
59
|
+
input,
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
timeout,
|
|
62
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
63
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
64
|
+
}).trim();
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default function (pi: any) {
|
|
71
|
+
// `deja install pi-auto` writes an extension of its own; when it is there,
|
|
72
|
+
// it already does everything below, and two copies would inject the
|
|
73
|
+
// recall twice and register /deja twice.
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(installerExtensionPath(homedir()))) return;
|
|
76
|
+
} catch {}
|
|
77
|
+
|
|
78
|
+
let injected = false;
|
|
79
|
+
let toldBuilding = false;
|
|
80
|
+
|
|
81
|
+
// pi keeps a footer status line: while the first index builds, that is
|
|
82
|
+
// where the user can see it happening instead of wondering why recall is
|
|
83
|
+
// quiet. Cleared as soon as the build finishes.
|
|
84
|
+
const showBuild = (ctx: any) => {
|
|
85
|
+
const status = run(["warmup-status"], "");
|
|
86
|
+
if (status) {
|
|
87
|
+
ctx.ui.setStatus("deja", status);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
ctx.ui.setStatus("deja", "");
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
pi.on("session_start", async (_event: any, ctx: any) => {
|
|
95
|
+
try {
|
|
96
|
+
showBuild(ctx);
|
|
97
|
+
} catch {
|
|
98
|
+
// memory is optional: never break the session over it
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// pi surfaces registered commands in the prompt box, which is how someone
|
|
103
|
+
// who never read the docs finds this at all.
|
|
104
|
+
pi.registerCommand("deja", {
|
|
105
|
+
description: "Search your own past coding sessions",
|
|
106
|
+
handler: async (args: string, ctx: any) => {
|
|
107
|
+
const query = (args || "").trim();
|
|
108
|
+
if (!query) {
|
|
109
|
+
ctx.ui.notify("Usage: /deja <what you are looking for>", "info");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// The user is waiting on this one, so it may outlive the hook budget:
|
|
113
|
+
// a first search can rebuild the index.
|
|
114
|
+
const found = run(argv("search", [], query), "", 120000);
|
|
115
|
+
ctx.ui.notify(found || "Nothing in your history matches " + query, "info");
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.on("before_agent_start", async (event: any, ctx: any) => {
|
|
120
|
+
try {
|
|
121
|
+
if (!injected) {
|
|
122
|
+
const { context: digest, receipt } = contextText(run(["hook-context"], ""));
|
|
123
|
+
if (digest) {
|
|
124
|
+
injected = true;
|
|
125
|
+
ctx.ui.setStatus("deja", "");
|
|
126
|
+
// The receipt is what tells the user memory arrived; without it the
|
|
127
|
+
// recall is invisible and reads as the model guessing.
|
|
128
|
+
if (receipt) ctx.ui.notify(receipt, "info");
|
|
129
|
+
// pi keeps the footer until it is cleared, so the session carries a
|
|
130
|
+
// quiet reminder that memory is on rather than a one-off toast.
|
|
131
|
+
const stats = run(["statusline"], "");
|
|
132
|
+
if (stats) ctx.ui.setStatus("deja", stats);
|
|
133
|
+
return { message: { customType: "deja-recall", content: digest, display: false } };
|
|
134
|
+
}
|
|
135
|
+
// Nothing to recall: either there is no history yet, or the first
|
|
136
|
+
// index is still building. Only the second is worth saying, once.
|
|
137
|
+
if (!toldBuilding && showBuild(ctx)) {
|
|
138
|
+
toldBuilding = true;
|
|
139
|
+
ctx.ui.notify(run(["warmup-status"], ""), "info");
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const key = sessionKey(event, ctx);
|
|
144
|
+
const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: key }));
|
|
145
|
+
if (!raw) return;
|
|
146
|
+
const resp = JSON.parse(raw);
|
|
147
|
+
if (resp && resp.systemMessage) ctx.ui.notify(resp.systemMessage, "info");
|
|
148
|
+
const extra = resp && resp.hookSpecificOutput && resp.hookSpecificOutput.additionalContext;
|
|
149
|
+
if (!extra) return;
|
|
150
|
+
return { message: { customType: "deja-recall", content: extra, display: false } };
|
|
151
|
+
} catch {
|
|
152
|
+
// memory is optional: never break the session over it
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
package/lib.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
|
|
3
|
+
// The pure half of the extension: everything that reads deja's output or
|
|
4
|
+
// decides what this package adds, with no process and no host, so it can be
|
|
5
|
+
// tested without pi.
|
|
6
|
+
|
|
7
|
+
// installerExtensionPath is where `deja install pi-auto` writes its own
|
|
8
|
+
// extension. pi loads that file and this package side by side, and both
|
|
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
|
+
// which is ~/.pi/agent with no override — pi has none either.
|
|
12
|
+
export function installerExtensionPath(home) {
|
|
13
|
+
return join(home || "", ".pi", "agent", "extensions", "deja.ts")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// contextText pulls the recall out of whatever hook-context printed. deja
|
|
17
|
+
// answers in the Claude Code hook shape; older builds and `--plain` print
|
|
18
|
+
// bare text, so both are accepted.
|
|
19
|
+
export function contextText(raw) {
|
|
20
|
+
const text = String(raw || "").trim()
|
|
21
|
+
if (!text) return { context: "", receipt: "" }
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(text)
|
|
24
|
+
return {
|
|
25
|
+
context: parsed?.hookSpecificOutput?.additionalContext || "",
|
|
26
|
+
receipt: parsed?.systemMessage || "",
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
return { context: text, receipt: "" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// argv builds the call for a query somebody typed. A query that starts with
|
|
34
|
+
// a dash is read by deja as a flag and the call fails, which run() turns into
|
|
35
|
+
// "nothing in the history" — a wrong answer where an error would at least be
|
|
36
|
+
// visible. `--` ends the flags; sent only when the query needs it.
|
|
37
|
+
export function argv(cmd, flags, text) {
|
|
38
|
+
const arg = String(text)
|
|
39
|
+
return arg.startsWith("-") ? [cmd, ...flags, "--", arg] : [cmd, ...flags, arg]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 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.
|
|
45
|
+
export function sessionKey(event, ctx) {
|
|
46
|
+
return (event && (event.sessionId || event.session_id)) || (ctx && ctx.session && ctx.session.id) || ""
|
|
47
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.ts",
|
|
8
|
+
"lib.mjs",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/vshulcz/deja-vu.git",
|
|
16
|
+
"directory": "extensions/pi"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://vshulcz.github.io/deja-vu/guide/memory-for-pi.html",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/vshulcz/deja-vu/issues"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi-package",
|
|
24
|
+
"pi-extension",
|
|
25
|
+
"pi-coding-agent",
|
|
26
|
+
"omp",
|
|
27
|
+
"memory",
|
|
28
|
+
"recall",
|
|
29
|
+
"session-history",
|
|
30
|
+
"agent"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@vshulcz/deja-vu": "^0.19.4"
|
|
37
|
+
},
|
|
38
|
+
"pi": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./index.ts"
|
|
41
|
+
],
|
|
42
|
+
"image": "https://raw.githubusercontent.com/vshulcz/deja-vu/assets/listings/recall-card.png"
|
|
43
|
+
},
|
|
44
|
+
"omp": {
|
|
45
|
+
"extensions": [
|
|
46
|
+
"./index.ts"
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"test": "node --test test/*.test.mjs"
|
|
51
|
+
}
|
|
52
|
+
}
|