@yyyeader/claude-recall 1.0.0 → 1.1.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/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  Search, find, and resume any Claude Code conversation across all your projects — instantly.
8
8
  </p>
9
9
  <p align="center">
10
- <a href="https://www.npmjs.com/package/claude-recall"><img src="https://img.shields.io/npm/v/claude-recall.svg" alt="npm version"></a>
11
- <a href="https://github.com/yyyeader/claude-recall/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/claude-recall.svg" alt="license"></a>
12
- <a href="https://www.npmjs.com/package/claude-recall"><img src="https://img.shields.io/npm/dm/claude-recall.svg" alt="downloads"></a>
10
+ <a href="https://www.npmjs.com/package/@yyyeader/claude-recall"><img src="https://img.shields.io/npm/v/@yyyeader/claude-recall.svg" alt="npm version"></a>
11
+ <a href="https://github.com/yyyeader/claude-recall/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@yyyeader/claude-recall.svg" alt="license"></a>
12
+ <a href="https://www.npmjs.com/package/@yyyeader/claude-recall"><img src="https://img.shields.io/npm/dm/@yyyeader/claude-recall.svg" alt="downloads"></a>
13
13
  </p>
14
14
  </p>
15
15
 
@@ -31,7 +31,7 @@ You know the session exists somewhere in `~/.claude/projects/`, but good luck fi
31
31
  ## The Solution
32
32
 
33
33
  ```bash
34
- npm install -g claude-recall
34
+ npm install -g @yyyeader/claude-recall
35
35
  ```
36
36
 
37
37
  ```bash
@@ -47,7 +47,7 @@ claude-recall -j | jq # Pipe JSON to your own tools
47
47
  ### 1. Install
48
48
 
49
49
  ```bash
50
- npm install -g claude-recall
50
+ npm install -g @yyyeader/claude-recall
51
51
  ```
52
52
 
53
53
  ### 2. Add the shell wrapper (recommended)
@@ -82,7 +82,7 @@ Want to search sessions from *inside* Claude Code?
82
82
  # Install the slash command
83
83
  claude-recall-install
84
84
  # Or manually:
85
- cp node_modules/claude-recall/commands/recall.md ~/.claude/commands/
85
+ cp node_modules/@yyyeader/claude-recall/commands/recall.md ~/.claude/commands/
86
86
  ```
87
87
 
88
88
  Then:
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
11
11
  const flags = {
12
12
  list: false,
13
13
  json: false,
14
- limit: 50,
14
+ limit: 0,
15
15
  project: null,
16
16
  help: false,
17
17
  keyword: [],
@@ -55,7 +55,7 @@ Usage:
55
55
  claude-recall -l [keyword] List sessions (no interaction)
56
56
  claude-recall -j [keyword] Output as JSON
57
57
  claude-recall -p <project> Filter by project name
58
- claude-recall -n <number> Limit results (default: 50)
58
+ claude-recall -n <number> Limit results (default: all)
59
59
 
60
60
  Options:
61
61
  -l, --list List mode (no fzf)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yyyeader/claude-recall",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Search and resume Claude Code sessions across all projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/scanner.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readdirSync, statSync } from "node:fs";
1
+ import { readdirSync, readFileSync, statSync, existsSync } from "node:fs";
2
2
  import { join, basename } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { resolvePath } from "./resolver.js";
@@ -8,7 +8,8 @@ const CLAUDE_DIR = join(homedir(), ".claude", "projects");
8
8
 
9
9
  /**
10
10
  * Scan all Claude Code sessions across all projects.
11
- * Returns an array of session objects sorted by modification time (newest first).
11
+ * Uses sessions-index.json when available (fast path),
12
+ * falls back to JSONL parsing for older projects.
12
13
  */
13
14
  export async function scanSessions({ keyword, project, limit } = {}) {
14
15
  let projectDirs;
@@ -25,8 +26,46 @@ export async function scanSessions({ keyword, project, limit } = {}) {
25
26
  for (const dir of projectDirs) {
26
27
  const projectEncoded = dir.name;
27
28
  const projectPath = join(CLAUDE_DIR, projectEncoded);
29
+ const indexFile = join(projectPath, "sessions-index.json");
28
30
 
29
- // List JSONL files directly under the project dir (skip subagents)
31
+ // Fast path: use sessions-index.json
32
+ if (existsSync(indexFile)) {
33
+ try {
34
+ const index = JSON.parse(readFileSync(indexFile, "utf8"));
35
+ for (const entry of index.entries || []) {
36
+ const workDir = entry.projectPath || resolvePath(projectEncoded);
37
+
38
+ if (project) {
39
+ const pl = project.toLowerCase();
40
+ if (
41
+ !workDir.toLowerCase().includes(pl) &&
42
+ !projectEncoded.toLowerCase().includes(pl)
43
+ ) {
44
+ continue;
45
+ }
46
+ }
47
+
48
+ sessions.push({
49
+ sessionId: entry.sessionId,
50
+ projectEncoded,
51
+ workDir,
52
+ filePath: entry.fullPath,
53
+ modifiedAt: new Date(entry.modified || entry.fileMtime),
54
+ summary:
55
+ [entry.firstPrompt, entry.summary].filter(Boolean).join(" — ") ||
56
+ "",
57
+ messageCount: entry.messageCount || 0,
58
+ gitBranch: entry.gitBranch || "",
59
+ fromIndex: true,
60
+ });
61
+ }
62
+ } catch {
63
+ // Fall through to JSONL scan
64
+ }
65
+ continue;
66
+ }
67
+
68
+ // Fallback: scan JSONL files directly
30
69
  let files;
31
70
  try {
32
71
  files = readdirSync(projectPath)
@@ -36,33 +75,13 @@ export async function scanSessions({ keyword, project, limit } = {}) {
36
75
  continue;
37
76
  }
38
77
 
39
- // Also check one level deeper for session directories (session-id/session.jsonl pattern)
40
- try {
41
- const subdirs = readdirSync(projectPath, { withFileTypes: true }).filter(
42
- (d) => d.isDirectory() && d.name !== "subagents"
43
- );
44
- for (const subdir of subdirs) {
45
- try {
46
- const subFiles = readdirSync(join(projectPath, subdir.name))
47
- .filter((f) => f.endsWith(".jsonl"))
48
- .map((f) => join(projectPath, subdir.name, f));
49
- files.push(...subFiles);
50
- } catch {
51
- // skip
52
- }
53
- }
54
- } catch {
55
- // skip
56
- }
57
-
58
78
  const workDir = resolvePath(projectEncoded);
59
79
 
60
- // Filter by project name if specified
61
80
  if (project) {
62
- const projectLower = project.toLowerCase();
81
+ const pl = project.toLowerCase();
63
82
  if (
64
- !projectEncoded.toLowerCase().includes(projectLower) &&
65
- !workDir.toLowerCase().includes(projectLower)
83
+ !projectEncoded.toLowerCase().includes(pl) &&
84
+ !workDir.toLowerCase().includes(pl)
66
85
  ) {
67
86
  continue;
68
87
  }
@@ -72,8 +91,6 @@ export async function scanSessions({ keyword, project, limit } = {}) {
72
91
  try {
73
92
  const stat = statSync(file);
74
93
  const sessionId = basename(file, ".jsonl");
75
-
76
- // Skip subagent files
77
94
  if (file.includes("/subagents/")) continue;
78
95
 
79
96
  sessions.push({
@@ -82,6 +99,7 @@ export async function scanSessions({ keyword, project, limit } = {}) {
82
99
  workDir,
83
100
  filePath: file,
84
101
  modifiedAt: stat.mtime,
102
+ fromIndex: false,
85
103
  });
86
104
  } catch {
87
105
  // skip
@@ -92,26 +110,33 @@ export async function scanSessions({ keyword, project, limit } = {}) {
92
110
  // Sort by modification time, newest first
93
111
  sessions.sort((a, b) => b.modifiedAt - a.modifiedAt);
94
112
 
95
- // Extract summaries (in parallel, batched)
96
- const batch = limit ? sessions.slice(0, limit) : sessions;
97
- await Promise.all(
98
- batch.map(async (s) => {
99
- s.summary = await extractSummary(s.filePath);
100
- })
101
- );
113
+ // Extract summaries for sessions that don't have one (JSONL fallback)
114
+ const needsSummary = sessions.filter((s) => !s.fromIndex);
115
+ if (needsSummary.length > 0) {
116
+ await Promise.all(
117
+ needsSummary.map(async (s) => {
118
+ s.summary = await extractSummary(s.filePath);
119
+ })
120
+ );
121
+ }
102
122
 
103
- // Filter by keyword if specified
104
- let result = batch;
123
+ // Filter by keyword
124
+ let result = sessions;
105
125
  if (keyword) {
106
126
  const kw = keyword.toLowerCase();
107
- result = batch.filter(
127
+ result = sessions.filter(
108
128
  (s) =>
109
- s.summary.toLowerCase().includes(kw) ||
129
+ (s.summary || "").toLowerCase().includes(kw) ||
110
130
  s.workDir.toLowerCase().includes(kw) ||
111
131
  s.projectEncoded.toLowerCase().includes(kw) ||
112
- s.sessionId.toLowerCase().includes(kw)
132
+ s.sessionId.toLowerCase().includes(kw) ||
133
+ (s.gitBranch || "").toLowerCase().includes(kw)
113
134
  );
114
135
  }
115
136
 
137
+ if (limit) {
138
+ result = result.slice(0, limit);
139
+ }
140
+
116
141
  return result;
117
142
  }