@nerviq/cli 0.9.3 → 0.9.5
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/bin/cli.js +103 -4
- package/package.json +6 -3
- package/src/aider/techniques.js +3 -0
- package/src/audit.js +3 -2
- package/src/catalog.js +87 -0
- package/src/codex/techniques.js +3 -0
- package/src/convert.js +336 -0
- package/src/copilot/techniques.js +3 -0
- package/src/cursor/techniques.js +3 -0
- package/src/doctor.js +253 -0
- package/src/feedback.js +173 -0
- package/src/freshness.js +177 -0
- package/src/gemini/techniques.js +3 -0
- package/src/mcp-server.js +373 -0
- package/src/migrate.js +354 -0
- package/src/opencode/techniques.js +3 -0
- package/src/source-urls.js +260 -0
- package/src/techniques.js +3 -0
- package/src/windsurf/techniques.js +3 -0
package/src/feedback.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
let lastTimestamp = '';
|
|
6
|
+
let counter = 0;
|
|
7
|
+
|
|
8
|
+
function timestampId() {
|
|
9
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
10
|
+
if (ts === lastTimestamp) {
|
|
11
|
+
counter += 1;
|
|
12
|
+
return `${ts}-${counter}`;
|
|
13
|
+
}
|
|
14
|
+
lastTimestamp = ts;
|
|
15
|
+
counter = 0;
|
|
16
|
+
return ts;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureFeedbackDir(dir) {
|
|
20
|
+
const feedbackDir = path.join(dir, '.claude', 'claudex-setup', 'feedback');
|
|
21
|
+
fs.mkdirSync(feedbackDir, { recursive: true });
|
|
22
|
+
return feedbackDir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeJson(filePath, payload) {
|
|
26
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
27
|
+
fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveFeedback(dir, payload) {
|
|
31
|
+
const feedbackDir = ensureFeedbackDir(dir);
|
|
32
|
+
const id = timestampId();
|
|
33
|
+
const keySlug = String(payload.key || 'finding').replace(/[^a-z0-9_-]+/gi, '-').toLowerCase();
|
|
34
|
+
const filePath = path.join(feedbackDir, `${id}-${keySlug}.json`);
|
|
35
|
+
const envelope = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
id,
|
|
38
|
+
createdAt: new Date().toISOString(),
|
|
39
|
+
...payload,
|
|
40
|
+
};
|
|
41
|
+
writeJson(filePath, envelope);
|
|
42
|
+
return {
|
|
43
|
+
...envelope,
|
|
44
|
+
filePath,
|
|
45
|
+
relativePath: path.relative(dir, filePath),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getFeedbackSummary(dir) {
|
|
50
|
+
const feedbackDir = ensureFeedbackDir(dir);
|
|
51
|
+
const files = fs.readdirSync(feedbackDir).filter((name) => name.endsWith('.json'));
|
|
52
|
+
const entries = [];
|
|
53
|
+
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const filePath = path.join(feedbackDir, file);
|
|
56
|
+
try {
|
|
57
|
+
entries.push(JSON.parse(fs.readFileSync(filePath, 'utf8')));
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore malformed artifacts so one bad file does not break the summary.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const summary = {
|
|
64
|
+
totalEntries: entries.length,
|
|
65
|
+
helpful: 0,
|
|
66
|
+
unhelpful: 0,
|
|
67
|
+
byKey: {},
|
|
68
|
+
relativeDir: path.relative(dir, feedbackDir),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const helpful = entry.helpful === true;
|
|
73
|
+
if (helpful) {
|
|
74
|
+
summary.helpful += 1;
|
|
75
|
+
} else if (entry.helpful === false) {
|
|
76
|
+
summary.unhelpful += 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const bucket = summary.byKey[entry.key] || { total: 0, helpful: 0, unhelpful: 0 };
|
|
80
|
+
bucket.total += 1;
|
|
81
|
+
if (helpful) {
|
|
82
|
+
bucket.helpful += 1;
|
|
83
|
+
} else if (entry.helpful === false) {
|
|
84
|
+
bucket.unhelpful += 1;
|
|
85
|
+
}
|
|
86
|
+
summary.byKey[entry.key] = bucket;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return summary;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function askQuestion(rl, prompt) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
rl.question(prompt, resolve);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function collectFeedback(dir, options = {}) {
|
|
99
|
+
const findings = Array.isArray(options.findings) ? options.findings : [];
|
|
100
|
+
const stdin = options.stdin || process.stdin;
|
|
101
|
+
const stdout = options.stdout || process.stdout;
|
|
102
|
+
|
|
103
|
+
if (findings.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
saved: 0,
|
|
106
|
+
skipped: 0,
|
|
107
|
+
helpful: 0,
|
|
108
|
+
unhelpful: 0,
|
|
109
|
+
entries: [],
|
|
110
|
+
relativeDir: path.relative(dir, ensureFeedbackDir(dir)),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!(stdin.isTTY && stdout.isTTY)) {
|
|
115
|
+
return {
|
|
116
|
+
mode: 'skipped-noninteractive',
|
|
117
|
+
saved: 0,
|
|
118
|
+
skipped: findings.length,
|
|
119
|
+
helpful: 0,
|
|
120
|
+
unhelpful: 0,
|
|
121
|
+
entries: [],
|
|
122
|
+
relativeDir: path.relative(dir, ensureFeedbackDir(dir)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
127
|
+
const entries = [];
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
for (const finding of findings) {
|
|
131
|
+
stdout.write(`\n Feedback for ${finding.name} (${finding.key})\n`);
|
|
132
|
+
let answer = await askQuestion(rl, ' Was this helpful? (y/n) ');
|
|
133
|
+
answer = String(answer || '').trim().toLowerCase();
|
|
134
|
+
|
|
135
|
+
if (!['y', 'yes', 'n', 'no'].includes(answer)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
entries.push(saveFeedback(dir, {
|
|
140
|
+
key: finding.key,
|
|
141
|
+
name: finding.name,
|
|
142
|
+
helpful: answer === 'y' || answer === 'yes',
|
|
143
|
+
platform: options.platform || null,
|
|
144
|
+
sourceCommand: options.sourceCommand || 'audit',
|
|
145
|
+
sourceUrl: finding.sourceUrl || null,
|
|
146
|
+
impact: finding.impact || null,
|
|
147
|
+
category: finding.category || null,
|
|
148
|
+
score: Number.isFinite(options.score) ? options.score : null,
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
rl.close();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const helpful = entries.filter((entry) => entry.helpful).length;
|
|
156
|
+
const unhelpful = entries.filter((entry) => entry.helpful === false).length;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
saved: entries.length,
|
|
160
|
+
skipped: findings.length - entries.length,
|
|
161
|
+
helpful,
|
|
162
|
+
unhelpful,
|
|
163
|
+
entries,
|
|
164
|
+
relativeDir: path.relative(dir, ensureFeedbackDir(dir)),
|
|
165
|
+
summary: getFeedbackSummary(dir),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
collectFeedback,
|
|
171
|
+
saveFeedback,
|
|
172
|
+
getFeedbackSummary,
|
|
173
|
+
};
|
package/src/freshness.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Freshness Operationalization
|
|
3
|
+
*
|
|
4
|
+
* Release gates, recurring probes, propagation checklists,
|
|
5
|
+
* and staleness blocking for Claude Code surfaces.
|
|
6
|
+
*
|
|
7
|
+
* P0 sources from docs.anthropic.com, propagation for CLAUDE.md format changes.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { version } = require('../package.json');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* P0 sources that must be fresh before any Claude Code release claim.
|
|
14
|
+
*/
|
|
15
|
+
const P0_SOURCES = [
|
|
16
|
+
{
|
|
17
|
+
key: 'claude-code-docs',
|
|
18
|
+
label: 'Claude Code Official Docs',
|
|
19
|
+
url: 'https://docs.anthropic.com/claude-code',
|
|
20
|
+
stalenessThresholdDays: 30,
|
|
21
|
+
verifiedAt: null,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: 'claude-md-format',
|
|
25
|
+
label: 'CLAUDE.md Format Documentation',
|
|
26
|
+
url: 'https://docs.anthropic.com/claude-code/claude-md',
|
|
27
|
+
stalenessThresholdDays: 30,
|
|
28
|
+
verifiedAt: null,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
key: 'claude-mcp-docs',
|
|
32
|
+
label: 'Claude Code MCP Documentation',
|
|
33
|
+
url: 'https://docs.anthropic.com/claude-code/mcp',
|
|
34
|
+
stalenessThresholdDays: 30,
|
|
35
|
+
verifiedAt: null,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'claude-hooks-docs',
|
|
39
|
+
label: 'Claude Code Hooks Documentation',
|
|
40
|
+
url: 'https://docs.anthropic.com/claude-code/hooks',
|
|
41
|
+
stalenessThresholdDays: 14,
|
|
42
|
+
verifiedAt: null,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: 'claude-security-docs',
|
|
46
|
+
label: 'Claude Code Security Documentation',
|
|
47
|
+
url: 'https://docs.anthropic.com/claude-code/security',
|
|
48
|
+
stalenessThresholdDays: 30,
|
|
49
|
+
verifiedAt: null,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'claude-permissions-docs',
|
|
53
|
+
label: 'Claude Code Permissions Documentation',
|
|
54
|
+
url: 'https://docs.anthropic.com/claude-code/permissions',
|
|
55
|
+
stalenessThresholdDays: 14,
|
|
56
|
+
verifiedAt: null,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'claude-settings-docs',
|
|
60
|
+
label: 'Claude Code Settings Documentation',
|
|
61
|
+
url: 'https://docs.anthropic.com/claude-code/settings',
|
|
62
|
+
stalenessThresholdDays: 30,
|
|
63
|
+
verifiedAt: null,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'anthropic-changelog',
|
|
67
|
+
label: 'Anthropic Changelog',
|
|
68
|
+
url: 'https://docs.anthropic.com/changelog',
|
|
69
|
+
stalenessThresholdDays: 14,
|
|
70
|
+
verifiedAt: null,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Propagation checklist: when a Claude Code source changes, these must update.
|
|
76
|
+
*/
|
|
77
|
+
const PROPAGATION_CHECKLIST = [
|
|
78
|
+
{
|
|
79
|
+
trigger: 'CLAUDE.md format change (new fields, import syntax, hierarchy change)',
|
|
80
|
+
targets: [
|
|
81
|
+
'src/context.js — update ProjectContext parsing',
|
|
82
|
+
'src/techniques.js — update memory/context checks',
|
|
83
|
+
'src/setup.js — update CLAUDE.md template generation',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
trigger: 'Hooks system change (event types, exit codes, schema)',
|
|
88
|
+
targets: [
|
|
89
|
+
'src/governance.js — update hookRegistry',
|
|
90
|
+
'src/techniques.js — update hooks checks',
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
trigger: 'MCP configuration format change',
|
|
95
|
+
targets: [
|
|
96
|
+
'src/techniques.js — update MCP checks',
|
|
97
|
+
'src/mcp-packs.js — update pack projections',
|
|
98
|
+
'src/context.js — update mcpConfig parsing',
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
trigger: 'Permissions model change (allow/deny lists, operator/user split)',
|
|
103
|
+
targets: [
|
|
104
|
+
'src/governance.js — update permissionProfiles',
|
|
105
|
+
'src/techniques.js — update permission checks',
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Release gate: check if all P0 sources are within staleness threshold.
|
|
112
|
+
*/
|
|
113
|
+
function checkReleaseGate(sourceVerifications = {}) {
|
|
114
|
+
const now = new Date();
|
|
115
|
+
const results = P0_SOURCES.map(source => {
|
|
116
|
+
const verifiedAt = sourceVerifications[source.key]
|
|
117
|
+
? new Date(sourceVerifications[source.key])
|
|
118
|
+
: source.verifiedAt ? new Date(source.verifiedAt) : null;
|
|
119
|
+
|
|
120
|
+
if (!verifiedAt) {
|
|
121
|
+
return { ...source, status: 'unverified', daysStale: null };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const daysSince = Math.floor((now - verifiedAt) / (1000 * 60 * 60 * 24));
|
|
125
|
+
const isStale = daysSince > source.stalenessThresholdDays;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...source,
|
|
129
|
+
verifiedAt: verifiedAt.toISOString(),
|
|
130
|
+
daysStale: daysSince,
|
|
131
|
+
status: isStale ? 'stale' : 'fresh',
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
ready: results.every(r => r.status === 'fresh'),
|
|
137
|
+
stale: results.filter(r => r.status === 'stale' || r.status === 'unverified'),
|
|
138
|
+
fresh: results.filter(r => r.status === 'fresh'),
|
|
139
|
+
results,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatReleaseGate(gateResult) {
|
|
144
|
+
const lines = [
|
|
145
|
+
`Claude Code Freshness Gate (nerviq v${version})`,
|
|
146
|
+
'═══════════════════════════════════════',
|
|
147
|
+
'',
|
|
148
|
+
`Status: ${gateResult.ready ? 'READY' : 'BLOCKED'}`,
|
|
149
|
+
`Fresh: ${gateResult.fresh.length}/${gateResult.results.length}`,
|
|
150
|
+
'',
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const result of gateResult.results) {
|
|
154
|
+
const icon = result.status === 'fresh' ? '✓' : result.status === 'stale' ? '✗' : '?';
|
|
155
|
+
const age = result.daysStale !== null ? ` (${result.daysStale}d ago)` : ' (unverified)';
|
|
156
|
+
lines.push(` ${icon} ${result.label}${age} — threshold: ${result.stalenessThresholdDays}d`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!gateResult.ready) {
|
|
160
|
+
lines.push('', 'Action required: verify stale/unverified sources before claiming release freshness.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getPropagationTargets(triggerKeyword) {
|
|
167
|
+
const keyword = triggerKeyword.toLowerCase();
|
|
168
|
+
return PROPAGATION_CHECKLIST.filter(item => item.trigger.toLowerCase().includes(keyword));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
P0_SOURCES,
|
|
173
|
+
PROPAGATION_CHECKLIST,
|
|
174
|
+
checkReleaseGate,
|
|
175
|
+
formatReleaseGate,
|
|
176
|
+
getPropagationTargets,
|
|
177
|
+
};
|
package/src/gemini/techniques.js
CHANGED
|
@@ -14,6 +14,7 @@ const os = require('os');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { GeminiProjectContext } = require('./context');
|
|
16
16
|
const { EMBEDDED_SECRET_PATTERNS, containsEmbeddedSecret } = require('../secret-patterns');
|
|
17
|
+
const { attachSourceUrls } = require('../source-urls');
|
|
17
18
|
|
|
18
19
|
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -2230,6 +2231,8 @@ const GEMINI_TECHNIQUES = {
|
|
|
2230
2231
|
},
|
|
2231
2232
|
};
|
|
2232
2233
|
|
|
2234
|
+
attachSourceUrls('gemini', GEMINI_TECHNIQUES);
|
|
2235
|
+
|
|
2233
2236
|
module.exports = {
|
|
2234
2237
|
GEMINI_TECHNIQUES,
|
|
2235
2238
|
};
|