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 CHANGED
@@ -55,17 +55,34 @@ AI 코딩 도구는 작업 중 많은 임시 런타임을 만듭니다.
55
55
 
56
56
  ## 설치
57
57
 
58
+ 먼저 설치나 정리 없이 읽기 전용 audit을 실행하세요:
59
+
58
60
  ```bash
59
- npm install -g z-clean
60
- zclean init
61
+ npx --yes z-clean audit
61
62
  ```
62
63
 
63
- 설치 없이 1회 실행:
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
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white)](#)
16
16
  [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey?style=flat-square)](#platform-status)
17
17
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-blue?style=flat-square)](#)
18
- [![Tests](https://img.shields.io/badge/tests-101%20passing-brightgreen?style=flat-square)](#)
18
+ [![Tests](https://img.shields.io/badge/tests-105%20passing-brightgreen?style=flat-square)](#)
19
19
  [![GitHub stars](https://img.shields.io/github/stars/TheStack-ai/zclean?style=flat-square)](https://github.com/TheStack-ai/zclean)
20
20
  [![Mentioned in Awesome Claude Code Toolkit](https://awesome.re/mentioned-badge.svg)](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
- npm install -g z-clean
56
- zclean init
57
+ npx --yes z-clean audit
57
58
  ```
58
59
 
59
- One-off dry run without installation:
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
- Review before cleaning:
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
- npm install -g z-clean
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "z-clean",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "AI coding runtime hygiene reports, zombie cleanup, and safe workspace cache cleanup",
5
5
  "bin": {
6
6
  "zclean": "bin/zclean.js"
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
- const configDir = getConfigDir();
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
- fs.writeFileSync(getConfigFile(), JSON.stringify(config, null, 2) + '\n', 'utf-8');
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 line = JSON.stringify({ timestamp: new Date().toISOString(), ...entry }) + '\n';
111
- fs.appendFileSync(getLogFile(), line, 'utf-8');
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.filter((line) => {
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 false;
155
+ return [];
149
156
  }
150
157
  });
151
- fs.writeFileSync(logFile, kept.join('\n') + (kept.length ? '\n' : ''), 'utf-8');
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
+ };
@@ -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
- { encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
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
- { encoding: 'utf-8', timeout: 15000, maxBuffer: 10 * 1024 * 1024 }
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
- { encoding: 'utf-8', timeout: 5000, maxBuffer: 1024 * 1024 }
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
- { encoding: 'utf-8', timeout: 3000, maxBuffer: 1024 * 1024 }
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,