@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 +6 -6
- package/bin/claude-recall.js +2 -2
- package/package.json +1 -1
- package/src/scanner.js +65 -40
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:
|
package/bin/claude-recall.js
CHANGED
|
@@ -11,7 +11,7 @@ const args = process.argv.slice(2);
|
|
|
11
11
|
const flags = {
|
|
12
12
|
list: false,
|
|
13
13
|
json: false,
|
|
14
|
-
limit:
|
|
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:
|
|
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
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
|
-
*
|
|
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
|
-
//
|
|
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
|
|
81
|
+
const pl = project.toLowerCase();
|
|
63
82
|
if (
|
|
64
|
-
!projectEncoded.toLowerCase().includes(
|
|
65
|
-
!workDir.toLowerCase().includes(
|
|
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 (
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
104
|
-
let result =
|
|
123
|
+
// Filter by keyword
|
|
124
|
+
let result = sessions;
|
|
105
125
|
if (keyword) {
|
|
106
126
|
const kw = keyword.toLowerCase();
|
|
107
|
-
result =
|
|
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
|
}
|