reclaude 1.0.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/LICENSE +21 -0
- package/README.md +79 -0
- package/index.js +964 -0
- package/package.json +20 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dongwook-chan
|
|
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,79 @@
|
|
|
1
|
+
# total-reclaude
|
|
2
|
+
|
|
3
|
+
Interactive CLI session picker for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Browse, filter, rename, and resume all your sessions from the terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx total-reclaude
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or install globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g total-reclaude
|
|
15
|
+
total-reclaude
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Prerequisites
|
|
19
|
+
|
|
20
|
+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) must be installed and on your PATH
|
|
21
|
+
- Node.js >= 14
|
|
22
|
+
|
|
23
|
+
## Screenshot
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Claude Code Sessions
|
|
27
|
+
─────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
Title Project Modified Session
|
|
30
|
+
|
|
31
|
+
> 1 minimize-sys-prompt my-project 03/07/2026, 17:06 6566b87b
|
|
32
|
+
2 debug-auth-flow my-app 03/07/2026, 15:51 eec06353
|
|
33
|
+
3 refactor-api-layer backend 03/07/2026, 15:41 d8d86cb2
|
|
34
|
+
4 add-dark-mode frontend 03/07/2026, 15:41 69b5fb55
|
|
35
|
+
5 how do I fix this bug? server 03/07/2026, 15:41 5ec54a4c
|
|
36
|
+
|
|
37
|
+
Page 1/1 ←/→ page | ↑/↓ select | Enter resume | r rename | / name | p path | s settings | q quit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **Bold** titles = named sessions (with custom title)
|
|
41
|
+
- *Dim italic* titles = unnamed sessions (showing first user message)
|
|
42
|
+
|
|
43
|
+
## Keybindings
|
|
44
|
+
|
|
45
|
+
| Key | Action |
|
|
46
|
+
|-----|--------|
|
|
47
|
+
| `↑` / `↓` | Move cursor |
|
|
48
|
+
| `Enter` | Resume selected session |
|
|
49
|
+
| `r` | Rename selected session |
|
|
50
|
+
| `/` | Filter sessions by name |
|
|
51
|
+
| `p` | Filter sessions by project path |
|
|
52
|
+
| `s` | Open settings |
|
|
53
|
+
| `←` / `→` | Navigate pages |
|
|
54
|
+
| `ESC` | Cancel / go back |
|
|
55
|
+
| `q` | Quit |
|
|
56
|
+
| `Ctrl+C` | Quit |
|
|
57
|
+
|
|
58
|
+
## Settings
|
|
59
|
+
|
|
60
|
+
Press `s` to open the settings panel:
|
|
61
|
+
|
|
62
|
+
| Setting | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| Full directory path | Show full `cwd` instead of short project name |
|
|
65
|
+
| Full session ID | Show complete session ID instead of first 8 characters |
|
|
66
|
+
| Page size | Number of sessions per page (5-50, adjustable with `-`/`+`) |
|
|
67
|
+
| Locale | Date format locale (e.g. `en-US`, `ko-KR`, `sv-SE`) |
|
|
68
|
+
|
|
69
|
+
Settings are saved to `~/.config/total-reclaude/config.json`.
|
|
70
|
+
|
|
71
|
+
## How it works
|
|
72
|
+
|
|
73
|
+
total-reclaude scans `~/.claude/projects/` for session files (`.jsonl`). Sessions with a custom title are shown in bold; unnamed sessions display the first user message in dim italic as a fallback title. All sessions are sorted by last modified time, with caching (`~/.cache/total-reclaude/sessions.json`) for fast startup.
|
|
74
|
+
|
|
75
|
+
When you select a session, it runs `claude --resume <session-id>` to resume it.
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
[MIT](LICENSE)
|
package/index.js
ADDED
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Interactive Claude Code session picker with pagination and filtering
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const { spawnSync } = require("child_process");
|
|
8
|
+
|
|
9
|
+
const CONFIG_PATH = path.join(
|
|
10
|
+
os.homedir(),
|
|
11
|
+
".config",
|
|
12
|
+
"total-reclaude",
|
|
13
|
+
"config.json"
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// Column definitions: id → { label, width, get(session, config) }
|
|
17
|
+
const COLUMN_DEFS = {
|
|
18
|
+
title: { label: "Title", width: 35, get: (s) => s.title || s.firstMessage },
|
|
19
|
+
firstMessage: { label: "First Message", width: 35, get: (s) => s.firstMessage },
|
|
20
|
+
project: { label: "Project", width: 20, get: (s) => s.proj },
|
|
21
|
+
path: { label: "Directory", width: 40, get: (s) => s.cwd },
|
|
22
|
+
modified: { label: "Modified", width: 20, get: (s, cfg) => formatDate(s.mtime, cfg) },
|
|
23
|
+
sid: { label: "Session", width: 8, get: (s) => s.sid.slice(0, 8) },
|
|
24
|
+
sid_full: { label: "Session ID", width: 36, get: (s) => s.sid },
|
|
25
|
+
};
|
|
26
|
+
const ALL_COLUMN_IDS = Object.keys(COLUMN_DEFS);
|
|
27
|
+
const DEFAULT_COLUMNS = ["title", "project", "modified", "sid"];
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CONFIG = {
|
|
30
|
+
pageSize: 10,
|
|
31
|
+
locale: "",
|
|
32
|
+
columns: DEFAULT_COLUMNS,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function formatDate(mtime, config) {
|
|
36
|
+
return new Date(mtime).toLocaleString(config.locale || undefined, {
|
|
37
|
+
year: "numeric",
|
|
38
|
+
month: "2-digit",
|
|
39
|
+
day: "2-digit",
|
|
40
|
+
hour: "2-digit",
|
|
41
|
+
minute: "2-digit",
|
|
42
|
+
hour12: false,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadConfig() {
|
|
47
|
+
try {
|
|
48
|
+
const raw = { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")) };
|
|
49
|
+
raw.pageSize = Math.max(5, Math.min(50, parseInt(raw.pageSize, 10) || 10));
|
|
50
|
+
raw.locale = typeof raw.locale === "string" ? raw.locale : "";
|
|
51
|
+
// Validate columns
|
|
52
|
+
if (!Array.isArray(raw.columns) || raw.columns.length === 0) {
|
|
53
|
+
raw.columns = DEFAULT_COLUMNS.slice();
|
|
54
|
+
} else {
|
|
55
|
+
raw.columns = raw.columns.filter((c) => ALL_COLUMN_IDS.includes(c));
|
|
56
|
+
if (raw.columns.length === 0) raw.columns = DEFAULT_COLUMNS.slice();
|
|
57
|
+
}
|
|
58
|
+
return raw;
|
|
59
|
+
} catch {
|
|
60
|
+
return { ...DEFAULT_CONFIG, columns: DEFAULT_COLUMNS.slice() };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function saveConfig(config) {
|
|
65
|
+
const dir = path.dirname(CONFIG_PATH);
|
|
66
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const CACHE_PATH = path.join(
|
|
71
|
+
os.homedir(),
|
|
72
|
+
".cache",
|
|
73
|
+
"total-reclaude",
|
|
74
|
+
"sessions.json"
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
function loadCache() {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function saveCache(cache) {
|
|
86
|
+
const dir = path.dirname(CACHE_PATH);
|
|
87
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(cache));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function extractProjName(projDir) {
|
|
92
|
+
const homePrefix = os.homedir().replace(/\//g, "-");
|
|
93
|
+
if (projDir.startsWith(homePrefix)) {
|
|
94
|
+
const rest = projDir.slice(homePrefix.length).replace(/^-/, "");
|
|
95
|
+
return rest || "~";
|
|
96
|
+
}
|
|
97
|
+
return projDir;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function extractFirstUserMessage(lines) {
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (!line || !line.includes('"user"')) continue;
|
|
103
|
+
try {
|
|
104
|
+
const d = JSON.parse(line);
|
|
105
|
+
if (d.type !== "user") continue;
|
|
106
|
+
const content = d.message && d.message.content;
|
|
107
|
+
if (!content) continue;
|
|
108
|
+
let text = "";
|
|
109
|
+
if (typeof content === "string") {
|
|
110
|
+
text = content;
|
|
111
|
+
} else if (Array.isArray(content)) {
|
|
112
|
+
for (const c of content) {
|
|
113
|
+
if (c && c.type === "text" && c.text) {
|
|
114
|
+
text = c.text;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
text = text.trim().split("\n")[0].trim();
|
|
120
|
+
if (text && !text.startsWith("[")) return text;
|
|
121
|
+
} catch {} // malformed JSONL lines are expected, skip
|
|
122
|
+
}
|
|
123
|
+
return "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseSessionFile(fpath) {
|
|
127
|
+
const content = fs.readFileSync(fpath, "utf8");
|
|
128
|
+
const lines = content.split("\n");
|
|
129
|
+
|
|
130
|
+
const titles = [];
|
|
131
|
+
let cwd = null;
|
|
132
|
+
let sid = null;
|
|
133
|
+
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (!line) continue;
|
|
136
|
+
if (line.includes('"custom-title"')) {
|
|
137
|
+
try {
|
|
138
|
+
const d = JSON.parse(line);
|
|
139
|
+
if (d.type === "custom-title") titles.push(d);
|
|
140
|
+
} catch {} // malformed JSONL lines are expected, skip
|
|
141
|
+
}
|
|
142
|
+
if (!cwd && line.includes('"cwd"')) {
|
|
143
|
+
try {
|
|
144
|
+
const d = JSON.parse(line);
|
|
145
|
+
if (d.cwd) cwd = d.cwd;
|
|
146
|
+
} catch {} // malformed JSONL lines are expected, skip
|
|
147
|
+
}
|
|
148
|
+
if (!sid && line.includes('"sessionId"')) {
|
|
149
|
+
try {
|
|
150
|
+
const d = JSON.parse(line);
|
|
151
|
+
if (d.sessionId) sid = d.sessionId;
|
|
152
|
+
} catch {} // malformed JSONL lines are expected, skip
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let title = "";
|
|
157
|
+
let sessionId = sid || "";
|
|
158
|
+
|
|
159
|
+
if (titles.length > 0) {
|
|
160
|
+
const last = titles[titles.length - 1];
|
|
161
|
+
title = last.customTitle || "";
|
|
162
|
+
sessionId = last.sessionId || sessionId;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const firstMessage = extractFirstUserMessage(lines);
|
|
166
|
+
if (!title && !firstMessage) return null;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
sid: sessionId,
|
|
170
|
+
title,
|
|
171
|
+
firstMessage,
|
|
172
|
+
cwd: cwd || "",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getSessions() {
|
|
177
|
+
const projectsDir = path.join(os.homedir(), ".claude", "projects");
|
|
178
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
179
|
+
|
|
180
|
+
const cache = loadCache();
|
|
181
|
+
const cachedDirs = cache.dirs || {};
|
|
182
|
+
const newDirs = {};
|
|
183
|
+
const sessions = [];
|
|
184
|
+
let dirty = false;
|
|
185
|
+
|
|
186
|
+
for (const projDir of fs.readdirSync(projectsDir)) {
|
|
187
|
+
const projPath = path.join(projectsDir, projDir);
|
|
188
|
+
let dirStat;
|
|
189
|
+
try {
|
|
190
|
+
dirStat = fs.statSync(projPath);
|
|
191
|
+
} catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!dirStat.isDirectory()) continue;
|
|
195
|
+
|
|
196
|
+
const dirMtime = dirStat.mtimeMs;
|
|
197
|
+
const cachedDir = cachedDirs[projDir];
|
|
198
|
+
const proj = extractProjName(projDir);
|
|
199
|
+
|
|
200
|
+
if (cachedDir && cachedDir.dmtime === dirMtime) {
|
|
201
|
+
newDirs[projDir] = cachedDir;
|
|
202
|
+
const files = cachedDir.files || {};
|
|
203
|
+
for (const [fname, entry] of Object.entries(files)) {
|
|
204
|
+
sessions.push({ mtime: entry.mtime, proj, ...entry.data, fpath: path.join(projPath, fname) });
|
|
205
|
+
}
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
dirty = true;
|
|
210
|
+
const cachedFiles = (cachedDir && cachedDir.files) || {};
|
|
211
|
+
const newFiles = {};
|
|
212
|
+
|
|
213
|
+
let files;
|
|
214
|
+
try {
|
|
215
|
+
files = fs.readdirSync(projPath).filter((f) => f.endsWith(".jsonl"));
|
|
216
|
+
} catch {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
if (file.includes("subagents")) continue;
|
|
222
|
+
const fpath = path.join(projPath, file);
|
|
223
|
+
|
|
224
|
+
let mtime;
|
|
225
|
+
try {
|
|
226
|
+
mtime = fs.statSync(fpath).mtimeMs;
|
|
227
|
+
} catch {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const cachedFile = cachedFiles[file];
|
|
232
|
+
if (cachedFile && cachedFile.mtime === mtime) {
|
|
233
|
+
newFiles[file] = cachedFile;
|
|
234
|
+
sessions.push({ mtime, proj, ...cachedFile.data, fpath });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const result = parseSessionFile(fpath);
|
|
240
|
+
if (!result) continue;
|
|
241
|
+
newFiles[file] = { mtime, data: result };
|
|
242
|
+
sessions.push({ mtime, proj, ...result, fpath });
|
|
243
|
+
} catch {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
newDirs[projDir] = { dmtime: dirMtime, files: newFiles };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (dirty || Object.keys(newDirs).length !== Object.keys(cachedDirs).length) {
|
|
252
|
+
saveCache({ dirs: newDirs });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
256
|
+
return sessions;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// East Asian wide character width calculation
|
|
260
|
+
function charWidth(code) {
|
|
261
|
+
if (
|
|
262
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
263
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
264
|
+
(code >= 0x3040 && code <= 0x33bf) ||
|
|
265
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
266
|
+
(code >= 0x4e00 && code <= 0xa4cf) ||
|
|
267
|
+
(code >= 0xac00 && code <= 0xd7af) ||
|
|
268
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
269
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
270
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
271
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
272
|
+
(code >= 0x20000 && code <= 0x2fffd) ||
|
|
273
|
+
(code >= 0x30000 && code <= 0x3fffd)
|
|
274
|
+
) {
|
|
275
|
+
return 2;
|
|
276
|
+
}
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function strWidth(str) {
|
|
281
|
+
let w = 0;
|
|
282
|
+
for (const ch of str) {
|
|
283
|
+
w += charWidth(ch.codePointAt(0));
|
|
284
|
+
}
|
|
285
|
+
return w;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function sliceByWidth(str, maxWidth) {
|
|
289
|
+
let w = 0;
|
|
290
|
+
let i = 0;
|
|
291
|
+
for (const ch of str) {
|
|
292
|
+
const cw = charWidth(ch.codePointAt(0));
|
|
293
|
+
if (w + cw > maxWidth) break;
|
|
294
|
+
w += cw;
|
|
295
|
+
i += ch.length;
|
|
296
|
+
}
|
|
297
|
+
return str.slice(0, i);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function padEndByWidth(str, targetWidth) {
|
|
301
|
+
const w = strWidth(str);
|
|
302
|
+
if (w >= targetWidth) return str;
|
|
303
|
+
return str + " ".repeat(targetWidth - w);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function renderColumnValue(colId, session, config) {
|
|
307
|
+
const def = COLUMN_DEFS[colId];
|
|
308
|
+
const raw = def.get(session, config);
|
|
309
|
+
const sliced = sliceByWidth(raw, def.width);
|
|
310
|
+
return padEndByWidth(sliced, def.width);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function render(filtered, page, inputBuf, mode, config, cursor) {
|
|
314
|
+
const cols = config.columns;
|
|
315
|
+
const total = filtered.length;
|
|
316
|
+
const totalPages = Math.max(1, Math.ceil(total / config.pageSize));
|
|
317
|
+
page = Math.max(0, Math.min(page, totalPages - 1));
|
|
318
|
+
const start = page * config.pageSize;
|
|
319
|
+
const end = Math.min(start + config.pageSize, total);
|
|
320
|
+
|
|
321
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
322
|
+
console.log();
|
|
323
|
+
console.log(" \x1b[1mClaude Code Sessions\x1b[0m");
|
|
324
|
+
console.log(
|
|
325
|
+
" \x1b[2m─────────────────────────────────────────────────────────────\x1b[0m"
|
|
326
|
+
);
|
|
327
|
+
if (mode === "filter_name") {
|
|
328
|
+
console.log(` \x1b[33mName filter: ${inputBuf}\x1b[0m`);
|
|
329
|
+
} else if (mode === "filter_path") {
|
|
330
|
+
console.log(` \x1b[33mPath filter: ${inputBuf}\x1b[0m`);
|
|
331
|
+
}
|
|
332
|
+
console.log();
|
|
333
|
+
|
|
334
|
+
if (total === 0) {
|
|
335
|
+
console.log(" \x1b[2mNo matching sessions.\x1b[0m");
|
|
336
|
+
} else {
|
|
337
|
+
// Header
|
|
338
|
+
const headerParts = cols.map((c) => COLUMN_DEFS[c].label.padEnd(COLUMN_DEFS[c].width));
|
|
339
|
+
console.log(` \x1b[2m ${headerParts.join(" ")}\x1b[0m`);
|
|
340
|
+
console.log();
|
|
341
|
+
|
|
342
|
+
for (let i = start; i < end; i++) {
|
|
343
|
+
const s = filtered[i];
|
|
344
|
+
const isCursor = i === cursor;
|
|
345
|
+
const pointer = isCursor ? "\x1b[1;33m>\x1b[0m" : " ";
|
|
346
|
+
|
|
347
|
+
// Build column values
|
|
348
|
+
const colValues = cols.map((c) => renderColumnValue(c, s, config));
|
|
349
|
+
|
|
350
|
+
// Title column gets special styling; others are dim
|
|
351
|
+
const titleIdx = cols.indexOf("title");
|
|
352
|
+
let line = "";
|
|
353
|
+
for (let ci = 0; ci < cols.length; ci++) {
|
|
354
|
+
if (ci > 0) line += " ";
|
|
355
|
+
if (ci === titleIdx) {
|
|
356
|
+
// Title column: styled based on named/cursor
|
|
357
|
+
if (s.title) {
|
|
358
|
+
const hl = isCursor ? "\x1b[1;33m" : "\x1b[1m";
|
|
359
|
+
line += `${hl}${colValues[ci]}\x1b[0m`;
|
|
360
|
+
} else {
|
|
361
|
+
const hl = isCursor ? "\x1b[33m" : "\x1b[2;3m";
|
|
362
|
+
line += `${hl}${colValues[ci]}\x1b[0m`;
|
|
363
|
+
}
|
|
364
|
+
} else {
|
|
365
|
+
line += `\x1b[2m${colValues[ci]}\x1b[0m`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const num = String(i + 1).padStart(3);
|
|
370
|
+
const numStyle = s.title ? "\x1b[1;36m" : "\x1b[2m";
|
|
371
|
+
console.log(` ${pointer} ${numStyle}${num}\x1b[0m ${line}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log();
|
|
376
|
+
console.log(
|
|
377
|
+
` \x1b[2mPage ${page + 1}/${totalPages} \u2190/\u2192 page | \u2191/\u2193 select | Enter resume | r rename | / name | p path | s settings | q quit\x1b[0m`
|
|
378
|
+
);
|
|
379
|
+
console.log();
|
|
380
|
+
|
|
381
|
+
return { page, totalPages };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function renderSettings(config, settingsCursor) {
|
|
385
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(" \x1b[1mSettings\x1b[0m");
|
|
388
|
+
console.log(
|
|
389
|
+
" \x1b[2m─────────────────────────────────────────────────────────────\x1b[0m"
|
|
390
|
+
);
|
|
391
|
+
console.log();
|
|
392
|
+
|
|
393
|
+
const items = [
|
|
394
|
+
{ label: `Page size: \x1b[1m${config.pageSize}\x1b[0m \x1b[2m(-/+ to adjust)\x1b[0m`, type: "pageSize" },
|
|
395
|
+
{ label: `Locale: \x1b[1m${config.locale || "system default"}\x1b[0m \x1b[2m(Enter to edit)\x1b[0m`, type: "locale" },
|
|
396
|
+
{ label: "", type: "separator" },
|
|
397
|
+
{ label: "\x1b[1mColumns\x1b[0m \x1b[2m(Space toggle, Shift+↑/↓ reorder)\x1b[0m", type: "header" },
|
|
398
|
+
];
|
|
399
|
+
|
|
400
|
+
// Add column items
|
|
401
|
+
for (const colId of ALL_COLUMN_IDS) {
|
|
402
|
+
const enabled = config.columns.includes(colId);
|
|
403
|
+
const def = COLUMN_DEFS[colId];
|
|
404
|
+
const orderNum = enabled ? config.columns.indexOf(colId) + 1 : "-";
|
|
405
|
+
items.push({ label: def.label, type: "column", colId, enabled, orderNum });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (let i = 0; i < items.length; i++) {
|
|
409
|
+
const item = items[i];
|
|
410
|
+
if (item.type === "separator") {
|
|
411
|
+
console.log();
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (item.type === "header") {
|
|
415
|
+
console.log(` ${item.label}`);
|
|
416
|
+
console.log();
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const pointer = i === settingsCursor ? "\x1b[1;33m>\x1b[0m" : " ";
|
|
421
|
+
|
|
422
|
+
if (item.type === "column") {
|
|
423
|
+
const check = item.enabled ? "\x1b[1;32m[x]\x1b[0m" : "\x1b[2m[ ]\x1b[0m";
|
|
424
|
+
const order = item.enabled ? `\x1b[1;36m${String(item.orderNum).padStart(2)}\x1b[0m` : "\x1b[2m -\x1b[0m";
|
|
425
|
+
console.log(` ${pointer} ${check} ${order} ${item.label}`);
|
|
426
|
+
} else {
|
|
427
|
+
console.log(` ${pointer} ${item.label}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
console.log();
|
|
432
|
+
console.log(
|
|
433
|
+
" \x1b[2m\u2191/\u2193 navigate | Space toggle | Shift+\u2191/\u2193 reorder | -/+ page size | q/ESC back\x1b[0m"
|
|
434
|
+
);
|
|
435
|
+
console.log();
|
|
436
|
+
|
|
437
|
+
return items;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function renderLocaleEditor(_config, localeBuf) {
|
|
441
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
442
|
+
console.log();
|
|
443
|
+
console.log(" \x1b[1mSet Date Locale\x1b[0m");
|
|
444
|
+
console.log(
|
|
445
|
+
" \x1b[2m─────────────────────────────────────────────────────────────\x1b[0m"
|
|
446
|
+
);
|
|
447
|
+
console.log();
|
|
448
|
+
console.log(` \x1b[33mLocale: ${localeBuf || "(empty = system default)"}\x1b[0m`);
|
|
449
|
+
console.log();
|
|
450
|
+
|
|
451
|
+
const preview = new Date().toLocaleString(localeBuf || undefined, {
|
|
452
|
+
year: "numeric",
|
|
453
|
+
month: "2-digit",
|
|
454
|
+
day: "2-digit",
|
|
455
|
+
hour: "2-digit",
|
|
456
|
+
minute: "2-digit",
|
|
457
|
+
hour12: false,
|
|
458
|
+
});
|
|
459
|
+
console.log(` \x1b[2mPreview: ${preview}\x1b[0m`);
|
|
460
|
+
console.log();
|
|
461
|
+
console.log(
|
|
462
|
+
" \x1b[2mType locale (e.g. en-US, ko-KR, sv-SE) | Enter confirm | ESC cancel\x1b[0m"
|
|
463
|
+
);
|
|
464
|
+
console.log();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function renderRename(session, renameBuf) {
|
|
468
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
469
|
+
console.log();
|
|
470
|
+
console.log(" \x1b[1mRename Session\x1b[0m");
|
|
471
|
+
console.log(
|
|
472
|
+
" \x1b[2m─────────────────────────────────────────────────────────────\x1b[0m"
|
|
473
|
+
);
|
|
474
|
+
console.log();
|
|
475
|
+
const displayName = session.title || session.firstMessage;
|
|
476
|
+
console.log(` \x1b[2mCurrent:\x1b[0m \x1b[1m${displayName}\x1b[0m`);
|
|
477
|
+
console.log(` \x1b[33mNew title: ${renameBuf}\x1b[0m \x1b[2m(max 100 chars)\x1b[0m`);
|
|
478
|
+
console.log();
|
|
479
|
+
console.log(
|
|
480
|
+
" \x1b[2mEnter confirm | ESC cancel\x1b[0m"
|
|
481
|
+
);
|
|
482
|
+
console.log();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function renameSession(session, newTitle) {
|
|
486
|
+
const cleaned = newTitle.replace(/[\x00-\x1f\x7f]/g, "").slice(0, 100);
|
|
487
|
+
if (!cleaned) return false;
|
|
488
|
+
const entry = {
|
|
489
|
+
type: "custom-title",
|
|
490
|
+
sessionId: session.sid,
|
|
491
|
+
customTitle: cleaned,
|
|
492
|
+
};
|
|
493
|
+
fs.appendFileSync(session.fpath, "\n" + JSON.stringify(entry));
|
|
494
|
+
session.title = cleaned;
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function resumeSession(s) {
|
|
499
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
500
|
+
console.log(`\n Resuming: ${s.title || s.firstMessage} (${s.sid.slice(0, 8)})\n`);
|
|
501
|
+
|
|
502
|
+
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
503
|
+
process.stdin.setRawMode(false);
|
|
504
|
+
const useShell = process.platform === "win32";
|
|
505
|
+
spawnSync("claude", ["--resume", s.sid], {
|
|
506
|
+
stdio: "inherit",
|
|
507
|
+
cwd,
|
|
508
|
+
shell: useShell,
|
|
509
|
+
});
|
|
510
|
+
process.exit(0);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function parseKey(buf) {
|
|
514
|
+
if (buf[0] === 0x1b) {
|
|
515
|
+
if (buf.length === 1) return "escape";
|
|
516
|
+
if (buf[1] === 0x5b) {
|
|
517
|
+
if (buf[2] === 0x44) return "left";
|
|
518
|
+
if (buf[2] === 0x43) return "right";
|
|
519
|
+
if (buf[2] === 0x41) return "up";
|
|
520
|
+
if (buf[2] === 0x42) return "down";
|
|
521
|
+
// Shift+Up: ESC [ 1 ; 2 A
|
|
522
|
+
if (buf[2] === 0x31 && buf[3] === 0x3b && buf[4] === 0x32) {
|
|
523
|
+
if (buf[5] === 0x41) return "shift_up";
|
|
524
|
+
if (buf[5] === 0x42) return "shift_down";
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
if (buf[0] === 0x03) return "quit"; // Ctrl+C
|
|
531
|
+
if (buf[0] === 0x0d || buf[0] === 0x0a) return "enter";
|
|
532
|
+
if (buf[0] === 0x7f) return "backspace";
|
|
533
|
+
const ch = buf.toString("utf8");
|
|
534
|
+
if (ch.length === 1 && ch.charCodeAt(0) >= 32) return ch;
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const SPLASH = [
|
|
539
|
+
"",
|
|
540
|
+
" \x1b[1;38;2;200;235;255m ████████ ████████ ████████ ██ ██ \x1b[0m",
|
|
541
|
+
" \x1b[1;38;2;175;225;248m ██ ██ ██ ██ ██ ██ \x1b[0m",
|
|
542
|
+
" \x1b[1;38;2;150;215;240m ██ ██ ██ ██ ████ ██ \x1b[0m",
|
|
543
|
+
" \x1b[1;38;2;125;200;230m ██ ██ ██ ██ ████ ██ \x1b[0m",
|
|
544
|
+
" \x1b[1;38;2;100;185;220m ██ ██ ██ ██ ██ ██ ██ \x1b[0m",
|
|
545
|
+
" \x1b[1;38;2;75;165;210m ██ ██ ██ ██ ██ ██ ██ \x1b[0m",
|
|
546
|
+
" \x1b[1;38;2;55;150;200m ██ ██ ██ ██ ████████ ██ \x1b[0m",
|
|
547
|
+
" \x1b[1;38;2;35;130;185m ██ ████████ ██ ██ ██ ████████\x1b[0m",
|
|
548
|
+
"",
|
|
549
|
+
" \x1b[1;38;2;200;235;255m███████ ████████ ████████ ██ ██ ██ ██ ███████ ████████\x1b[0m",
|
|
550
|
+
" \x1b[1;38;2;175;225;248m██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \x1b[0m",
|
|
551
|
+
" \x1b[1;38;2;150;215;240m██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ \x1b[0m",
|
|
552
|
+
" \x1b[1;38;2;125;200;230m███████ ███████ ██ ██ ████ ██ ██ ██ ██ ████████\x1b[0m",
|
|
553
|
+
" \x1b[1;38;2;100;185;220m██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \x1b[0m",
|
|
554
|
+
" \x1b[1;38;2;75;165;210m██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ \x1b[0m",
|
|
555
|
+
" \x1b[1;38;2;55;150;200m██ ██ ██ ██ ██ ████████ ██ ██ ██ ██ ██ \x1b[0m",
|
|
556
|
+
" \x1b[1;38;2;35;130;185m██ ██ ████████ ████████ ████████ ██ ██ ████████ ███████ ████████\x1b[0m",
|
|
557
|
+
"",
|
|
558
|
+
" \x1b[38;2;120;160;180m GET READY FOR THE RESUME OF YOUR SESSIONS\x1b[0m",
|
|
559
|
+
"",
|
|
560
|
+
" \x1b[2;3m Loading sessions...\x1b[0m",
|
|
561
|
+
"",
|
|
562
|
+
].join("\n");
|
|
563
|
+
|
|
564
|
+
function showSplash() {
|
|
565
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
566
|
+
process.stdout.write(SPLASH + "\n");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const REPO = "dongwook-chan/total-reclaude";
|
|
570
|
+
|
|
571
|
+
function isRepoStarred() {
|
|
572
|
+
try {
|
|
573
|
+
const res = spawnSync("gh", ["api", `user/starred/${REPO}`], { encoding: "utf8", timeout: 5000 });
|
|
574
|
+
return res.status === 0; // 204 (exit 0) = starred, 404 (exit 1) = not
|
|
575
|
+
} catch {
|
|
576
|
+
return true; // if gh fails, assume starred (don't bother user)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function starRepo() {
|
|
581
|
+
try {
|
|
582
|
+
spawnSync("gh", ["api", "-X", "PUT", `user/starred/${REPO}`], { encoding: "utf8", timeout: 5000 });
|
|
583
|
+
} catch {} // best effort
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function showStarPrompt() {
|
|
587
|
+
try {
|
|
588
|
+
// Check if gh is available
|
|
589
|
+
const which = spawnSync("which", ["gh"], { encoding: "utf8" });
|
|
590
|
+
if (which.status !== 0) return;
|
|
591
|
+
|
|
592
|
+
if (isRepoStarred()) return;
|
|
593
|
+
|
|
594
|
+
process.stdout.write(`\x1b[33m \u2605 Enjoy total-reclaude? Star it on GitHub! (y/n) \x1b[0m`);
|
|
595
|
+
|
|
596
|
+
// Single keypress without Enter via bash read -n1
|
|
597
|
+
const result = spawnSync("bash", ["-c", 'read -rsn1 key < /dev/tty; echo -n "$key"'], { encoding: "utf8", timeout: 10000 });
|
|
598
|
+
const key = (result.stdout || "").trim();
|
|
599
|
+
|
|
600
|
+
if (key === "y" || key === "Y") {
|
|
601
|
+
starRepo();
|
|
602
|
+
console.log(`\x1b[32m Thank you! \u2605\x1b[0m`);
|
|
603
|
+
} else {
|
|
604
|
+
console.log();
|
|
605
|
+
}
|
|
606
|
+
} catch {} // never let this break the exit flow
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function cleanup() {
|
|
610
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
611
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function main() {
|
|
615
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
616
|
+
console.log("Usage: total-reclaude\n");
|
|
617
|
+
console.log("Interactive Claude Code session picker: filter, rename, and resume sessions.\n");
|
|
618
|
+
console.log("Keybindings:");
|
|
619
|
+
console.log(" Up/Down Move cursor");
|
|
620
|
+
console.log(" Enter Resume selected session");
|
|
621
|
+
console.log(" r Rename selected session");
|
|
622
|
+
console.log(" / Filter by name");
|
|
623
|
+
console.log(" p Filter by path");
|
|
624
|
+
console.log(" s Settings (columns, page size, locale)");
|
|
625
|
+
console.log(" Left/Right Page navigation");
|
|
626
|
+
console.log(" q, Ctrl+C Quit");
|
|
627
|
+
console.log(" ESC Cancel / back\n");
|
|
628
|
+
console.log("Available columns: " + ALL_COLUMN_IDS.join(", "));
|
|
629
|
+
console.log("Config: " + CONFIG_PATH);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
633
|
+
console.log(require("./package.json").version);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (!process.stdin.isTTY) {
|
|
638
|
+
console.error("total-reclaude requires an interactive terminal.");
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
showSplash();
|
|
643
|
+
showStarPrompt();
|
|
644
|
+
const { execSync } = require("child_process");
|
|
645
|
+
execSync("sleep 1");
|
|
646
|
+
const allSessions = getSessions();
|
|
647
|
+
if (allSessions.length === 0) {
|
|
648
|
+
console.log("No sessions found.");
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let config = loadConfig();
|
|
653
|
+
let page = 0;
|
|
654
|
+
let totalPages = 1;
|
|
655
|
+
let inputBuf = "";
|
|
656
|
+
let mode = "normal";
|
|
657
|
+
let cursor = 0;
|
|
658
|
+
let settingsCursor = 0;
|
|
659
|
+
let settingsItems = [];
|
|
660
|
+
let settingsDirty = false;
|
|
661
|
+
let filtered = allSessions.slice();
|
|
662
|
+
|
|
663
|
+
process.on("exit", cleanup);
|
|
664
|
+
process.on("uncaughtException", (err) => {
|
|
665
|
+
cleanup();
|
|
666
|
+
console.error(err);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
({ page, totalPages } = render(filtered, page, inputBuf, mode, config, cursor));
|
|
671
|
+
|
|
672
|
+
function applyFilter() {
|
|
673
|
+
const q = inputBuf.toLowerCase();
|
|
674
|
+
if (!q) return allSessions.slice();
|
|
675
|
+
if (mode === "filter_name") {
|
|
676
|
+
return allSessions.filter((s) => s.title.toLowerCase().includes(q) || s.firstMessage.toLowerCase().includes(q));
|
|
677
|
+
} else if (mode === "filter_path") {
|
|
678
|
+
return allSessions.filter(
|
|
679
|
+
(s) =>
|
|
680
|
+
s.proj.toLowerCase().includes(q) || s.cwd.toLowerCase().includes(q)
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return allSessions.slice();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function rerender() {
|
|
687
|
+
if (filtered.length > 0) {
|
|
688
|
+
cursor = Math.max(0, Math.min(cursor, filtered.length - 1));
|
|
689
|
+
} else {
|
|
690
|
+
cursor = 0;
|
|
691
|
+
}
|
|
692
|
+
const cursorPage = Math.floor(cursor / config.pageSize);
|
|
693
|
+
page = cursorPage;
|
|
694
|
+
({ page, totalPages } = render(filtered, page, inputBuf, mode, config, cursor));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Settings helpers
|
|
698
|
+
function isSelectableSettingsItem(idx) {
|
|
699
|
+
const item = settingsItems[idx];
|
|
700
|
+
return item && item.type !== "separator" && item.type !== "header";
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function moveSettingsCursor(dir) {
|
|
704
|
+
let next = settingsCursor + dir;
|
|
705
|
+
while (next >= 0 && next < settingsItems.length && !isSelectableSettingsItem(next)) {
|
|
706
|
+
next += dir;
|
|
707
|
+
}
|
|
708
|
+
if (next >= 0 && next < settingsItems.length) {
|
|
709
|
+
settingsCursor = next;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function rerenderSettings() {
|
|
714
|
+
settingsItems = renderSettings(config, settingsCursor);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
process.stdin.setRawMode(true);
|
|
718
|
+
process.stdin.resume();
|
|
719
|
+
|
|
720
|
+
process.stdin.on("data", (buf) => {
|
|
721
|
+
const key = parseKey(buf);
|
|
722
|
+
if (key === null) return;
|
|
723
|
+
|
|
724
|
+
if (key === "quit") {
|
|
725
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
726
|
+
process.stdin.setRawMode(false);
|
|
727
|
+
process.exit(0);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Settings mode
|
|
731
|
+
if (mode === "settings") {
|
|
732
|
+
if (key === "q" || key === "escape") {
|
|
733
|
+
if (settingsDirty) {
|
|
734
|
+
saveConfig(config);
|
|
735
|
+
settingsDirty = false;
|
|
736
|
+
}
|
|
737
|
+
mode = "normal";
|
|
738
|
+
rerender();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (key === "up") {
|
|
743
|
+
moveSettingsCursor(-1);
|
|
744
|
+
rerenderSettings();
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (key === "down") {
|
|
748
|
+
moveSettingsCursor(1);
|
|
749
|
+
rerenderSettings();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const currentItem = settingsItems[settingsCursor];
|
|
754
|
+
|
|
755
|
+
if (key === "-") {
|
|
756
|
+
config.pageSize = Math.max(5, config.pageSize - 5);
|
|
757
|
+
settingsDirty = true;
|
|
758
|
+
rerenderSettings();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (key === "=" || key === "+") {
|
|
762
|
+
config.pageSize = Math.min(50, config.pageSize + 5);
|
|
763
|
+
settingsDirty = true;
|
|
764
|
+
rerenderSettings();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (currentItem && currentItem.type === "locale" && key === "enter") {
|
|
769
|
+
if (settingsDirty) {
|
|
770
|
+
saveConfig(config);
|
|
771
|
+
settingsDirty = false;
|
|
772
|
+
}
|
|
773
|
+
mode = "settings_locale";
|
|
774
|
+
inputBuf = config.locale || "";
|
|
775
|
+
renderLocaleEditor(config, inputBuf);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (currentItem && currentItem.type === "column") {
|
|
780
|
+
if (key === " ") {
|
|
781
|
+
// Toggle column
|
|
782
|
+
const colId = currentItem.colId;
|
|
783
|
+
const idx = config.columns.indexOf(colId);
|
|
784
|
+
if (idx >= 0) {
|
|
785
|
+
// Don't allow removing last column
|
|
786
|
+
if (config.columns.length > 1) {
|
|
787
|
+
config.columns.splice(idx, 1);
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
config.columns.push(colId);
|
|
791
|
+
}
|
|
792
|
+
settingsDirty = true;
|
|
793
|
+
rerenderSettings();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (key === "shift_up" || key === "shift_down") {
|
|
798
|
+
const colId = currentItem.colId;
|
|
799
|
+
const idx = config.columns.indexOf(colId);
|
|
800
|
+
if (idx < 0) return; // not enabled, can't reorder
|
|
801
|
+
const newIdx = key === "shift_up" ? idx - 1 : idx + 1;
|
|
802
|
+
if (newIdx < 0 || newIdx >= config.columns.length) return;
|
|
803
|
+
// Swap
|
|
804
|
+
[config.columns[idx], config.columns[newIdx]] = [config.columns[newIdx], config.columns[idx]];
|
|
805
|
+
settingsDirty = true;
|
|
806
|
+
rerenderSettings();
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Rename input mode
|
|
815
|
+
if (mode === "rename_input") {
|
|
816
|
+
if (key === "escape") {
|
|
817
|
+
mode = "normal";
|
|
818
|
+
inputBuf = "";
|
|
819
|
+
rerender();
|
|
820
|
+
} else if (key === "enter" && inputBuf) {
|
|
821
|
+
renameSession(filtered[cursor], inputBuf);
|
|
822
|
+
mode = "normal";
|
|
823
|
+
inputBuf = "";
|
|
824
|
+
rerender();
|
|
825
|
+
} else if (key === "backspace") {
|
|
826
|
+
inputBuf = inputBuf.slice(0, -1);
|
|
827
|
+
renderRename(filtered[cursor], inputBuf);
|
|
828
|
+
} else if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
829
|
+
if (inputBuf.length < 100) {
|
|
830
|
+
inputBuf += key;
|
|
831
|
+
}
|
|
832
|
+
renderRename(filtered[cursor], inputBuf);
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Locale editor mode
|
|
838
|
+
if (mode === "settings_locale") {
|
|
839
|
+
if (key === "escape") {
|
|
840
|
+
mode = "settings";
|
|
841
|
+
inputBuf = "";
|
|
842
|
+
rerenderSettings();
|
|
843
|
+
} else if (key === "enter") {
|
|
844
|
+
config.locale = inputBuf;
|
|
845
|
+
settingsDirty = true;
|
|
846
|
+
mode = "settings";
|
|
847
|
+
inputBuf = "";
|
|
848
|
+
rerenderSettings();
|
|
849
|
+
} else if (key === "backspace") {
|
|
850
|
+
inputBuf = inputBuf.slice(0, -1);
|
|
851
|
+
renderLocaleEditor(config, inputBuf);
|
|
852
|
+
} else if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
853
|
+
inputBuf += key;
|
|
854
|
+
renderLocaleEditor(config, inputBuf);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Arrow navigation
|
|
860
|
+
if (key === "up") {
|
|
861
|
+
if (cursor > 0) {
|
|
862
|
+
cursor -= 1;
|
|
863
|
+
rerender();
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (key === "down") {
|
|
868
|
+
if (cursor < filtered.length - 1) {
|
|
869
|
+
cursor += 1;
|
|
870
|
+
rerender();
|
|
871
|
+
}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (key === "left") {
|
|
875
|
+
if (page > 0) {
|
|
876
|
+
page -= 1;
|
|
877
|
+
cursor = page * config.pageSize;
|
|
878
|
+
rerender();
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (key === "right") {
|
|
883
|
+
if (page < totalPages - 1) {
|
|
884
|
+
page += 1;
|
|
885
|
+
cursor = page * config.pageSize;
|
|
886
|
+
rerender();
|
|
887
|
+
}
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (key === "escape") {
|
|
892
|
+
if (mode !== "normal") {
|
|
893
|
+
mode = "normal";
|
|
894
|
+
inputBuf = "";
|
|
895
|
+
filtered = allSessions.slice();
|
|
896
|
+
cursor = 0;
|
|
897
|
+
page = 0;
|
|
898
|
+
rerender();
|
|
899
|
+
}
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
if (key === "enter") {
|
|
904
|
+
if (filtered.length > 0) {
|
|
905
|
+
resumeSession(filtered[cursor]);
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (mode === "normal") {
|
|
911
|
+
if (key === "q") {
|
|
912
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
913
|
+
process.stdin.setRawMode(false);
|
|
914
|
+
process.exit(0);
|
|
915
|
+
} else if (key === "/") {
|
|
916
|
+
mode = "filter_name";
|
|
917
|
+
inputBuf = "";
|
|
918
|
+
cursor = 0;
|
|
919
|
+
page = 0;
|
|
920
|
+
rerender();
|
|
921
|
+
} else if (key === "p") {
|
|
922
|
+
mode = "filter_path";
|
|
923
|
+
inputBuf = "";
|
|
924
|
+
cursor = 0;
|
|
925
|
+
page = 0;
|
|
926
|
+
rerender();
|
|
927
|
+
} else if (key === "r") {
|
|
928
|
+
if (filtered.length > 0) {
|
|
929
|
+
mode = "rename_input";
|
|
930
|
+
inputBuf = "";
|
|
931
|
+
renderRename(filtered[cursor], inputBuf);
|
|
932
|
+
}
|
|
933
|
+
} else if (key === "s") {
|
|
934
|
+
mode = "settings";
|
|
935
|
+
settingsDirty = false;
|
|
936
|
+
settingsCursor = 0;
|
|
937
|
+
settingsItems = renderSettings(config, settingsCursor);
|
|
938
|
+
}
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Filter mode
|
|
943
|
+
if (key === "backspace") {
|
|
944
|
+
if (inputBuf) {
|
|
945
|
+
inputBuf = inputBuf.slice(0, -1);
|
|
946
|
+
filtered = inputBuf ? applyFilter() : allSessions.slice();
|
|
947
|
+
} else {
|
|
948
|
+
mode = "normal";
|
|
949
|
+
filtered = allSessions.slice();
|
|
950
|
+
}
|
|
951
|
+
cursor = 0;
|
|
952
|
+
page = 0;
|
|
953
|
+
rerender();
|
|
954
|
+
} else if (key.length === 1 && key.charCodeAt(0) >= 32) {
|
|
955
|
+
inputBuf += key;
|
|
956
|
+
filtered = applyFilter();
|
|
957
|
+
cursor = 0;
|
|
958
|
+
page = 0;
|
|
959
|
+
rerender();
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reclaude",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive Claude Code session picker: filter, rename, and resume sessions from your terminal",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"reclaude": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["index.js", "LICENSE"],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=14"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["claude", "claude-code", "session", "picker", "cli", "resume", "rename"],
|
|
14
|
+
"author": "dongwook-chan",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/dongwook-chan/total-reclaude.git"
|
|
19
|
+
}
|
|
20
|
+
}
|