ccperm 1.8.6 → 1.9.1

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,112 @@
1
+ "use strict";
2
+ // Pattern-based permission explainer
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.explainPermission = explainPermission;
5
+ const BASH_COMMANDS = {
6
+ // [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'],
10
+ 'node': ['Run Node.js scripts', 'yellow'],
11
+ 'bun': ['Bun runtime — run scripts, install packages', 'yellow'],
12
+ 'deno': ['Deno runtime — run scripts', 'yellow'],
13
+ 'python': ['Run Python scripts', 'yellow'],
14
+ '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'],
21
+ '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
+ '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'],
39
+ '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'],
53
+ 'mkdir': ['Create directories', 'green'],
54
+ 'cp': ['Copy files', 'green'],
55
+ 'mv': ['Move/rename files', 'yellow'],
56
+ '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'],
63
+ 'gcloud': ['Google Cloud CLI', 'red'],
64
+ 'az': ['Azure CLI', 'red'],
65
+ 'kubectl': ['Kubernetes cluster management', 'red'],
66
+ 'terraform': ['Infrastructure as Code', 'red'],
67
+ };
68
+ 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',
75
+ };
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];
108
+ if (toolName && TOOL_DESCRIPTIONS[toolName]) {
109
+ return { description: TOOL_DESCRIPTIONS[toolName], risk: 'green' };
110
+ }
111
+ return { description: perm, risk: 'yellow' };
112
+ }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.startInteractive = startInteractive;
7
7
  const node_readline_1 = __importDefault(require("node:readline"));
8
8
  const colors_js_1 = require("./colors.js");
9
+ const explain_js_1 = require("./explain.js");
9
10
  // strip ANSI escape codes for visible length
10
11
  function visLen(s) {
11
12
  return s.replace(/\x1b\[[0-9;]*m/g, '').length;
@@ -42,7 +43,7 @@ function startInteractive(merged, results) {
42
43
  resolve();
43
44
  return;
44
45
  }
45
- const state = { view: 'list', cursor: 0, scrollOffset: 0, selectedProject: 0, detailCursor: 0, detailScroll: 0, expanded: new Set() };
46
+ const state = { view: 'list', cursor: 0, scrollOffset: 0, selectedProject: 0, detailCursor: 0, detailScroll: 0, expanded: new Set(), showInfo: false };
46
47
  node_readline_1.default.emitKeypressEvents(process.stdin);
47
48
  if (process.stdin.isTTY)
48
49
  process.stdin.setRawMode(true);
@@ -101,6 +102,9 @@ function startInteractive(merged, results) {
101
102
  else if (key.name === 'return') {
102
103
  state._toggle = true;
103
104
  }
105
+ else if (key.name === 'i') {
106
+ state.showInfo = !state.showInfo;
107
+ }
104
108
  }
105
109
  render();
106
110
  };
@@ -116,7 +120,7 @@ function renderList(state, withPerms, emptyCount) {
116
120
  const cats = ['Bash', 'WebFetch', 'MCP', 'Tools'];
117
121
  const catsPresent = cats.filter((c) => withPerms.some((r) => r.groups.has(c)));
118
122
  const catColWidth = catsPresent.length * 7;
119
- const maxName = Math.max(...withPerms.map((r) => r.shortName.length), 7);
123
+ const maxName = Math.max(...withPerms.map((r) => r.isGlobal ? r.shortName.length + 2 : r.shortName.length), 7);
120
124
  const nameWidth = Math.min(maxName, inner - catColWidth - 16);
121
125
  const hasGlobalSep = withPerms.some((r) => r.isGlobal) && withPerms.some((r) => !r.isGlobal);
122
126
  // box takes: top(1) + header(2) + sep(1) + content + globalSep?(1) + emptyLine?(1) + bottom(1)
@@ -136,12 +140,12 @@ function renderList(state, withPerms, emptyCount) {
136
140
  for (let i = state.scrollOffset; i < end; i++) {
137
141
  const r = withPerms[i];
138
142
  const isCursor = i === state.cursor;
139
- const truncName = r.shortName.length > nameWidth ? r.shortName.slice(0, nameWidth - 1) + '…' : r.shortName;
143
+ const displayName = r.isGlobal ? `★ ${r.shortName}` : r.shortName;
144
+ const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
140
145
  const typeTag = r.isGlobal ? pad('', 7) : `${colors_js_1.DIM} ${pad(r.fileType, 6)}${colors_js_1.NC}`;
141
- const prefix = r.isGlobal ? '★ ' : '';
142
146
  const marker = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
143
147
  const nameStyle = isCursor ? `${colors_js_1.BOLD}` : r.isGlobal ? `${colors_js_1.YELLOW}` : '';
144
- const nameCol = `${marker}${nameStyle}${prefix}${pad(truncName, nameWidth)}${colors_js_1.NC}${typeTag}`;
148
+ const nameCol = `${marker}${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}${typeTag}`;
145
149
  const catCols = catsPresent.map((c) => {
146
150
  const count = r.groups.get(c) || 0;
147
151
  if (count > 0)
@@ -179,10 +183,21 @@ function renderDetail(state, withPerms, results) {
179
183
  const arrow = isOpen ? '▾' : '▸';
180
184
  navRows.push({ text: `${colors_js_1.YELLOW}${arrow} ${group.category}${colors_js_1.NC} ${colors_js_1.DIM}(${group.items.length})${colors_js_1.NC}`, key });
181
185
  if (isOpen) {
182
- const maxLen = w - 12;
183
186
  for (const item of group.items) {
184
- const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
185
- navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}` });
187
+ if (state.showInfo) {
188
+ const info = (0, explain_js_1.explainPermission)(item.name);
189
+ const riskColor = info.risk === 'red' ? colors_js_1.RED : info.risk === 'yellow' ? colors_js_1.YELLOW : colors_js_1.GREEN;
190
+ 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}` });
195
+ }
196
+ else {
197
+ const maxLen = w - 12;
198
+ const name = item.name.length > maxLen ? item.name.slice(0, maxLen - 1) + '…' : item.name;
199
+ navRows.push({ text: ` ${colors_js_1.DIM}${name}${colors_js_1.NC}`, perm: item.name });
200
+ }
186
201
  }
187
202
  }
188
203
  }
@@ -219,7 +234,8 @@ function renderDetail(state, withPerms, results) {
219
234
  const prefix = isCursor ? `${colors_js_1.CYAN}▸ ` : ' ';
220
235
  lines.push(boxLine(`${prefix}${row.text}`, w));
221
236
  }
222
- lines.push(boxBottom('[↑↓] navigate [Enter] expand [Esc] back [q] quit', w));
237
+ const infoHint = state.showInfo ? '[i] hide info' : '[i] info';
238
+ lines.push(boxBottom(`[↑↓] navigate [Enter] expand ${infoHint} [Esc] back [q] quit`, w));
223
239
  process.stdout.write(lines.join('\n') + '\n');
224
240
  }
225
241
  function pad(s, n) {
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.shortName.length), 7);
22
+ const maxName = Math.max(...withPerms.map((r) => r.isGlobal ? r.shortName.length + 2 : 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,11 +27,11 @@ 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 truncName = result.shortName.length > nameWidth ? result.shortName.slice(0, nameWidth - 1) + '…' : result.shortName;
30
+ const displayName = result.isGlobal ? `★ ${result.shortName}` : result.shortName;
31
+ const truncName = displayName.length > nameWidth ? displayName.slice(0, nameWidth - 1) + '…' : displayName;
31
32
  const typeTag = result.isGlobal ? pad('', 7) : `${colors_js_1.DIM} ${pad(result.fileType, 6)}${colors_js_1.NC}`;
32
- const prefix = result.isGlobal ? '★ ' : '';
33
33
  const nameStyle = result.isGlobal ? `${colors_js_1.YELLOW}` : '';
34
- const nameCol = ` ${nameStyle}${prefix}${pad(truncName, nameWidth)}${colors_js_1.NC}${typeTag}`;
34
+ const nameCol = ` ${nameStyle}${pad(truncName, nameWidth)}${colors_js_1.NC}${typeTag}`;
35
35
  const catCols = catsPresent.map((c) => {
36
36
  const count = result.groups.get(c) || 0;
37
37
  return count > 0 ? rpad(count, 5) : `${colors_js_1.DIM}${rpad('·', 5)}${colors_js_1.NC}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccperm",
3
- "version": "1.8.6",
3
+ "version": "1.9.1",
4
4
  "description": "Audit Claude Code permissions across all your projects",
5
5
  "bin": {
6
6
  "ccperm": "bin/ccperm.js"