z-clean 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +20 -3
- package/README.md +16 -6
- package/README.zh.md +20 -3
- package/package.json +1 -1
- package/src/config.js +20 -13
- package/src/killer.js +0 -2
- package/src/process-diagnostic.js +17 -0
- package/src/storage-security.js +65 -0
- package/src/windows-processes.js +37 -12
package/README.ko.md
CHANGED
|
@@ -55,17 +55,34 @@ AI 코딩 도구는 작업 중 많은 임시 런타임을 만듭니다.
|
|
|
55
55
|
|
|
56
56
|
## 설치
|
|
57
57
|
|
|
58
|
+
먼저 설치나 정리 없이 읽기 전용 audit을 실행하세요:
|
|
59
|
+
|
|
58
60
|
```bash
|
|
59
|
-
|
|
60
|
-
zclean init
|
|
61
|
+
npx --yes z-clean audit
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
정리 전에 후보를 검토하세요:
|
|
64
65
|
|
|
65
66
|
```bash
|
|
66
67
|
npx --yes z-clean
|
|
68
|
+
npx --yes z-clean report
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
계속 사용하려면 전역 설치하세요:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g z-clean
|
|
75
|
+
zclean report
|
|
67
76
|
```
|
|
68
77
|
|
|
78
|
+
자동 실행은 선택 사항입니다:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
zclean init
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`zclean init`은 Claude Code session hook과 OS scheduler를 설치합니다. dry-run 결과를 확인한 후에만 실행하세요. 상주 daemon을 설치하지는 않습니다.
|
|
85
|
+
|
|
69
86
|
## 자주 쓰는 명령
|
|
70
87
|
|
|
71
88
|
```bash
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ AI coding runtime hygiene
|
|
|
15
15
|
[](#)
|
|
16
16
|
[](#platform-status)
|
|
17
17
|
[](#)
|
|
18
|
-
[](#)
|
|
19
19
|
[](https://github.com/TheStack-ai/zclean)
|
|
20
20
|
[](https://github.com/rohitg00/awesome-claude-code-toolkit)
|
|
21
21
|
|
|
@@ -51,22 +51,24 @@ The goal is narrower: **AI coding cleanup and developer workspace hygiene**.
|
|
|
51
51
|
|
|
52
52
|
## Quick Start
|
|
53
53
|
|
|
54
|
+
Start with a read-only audit. This does not install hooks, schedule jobs, or clean anything:
|
|
55
|
+
|
|
54
56
|
```bash
|
|
55
|
-
|
|
56
|
-
zclean init
|
|
57
|
+
npx --yes z-clean audit
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Review the candidates before cleaning:
|
|
60
61
|
|
|
61
62
|
```bash
|
|
62
63
|
npx --yes z-clean
|
|
64
|
+
npx --yes z-clean report
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
Install the CLI when you are ready to keep using it:
|
|
66
68
|
|
|
67
69
|
```bash
|
|
70
|
+
npm install -g z-clean
|
|
68
71
|
zclean report
|
|
69
|
-
zclean report --json
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
Clean only when you decide:
|
|
@@ -76,6 +78,14 @@ zclean --yes
|
|
|
76
78
|
zclean cache --yes
|
|
77
79
|
```
|
|
78
80
|
|
|
81
|
+
Optional automation:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
zclean init
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`zclean init` installs the Claude Code session hook and the native OS scheduler. Run it only after reviewing the dry-run output. It does not install a persistent daemon.
|
|
88
|
+
|
|
79
89
|
## Why developers use it
|
|
80
90
|
|
|
81
91
|
AI coding tools spawn many short-lived runtimes while they work:
|
package/README.zh.md
CHANGED
|
@@ -55,17 +55,34 @@ AI 编程工具在工作时会启动很多临时运行时:
|
|
|
55
55
|
|
|
56
56
|
## 安装
|
|
57
57
|
|
|
58
|
+
先运行只读 audit;它不会安装 hook、创建定时任务或执行清理:
|
|
59
|
+
|
|
58
60
|
```bash
|
|
59
|
-
|
|
60
|
-
zclean init
|
|
61
|
+
npx --yes z-clean audit
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
清理前先查看候选项:
|
|
64
65
|
|
|
65
66
|
```bash
|
|
66
67
|
npx --yes z-clean
|
|
68
|
+
npx --yes z-clean report
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
准备长期使用时再全局安装:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g z-clean
|
|
75
|
+
zclean report
|
|
67
76
|
```
|
|
68
77
|
|
|
78
|
+
自动运行是可选项:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
zclean init
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`zclean init` 会安装 Claude Code session hook 和原生 OS scheduler。请先检查 dry-run 结果;它不会安装常驻 daemon。
|
|
85
|
+
|
|
69
86
|
## 常用命令
|
|
70
87
|
|
|
71
88
|
```bash
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const {
|
|
7
|
+
appendPrivateFile,
|
|
8
|
+
sanitizeHistoryEntry,
|
|
9
|
+
sanitizeHistoryFile,
|
|
10
|
+
secureDirectory,
|
|
11
|
+
secureFile,
|
|
12
|
+
writePrivateFile,
|
|
13
|
+
} = require('./storage-security');
|
|
6
14
|
|
|
7
15
|
const CONFIG_DIR_ENV = 'ZCLEAN_CONFIG_DIR';
|
|
8
16
|
|
|
@@ -68,10 +76,7 @@ function parseMemory(str) {
|
|
|
68
76
|
* Ensure the config directory exists.
|
|
69
77
|
*/
|
|
70
78
|
function ensureConfigDir() {
|
|
71
|
-
|
|
72
|
-
if (!fs.existsSync(configDir)) {
|
|
73
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
74
|
-
}
|
|
79
|
+
secureDirectory(getConfigDir());
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
/**
|
|
@@ -79,8 +84,10 @@ function ensureConfigDir() {
|
|
|
79
84
|
*/
|
|
80
85
|
function loadConfig() {
|
|
81
86
|
ensureConfigDir();
|
|
87
|
+
sanitizeHistoryFile(getLogFile());
|
|
82
88
|
const configFile = getConfigFile();
|
|
83
89
|
if (fs.existsSync(configFile)) {
|
|
90
|
+
secureFile(configFile);
|
|
84
91
|
try {
|
|
85
92
|
const raw = fs.readFileSync(configFile, 'utf-8');
|
|
86
93
|
const userConfig = JSON.parse(raw);
|
|
@@ -98,7 +105,7 @@ function loadConfig() {
|
|
|
98
105
|
*/
|
|
99
106
|
function saveConfig(config) {
|
|
100
107
|
ensureConfigDir();
|
|
101
|
-
|
|
108
|
+
writePrivateFile(getConfigFile(), JSON.stringify(config, null, 2) + '\n');
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
/**
|
|
@@ -107,8 +114,8 @@ function saveConfig(config) {
|
|
|
107
114
|
*/
|
|
108
115
|
function appendLog(entry) {
|
|
109
116
|
ensureConfigDir();
|
|
110
|
-
const
|
|
111
|
-
|
|
117
|
+
const sanitized = sanitizeHistoryEntry({ timestamp: new Date().toISOString(), ...entry });
|
|
118
|
+
appendPrivateFile(getLogFile(), JSON.stringify(sanitized) + '\n');
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
/**
|
|
@@ -122,7 +129,7 @@ function readLogs(limit = 50) {
|
|
|
122
129
|
return lines
|
|
123
130
|
.slice(-limit)
|
|
124
131
|
.map((line) => {
|
|
125
|
-
try { return JSON.parse(line); } catch { return null; }
|
|
132
|
+
try { return sanitizeHistoryEntry(JSON.parse(line)); } catch { return null; }
|
|
126
133
|
})
|
|
127
134
|
.filter(Boolean);
|
|
128
135
|
} catch {
|
|
@@ -140,15 +147,15 @@ function pruneLogs(config) {
|
|
|
140
147
|
|
|
141
148
|
const cutoff = Date.now() - retentionMs;
|
|
142
149
|
const lines = fs.readFileSync(logFile, 'utf-8').trim().split('\n').filter(Boolean);
|
|
143
|
-
const kept = lines.
|
|
150
|
+
const kept = lines.flatMap((line) => {
|
|
144
151
|
try {
|
|
145
|
-
const entry = JSON.parse(line);
|
|
146
|
-
return new Date(entry.timestamp).getTime() >= cutoff;
|
|
152
|
+
const entry = sanitizeHistoryEntry(JSON.parse(line));
|
|
153
|
+
return new Date(entry.timestamp).getTime() >= cutoff ? [JSON.stringify(entry)] : [];
|
|
147
154
|
} catch {
|
|
148
|
-
return
|
|
155
|
+
return [];
|
|
149
156
|
}
|
|
150
157
|
});
|
|
151
|
-
|
|
158
|
+
writePrivateFile(logFile, kept.join('\n') + (kept.length ? '\n' : ''));
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
/**
|
package/src/killer.js
CHANGED
|
@@ -56,7 +56,6 @@ function killZombies(zombies, config) {
|
|
|
56
56
|
action: 'kill',
|
|
57
57
|
pid: proc.pid,
|
|
58
58
|
name: proc.name,
|
|
59
|
-
cmd: proc.cmd,
|
|
60
59
|
reason: proc.reason,
|
|
61
60
|
memFreed: proc.mem,
|
|
62
61
|
});
|
|
@@ -69,7 +68,6 @@ function killZombies(zombies, config) {
|
|
|
69
68
|
action: 'kill-failed',
|
|
70
69
|
pid: proc.pid,
|
|
71
70
|
name: proc.name,
|
|
72
|
-
cmd: proc.cmd,
|
|
73
71
|
error: killResult.error,
|
|
74
72
|
});
|
|
75
73
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function providerDiagnostic(provider, code, error) {
|
|
4
|
+
return {
|
|
5
|
+
code,
|
|
6
|
+
provider,
|
|
7
|
+
message: diagnosticMessage(error),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function diagnosticMessage(error) {
|
|
12
|
+
const stderr = error && error.stderr ? String(error.stderr).trim() : '';
|
|
13
|
+
const fallback = error instanceof Error ? error.message : String(error || '');
|
|
14
|
+
return (stderr || fallback).replace(/\s+/g, ' ').slice(0, 500);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = { providerDiagnostic };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const SENSITIVE_HISTORY_FIELDS = ['args', 'argv', 'cmd', 'command', 'commandLine'];
|
|
6
|
+
|
|
7
|
+
function secureDirectory(directory) {
|
|
8
|
+
if (!fs.existsSync(directory)) {
|
|
9
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
10
|
+
}
|
|
11
|
+
if (process.platform !== 'win32') fs.chmodSync(directory, 0o700);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writePrivateFile(file, content) {
|
|
15
|
+
fs.writeFileSync(file, content, { encoding: 'utf-8', mode: 0o600 });
|
|
16
|
+
secureFile(file);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function appendPrivateFile(file, content) {
|
|
20
|
+
fs.appendFileSync(file, content, { encoding: 'utf-8', mode: 0o600 });
|
|
21
|
+
secureFile(file);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function secureFile(file) {
|
|
25
|
+
if (process.platform !== 'win32') fs.chmodSync(file, 0o600);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeHistoryEntry(entry) {
|
|
29
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return entry;
|
|
30
|
+
const sanitized = { ...entry };
|
|
31
|
+
for (const field of SENSITIVE_HISTORY_FIELDS) delete sanitized[field];
|
|
32
|
+
return sanitized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitizeHistoryFile(file) {
|
|
36
|
+
if (!fs.existsSync(file)) return;
|
|
37
|
+
|
|
38
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
39
|
+
const lines = raw.split(/\r?\n/).filter(Boolean);
|
|
40
|
+
let changed = false;
|
|
41
|
+
const sanitized = lines.map((line) => {
|
|
42
|
+
try {
|
|
43
|
+
const next = JSON.stringify(sanitizeHistoryEntry(JSON.parse(line)));
|
|
44
|
+
if (next !== line) changed = true;
|
|
45
|
+
return next;
|
|
46
|
+
} catch {
|
|
47
|
+
return line;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (changed) {
|
|
52
|
+
writePrivateFile(file, sanitized.join('\n') + (sanitized.length ? '\n' : ''));
|
|
53
|
+
} else {
|
|
54
|
+
secureFile(file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
appendPrivateFile,
|
|
60
|
+
sanitizeHistoryEntry,
|
|
61
|
+
sanitizeHistoryFile,
|
|
62
|
+
secureDirectory,
|
|
63
|
+
secureFile,
|
|
64
|
+
writePrivateFile,
|
|
65
|
+
};
|
package/src/windows-processes.js
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { providerDiagnostic } = require('./process-diagnostic');
|
|
4
|
+
|
|
3
5
|
function normalizePid(pid) {
|
|
4
6
|
const parsed = Number(pid);
|
|
5
7
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
function readWMICProcesses(runtime) {
|
|
11
|
+
try {
|
|
12
|
+
runtime.execSync('where wmic', {
|
|
13
|
+
encoding: 'utf-8',
|
|
14
|
+
timeout: 5000,
|
|
15
|
+
maxBuffer: 1024 * 1024,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
});
|
|
18
|
+
} catch {
|
|
19
|
+
return { processes: [], warnings: [] };
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
let output;
|
|
10
23
|
try {
|
|
11
24
|
output = runtime.execSync(
|
|
12
25
|
'wmic process get ProcessId,ParentProcessId,CommandLine,WorkingSetSize,CreationDate /format:csv',
|
|
13
|
-
{
|
|
26
|
+
{
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: 15000,
|
|
29
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
30
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
31
|
+
}
|
|
14
32
|
);
|
|
15
33
|
} catch (err) {
|
|
16
34
|
return {
|
|
@@ -35,7 +53,12 @@ function readCIMProcesses(runtime) {
|
|
|
35
53
|
try {
|
|
36
54
|
output = runtime.execSync(
|
|
37
55
|
'powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CommandLine,WorkingSetSize,CreationDate | ConvertTo-Json -Compress"',
|
|
38
|
-
{
|
|
56
|
+
{
|
|
57
|
+
encoding: 'utf-8',
|
|
58
|
+
timeout: 15000,
|
|
59
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
61
|
+
}
|
|
39
62
|
);
|
|
40
63
|
} catch (err) {
|
|
41
64
|
return {
|
|
@@ -63,7 +86,12 @@ function readWindowsProcess(pid, runtime) {
|
|
|
63
86
|
try {
|
|
64
87
|
output = runtime.execSync(
|
|
65
88
|
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process -Filter 'ProcessId = ${safePid}' | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json -Compress"`,
|
|
66
|
-
{
|
|
89
|
+
{
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
timeout: 5000,
|
|
92
|
+
maxBuffer: 1024 * 1024,
|
|
93
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
|
+
}
|
|
67
95
|
);
|
|
68
96
|
} catch {
|
|
69
97
|
return null;
|
|
@@ -87,7 +115,12 @@ function windowsProcessExists(pid, runtime) {
|
|
|
87
115
|
try {
|
|
88
116
|
const output = runtime.execSync(
|
|
89
117
|
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "Get-CimInstance Win32_Process -Filter 'ProcessId = ${safePid}' | Select-Object ProcessId | ConvertTo-Json -Compress"`,
|
|
90
|
-
{
|
|
118
|
+
{
|
|
119
|
+
encoding: 'utf-8',
|
|
120
|
+
timeout: 3000,
|
|
121
|
+
maxBuffer: 1024 * 1024,
|
|
122
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
+
}
|
|
91
124
|
);
|
|
92
125
|
return parseJsonRows(String(output || '')).some((row) => Number(readField(row, 'ProcessId')) === safePid);
|
|
93
126
|
} catch {
|
|
@@ -238,14 +271,6 @@ function parseCsvLine(line) {
|
|
|
238
271
|
return parts;
|
|
239
272
|
}
|
|
240
273
|
|
|
241
|
-
function providerDiagnostic(provider, code, err) {
|
|
242
|
-
return {
|
|
243
|
-
code,
|
|
244
|
-
provider,
|
|
245
|
-
message: err instanceof Error ? err.message : String(err || ''),
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
|
|
249
274
|
module.exports = {
|
|
250
275
|
normalizePid,
|
|
251
276
|
readCIMProcesses,
|