ccperm 1.9.1 → 1.9.3

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
@@ -4,84 +4,78 @@
4
4
 
5
5
  [English](README.md)
6
6
 
7
- Claude Code는 프로젝트마다 `.claude/settings*.json`에 허용한 권한(Bash 명령, WebFetch 도메인, MCP 도구 등)을 저장합니다. 여러 프로젝트를 오가다 보면 어디서 뭘 허용했는지 파악하기 어려운데, **ccperm**으로 전체 권한을 한번에 점검할 있습니다.
7
+ Claude Code는 프로젝트마다 `.claude/settings*.json`에 허용한 권한(Bash 명령, WebFetch 도메인, MCP 도구 등)을 저장합니다. 여러 프로젝트를 오가다 보면 어디서 뭘 허용했는지 파악하기 어려운데, **ccperm**은 디렉토리 전체를 스캔해서 모든 설정 파일을 찾고, 인터랙티브 TUI 또는 텍스트로 보여줍니다.
8
8
 
9
9
  ## 빠른 시작
10
10
 
11
11
  ```bash
12
- npx ccperm --all
12
+ npx ccperm
13
13
  ```
14
14
 
15
15
  설치 없이 바로 실행됩니다. 글로벌 설치도 가능:
16
16
 
17
17
  ```bash
18
18
  npm i -g ccperm
19
- ccperm --all
19
+ ccperm
20
20
  ```
21
21
 
22
- ## 사용법
23
-
24
- ```bash
25
- npx ccperm # 현재 프로젝트 권한 점검
26
- npx ccperm --all # 홈 디렉토리 아래 모든 프로젝트 점검
27
- npx ccperm --fix # deprecated 패턴 자동 수정
28
- npx ccperm --all --fix # 전체 점검 + 수정
29
- ```
30
-
31
- ## 출력 예시
32
-
33
- ```
34
- ━━━ Claude Code Permission Audit ━━━
35
-
36
- Scope: ~ (all projects)
37
- Scanned 12 files:
38
-
39
- ~/Documents/project-a/.claude/settings.local.json (Bash: 5, WebFetch: 3, Tools: 1)
40
- Bash (5)
41
- npm run build *
42
- docker compose *
43
- curl *
44
- git add *
45
- ssh *
46
- WebFetch (3)
47
- github.com
48
- docs.anthropic.com
49
- api.example.com
50
- Tools (1)
51
- WebSearch
52
-
53
- ~/Documents/project-b/.claude/settings.local.json (Bash: 2, MCP: 3)
54
- Bash (2)
55
- python3 *
56
- pytest *
57
- MCP (3)
58
- browseros__browser_navigate
59
- browseros__browser_click_element
60
- browseros__browser_get_screenshot
61
-
62
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63
- All clean! No deprecated :* patterns found.
64
- ```
22
+ 기본 동작: `~` 아래 모든 프로젝트를 스캔하고 인터랙티브 TUI를 실행합니다.
65
23
 
66
24
  ## 옵션
67
25
 
68
26
  | 플래그 | 설명 |
69
27
  |--------|------|
70
- | `--all` | 디렉토리 아래 전체 프로젝트 스캔 |
71
- | `--fix` | deprecated `:*` 패턴을 ` *`로 자동 수정 |
28
+ | `--cwd` | 현재 디렉토리만 스캔 (기본값: `~` 아래 전체) |
29
+ | `--static` | 텍스트 출력 강제 (파이프/비TTY 환경에서 기본값) |
30
+ | `--verbose` | 모든 권한을 상세 나열하는 텍스트 출력 |
31
+ | `--update` | `npm install -g ccperm@latest`로 자체 업데이트 |
32
+ | `--debug` | 스캔 진단 정보 표시 (파일 경로, 소요 시간) |
72
33
  | `--help`, `-h` | 도움말 표시 |
73
34
  | `--version`, `-v` | 버전 표시 |
74
- | `--update` | 업데이트 확인 |
75
35
 
76
- ## Deprecated 패턴 수정
36
+ ## 인터랙티브 TUI
37
+
38
+ TTY 환경(기본)에서는 박스 프레임 TUI가 실행됩니다:
77
39
 
78
- Claude Code가 이전에 "Allow always" 권한을 `:*`로 저장하는 버그가 있었습니다. 이로 인해 권한 팝업이 반복되는데, `--fix`로 자동 수정할 있습니다.
40
+ **목록 뷰** -- 프로젝트가 권한 기준으로 정렬됩니다. 상단에 `GLOBAL` 섹션이 `~/.claude/` 설정을 보여줍니다. 행은 카테고리별 개수(Bash, WebFetch, MCP, Tools)와 `shared`/`local` 라벨로 `settings.json`과 `settings.local.json`을 구분합니다. 최대 25행 표시, 나머지는 스크롤.
79
41
 
80
42
  ```
81
- 수정 전: Bash(npm run build:*)
82
- 수정 후: Bash(npm run build *)
43
+ ccperm ──────────────────────────────── 1/8
44
+ PROJECT Bash WebFetch MCP TOTAL
45
+ ├──────────────────────────────────────────────┤
46
+ │ ★ GLOBAL 2 2 │
47
+ ├──────────────────────────────────────────────┤
48
+ │▸ my-project local 5 3 · 8 │
49
+ │ other-app shared 2 · 3 5 │
50
+ │ ... │
51
+ └ [↑↓] navigate [Enter] detail [q] quit ────┘
83
52
  ```
84
53
 
54
+ **상세 뷰** -- Enter로 프로젝트를 펼칩니다. 카테고리(Bash, WebFetch, MCP, Tools)를 Enter로 접고 펼 수 있습니다.
55
+
56
+ **정보 모드** -- `[i]`를 누르면 각 권한에 위험도 표시(`●` 초록/노랑/빨강)와 설명이 나타납니다.
57
+
58
+ 키 조작: `↑↓` 이동, `Enter` 선택/펼치기, `[i]` 정보 토글, `Esc`/`Backspace` 뒤로, `q`/`Ctrl+C` 종료.
59
+
60
+ ## 텍스트 출력
61
+
62
+ `--static` 플래그(또는 파이프)로 텍스트 출력:
63
+
64
+ ```bash
65
+ ccperm --static # 요약 테이블
66
+ ccperm --static --verbose # 전체 권한 상세 나열
67
+ ```
68
+
69
+ ## 권한 레벨
70
+
71
+ ccperm은 Claude Code 설정을 세 단계로 구분합니다:
72
+
73
+ | 레벨 | 파일 | 범위 |
74
+ |------|------|------|
75
+ | **global** | `~/.claude/settings.json` | 모든 프로젝트에 적용 |
76
+ | **shared** | `<project>/.claude/settings.json` | 프로젝트별, git에 커밋됨 |
77
+ | **local** | `<project>/.claude/settings.local.json` | 프로젝트별, gitignore 대상 |
78
+
85
79
  ## 요구사항
86
80
 
87
81
  - Node.js >= 18
package/README.md CHANGED
@@ -4,84 +4,78 @@ Audit Claude Code permissions across all your projects.
4
4
 
5
5
  [한국어](README.ko.md)
6
6
 
7
- Claude Code stores allowed permissions (Bash commands, WebFetch domains, MCP tools, etc.) in `.claude/settings*.json` per project. As you work across many projects, these permissions pile up silently. **ccperm** lets you see exactly what you've allowed, everywhere.
7
+ Claude Code stores allowed permissions (Bash commands, WebFetch domains, MCP tools, etc.) in `.claude/settings*.json` per project. As you work across many projects, these permissions pile up silently. **ccperm** scans your home directory, finds every settings file, and shows what you've allowed -- in an interactive TUI or static text output.
8
8
 
9
9
  ## Quick Start
10
10
 
11
11
  ```bash
12
- npx ccperm --all
12
+ npx ccperm
13
13
  ```
14
14
 
15
15
  No install needed. Or install globally:
16
16
 
17
17
  ```bash
18
18
  npm i -g ccperm
19
- ccperm --all
19
+ ccperm
20
20
  ```
21
21
 
22
- ## Usage
23
-
24
- ```bash
25
- npx ccperm # Audit current project
26
- npx ccperm --all # Audit all projects under ~
27
- npx ccperm --fix # Auto-fix deprecated patterns
28
- npx ccperm --all --fix # Audit + fix all projects
29
- ```
30
-
31
- ## Output example
32
-
33
- ```
34
- ━━━ Claude Code Permission Audit ━━━
35
-
36
- Scope: ~ (all projects)
37
- Scanned 12 files:
38
-
39
- ~/Documents/project-a/.claude/settings.local.json (Bash: 5, WebFetch: 3, Tools: 1)
40
- Bash (5)
41
- npm run build *
42
- docker compose *
43
- curl *
44
- git add *
45
- ssh *
46
- WebFetch (3)
47
- github.com
48
- docs.anthropic.com
49
- api.example.com
50
- Tools (1)
51
- WebSearch
52
-
53
- ~/Documents/project-b/.claude/settings.local.json (Bash: 2, MCP: 3)
54
- Bash (2)
55
- python3 *
56
- pytest *
57
- MCP (3)
58
- browseros__browser_navigate
59
- browseros__browser_click_element
60
- browseros__browser_get_screenshot
61
-
62
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63
- All clean! No deprecated :* patterns found.
64
- ```
22
+ By default, ccperm scans all projects under `~` and launches an interactive TUI.
65
23
 
66
24
  ## Options
67
25
 
68
26
  | Flag | Description |
69
27
  |------|-------------|
70
- | `--all` | Scan all projects under home directory |
71
- | `--fix` | Auto-fix deprecated `:*` patterns to ` *` |
28
+ | `--cwd` | Scan current directory only (default: all projects under `~`) |
29
+ | `--static` | Force text output (default when piped / non-TTY) |
30
+ | `--verbose` | Detailed static output with all permissions listed |
31
+ | `--update` | Self-update via `npm install -g ccperm@latest` |
32
+ | `--debug` | Show scan diagnostics (file paths, timing) |
72
33
  | `--help`, `-h` | Show help |
73
34
  | `--version`, `-v` | Show version |
74
- | `--update` | Check for updates |
75
35
 
76
- ## Deprecated pattern fix
36
+ ## Interactive TUI
37
+
38
+ When running in a TTY (the default), ccperm opens a box-frame TUI:
77
39
 
78
- Claude Code previously saved "Allow always" permissions with `:*` instead of ` *`, causing permission popups to repeat. `--fix` detects and corrects this automatically.
40
+ **List view** -- Projects sorted by permission count. `GLOBAL` section at top shows `~/.claude/` settings. Each row shows category counts (Bash, WebFetch, MCP, Tools) and a `shared`/`local` label distinguishing `settings.json` vs `settings.local.json`. Max 25 visible rows; scroll for more.
79
41
 
80
42
  ```
81
- Before: Bash(npm run build:*)
82
- After: Bash(npm run build *)
43
+ ccperm ──────────────────────────────── 1/8 ┐
44
+ │ PROJECT Bash WebFetch MCP TOTAL
45
+ ├──────────────────────────────────────────────┤
46
+ │ ★ GLOBAL 2 2 │
47
+ ├──────────────────────────────────────────────┤
48
+ │▸ my-project local 5 3 · 8 │
49
+ │ other-app shared 2 · 3 5 │
50
+ │ ... │
51
+ └ [↑↓] navigate [Enter] detail [q] quit ────┘
83
52
  ```
84
53
 
54
+ **Detail view** -- Press Enter to expand a project. Categories (Bash, WebFetch, MCP, Tools) are collapsible; press Enter to toggle.
55
+
56
+ **Info mode** -- Press `[i]` to show risk indicators (`●` green/yellow/red) and descriptions for each permission.
57
+
58
+ Keys: `↑↓` navigate, `Enter` select/expand, `[i]` toggle info, `Esc`/`Backspace` back, `q`/`Ctrl+C` quit.
59
+
60
+ ## Static Output
61
+
62
+ Use `--static` (or pipe to another command) for text output:
63
+
64
+ ```bash
65
+ ccperm --static # compact table
66
+ ccperm --static --verbose # full permission listing
67
+ ```
68
+
69
+ ## Permission Levels
70
+
71
+ ccperm distinguishes three levels of Claude Code settings:
72
+
73
+ | Level | File | Scope |
74
+ |-------|------|-------|
75
+ | **global** | `~/.claude/settings.json` | Applies to all projects |
76
+ | **shared** | `<project>/.claude/settings.json` | Per-project, committed to git |
77
+ | **local** | `<project>/.claude/settings.local.json` | Per-project, gitignored |
78
+
85
79
  ## Requirements
86
80
 
87
81
  - Node.js >= 18
package/dist/explain.js CHANGED
@@ -1,112 +1,127 @@
1
1
  "use strict";
2
2
  // Pattern-based permission explainer
3
+ // Input is the "label" from categorize(), not the raw permission string
3
4
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.explainPermission = explainPermission;
5
+ exports.explainBash = explainBash;
6
+ exports.explainWebFetch = explainWebFetch;
7
+ exports.explainMcp = explainMcp;
8
+ exports.explainTool = explainTool;
9
+ exports.explain = explain;
5
10
  const BASH_COMMANDS = {
6
11
  // [description, risk: green/yellow/red]
7
- 'git': ['Git version control commands', 'green'],
8
- 'npm': ['Node.js package manager can run scripts', 'yellow'],
9
- 'npx': ['Run npm packages — can execute arbitrary code', 'yellow'],
12
+ 'git': ['Git version control', 'green'],
13
+ 'npm': ['Package manager (can run scripts)', 'yellow'],
14
+ 'npx': ['Run npm packages', 'yellow'],
10
15
  'node': ['Run Node.js scripts', 'yellow'],
11
- 'bun': ['Bun runtime — run scripts, install packages', 'yellow'],
12
- 'deno': ['Deno runtime — run scripts', 'yellow'],
16
+ 'bun': ['Bun runtime', 'yellow'],
17
+ 'deno': ['Deno runtime', 'yellow'],
13
18
  'python': ['Run Python scripts', 'yellow'],
14
19
  'python3': ['Run Python scripts', 'yellow'],
15
- 'pip': ['Python package manager — can run setup scripts', 'yellow'],
16
- 'pip3': ['Python package manager — can run setup scripts', 'yellow'],
17
- 'docker': ['Docker container management', 'yellow'],
18
- 'docker-compose': ['Docker Compose multi-container management', 'yellow'],
19
- 'curl': ['HTTP requests from command line', 'yellow'],
20
- 'wget': ['Download files from the web', 'yellow'],
20
+ 'pip': ['Python package manager', 'yellow'],
21
+ 'pip3': ['Python package manager', 'yellow'],
22
+ 'docker': ['Container management', 'yellow'],
23
+ 'docker-compose': ['Multi-container management', 'yellow'],
24
+ 'curl': ['HTTP requests', 'yellow'],
25
+ 'wget': ['Download files', 'yellow'],
21
26
  'ssh': ['Remote shell access', 'red'],
22
- 'scp': ['Remote file copy over SSH', 'red'],
23
- 'rsync': ['File synchronization (local or remote)', 'yellow'],
24
- 'rm': ['Delete files and directories', 'red'],
25
- 'chmod': ['Change file permissions', 'yellow'],
26
- 'chown': ['Change file ownership', 'red'],
27
+ 'scp': ['Remote file copy', 'red'],
28
+ 'rsync': ['File sync (local/remote)', 'yellow'],
29
+ 'rm': ['Delete files', 'red'],
30
+ 'chmod': ['Change permissions', 'yellow'],
31
+ 'chown': ['Change ownership', 'red'],
27
32
  'kill': ['Terminate processes', 'yellow'],
28
- 'sudo': ['Run commands as superuser', 'red'],
29
- 'apt': ['System package manager (Debian/Ubuntu)', 'red'],
30
- 'apt-get': ['System package manager (Debian/Ubuntu)', 'red'],
31
- 'brew': ['Homebrew package manager (macOS)', 'yellow'],
32
- 'make': ['Build automation — runs Makefile targets', 'yellow'],
33
- 'cargo': ['Rust package manager and build tool', 'yellow'],
34
- 'go': ['Go build and package tool', 'yellow'],
35
- 'mvn': ['Maven Java build tool', 'yellow'],
36
- 'gradle': ['Gradle build tool', 'yellow'],
37
- 'yarn': ['Yarn package manager — can run scripts', 'yellow'],
38
- 'pnpm': ['pnpm package manager — can run scripts', 'yellow'],
33
+ 'sudo': ['Superuser access', 'red'],
34
+ 'apt': ['System packages (Debian)', 'red'],
35
+ 'apt-get': ['System packages (Debian)', 'red'],
36
+ 'brew': ['Homebrew packages', 'yellow'],
37
+ 'make': ['Build automation', 'yellow'],
38
+ 'cargo': ['Rust build tool', 'yellow'],
39
+ 'go': ['Go build tool', 'yellow'],
40
+ 'mvn': ['Maven build', 'yellow'],
41
+ 'gradle': ['Gradle build', 'yellow'],
42
+ 'yarn': ['Package manager', 'yellow'],
43
+ 'pnpm': ['Package manager', 'yellow'],
39
44
  'tsc': ['TypeScript compiler', 'green'],
40
- 'eslint': ['JavaScript/TypeScript linter', 'green'],
41
- 'prettier': ['Code formatter', 'green'],
42
- 'jest': ['JavaScript test runner', 'green'],
43
- 'vitest': ['Vite-based test runner', 'green'],
44
- 'cat': ['Read file contents', 'green'],
45
- 'ls': ['List directory contents', 'green'],
46
- 'find': ['Search for files', 'green'],
47
- 'grep': ['Search text patterns in files', 'green'],
48
- 'sed': ['Stream editor — modify file contents', 'yellow'],
49
- 'awk': ['Text processing language', 'green'],
50
- 'wc': ['Count lines/words/bytes', 'green'],
51
- 'head': ['Show first lines of file', 'green'],
52
- 'tail': ['Show last lines of file', 'green'],
45
+ 'eslint': ['Linter', 'green'],
46
+ 'prettier': ['Formatter', 'green'],
47
+ 'jest': ['Test runner', 'green'],
48
+ 'vitest': ['Test runner', 'green'],
49
+ 'cat': ['Read files', 'green'],
50
+ 'ls': ['List directories', 'green'],
51
+ 'find': ['Search files', 'green'],
52
+ 'grep': ['Search text', 'green'],
53
+ 'sed': ['Stream editor', 'yellow'],
54
+ 'awk': ['Text processing', 'green'],
55
+ 'wc': ['Count lines/words', 'green'],
56
+ 'head': ['First lines of file', 'green'],
57
+ 'tail': ['Last lines of file', 'green'],
53
58
  'mkdir': ['Create directories', 'green'],
54
59
  'cp': ['Copy files', 'green'],
55
60
  'mv': ['Move/rename files', 'yellow'],
56
61
  'echo': ['Print text', 'green'],
57
- 'env': ['Show/set environment variables', 'green'],
58
- 'which': ['Locate a command', 'green'],
59
- 'gh': ['GitHub CLI — repos, PRs, issues', 'yellow'],
60
- 'heroku': ['Heroku platform CLI', 'yellow'],
61
- 'vercel': ['Vercel deployment CLI', 'yellow'],
62
- 'aws': ['AWS CLI — cloud infrastructure', 'red'],
62
+ 'env': ['Environment variables', 'green'],
63
+ 'which': ['Locate command', 'green'],
64
+ 'gh': ['GitHub CLI', 'yellow'],
65
+ 'heroku': ['Heroku CLI', 'yellow'],
66
+ 'vercel': ['Vercel CLI', 'yellow'],
67
+ 'aws': ['AWS CLI', 'red'],
63
68
  'gcloud': ['Google Cloud CLI', 'red'],
64
69
  'az': ['Azure CLI', 'red'],
65
- 'kubectl': ['Kubernetes cluster management', 'red'],
70
+ 'kubectl': ['Kubernetes CLI', 'red'],
66
71
  'terraform': ['Infrastructure as Code', 'red'],
72
+ 'dd': ['Low-level disk copy', 'red'],
73
+ 'jq': ['JSON processor', 'green'],
74
+ 'bunx': ['Run bun packages', 'yellow'],
75
+ 'claude': ['Claude Code CLI', 'green'],
76
+ 'defaults': ['macOS defaults', 'yellow'],
67
77
  };
68
78
  const TOOL_DESCRIPTIONS = {
69
- 'Read': 'Read file contents from disk',
70
- 'Write': 'Create or overwrite files',
71
- 'Edit': 'Modify existing files (partial edits)',
72
- 'Glob': 'Search for files by name pattern',
73
- 'Grep': 'Search file contents with regex',
74
- 'WebSearch': 'Search the web via search engine',
79
+ 'Read': ['Read file contents', 'green'],
80
+ 'Write': ['Create/overwrite files', 'yellow'],
81
+ 'Edit': ['Modify existing files', 'yellow'],
82
+ 'Glob': ['Search files by pattern', 'green'],
83
+ 'Grep': ['Search file contents', 'green'],
84
+ 'WebSearch': ['Web search', 'green'],
75
85
  };
76
- function explainPermission(perm) {
77
- // Bash permissions
78
- const bashMatch = perm.match(/^Bash\((.+?)[\s)]/);
79
- if (bashMatch || perm === 'Bash') {
80
- const cmd = bashMatch ? bashMatch[1] : '';
81
- const entry = BASH_COMMANDS[cmd];
82
- if (entry) {
83
- return { description: entry[0], risk: entry[1], detail: `Command: ${cmd}` };
84
- }
85
- if (cmd) {
86
- return { description: `Run "${cmd}" command`, risk: 'yellow', detail: `Command: ${cmd}` };
87
- }
88
- return { description: 'Run shell commands', risk: 'red' };
89
- }
90
- // WebFetch
91
- const fetchMatch = perm.match(/^WebFetch\(domain:(.+)\)$/);
92
- if (fetchMatch) {
93
- const domain = fetchMatch[1];
94
- return { description: `HTTP requests to ${domain}`, risk: 'yellow', detail: `Domain: ${domain}` };
95
- }
96
- if (perm.startsWith('WebFetch')) {
97
- return { description: 'HTTP requests to external URLs', risk: 'yellow' };
98
- }
99
- // MCP tools
100
- if (perm.startsWith('mcp__') || perm.startsWith('mcp_')) {
101
- const parts = perm.split('__');
102
- const server = parts[1] || 'unknown';
103
- const tool = parts.slice(2).join('__') || 'unknown';
104
- return { description: `MCP: ${server} → ${tool}`, risk: 'yellow', detail: `Server: ${server}, Tool: ${tool}` };
105
- }
106
- // Standard tools
107
- const toolName = perm.match(/^(Read|Write|Edit|Glob|Grep|WebSearch)/)?.[1];
86
+ // Extract first command word from a bash label like "git branch:*" or "npm run build"
87
+ function extractCmd(label) {
88
+ // Remove :* or * suffix patterns
89
+ const clean = label.replace(/[:]\*.*$/, '').replace(/\s\*.*$/, '');
90
+ // Get first word
91
+ return clean.split(/[\s(]/)[0];
92
+ }
93
+ function explainBash(label) {
94
+ const cmd = extractCmd(label);
95
+ const entry = BASH_COMMANDS[cmd];
96
+ if (entry)
97
+ return { description: entry[0], risk: entry[1] };
98
+ return { description: '', risk: 'yellow' };
99
+ }
100
+ function explainWebFetch(label) {
101
+ return { description: label, risk: 'yellow' };
102
+ }
103
+ function explainMcp(label) {
104
+ const parts = label.replace(/^mcp__?/, '').split('__');
105
+ const server = parts[0] || '';
106
+ const tool = parts.slice(1).join(' ') || '';
107
+ return { description: tool ? `${server}: ${tool}` : server, risk: 'yellow' };
108
+ }
109
+ function explainTool(label) {
110
+ const toolName = label.match(/^(Read|Write|Edit|Glob|Grep|WebSearch)/)?.[1];
108
111
  if (toolName && TOOL_DESCRIPTIONS[toolName]) {
109
- return { description: TOOL_DESCRIPTIONS[toolName], risk: 'green' };
112
+ const entry = TOOL_DESCRIPTIONS[toolName];
113
+ return { description: entry[0], risk: entry[1] };
110
114
  }
111
- return { description: perm, risk: 'yellow' };
115
+ return { description: '', risk: 'yellow' };
116
+ }
117
+ function explain(category, label) {
118
+ if (category === 'Bash')
119
+ return explainBash(label);
120
+ if (category === 'WebFetch')
121
+ return explainWebFetch(label);
122
+ if (category === 'MCP')
123
+ return explainMcp(label);
124
+ if (category === 'Tools')
125
+ return explainTool(label);
126
+ return { description: '', risk: 'yellow' };
112
127
  }
@@ -120,7 +120,7 @@ function renderList(state, withPerms, emptyCount) {
120
120
  const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
121
121
  const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
122
122
  const catColWidth = catsPresent.length * 7;
123
- const maxName = Math.max(...withPerms.map((r) => r.isGlobal ? r.shortName.length + 2 : r.shortName.length), 7);
123
+ const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
124
124
  const nameWidth = Math.min(maxName, inner - catColWidth - 16);
125
125
  const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
126
126
  // box takes: top(1) + header(2) + sep(1) + content + globalSep?(1) + emptyLine?(1) + bottom(1)
@@ -140,8 +140,7 @@ function renderList(state, withPerms, emptyCount) {
140
140
  for (let i = state.scrollOffset; i < end; i++) {
141
141
  const r = withPerms[i];
142
142
  const isCursor = i === state.cursor;
143
- const displayName = r.isGlobal ? `★ ${r.shortName}` : r.shortName;
144
- const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
143
+ const truncName = r.shortName.length > nameWidth ? r.shortName.slice(0, nameWidth - 1) + '…' : r.shortName;
145
144
  const typeTag = r.isGlobal ? pad('', 7) : `${colors_js_1.DIM} ${pad(r.fileType, 6)}${colors_js_1.NC}`;
146
145
  const marker = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
147
146
  const nameStyle = isCursor ? `${colors_js_1.BOLD}` : r.isGlobal ? `${colors_js_1.YELLOW}` : '';
@@ -185,16 +184,16 @@ function renderDetail(state, withPerms, results) {
185
184
  if (isOpen) {
186
185
  for (const item of group.items) {
187
186
  if (state.showInfo) {
188
- const info = (0, explain_js_1.explainPermission)(item.name);
187
+ const info = (0, explain_js_1.explain)(group.category, item.name);
189
188
  const riskColor = info.risk === 'red' ? colors_js_1.RED : info.risk === 'yellow' ? colors_js_1.YELLOW : colors_js_1.GREEN;
190
189
  const dot = `${riskColor}●${colors_js_1.NC}`;
191
- const maxLen = w - 16;
192
- const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
193
- navRows.push({ text: ` ${dot} ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name });
194
- navRows.push({ text: ` ${colors_js_1.DIM}${info.description}${colors_js_1.NC}` });
190
+ const desc = info.description ? ` ${colors_js_1.DIM}${info.description}${colors_js_1.NC}` : '';
191
+ const maxLen = w - 8 - (info.description ? info.description.length + 2 : 0);
192
+ const name = item.name.length > maxLen ? item.name.slice(0, Math.max(8, maxLen) - 1) + '…' : item.name;
193
+ navRows.push({ text: ` ${dot} ${name}${desc}`, perm: item.name });
195
194
  }
196
195
  else {
197
- const maxLen = w - 12;
196
+ const maxLen = w - 8;
198
197
  const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
199
198
  navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name });
200
199
  }
package/dist/renderer.js CHANGED
@@ -19,7 +19,7 @@ function printCompact(entries, summary) {
19
19
  const withPerms = [...globals, ...projects];
20
20
  const emptyCount = entries.filter((r) => r.totalCount === 0 && !r.isGlobal).length;
21
21
  // header
22
- const maxName = Math.max(...withPerms.map((r) => r.isGlobal ? r.shortName.length + 2 : r.shortName.length), 7);
22
+ const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
23
23
  const nameWidth = Math.min(maxName, 40);
24
24
  const header = ` ${colors_js_1.DIM}${pad('PROJECT', nameWidth)} ${catsPresent.map((c) => rpad(c, 5)).join(' ')} TOTAL${colors_js_1.NC}`;
25
25
  console.log(header);
@@ -27,8 +27,7 @@ function printCompact(entries, summary) {
27
27
  // rows
28
28
  for (let i = 0; i < withPerms.length; i++) {
29
29
  const result = withPerms[i];
30
- const displayName = result.isGlobal ? `★ ${result.shortName}` : result.shortName;
31
- const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
30
+ const truncName = result.shortName.length > nameWidth ? result.shortName.slice(0, nameWidth - 1) + '…' : result.shortName;
32
31
  const typeTag = result.isGlobal ? pad('', 7) : `${colors_js_1.DIM} ${pad(result.fileType, 6)}${colors_js_1.NC}`;
33
32
  const nameStyle = result.isGlobal ? `${colors_js_1.YELLOW}` : '';
34
33
  const nameCol = ` ${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}${typeTag}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"