@ogulcancelik/pi-session-recall 1.0.6 → 1.0.7
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 +35 -13
- package/package.json +1 -1
- package/session-recall.ts +10 -191
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# pi-session-recall
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Give [pi](https://github.com/earendil-works/pi) access to past conversations. Ask a normal question about previous work, and the agent can find the relevant sessions and inspect them for an answer.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -16,31 +16,53 @@ Or add manually to `~/.pi/agent/settings.json`:
|
|
|
16
16
|
}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## How it works
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
The extension adds two tools to pi: `session_search` and `session_query`. You do not need to call them yourself. Ask the agent a natural-language question such as:
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
> What did we decide about authentication and security?
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
The agent can then:
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
1. Search for a few distinctive terms or exact phrases across your saved session files.
|
|
28
|
+
2. Review the matching snippets and select the most relevant sessions.
|
|
29
|
+
3. Ask `session_query` a focused question about each selected session.
|
|
30
|
+
4. Combine the answers and respond with the relevant decisions, changes, or context.
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
For example, the tool flow may look like this:
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
You: What did we decide about authentication and security?
|
|
36
|
+
Agent: session_search("authentication")
|
|
37
|
+
Agent: session_search("security")
|
|
38
|
+
Agent: session_query(<matching-session>, "What decisions did we make about authentication and security?")
|
|
39
|
+
Agent: <answers your question>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Other useful prompts include:
|
|
30
43
|
|
|
31
|
-
|
|
44
|
+
- `How did we fix the Cannot find module '@sinclair/typebox' error?`
|
|
45
|
+
- `Which files did we change when we added passkey authentication?`
|
|
46
|
+
- `Find the session where we discussed Blender VAT baking.`
|
|
47
|
+
- `What approach did we reject for token refresh, and why?`
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
There is no background indexing or vector database. Recall happens on demand: the main agent searches session JSONL files, then an LLM reads the relevant conversation and answers a focused question.
|
|
34
50
|
|
|
35
|
-
###
|
|
51
|
+
### `session_search`
|
|
52
|
+
|
|
53
|
+
Searches all past sessions using case-insensitive, literal fixed-string matching—essentially `rg -i -F`. It is not regex or semantic search. One call should contain one distinctive token or exact phrase, such as a filename, package name, error string, function name, issue ID, or remembered wording.
|
|
54
|
+
|
|
55
|
+
Spaces are treated as part of the exact phrase. To search for separate concepts such as authentication and security, the agent makes separate calls rather than searching for `authentication security` as one phrase.
|
|
56
|
+
|
|
57
|
+
### `session_query`
|
|
36
58
|
|
|
37
|
-
|
|
59
|
+
Loads one selected session and sends its conversation to an LLM with a focused question. It includes user and assistant messages plus tool calls, while omitting assistant thinking and tool results to reduce cost.
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
For a session larger than the model's context window, it keeps the beginning, end, and sections relevant to the question. Omitted sections are marked with `[... N messages omitted ...]`.
|
|
40
62
|
|
|
41
63
|
## Configuration
|
|
42
64
|
|
|
43
|
-
|
|
65
|
+
To use a dedicated model for `session_query`, create or edit `~/.pi/agent/session-recall.json`:
|
|
44
66
|
|
|
45
67
|
```json
|
|
46
68
|
{
|
package/package.json
CHANGED
package/session-recall.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Session Recall Extension - Search and query past pi sessions
|
|
3
3
|
*
|
|
4
|
-
* Two tools
|
|
4
|
+
* Two tools:
|
|
5
5
|
*
|
|
6
|
-
* Tools:
|
|
7
6
|
* 1. session_search - Find relevant past sessions by keywords.
|
|
8
7
|
* Use when the user mentions something from a previous conversation
|
|
9
8
|
* and you need to find which session it was in.
|
|
@@ -13,11 +12,6 @@
|
|
|
13
12
|
* specific session, or when you already know the session path
|
|
14
13
|
* (e.g., from a handoff).
|
|
15
14
|
*
|
|
16
|
-
* Command:
|
|
17
|
-
* /session-recall - Configure the model used for session queries.
|
|
18
|
-
* Shows all available models and lets you pick one. Persists the
|
|
19
|
-
* choice to ~/.pi/agent/session-recall.json (or equivalent).
|
|
20
|
-
*
|
|
21
15
|
* Typical flow: user says "remember when we tried X?" →
|
|
22
16
|
* session_search("X") → find the right session →
|
|
23
17
|
* session_query(path, "what approach did we take for X?")
|
|
@@ -25,12 +19,12 @@
|
|
|
25
19
|
|
|
26
20
|
import {
|
|
27
21
|
cleanupSessionResources,
|
|
28
|
-
complete,
|
|
29
22
|
type Model,
|
|
30
23
|
type Api,
|
|
31
24
|
type Message,
|
|
32
25
|
} from "@earendil-works/pi-ai";
|
|
33
|
-
import
|
|
26
|
+
import { complete } from "@earendil-works/pi-ai/compat";
|
|
27
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
34
28
|
import {
|
|
35
29
|
getAgentDir,
|
|
36
30
|
SessionManager,
|
|
@@ -39,21 +33,12 @@ import {
|
|
|
39
33
|
serializeConversation,
|
|
40
34
|
type SessionEntry,
|
|
41
35
|
} from "@earendil-works/pi-coding-agent";
|
|
42
|
-
import {
|
|
43
|
-
Container,
|
|
44
|
-
Input,
|
|
45
|
-
Markdown,
|
|
46
|
-
SelectList,
|
|
47
|
-
type SelectItem,
|
|
48
|
-
type SelectListTheme,
|
|
49
|
-
Spacer,
|
|
50
|
-
Text,
|
|
51
|
-
} from "@earendil-works/pi-tui";
|
|
36
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
52
37
|
import { Type } from "@sinclair/typebox";
|
|
53
38
|
import { execFileSync } from "node:child_process";
|
|
54
39
|
import { randomBytes } from "node:crypto";
|
|
55
|
-
import { existsSync, readFileSync, readdirSync
|
|
56
|
-
import { join
|
|
40
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
41
|
+
import { join } from "node:path";
|
|
57
42
|
import { homedir } from "node:os";
|
|
58
43
|
|
|
59
44
|
// ── Config ───────────────────────────────────────────────────────────────────
|
|
@@ -79,19 +64,10 @@ function loadConfig(): SessionRecallConfig {
|
|
|
79
64
|
}
|
|
80
65
|
}
|
|
81
66
|
|
|
82
|
-
function saveConfig(config: SessionRecallConfig): void {
|
|
83
|
-
const configPath = getConfigPath();
|
|
84
|
-
const dir = dirname(configPath);
|
|
85
|
-
if (!existsSync(dir)) {
|
|
86
|
-
mkdirSync(dir, { recursive: true });
|
|
87
|
-
}
|
|
88
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
89
|
-
}
|
|
90
|
-
|
|
91
67
|
/**
|
|
92
68
|
* Resolve the query model from config → fallback to current session model.
|
|
93
69
|
*/
|
|
94
|
-
function resolveQueryModel(ctx:
|
|
70
|
+
function resolveQueryModel(ctx: ExtensionContext): Model<Api> | undefined {
|
|
95
71
|
const config = loadConfig();
|
|
96
72
|
if (config.queryModel) {
|
|
97
73
|
const found = ctx.modelRegistry.find(config.queryModel.provider, config.queryModel.id);
|
|
@@ -565,168 +541,11 @@ const MAX_SNIPPETS_PER_SESSION = 3;
|
|
|
565
541
|
// ── Extension ────────────────────────────────────────────────────────────────
|
|
566
542
|
|
|
567
543
|
export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
568
|
-
// ── /session-recall command ────────────────────────────────────────────
|
|
569
|
-
|
|
570
|
-
pi.registerCommand("session-recall", {
|
|
571
|
-
description: "Configure the model used for session queries",
|
|
572
|
-
handler: async (_args, ctx) => {
|
|
573
|
-
if (!ctx.hasUI) {
|
|
574
|
-
ctx.ui.notify("Interactive mode required for /session-recall", "warning");
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const config = loadConfig();
|
|
579
|
-
const available = ctx.modelRegistry.getAvailable();
|
|
580
|
-
|
|
581
|
-
if (available.length === 0) {
|
|
582
|
-
ctx.ui.notify("No models available. Configure API keys first.", "warning");
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
const currentKey = config.queryModel
|
|
587
|
-
? `${config.queryModel.provider}/${config.queryModel.id}`
|
|
588
|
-
: null;
|
|
589
|
-
|
|
590
|
-
// Build select items: session model fallback + all available models
|
|
591
|
-
const SESSION_MODEL_VALUE = "__session_model__";
|
|
592
|
-
const items: SelectItem[] = [
|
|
593
|
-
{
|
|
594
|
-
value: SESSION_MODEL_VALUE,
|
|
595
|
-
label: "(use current session model)",
|
|
596
|
-
description: currentKey === null ? "active" : undefined,
|
|
597
|
-
},
|
|
598
|
-
...available.map((m) => {
|
|
599
|
-
const key = `${m.provider}/${m.id}`;
|
|
600
|
-
return {
|
|
601
|
-
value: key,
|
|
602
|
-
label: key,
|
|
603
|
-
description: key === currentKey ? "active" : undefined,
|
|
604
|
-
};
|
|
605
|
-
}),
|
|
606
|
-
];
|
|
607
|
-
|
|
608
|
-
const choice = await ctx.ui.custom<string | undefined>(
|
|
609
|
-
(tui, theme, _kb, done) => {
|
|
610
|
-
const selectTheme: SelectListTheme = {
|
|
611
|
-
selectedPrefix: (t) => theme.fg("accent", t),
|
|
612
|
-
selectedText: (t) => theme.fg("accent", t),
|
|
613
|
-
description: (t) => theme.fg("muted", t),
|
|
614
|
-
scrollInfo: (t) => theme.fg("muted", t),
|
|
615
|
-
noMatch: (t) => theme.fg("muted", t),
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
const container = new Container();
|
|
619
|
-
const title = new Text(
|
|
620
|
-
theme.bold("Query model for session_query") +
|
|
621
|
-
theme.fg("muted", " (type to filter)"),
|
|
622
|
-
0,
|
|
623
|
-
0,
|
|
624
|
-
);
|
|
625
|
-
const input = new Input();
|
|
626
|
-
const listSpacer = new Spacer(1);
|
|
627
|
-
const hint = new Text(
|
|
628
|
-
theme.fg("muted", " ↑/↓ navigate · Enter select · Esc cancel"),
|
|
629
|
-
0,
|
|
630
|
-
0,
|
|
631
|
-
);
|
|
632
|
-
|
|
633
|
-
// Fuzzy filter: every term must appear somewhere in the label
|
|
634
|
-
const fuzzyMatch = (label: string, query: string): boolean => {
|
|
635
|
-
const lower = label.toLowerCase();
|
|
636
|
-
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
637
|
-
return terms.every((term) => lower.includes(term));
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
let list: SelectList;
|
|
641
|
-
|
|
642
|
-
const buildList = (filtered: SelectItem[]): SelectList => {
|
|
643
|
-
const sl = new SelectList(filtered, 10, selectTheme);
|
|
644
|
-
sl.onSelect = (item) => done(item.value);
|
|
645
|
-
sl.onCancel = () => done(undefined);
|
|
646
|
-
return sl;
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
// Initial list with all items, pre-select active
|
|
650
|
-
list = buildList(items);
|
|
651
|
-
const activeIdx = items.findIndex(
|
|
652
|
-
(i) => i.value === (currentKey ?? SESSION_MODEL_VALUE),
|
|
653
|
-
);
|
|
654
|
-
if (activeIdx >= 0) list.setSelectedIndex(activeIdx);
|
|
655
|
-
|
|
656
|
-
container.addChild(title);
|
|
657
|
-
container.addChild(new Spacer(1));
|
|
658
|
-
container.addChild(input);
|
|
659
|
-
container.addChild(listSpacer);
|
|
660
|
-
container.addChild(list);
|
|
661
|
-
container.addChild(new Spacer(1));
|
|
662
|
-
container.addChild(hint);
|
|
663
|
-
|
|
664
|
-
input.onSubmit = () => {
|
|
665
|
-
const selected = list.getSelectedItem();
|
|
666
|
-
if (selected) done(selected.value);
|
|
667
|
-
};
|
|
668
|
-
input.onEscape = () => done(undefined);
|
|
669
|
-
|
|
670
|
-
const rebuildList = () => {
|
|
671
|
-
const query = input.getValue();
|
|
672
|
-
const filtered = query
|
|
673
|
-
? items.filter((item) => fuzzyMatch(item.label, query))
|
|
674
|
-
: items;
|
|
675
|
-
|
|
676
|
-
container.removeChild(list);
|
|
677
|
-
list = buildList(filtered);
|
|
678
|
-
// Re-insert list at correct position (after listSpacer)
|
|
679
|
-
const spacerIdx = container.children.indexOf(listSpacer);
|
|
680
|
-
container.children.splice(spacerIdx + 1, 0, list);
|
|
681
|
-
tui.requestRender();
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
container.handleInput = (data: string) => {
|
|
685
|
-
// Navigation keys go to the select list
|
|
686
|
-
const isNav =
|
|
687
|
-
data === "\x1b[A" || // up
|
|
688
|
-
data === "\x1b[B" || // down
|
|
689
|
-
data === "\r" || // enter
|
|
690
|
-
data === "\n";
|
|
691
|
-
|
|
692
|
-
if (isNav) {
|
|
693
|
-
list.handleInput(data);
|
|
694
|
-
} else if (data === "\x1b" || data === "\x03") {
|
|
695
|
-
done(undefined);
|
|
696
|
-
} else {
|
|
697
|
-
input.handleInput(data);
|
|
698
|
-
rebuildList();
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
return container;
|
|
703
|
-
},
|
|
704
|
-
);
|
|
705
|
-
|
|
706
|
-
if (choice === undefined) return; // cancelled
|
|
707
|
-
|
|
708
|
-
if (choice === SESSION_MODEL_VALUE) {
|
|
709
|
-
delete config.queryModel;
|
|
710
|
-
saveConfig(config);
|
|
711
|
-
ctx.ui.notify("Session query will use the current session model", "info");
|
|
712
|
-
} else {
|
|
713
|
-
const slashIdx = choice.indexOf("/");
|
|
714
|
-
if (slashIdx === -1) return;
|
|
715
|
-
|
|
716
|
-
const provider = choice.slice(0, slashIdx);
|
|
717
|
-
const id = choice.slice(slashIdx + 1);
|
|
718
|
-
config.queryModel = { provider, id };
|
|
719
|
-
saveConfig(config);
|
|
720
|
-
ctx.ui.notify(`Session query model set to ${choice}`, "info");
|
|
721
|
-
}
|
|
722
|
-
},
|
|
723
|
-
});
|
|
724
|
-
|
|
725
544
|
// ── session_search tool ────────────────────────────────────────────────
|
|
726
545
|
|
|
727
546
|
pi.registerTool({
|
|
728
547
|
name: "session_search",
|
|
729
|
-
label:
|
|
548
|
+
label: "Session Search",
|
|
730
549
|
description:
|
|
731
550
|
"Find past sessions by literal text search. This is essentially `rg -i -F` over session JSONL files, not semantic search. " +
|
|
732
551
|
"Use one exact token or phrase. Spaces are exact spaces in an exact phrase, so only use spaces for wording you expect appeared in the session, such as an error message. " +
|
|
@@ -856,7 +675,7 @@ export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
|
856
675
|
|
|
857
676
|
pi.registerTool({
|
|
858
677
|
name: "session_query",
|
|
859
|
-
label:
|
|
678
|
+
label: "Session Query",
|
|
860
679
|
description:
|
|
861
680
|
"Query a specific session file to get detailed information. Use after session_search to dig into a particular session, " +
|
|
862
681
|
"or when you already have a session path (e.g., from a handoff). Sends the conversation and tool calls, without assistant thinking or tool output, to an LLM for analysis.",
|
|
@@ -956,7 +775,7 @@ export default function sessionRecallExtension(pi: ExtensionAPI) {
|
|
|
956
775
|
if (!queryModel) {
|
|
957
776
|
return errorResult(
|
|
958
777
|
"Error: No model available to analyze the session. " +
|
|
959
|
-
"Configure
|
|
778
|
+
"Configure queryModel in session-recall.json or set up API keys.",
|
|
960
779
|
);
|
|
961
780
|
}
|
|
962
781
|
|