@nerviq/cli 1.19.0 → 1.20.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.
- package/README.md +1 -1
- package/bin/cli.js +1 -0
- package/package.json +2 -1
- package/src/gemini/context.js +74 -5
- package/src/gemini/techniques.js +91 -19
package/README.md
CHANGED
package/bin/cli.js
CHANGED
|
@@ -2536,6 +2536,7 @@ async function main() {
|
|
|
2536
2536
|
}
|
|
2537
2537
|
} catch (err) {
|
|
2538
2538
|
console.error(`\n Error: ${err.message}`);
|
|
2539
|
+
if (process.env.NERVIQ_DEBUG) console.error(err.stack);
|
|
2539
2540
|
console.error(' Fix: Run `npx nerviq doctor` to diagnose common issues, or check https://github.com/nerviq/nerviq#troubleshooting');
|
|
2540
2541
|
process.exit(2);
|
|
2541
2542
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nerviq/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.0",
|
|
4
4
|
"description": "The intelligent nervous system for AI coding agents — 2,441 checks (8 platforms × ~300 governance rules), 10 languages, 62 domain packs. Audit, align, and amplify.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"build": "npm pack --dry-run",
|
|
19
19
|
"test": "node test/run.js",
|
|
20
20
|
"verify:release-metadata": "node tools/validate-release-metadata.js",
|
|
21
|
+
"prepublish:check": "node tools/pre-publish.js",
|
|
21
22
|
"test:jest": "jest",
|
|
22
23
|
"test:coverage": "jest --coverage",
|
|
23
24
|
"test:all": "npm test && npx jest && node test/check-matrix.js && node test/codex-check-matrix.js && node test/gemini-check-matrix.js && node test/copilot-check-matrix.js && node test/cursor-check-matrix.js && node test/windsurf-check-matrix.js && node test/aider-check-matrix.js && node test/opencode-check-matrix.js && node test/golden-matrix.js && node test/codex-golden-matrix.js && node test/gemini-golden-matrix.js && node test/copilot-golden-matrix.js && node test/cursor-golden-matrix.js && node test/windsurf-golden-matrix.js && node test/aider-golden-matrix.js && node test/opencode-golden-matrix.js",
|
package/src/gemini/context.js
CHANGED
|
@@ -54,18 +54,87 @@ class GeminiProjectContext extends ProjectContext {
|
|
|
54
54
|
|
|
55
55
|
geminiMdContent() {
|
|
56
56
|
const direct = this.fileContent('GEMINI.md');
|
|
57
|
-
if (direct) return direct;
|
|
57
|
+
if (direct) return this._expandGeminiMdImports(direct);
|
|
58
58
|
|
|
59
|
-
// Fallback: use context.fileName from settings if configured
|
|
59
|
+
// Fallback: use context.fileName from settings if configured.
|
|
60
|
+
// Per Gemini CLI spec, context.fileName may be a string or an array of strings.
|
|
60
61
|
const contextFileName = this.configValue('context.fileName');
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const candidates = Array.isArray(contextFileName)
|
|
63
|
+
? contextFileName.filter(n => typeof n === 'string' && n.length > 0)
|
|
64
|
+
: (typeof contextFileName === 'string' && contextFileName ? [contextFileName] : []);
|
|
65
|
+
for (const name of candidates) {
|
|
66
|
+
const content = this.fileContent(name);
|
|
67
|
+
if (content) return this._expandGeminiMdImports(content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Further fallback: recognise common alternate instruction surfaces
|
|
71
|
+
// (AGENTS.md, CLAUDE.md) and Gemini Code Assist styleguides
|
|
72
|
+
// (.gemini/styleguide.md) even when not explicitly declared in settings,
|
|
73
|
+
// mirroring how real Gemini-using repos document guidance.
|
|
74
|
+
for (const alt of ['AGENTS.md', 'CLAUDE.md', '.gemini/styleguide.md']) {
|
|
75
|
+
const content = this.fileContent(alt);
|
|
76
|
+
if (content) return this._expandGeminiMdImports(content);
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
return null;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Expand Gemini CLI-style imports inside an instructions file. Gemini CLI
|
|
84
|
+
* supports `@path/to/file.md` imports and treats GEMINI.md files that are
|
|
85
|
+
* a single pointer line as an alias for the referenced file. For audit
|
|
86
|
+
* purposes we concatenate the referenced bodies so substance/architecture/
|
|
87
|
+
* command checks see the effective instructions bundle.
|
|
88
|
+
*/
|
|
89
|
+
_expandGeminiMdImports(content, depth = 0) {
|
|
90
|
+
if (!content || depth > 3) return content || '';
|
|
91
|
+
let out = content;
|
|
92
|
+
const importRe = /@([^\s@]+\.(?:md|markdown|MD))/g;
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
96
|
+
const ref = m[1].replace(/^\.\//, '');
|
|
97
|
+
if (seen.has(ref)) continue;
|
|
98
|
+
seen.add(ref);
|
|
99
|
+
const body = this.fileContent(ref);
|
|
100
|
+
if (body) out += '\n\n' + this._expandGeminiMdImports(body, depth + 1);
|
|
101
|
+
}
|
|
102
|
+
// "Pointer" GEMINI.md: the whole file is a single relative path to another
|
|
103
|
+
// markdown doc (no @ prefix). Observed in google/dotprompt.
|
|
104
|
+
const trimmed = content.trim();
|
|
105
|
+
if (/^[\w./-]+\.(md|markdown)$/.test(trimmed) && !trimmed.includes('\n')) {
|
|
106
|
+
const body = this.fileContent(trimmed);
|
|
107
|
+
if (body) out += '\n\n' + this._expandGeminiMdImports(body, depth + 1);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns true when the repo exposes any Gemini-recognisable instruction
|
|
114
|
+
* surface — GEMINI.md (directly or via context.fileName override), an
|
|
115
|
+
* imported pointer, AGENTS.md, or CLAUDE.md. Used to gate checks that
|
|
116
|
+
* would otherwise hard-fail on repos that use alternative conventions.
|
|
117
|
+
*/
|
|
118
|
+
hasAnyInstructionsSurface() {
|
|
119
|
+
return Boolean(this.geminiMdContent());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns true when the repo exposes any evidence of Gemini CLI usage.
|
|
124
|
+
* This is deliberately narrower than `isGeminiRepo`: it also counts
|
|
125
|
+
* `.idx/airules.md` (Project IDX) and Gemini-specific settings keys.
|
|
126
|
+
*/
|
|
127
|
+
hasGeminiCliSurface() {
|
|
128
|
+
if (this.fileContent('.gemini/settings.json')) return true;
|
|
129
|
+
if (this.fileContent('GEMINI.md')) return true;
|
|
130
|
+
const extDirs = this.extensionDirs ? this.extensionDirs() : [];
|
|
131
|
+
if (extDirs.length > 0) return true;
|
|
132
|
+
const cmdFiles = this.commandFiles ? this.commandFiles() : [];
|
|
133
|
+
if (cmdFiles.length > 0) return true;
|
|
134
|
+
if (this.fileContent('.idx/airules.md')) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
69
138
|
globalGeminiMdContent() {
|
|
70
139
|
const homeDir = os.homedir();
|
|
71
140
|
const globalPath = path.join(homeDir, '.gemini', 'GEMINI.md');
|
package/src/gemini/techniques.js
CHANGED
|
@@ -93,10 +93,37 @@ function settingsData(ctx) {
|
|
|
93
93
|
return result && result.ok ? result.data : null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* True when .gemini/settings.json is effectively an MCP-only config — i.e.
|
|
98
|
+
* it configures external tool servers but does not attempt to tune CLI
|
|
99
|
+
* behaviour (model, sandbox, approval, theme, history, etc.). Checks that
|
|
100
|
+
* assert on CLI-behaviour keys should be N/A on such configs.
|
|
101
|
+
*/
|
|
102
|
+
function isMcpOnlySettings(data) {
|
|
103
|
+
if (!data || typeof data !== 'object') return false;
|
|
104
|
+
const keys = Object.keys(data).filter(k => k !== '$schema' && k !== 'ide' && k !== 'context');
|
|
105
|
+
if (keys.length === 0) return true;
|
|
106
|
+
const behaviourKeys = new Set(['model', 'sandbox', 'safety', 'theme', 'approval', 'approvalMode', 'history', 'session', 'telemetry', 'hooks', 'tools', 'skills', 'commands', 'extensions', 'security']);
|
|
107
|
+
return keys.every(k => k === 'mcpServers' || !behaviourKeys.has(k));
|
|
108
|
+
}
|
|
109
|
+
|
|
96
110
|
function docsBundle(ctx) {
|
|
97
111
|
const gmd = geminiMd(ctx) || '';
|
|
98
112
|
const readme = ctx.fileContent('README.md') || '';
|
|
99
|
-
|
|
113
|
+
const agents = ctx.fileContent('AGENTS.md') || '';
|
|
114
|
+
const claudeMd = ctx.fileContent('CLAUDE.md') || '';
|
|
115
|
+
const contributing = ctx.fileContent('CONTRIBUTING.md') || '';
|
|
116
|
+
const architecture = ctx.fileContent('ARCHITECTURE.md') || '';
|
|
117
|
+
const development = ctx.fileContent('DEVELOPMENT.md') || ctx.fileContent('docs/development.md') || '';
|
|
118
|
+
return [gmd, readme, agents, claudeMd, contributing, architecture, development].join('\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Broader bundle for stack-specific docs discovery (mirrors the Copilot
|
|
122
|
+
// PP-01 stackDocsBundle approach): consults common developer docs in
|
|
123
|
+
// addition to the instruction surfaces so stack checks don't hard-fail
|
|
124
|
+
// when conventions live in CONTRIBUTING/DEVELOPMENT instead of GEMINI.md.
|
|
125
|
+
function stackDocsBundle(ctx) {
|
|
126
|
+
return docsBundle(ctx);
|
|
100
127
|
}
|
|
101
128
|
|
|
102
129
|
function expectedVerificationCategories(ctx) {
|
|
@@ -262,11 +289,18 @@ const GEMINI_TECHNIQUES = {
|
|
|
262
289
|
|
|
263
290
|
geminiMdArchitecture: {
|
|
264
291
|
id: 'GM-A04',
|
|
265
|
-
name: '
|
|
292
|
+
name: 'Instructions have architecture section or Mermaid diagram',
|
|
266
293
|
check: (ctx) => {
|
|
267
294
|
const content = geminiMd(ctx);
|
|
268
295
|
if (!content) return null;
|
|
269
|
-
|
|
296
|
+
if (hasArchitecture(content)) return true;
|
|
297
|
+
// Credit an ARCHITECTURE.md at repo root, or architecture content
|
|
298
|
+
// surfaced in README.md — both are legitimate ways Gemini will pick
|
|
299
|
+
// up repo shape, especially when GEMINI.md is a pointer/import.
|
|
300
|
+
const arch = ctx.fileContent('ARCHITECTURE.md') || ctx.fileContent('docs/architecture.md');
|
|
301
|
+
if (arch) return true;
|
|
302
|
+
const readme = ctx.fileContent('README.md') || '';
|
|
303
|
+
return hasArchitecture(readme);
|
|
270
304
|
},
|
|
271
305
|
impact: 'medium',
|
|
272
306
|
rating: 4,
|
|
@@ -352,7 +386,15 @@ const GEMINI_TECHNIQUES = {
|
|
|
352
386
|
geminiSettingsExists: {
|
|
353
387
|
id: 'GM-B01',
|
|
354
388
|
name: '.gemini/settings.json exists',
|
|
355
|
-
check: (ctx) =>
|
|
389
|
+
check: (ctx) => {
|
|
390
|
+
if (ctx.fileContent('.gemini/settings.json')) return true;
|
|
391
|
+
// N/A when the repo uses only the GEMINI.md-instruction convention
|
|
392
|
+
// without any .gemini/ configuration directory. settings.json is
|
|
393
|
+
// opt-in for CLI tuning; instruction-only repos should not fail.
|
|
394
|
+
const hasGeminiDir = ctx.hasDir && ctx.hasDir('.gemini');
|
|
395
|
+
if (!hasGeminiDir) return null;
|
|
396
|
+
return false;
|
|
397
|
+
},
|
|
356
398
|
impact: 'high',
|
|
357
399
|
rating: 5,
|
|
358
400
|
category: 'config',
|
|
@@ -397,6 +439,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
397
439
|
check: (ctx) => {
|
|
398
440
|
const data = settingsData(ctx);
|
|
399
441
|
if (!data) return null;
|
|
442
|
+
if (isMcpOnlySettings(data)) return null;
|
|
400
443
|
if (!data.model) return false;
|
|
401
444
|
// v0.36.0: model field MUST be an object { name: "..." }, not a string
|
|
402
445
|
// String format causes exit code 41: "Expected object, received string"
|
|
@@ -419,8 +462,9 @@ const GEMINI_TECHNIQUES = {
|
|
|
419
462
|
check: (ctx) => {
|
|
420
463
|
const data = settingsData(ctx);
|
|
421
464
|
if (!data) return null;
|
|
422
|
-
|
|
423
|
-
|
|
465
|
+
if (isMcpOnlySettings(data)) return null;
|
|
466
|
+
// At least one CLI-behaviour setting should be explicit.
|
|
467
|
+
return Boolean(data.sandbox || data.safety || data.theme || data.approval || data.approvalMode);
|
|
424
468
|
},
|
|
425
469
|
impact: 'medium',
|
|
426
470
|
rating: 4,
|
|
@@ -479,13 +523,17 @@ const GEMINI_TECHNIQUES = {
|
|
|
479
523
|
|
|
480
524
|
geminiEnvApiKey: {
|
|
481
525
|
id: 'GM-B07',
|
|
482
|
-
name: '
|
|
526
|
+
name: 'API key / auth documented (env file, README, or GEMINI.md)',
|
|
483
527
|
check: (ctx) => {
|
|
484
528
|
const envContent = ctx.fileContent('.env') || '';
|
|
485
|
-
const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || '';
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
529
|
+
const envExample = ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || ctx.fileContent('.env.sample') || '';
|
|
530
|
+
const docs = docsBundle(ctx);
|
|
531
|
+
const combined = `${envContent}\n${envExample}\n${docs}`;
|
|
532
|
+
if (!combined.trim()) return null;
|
|
533
|
+
// Credit env files OR documentation that mentions any Gemini/Google auth mechanism,
|
|
534
|
+
// including `gemini auth`, ADC / application default credentials, Vertex AI, or a
|
|
535
|
+
// direct mention of the standard env var names.
|
|
536
|
+
return /\bGEMINI_API_KEY\b|\bGOOGLE_API_KEY\b|\bGOOGLE_APPLICATION_CREDENTIALS\b|\bgcloud\b|\bapplication[- ]default credentials?\b|\bADC\b|\bvertex[- ]?ai\b|\bgemini auth\b|\bservice account\b/i.test(combined);
|
|
489
537
|
},
|
|
490
538
|
impact: 'high',
|
|
491
539
|
rating: 4,
|
|
@@ -542,6 +590,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
542
590
|
check: (ctx) => {
|
|
543
591
|
const data = settingsData(ctx);
|
|
544
592
|
if (!data) return null;
|
|
593
|
+
if (isMcpOnlySettings(data)) return null;
|
|
545
594
|
return Boolean(data.sandbox && (data.sandbox.mode || typeof data.sandbox === 'string'));
|
|
546
595
|
},
|
|
547
596
|
impact: 'high',
|
|
@@ -1017,6 +1066,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
1017
1066
|
check: (ctx) => {
|
|
1018
1067
|
const data = settingsData(ctx);
|
|
1019
1068
|
if (!data) return null;
|
|
1069
|
+
if (isMcpOnlySettings(data)) return null;
|
|
1020
1070
|
const sandbox = data.sandbox;
|
|
1021
1071
|
if (!sandbox) return false;
|
|
1022
1072
|
const mode = typeof sandbox === 'string' ? sandbox : sandbox.mode;
|
|
@@ -1633,8 +1683,9 @@ const GEMINI_TECHNIQUES = {
|
|
|
1633
1683
|
id: 'GM-J01',
|
|
1634
1684
|
name: 'Rate limit/quota awareness documented',
|
|
1635
1685
|
check: (ctx) => {
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1686
|
+
const docs = docsBundle(ctx);
|
|
1687
|
+
if (!docs.trim()) return null;
|
|
1688
|
+
return /\brate[- ]?limit\b|\bquota\b|\brequests? per\b|\bcost\b|\btoken\b.*\blimit\b|\bthrottl|\b429\b/i.test(docs);
|
|
1638
1689
|
},
|
|
1639
1690
|
impact: 'medium',
|
|
1640
1691
|
rating: 3,
|
|
@@ -1677,6 +1728,7 @@ const GEMINI_TECHNIQUES = {
|
|
|
1677
1728
|
check: (ctx) => {
|
|
1678
1729
|
const data = settingsData(ctx);
|
|
1679
1730
|
if (!data) return null;
|
|
1731
|
+
if (isMcpOnlySettings(data)) return null;
|
|
1680
1732
|
// Check if session/history settings are explicit
|
|
1681
1733
|
return data.history !== undefined || data.session !== undefined || data.telemetry !== undefined;
|
|
1682
1734
|
},
|
|
@@ -1836,11 +1888,11 @@ const GEMINI_TECHNIQUES = {
|
|
|
1836
1888
|
|
|
1837
1889
|
geminiTokenUsageAwareness: {
|
|
1838
1890
|
id: 'GM-K05',
|
|
1839
|
-
name: 'Token usage awareness
|
|
1891
|
+
name: 'Token usage awareness documented',
|
|
1840
1892
|
check: (ctx) => {
|
|
1841
|
-
const
|
|
1842
|
-
if (!
|
|
1843
|
-
return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(
|
|
1893
|
+
const docs = docsBundle(ctx);
|
|
1894
|
+
if (!docs.trim()) return null;
|
|
1895
|
+
return /\btoken\b|\bcontext window\b|\bcontext length\b|\b1M\b|\btruncat/i.test(docs);
|
|
1844
1896
|
},
|
|
1845
1897
|
impact: 'low',
|
|
1846
1898
|
rating: 2,
|
|
@@ -1863,7 +1915,12 @@ const GEMINI_TECHNIQUES = {
|
|
|
1863
1915
|
name: 'Custom commands exist in .gemini/commands/',
|
|
1864
1916
|
check: (ctx) => {
|
|
1865
1917
|
const commandFiles = ctx.commandFiles ? ctx.commandFiles() : [];
|
|
1866
|
-
|
|
1918
|
+
if (commandFiles.length > 0) return true;
|
|
1919
|
+
// Custom commands are opt-in — only fire when the repo already has
|
|
1920
|
+
// a .gemini/commands/ directory (implying the user intends to use
|
|
1921
|
+
// commands but hasn't populated it).
|
|
1922
|
+
const hasCommandsDir = ctx.hasDir && ctx.hasDir('.gemini/commands');
|
|
1923
|
+
return hasCommandsDir ? false : null;
|
|
1867
1924
|
},
|
|
1868
1925
|
impact: 'medium',
|
|
1869
1926
|
rating: 3,
|
|
@@ -2109,7 +2166,22 @@ const GEMINI_TECHNIQUES = {
|
|
|
2109
2166
|
},
|
|
2110
2167
|
geminiPropagationCompleteness: {
|
|
2111
2168
|
id: 'GM-P03', name: 'No dangling surface references',
|
|
2112
|
-
check: (ctx) => {
|
|
2169
|
+
check: (ctx) => {
|
|
2170
|
+
const g = ctx.geminiMdContent();
|
|
2171
|
+
if (!g) return null;
|
|
2172
|
+
const issues = [];
|
|
2173
|
+
// Require specific Gemini-CLI vocabulary before asserting a dangling
|
|
2174
|
+
// reference. "skills" is too generic a word (appears in unrelated
|
|
2175
|
+
// product copy); require `.gemini/skills` path or `gemini skills`
|
|
2176
|
+
// phrasing. Same for hooks/extensions.
|
|
2177
|
+
if (/\.gemini\/hooks\b|\bgemini hooks?\b|\bhooksConfig\b|\bBeforeTool\b|\bAfterTool\b/i.test(g)) {
|
|
2178
|
+
const s = ctx.settingsJson();
|
|
2179
|
+
if (!s || !s.ok || (!s.data || (!s.data.hooks && !s.data.BeforeTool && !s.data.AfterTool))) issues.push('hooks');
|
|
2180
|
+
}
|
|
2181
|
+
if (/\.gemini\/skills\b|\bgemini skills?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/skills') : false)) issues.push('skills');
|
|
2182
|
+
if (/\.gemini\/extensions\b|\bgemini extensions?\b/i.test(g) && !(ctx.hasDir ? ctx.hasDir('.gemini/extensions') : false)) issues.push('extensions');
|
|
2183
|
+
return issues.length === 0;
|
|
2184
|
+
},
|
|
2113
2185
|
impact: 'high', rating: 4, category: 'release-freshness',
|
|
2114
2186
|
fix: 'Ensure all surfaces mentioned in GEMINI.md have corresponding definition files.',
|
|
2115
2187
|
template: 'gemini-md', file: () => 'GEMINI.md', line: () => 1,
|