cc-reviewer 5.2.0 → 5.3.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.
@@ -16,12 +16,7 @@ import { registerAdapter, } from './base.js';
16
16
  import { CliExecutor } from '../executor.js';
17
17
  import { ClaudeEventDecoder } from '../decoders/index.js';
18
18
  import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
19
- // =============================================================================
20
- // CONFIGURATION
21
- // =============================================================================
22
- const INACTIVITY_TIMEOUT_MS = 300_000; // 5 min — Opus has long thinking phases
23
- const MAX_TIMEOUT_MS = 3_600_000; // 60 min absolute max
24
- const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
19
+ import { getConfig } from '../config.js';
25
20
  // Write tools explicitly blocked as defense-in-depth
26
21
  const DISALLOWED_TOOLS = 'Edit Write NotebookEdit';
27
22
  // =============================================================================
@@ -43,12 +38,18 @@ export class ClaudeAdapter {
43
38
  }
44
39
  async isAvailable() {
45
40
  return new Promise((resolve) => {
41
+ let settled = false;
42
+ const done = (result) => { if (!settled) {
43
+ settled = true;
44
+ clearTimeout(timer);
45
+ resolve(result);
46
+ } };
46
47
  const proc = spawn('claude', ['--version'], {
47
48
  stdio: ['ignore', 'pipe', 'pipe'],
48
49
  });
49
- proc.on('close', (code) => resolve(code === 0));
50
- proc.on('error', () => resolve(false));
51
- setTimeout(() => { proc.kill(); resolve(false); }, 5000);
50
+ proc.on('close', (code) => done(code === 0));
51
+ proc.on('error', () => done(false));
52
+ const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
52
53
  });
53
54
  }
54
55
  async runReview(request) {
@@ -86,9 +87,10 @@ export class ClaudeAdapter {
86
87
  }
87
88
  }
88
89
  async runCli(prompt, workingDir) {
90
+ const cfg = getConfig().claude;
89
91
  const args = [
90
92
  '-p', // Non-interactive, print and exit
91
- '--model', 'opus', // Use Opus
93
+ '--model', cfg.model, // Model from config (default: opus)
92
94
  '--setting-sources', '', // Skip hooks, plugins, CLAUDE.md (preserves OAuth auth; --bare kills keychain)
93
95
  '--permission-mode', 'plan', // Read-only enforcement (layer 1)
94
96
  '--verbose', // Required for stream-json
@@ -111,9 +113,9 @@ export class ClaudeAdapter {
111
113
  args,
112
114
  cwd: workingDir,
113
115
  stdin: prompt,
114
- inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS,
115
- maxTimeoutMs: MAX_TIMEOUT_MS,
116
- maxBufferSize: MAX_BUFFER_SIZE,
116
+ inactivityTimeoutMs: cfg.inactivityTimeoutMs,
117
+ maxTimeoutMs: cfg.maxTimeoutMs,
118
+ maxBufferSize: cfg.maxBufferSize,
117
119
  onLine: (line) => {
118
120
  decoder.processLine(line);
119
121
  },
@@ -11,15 +11,7 @@ import { registerAdapter, } from './base.js';
11
11
  import { CliExecutor } from '../executor.js';
12
12
  import { CodexEventDecoder } from '../decoders/index.js';
13
13
  import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
14
- // =============================================================================
15
- // CONFIGURATION
16
- // =============================================================================
17
- const INACTIVITY_TIMEOUT_MS = {
18
- high: 180_000, // 3 min — covers reasoning gaps between tool use bursts
19
- xhigh: 300_000, // 5 min — xhigh has longer reasoning phases
20
- };
21
- const MAX_TIMEOUT_MS = 3_600_000; // 60 min absolute max
22
- const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
14
+ import { getConfig } from '../config.js';
23
15
  // =============================================================================
24
16
  // CODEX ADAPTER
25
17
  // =============================================================================
@@ -39,12 +31,18 @@ export class CodexAdapter {
39
31
  }
40
32
  async isAvailable() {
41
33
  return new Promise((resolve) => {
34
+ let settled = false;
35
+ const done = (result) => { if (!settled) {
36
+ settled = true;
37
+ clearTimeout(timer);
38
+ resolve(result);
39
+ } };
42
40
  const proc = spawn('codex', ['--version'], {
43
41
  stdio: ['ignore', 'pipe', 'pipe'],
44
42
  });
45
- proc.on('close', (code) => resolve(code === 0));
46
- proc.on('error', () => resolve(false));
47
- setTimeout(() => { proc.kill(); resolve(false); }, 5000);
43
+ proc.on('close', (code) => done(code === 0));
44
+ proc.on('error', () => done(false));
45
+ const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
48
46
  });
49
47
  }
50
48
  async runReview(request) {
@@ -62,7 +60,8 @@ export class CodexAdapter {
62
60
  const prompt = request.reviewMode === 'adversarial'
63
61
  ? buildAdversarialHandoffPrompt({ handoff })
64
62
  : buildHandoffPrompt({ handoff, role: selectRole(request.focusAreas) });
65
- const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort || 'high', request.serviceTier);
63
+ const cfg = getConfig().codex;
64
+ const result = await this.runCli(prompt, request.workingDir, request.reasoningEffort ?? cfg.reasoningEffort, request.serviceTier);
66
65
  if (result.exitCode !== 0) {
67
66
  const error = this.categorizeError(result.stderr);
68
67
  return { success: false, error, suggestion: this.getSuggestion(error), executionTimeMs: Date.now() - startTime };
@@ -82,10 +81,11 @@ export class CodexAdapter {
82
81
  }
83
82
  }
84
83
  async runCli(prompt, workingDir, reasoningEffort, serviceTier) {
84
+ const cfg = getConfig().codex;
85
85
  const args = [
86
86
  'exec',
87
87
  '--json', // JSONL streaming events
88
- '-m', 'gpt-5.4',
88
+ '-m', cfg.model,
89
89
  '-c', `model_reasoning_effort=${reasoningEffort}`,
90
90
  '-c', 'model_reasoning_summary_format=experimental',
91
91
  '--full-auto',
@@ -94,9 +94,9 @@ export class CodexAdapter {
94
94
  '-C', workingDir,
95
95
  '-', // Read prompt from stdin
96
96
  ];
97
- // Default to 'fast' tier when caller omits serviceTier.
98
- // Explicit 'default' is a user opt-out and emits no flag (uses Codex API default).
99
- const effectiveTier = serviceTier === undefined ? 'fast' : serviceTier;
97
+ // Caller-supplied serviceTier overrides config. Explicit 'default' is an
98
+ // opt-out and emits no flag (uses Codex API default).
99
+ const effectiveTier = serviceTier ?? cfg.serviceTier;
100
100
  if (effectiveTier !== 'default') {
101
101
  args.push('-c', `service_tier=${effectiveTier}`);
102
102
  }
@@ -114,9 +114,9 @@ export class CodexAdapter {
114
114
  args,
115
115
  cwd: workingDir,
116
116
  stdin: prompt,
117
- inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS[reasoningEffort] || INACTIVITY_TIMEOUT_MS.high,
118
- maxTimeoutMs: MAX_TIMEOUT_MS,
119
- maxBufferSize: MAX_BUFFER_SIZE,
117
+ inactivityTimeoutMs: cfg.inactivityTimeoutMs[reasoningEffort] ?? cfg.inactivityTimeoutMs.high,
118
+ maxTimeoutMs: cfg.maxTimeoutMs,
119
+ maxBufferSize: cfg.maxBufferSize,
120
120
  onLine: (line) => {
121
121
  decoder.processLine(line);
122
122
  },
@@ -11,12 +11,7 @@ import { registerAdapter, } from './base.js';
11
11
  import { CliExecutor } from '../executor.js';
12
12
  import { GeminiEventDecoder } from '../decoders/index.js';
13
13
  import { buildSimpleHandoff, buildHandoffPrompt, buildAdversarialHandoffPrompt, selectRole, } from '../handoff.js';
14
- // =============================================================================
15
- // CONFIGURATION
16
- // =============================================================================
17
- const INACTIVITY_TIMEOUT_MS = 300_000; // 5 min — covers reasoning gaps between tool use
18
- const MAX_TIMEOUT_MS = 3_600_000; // 60 min absolute max
19
- const MAX_BUFFER_SIZE = 1024 * 1024; // 1MB max buffer
14
+ import { getConfig } from '../config.js';
20
15
  // =============================================================================
21
16
  // GEMINI ADAPTER
22
17
  // =============================================================================
@@ -36,12 +31,18 @@ export class GeminiAdapter {
36
31
  }
37
32
  async isAvailable() {
38
33
  return new Promise((resolve) => {
34
+ let settled = false;
35
+ const done = (result) => { if (!settled) {
36
+ settled = true;
37
+ clearTimeout(timer);
38
+ resolve(result);
39
+ } };
39
40
  const proc = spawn('gemini', ['--version'], {
40
41
  stdio: ['ignore', 'pipe', 'pipe'],
41
42
  });
42
- proc.on('close', (code) => resolve(code === 0));
43
- proc.on('error', () => resolve(false));
44
- setTimeout(() => { proc.kill(); resolve(false); }, 5000);
43
+ proc.on('close', (code) => done(code === 0));
44
+ proc.on('error', () => done(false));
45
+ const timer = setTimeout(() => { proc.kill(); done(false); }, 5000);
45
46
  });
46
47
  }
47
48
  async runReview(request) {
@@ -79,6 +80,7 @@ export class GeminiAdapter {
79
80
  }
80
81
  }
81
82
  async runCli(prompt, workingDir) {
83
+ const cfg = getConfig().gemini;
82
84
  const args = [
83
85
  '--sandbox',
84
86
  '--approval-mode', 'plan',
@@ -86,6 +88,9 @@ export class GeminiAdapter {
86
88
  '--include-directories', workingDir,
87
89
  '-p', '',
88
90
  ];
91
+ if (cfg.model) {
92
+ args.push('--model', cfg.model);
93
+ }
89
94
  const decoder = new GeminiEventDecoder();
90
95
  const cliStartTime = Date.now();
91
96
  console.error('[gemini] Running...');
@@ -99,9 +104,9 @@ export class GeminiAdapter {
99
104
  args,
100
105
  cwd: workingDir,
101
106
  stdin: prompt,
102
- inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS,
103
- maxTimeoutMs: MAX_TIMEOUT_MS,
104
- maxBufferSize: MAX_BUFFER_SIZE,
107
+ inactivityTimeoutMs: cfg.inactivityTimeoutMs,
108
+ maxTimeoutMs: cfg.maxTimeoutMs,
109
+ maxBufferSize: cfg.maxBufferSize,
105
110
  onLine: (line) => {
106
111
  decoder.processLine(line);
107
112
  },
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Runtime configuration for cc-reviewer.
3
+ *
4
+ * Config file: ~/.config/cc-reviewer/config.json
5
+ *
6
+ * Semantics:
7
+ * - Lazy, cached load. `getConfig()` returns the cached config or reads once.
8
+ * - Missing file → defaults in memory (no write). Use `initConfig()` from the
9
+ * server entry point to create the file with defaults on first launch.
10
+ * - Invalid JSON or schema violations → fall back to defaults, warn on stderr.
11
+ * - Partial user configs are deep-merged against defaults via Zod `.default()`.
12
+ * - Tool-call arguments still override config (e.g. `reasoningEffort` on a
13
+ * single `codex_review` call). Config only sets defaults.
14
+ */
15
+ import { z } from 'zod';
16
+ export declare const CodexConfigSchema: z.ZodDefault<z.ZodObject<{
17
+ model: z.ZodDefault<z.ZodString>;
18
+ reasoningEffort: z.ZodDefault<z.ZodEnum<["high", "xhigh"]>>;
19
+ serviceTier: z.ZodDefault<z.ZodEnum<["default", "fast", "flex"]>>;
20
+ inactivityTimeoutMs: z.ZodDefault<z.ZodObject<{
21
+ high: z.ZodDefault<z.ZodNumber>;
22
+ xhigh: z.ZodDefault<z.ZodNumber>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ high: number;
25
+ xhigh: number;
26
+ }, {
27
+ high?: number | undefined;
28
+ xhigh?: number | undefined;
29
+ }>>;
30
+ maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
31
+ maxBufferSize: z.ZodDefault<z.ZodNumber>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ model: string;
34
+ reasoningEffort: "high" | "xhigh";
35
+ serviceTier: "default" | "fast" | "flex";
36
+ inactivityTimeoutMs: {
37
+ high: number;
38
+ xhigh: number;
39
+ };
40
+ maxTimeoutMs: number;
41
+ maxBufferSize: number;
42
+ }, {
43
+ model?: string | undefined;
44
+ reasoningEffort?: "high" | "xhigh" | undefined;
45
+ serviceTier?: "default" | "fast" | "flex" | undefined;
46
+ inactivityTimeoutMs?: {
47
+ high?: number | undefined;
48
+ xhigh?: number | undefined;
49
+ } | undefined;
50
+ maxTimeoutMs?: number | undefined;
51
+ maxBufferSize?: number | undefined;
52
+ }>>;
53
+ export declare const ClaudeConfigSchema: z.ZodDefault<z.ZodObject<{
54
+ model: z.ZodDefault<z.ZodString>;
55
+ inactivityTimeoutMs: z.ZodDefault<z.ZodNumber>;
56
+ maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
57
+ maxBufferSize: z.ZodDefault<z.ZodNumber>;
58
+ }, "strip", z.ZodTypeAny, {
59
+ model: string;
60
+ inactivityTimeoutMs: number;
61
+ maxTimeoutMs: number;
62
+ maxBufferSize: number;
63
+ }, {
64
+ model?: string | undefined;
65
+ inactivityTimeoutMs?: number | undefined;
66
+ maxTimeoutMs?: number | undefined;
67
+ maxBufferSize?: number | undefined;
68
+ }>>;
69
+ export declare const GeminiConfigSchema: z.ZodDefault<z.ZodObject<{
70
+ model: z.ZodDefault<z.ZodNullable<z.ZodString>>;
71
+ inactivityTimeoutMs: z.ZodDefault<z.ZodNumber>;
72
+ maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
73
+ maxBufferSize: z.ZodDefault<z.ZodNumber>;
74
+ }, "strip", z.ZodTypeAny, {
75
+ model: string | null;
76
+ inactivityTimeoutMs: number;
77
+ maxTimeoutMs: number;
78
+ maxBufferSize: number;
79
+ }, {
80
+ model?: string | null | undefined;
81
+ inactivityTimeoutMs?: number | undefined;
82
+ maxTimeoutMs?: number | undefined;
83
+ maxBufferSize?: number | undefined;
84
+ }>>;
85
+ export declare const ConfigSchema: z.ZodDefault<z.ZodObject<{
86
+ codex: z.ZodDefault<z.ZodObject<{
87
+ model: z.ZodDefault<z.ZodString>;
88
+ reasoningEffort: z.ZodDefault<z.ZodEnum<["high", "xhigh"]>>;
89
+ serviceTier: z.ZodDefault<z.ZodEnum<["default", "fast", "flex"]>>;
90
+ inactivityTimeoutMs: z.ZodDefault<z.ZodObject<{
91
+ high: z.ZodDefault<z.ZodNumber>;
92
+ xhigh: z.ZodDefault<z.ZodNumber>;
93
+ }, "strip", z.ZodTypeAny, {
94
+ high: number;
95
+ xhigh: number;
96
+ }, {
97
+ high?: number | undefined;
98
+ xhigh?: number | undefined;
99
+ }>>;
100
+ maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
101
+ maxBufferSize: z.ZodDefault<z.ZodNumber>;
102
+ }, "strip", z.ZodTypeAny, {
103
+ model: string;
104
+ reasoningEffort: "high" | "xhigh";
105
+ serviceTier: "default" | "fast" | "flex";
106
+ inactivityTimeoutMs: {
107
+ high: number;
108
+ xhigh: number;
109
+ };
110
+ maxTimeoutMs: number;
111
+ maxBufferSize: number;
112
+ }, {
113
+ model?: string | undefined;
114
+ reasoningEffort?: "high" | "xhigh" | undefined;
115
+ serviceTier?: "default" | "fast" | "flex" | undefined;
116
+ inactivityTimeoutMs?: {
117
+ high?: number | undefined;
118
+ xhigh?: number | undefined;
119
+ } | undefined;
120
+ maxTimeoutMs?: number | undefined;
121
+ maxBufferSize?: number | undefined;
122
+ }>>;
123
+ claude: z.ZodDefault<z.ZodObject<{
124
+ model: z.ZodDefault<z.ZodString>;
125
+ inactivityTimeoutMs: z.ZodDefault<z.ZodNumber>;
126
+ maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
127
+ maxBufferSize: z.ZodDefault<z.ZodNumber>;
128
+ }, "strip", z.ZodTypeAny, {
129
+ model: string;
130
+ inactivityTimeoutMs: number;
131
+ maxTimeoutMs: number;
132
+ maxBufferSize: number;
133
+ }, {
134
+ model?: string | undefined;
135
+ inactivityTimeoutMs?: number | undefined;
136
+ maxTimeoutMs?: number | undefined;
137
+ maxBufferSize?: number | undefined;
138
+ }>>;
139
+ gemini: z.ZodDefault<z.ZodObject<{
140
+ model: z.ZodDefault<z.ZodNullable<z.ZodString>>;
141
+ inactivityTimeoutMs: z.ZodDefault<z.ZodNumber>;
142
+ maxTimeoutMs: z.ZodDefault<z.ZodNumber>;
143
+ maxBufferSize: z.ZodDefault<z.ZodNumber>;
144
+ }, "strip", z.ZodTypeAny, {
145
+ model: string | null;
146
+ inactivityTimeoutMs: number;
147
+ maxTimeoutMs: number;
148
+ maxBufferSize: number;
149
+ }, {
150
+ model?: string | null | undefined;
151
+ inactivityTimeoutMs?: number | undefined;
152
+ maxTimeoutMs?: number | undefined;
153
+ maxBufferSize?: number | undefined;
154
+ }>>;
155
+ }, "strip", z.ZodTypeAny, {
156
+ codex: {
157
+ model: string;
158
+ reasoningEffort: "high" | "xhigh";
159
+ serviceTier: "default" | "fast" | "flex";
160
+ inactivityTimeoutMs: {
161
+ high: number;
162
+ xhigh: number;
163
+ };
164
+ maxTimeoutMs: number;
165
+ maxBufferSize: number;
166
+ };
167
+ claude: {
168
+ model: string;
169
+ inactivityTimeoutMs: number;
170
+ maxTimeoutMs: number;
171
+ maxBufferSize: number;
172
+ };
173
+ gemini: {
174
+ model: string | null;
175
+ inactivityTimeoutMs: number;
176
+ maxTimeoutMs: number;
177
+ maxBufferSize: number;
178
+ };
179
+ }, {
180
+ codex?: {
181
+ model?: string | undefined;
182
+ reasoningEffort?: "high" | "xhigh" | undefined;
183
+ serviceTier?: "default" | "fast" | "flex" | undefined;
184
+ inactivityTimeoutMs?: {
185
+ high?: number | undefined;
186
+ xhigh?: number | undefined;
187
+ } | undefined;
188
+ maxTimeoutMs?: number | undefined;
189
+ maxBufferSize?: number | undefined;
190
+ } | undefined;
191
+ claude?: {
192
+ model?: string | undefined;
193
+ inactivityTimeoutMs?: number | undefined;
194
+ maxTimeoutMs?: number | undefined;
195
+ maxBufferSize?: number | undefined;
196
+ } | undefined;
197
+ gemini?: {
198
+ model?: string | null | undefined;
199
+ inactivityTimeoutMs?: number | undefined;
200
+ maxTimeoutMs?: number | undefined;
201
+ maxBufferSize?: number | undefined;
202
+ } | undefined;
203
+ }>>;
204
+ export type Config = z.infer<typeof ConfigSchema>;
205
+ export type CodexConfig = z.infer<typeof CodexConfigSchema>;
206
+ export type ClaudeConfig = z.infer<typeof ClaudeConfigSchema>;
207
+ export type GeminiConfig = z.infer<typeof GeminiConfigSchema>;
208
+ export declare const DEFAULT_CONFIG: Config;
209
+ export declare function getConfigPath(): string;
210
+ export declare function getConfig(): Config;
211
+ /**
212
+ * Create the config file with defaults if it does not exist.
213
+ * Uses the exclusive `wx` flag for atomic creation — safe against TOCTOU races
214
+ * when multiple server instances start concurrently.
215
+ * Refreshes the cached config so subsequent `getConfig()` calls see disk state.
216
+ */
217
+ export declare function initConfig(): {
218
+ path: string;
219
+ created: boolean;
220
+ };
221
+ /** Test-only hook. Redirects the config path and clears the cache. */
222
+ export declare function setConfigPathForTesting(path: string | null): void;
package/dist/config.js ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Runtime configuration for cc-reviewer.
3
+ *
4
+ * Config file: ~/.config/cc-reviewer/config.json
5
+ *
6
+ * Semantics:
7
+ * - Lazy, cached load. `getConfig()` returns the cached config or reads once.
8
+ * - Missing file → defaults in memory (no write). Use `initConfig()` from the
9
+ * server entry point to create the file with defaults on first launch.
10
+ * - Invalid JSON or schema violations → fall back to defaults, warn on stderr.
11
+ * - Partial user configs are deep-merged against defaults via Zod `.default()`.
12
+ * - Tool-call arguments still override config (e.g. `reasoningEffort` on a
13
+ * single `codex_review` call). Config only sets defaults.
14
+ */
15
+ import { z } from 'zod';
16
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
17
+ import { dirname, join } from 'path';
18
+ import { homedir } from 'os';
19
+ // =============================================================================
20
+ // SCHEMA
21
+ // =============================================================================
22
+ export const CodexConfigSchema = z
23
+ .object({
24
+ model: z.string().default('gpt-5.4'),
25
+ reasoningEffort: z.enum(['high', 'xhigh']).default('high'),
26
+ serviceTier: z.enum(['default', 'fast', 'flex']).default('fast'),
27
+ inactivityTimeoutMs: z
28
+ .object({
29
+ high: z.number().int().positive().default(180_000),
30
+ xhigh: z.number().int().positive().default(300_000),
31
+ })
32
+ .default({}),
33
+ maxTimeoutMs: z.number().int().positive().default(3_600_000),
34
+ maxBufferSize: z.number().int().positive().default(1_048_576),
35
+ })
36
+ .default({});
37
+ export const ClaudeConfigSchema = z
38
+ .object({
39
+ model: z.string().default('opus'),
40
+ inactivityTimeoutMs: z.number().int().positive().default(300_000),
41
+ maxTimeoutMs: z.number().int().positive().default(3_600_000),
42
+ maxBufferSize: z.number().int().positive().default(1_048_576),
43
+ })
44
+ .default({});
45
+ export const GeminiConfigSchema = z
46
+ .object({
47
+ model: z.string().nullable().default('gemini-3.1-pro-preview'),
48
+ inactivityTimeoutMs: z.number().int().positive().default(300_000),
49
+ maxTimeoutMs: z.number().int().positive().default(3_600_000),
50
+ maxBufferSize: z.number().int().positive().default(1_048_576),
51
+ })
52
+ .default({});
53
+ export const ConfigSchema = z
54
+ .object({
55
+ codex: CodexConfigSchema,
56
+ claude: ClaudeConfigSchema,
57
+ gemini: GeminiConfigSchema,
58
+ })
59
+ .default({});
60
+ export const DEFAULT_CONFIG = ConfigSchema.parse({});
61
+ // =============================================================================
62
+ // STATE
63
+ // =============================================================================
64
+ const DEFAULT_CONFIG_PATH = join(homedir(), '.config', 'cc-reviewer', 'config.json');
65
+ let _configPath = DEFAULT_CONFIG_PATH;
66
+ let _cached = null;
67
+ let _cachedMtimeMs = 0;
68
+ // =============================================================================
69
+ // PUBLIC API
70
+ // =============================================================================
71
+ export function getConfigPath() {
72
+ return _configPath;
73
+ }
74
+ export function getConfig() {
75
+ // Hot-reload: re-read if the file's mtime has changed since last load.
76
+ if (_cached) {
77
+ try {
78
+ if (existsSync(_configPath)) {
79
+ const mtime = statSync(_configPath).mtimeMs;
80
+ if (mtime !== _cachedMtimeMs) {
81
+ _cached = loadConfigFromDisk(_configPath);
82
+ _cachedMtimeMs = mtime;
83
+ }
84
+ }
85
+ }
86
+ catch {
87
+ // statSync failure is non-fatal — keep using the cached config.
88
+ }
89
+ return _cached;
90
+ }
91
+ _cached = loadConfigFromDisk(_configPath);
92
+ if (existsSync(_configPath)) {
93
+ try {
94
+ _cachedMtimeMs = statSync(_configPath).mtimeMs;
95
+ }
96
+ catch { /* ignore */ }
97
+ }
98
+ return _cached;
99
+ }
100
+ /**
101
+ * Create the config file with defaults if it does not exist.
102
+ * Uses the exclusive `wx` flag for atomic creation — safe against TOCTOU races
103
+ * when multiple server instances start concurrently.
104
+ * Refreshes the cached config so subsequent `getConfig()` calls see disk state.
105
+ */
106
+ export function initConfig() {
107
+ const path = _configPath;
108
+ mkdirSync(dirname(path), { recursive: true });
109
+ try {
110
+ writeFileSync(path, JSON.stringify(DEFAULT_CONFIG, null, 2) + '\n', { encoding: 'utf-8', flag: 'wx' });
111
+ _cached = DEFAULT_CONFIG;
112
+ _cachedMtimeMs = statSync(path).mtimeMs;
113
+ return { path, created: true };
114
+ }
115
+ catch (error) {
116
+ if (error.code === 'EEXIST') {
117
+ _cached = loadConfigFromDisk(path);
118
+ try {
119
+ _cachedMtimeMs = statSync(path).mtimeMs;
120
+ }
121
+ catch { /* ignore */ }
122
+ return { path, created: false };
123
+ }
124
+ throw error;
125
+ }
126
+ }
127
+ /** Test-only hook. Redirects the config path and clears the cache. */
128
+ export function setConfigPathForTesting(path) {
129
+ _configPath = path ?? DEFAULT_CONFIG_PATH;
130
+ _cached = null;
131
+ _cachedMtimeMs = 0;
132
+ }
133
+ // =============================================================================
134
+ // INTERNAL
135
+ // =============================================================================
136
+ /**
137
+ * Parse each adapter's config independently so a typo in one section only
138
+ * resets that adapter to defaults — the other adapters' settings survive.
139
+ */
140
+ function loadConfigFromDisk(path) {
141
+ if (!existsSync(path))
142
+ return DEFAULT_CONFIG;
143
+ let raw;
144
+ try {
145
+ raw = JSON.parse(readFileSync(path, 'utf-8'));
146
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
147
+ console.error(`[cc-reviewer] Config at ${path} is not a JSON object — using defaults.`);
148
+ return DEFAULT_CONFIG;
149
+ }
150
+ }
151
+ catch (error) {
152
+ const msg = error instanceof Error ? error.message : String(error);
153
+ console.error(`[cc-reviewer] Invalid JSON in ${path} — using defaults. Error: ${msg}`);
154
+ return DEFAULT_CONFIG;
155
+ }
156
+ const adapters = [
157
+ { key: 'codex', schema: CodexConfigSchema },
158
+ { key: 'claude', schema: ClaudeConfigSchema },
159
+ { key: 'gemini', schema: GeminiConfigSchema },
160
+ ];
161
+ const result = {};
162
+ for (const { key, schema } of adapters) {
163
+ const section = raw[key];
164
+ try {
165
+ result[key] = schema.parse(section);
166
+ }
167
+ catch (error) {
168
+ const msg = error instanceof Error ? error.message : String(error);
169
+ console.error(`[cc-reviewer] Invalid "${key}" config — using ${key} defaults. Error: ${msg}`);
170
+ result[key] = schema.parse(undefined);
171
+ }
172
+ }
173
+ return result;
174
+ }
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
21
21
  import { handleCodexReview, handleGeminiReview, handleClaudeReview, handleMultiReview, ReviewInputSchema, TOOL_DEFINITIONS } from './tools/feedback.js';
22
22
  import { logCliStatus } from './cli/check.js';
23
23
  import { installCommands } from './commands.js';
24
+ import { initConfig } from './config.js';
24
25
  // Read version from package.json
25
26
  import { readFileSync } from 'fs';
26
27
  import { join, dirname } from 'path';
@@ -111,6 +112,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
111
112
  });
112
113
  // Start the server
113
114
  async function main() {
115
+ // Initialize config (writes defaults to ~/.config/cc-reviewer/config.json on first run)
116
+ try {
117
+ const cfg = initConfig();
118
+ console.error(cfg.created
119
+ ? `[cc-reviewer] Initialized config at ${cfg.path}`
120
+ : `[cc-reviewer] Loaded config from ${cfg.path}`);
121
+ }
122
+ catch (error) {
123
+ const msg = error instanceof Error ? error.message : String(error);
124
+ console.error(`[cc-reviewer] Warning: Could not initialize config: ${msg}`);
125
+ }
114
126
  // Auto-install slash commands
115
127
  const result = installCommands();
116
128
  if (result.success) {
package/dist/schema.d.ts CHANGED
@@ -63,7 +63,7 @@ export declare const ReviewFinding: z.ZodObject<{
63
63
  owasp_category: z.ZodOptional<z.ZodString>;
64
64
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
65
65
  }, "strip", z.ZodTypeAny, {
66
- severity: "info" | "high" | "critical" | "medium" | "low";
66
+ severity: "high" | "info" | "critical" | "medium" | "low";
67
67
  title: string;
68
68
  description: string;
69
69
  category: "other" | "performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation" | "best-practice";
@@ -82,7 +82,7 @@ export declare const ReviewFinding: z.ZodObject<{
82
82
  owasp_category?: string | undefined;
83
83
  tags?: string[] | undefined;
84
84
  }, {
85
- severity: "info" | "high" | "critical" | "medium" | "low";
85
+ severity: "high" | "info" | "critical" | "medium" | "low";
86
86
  title: string;
87
87
  description: string;
88
88
  category: "other" | "performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation" | "best-practice";
@@ -266,7 +266,7 @@ export declare const ReviewOutput: z.ZodObject<{
266
266
  owasp_category: z.ZodOptional<z.ZodString>;
267
267
  tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
268
268
  }, "strip", z.ZodTypeAny, {
269
- severity: "info" | "high" | "critical" | "medium" | "low";
269
+ severity: "high" | "info" | "critical" | "medium" | "low";
270
270
  title: string;
271
271
  description: string;
272
272
  category: "other" | "performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation" | "best-practice";
@@ -285,7 +285,7 @@ export declare const ReviewOutput: z.ZodObject<{
285
285
  owasp_category?: string | undefined;
286
286
  tags?: string[] | undefined;
287
287
  }, {
288
- severity: "info" | "high" | "critical" | "medium" | "low";
288
+ severity: "high" | "info" | "critical" | "medium" | "low";
289
289
  title: string;
290
290
  description: string;
291
291
  category: "other" | "performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation" | "best-practice";
@@ -431,7 +431,7 @@ export declare const ReviewOutput: z.ZodObject<{
431
431
  execution_notes: z.ZodOptional<z.ZodNullable<z.ZodString>>;
432
432
  }, "strip", z.ZodTypeAny, {
433
433
  findings: {
434
- severity: "info" | "high" | "critical" | "medium" | "low";
434
+ severity: "high" | "info" | "critical" | "medium" | "low";
435
435
  title: string;
436
436
  description: string;
437
437
  category: "other" | "performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation" | "best-practice";
@@ -499,7 +499,7 @@ export declare const ReviewOutput: z.ZodObject<{
499
499
  execution_notes?: string | null | undefined;
500
500
  }, {
501
501
  findings: {
502
- severity: "info" | "high" | "critical" | "medium" | "low";
502
+ severity: "high" | "info" | "critical" | "medium" | "low";
503
503
  title: string;
504
504
  description: string;
505
505
  category: "other" | "performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation" | "best-practice";
@@ -18,20 +18,20 @@ export declare const ReviewInputSchema: z.ZodObject<{
18
18
  workingDir: string;
19
19
  ccOutput: string;
20
20
  outputType: "findings" | "analysis" | "plan" | "proposal";
21
+ reasoningEffort?: "high" | "xhigh" | undefined;
22
+ serviceTier?: "default" | "fast" | "flex" | undefined;
21
23
  focusAreas?: ("performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation")[] | undefined;
22
24
  analyzedFiles?: string[] | undefined;
23
25
  customPrompt?: string | undefined;
24
- reasoningEffort?: "high" | "xhigh" | undefined;
25
- serviceTier?: "default" | "fast" | "flex" | undefined;
26
26
  }, {
27
27
  workingDir: string;
28
28
  ccOutput: string;
29
29
  outputType: "findings" | "analysis" | "plan" | "proposal";
30
+ reasoningEffort?: "high" | "xhigh" | undefined;
31
+ serviceTier?: "default" | "fast" | "flex" | undefined;
30
32
  focusAreas?: ("performance" | "security" | "testing" | "architecture" | "correctness" | "maintainability" | "scalability" | "documentation")[] | undefined;
31
33
  analyzedFiles?: string[] | undefined;
32
34
  customPrompt?: string | undefined;
33
- reasoningEffort?: "high" | "xhigh" | undefined;
34
- serviceTier?: "default" | "fast" | "flex" | undefined;
35
35
  }>;
36
36
  export type ReviewInput = z.infer<typeof ReviewInputSchema>;
37
37
  export declare function handleCodexReview(input: ReviewInput): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-reviewer",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "MCP server for Claude Code - Get second-opinion feedback from Codex/Gemini CLIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",