@mcgrapeng/ccg 3.1.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/CHANGELOG.md +119 -0
- package/LICENSE +21 -0
- package/README.ja.md +220 -0
- package/README.ko.md +219 -0
- package/README.md +220 -0
- package/README.zh-CN.md +219 -0
- package/bin/ccg.js +391 -0
- package/ccg.md +303 -0
- package/ccg.sh +928 -0
- package/package.json +54 -0
- package/scripts/curl-install.sh +97 -0
- package/scripts/install.sh +55 -0
package/bin/ccg.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @mcgrapeng/ccg — Node.js CLI entry.
|
|
4
|
+
*
|
|
5
|
+
* Subcommands:
|
|
6
|
+
* install install /ccg slash command into ~/.claude/commands/
|
|
7
|
+
* uninstall remove the slash command
|
|
8
|
+
* doctor preflight: check Codex CLI, Gemini CLI, GEMINI_API_KEY
|
|
9
|
+
* version print version
|
|
10
|
+
* help show this message
|
|
11
|
+
*
|
|
12
|
+
* The CLI is a thin orchestrator. All review logic lives in the bash core
|
|
13
|
+
* (ccg.sh), which gets installed into ~/.claude/commands/ as a slash command.
|
|
14
|
+
*
|
|
15
|
+
* Source: https://github.com/mcgrapeng/ccg
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
const fs = require("node:fs");
|
|
21
|
+
const path = require("node:path");
|
|
22
|
+
const os = require("node:os");
|
|
23
|
+
const { spawnSync, execFileSync } = require("node:child_process");
|
|
24
|
+
|
|
25
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
26
|
+
const PKG = require(path.join(PKG_ROOT, "package.json"));
|
|
27
|
+
|
|
28
|
+
const CCG_SH = path.join(PKG_ROOT, "ccg.sh");
|
|
29
|
+
const CCG_MD = path.join(PKG_ROOT, "ccg.md");
|
|
30
|
+
const TARGET_DIR = path.join(os.homedir(), ".claude", "commands");
|
|
31
|
+
|
|
32
|
+
// ──────────────────────────────────────────────────────────────
|
|
33
|
+
// pretty helpers
|
|
34
|
+
// ──────────────────────────────────────────────────────────────
|
|
35
|
+
const TTY = process.stdout.isTTY;
|
|
36
|
+
const C = {
|
|
37
|
+
reset: TTY ? "\x1b[0m" : "",
|
|
38
|
+
dim: TTY ? "\x1b[2m" : "",
|
|
39
|
+
bold: TTY ? "\x1b[1m" : "",
|
|
40
|
+
green: TTY ? "\x1b[32m" : "",
|
|
41
|
+
red: TTY ? "\x1b[31m" : "",
|
|
42
|
+
yellow: TTY ? "\x1b[33m" : "",
|
|
43
|
+
cyan: TTY ? "\x1b[36m" : "",
|
|
44
|
+
};
|
|
45
|
+
const ok = (m) => console.log(`${C.green}✓${C.reset} ${m}`);
|
|
46
|
+
const warn = (m) => console.log(`${C.yellow}!${C.reset} ${m}`);
|
|
47
|
+
const fail = (m) => console.error(`${C.red}✗${C.reset} ${m}`);
|
|
48
|
+
const head = (m) => console.log(`\n${C.bold}${m}${C.reset}`);
|
|
49
|
+
|
|
50
|
+
// ──────────────────────────────────────────────────────────────
|
|
51
|
+
// commands
|
|
52
|
+
// ──────────────────────────────────────────────────────────────
|
|
53
|
+
function which(cmd) {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync(process.platform === "win32" ? "where" : "command", ["-v", cmd], {
|
|
56
|
+
stdio: "ignore",
|
|
57
|
+
shell: true,
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
} catch {
|
|
61
|
+
// Fallback for systems where `command -v` cannot be located via execFile
|
|
62
|
+
const r = spawnSync("sh", ["-c", `command -v "${cmd}" >/dev/null 2>&1`]);
|
|
63
|
+
return r.status === 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cmdInstall() {
|
|
68
|
+
head("Installing /ccg slash command for Claude Code");
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(CCG_SH) || !fs.existsSync(CCG_MD)) {
|
|
71
|
+
fail("Package payload missing (ccg.sh / ccg.md). Reinstall the npm package.");
|
|
72
|
+
process.exit(2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(TARGET_DIR, { recursive: true });
|
|
76
|
+
fs.copyFileSync(CCG_SH, path.join(TARGET_DIR, "ccg.sh"));
|
|
77
|
+
fs.chmodSync(path.join(TARGET_DIR, "ccg.sh"), 0o755);
|
|
78
|
+
fs.copyFileSync(CCG_MD, path.join(TARGET_DIR, "ccg.md"));
|
|
79
|
+
fs.chmodSync(path.join(TARGET_DIR, "ccg.md"), 0o644);
|
|
80
|
+
|
|
81
|
+
ok(`wrote ${path.join(TARGET_DIR, "ccg.sh")}`);
|
|
82
|
+
ok(`wrote ${path.join(TARGET_DIR, "ccg.md")}`);
|
|
83
|
+
|
|
84
|
+
cmdDoctor({ silent: false, exitOnFail: false });
|
|
85
|
+
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(`${C.cyan}Next:${C.reset} open Claude Code and type ${C.bold}/ccg${C.reset} on a diff.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function cmdUninstall() {
|
|
91
|
+
head("Uninstalling /ccg slash command");
|
|
92
|
+
let removed = 0;
|
|
93
|
+
for (const f of ["ccg.sh", "ccg.md"]) {
|
|
94
|
+
const p = path.join(TARGET_DIR, f);
|
|
95
|
+
if (fs.existsSync(p)) {
|
|
96
|
+
fs.unlinkSync(p);
|
|
97
|
+
ok(`removed ${p}`);
|
|
98
|
+
removed += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (removed === 0) warn("Nothing to remove (slash command was not installed).");
|
|
102
|
+
console.log(`\n${C.dim}User data (cache / ledger) was NOT touched — they live under XDG paths.${C.reset}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cmdDoctor({ silent = false, exitOnFail = true } = {}) {
|
|
106
|
+
if (!silent) head("Preflight checks");
|
|
107
|
+
|
|
108
|
+
const hasCodex = which("codex");
|
|
109
|
+
const hasGemini = which("gemini");
|
|
110
|
+
const hasGeminiKey = Boolean(
|
|
111
|
+
process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY.trim(),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const results = [
|
|
115
|
+
{
|
|
116
|
+
label: "Codex CLI (codex)",
|
|
117
|
+
pass: hasCodex,
|
|
118
|
+
hint: "npm i -g @openai/codex",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
label: "Gemini CLI (gemini)",
|
|
122
|
+
pass: hasGemini,
|
|
123
|
+
hint: "npm i -g @google/gemini-cli",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
label: "GEMINI_API_KEY environment variable",
|
|
127
|
+
pass: hasGeminiKey,
|
|
128
|
+
hint: 'put `export GEMINI_API_KEY="..."` into ~/.zshenv (works in non-interactive shells)',
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
let anyFail = false;
|
|
133
|
+
for (const r of results) {
|
|
134
|
+
if (r.pass) ok(r.label);
|
|
135
|
+
else {
|
|
136
|
+
warn(`${r.label} ${C.dim}→ ${r.hint}${C.reset}`);
|
|
137
|
+
anyFail = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!silent) {
|
|
142
|
+
if (!anyFail) {
|
|
143
|
+
console.log(`\n${C.green}All checks passed.${C.reset} /ccg is ready.`);
|
|
144
|
+
} else if (hasCodex || hasGemini) {
|
|
145
|
+
console.log(
|
|
146
|
+
`\n${C.yellow}/ccg can run in degraded single-source mode.${C.reset} Install the missing tool for full divergence detection.`,
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
console.log(
|
|
150
|
+
`\n${C.red}/ccg cannot run yet — install at least one of Codex/Gemini and ensure GEMINI_API_KEY for full features.${C.reset}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (exitOnFail && anyFail) process.exitCode = 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ──────────────────────────────────────────────────────────────
|
|
159
|
+
// `ccg about` — 7-layer capability probe
|
|
160
|
+
//
|
|
161
|
+
// Shows what ccg can do in this environment, not what the README claims.
|
|
162
|
+
// Each layer is independently usable; layers below answer real engineering
|
|
163
|
+
// problems even if you never touch the divergence-detection layer above.
|
|
164
|
+
// ──────────────────────────────────────────────────────────────
|
|
165
|
+
function cmdAbout() {
|
|
166
|
+
const xdg = {
|
|
167
|
+
config: process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config"),
|
|
168
|
+
cache: process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"),
|
|
169
|
+
data: process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local/share"),
|
|
170
|
+
};
|
|
171
|
+
const cacheDir = path.join(xdg.cache, "ccg", "cache");
|
|
172
|
+
const dataDir = path.join(xdg.data, "ccg");
|
|
173
|
+
const ledger = path.join(dataDir, "ledger.jsonl");
|
|
174
|
+
const usageLog = path.join(dataDir, "usage.log");
|
|
175
|
+
|
|
176
|
+
const hasCodex = which("codex");
|
|
177
|
+
const hasGemini = which("gemini");
|
|
178
|
+
const hasGeminiKey = Boolean(
|
|
179
|
+
process.env.GEMINI_API_KEY && process.env.GEMINI_API_KEY.trim(),
|
|
180
|
+
);
|
|
181
|
+
const hasGit = which("git");
|
|
182
|
+
const installedSlash =
|
|
183
|
+
fs.existsSync(path.join(TARGET_DIR, "ccg.sh")) &&
|
|
184
|
+
fs.existsSync(path.join(TARGET_DIR, "ccg.md"));
|
|
185
|
+
const fileExists = (p) => {
|
|
186
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
187
|
+
};
|
|
188
|
+
const countLines = (p) => {
|
|
189
|
+
try {
|
|
190
|
+
return fs
|
|
191
|
+
.readFileSync(p, "utf8")
|
|
192
|
+
.split("\n")
|
|
193
|
+
.filter((line) => line.trim().length > 0).length;
|
|
194
|
+
} catch { return 0; }
|
|
195
|
+
};
|
|
196
|
+
const dirSize = (p) => {
|
|
197
|
+
try {
|
|
198
|
+
let total = 0;
|
|
199
|
+
for (const entry of fs.readdirSync(p, { withFileTypes: true })) {
|
|
200
|
+
const full = path.join(p, entry.name);
|
|
201
|
+
if (entry.isFile()) total += fs.statSync(full).size;
|
|
202
|
+
}
|
|
203
|
+
return total;
|
|
204
|
+
} catch { return 0; }
|
|
205
|
+
};
|
|
206
|
+
const fmtSize = (b) =>
|
|
207
|
+
b < 1024 ? `${b}B` : b < 1024 ** 2 ? `${(b / 1024).toFixed(1)}KB` : `${(b / 1024 ** 2).toFixed(1)}MB`;
|
|
208
|
+
|
|
209
|
+
const ledgerEntries = countLines(ledger);
|
|
210
|
+
const usageEntries = countLines(usageLog);
|
|
211
|
+
const cacheSize = dirSize(cacheDir);
|
|
212
|
+
|
|
213
|
+
// ── header ────────────────────────────────────────────
|
|
214
|
+
console.log(`${C.bold}@mcgrapeng/ccg${C.reset} v${PKG.version}`);
|
|
215
|
+
console.log(`${C.dim}A production-grade orchestrator for using Codex + Gemini CLI from inside Claude Code.${C.reset}`);
|
|
216
|
+
console.log("");
|
|
217
|
+
console.log(`Source: ${PKG.repository.url.replace(/^git\+/, "").replace(/\.git$/, "")}`);
|
|
218
|
+
console.log("");
|
|
219
|
+
|
|
220
|
+
// ── 7-layer capability matrix ─────────────────────────
|
|
221
|
+
console.log(`${C.bold}7 layers of capability${C.reset}`);
|
|
222
|
+
console.log(`${C.dim}(Each layer is useful on its own. The famous "divergence detection" is just L7.)${C.reset}`);
|
|
223
|
+
console.log("");
|
|
224
|
+
|
|
225
|
+
const layers = [
|
|
226
|
+
{
|
|
227
|
+
id: "L1",
|
|
228
|
+
name: "Safe CLI scheduling",
|
|
229
|
+
solves: "timeouts / stdin preservation / secret redaction / cleanup safety",
|
|
230
|
+
status: "always-on",
|
|
231
|
+
detail: `cleanup, mktemp 700, 7-pattern redaction (sk-/AIza/Bearer/JWT/ghp_/AKIA/Slack)`,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "L2",
|
|
235
|
+
name: "Content-addressed cache",
|
|
236
|
+
solves: "stop paying for the same prompt twice during debugging",
|
|
237
|
+
status: process.env.CCG_NO_CACHE === "1" ? "disabled (CCG_NO_CACHE=1)" : "always-on",
|
|
238
|
+
detail: `SHA-256 prompt + model key, 24h TTL, failed calls NOT cached. Current: ${fmtSize(cacheSize)} at ${cacheDir}`,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "L3",
|
|
242
|
+
name: "Smart diff capture",
|
|
243
|
+
solves: "captures changes even after you've committed them",
|
|
244
|
+
status: hasGit ? "ready" : "git missing",
|
|
245
|
+
detail: "4-level fallback: worktree → staged → upstream → origin-head (reports CCG_DIFF_SOURCE)",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "L4",
|
|
249
|
+
name: "Usage tracking & cost telemetry",
|
|
250
|
+
solves: "see how much you spend per month / per model",
|
|
251
|
+
status: "always-on",
|
|
252
|
+
detail: `${usageEntries} call(s) logged at ${usageLog}. Run: ${C.cyan}source ccg.sh && ccg_usage --this-month${C.reset}`,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
id: "L5",
|
|
256
|
+
name: "Risk-aware auto routing",
|
|
257
|
+
solves: "auto-pick cost/balanced/quality based on path+content+size",
|
|
258
|
+
status: "always-on",
|
|
259
|
+
detail: "Pure rule scoring (no LLM). Path matches auth/payment → +25..+40; SQL+interp → +30; docs only → -40",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: "L6",
|
|
263
|
+
name: "Review ledger",
|
|
264
|
+
solves: "stateless AI can't tell you 'what did the model say last time on src/auth.ts?'",
|
|
265
|
+
status: "always-on",
|
|
266
|
+
detail: `${ledgerEntries} review(s) logged at ${ledger}. Query: ${C.cyan}ccg_ledger_query "src/auth"${C.reset}`,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: "L7",
|
|
270
|
+
name: "Divergence detection (synthesis)",
|
|
271
|
+
solves: "single-model review can't see its own blind spots",
|
|
272
|
+
status:
|
|
273
|
+
hasCodex && hasGemini && hasGeminiKey
|
|
274
|
+
? "full (Codex + Gemini)"
|
|
275
|
+
: hasCodex || (hasGemini && hasGeminiKey)
|
|
276
|
+
? "degraded (single source)"
|
|
277
|
+
: "unavailable",
|
|
278
|
+
detail: "Codex + Gemini → same prompt → Claude surfaces AGREEMENT / DIVERGENCE / BLINDSPOT",
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const layer of layers) {
|
|
283
|
+
const isFull =
|
|
284
|
+
layer.status.includes("ready") ||
|
|
285
|
+
layer.status === "always-on" ||
|
|
286
|
+
layer.status.startsWith("full");
|
|
287
|
+
const isDegraded = layer.status.startsWith("degraded") || layer.status.includes("disabled");
|
|
288
|
+
const isFail =
|
|
289
|
+
layer.status.includes("missing") || layer.status.includes("unavailable");
|
|
290
|
+
|
|
291
|
+
const dot = isFull
|
|
292
|
+
? `${C.green}●${C.reset}`
|
|
293
|
+
: isDegraded
|
|
294
|
+
? `${C.yellow}●${C.reset}`
|
|
295
|
+
: isFail
|
|
296
|
+
? `${C.red}●${C.reset}`
|
|
297
|
+
: `${C.dim}●${C.reset}`;
|
|
298
|
+
console.log(` ${dot} ${C.bold}${layer.id}${C.reset} ${layer.name} ${C.dim}— ${layer.status}${C.reset}`);
|
|
299
|
+
console.log(` ${C.dim}why:${C.reset} ${layer.solves}`);
|
|
300
|
+
console.log(` ${C.dim}how:${C.reset} ${layer.detail}`);
|
|
301
|
+
console.log("");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── environment ─────────────────────────────────────────
|
|
305
|
+
console.log(`${C.bold}Environment${C.reset}`);
|
|
306
|
+
const envRow = (label, status, hint) => {
|
|
307
|
+
const dot = status ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
|
|
308
|
+
console.log(` ${dot} ${label}${status ? "" : ` ${C.dim}→ ${hint}${C.reset}`}`);
|
|
309
|
+
};
|
|
310
|
+
envRow("Claude Code slash command installed", installedSlash, `run: ${C.cyan}ccg install${C.reset}`);
|
|
311
|
+
envRow("Codex CLI", hasCodex, "npm i -g @openai/codex");
|
|
312
|
+
envRow("Gemini CLI", hasGemini, "npm i -g @google/gemini-cli");
|
|
313
|
+
envRow("GEMINI_API_KEY env var", hasGeminiKey, 'add to ~/.zshenv: export GEMINI_API_KEY="..."');
|
|
314
|
+
envRow("git available", hasGit, "install git for diff capture");
|
|
315
|
+
console.log("");
|
|
316
|
+
|
|
317
|
+
// ── storage ─────────────────────────────────────────────
|
|
318
|
+
console.log(`${C.bold}Storage (XDG)${C.reset}`);
|
|
319
|
+
console.log(` config: ${path.join(xdg.config, "ccg")}/${fileExists(path.join(xdg.config, "ccg/config.json")) ? "" : ` ${C.dim}(no config.json yet)${C.reset}`}`);
|
|
320
|
+
console.log(` cache: ${cacheDir} ${C.dim}(${fmtSize(cacheSize)})${C.reset}`);
|
|
321
|
+
console.log(` data: ${dataDir} ${C.dim}(ledger=${ledgerEntries}, usage=${usageEntries})${C.reset}`);
|
|
322
|
+
console.log("");
|
|
323
|
+
|
|
324
|
+
// ── quick reference ────────────────────────────────────
|
|
325
|
+
console.log(`${C.bold}Common commands${C.reset}`);
|
|
326
|
+
console.log(` ${C.cyan}/ccg${C.reset} ${C.dim}(inside Claude Code) full divergence review on current diff${C.reset}`);
|
|
327
|
+
console.log(` ${C.cyan}source ~/.claude/commands/ccg.sh${C.reset}`);
|
|
328
|
+
console.log(` ${C.cyan}ccg_usage --this-month${C.reset} ${C.dim}month-to-date cost${C.reset}`);
|
|
329
|
+
console.log(` ${C.cyan}ccg_ledger_query "path/fragment"${C.reset} ${C.dim}past reviews touching a path${C.reset}`);
|
|
330
|
+
console.log(` ${C.cyan}ccg_risk_score <diff_file>${C.reset} ${C.dim}preview routing decision${C.reset}`);
|
|
331
|
+
console.log("");
|
|
332
|
+
console.log(` ${C.cyan}ccg doctor${C.reset} ${C.dim}re-check environment${C.reset}`);
|
|
333
|
+
console.log(` ${C.cyan}ccg uninstall${C.reset} ${C.dim}remove slash command (user data preserved)${C.reset}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function cmdVersion() {
|
|
337
|
+
console.log(PKG.version);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function cmdHelp() {
|
|
341
|
+
console.log(`${C.bold}@mcgrapeng/ccg${C.reset} v${PKG.version} — Code Divergence Detector
|
|
342
|
+
|
|
343
|
+
${C.bold}Usage${C.reset}
|
|
344
|
+
npx @mcgrapeng/ccg install Install /ccg into ~/.claude/commands/
|
|
345
|
+
npx @mcgrapeng/ccg uninstall Remove the slash command
|
|
346
|
+
npx @mcgrapeng/ccg about What can ccg do? 7-layer capability probe
|
|
347
|
+
npx @mcgrapeng/ccg doctor Verify Codex / Gemini / API key
|
|
348
|
+
npx @mcgrapeng/ccg version Print version
|
|
349
|
+
npx @mcgrapeng/ccg help Show this message
|
|
350
|
+
|
|
351
|
+
${C.bold}After install${C.reset}
|
|
352
|
+
Open Claude Code → type ${C.bold}/ccg${C.reset} on a diff.
|
|
353
|
+
|
|
354
|
+
${C.bold}Source${C.reset} ${PKG.repository.url.replace(/^git\+/, "").replace(/\.git$/, "")}
|
|
355
|
+
${C.bold}License${C.reset} ${PKG.license}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ──────────────────────────────────────────────────────────────
|
|
359
|
+
// dispatch
|
|
360
|
+
// ──────────────────────────────────────────────────────────────
|
|
361
|
+
const [, , subcommand = "help", ...rest] = process.argv;
|
|
362
|
+
const dispatch = {
|
|
363
|
+
install: cmdInstall,
|
|
364
|
+
uninstall: cmdUninstall,
|
|
365
|
+
remove: cmdUninstall,
|
|
366
|
+
doctor: cmdDoctor,
|
|
367
|
+
preflight: cmdDoctor,
|
|
368
|
+
about: cmdAbout,
|
|
369
|
+
capabilities: cmdAbout,
|
|
370
|
+
caps: cmdAbout,
|
|
371
|
+
version: cmdVersion,
|
|
372
|
+
"-v": cmdVersion,
|
|
373
|
+
"--version": cmdVersion,
|
|
374
|
+
help: cmdHelp,
|
|
375
|
+
"-h": cmdHelp,
|
|
376
|
+
"--help": cmdHelp,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const action = dispatch[subcommand];
|
|
380
|
+
if (!action) {
|
|
381
|
+
fail(`Unknown subcommand: ${subcommand}`);
|
|
382
|
+
cmdHelp();
|
|
383
|
+
process.exit(2);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
action(rest);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
fail(err.message || String(err));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
package/ccg.md
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Code-divergence detector. Run Codex+Gemini in parallel on a diff, then surface where they DISAGREE (high-signal) vs where they agree. Auto-picks risk-aware mode. Logs each review to ledger. See README.md.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# CCG v3 — 代码分歧检测器
|
|
6
|
+
|
|
7
|
+
**身份**:不是 review 工具,是**分歧检测器**。两个独立模型家族(Codex/Gemini)评审同一份 diff,Claude 综合时把"两边都说没事 / 两边都说有问题"压成低优先级,把"两边判断不一致的点"放到聚光灯下——这才是真正值得人类介入的地方。
|
|
8
|
+
|
|
9
|
+
**默认行为**:无参数 → 抓 git diff → 风险打分 → 自动选 mode → 并行评审 → 输出 AGREEMENT / DIVERGENCE / BLINDSPOT 三段 → 落 ledger。
|
|
10
|
+
|
|
11
|
+
## 三柱设计
|
|
12
|
+
|
|
13
|
+
| 柱 | 解决的问题 | 实现 |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Divergence Engine | 单源 review 看不到"自己看不到的盲区" | 同 prompt 双投喂 + Claude 综合时强调分歧 |
|
|
16
|
+
| Risk-Aware Routing | 用户不应该手选 cost/balanced/quality | `ccg_risk_score` 基于路径/内容/规模规则打分自动选 |
|
|
17
|
+
| Review Ledger | 这次评审的判断下次没法复用 | 每次评审追加 JSONL,按路径可查历史 |
|
|
18
|
+
|
|
19
|
+
## 配置 (环境变量)
|
|
20
|
+
|
|
21
|
+
| 变量 | 默认 | 说明 |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `CCG_MODE` | `auto` | `auto` (按 risk score 决) / `cost` / `balanced` / `quality` |
|
|
24
|
+
| `CCG_CODEX_MODEL` | — | 显式 codex 模型(优先于 CCG_MODE) |
|
|
25
|
+
| `CCG_GEMINI_MODEL` | — | 显式 gemini 模型 |
|
|
26
|
+
| `CCG_CODEX_TIMEOUT` | `240` | Codex 超时秒数 |
|
|
27
|
+
| `CCG_GEMINI_TIMEOUT` | `120` | Gemini 超时秒数 |
|
|
28
|
+
| `CCG_NO_CACHE` | `0` | `1` = 跳过 24h prompt-hash 缓存 |
|
|
29
|
+
| `CCG_CACHE_TTL_HOURS` | `24` | 缓存 TTL |
|
|
30
|
+
| `CCG_MAX_PROMPT_KB` | `100` | 防止把整个 repo 塞进 prompt |
|
|
31
|
+
| `CCG_KEEP_ARTIFACTS` | `0` | `1` = 保留临时文件 |
|
|
32
|
+
| `CCG_LEDGER_LOG` | `$XDG_DATA_HOME/ccg/ledger.jsonl` | 评审历史落盘位置(fallback `~/.local/share/ccg/`,自动迁移老路径 `~/.ccg/`) |
|
|
33
|
+
|
|
34
|
+
### Mode → 默认模型映射
|
|
35
|
+
|
|
36
|
+
| Mode | Codex | Gemini |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `cost` | gpt-5-nano | gemini-2.5-flash-lite |
|
|
39
|
+
| `balanced` (默认) | gpt-5-mini | gemini-2.5-flash |
|
|
40
|
+
| `quality` | gpt-5 | gemini-2.5-pro |
|
|
41
|
+
|
|
42
|
+
> ⚠️ 第三方代理若不支持上述模型名,会 5xx/404。用 `CCG_CODEX_MODEL`/`CCG_GEMINI_MODEL` 显式指定代理实际支持的型号。
|
|
43
|
+
|
|
44
|
+
## 前置依赖
|
|
45
|
+
|
|
46
|
+
- `npm i -g @openai/codex` `npm i -g @google/gemini-cli`
|
|
47
|
+
- `export GEMINI_API_KEY=...` 放在 `~/.zshenv`(非交互 shell 也能读到)
|
|
48
|
+
|
|
49
|
+
## 执行协议(Claude 严格按以下步骤)
|
|
50
|
+
|
|
51
|
+
### 步骤 0. 初始化工作目录
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
source ~/.claude/commands/ccg.sh
|
|
55
|
+
ccg_init
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**⚠️ 跨 Bash 调用 env vars 不持久。把输出的字面 `CCG_DIR=...` 路径记住**,后续每个 Bash 调用开头先 `CCG_DIR=<字面路径>` 重建。
|
|
59
|
+
|
|
60
|
+
### 步骤 1. 预检
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
source ~/.claude/commands/ccg.sh
|
|
64
|
+
ccg_preflight
|
|
65
|
+
```
|
|
66
|
+
- `CCG_PREFLIGHT_CODEX=missing` → 说明缺哪个 CLI + 安装命令
|
|
67
|
+
- `CCG_PREFLIGHT_GEMINI=no-api-key` → 标注 Gemini 不可用,跳过
|
|
68
|
+
- 两者都不可用 → Claude 独立回答
|
|
69
|
+
|
|
70
|
+
### 步骤 2. 确定任务输入(两种模式)
|
|
71
|
+
|
|
72
|
+
**A. 用户给了参数**(如 `/ccg 评审 src/auth.ts`):直接用用户输入作为 prompt。跳过 risk_score(让用户隐式选 mode 或用环境变量)。
|
|
73
|
+
|
|
74
|
+
**B. 用户没给参数**(裸 `/ccg`):**自动评审 git diff**:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
CCG_DIR=<字面路径>
|
|
78
|
+
source ~/.claude/commands/ccg.sh
|
|
79
|
+
ccg_diff_capture "$CCG_DIR/diff.txt"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- `CCG_DIFF_OK` + `CCG_DIFF_SOURCE=worktree` → 工作区脏改动
|
|
83
|
+
- `CCG_DIFF_SOURCE=staged` → 已 git add 但未 commit
|
|
84
|
+
- `CCG_DIFF_SOURCE=upstream:<branch>` → 已 commit 但未 push(或本地领先上游)
|
|
85
|
+
- `CCG_DIFF_SOURCE=origin-head` → 没有上游时,对比 origin/HEAD
|
|
86
|
+
- `CCG_DIFF_FAIL=not-a-git-repo` → 提示用户在 git 仓库下使用,或显式给参数
|
|
87
|
+
- `CCG_DIFF_FAIL=empty-diff` → 提示用户 "工作区干净 + 分支已对齐上游,没东西要评审"
|
|
88
|
+
|
|
89
|
+
**Claude 必须在最终输出中显示对比的是哪个 source,让用户知道评审的范围。**
|
|
90
|
+
|
|
91
|
+
### 步骤 3. 风险打分 + 自动选 mode(仅 B 模式)
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
CCG_DIR=<字面路径>
|
|
95
|
+
source ~/.claude/commands/ccg.sh
|
|
96
|
+
ccg_risk_score "$CCG_DIR/diff.txt" | tee "$CCG_DIR/risk.txt"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
读取 `CCG_RISK_MODE`:
|
|
100
|
+
- 用户已设 `CCG_MODE` 非空非 auto → 尊重用户选择
|
|
101
|
+
- 否则用 risk_score 给的 mode:`export CCG_MODE=<推荐值>`
|
|
102
|
+
|
|
103
|
+
> 设计立场:路径/内容/规模规则比 LLM 判断更**可解释、零成本、可改**。社区贡献者可以直接 PR 改权重。
|
|
104
|
+
|
|
105
|
+
### 步骤 4. 写 prompt(结构化输出协议)
|
|
106
|
+
|
|
107
|
+
**核心改动**:要求两端都按下面格式输出(便于 Claude 后续做对齐和分歧检测)。
|
|
108
|
+
|
|
109
|
+
Prompt 模板:
|
|
110
|
+
|
|
111
|
+
````
|
|
112
|
+
你是代码评审者。仔细评审下面这段 diff,按 *严格* 的输出格式:
|
|
113
|
+
|
|
114
|
+
===FINDINGS===
|
|
115
|
+
[FINDING]
|
|
116
|
+
file: <path>:<line>
|
|
117
|
+
severity: critical|high|medium|low|nit
|
|
118
|
+
category: bug|security|perf|readability|test|other
|
|
119
|
+
title: <一行)
|
|
120
|
+
detail: <2-4 行解释,必须可独立读懂>
|
|
121
|
+
[/FINDING]
|
|
122
|
+
(每个发现一个 [FINDING]…[/FINDING] 块;按 severity 倒序)
|
|
123
|
+
|
|
124
|
+
===VERDICT===
|
|
125
|
+
<3-5 行:整体判断 + 是否阻塞合并>
|
|
126
|
+
|
|
127
|
+
===END===
|
|
128
|
+
|
|
129
|
+
不要寒暄,不要在外面加任何文字。如果没有发现问题,FINDINGS 段落留空,但格式仍要保留。
|
|
130
|
+
|
|
131
|
+
待评审的 diff(source: <CCG_DIFF_SOURCE>):
|
|
132
|
+
|
|
133
|
+
<贴入 diff 内容>
|
|
134
|
+
````
|
|
135
|
+
|
|
136
|
+
用 **Write tool**(不是 echo)写入:
|
|
137
|
+
- `<CCG_DIR>/codex.prompt`
|
|
138
|
+
- `<CCG_DIR>/gemini.prompt`
|
|
139
|
+
|
|
140
|
+
两份 prompt 内容**完全相同**。让两个独立大脑产生差异——这正是分歧检测的来源。
|
|
141
|
+
|
|
142
|
+
### 步骤 5. 并行调用 CLI(单消息两个 Bash 调用)
|
|
143
|
+
|
|
144
|
+
helper 内部:检查大小 → 查缓存(命中则 $0)→ 真打 API → 落 cache + usage.log。
|
|
145
|
+
|
|
146
|
+
**Codex** (timeout 260000):
|
|
147
|
+
```bash
|
|
148
|
+
CCG_DIR=<字面路径>
|
|
149
|
+
source ~/.claude/commands/ccg.sh
|
|
150
|
+
ccg_codex "$CCG_DIR/codex.prompt" "$CCG_DIR/codex.result"
|
|
151
|
+
echo "---ANSWER---"
|
|
152
|
+
cat "$CCG_DIR/codex.result"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Gemini** (timeout 140000):
|
|
156
|
+
```bash
|
|
157
|
+
CCG_DIR=<字面路径>
|
|
158
|
+
source ~/.claude/commands/ccg.sh
|
|
159
|
+
ccg_gemini "$CCG_DIR/gemini.prompt" "$CCG_DIR/gemini.result"
|
|
160
|
+
echo "---ANSWER---"
|
|
161
|
+
cat "$CCG_DIR/gemini.result"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**两个 Bash 调用必须在同一条 assistant message 内发出**,才能真并行。
|
|
165
|
+
|
|
166
|
+
### 步骤 6. 实际成本
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
CCG_DIR=<字面路径>
|
|
170
|
+
source ~/.claude/commands/ccg.sh
|
|
171
|
+
ccg_actual "$CCG_DIR/codex.prompt" "$CCG_DIR/codex.result" codex
|
|
172
|
+
ccg_actual "$CCG_DIR/gemini.prompt" "$CCG_DIR/gemini.result" gemini
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 步骤 7. 健康判定 + 综合输出(**Pillar 1 关键**)
|
|
176
|
+
|
|
177
|
+
| Codex | Gemini | 路径 |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| OK | OK | 完整三段输出(AGREEMENT / DIVERGENCE / BLINDSPOT)|
|
|
180
|
+
| OK | FAIL | 只能输出 Codex 视角,标注分歧无法验证 |
|
|
181
|
+
| FAIL | OK | 只能输出 Gemini 视角,标注分歧无法验证 |
|
|
182
|
+
| FAIL | FAIL | Claude 独立回答 + 标注顾问不可用 |
|
|
183
|
+
|
|
184
|
+
**Claude 的综合规则(必须严格遵守):**
|
|
185
|
+
|
|
186
|
+
1. **解析两边的 [FINDING] 块**,按 `(file, line, category, title 关键字)` 做对齐。Levenshtein 不重要,类别 + 文件 + 大致位置一致即视为同一发现。
|
|
187
|
+
|
|
188
|
+
2. **AGREEMENT**:两边都报告了的发现。**降级展示**——通常这种问题 Claude 自己也能发现,新增信息量低。一句话带过即可,**不要展开**。
|
|
189
|
+
|
|
190
|
+
3. **DIVERGENCE**:核心。**每条都展开**,必须包含:
|
|
191
|
+
- 一方说什么、另一方说什么(或没说)
|
|
192
|
+
- Claude 的判断:哪边更可能对?为什么?
|
|
193
|
+
- 用户行动建议(接受 / 驳回 / 需要人决定)
|
|
194
|
+
- 如果 Claude 也判断不了,**明确说"NEEDS HUMAN DECISION"**——这是工具最有价值的输出,不要装得自己都懂。
|
|
195
|
+
|
|
196
|
+
4. **BLINDSPOT**:Claude 综合时怀疑两边都漏掉的点。慎用,每次最多 1-2 条,不要为了凑数硬挤。
|
|
197
|
+
|
|
198
|
+
5. **VERDICT**:merge / fix / discuss 三选一,给一句话理由。
|
|
199
|
+
|
|
200
|
+
### 步骤 8. 综合输出模板(**严格按此格式**)
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
## CCG v3 综合结果
|
|
204
|
+
|
|
205
|
+
📍 评审范围:<source 标签,如 worktree | staged | upstream:origin/main>
|
|
206
|
+
🎯 模式:<mode>(risk score: <分数> | 触发: <reasons>)
|
|
207
|
+
🩺 顾问状态:Codex ✓ | Gemini ✓
|
|
208
|
+
💰 本次成本:Codex $0.0023 + Gemini $0.0008 = **$0.0031**
|
|
209
|
+
|
|
210
|
+
═══ AGREEMENT (N) ═══
|
|
211
|
+
两边都指出,新增信息量低:
|
|
212
|
+
- file:line — 简短描述(不展开)
|
|
213
|
+
- ...
|
|
214
|
+
|
|
215
|
+
═══ DIVERGENCE (M) ═══ ★ 这一段是 ccg 的核心价值 ★
|
|
216
|
+
|
|
217
|
+
▸ DIV#1 — file:line
|
|
218
|
+
🔵 Codex: <Codex 的判断>
|
|
219
|
+
🟢 Gemini: <Gemini 的判断 / 没提到>
|
|
220
|
+
⚖️ Claude 综合: <哪边更可信,为什么>
|
|
221
|
+
➡️ 建议: <accept Codex / accept Gemini / NEEDS HUMAN DECISION>
|
|
222
|
+
|
|
223
|
+
▸ DIV#2 — ...
|
|
224
|
+
|
|
225
|
+
═══ BLINDSPOT (≤2) ═══
|
|
226
|
+
两边都没提但 Claude 怀疑:
|
|
227
|
+
- ...(如果不确定就不写)
|
|
228
|
+
|
|
229
|
+
═══ VERDICT ═══
|
|
230
|
+
<merge | fix-required | discuss>
|
|
231
|
+
<一句话理由>
|
|
232
|
+
|
|
233
|
+
═══ 来源原文(折叠展示)═══
|
|
234
|
+
🔵 Codex (gpt-5-mini): <VERDICT 段原样>
|
|
235
|
+
🟢 Gemini (gemini-2.5-flash): <VERDICT 段原样>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**关键纪律:**
|
|
239
|
+
- AGREEMENT 段越短越好;DIVERGENCE 段越展开越好。这反映 ccg 的产品立场。
|
|
240
|
+
- 任何"NEEDS HUMAN DECISION"的 DIVERGENCE 必须明确 — 这是 ccg 的核心价值信号
|
|
241
|
+
- 失败原因摘要给用户(helper 已脱敏 API key/URL)
|
|
242
|
+
- 用户没问怎么修,不给修复代码
|
|
243
|
+
|
|
244
|
+
### 步骤 9. 落 ledger
|
|
245
|
+
|
|
246
|
+
把上面综合输出的核心部分写到 `$CCG_DIR/synthesis.txt`(前 400 字符即可,ledger 自截断),然后:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
CCG_DIR=<字面路径>
|
|
250
|
+
source ~/.claude/commands/ccg.sh
|
|
251
|
+
ccg_ledger_record "$CCG_DIR"
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### 步骤 10. 清理
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
CCG_DIR=<字面路径>
|
|
258
|
+
source ~/.claude/commands/ccg.sh
|
|
259
|
+
ccg_cleanup "$CCG_DIR"
|
|
260
|
+
```
|
|
261
|
+
`CCG_KEEP_ARTIFACTS=1` 时跳过(调试用)。
|
|
262
|
+
|
|
263
|
+
## 用量与历史查询(用户主动触发)
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
source ~/.claude/commands/ccg.sh
|
|
267
|
+
ccg_usage --this-month # 本月成本
|
|
268
|
+
ccg_usage --all # 全部
|
|
269
|
+
ccg_usage --since=2026-05 # 自指定时间起
|
|
270
|
+
|
|
271
|
+
ccg_ledger_query # 最近 5 条评审
|
|
272
|
+
ccg_ledger_query "src/auth.ts" # 这个文件历史评审过几次
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## 故障排除
|
|
276
|
+
|
|
277
|
+
| 症状 | 原因 | 解决 |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| `CCG_PREFLIGHT_CODEX=missing` | 没装 Codex CLI | `npm i -g @openai/codex` |
|
|
280
|
+
| `CCG_PREFLIGHT_GEMINI=missing` | 没装 Gemini CLI | `npm i -g @google/gemini-cli` |
|
|
281
|
+
| `CCG_PREFLIGHT_GEMINI=no-api-key` | 任何 shell 模式拿不到 `GEMINI_API_KEY` | 写到 `~/.zshenv` |
|
|
282
|
+
| `CCG_DIFF_FAIL=not-a-git-repo` | 不在 git 仓库 | cd 进仓库;或显式给参数 |
|
|
283
|
+
| `CCG_DIFF_FAIL=empty-diff` | 工作区干净 + 分支与上游对齐 | 改点东西;或显式给参数;或对比指定 ref |
|
|
284
|
+
| `CCG_*_FAIL=prompt-too-large-Nb-max-Mb` | prompt 超过 100KB | 缩小范围,或 `CCG_MAX_PROMPT_KB=500` |
|
|
285
|
+
| `CCG_*_FAIL=Model ... not registered / 503` | 代理不支持当前模型 | 显式 `CCG_CODEX_MODEL=<代理支持的型号>` |
|
|
286
|
+
| `CCG_GEMINI_FAIL=error-leaked-to-stdout` | 代理把错误写到 stdout | 检查 `$CCG_DIR/gemini.err`(需 `CCG_KEEP_ARTIFACTS=1`) |
|
|
287
|
+
| `CCG_*_FAIL=timeout-Ns` | CLI 超时 | 调大 `CCG_*_TIMEOUT` |
|
|
288
|
+
|
|
289
|
+
## 已知设计取舍
|
|
290
|
+
|
|
291
|
+
- **Divergence over consensus**:放弃"给一份完整 review 报告",转向"标记需要人裁决的点"。AGREEMENT 段意识形态上**故意降级**——单源 Claude 也能发现,无新增信号
|
|
292
|
+
- **同 prompt 双投喂**:训练数据差异自然产生多样性,比拍脑袋分工(codex=arch、gemini=ux)更可靠
|
|
293
|
+
- **24h 缓存**:调试同段代码反复跑时省 90% 费用;改了代码 prompt hash 自然变了,无需手动失效
|
|
294
|
+
- **prompt 大小硬限**:100KB ≈ 32k token,比 codex/gemini context window 小一个数量级,防止把 repo 塞进去
|
|
295
|
+
- **风险打分纯规则**:可解释、零成本、社区可改。LLM 自我打分会跟主评审产生循环
|
|
296
|
+
- **Ledger JSONL**:append-only、grep-able。前 50 次看不出价值,长期是 stateless 工具复制不来的护城河
|
|
297
|
+
- **失败不入缓存**:FAIL 的调用既不入 cache 也不入 usage.log($0 失败不该污染历史)
|
|
298
|
+
|
|
299
|
+
## 注意事项
|
|
300
|
+
|
|
301
|
+
- 不要在 prompt 中泄露密钥(helper 会脱敏 stderr 和 ledger,但不脱敏 stdin)
|
|
302
|
+
- Codex exec 在沙箱中运行,不会修改本地文件
|
|
303
|
+
- 价格估算 ±15%(基于字符数 / 3.0 启发式);要精确请用 tiktoken
|