@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.
Files changed (3) hide show
  1. package/README.md +35 -13
  2. package/package.json +1 -1
  3. package/session-recall.ts +10 -191
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pi-session-recall
2
2
 
3
- Recall past [pi](https://github.com/earendil-works/pi) sessions. Search through conversation history and query specific sessions with an LLM.
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
- ## What it does
19
+ ## How it works
20
20
 
21
- Two tools that let the agent recall past sessions:
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
- ### `session_search`
23
+ > What did we decide about authentication and security?
24
24
 
25
- Literal text search across all past sessions using ripgrep-style fixed-string matching. It is not semantic search. The agent should search for one distinctive token or exact phrase at a time, such as a filename, package name, error string, function name, issue id, or remembered wording.
25
+ The agent can then:
26
26
 
27
- Spaces mean exact spaces in an exact phrase. For unrelated concepts, the agent should call `session_search` multiple times instead of combining them into one query.
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
- ### `session_query`
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
- Deep-dives into a specific session file. Loads the conversation, sends it to an LLM, and answers your question about it. It includes user and assistant text plus tool calls, while omitting assistant thinking and tool results to keep queries efficient.
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
- For large sessions that exceed the model's context window, it uses smart windowing: keeps the first/last messages plus keyword-relevant sections, marking gaps with `[... N messages omitted ...]`.
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
- ### `/session-recall`
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
- Command to configure which model is used for `session_query`. Opens a picker with all your available models.
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
- By default, queries use your **current session model**. If you want to save tokens, pick a cheaper model (e.g. Haiku, GPT-4o mini).
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
- Config is stored at `~/.pi/agent/session-recall.json`:
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogulcancelik/pi-session-recall",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Search and query past pi sessions. Recall previous conversations on demand.",
5
5
  "type": "module",
6
6
  "keywords": [
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 + one command:
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 type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
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, writeFileSync, mkdirSync } from "node:fs";
56
- import { join, dirname } from "node:path";
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: ExtensionCommandContext): Model<Api> | undefined {
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: (params) => `Session Search: ${params.query}`,
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: (params) => `Session Query: ${params.question}`,
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 one with /session-recall or set up API keys.",
778
+ "Configure queryModel in session-recall.json or set up API keys.",
960
779
  );
961
780
  }
962
781