claudex-setup 1.6.0 → 1.8.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.
@@ -0,0 +1,348 @@
1
+ const { DOMAIN_PACKS } = require('./domain-packs');
2
+ const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
3
+
4
+ const PERMISSION_PROFILES = [
5
+ {
6
+ key: 'read-only',
7
+ label: 'Read-Only',
8
+ risk: 'low',
9
+ defaultMode: 'plan',
10
+ useWhen: 'Security review, discovery, and first contact with a mature repo.',
11
+ behavior: 'No file writes. Safe for audits, workshops, and approval flows.',
12
+ deny: ['Write(**)', 'Edit(**)', 'MultiEdit(**)', 'Bash(rm -rf *)', 'Bash(git reset --hard *)'],
13
+ },
14
+ {
15
+ key: 'suggest-only',
16
+ label: 'Suggest-Only',
17
+ risk: 'low',
18
+ defaultMode: 'acceptEdits',
19
+ useWhen: 'Teams want structured proposals and exported plans without automatic apply.',
20
+ behavior: 'Generates plans and proposal bundles, but no source changes are applied.',
21
+ deny: ['Bash(rm -rf *)', 'Bash(git reset --hard *)', 'Bash(git clean *)', 'Read(./.env*)'],
22
+ },
23
+ {
24
+ key: 'safe-write',
25
+ label: 'Safe-Write',
26
+ risk: 'medium',
27
+ defaultMode: 'acceptEdits',
28
+ useWhen: 'Starter repos or tightly scoped apply flows with visible rollback.',
29
+ behavior: 'Allows creation of missing Claude artifacts while preserving existing files.',
30
+ deny: ['Read(./.env*)', 'Read(./secrets/**)', 'Bash(rm -rf *)', 'Bash(git push --force *)'],
31
+ },
32
+ {
33
+ key: 'power-user',
34
+ label: 'Power-User',
35
+ risk: 'medium',
36
+ defaultMode: 'acceptEdits',
37
+ useWhen: 'Experienced maintainers who understand the repo and want faster iteration.',
38
+ behavior: 'Broader local automation with fewer prompts, still without bypass defaults.',
39
+ deny: ['Read(./.env*)', 'Bash(rm -rf *)'],
40
+ },
41
+ {
42
+ key: 'internal-research',
43
+ label: 'Internal-Research',
44
+ risk: 'high',
45
+ defaultMode: 'bypassPermissions',
46
+ useWhen: 'Internal experiments only, never as a product-facing default.',
47
+ behavior: 'Maximum autonomy for research workflows, suitable only with explicit human oversight.',
48
+ deny: [],
49
+ },
50
+ ];
51
+
52
+ const HOOK_REGISTRY = [
53
+ {
54
+ key: 'protect-secrets',
55
+ file: '.claude/hooks/protect-secrets.sh',
56
+ triggerPoint: 'PreToolUse',
57
+ matcher: 'Read|Write|Edit',
58
+ purpose: 'Blocks direct access to secret or credential files before a tool runs.',
59
+ filesTouched: [],
60
+ sideEffects: ['Stops the action and returns a block decision when a secret path is targeted.'],
61
+ risk: 'low',
62
+ dryRunExample: 'Attempt to read `.env` and confirm the hook blocks the request.',
63
+ rollbackPath: 'Remove the PreToolUse registration from settings.json.',
64
+ },
65
+ {
66
+ key: 'on-edit-lint',
67
+ file: '.claude/hooks/on-edit-lint.sh',
68
+ triggerPoint: 'PostToolUse',
69
+ matcher: 'Write|Edit',
70
+ purpose: 'Runs the repo linter or formatter after file edits when tooling is available.',
71
+ filesTouched: ['Working tree files targeted by eslint/ruff fixes'],
72
+ sideEffects: ['May auto-fix formatting or lint issues.', 'Can modify the same files that were just edited.'],
73
+ risk: 'medium',
74
+ dryRunExample: 'Edit a JS or Python file and inspect whether eslint or ruff would run.',
75
+ rollbackPath: 'Remove the PostToolUse hook entry or delete the script.',
76
+ },
77
+ {
78
+ key: 'log-changes',
79
+ file: '.claude/hooks/log-changes.sh',
80
+ triggerPoint: 'PostToolUse',
81
+ matcher: 'Write|Edit',
82
+ purpose: 'Appends a durable file-change log under `.claude/logs/` for later review.',
83
+ filesTouched: ['.claude/logs/file-changes.log'],
84
+ sideEffects: ['Creates the logs directory on first use.', 'Adds a timestamped audit line per file change.'],
85
+ risk: 'low',
86
+ dryRunExample: 'Edit one file and verify the log entry is appended.',
87
+ rollbackPath: 'Remove the PostToolUse hook entry and delete the log file if desired.',
88
+ },
89
+ ];
90
+
91
+ const POLICY_PACKS = [
92
+ {
93
+ key: 'baseline-engineering',
94
+ label: 'Baseline Engineering',
95
+ modules: ['CLAUDE.md baseline', 'commands', 'rules', 'safe-write profile'],
96
+ useWhen: 'General product teams that want a pragmatic default.',
97
+ },
98
+ {
99
+ key: 'security-sensitive',
100
+ label: 'Security-Sensitive',
101
+ modules: ['read-only profile', 'suggest-only mode', 'protect-secrets hook', 'approval checklist'],
102
+ useWhen: 'Auth, payments, customer data, or regulated surfaces.',
103
+ },
104
+ {
105
+ key: 'oss-friendly',
106
+ label: 'OSS-Friendly',
107
+ modules: ['suggest-only profile', 'minimal commands', 'light rules', 'manual merge expectations'],
108
+ useWhen: 'Open-source repos with many external contributors.',
109
+ },
110
+ {
111
+ key: 'regulated-lite',
112
+ label: 'Regulated-Lite',
113
+ modules: ['suggest-only or safe-write profile', 'activity artifacts', 'rollback manifests', 'benchmark evidence'],
114
+ useWhen: 'Teams that need auditable change paths before broader adoption.',
115
+ },
116
+ ];
117
+
118
+ const PILOT_ROLLOUT_KIT = {
119
+ recommendedScope: [
120
+ 'Pick 1-2 repos with active maintainers and low blast radius.',
121
+ 'Run discover and suggest-only first; avoid direct writes on mature repos.',
122
+ 'Choose one permission profile before any pilot starts.',
123
+ 'Define success metrics before the first benchmark run.',
124
+ ],
125
+ approvals: [
126
+ 'Engineering owner approves scope and rollback expectations.',
127
+ 'Security owner approves the selected permission profile and hooks.',
128
+ 'Pilot owner records the benchmark baseline and acceptance criteria.',
129
+ ],
130
+ successMetrics: [
131
+ 'Score delta and organic score delta',
132
+ 'Number of recommendations accepted',
133
+ 'Time to first useful Claude workflow',
134
+ 'Rollback-free apply rate',
135
+ ],
136
+ rollbackExpectations: [
137
+ 'Every apply batch must emit a rollback artifact.',
138
+ 'If a created artifact is rejected, delete the files listed in the rollback manifest.',
139
+ 'Record the rollback event in the activity log for auditability.',
140
+ ],
141
+ };
142
+
143
+ function clone(value) {
144
+ return JSON.parse(JSON.stringify(value));
145
+ }
146
+
147
+ function mergeUnique(existing = [], additions = []) {
148
+ return [...new Set([...(Array.isArray(existing) ? existing : []), ...additions])];
149
+ }
150
+
151
+ function mergeHooks(existingHooks = {}, nextHooks = {}) {
152
+ const merged = clone(existingHooks || {});
153
+
154
+ for (const [stage, blocks] of Object.entries(nextHooks)) {
155
+ const targetBlocks = Array.isArray(merged[stage]) ? clone(merged[stage]) : [];
156
+ for (const incoming of blocks) {
157
+ const index = targetBlocks.findIndex(block => block.matcher === incoming.matcher);
158
+ if (index === -1) {
159
+ targetBlocks.push(clone(incoming));
160
+ continue;
161
+ }
162
+
163
+ const current = targetBlocks[index];
164
+ const existingCommands = new Set((current.hooks || []).map(hook => `${hook.type}:${hook.command}:${hook.timeout || ''}`));
165
+ const mergedHooks = [...(current.hooks || [])];
166
+ for (const hook of incoming.hooks || []) {
167
+ const signature = `${hook.type}:${hook.command}:${hook.timeout || ''}`;
168
+ if (!existingCommands.has(signature)) {
169
+ mergedHooks.push(clone(hook));
170
+ existingCommands.add(signature);
171
+ }
172
+ }
173
+ targetBlocks[index] = { ...current, hooks: mergedHooks };
174
+ }
175
+ merged[stage] = targetBlocks;
176
+ }
177
+
178
+ return merged;
179
+ }
180
+
181
+ function getPermissionProfile(key = 'safe-write') {
182
+ return PERMISSION_PROFILES.find(profile => profile.key === key) ||
183
+ PERMISSION_PROFILES.find(profile => profile.key === 'safe-write');
184
+ }
185
+
186
+ function isWritableProfile(key = 'safe-write') {
187
+ return ['safe-write', 'power-user', 'internal-research'].includes(getPermissionProfile(key).key);
188
+ }
189
+
190
+ function ensureWritableProfile(key = 'safe-write', commandName = 'apply', dryRun = false) {
191
+ const profile = getPermissionProfile(key);
192
+ if (!dryRun && !isWritableProfile(profile.key)) {
193
+ throw new Error(`${commandName} requires a writable profile. Use --profile safe-write or --dry-run.`);
194
+ }
195
+ return profile;
196
+ }
197
+
198
+ function buildHookConfig(hookFiles, profileKey) {
199
+ const profile = getPermissionProfile(profileKey);
200
+ if (!isWritableProfile(profile.key)) {
201
+ return {};
202
+ }
203
+
204
+ const uniqueFiles = [...new Set(hookFiles)].sort();
205
+ if (uniqueFiles.length === 0) {
206
+ return {};
207
+ }
208
+
209
+ const hookConfig = {
210
+ PostToolUse: [{
211
+ matcher: 'Write|Edit',
212
+ hooks: uniqueFiles
213
+ .filter(file => file !== 'protect-secrets.sh' && file !== 'session-start.sh')
214
+ .map(file => ({
215
+ type: 'command',
216
+ command: `bash .claude/hooks/${file}`,
217
+ timeout: 10,
218
+ })),
219
+ }],
220
+ };
221
+
222
+ if (uniqueFiles.includes('protect-secrets.sh')) {
223
+ hookConfig.PreToolUse = [{
224
+ matcher: 'Read|Write|Edit',
225
+ hooks: [{
226
+ type: 'command',
227
+ command: 'bash .claude/hooks/protect-secrets.sh',
228
+ timeout: 5,
229
+ }],
230
+ }];
231
+ }
232
+
233
+ if (uniqueFiles.includes('session-start.sh')) {
234
+ hookConfig.SessionStart = [{
235
+ matcher: '*',
236
+ hooks: [{
237
+ type: 'command',
238
+ command: 'bash .claude/hooks/session-start.sh',
239
+ timeout: 5,
240
+ }],
241
+ }];
242
+ }
243
+
244
+ if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
245
+ delete hookConfig.PostToolUse;
246
+ }
247
+
248
+ return hookConfig;
249
+ }
250
+
251
+ function buildSettingsForProfile({ profileKey = 'safe-write', hookFiles = [], existingSettings = null, mcpPackKeys = [] } = {}) {
252
+ const profile = getPermissionProfile(profileKey);
253
+ const base = existingSettings ? clone(existingSettings) : {};
254
+ const selectedMcpPacks = normalizeMcpPackKeys(mcpPackKeys);
255
+ base.permissions = base.permissions || {};
256
+ base.permissions.defaultMode = profile.defaultMode;
257
+ base.permissions.deny = mergeUnique(base.permissions.deny, profile.deny);
258
+
259
+ const hookConfig = buildHookConfig(hookFiles, profile.key);
260
+ if (Object.keys(hookConfig).length > 0) {
261
+ base.hooks = mergeHooks(base.hooks, hookConfig);
262
+ }
263
+
264
+ if (selectedMcpPacks.length > 0) {
265
+ base.mcpServers = mergeMcpServers(base.mcpServers, selectedMcpPacks);
266
+ }
267
+
268
+ base.claudexSetup = {
269
+ ...(base.claudexSetup || {}),
270
+ profile: profile.key,
271
+ mcpPacks: selectedMcpPacks,
272
+ };
273
+
274
+ return base;
275
+ }
276
+
277
+ function getGovernanceSummary() {
278
+ return {
279
+ permissionProfiles: PERMISSION_PROFILES,
280
+ hookRegistry: HOOK_REGISTRY,
281
+ policyPacks: POLICY_PACKS,
282
+ domainPacks: DOMAIN_PACKS,
283
+ mcpPacks: MCP_PACKS,
284
+ pilotRolloutKit: PILOT_ROLLOUT_KIT,
285
+ };
286
+ }
287
+
288
+ function printGovernanceSummary(summary, options = {}) {
289
+ if (options.json) {
290
+ console.log(JSON.stringify(summary, null, 2));
291
+ return;
292
+ }
293
+
294
+ console.log('');
295
+ console.log(' claudex-setup governance');
296
+ console.log(' ═══════════════════════════════════════');
297
+ console.log(' Safe defaults, hook transparency, and pilot guidance.');
298
+ console.log('');
299
+
300
+ console.log(' Permission Profiles');
301
+ for (const profile of summary.permissionProfiles) {
302
+ console.log(` - ${profile.label} [${profile.risk}]`);
303
+ console.log(` ${profile.useWhen}`);
304
+ console.log(` defaultMode=${profile.defaultMode}`);
305
+ }
306
+ console.log('');
307
+
308
+ console.log(' Hook Registry');
309
+ for (const hook of summary.hookRegistry) {
310
+ console.log(` - ${hook.file}`);
311
+ console.log(` ${hook.triggerPoint} ${hook.matcher} -> ${hook.purpose}`);
312
+ }
313
+ console.log('');
314
+
315
+ console.log(' Policy Packs');
316
+ for (const pack of summary.policyPacks) {
317
+ console.log(` - ${pack.label}: ${pack.modules.join(', ')}`);
318
+ }
319
+ console.log('');
320
+
321
+ console.log(' Domain Packs');
322
+ for (const pack of summary.domainPacks) {
323
+ console.log(` - ${pack.label}: ${pack.useWhen}`);
324
+ }
325
+ console.log('');
326
+
327
+ console.log(' MCP Packs');
328
+ for (const pack of summary.mcpPacks) {
329
+ console.log(` - ${pack.label}: ${Object.keys(pack.servers).join(', ')}`);
330
+ }
331
+ console.log('');
332
+
333
+ console.log(' Pilot Rollout Kit');
334
+ for (const item of summary.pilotRolloutKit.recommendedScope) {
335
+ console.log(` - ${item}`);
336
+ }
337
+ console.log('');
338
+ }
339
+
340
+ module.exports = {
341
+ PERMISSION_PROFILES,
342
+ getPermissionProfile,
343
+ isWritableProfile,
344
+ ensureWritableProfile,
345
+ buildSettingsForProfile,
346
+ getGovernanceSummary,
347
+ printGovernanceSummary,
348
+ };
package/src/index.js CHANGED
@@ -1,4 +1,24 @@
1
1
  const { audit } = require('./audit');
2
2
  const { setup } = require('./setup');
3
+ const { analyzeProject } = require('./analyze');
4
+ const { buildProposalBundle, applyProposalBundle } = require('./plans');
5
+ const { getGovernanceSummary } = require('./governance');
6
+ const { runBenchmark } = require('./benchmark');
7
+ const { DOMAIN_PACKS, detectDomainPacks } = require('./domain-packs');
8
+ const { MCP_PACKS, getMcpPack, mergeMcpServers, recommendMcpPacks } = require('./mcp-packs');
3
9
 
4
- module.exports = { audit, setup };
10
+ module.exports = {
11
+ audit,
12
+ setup,
13
+ analyzeProject,
14
+ buildProposalBundle,
15
+ applyProposalBundle,
16
+ getGovernanceSummary,
17
+ runBenchmark,
18
+ DOMAIN_PACKS,
19
+ detectDomainPacks,
20
+ MCP_PACKS,
21
+ getMcpPack,
22
+ mergeMcpServers,
23
+ recommendMcpPacks,
24
+ };
@@ -41,8 +41,8 @@ async function interactive(options) {
41
41
  for (const [key, technique] of Object.entries(TECHNIQUES)) {
42
42
  results.push({ key, ...technique, passed: technique.check(ctx) });
43
43
  }
44
- const failed = results.filter(r => !r.passed);
45
- const passed = results.filter(r => r.passed);
44
+ const failed = results.filter(r => r.passed === false);
45
+ const passed = results.filter(r => r.passed === true);
46
46
 
47
47
  console.log(` ${c(`${passed.length}/${results.length}`, 'bold')} checks already passing.`);
48
48
  console.log(` ${c(String(failed.length), 'yellow')} improvements available.`);
@@ -0,0 +1,85 @@
1
+ const MCP_PACKS = [
2
+ {
3
+ key: 'context7-docs',
4
+ label: 'Context7 Docs',
5
+ useWhen: 'Repos that benefit from live, current framework and library documentation during Claude sessions.',
6
+ adoption: 'Safe default docs pack for most application repos.',
7
+ servers: {
8
+ context7: {
9
+ command: 'npx',
10
+ args: ['-y', '@upstash/context7-mcp@latest'],
11
+ },
12
+ },
13
+ },
14
+ {
15
+ key: 'next-devtools',
16
+ label: 'Next.js Devtools',
17
+ useWhen: 'Next.js repos that need runtime-aware debugging and framework-specific tooling.',
18
+ adoption: 'Useful companion pack for frontend-ui repos running Next.js.',
19
+ servers: {
20
+ 'next-devtools': {
21
+ command: 'npx',
22
+ args: ['-y', 'next-devtools-mcp@latest'],
23
+ },
24
+ },
25
+ },
26
+ ];
27
+
28
+ function clone(value) {
29
+ return JSON.parse(JSON.stringify(value));
30
+ }
31
+
32
+ function getMcpPack(key) {
33
+ return MCP_PACKS.find(pack => pack.key === key) || null;
34
+ }
35
+
36
+ function normalizeMcpPackKeys(keys = []) {
37
+ return [...new Set((Array.isArray(keys) ? keys : [])
38
+ .map(key => `${key}`.trim())
39
+ .filter(Boolean))]
40
+ .filter(key => !!getMcpPack(key));
41
+ }
42
+
43
+ function mergeMcpServers(existing = {}, packKeys = []) {
44
+ const merged = clone(existing || {});
45
+ for (const key of normalizeMcpPackKeys(packKeys)) {
46
+ const pack = getMcpPack(key);
47
+ if (!pack) continue;
48
+ for (const [serverName, serverConfig] of Object.entries(pack.servers || {})) {
49
+ if (!merged[serverName]) {
50
+ merged[serverName] = clone(serverConfig);
51
+ }
52
+ }
53
+ }
54
+ return merged;
55
+ }
56
+
57
+ function recommendMcpPacks(stacks = [], domainPacks = []) {
58
+ const recommended = new Set();
59
+ const stackKeys = new Set(stacks.map(stack => stack.key));
60
+
61
+ for (const pack of domainPacks) {
62
+ for (const key of pack.recommendedMcpPacks || []) {
63
+ recommended.add(key);
64
+ }
65
+ }
66
+
67
+ if (stackKeys.has('nextjs')) {
68
+ recommended.add('next-devtools');
69
+ }
70
+ if (stackKeys.size > 0) {
71
+ recommended.add('context7-docs');
72
+ }
73
+
74
+ return MCP_PACKS
75
+ .filter(pack => recommended.has(pack.key))
76
+ .map(pack => clone(pack));
77
+ }
78
+
79
+ module.exports = {
80
+ MCP_PACKS,
81
+ getMcpPack,
82
+ normalizeMcpPackKeys,
83
+ mergeMcpServers,
84
+ recommendMcpPacks,
85
+ };