@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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/index.ts +155 -0
  4. package/lib.mjs +47 -0
  5. 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
+ }