claude-ps 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ziheng
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,31 @@
1
+ # claude-ps
2
+
3
+ TUI application for viewing and managing Claude Code processes.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g claude-ps
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ claude-ps # 启动交互界面
15
+ claude-ps --list # 列出进程(非交互)
16
+ claude-ps --json # JSON 格式输出
17
+ claude-ps --interval 5 # 设置刷新间隔(秒)
18
+ ```
19
+
20
+ ## Keybindings
21
+
22
+ - `↑/↓` or `j/k` - 移动选择
23
+ - `s` - 切换排序方式
24
+ - `r` - 刷新
25
+ - `d` - 终止进程 (SIGTERM)
26
+ - `D` - 强制终止 (SIGKILL)
27
+ - `q/ESC` - 退出
28
+
29
+ ## License
30
+
31
+ MIT
@@ -0,0 +1,106 @@
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 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
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,609 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getClaudeProcesses,
4
+ killProcess
5
+ } from "./chunk-TPK4XYR3.js";
6
+
7
+ // src/index.tsx
8
+ import { withFullScreen } from "fullscreen-ink";
9
+ import meow from "meow";
10
+
11
+ // src/App.tsx
12
+ import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
13
+
14
+ // src/components/DetailPanel.tsx
15
+ import { Box, Text } from "ink";
16
+
17
+ // src/constants/theme.ts
18
+ var USAGE_THRESHOLD = {
19
+ LOW: 30,
20
+ HIGH: 70
21
+ };
22
+ var COLORS = {
23
+ /** 使用率颜色 */
24
+ usage: {
25
+ low: "green",
26
+ medium: "yellow",
27
+ high: "red"
28
+ },
29
+ /** 标签颜色 */
30
+ label: "gray",
31
+ /** 数值颜色 */
32
+ value: "cyan",
33
+ /** 当前终端进程 */
34
+ current: "green",
35
+ /** 孤儿进程 */
36
+ orphan: "red",
37
+ /** 选中项 */
38
+ selected: "yellow",
39
+ /** 标题 */
40
+ title: "cyan"
41
+ };
42
+ var COLUMN_WIDTH = {
43
+ prefix: 2,
44
+ pid: 6,
45
+ cpu: 6,
46
+ memory: 6,
47
+ elapsed: 9
48
+ };
49
+ var ESTIMATED_TOTAL_MEMORY_MB = 16 * 1024;
50
+ function getUsageColor(percent) {
51
+ if (percent < USAGE_THRESHOLD.LOW) return COLORS.usage.low;
52
+ if (percent < USAGE_THRESHOLD.HIGH) return COLORS.usage.medium;
53
+ return COLORS.usage.high;
54
+ }
55
+
56
+ // src/utils/format.ts
57
+ function formatMemory(memPercent) {
58
+ const usedMB = memPercent / 100 * ESTIMATED_TOTAL_MEMORY_MB;
59
+ if (usedMB < 1024) return `${Math.round(usedMB)}MB`;
60
+ return `${(usedMB / 1024).toFixed(1)}GB`;
61
+ }
62
+ function formatElapsed(elapsed) {
63
+ if (!elapsed) return "\u672A\u77E5";
64
+ const parts = elapsed.split(/[-:]/);
65
+ if (parts.length === 2) {
66
+ return `${parts[0]}\u5206${parts[1]}\u79D2`;
67
+ }
68
+ if (parts.length === 3) {
69
+ return `${parts[0]}\u65F6${parts[1]}\u5206`;
70
+ }
71
+ if (parts.length === 4) {
72
+ return `${parts[0]}\u5929${parts[1]}\u65F6`;
73
+ }
74
+ return elapsed;
75
+ }
76
+ function shortenPath(inputPath, maxLen = 25) {
77
+ let path = inputPath;
78
+ const home = process.env.HOME || "";
79
+ if (home && path.startsWith(home)) {
80
+ path = `~${path.slice(home.length)}`;
81
+ }
82
+ if (path.length <= maxLen) return path;
83
+ const half = Math.floor((maxLen - 3) / 2);
84
+ return `${path.slice(0, half)}...${path.slice(-half)}`;
85
+ }
86
+
87
+ // src/components/DetailPanel.tsx
88
+ import { jsx, jsxs } from "react/jsx-runtime";
89
+ function DetailPanel({ process: proc }) {
90
+ if (!proc) {
91
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: 1, children: /* @__PURE__ */ jsx(Text, { color: COLORS.label, children: "\u9009\u62E9\u4E00\u4E2A\u8FDB\u7A0B\u67E5\u770B\u8BE6\u60C5" }) });
92
+ }
93
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingLeft: 1, children: [
94
+ /* @__PURE__ */ jsxs(Box, { children: [
95
+ /* @__PURE__ */ jsx(Text, { color: COLORS.title, children: "CPU: " }),
96
+ /* @__PURE__ */ jsxs(Text, { children: [
97
+ proc.cpu.toFixed(1),
98
+ "%"
99
+ ] }),
100
+ /* @__PURE__ */ jsx(Text, { children: " " }),
101
+ /* @__PURE__ */ jsx(Text, { color: COLORS.title, children: "\u5185\u5B58: " }),
102
+ /* @__PURE__ */ jsx(Text, { children: formatMemory(proc.memory) }),
103
+ /* @__PURE__ */ jsx(Text, { children: " " }),
104
+ /* @__PURE__ */ jsx(Text, { color: COLORS.title, children: "\u65F6\u957F: " }),
105
+ /* @__PURE__ */ jsx(Text, { children: formatElapsed(proc.elapsed) })
106
+ ] }),
107
+ /* @__PURE__ */ jsx(Text, { children: " " }),
108
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
109
+ /* @__PURE__ */ jsx(Text, { color: COLORS.title, children: "Session:" }),
110
+ /* @__PURE__ */ jsx(Text, { color: COLORS.label, wrap: "truncate", children: proc.sessionPath || "\u65E0\u4F1A\u8BDD\u6587\u4EF6" })
111
+ ] }),
112
+ /* @__PURE__ */ jsx(Text, { children: " " }),
113
+ /* @__PURE__ */ jsx(Text, { color: COLORS.title, bold: true, children: "\u2500 \u6700\u8FD1\u5BF9\u8BDD \u2500" }),
114
+ proc.messages.length === 0 ? /* @__PURE__ */ jsx(Text, { color: COLORS.label, children: "\u65E0\u5BF9\u8BDD\u8BB0\u5F55" }) : proc.messages.map((msg) => /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
115
+ /* @__PURE__ */ jsxs(Text, { color: msg.role === "user" ? COLORS.current : "blue", children: [
116
+ "[",
117
+ msg.role === "user" ? "User" : "Claude",
118
+ "]",
119
+ " "
120
+ ] }),
121
+ /* @__PURE__ */ jsx(Text, { wrap: "truncate", children: msg.content })
122
+ ] }, msg.timestamp))
123
+ ] });
124
+ }
125
+
126
+ // src/components/HelpBar.tsx
127
+ import { Box as Box2, Text as Text2 } from "ink";
128
+ import React from "react";
129
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
130
+ var hints = [
131
+ { key: "\u2191/\u2193", desc: "\u79FB\u52A8" },
132
+ { key: "d", desc: "\u7EC8\u6B62" },
133
+ { key: "D", desc: "\u5F3A\u6740" },
134
+ { key: "s", desc: "\u6392\u5E8F" },
135
+ { key: "r", desc: "\u5237\u65B0" },
136
+ { key: "q", desc: "\u9000\u51FA" }
137
+ ];
138
+ var sortFieldLabels = {
139
+ cpu: "CPU",
140
+ memory: "\u5185\u5B58",
141
+ elapsed: "\u65F6\u957F",
142
+ default: "PID"
143
+ };
144
+ function HelpBar({ processCount, interval: interval2, sortField }) {
145
+ return /* @__PURE__ */ jsxs2(Box2, { justifyContent: "space-between", children: [
146
+ /* @__PURE__ */ jsx2(Box2, { children: hints.map((hint, i) => /* @__PURE__ */ jsxs2(React.Fragment, { children: [
147
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.selected, children: hint.key }),
148
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.label, children: [
149
+ " ",
150
+ hint.desc
151
+ ] }),
152
+ i < hints.length - 1 && /* @__PURE__ */ jsx2(Text2, { children: " " })
153
+ ] }, hint.key)) }),
154
+ /* @__PURE__ */ jsxs2(Box2, { children: [
155
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.value, children: [
156
+ processCount,
157
+ " \u8FDB\u7A0B"
158
+ ] }),
159
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.label, children: " | " }),
160
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.label, children: "\u6392\u5E8F: " }),
161
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.value, children: sortFieldLabels[sortField] }),
162
+ /* @__PURE__ */ jsx2(Text2, { color: COLORS.label, children: " | " }),
163
+ /* @__PURE__ */ jsxs2(Text2, { color: COLORS.label, children: [
164
+ "\u5237\u65B0: ",
165
+ interval2,
166
+ "s"
167
+ ] })
168
+ ] })
169
+ ] });
170
+ }
171
+
172
+ // src/components/ProcessList.tsx
173
+ import { Box as Box3, Text as Text3 } from "ink";
174
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
175
+ function formatPercent(value, width) {
176
+ const str = `${value.toFixed(1)}%`;
177
+ return str.padStart(width);
178
+ }
179
+ function ProcessList({
180
+ processes,
181
+ selectedIndex,
182
+ loading
183
+ }) {
184
+ if (loading) {
185
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u52A0\u8F7D\u4E2D..." }) });
186
+ }
187
+ if (processes.length === 0) {
188
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u65E0\u8FD0\u884C\u4E2D\u7684 Claude \u8FDB\u7A0B" }) });
189
+ }
190
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
191
+ /* @__PURE__ */ jsxs3(Box3, { children: [
192
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.title, children: " " }),
193
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.pid, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.title, children: "PID" }) }),
194
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.cpu, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.title, children: "CPU" }) }),
195
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.memory, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.title, children: "MEM" }) }),
196
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.elapsed, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.title, children: "\u65F6\u957F" }) }),
197
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: COLORS.title, children: "\u5DE5\u4F5C\u76EE\u5F55" })
198
+ ] }),
199
+ /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u2500".repeat(60) }),
200
+ processes.map((proc, index) => {
201
+ const isSelected = index === selectedIndex;
202
+ const prefix = isSelected ? "\u25B6 " : " ";
203
+ let cwdColor = void 0;
204
+ if (proc.isOrphan) cwdColor = COLORS.orphan;
205
+ else if (proc.isCurrent) cwdColor = COLORS.current;
206
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
207
+ /* @__PURE__ */ jsx3(Text3, { color: isSelected ? COLORS.selected : void 0, children: prefix }),
208
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.pid, children: /* @__PURE__ */ jsx3(Text3, { color: isSelected ? COLORS.selected : void 0, children: proc.pid }) }),
209
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.cpu, children: /* @__PURE__ */ jsx3(Text3, { color: getUsageColor(proc.cpu), children: formatPercent(proc.cpu, COLUMN_WIDTH.cpu - 1) }) }),
210
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.memory, children: /* @__PURE__ */ jsx3(Text3, { color: getUsageColor(proc.memory), children: formatPercent(proc.memory, COLUMN_WIDTH.memory - 1) }) }),
211
+ /* @__PURE__ */ jsx3(Box3, { width: COLUMN_WIDTH.elapsed, children: /* @__PURE__ */ jsx3(Text3, { color: isSelected ? COLORS.selected : "gray", children: formatElapsed(proc.elapsed).slice(0, COLUMN_WIDTH.elapsed - 1) }) }),
212
+ /* @__PURE__ */ jsxs3(Text3, { color: cwdColor, children: [
213
+ shortenPath(proc.cwd, 30),
214
+ proc.isCurrent ? " \u25CF" : ""
215
+ ] })
216
+ ] }, proc.pid);
217
+ })
218
+ ] });
219
+ }
220
+
221
+ // src/components/StatusBar.tsx
222
+ import { Box as Box4, Text as Text4 } from "ink";
223
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
224
+ function calculateStats(processes) {
225
+ const totalCpu = processes.reduce((sum, p) => sum + p.cpu, 0);
226
+ const totalMemoryPercent = processes.reduce((sum, p) => sum + p.memory, 0);
227
+ const totalMemoryMB = totalMemoryPercent / 100 * ESTIMATED_TOTAL_MEMORY_MB;
228
+ const orphanCount = processes.filter((p) => p.isOrphan).length;
229
+ const currentCount = processes.filter((p) => p.isCurrent).length;
230
+ return {
231
+ count: processes.length,
232
+ totalCpu,
233
+ totalMemory: totalMemoryMB < 1024 ? `${Math.round(totalMemoryMB)}MB` : `${(totalMemoryMB / 1024).toFixed(1)}GB`,
234
+ orphanCount,
235
+ currentCount
236
+ };
237
+ }
238
+ function StatusBar({ processes }) {
239
+ const stats = calculateStats(processes);
240
+ return /* @__PURE__ */ jsxs4(Box4, { children: [
241
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, children: "\u8FDB\u7A0B: " }),
242
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.value, children: stats.count }),
243
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, children: " | CPU: " }),
244
+ /* @__PURE__ */ jsxs4(Text4, { color: COLORS.value, children: [
245
+ stats.totalCpu.toFixed(1),
246
+ "%"
247
+ ] }),
248
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, children: " | \u5185\u5B58: " }),
249
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.value, children: stats.totalMemory }),
250
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, children: " | \u5B64\u513F: " }),
251
+ /* @__PURE__ */ jsx4(Text4, { color: stats.orphanCount > 0 ? COLORS.orphan : COLORS.value, children: stats.orphanCount }),
252
+ /* @__PURE__ */ jsx4(Text4, { color: COLORS.label, children: " | \u5F53\u524D: " }),
253
+ /* @__PURE__ */ jsx4(Text4, { color: stats.currentCount > 0 ? COLORS.current : COLORS.value, children: stats.currentCount })
254
+ ] });
255
+ }
256
+
257
+ // src/hooks/useProcesses.ts
258
+ import { useCallback, useEffect, useMemo, useState } from "react";
259
+
260
+ // src/utils/session.ts
261
+ import { readFile, readdir } from "fs/promises";
262
+ import { homedir } from "os";
263
+ import { join } from "path";
264
+ function cwdToProjectDir(cwd) {
265
+ return cwd.replace(/\//g, "-").replace(/^-/, "-");
266
+ }
267
+ async function getSessionPath(cwd, startTime) {
268
+ const projectDir = cwdToProjectDir(cwd);
269
+ const sessionsDir = join(homedir(), ".claude", "projects", projectDir);
270
+ try {
271
+ const files = await readdir(sessionsDir);
272
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
273
+ if (jsonlFiles.length === 0) return "";
274
+ const { stat } = await import("fs/promises");
275
+ const fileStats = await Promise.all(
276
+ jsonlFiles.map(async (f) => {
277
+ const path = join(sessionsDir, f);
278
+ const s = await stat(path);
279
+ return {
280
+ path,
281
+ birthtime: s.birthtime,
282
+ mtimeMs: s.mtimeMs,
283
+ size: s.size
284
+ };
285
+ })
286
+ );
287
+ const minSize = 1024;
288
+ const validFiles = fileStats.filter((f) => f.size >= minSize);
289
+ if (startTime && validFiles.length > 0) {
290
+ const startMs = startTime.getTime();
291
+ const birthtimeThreshold = 1e4;
292
+ const birthtimeMatched = validFiles.filter(
293
+ (f) => Math.abs(f.birthtime.getTime() - startMs) < birthtimeThreshold
294
+ ).sort((a, b) => b.mtimeMs - a.mtimeMs);
295
+ if (birthtimeMatched.length > 0) {
296
+ return birthtimeMatched[0].path;
297
+ }
298
+ const mtimeThreshold = 6e4;
299
+ const mtimeMatched = validFiles.filter((f) => {
300
+ const mtimeDiff = f.mtimeMs - startMs;
301
+ return mtimeDiff >= 0 && mtimeDiff < mtimeThreshold;
302
+ }).sort((a, b) => a.mtimeMs - b.mtimeMs);
303
+ if (mtimeMatched.length > 0) {
304
+ return mtimeMatched[0].path;
305
+ }
306
+ }
307
+ const fallbackFiles = validFiles.length > 0 ? validFiles : fileStats;
308
+ fallbackFiles.sort((a, b) => b.mtimeMs - a.mtimeMs);
309
+ return fallbackFiles[0]?.path || "";
310
+ } catch {
311
+ return "";
312
+ }
313
+ }
314
+ async function getRecentMessages(sessionPath, limit = 5) {
315
+ if (!sessionPath) return [];
316
+ try {
317
+ const content = await readFile(sessionPath, "utf-8");
318
+ const lines = content.trim().split("\n");
319
+ const messages = [];
320
+ for (const line of lines) {
321
+ try {
322
+ const entry = JSON.parse(line);
323
+ if (entry.type === "user" && entry.message?.content) {
324
+ const text = extractUserText(entry.message.content);
325
+ if (text && !isMetaMessage(text)) {
326
+ messages.push({
327
+ role: "user",
328
+ content: truncate(text, 100),
329
+ timestamp: entry.timestamp || ""
330
+ });
331
+ }
332
+ } else if (entry.type === "assistant" && entry.message?.content) {
333
+ const text = extractAssistantText(entry.message.content);
334
+ if (text) {
335
+ messages.push({
336
+ role: "assistant",
337
+ content: truncate(text, 100),
338
+ timestamp: entry.timestamp || ""
339
+ });
340
+ }
341
+ }
342
+ } catch {
343
+ }
344
+ }
345
+ return messages.slice(-limit);
346
+ } catch {
347
+ return [];
348
+ }
349
+ }
350
+ function extractUserText(content) {
351
+ if (typeof content === "string") {
352
+ return content;
353
+ }
354
+ return "";
355
+ }
356
+ function extractAssistantText(content) {
357
+ if (!Array.isArray(content)) return "";
358
+ const textParts = content.filter((item) => item.type === "text" && item.text).map((item) => item.text);
359
+ return textParts.join("\n");
360
+ }
361
+ function isMetaMessage(text) {
362
+ return text.startsWith("<local-command") || text.startsWith("<command-name>") || text.startsWith("<command-message>");
363
+ }
364
+ function truncate(text, maxLen) {
365
+ const clean = text.replace(/\s+/g, " ").trim();
366
+ if (clean.length <= maxLen) return clean;
367
+ return `${clean.slice(0, maxLen - 3)}...`;
368
+ }
369
+
370
+ // src/hooks/useProcesses.ts
371
+ function sortProcesses(processes, sortField) {
372
+ const sorted = [...processes];
373
+ switch (sortField) {
374
+ case "cpu":
375
+ sorted.sort((a, b) => b.cpu - a.cpu);
376
+ break;
377
+ case "memory":
378
+ sorted.sort((a, b) => b.memory - a.memory);
379
+ break;
380
+ case "elapsed":
381
+ sorted.sort((a, b) => a.startTime.getTime() - b.startTime.getTime());
382
+ break;
383
+ default:
384
+ sorted.sort((a, b) => a.pid - b.pid);
385
+ }
386
+ return sorted;
387
+ }
388
+ function useProcesses(interval2) {
389
+ const [rawProcesses, setRawProcesses] = useState([]);
390
+ const [loading, setLoading] = useState(true);
391
+ const [error, setError] = useState(null);
392
+ const [selectedIndex, setSelectedIndex] = useState(0);
393
+ const [sortField, setSortField] = useState("default");
394
+ const refresh = useCallback(async () => {
395
+ try {
396
+ const procs = await getClaudeProcesses();
397
+ const enriched = await Promise.all(
398
+ procs.map(async (proc) => {
399
+ const sessionPath = await getSessionPath(proc.cwd, proc.startTime);
400
+ const messages = await getRecentMessages(sessionPath);
401
+ return {
402
+ ...proc,
403
+ sessionPath,
404
+ messages
405
+ };
406
+ })
407
+ );
408
+ setRawProcesses(enriched);
409
+ setError(null);
410
+ setSelectedIndex(
411
+ (prev) => Math.min(prev, Math.max(0, enriched.length - 1))
412
+ );
413
+ } catch (e) {
414
+ setError(e instanceof Error ? e.message : "\u672A\u77E5\u9519\u8BEF");
415
+ } finally {
416
+ setLoading(false);
417
+ }
418
+ }, []);
419
+ useEffect(() => {
420
+ refresh();
421
+ const timer = setInterval(refresh, interval2 * 1e3);
422
+ return () => clearInterval(timer);
423
+ }, [refresh, interval2]);
424
+ const processes = useMemo(
425
+ () => sortProcesses(rawProcesses, sortField),
426
+ [rawProcesses, sortField]
427
+ );
428
+ const selectNext = useCallback(() => {
429
+ setSelectedIndex((prev) => Math.min(prev + 1, processes.length - 1));
430
+ }, [processes.length]);
431
+ const selectPrev = useCallback(() => {
432
+ setSelectedIndex((prev) => Math.max(prev - 1, 0));
433
+ }, []);
434
+ const cycleSortField = useCallback(() => {
435
+ setSortField((prev) => {
436
+ const order = ["cpu", "memory", "elapsed", "default"];
437
+ const idx = order.indexOf(prev);
438
+ return order[(idx + 1) % order.length];
439
+ });
440
+ }, []);
441
+ const killSelected = useCallback(
442
+ async (force = false) => {
443
+ const proc = processes[selectedIndex];
444
+ if (!proc) return false;
445
+ const success = await killProcess(proc.pid, force);
446
+ if (success) {
447
+ await refresh();
448
+ }
449
+ return success;
450
+ },
451
+ [processes, selectedIndex, refresh]
452
+ );
453
+ const selectedProcess = processes[selectedIndex] || null;
454
+ return {
455
+ processes,
456
+ loading,
457
+ error,
458
+ selectedIndex,
459
+ selectedProcess,
460
+ sortField,
461
+ refresh,
462
+ selectNext,
463
+ selectPrev,
464
+ cycleSortField,
465
+ killSelected
466
+ };
467
+ }
468
+
469
+ // src/App.tsx
470
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
471
+ function App({ interval: interval2 }) {
472
+ const { exit } = useApp();
473
+ const { stdout } = useStdout();
474
+ const termWidth = stdout?.columns || 80;
475
+ const termHeight = stdout?.rows || 24;
476
+ const {
477
+ processes,
478
+ loading,
479
+ error,
480
+ selectedIndex,
481
+ selectedProcess,
482
+ sortField,
483
+ refresh,
484
+ selectNext,
485
+ selectPrev,
486
+ cycleSortField,
487
+ killSelected
488
+ } = useProcesses(interval2);
489
+ useInput((input, key) => {
490
+ if (input === "q" || key.escape) {
491
+ exit();
492
+ } else if (key.downArrow || input === "j") {
493
+ selectNext();
494
+ } else if (key.upArrow || input === "k") {
495
+ selectPrev();
496
+ } else if (input === "r") {
497
+ refresh();
498
+ } else if (input === "s") {
499
+ cycleSortField();
500
+ } else if (input === "d") {
501
+ killSelected(false);
502
+ } else if (input === "D") {
503
+ killSelected(true);
504
+ }
505
+ });
506
+ const leftWidth = Math.floor(termWidth * 0.45);
507
+ const rightWidth = termWidth - leftWidth - 1;
508
+ const contentHeight = termHeight - 4;
509
+ if (error) {
510
+ return /* @__PURE__ */ jsx5(Box5, { flexDirection: "column", width: termWidth, height: termHeight, children: /* @__PURE__ */ jsxs5(Text5, { color: COLORS.orphan, children: [
511
+ "\u9519\u8BEF: ",
512
+ error
513
+ ] }) });
514
+ }
515
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width: termWidth, height: termHeight, children: [
516
+ /* @__PURE__ */ jsxs5(Box5, { children: [
517
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: COLORS.title, children: "claude-ps" }),
518
+ /* @__PURE__ */ jsx5(Text5, { color: COLORS.label, children: " - Claude Code \u8FDB\u7A0B\u7BA1\u7406\u5668" })
519
+ ] }),
520
+ /* @__PURE__ */ jsx5(StatusBar, { processes }),
521
+ /* @__PURE__ */ jsxs5(Box5, { height: contentHeight, children: [
522
+ /* @__PURE__ */ jsx5(Box5, { width: leftWidth, flexDirection: "column", height: contentHeight, children: /* @__PURE__ */ jsx5(
523
+ ProcessList,
524
+ {
525
+ processes,
526
+ selectedIndex,
527
+ loading
528
+ }
529
+ ) }),
530
+ /* @__PURE__ */ jsx5(Box5, { width: 1, flexDirection: "column", height: contentHeight, children: /* @__PURE__ */ jsx5(Text5, { color: COLORS.label, children: "\u2502\n".repeat(contentHeight).trim() }) }),
531
+ /* @__PURE__ */ jsx5(Box5, { width: rightWidth, flexDirection: "column", height: contentHeight, children: /* @__PURE__ */ jsx5(DetailPanel, { process: selectedProcess }) })
532
+ ] }),
533
+ /* @__PURE__ */ jsx5(
534
+ Box5,
535
+ {
536
+ borderStyle: "single",
537
+ borderTop: true,
538
+ borderBottom: false,
539
+ borderLeft: false,
540
+ borderRight: false,
541
+ children: /* @__PURE__ */ jsx5(
542
+ HelpBar,
543
+ {
544
+ processCount: processes.length,
545
+ interval: interval2,
546
+ sortField
547
+ }
548
+ )
549
+ }
550
+ )
551
+ ] });
552
+ }
553
+
554
+ // src/index.tsx
555
+ import { jsx as jsx6 } from "react/jsx-runtime";
556
+ var cli = meow(
557
+ `
558
+ Usage
559
+ $ claude-ps [options]
560
+
561
+ Options
562
+ -l, --list \u975E\u4EA4\u4E92\u6A21\u5F0F\uFF0C\u4EC5\u5217\u51FA\u8FDB\u7A0B
563
+ -j, --json JSON \u683C\u5F0F\u8F93\u51FA\uFF08\u914D\u5408 --list\uFF09
564
+ -i, --interval \u5237\u65B0\u95F4\u9694\u79D2\u6570\uFF08\u9ED8\u8BA4 2\uFF09
565
+ -v, --version \u663E\u793A\u7248\u672C
566
+ -h, --help \u663E\u793A\u5E2E\u52A9
567
+
568
+ Examples
569
+ $ claude-ps \u542F\u52A8 TUI
570
+ $ claude-ps --list \u5217\u51FA\u8FDB\u7A0B\u540E\u9000\u51FA
571
+ $ claude-ps --json JSON \u683C\u5F0F\u8F93\u51FA
572
+ $ claude-ps -i 5 \u8BBE\u7F6E\u5237\u65B0\u95F4\u9694\u4E3A 5 \u79D2
573
+ `,
574
+ {
575
+ importMeta: import.meta,
576
+ flags: {
577
+ list: {
578
+ type: "boolean",
579
+ shortFlag: "l",
580
+ default: false
581
+ },
582
+ json: {
583
+ type: "boolean",
584
+ shortFlag: "j",
585
+ default: false
586
+ },
587
+ interval: {
588
+ type: "number",
589
+ shortFlag: "i",
590
+ default: 2
591
+ }
592
+ }
593
+ }
594
+ );
595
+ var { list, json, interval } = cli.flags;
596
+ if (list || json) {
597
+ const { getClaudeProcesses: getClaudeProcesses2 } = await import("./process-2SZ7S64S.js");
598
+ const processes = await getClaudeProcesses2();
599
+ if (json) {
600
+ console.log(JSON.stringify(processes, null, 2));
601
+ } else {
602
+ console.log("PID TTY CWD");
603
+ for (const proc of processes) {
604
+ console.log(`${proc.pid} ${proc.tty} ${proc.cwd}`);
605
+ }
606
+ }
607
+ process.exit(0);
608
+ }
609
+ withFullScreen(/* @__PURE__ */ jsx6(App, { interval })).start();
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getClaudeProcesses,
4
+ killProcess
5
+ } from "./chunk-TPK4XYR3.js";
6
+ export {
7
+ getClaudeProcesses,
8
+ killProcess
9
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "claude-ps",
3
+ "version": "0.1.0",
4
+ "description": "TUI application for viewing and managing Claude Code processes",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "claude-ps": "dist/index.js"
9
+ },
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "dev": "tsx src/index.tsx",
13
+ "build": "tsup",
14
+ "lint": "biome check .",
15
+ "lint:fix": "biome check --write .",
16
+ "format": "biome format --write .",
17
+ "typecheck": "tsc --noEmit",
18
+ "all": "pnpm format && pnpm typecheck && pnpm lint"
19
+ },
20
+ "keywords": ["claude", "tui", "terminal", "process-manager"],
21
+ "author": "ziheng",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "fullscreen-ink": "^0.1.0",
26
+ "ink": "^5.0.1",
27
+ "meow": "^13.2.0",
28
+ "react": "^18.3.1"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^1.9.0",
32
+ "@types/react": "^18.3.0",
33
+ "tsup": "^8.3.0",
34
+ "tsx": "^4.19.0",
35
+ "typescript": "^5.6.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ }
40
+ }