@nerviq/cli 1.8.3 → 1.8.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nerviq/cli",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "The intelligent nervous system for AI coding agents — 2,431 checks across 8 platforms, 10 languages, and 62 domain packs. Audit, align, and amplify.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,101 @@
1
+ const { loadPatterns } = require('./usage-patterns');
2
+ const { readSnapshotIndex } = require('./activity');
3
+
4
+ const MIN_EVENTS = 2;
5
+ const SUPPRESS_THRESHOLD = 3;
6
+ const RECENT_AUDITS = 10;
7
+
8
+ /**
9
+ * Analyze usage patterns and audit history to suggest rules.
10
+ */
11
+ function analyzeSuggestions(dir) {
12
+ const patterns = loadPatterns(dir);
13
+ const snapshots = readSnapshotIndex(dir);
14
+
15
+ const keys = Object.keys(patterns);
16
+ const totalEvents = keys.reduce((sum, k) => {
17
+ const e = patterns[k];
18
+ return sum + e.accepted + e.rejected + e.skipped;
19
+ }, 0);
20
+
21
+ // Checks always accepted -> suggest as required
22
+ const suggestedRules = keys
23
+ .filter(k => {
24
+ const e = patterns[k];
25
+ return e.accepted >= MIN_EVENTS && e.rejected === 0;
26
+ })
27
+ .map(k => ({ key: k, accepted: patterns[k].accepted, total: patterns[k].accepted }));
28
+
29
+ // Checks always rejected -> suggest suppressing
30
+ const suggestedSuppressions = keys
31
+ .filter(k => {
32
+ const e = patterns[k];
33
+ return e.rejected >= SUPPRESS_THRESHOLD && e.accepted === 0;
34
+ })
35
+ .map(k => ({ key: k, rejected: patterns[k].rejected, total: patterns[k].rejected }));
36
+
37
+ // From audit snapshots: checks that repeatedly appear in topActionKeys (failing)
38
+ const auditSnapshots = snapshots
39
+ .filter(s => s.snapshotKind === 'audit' && s.summary && Array.isArray(s.summary.topActionKeys))
40
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
41
+ .slice(0, RECENT_AUDITS);
42
+
43
+ const failCounts = {};
44
+ for (const snap of auditSnapshots) {
45
+ for (const key of snap.summary.topActionKeys) {
46
+ failCounts[key] = (failCounts[key] || 0) + 1;
47
+ }
48
+ }
49
+
50
+ const suggestedPriorities = Object.entries(failCounts)
51
+ .filter(([, count]) => count >= 2)
52
+ .sort((a, b) => b[1] - a[1])
53
+ .map(([key, count]) => ({ key, failCount: count, auditCount: auditSnapshots.length }));
54
+
55
+ return { totalEvents, auditCount: auditSnapshots.length, suggestedRules, suggestedSuppressions, suggestedPriorities };
56
+ }
57
+
58
+ /**
59
+ * Format suggestions for CLI output.
60
+ */
61
+ function formatSuggestions(suggestions) {
62
+ const { totalEvents, auditCount, suggestedRules, suggestedSuppressions, suggestedPriorities } = suggestions;
63
+
64
+ if (totalEvents === 0 && auditCount === 0) {
65
+ return ' No usage data yet. Run nerviq fix or nerviq audit to build pattern history.';
66
+ }
67
+
68
+ const sources = [];
69
+ if (totalEvents > 0) sources.push(`${totalEvents} pattern events`);
70
+ if (auditCount > 0) sources.push(`${auditCount} audit snapshots`);
71
+ const lines = [` Auto-Suggested Rules (based on ${sources.join(', ')}):`];
72
+
73
+ if (suggestedRules.length > 0) {
74
+ lines.push('', ' Suggested as required (always accepted):');
75
+ for (const r of suggestedRules) {
76
+ lines.push(` + ${r.key.padEnd(20)} — accepted ${r.accepted}/${r.total} times, consider making mandatory`);
77
+ }
78
+ }
79
+
80
+ if (suggestedSuppressions.length > 0) {
81
+ lines.push('', ' Suggested to suppress (always rejected):');
82
+ for (const s of suggestedSuppressions) {
83
+ lines.push(` - ${s.key.padEnd(20)} — rejected ${s.rejected}/${s.total} times, may not fit your workflow`);
84
+ }
85
+ }
86
+
87
+ if (suggestedPriorities.length > 0) {
88
+ lines.push('', ' Priority focus (failing repeatedly):');
89
+ for (const p of suggestedPriorities) {
90
+ lines.push(` ! ${p.key.padEnd(20)} — failed in ${p.failCount}/${p.auditCount} recent audits`);
91
+ }
92
+ }
93
+
94
+ if (suggestedRules.length === 0 && suggestedSuppressions.length === 0 && suggestedPriorities.length === 0) {
95
+ lines.push('', ' No strong patterns detected yet. Keep using nerviq fix and audit to build history.');
96
+ }
97
+
98
+ return lines.join('\n');
99
+ }
100
+
101
+ module.exports = { analyzeSuggestions, formatSuggestions };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Team profiles — save and share org-specific check weights and preferences.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const PROFILES_DIR = 'profiles';
9
+
10
+ function profilesDir(dir) {
11
+ return path.join(dir, '.nerviq', PROFILES_DIR);
12
+ }
13
+
14
+ function profilePath(dir, name) {
15
+ return path.join(profilesDir(dir), `${name}.json`);
16
+ }
17
+
18
+ function validateProfileName(name) {
19
+ if (!name || typeof name !== 'string') throw new Error('Profile name is required.');
20
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
21
+ throw new Error(`Invalid profile name '${name}'. Use only letters, numbers, hyphens, and underscores.`);
22
+ }
23
+ if (name.length > 64) throw new Error('Profile name must be 64 characters or fewer.');
24
+ }
25
+
26
+ function saveProfile(dir, profileName, options = {}) {
27
+ validateProfileName(profileName);
28
+ const profileDir = profilesDir(dir);
29
+ fs.mkdirSync(profileDir, { recursive: true });
30
+
31
+ const profile = {
32
+ name: profileName,
33
+ created: new Date().toISOString().split('T')[0],
34
+ platforms: options.platforms || ['claude'],
35
+ threshold: options.threshold != null ? Number(options.threshold) : 70,
36
+ suppressedChecks: options.suppressedChecks || [],
37
+ priorityBoosts: options.priorityBoosts || [],
38
+ customWeights: options.customWeights || {},
39
+ description: options.description || '',
40
+ };
41
+
42
+ const filePath = profilePath(dir, profileName);
43
+ fs.writeFileSync(filePath, JSON.stringify(profile, null, 2), 'utf8');
44
+ return { saved: true, path: filePath, profile };
45
+ }
46
+
47
+ function loadProfile(dir, profileName) {
48
+ validateProfileName(profileName);
49
+ const filePath = profilePath(dir, profileName);
50
+ if (!fs.existsSync(filePath)) {
51
+ throw new Error(`Profile '${profileName}' not found. Run 'nerviq profile list' to see available profiles.`);
52
+ }
53
+ const raw = fs.readFileSync(filePath, 'utf8');
54
+ let profile;
55
+ try {
56
+ profile = JSON.parse(raw);
57
+ } catch {
58
+ throw new Error(`Profile '${profileName}' contains invalid JSON.`);
59
+ }
60
+ return profile;
61
+ }
62
+
63
+ function listProfiles(dir) {
64
+ const profDir = profilesDir(dir);
65
+ if (!fs.existsSync(profDir)) return [];
66
+ return fs.readdirSync(profDir)
67
+ .filter(f => f.endsWith('.json'))
68
+ .map(f => {
69
+ const name = f.replace(/\.json$/, '');
70
+ try {
71
+ const data = JSON.parse(fs.readFileSync(path.join(profDir, f), 'utf8'));
72
+ return { name, description: data.description || '', platforms: data.platforms || [], threshold: data.threshold };
73
+ } catch {
74
+ return { name, description: '(invalid)', platforms: [], threshold: null };
75
+ }
76
+ });
77
+ }
78
+
79
+ function exportProfile(dir, profileName) {
80
+ const profile = loadProfile(dir, profileName);
81
+ return JSON.stringify(profile, null, 2);
82
+ }
83
+
84
+ function applyProfileToOptions(profile, options) {
85
+ const merged = { ...options };
86
+ if (profile.threshold != null && merged.threshold == null) {
87
+ merged.threshold = profile.threshold;
88
+ }
89
+ if (profile.platforms && profile.platforms.length > 0 && !options.platform) {
90
+ merged.platform = profile.platforms[0];
91
+ }
92
+ merged.suppressedChecks = profile.suppressedChecks || [];
93
+ merged.priorityBoosts = profile.priorityBoosts || [];
94
+ merged.customWeights = profile.customWeights || {};
95
+ return merged;
96
+ }
97
+
98
+ function formatProfileList(profiles) {
99
+ if (profiles.length === 0) return ' No profiles found. Create one with: nerviq profile save <name>';
100
+ const lines = profiles.map(p => {
101
+ const desc = p.description ? ` — ${p.description}` : '';
102
+ const plats = p.platforms.length > 0 ? ` [${p.platforms.join(', ')}]` : '';
103
+ return ` ${p.name}${plats}${desc}`;
104
+ });
105
+ return lines.join('\n');
106
+ }
107
+
108
+ function formatProfile(profile) {
109
+ const lines = [
110
+ ` Name: ${profile.name}`,
111
+ ` Created: ${profile.created || 'unknown'}`,
112
+ ` Platforms: ${(profile.platforms || []).join(', ') || 'any'}`,
113
+ ` Threshold: ${profile.threshold != null ? profile.threshold : 'default'}`,
114
+ ` Suppressed: ${(profile.suppressedChecks || []).join(', ') || 'none'}`,
115
+ ` Boosted: ${(profile.priorityBoosts || []).join(', ') || 'none'}`,
116
+ ` Description: ${profile.description || '(none)'}`,
117
+ ];
118
+ return lines.join('\n');
119
+ }
120
+
121
+ module.exports = { saveProfile, loadProfile, listProfiles, exportProfile, applyProfileToOptions, formatProfileList, formatProfile, validateProfileName };