claude-ps 0.2.4 → 0.2.6

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.
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/session.ts
4
+ import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+ import pLimit from "p-limit";
8
+
9
+ // src/utils/cache.ts
10
+ import { readFile, stat } from "fs/promises";
11
+ var FileCache = class {
12
+ cache = /* @__PURE__ */ new Map();
13
+ maxSize;
14
+ ttl;
15
+ loader;
16
+ constructor(options) {
17
+ this.maxSize = options.maxSize ?? 50;
18
+ this.ttl = options.ttl ?? 5 * 60 * 1e3;
19
+ this.loader = options.loader;
20
+ }
21
+ /**
22
+ * 获取文件内容(优先从缓存读取)
23
+ * @param path 文件路径
24
+ * @returns 文件内容
25
+ */
26
+ async get(path) {
27
+ try {
28
+ const stats = await stat(path);
29
+ const currentMtime = stats.mtimeMs;
30
+ const cached = this.cache.get(path);
31
+ const now = Date.now();
32
+ if (cached && cached.mtimeMs === currentMtime && now - cached.cachedAt < this.ttl) {
33
+ this.cache.delete(path);
34
+ this.cache.set(path, cached);
35
+ return cached.data;
36
+ }
37
+ const data = await this.loader(path);
38
+ this.cache.set(path, {
39
+ data,
40
+ mtimeMs: currentMtime,
41
+ cachedAt: now
42
+ });
43
+ this.cleanup();
44
+ return data;
45
+ } catch {
46
+ return this.loader(path).catch(() => {
47
+ throw new Error(`Failed to load file: ${path}`);
48
+ });
49
+ }
50
+ }
51
+ /**
52
+ * 清理过期和超出大小限制的缓存
53
+ */
54
+ cleanup() {
55
+ const now = Date.now();
56
+ const entries = Array.from(this.cache.entries());
57
+ for (const [key, entry] of entries) {
58
+ if (now - entry.cachedAt >= this.ttl) {
59
+ this.cache.delete(key);
60
+ }
61
+ }
62
+ while (this.cache.size > this.maxSize) {
63
+ const firstKey = this.cache.keys().next().value;
64
+ if (firstKey) {
65
+ this.cache.delete(firstKey);
66
+ } else {
67
+ break;
68
+ }
69
+ }
70
+ }
71
+ /**
72
+ * 清除指定文件的缓存
73
+ * @param path 文件路径
74
+ */
75
+ clear(path) {
76
+ this.cache.delete(path);
77
+ }
78
+ /**
79
+ * 清除所有缓存
80
+ */
81
+ clearAll() {
82
+ this.cache.clear();
83
+ }
84
+ /**
85
+ * 获取缓存大小
86
+ */
87
+ get size() {
88
+ return this.cache.size;
89
+ }
90
+ };
91
+
92
+ // src/utils/session.ts
93
+ var statLimit = pLimit(10);
94
+ var sessionContentCache = new FileCache({
95
+ maxSize: 50,
96
+ ttl: 5 * 60 * 1e3,
97
+ // 5 分钟
98
+ loader: async (path) => {
99
+ return readFile2(path, "utf-8");
100
+ }
101
+ });
102
+ var MAX_MESSAGES = 100;
103
+ function cwdToProjectDir(cwd) {
104
+ return cwd.replace(/\//g, "-").replace(/^-/, "-");
105
+ }
106
+ async function getSessionPath(cwd, startTime) {
107
+ const projectDir = cwdToProjectDir(cwd);
108
+ const sessionsDir = join(homedir(), ".claude", "projects", projectDir);
109
+ try {
110
+ const files = await readdir(sessionsDir);
111
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
112
+ if (process.env.DEBUG_SESSION) {
113
+ console.error(`[DEBUG] cwd: ${cwd}`);
114
+ console.error(`[DEBUG] projectDir: ${projectDir}`);
115
+ console.error(`[DEBUG] sessionsDir: ${sessionsDir}`);
116
+ console.error(`[DEBUG] found ${jsonlFiles.length} jsonl files`);
117
+ }
118
+ if (jsonlFiles.length === 0) return "";
119
+ const fileStats = await Promise.all(
120
+ jsonlFiles.map(
121
+ (f) => statLimit(async () => {
122
+ const path = join(sessionsDir, f);
123
+ const s = await stat2(path);
124
+ return {
125
+ path,
126
+ birthtime: s.birthtime,
127
+ mtimeMs: s.mtimeMs,
128
+ size: s.size
129
+ };
130
+ })
131
+ )
132
+ );
133
+ if (process.env.DEBUG_SESSION) {
134
+ console.error(`[DEBUG] total files: ${fileStats.length}`);
135
+ for (const f of fileStats) {
136
+ console.error(
137
+ `[DEBUG] ${f.path.split("/").pop()}: ${f.size} bytes, birth: ${f.birthtime.toISOString()}, mtime: ${new Date(f.mtimeMs).toISOString()}`
138
+ );
139
+ }
140
+ if (startTime) {
141
+ console.error(`[DEBUG] startTime: ${startTime.toISOString()}`);
142
+ }
143
+ }
144
+ if (startTime && fileStats.length > 0) {
145
+ const startMs = startTime.getTime();
146
+ const birthtimeThreshold = 3e5;
147
+ const birthtimeMatched = fileStats.filter(
148
+ (f) => Math.abs(f.birthtime.getTime() - startMs) < birthtimeThreshold
149
+ ).sort((a, b) => b.mtimeMs - a.mtimeMs);
150
+ if (birthtimeMatched.length > 0) {
151
+ if (process.env.DEBUG_SESSION) {
152
+ console.error(
153
+ `[DEBUG] matched by birthtime: ${birthtimeMatched[0].path}`
154
+ );
155
+ }
156
+ return birthtimeMatched[0].path;
157
+ }
158
+ const mtimeThreshold = 6e5;
159
+ const mtimeMatched = fileStats.filter((f) => {
160
+ const mtimeDiff = f.mtimeMs - startMs;
161
+ return Math.abs(mtimeDiff) < mtimeThreshold;
162
+ }).sort(
163
+ (a, b) => Math.abs(a.mtimeMs - startMs) - Math.abs(b.mtimeMs - startMs)
164
+ );
165
+ if (mtimeMatched.length > 0) {
166
+ if (process.env.DEBUG_SESSION) {
167
+ console.error(`[DEBUG] matched by mtime: ${mtimeMatched[0].path}`);
168
+ }
169
+ return mtimeMatched[0].path;
170
+ }
171
+ }
172
+ fileStats.sort((a, b) => b.mtimeMs - a.mtimeMs);
173
+ if (process.env.DEBUG_SESSION && fileStats[0]) {
174
+ console.error(`[DEBUG] fallback to latest: ${fileStats[0].path}`);
175
+ }
176
+ return fileStats[0]?.path || "";
177
+ } catch {
178
+ return "";
179
+ }
180
+ }
181
+ async function getRecentMessages(sessionPath, limit = 5) {
182
+ if (!sessionPath) return [];
183
+ try {
184
+ const content = await readFile2(sessionPath, "utf-8");
185
+ const lines = content.trim().split("\n");
186
+ const messages = [];
187
+ for (const line of lines) {
188
+ try {
189
+ const entry = JSON.parse(line);
190
+ if (entry.type === "user" && entry.message?.content) {
191
+ const text = extractUserText(entry.message.content);
192
+ if (text && !isMetaMessage(text)) {
193
+ messages.push({
194
+ role: "user",
195
+ content: truncate(text, 100),
196
+ timestamp: entry.timestamp || ""
197
+ });
198
+ }
199
+ } else if (entry.type === "assistant" && entry.message?.content) {
200
+ const text = extractAssistantText(entry.message.content);
201
+ if (text) {
202
+ messages.push({
203
+ role: "assistant",
204
+ content: truncate(text, 100),
205
+ timestamp: entry.timestamp || ""
206
+ });
207
+ }
208
+ }
209
+ } catch {
210
+ }
211
+ }
212
+ return messages.slice(-limit);
213
+ } catch {
214
+ return [];
215
+ }
216
+ }
217
+ async function getNewMessages(sessionPath, fromLine) {
218
+ if (!sessionPath) return { messages: [], totalLines: 0 };
219
+ try {
220
+ const content = await sessionContentCache.get(sessionPath);
221
+ const lines = content.trim().split("\n");
222
+ const totalLines = lines.length;
223
+ const newLines = lines.slice(fromLine);
224
+ const messages = [];
225
+ for (const line of newLines) {
226
+ try {
227
+ const entry = JSON.parse(line);
228
+ if (entry.type === "user" && entry.message?.content) {
229
+ const text = extractUserText(entry.message.content);
230
+ if (text && !isMetaMessage(text)) {
231
+ messages.push({
232
+ role: "user",
233
+ content: truncate(text, 100),
234
+ timestamp: entry.timestamp || ""
235
+ });
236
+ }
237
+ } else if (entry.type === "assistant" && entry.message?.content) {
238
+ const text = extractAssistantText(entry.message.content);
239
+ if (text) {
240
+ messages.push({
241
+ role: "assistant",
242
+ content: truncate(text, 100),
243
+ timestamp: entry.timestamp || ""
244
+ });
245
+ }
246
+ }
247
+ } catch {
248
+ }
249
+ }
250
+ return { messages, totalLines };
251
+ } catch {
252
+ return { messages: [], totalLines: 0 };
253
+ }
254
+ }
255
+ async function getAllMessages(sessionPath) {
256
+ if (!sessionPath) return { messages: [], lineCount: 0 };
257
+ try {
258
+ const content = await sessionContentCache.get(sessionPath);
259
+ const lines = content.trim().split("\n");
260
+ const messages = [];
261
+ for (const line of lines) {
262
+ try {
263
+ const entry = JSON.parse(line);
264
+ if (entry.type === "user" && entry.message?.content) {
265
+ const text = extractUserText(entry.message.content);
266
+ if (text && !isMetaMessage(text)) {
267
+ messages.push({
268
+ role: "user",
269
+ content: truncate(text, 100),
270
+ timestamp: entry.timestamp || ""
271
+ });
272
+ }
273
+ } else if (entry.type === "assistant" && entry.message?.content) {
274
+ const text = extractAssistantText(entry.message.content);
275
+ if (text) {
276
+ messages.push({
277
+ role: "assistant",
278
+ content: truncate(text, 100),
279
+ timestamp: entry.timestamp || ""
280
+ });
281
+ }
282
+ }
283
+ } catch {
284
+ }
285
+ }
286
+ const limitedMessages = messages.length > MAX_MESSAGES ? messages.slice(-MAX_MESSAGES) : messages;
287
+ return { messages: limitedMessages, lineCount: lines.length };
288
+ } catch {
289
+ return { messages: [], lineCount: 0 };
290
+ }
291
+ }
292
+ function extractUserText(content) {
293
+ if (typeof content === "string") {
294
+ return content;
295
+ }
296
+ return "";
297
+ }
298
+ function extractAssistantText(content) {
299
+ if (!Array.isArray(content)) return "";
300
+ const textParts = content.filter((item) => item.type === "text" && item.text).map((item) => item.text);
301
+ return textParts.join("\n");
302
+ }
303
+ function isMetaMessage(text) {
304
+ return text.startsWith("<local-command") || text.startsWith("<command-name>") || text.startsWith("<command-message>");
305
+ }
306
+ function truncate(text, maxLen) {
307
+ const clean = text.replace(/\s+/g, " ").trim();
308
+ if (clean.length <= maxLen) return clean;
309
+ return `${clean.slice(0, maxLen - 3)}...`;
310
+ }
311
+ function formatSize(bytes) {
312
+ if (bytes < 1024) return `${bytes} B`;
313
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
314
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
315
+ }
316
+ function formatTimeDiff(date, referenceDate) {
317
+ const diffMs = date.getTime() - referenceDate.getTime();
318
+ const diffSec = Math.abs(diffMs / 1e3);
319
+ if (diffSec < 60) {
320
+ return diffMs >= 0 ? `\u542F\u52A8\u540E ${diffSec.toFixed(0)} \u79D2` : `\u542F\u52A8\u524D ${diffSec.toFixed(0)} \u79D2`;
321
+ }
322
+ const diffMin = diffSec / 60;
323
+ return diffMs >= 0 ? `\u542F\u52A8\u540E ${diffMin.toFixed(1)} \u5206\u949F` : `\u542F\u52A8\u524D ${diffMin.toFixed(1)} \u5206\u949F`;
324
+ }
325
+ function clearSessionCache(sessionPath) {
326
+ sessionContentCache.clear(sessionPath);
327
+ }
328
+ function clearAllSessionCache() {
329
+ sessionContentCache.clearAll();
330
+ }
331
+ async function debugSessionMatching(processes) {
332
+ const output = [];
333
+ output.push("=== Claude Code \u4F1A\u8BDD\u8C03\u8BD5\u4FE1\u606F ===\n");
334
+ if (processes.length === 0) {
335
+ output.push("\u672A\u627E\u5230\u8FD0\u884C\u4E2D\u7684 Claude Code \u8FDB\u7A0B\n");
336
+ return output.join("\n");
337
+ }
338
+ let successCount = 0;
339
+ for (let i = 0; i < processes.length; i++) {
340
+ const proc = processes[i];
341
+ output.push(`\u8FDB\u7A0B #${i + 1} (PID: ${proc.pid})`);
342
+ output.push(` \u5DE5\u4F5C\u76EE\u5F55: ${proc.cwd}`);
343
+ output.push(` \u542F\u52A8\u65F6\u95F4: ${proc.startTime.toISOString()}`);
344
+ const projectDir = cwdToProjectDir(proc.cwd);
345
+ const sessionsDir = join(homedir(), ".claude", "projects", projectDir);
346
+ output.push(` \u9879\u76EE\u76EE\u5F55: ${projectDir}`);
347
+ output.push(` \u4F1A\u8BDD\u76EE\u5F55: ${sessionsDir}`);
348
+ output.push("");
349
+ try {
350
+ const files = await readdir(sessionsDir);
351
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
352
+ if (jsonlFiles.length === 0) {
353
+ output.push(" \u26A0\uFE0F \u672A\u627E\u5230\u4F1A\u8BDD\u6587\u4EF6");
354
+ output.push("");
355
+ continue;
356
+ }
357
+ output.push(" \u627E\u5230\u7684\u4F1A\u8BDD\u6587\u4EF6:");
358
+ const fileStats = await Promise.all(
359
+ jsonlFiles.map(
360
+ (f) => statLimit(async () => {
361
+ const path = join(sessionsDir, f);
362
+ const s = await stat2(path);
363
+ return {
364
+ name: f,
365
+ path,
366
+ birthtime: s.birthtime,
367
+ mtime: new Date(s.mtimeMs),
368
+ size: s.size
369
+ };
370
+ })
371
+ )
372
+ );
373
+ for (const file of fileStats) {
374
+ const ignored = file.size < 1024;
375
+ const prefix = ignored ? " \u2717" : " \u2713";
376
+ output.push(`${prefix} ${file.name}`);
377
+ output.push(` \u5927\u5C0F: ${formatSize(file.size)}`);
378
+ if (ignored) {
379
+ output.push(" (< 1KB, \u5DF2\u5FFD\u7565)");
380
+ }
381
+ output.push(
382
+ ` \u521B\u5EFA: ${file.birthtime.toISOString()} (${formatTimeDiff(file.birthtime, proc.startTime)})`
383
+ );
384
+ output.push(
385
+ ` \u4FEE\u6539: ${file.mtime.toISOString()} (${formatTimeDiff(file.mtime, proc.startTime)})`
386
+ );
387
+ output.push("");
388
+ }
389
+ const startMs = proc.startTime.getTime();
390
+ const validFiles = fileStats.filter((f) => f.size >= 1024);
391
+ let matchedFile = null;
392
+ let matchStrategy = "";
393
+ const birthtimeThreshold = 3e5;
394
+ const birthtimeMatched = validFiles.filter(
395
+ (f) => Math.abs(f.birthtime.getTime() - startMs) < birthtimeThreshold
396
+ ).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
397
+ if (birthtimeMatched.length > 0) {
398
+ matchedFile = birthtimeMatched[0];
399
+ matchStrategy = "\u521B\u5EFA\u65F6\u95F4\u5339\u914D \u2713";
400
+ } else {
401
+ const mtimeThreshold = 6e5;
402
+ const mtimeMatched = validFiles.filter((f) => {
403
+ const mtimeDiff = f.mtime.getTime() - startMs;
404
+ return Math.abs(mtimeDiff) < mtimeThreshold;
405
+ }).sort(
406
+ (a, b) => Math.abs(a.mtime.getTime() - startMs) - Math.abs(b.mtime.getTime() - startMs)
407
+ );
408
+ if (mtimeMatched.length > 0) {
409
+ matchedFile = mtimeMatched[0];
410
+ matchStrategy = "\u4FEE\u6539\u65F6\u95F4\u5339\u914D \u2713";
411
+ } else {
412
+ const sorted = [...validFiles].sort(
413
+ (a, b) => b.mtime.getTime() - a.mtime.getTime()
414
+ );
415
+ if (sorted.length > 0) {
416
+ matchedFile = sorted[0];
417
+ matchStrategy = "\u56DE\u9000\u5230\u6700\u65B0\u6587\u4EF6";
418
+ }
419
+ }
420
+ }
421
+ output.push(" \u5339\u914D\u7ED3\u679C:");
422
+ output.push(` \u7B56\u7565: ${matchStrategy}`);
423
+ if (matchedFile) {
424
+ output.push(` \u9009\u62E9: ${matchedFile.name}`);
425
+ try {
426
+ const { messages } = await getAllMessages(matchedFile.path);
427
+ output.push(` \u6D88\u606F\u6570: ${messages.length} \u6761`);
428
+ successCount++;
429
+ } catch {
430
+ output.push(" \u6D88\u606F\u6570: \u8BFB\u53D6\u5931\u8D25");
431
+ }
432
+ } else {
433
+ output.push(" \u9009\u62E9: \u65E0");
434
+ }
435
+ output.push("");
436
+ output.push("---");
437
+ output.push("");
438
+ } catch (error) {
439
+ const errorMsg = error instanceof Error ? error.message : String(error);
440
+ output.push(` \u274C \u9519\u8BEF: ${errorMsg}`);
441
+ output.push("");
442
+ output.push("---");
443
+ output.push("");
444
+ }
445
+ }
446
+ output.push(
447
+ `\u603B\u8BA1: ${processes.length} \u4E2A\u8FDB\u7A0B, ${successCount} \u4E2A\u6210\u529F\u5339\u914D\u4F1A\u8BDD`
448
+ );
449
+ return output.join("\n");
450
+ }
451
+
452
+ export {
453
+ getSessionPath,
454
+ getRecentMessages,
455
+ getNewMessages,
456
+ getAllMessages,
457
+ clearSessionCache,
458
+ clearAllSessionCache,
459
+ debugSessionMatching
460
+ };
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/process.ts
4
+ import { exec } from "child_process";
5
+ import { promisify } from "util";
6
+ import pLimit from "p-limit";
7
+ var execAsync = promisify(exec);
8
+ var processLimit = pLimit(8);
9
+ async function getCurrentTty() {
10
+ try {
11
+ const { stdout } = await execAsync("tty");
12
+ return stdout.trim().replace("/dev/", "");
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+ async function batchGetProcessCwd(pids) {
18
+ if (pids.length === 0) return /* @__PURE__ */ new Map();
19
+ return processLimit(async () => {
20
+ try {
21
+ const pidList = pids.join(",");
22
+ const { stdout } = await execAsync(
23
+ `lsof -p ${pidList} 2>/dev/null | grep cwd`
24
+ );
25
+ const result = /* @__PURE__ */ new Map();
26
+ const lines = stdout.trim().split("\n");
27
+ for (const line of lines) {
28
+ const parts = line.trim().split(/\s+/);
29
+ if (parts.length >= 9) {
30
+ const pid = Number.parseInt(parts[1], 10);
31
+ const cwd = parts[8];
32
+ if (cwd.startsWith("/")) {
33
+ result.set(pid, cwd);
34
+ }
35
+ }
36
+ }
37
+ return result;
38
+ } catch {
39
+ return /* @__PURE__ */ new Map();
40
+ }
41
+ });
42
+ }
43
+ async function batchGetProcessStats(pids) {
44
+ if (pids.length === 0) return /* @__PURE__ */ new Map();
45
+ return processLimit(async () => {
46
+ try {
47
+ const pidList = pids.join(",");
48
+ const { stdout } = await execAsync(
49
+ `ps -p ${pidList} -o pid,%cpu,%mem,etime 2>/dev/null`
50
+ );
51
+ const result = /* @__PURE__ */ new Map();
52
+ const lines = stdout.trim().split("\n");
53
+ for (let i = 1; i < lines.length; i++) {
54
+ const parts = lines[i].trim().split(/\s+/);
55
+ if (parts.length >= 4) {
56
+ const pid = Number.parseInt(parts[0], 10);
57
+ result.set(pid, {
58
+ cpu: Number.parseFloat(parts[1]) || 0,
59
+ memory: Number.parseFloat(parts[2]) || 0,
60
+ elapsed: parts[3] || ""
61
+ });
62
+ }
63
+ }
64
+ return result;
65
+ } catch {
66
+ return /* @__PURE__ */ new Map();
67
+ }
68
+ });
69
+ }
70
+ function parseElapsedToDate(elapsed) {
71
+ const now = /* @__PURE__ */ new Date();
72
+ const parts = elapsed.split(/[-:]/);
73
+ let seconds = 0;
74
+ if (parts.length === 2) {
75
+ seconds = Number.parseInt(parts[0]) * 60 + Number.parseInt(parts[1]);
76
+ } else if (parts.length === 3) {
77
+ seconds = Number.parseInt(parts[0]) * 3600 + Number.parseInt(parts[1]) * 60 + Number.parseInt(parts[2]);
78
+ } else if (parts.length === 4) {
79
+ seconds = Number.parseInt(parts[0]) * 86400 + Number.parseInt(parts[1]) * 3600 + Number.parseInt(parts[2]) * 60 + Number.parseInt(parts[3]);
80
+ }
81
+ return new Date(now.getTime() - seconds * 1e3);
82
+ }
83
+ async function getClaudeProcesses() {
84
+ const currentTty = await getCurrentTty();
85
+ let stdout;
86
+ try {
87
+ const result = await execAsync(
88
+ `ps -eo pid,tty,command | grep -E '^\\s*[0-9]+\\s+\\S+\\s+claude(\\s|$)' | grep -v 'chrome-native-host' | grep -v grep`
89
+ );
90
+ stdout = result.stdout;
91
+ } catch {
92
+ return [];
93
+ }
94
+ const lines = stdout.trim().split("\n").filter(Boolean);
95
+ if (lines.length === 0) return [];
96
+ const basicInfo = [];
97
+ for (const line of lines) {
98
+ const match = line.trim().match(/^(\d+)\s+(\S+)\s+(.+)$/);
99
+ if (match) {
100
+ basicInfo.push({
101
+ pid: Number.parseInt(match[1]),
102
+ tty: match[2]
103
+ });
104
+ }
105
+ }
106
+ if (basicInfo.length === 0) return [];
107
+ const pids = basicInfo.map((p) => p.pid);
108
+ const [cwdMap, statsMap] = await Promise.all([
109
+ batchGetProcessCwd(pids),
110
+ batchGetProcessStats(pids)
111
+ ]);
112
+ const processes = [];
113
+ for (const info of basicInfo) {
114
+ const cwd = cwdMap.get(info.pid) || "\u672A\u77E5";
115
+ const stats = statsMap.get(info.pid) || {
116
+ cpu: 0,
117
+ memory: 0,
118
+ elapsed: ""
119
+ };
120
+ const isOrphan = info.tty === "??" || info.tty === "?";
121
+ const isCurrent = currentTty !== "" && info.tty === currentTty;
122
+ processes.push({
123
+ pid: info.pid,
124
+ tty: info.tty,
125
+ cwd,
126
+ isCurrent,
127
+ isOrphan,
128
+ cpu: stats.cpu,
129
+ memory: stats.memory,
130
+ elapsed: stats.elapsed,
131
+ startTime: parseElapsedToDate(stats.elapsed),
132
+ sessionPath: ""
133
+ });
134
+ }
135
+ return processes;
136
+ }
137
+ async function killProcess(pid, force = false) {
138
+ try {
139
+ const signal = force ? "KILL" : "TERM";
140
+ await execAsync(`kill -${signal} ${pid}`);
141
+ return true;
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+
147
+ export {
148
+ getClaudeProcesses,
149
+ killProcess
150
+ };
package/dist/index.js CHANGED
@@ -2,7 +2,13 @@
2
2
  import {
3
3
  getClaudeProcesses,
4
4
  killProcess
5
- } from "./chunk-EZHIVMX4.js";
5
+ } from "./chunk-KF2FBZRQ.js";
6
+ import {
7
+ clearSessionCache,
8
+ getAllMessages,
9
+ getNewMessages,
10
+ getSessionPath
11
+ } from "./chunk-JYWGOPVM.js";
6
12
 
7
13
  // src/index.tsx
8
14
  import { withFullScreen } from "fullscreen-ink";
@@ -193,7 +199,10 @@ function formatElapsed(elapsed) {
193
199
 
194
200
  // src/components/ui/DetailPanel.tsx
195
201
  import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
196
- function DetailPanel({ process: proc, isLive = true }) {
202
+ function DetailPanel({
203
+ process: proc,
204
+ isLive = true
205
+ }) {
197
206
  if (!proc) {
198
207
  return /* @__PURE__ */ jsx4(EmptyPrompt, { message: "\u9009\u62E9\u4E00\u4E2A\u8FDB\u7A0B\u67E5\u770B\u8BE6\u60C5" });
199
208
  }
@@ -300,159 +309,12 @@ function ProcessList({
300
309
  }
301
310
 
302
311
  // src/hooks/useProcesses.ts
312
+ import pLimit from "p-limit";
303
313
  import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState } from "react";
304
314
 
305
- // src/utils/session.ts
306
- import { readFile, readdir } from "fs/promises";
307
- import { homedir } from "os";
308
- import { join } from "path";
309
- function cwdToProjectDir(cwd) {
310
- return cwd.replace(/\//g, "-").replace(/^-/, "-");
311
- }
312
- async function getSessionPath(cwd, startTime) {
313
- const projectDir = cwdToProjectDir(cwd);
314
- const sessionsDir = join(homedir(), ".claude", "projects", projectDir);
315
- try {
316
- const files = await readdir(sessionsDir);
317
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
318
- if (jsonlFiles.length === 0) return "";
319
- const { stat } = await import("fs/promises");
320
- const fileStats = await Promise.all(
321
- jsonlFiles.map(async (f) => {
322
- const path = join(sessionsDir, f);
323
- const s = await stat(path);
324
- return {
325
- path,
326
- birthtime: s.birthtime,
327
- mtimeMs: s.mtimeMs,
328
- size: s.size
329
- };
330
- })
331
- );
332
- const minSize = 1024;
333
- const validFiles = fileStats.filter((f) => f.size >= minSize);
334
- if (startTime && validFiles.length > 0) {
335
- const startMs = startTime.getTime();
336
- const birthtimeThreshold = 1e4;
337
- const birthtimeMatched = validFiles.filter(
338
- (f) => Math.abs(f.birthtime.getTime() - startMs) < birthtimeThreshold
339
- ).sort((a, b) => b.mtimeMs - a.mtimeMs);
340
- if (birthtimeMatched.length > 0) {
341
- return birthtimeMatched[0].path;
342
- }
343
- const mtimeThreshold = 6e4;
344
- const mtimeMatched = validFiles.filter((f) => {
345
- const mtimeDiff = f.mtimeMs - startMs;
346
- return mtimeDiff >= 0 && mtimeDiff < mtimeThreshold;
347
- }).sort((a, b) => a.mtimeMs - b.mtimeMs);
348
- if (mtimeMatched.length > 0) {
349
- return mtimeMatched[0].path;
350
- }
351
- }
352
- const fallbackFiles = validFiles.length > 0 ? validFiles : fileStats;
353
- fallbackFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
354
- return fallbackFiles[0]?.path || "";
355
- } catch {
356
- return "";
357
- }
358
- }
359
- async function getNewMessages(sessionPath, fromLine) {
360
- if (!sessionPath) return { messages: [], totalLines: 0 };
361
- try {
362
- const content = await readFile(sessionPath, "utf-8");
363
- const lines = content.trim().split("\n");
364
- const totalLines = lines.length;
365
- const newLines = lines.slice(fromLine);
366
- const messages = [];
367
- for (const line of newLines) {
368
- try {
369
- const entry = JSON.parse(line);
370
- if (entry.type === "user" && entry.message?.content) {
371
- const text = extractUserText(entry.message.content);
372
- if (text && !isMetaMessage(text)) {
373
- messages.push({
374
- role: "user",
375
- content: truncate(text, 100),
376
- timestamp: entry.timestamp || ""
377
- });
378
- }
379
- } else if (entry.type === "assistant" && entry.message?.content) {
380
- const text = extractAssistantText(entry.message.content);
381
- if (text) {
382
- messages.push({
383
- role: "assistant",
384
- content: truncate(text, 100),
385
- timestamp: entry.timestamp || ""
386
- });
387
- }
388
- }
389
- } catch {
390
- }
391
- }
392
- return { messages, totalLines };
393
- } catch {
394
- return { messages: [], totalLines: 0 };
395
- }
396
- }
397
- async function getAllMessages(sessionPath) {
398
- if (!sessionPath) return [];
399
- try {
400
- const content = await readFile(sessionPath, "utf-8");
401
- const lines = content.trim().split("\n");
402
- const messages = [];
403
- for (const line of lines) {
404
- try {
405
- const entry = JSON.parse(line);
406
- if (entry.type === "user" && entry.message?.content) {
407
- const text = extractUserText(entry.message.content);
408
- if (text && !isMetaMessage(text)) {
409
- messages.push({
410
- role: "user",
411
- content: truncate(text, 100),
412
- timestamp: entry.timestamp || ""
413
- });
414
- }
415
- } else if (entry.type === "assistant" && entry.message?.content) {
416
- const text = extractAssistantText(entry.message.content);
417
- if (text) {
418
- messages.push({
419
- role: "assistant",
420
- content: truncate(text, 100),
421
- timestamp: entry.timestamp || ""
422
- });
423
- }
424
- }
425
- } catch {
426
- }
427
- }
428
- return messages;
429
- } catch {
430
- return [];
431
- }
432
- }
433
- function extractUserText(content) {
434
- if (typeof content === "string") {
435
- return content;
436
- }
437
- return "";
438
- }
439
- function extractAssistantText(content) {
440
- if (!Array.isArray(content)) return "";
441
- const textParts = content.filter((item) => item.type === "text" && item.text).map((item) => item.text);
442
- return textParts.join("\n");
443
- }
444
- function isMetaMessage(text) {
445
- return text.startsWith("<local-command") || text.startsWith("<command-name>") || text.startsWith("<command-message>");
446
- }
447
- function truncate(text, maxLen) {
448
- const clean = text.replace(/\s+/g, " ").trim();
449
- if (clean.length <= maxLen) return clean;
450
- return `${clean.slice(0, maxLen - 3)}...`;
451
- }
452
-
453
315
  // src/hooks/useSessionWatcher.ts
454
- import { useEffect, useRef } from "react";
455
316
  import chokidar from "chokidar";
317
+ import { useEffect, useRef } from "react";
456
318
  function useSessionWatcher(sessionPaths, onFileChange) {
457
319
  const watcherRef = useRef(null);
458
320
  const debounceTimersRef = useRef(/* @__PURE__ */ new Map());
@@ -470,6 +332,7 @@ function useSessionWatcher(sessionPaths, onFileChange) {
470
332
  }
471
333
  });
472
334
  watcher.on("change", (path) => {
335
+ clearSessionCache(path);
473
336
  const existingTimer = debounceTimersRef.current.get(path);
474
337
  if (existingTimer) {
475
338
  clearTimeout(existingTimer);
@@ -495,6 +358,7 @@ function useSessionWatcher(sessionPaths, onFileChange) {
495
358
  }
496
359
 
497
360
  // src/hooks/useProcesses.ts
361
+ var sessionLimit = pLimit(5);
498
362
  function sortProcesses(processes, sortField) {
499
363
  const sorted = [...processes];
500
364
  switch (sortField) {
@@ -523,22 +387,20 @@ function useProcesses(interval2) {
523
387
  try {
524
388
  const procs = await getClaudeProcesses();
525
389
  const enriched = await Promise.all(
526
- procs.map(async (proc) => {
527
- const sessionPath = await getSessionPath(proc.cwd, proc.startTime);
528
- const messages = await getAllMessages(sessionPath);
529
- if (sessionPath && !sessionLineNumbers.current.has(sessionPath)) {
530
- const content = await import("fs/promises").then(
531
- (m) => m.readFile(sessionPath, "utf-8").catch(() => "")
532
- );
533
- const lines = content.trim().split("\n");
534
- sessionLineNumbers.current.set(sessionPath, lines.length);
535
- }
536
- return {
537
- ...proc,
538
- sessionPath,
539
- messages
540
- };
541
- })
390
+ procs.map(
391
+ (proc) => sessionLimit(async () => {
392
+ const sessionPath = await getSessionPath(proc.cwd, proc.startTime);
393
+ const { messages, lineCount } = await getAllMessages(sessionPath);
394
+ if (sessionPath && !sessionLineNumbers.current.has(sessionPath)) {
395
+ sessionLineNumbers.current.set(sessionPath, lineCount);
396
+ }
397
+ return {
398
+ ...proc,
399
+ sessionPath,
400
+ messages
401
+ };
402
+ })
403
+ )
542
404
  );
543
405
  setRawProcesses(enriched);
544
406
  setError(null);
@@ -750,6 +612,7 @@ var cli = meow(
750
612
  Options
751
613
  -l, --list \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF0C\u4EC5\u5217\u51FA\u8FDB\u7A0B
752
614
  -j, --json JSON \u683C\u5F0F\u8F93\u51FA\uFF08\u914D\u5408 --list\uFF09
615
+ -d, --debug \u663E\u793A\u4F1A\u8BDD\u5339\u914D\u8C03\u8BD5\u4FE1\u606F
753
616
  -i, --interval \u5237\u65B0\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 2\uFF09
754
617
  -v, --version \u663E\u793A\u7248\u672C
755
618
  -h, --help \u663E\u793A\u5E2E\u52A9
@@ -758,6 +621,7 @@ var cli = meow(
758
621
  $ claude-ps \u542F\u52A8 TUI
759
622
  $ claude-ps --list \u5217\u51FA\u8FDB\u7A0B\u540E\u9000\u51FA
760
623
  $ claude-ps --json JSON \u683C\u5F0F\u8F93\u51FA
624
+ $ claude-ps --debug \u663E\u793A\u4F1A\u8BDD\u5339\u914D\u8BE6\u60C5
761
625
  $ claude-ps -i 5 \u8BBE\u7F6E\u5237\u65B0\u95F4\u9694\u4E3A 5 \u79D2
762
626
  `,
763
627
  {
@@ -778,6 +642,11 @@ var cli = meow(
778
642
  shortFlag: "j",
779
643
  default: false
780
644
  },
645
+ debug: {
646
+ type: "boolean",
647
+ shortFlag: "d",
648
+ default: false
649
+ },
781
650
  interval: {
782
651
  type: "number",
783
652
  shortFlag: "i",
@@ -791,11 +660,15 @@ var cli = meow(
791
660
  }
792
661
  }
793
662
  );
794
- var { list, json, interval } = cli.flags;
795
- if (list || json) {
796
- const { getClaudeProcesses: getClaudeProcesses2 } = await import("./process-AUO5UVTV.js");
663
+ var { list, json, debug, interval } = cli.flags;
664
+ if (list || json || debug) {
665
+ const { getClaudeProcesses: getClaudeProcesses2 } = await import("./process-XJGJPUAG.js");
797
666
  const processes = await getClaudeProcesses2();
798
- if (json) {
667
+ if (debug) {
668
+ const { debugSessionMatching } = await import("./session-FTYMYOIH.js");
669
+ const debugInfo = await debugSessionMatching(processes);
670
+ console.log(debugInfo);
671
+ } else if (json) {
799
672
  console.log(JSON.stringify(processes, null, 2));
800
673
  } else {
801
674
  console.log("PID TTY CWD");
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getClaudeProcesses,
4
4
  killProcess
5
- } from "./chunk-EZHIVMX4.js";
5
+ } from "./chunk-KF2FBZRQ.js";
6
6
  export {
7
7
  getClaudeProcesses,
8
8
  killProcess
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ clearAllSessionCache,
4
+ clearSessionCache,
5
+ debugSessionMatching,
6
+ getAllMessages,
7
+ getNewMessages,
8
+ getRecentMessages,
9
+ getSessionPath
10
+ } from "./chunk-JYWGOPVM.js";
11
+ export {
12
+ clearAllSessionCache,
13
+ clearSessionCache,
14
+ debugSessionMatching,
15
+ getAllMessages,
16
+ getNewMessages,
17
+ getRecentMessages,
18
+ getSessionPath
19
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-ps",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "TUI application for viewing and managing Claude Code processes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,7 @@
33
33
  "fullscreen-ink": "^0.1.0",
34
34
  "ink": "^5.0.1",
35
35
  "meow": "^13.2.0",
36
+ "p-limit": "^7.3.0",
36
37
  "react": "^18.3.1"
37
38
  },
38
39
  "devDependencies": {
@@ -1,106 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/utils/process.ts
4
- import { exec } from "child_process";
5
- import { promisify } from "util";
6
- var execAsync = promisify(exec);
7
- async function getCurrentTty() {
8
- try {
9
- const { stdout } = await execAsync("tty");
10
- return stdout.trim().replace("/dev/", "");
11
- } catch {
12
- return "";
13
- }
14
- }
15
- async function getProcessCwd(pid) {
16
- try {
17
- const { stdout } = await execAsync(`lsof -p ${pid} 2>/dev/null | grep cwd`);
18
- const match = stdout.trim().match(/\s(\/.+)$/);
19
- return match ? match[1] : "";
20
- } catch {
21
- return "";
22
- }
23
- }
24
- async function getProcessStats(pid) {
25
- try {
26
- const { stdout } = await execAsync(
27
- `ps -p ${pid} -o %cpu,%mem,etime 2>/dev/null`
28
- );
29
- const lines = stdout.trim().split("\n");
30
- if (lines.length < 2) return { cpu: 0, memory: 0, elapsed: "" };
31
- const parts = lines[1].trim().split(/\s+/);
32
- return {
33
- cpu: Number.parseFloat(parts[0]) || 0,
34
- memory: Number.parseFloat(parts[1]) || 0,
35
- elapsed: parts[2] || ""
36
- };
37
- } catch {
38
- return { cpu: 0, memory: 0, elapsed: "" };
39
- }
40
- }
41
- function parseElapsedToDate(elapsed) {
42
- const now = /* @__PURE__ */ new Date();
43
- const parts = elapsed.split(/[-:]/);
44
- let seconds = 0;
45
- if (parts.length === 2) {
46
- seconds = Number.parseInt(parts[0]) * 60 + Number.parseInt(parts[1]);
47
- } else if (parts.length === 3) {
48
- seconds = Number.parseInt(parts[0]) * 3600 + Number.parseInt(parts[1]) * 60 + Number.parseInt(parts[2]);
49
- } else if (parts.length === 4) {
50
- seconds = Number.parseInt(parts[0]) * 86400 + Number.parseInt(parts[1]) * 3600 + Number.parseInt(parts[2]) * 60 + Number.parseInt(parts[3]);
51
- }
52
- return new Date(now.getTime() - seconds * 1e3);
53
- }
54
- async function getClaudeProcesses() {
55
- const currentTty = await getCurrentTty();
56
- let stdout;
57
- try {
58
- const result = await execAsync(
59
- `ps -eo pid,tty,command | grep -E '^\\s*[0-9]+\\s+\\S+\\s+claude(\\s|$)' | grep -v 'chrome-native-host' | grep -v grep`
60
- );
61
- stdout = result.stdout;
62
- } catch {
63
- return [];
64
- }
65
- const lines = stdout.trim().split("\n").filter(Boolean);
66
- const processes = [];
67
- for (const line of lines) {
68
- const match = line.trim().match(/^(\d+)\s+(\S+)\s+(.+)$/);
69
- if (!match) continue;
70
- const pid = Number.parseInt(match[1]);
71
- const tty = match[2];
72
- const [cwd, stats] = await Promise.all([
73
- getProcessCwd(pid),
74
- getProcessStats(pid)
75
- ]);
76
- const isOrphan = tty === "??" || tty === "?";
77
- const isCurrent = currentTty !== "" && tty === currentTty;
78
- processes.push({
79
- pid,
80
- tty,
81
- cwd: cwd || "\u672A\u77E5",
82
- isCurrent,
83
- isOrphan,
84
- cpu: stats.cpu,
85
- memory: stats.memory,
86
- elapsed: stats.elapsed,
87
- startTime: parseElapsedToDate(stats.elapsed),
88
- sessionPath: ""
89
- });
90
- }
91
- return processes;
92
- }
93
- async function killProcess(pid, force = false) {
94
- try {
95
- const signal = force ? "KILL" : "TERM";
96
- await execAsync(`kill -${signal} ${pid}`);
97
- return true;
98
- } catch {
99
- return false;
100
- }
101
- }
102
-
103
- export {
104
- getClaudeProcesses,
105
- killProcess
106
- };