cc-reviewer 5.1.1 → 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.
@@ -49,9 +49,9 @@ Call `codex_review` with:
49
49
  ```
50
50
 
51
51
  ### Service Tier (from $ARGUMENTS)
52
- - If user says "fast mode", "fast", or "priority" → set `serviceTier: "fast"` (priority processing, ~2x cost)
53
52
  - If user says "flex", "cheap", or "budget" → set `serviceTier: "flex"` (50% cheaper, slower)
54
- - Otherwiseomit `serviceTier` (uses default tier)
53
+ - If user says "default tier" or "standard tier" set `serviceTier: "default"` (API default)
54
+ - Otherwise → omit `serviceTier` (defaults to `"fast"` — priority processing, ~2x cost)
55
55
 
56
56
  ### Structure your ccOutput:
57
57
 
@@ -18,7 +18,7 @@ Use the `codex_review` MCP tool with `reasoningEffort: "xhigh"` for deeper analy
18
18
  - `workingDir`: current working directory
19
19
  - `ccOutput`: brief summary of recent changes or context
20
20
  - `reasoningEffort`: "xhigh" (this is the key difference from /codex-review)
21
- - `serviceTier`: if user says "fast mode"/"fast"/"priority" → "fast"; if "flex"/"cheap"/"budget" → "flex"; otherwise omit
21
+ - `serviceTier`: if user says "flex"/"cheap"/"budget" → "flex"; if "default tier"/"standard tier" → "default"; otherwise omit (defaults to "fast" — priority processing, ~2x cost)
22
22
  - `focusAreas`: extracted from $ARGUMENTS if it's a known focus area
23
23
  - `customPrompt`: $ARGUMENTS if it's custom text
24
24
 
@@ -58,9 +58,9 @@ Call `multi_review` with:
58
58
  ```
59
59
 
60
60
  ### Service Tier (from $ARGUMENTS, applies to Codex only)
61
- - If user says "fast mode", "fast", or "priority" → set `serviceTier: "fast"`
62
61
  - If user says "flex", "cheap", or "budget" → set `serviceTier: "flex"`
63
- - Otherwiseomit `serviceTier`
62
+ - If user says "default tier" or "standard tier" set `serviceTier: "default"`
63
+ - Otherwise → omit `serviceTier` (defaults to `"fast"` — priority processing, ~2x cost)
64
64
 
65
65
  ### Structure your ccOutput:
66
66
 
@@ -39,7 +39,7 @@ export interface ReviewRequest {
39
39
  customPrompt?: string;
40
40
  /** Reasoning effort level (for models that support it) */
41
41
  reasoningEffort?: ReasoningEffort;
42
- /** Service tier (for models that support it: priority = fast, flex = cheap) */
42
+ /** Service tier (Codex). Omit for the review chain's default 'fast' (priority). Pass 'flex' for cheap/slow or 'default' for the Codex API default tier. */
43
43
  serviceTier?: ServiceTier;
44
44
  /** Review mode: standard finds bugs, adversarial challenges assumptions */
45
45
  reviewMode?: 'standard' | 'adversarial';
@@ -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,12 +94,15 @@ export class CodexAdapter {
94
94
  '-C', workingDir,
95
95
  '-', // Read prompt from stdin
96
96
  ];
97
- if (serviceTier && serviceTier !== 'default') {
98
- args.push('-c', `service_tier=${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
+ if (effectiveTier !== 'default') {
101
+ args.push('-c', `service_tier=${effectiveTier}`);
99
102
  }
100
103
  const decoder = new CodexEventDecoder();
101
104
  const cliStartTime = Date.now();
102
- const tierLabel = serviceTier && serviceTier !== 'default' ? ` [${serviceTier}]` : '';
105
+ const tierLabel = effectiveTier !== 'default' ? ` [${effectiveTier}]` : '';
103
106
  console.error(`[codex] Running with ${reasoningEffort} reasoning${tierLabel}...`);
104
107
  decoder.onProgress = (eventType, detail) => {
105
108
  const elapsed = Math.round((Date.now() - cliStartTime) / 1000);
@@ -111,9 +114,9 @@ export class CodexAdapter {
111
114
  args,
112
115
  cwd: workingDir,
113
116
  stdin: prompt,
114
- inactivityTimeoutMs: INACTIVITY_TIMEOUT_MS[reasoningEffort] || INACTIVITY_TIMEOUT_MS.high,
115
- maxTimeoutMs: MAX_TIMEOUT_MS,
116
- maxBufferSize: MAX_BUFFER_SIZE,
117
+ inactivityTimeoutMs: cfg.inactivityTimeoutMs[reasoningEffort] ?? cfg.inactivityTimeoutMs.high,
118
+ maxTimeoutMs: cfg.maxTimeoutMs,
119
+ maxBufferSize: cfg.maxBufferSize,
117
120
  onLine: (line) => {
118
121
  decoder.processLine(line);
119
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<{
@@ -20,7 +20,7 @@ export const ReviewInputSchema = z.object({
20
20
  ])).optional().describe('Areas to focus the review on'),
21
21
  customPrompt: z.string().optional().describe('Custom instructions for the reviewer'),
22
22
  reasoningEffort: z.enum(['high', 'xhigh']).optional().describe('Codex reasoning effort level (default: high, use xhigh for deeper analysis)'),
23
- serviceTier: z.enum(['default', 'fast', 'flex']).optional().describe('Codex service tier (default: default, fast = priority processing, flex = cheaper/slower)')
23
+ serviceTier: z.enum(['default', 'fast', 'flex']).optional().describe('Codex service tier (default when omitted: fast = priority processing, ~2x cost; flex = 50% cheaper/slower; default = API default tier)')
24
24
  });
25
25
  // =============================================================================
26
26
  // HELPERS
@@ -143,7 +143,7 @@ export const TOOL_DEFINITIONS = {
143
143
  focusAreas: { type: 'array', items: { type: 'string', enum: ['security', 'performance', 'architecture', 'correctness', 'maintainability', 'scalability', 'testing', 'documentation'] }, description: 'Areas to focus the review on' },
144
144
  customPrompt: { type: 'string', description: 'Custom instructions for the reviewer' },
145
145
  reasoningEffort: { type: 'string', enum: ['high', 'xhigh'], description: 'Codex reasoning effort (default: high, use xhigh for deeper analysis)' },
146
- serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: 'Codex service tier (fast = priority processing, flex = cheaper/slower)' }
146
+ serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: 'Codex service tier (omit for fast default; fast = priority ~2x cost, flex = 50% cheaper/slower, default = API default tier)' }
147
147
  },
148
148
  required: ['workingDir', 'ccOutput', 'outputType']
149
149
  }
@@ -192,7 +192,7 @@ export const TOOL_DEFINITIONS = {
192
192
  analyzedFiles: { type: 'array', items: { type: 'string' }, description: 'File paths that CC analyzed' },
193
193
  focusAreas: { type: 'array', items: { type: 'string', enum: ['security', 'performance', 'architecture', 'correctness', 'maintainability', 'scalability', 'testing', 'documentation'] }, description: 'Areas to focus the review on' },
194
194
  customPrompt: { type: 'string', description: 'Custom instructions for standard review + adversarial focus steering' },
195
- serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: 'Codex service tier (fast = priority processing, flex = cheaper/slower). Only applies to Codex.' }
195
+ serviceTier: { type: 'string', enum: ['default', 'fast', 'flex'], description: 'Codex service tier — only applies to Codex. Omit for fast default; fast = priority ~2x cost, flex = 50% cheaper/slower, default = API default tier.' }
196
196
  },
197
197
  required: ['workingDir', 'ccOutput', 'outputType']
198
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-reviewer",
3
- "version": "5.1.1",
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",