@kaitranntt/ccs 7.63.0-dev.1 → 7.63.0-dev.3
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.
|
@@ -183,23 +183,58 @@ function extractDuckDuckGoResults(html, count) {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
function formatStructuredSearchResults(query, providerName, results) {
|
|
186
|
+
const lines = [
|
|
187
|
+
'CCS local WebSearch evidence',
|
|
188
|
+
`Provider: ${providerName}`,
|
|
189
|
+
`Query: "${query}"`,
|
|
190
|
+
`Result count: ${results.length}`,
|
|
191
|
+
'',
|
|
192
|
+
];
|
|
193
|
+
|
|
186
194
|
if (!results.length) {
|
|
187
|
-
|
|
195
|
+
lines.push('No results found.');
|
|
196
|
+
return lines.join('\n');
|
|
188
197
|
}
|
|
189
198
|
|
|
190
|
-
const lines = [`Search results for "${query}" via ${providerName}:`, ''];
|
|
191
199
|
for (const [index, result] of results.entries()) {
|
|
192
200
|
lines.push(`${index + 1}. ${result.title}`);
|
|
193
|
-
lines.push(` ${result.url}`);
|
|
201
|
+
lines.push(` URL: ${result.url}`);
|
|
194
202
|
if (result.description) {
|
|
195
|
-
lines.push(` ${result.description}`);
|
|
203
|
+
lines.push(` Snippet: ${result.description}`);
|
|
196
204
|
}
|
|
197
205
|
lines.push('');
|
|
198
206
|
}
|
|
199
|
-
lines.push('Use these results to answer the user directly.');
|
|
200
207
|
return lines.join('\n');
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
function buildSuccessHookOutput(query, providerName, content) {
|
|
211
|
+
return {
|
|
212
|
+
hookSpecificOutput: {
|
|
213
|
+
hookEventName: 'PreToolUse',
|
|
214
|
+
permissionDecision: 'deny',
|
|
215
|
+
permissionDecisionReason: `CCS already retrieved WebSearch results locally via ${providerName}. Use the provided context instead of calling native WebSearch for "${query}".`,
|
|
216
|
+
additionalContext: content,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildFailureHookOutput(query, errors) {
|
|
222
|
+
const detail = errors.map((entry) => `${entry.provider}: ${entry.error}`).join(' | ');
|
|
223
|
+
return {
|
|
224
|
+
hookSpecificOutput: {
|
|
225
|
+
hookEventName: 'PreToolUse',
|
|
226
|
+
permissionDecision: 'deny',
|
|
227
|
+
permissionDecisionReason: `CCS could not complete local WebSearch for "${query}". Native WebSearch is unavailable for this profile.`,
|
|
228
|
+
additionalContext: `CCS local WebSearch failed for "${query}". Attempted providers: ${detail}`,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function emitHookOutput(output) {
|
|
234
|
+
console.log(JSON.stringify(output));
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
204
239
|
const controller = new AbortController();
|
|
205
240
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -534,34 +569,11 @@ function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
|
534
569
|
}
|
|
535
570
|
|
|
536
571
|
function outputSuccess(query, content, providerName) {
|
|
537
|
-
|
|
538
|
-
decision: 'block',
|
|
539
|
-
reason: `WebSearch handled via ${providerName}`,
|
|
540
|
-
hookSpecificOutput: {
|
|
541
|
-
hookEventName: 'PreToolUse',
|
|
542
|
-
permissionDecision: 'deny',
|
|
543
|
-
permissionDecisionReason: `[WebSearch Result via ${providerName}]\n\nQuery: "${query}"\n\n${content}`,
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
console.log(JSON.stringify(output));
|
|
548
|
-
process.exit(2);
|
|
572
|
+
emitHookOutput(buildSuccessHookOutput(query, providerName, content));
|
|
549
573
|
}
|
|
550
574
|
|
|
551
575
|
function outputAllFailedMessage(query, errors) {
|
|
552
|
-
|
|
553
|
-
const output = {
|
|
554
|
-
decision: 'block',
|
|
555
|
-
reason: 'WebSearch fallback failed',
|
|
556
|
-
hookSpecificOutput: {
|
|
557
|
-
hookEventName: 'PreToolUse',
|
|
558
|
-
permissionDecision: 'deny',
|
|
559
|
-
permissionDecisionReason: `WebSearch could not be completed for "${query}". ${detail}`,
|
|
560
|
-
},
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
console.log(JSON.stringify(output));
|
|
564
|
-
process.exit(2);
|
|
576
|
+
emitHookOutput(buildFailureHookOutput(query, errors));
|
|
565
577
|
}
|
|
566
578
|
|
|
567
579
|
async function processHook(input) {
|
|
@@ -675,6 +687,8 @@ if (require.main === module) {
|
|
|
675
687
|
}
|
|
676
688
|
|
|
677
689
|
module.exports = {
|
|
690
|
+
buildFailureHookOutput,
|
|
691
|
+
buildSuccessHookOutput,
|
|
678
692
|
extractDuckDuckGoResults,
|
|
679
693
|
formatStructuredSearchResults,
|
|
680
694
|
tryExaSearch,
|
package/package.json
CHANGED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const ASSESSMENTS = {
|
|
6
|
+
approved: '✅ APPROVED',
|
|
7
|
+
approved_with_notes: '⚠️ APPROVED WITH NOTES',
|
|
8
|
+
changes_requested: '❌ CHANGES REQUESTED',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SEVERITY_ORDER = ['high', 'medium', 'low'];
|
|
12
|
+
const SEVERITY_HEADERS = {
|
|
13
|
+
high: '### 🔴 High',
|
|
14
|
+
medium: '### 🟡 Medium',
|
|
15
|
+
low: '### 🟢 Low',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function cleanText(value) {
|
|
19
|
+
return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeMarkdownText(value) {
|
|
23
|
+
return cleanText(value).replace(/\\/g, '\\\\').replace(/([`*_{}\[\]<>])/g, '\\$1');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function renderCode(value) {
|
|
27
|
+
const text = cleanText(value);
|
|
28
|
+
const longestFence = Math.max(...[...text.matchAll(/`+/g)].map((match) => match[0].length), 0);
|
|
29
|
+
const fence = '`'.repeat(longestFence + 1);
|
|
30
|
+
return `${fence}${text}${fence}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readExecutionMetadata(executionFile) {
|
|
34
|
+
if (!executionFile || !fs.existsSync(executionFile)) {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const turns = JSON.parse(fs.readFileSync(executionFile, 'utf8'));
|
|
40
|
+
const init = turns.find((turn) => turn?.type === 'system' && turn?.subtype === 'init');
|
|
41
|
+
const result = [...turns].reverse().find((turn) => turn?.type === 'result');
|
|
42
|
+
return {
|
|
43
|
+
runtimeTools: Array.isArray(init?.tools) ? init.tools : [],
|
|
44
|
+
turnsUsed: typeof result?.num_turns === 'number' ? result.num_turns : null,
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeStructuredOutput(raw) {
|
|
52
|
+
if (!raw) {
|
|
53
|
+
return { ok: false, reason: 'missing structured output' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let parsed;
|
|
57
|
+
try {
|
|
58
|
+
parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
59
|
+
} catch {
|
|
60
|
+
return { ok: false, reason: 'structured output is not valid JSON' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
64
|
+
return { ok: false, reason: 'structured output must be an object' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const summary = cleanText(parsed.summary);
|
|
68
|
+
const overallAssessment = cleanText(parsed.overallAssessment);
|
|
69
|
+
const overallRationale = cleanText(parsed.overallRationale);
|
|
70
|
+
const notes = Array.isArray(parsed.notes) ? parsed.notes.map(cleanText).filter(Boolean) : [];
|
|
71
|
+
const findings = Array.isArray(parsed.findings) ? parsed.findings : null;
|
|
72
|
+
|
|
73
|
+
if (!summary || !ASSESSMENTS[overallAssessment] || !overallRationale || findings === null) {
|
|
74
|
+
return { ok: false, reason: 'structured output is missing required review fields' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalizedFindings = [];
|
|
78
|
+
for (const finding of findings) {
|
|
79
|
+
const severity = cleanText(finding?.severity);
|
|
80
|
+
const title = cleanText(finding?.title);
|
|
81
|
+
const file = cleanText(finding?.file);
|
|
82
|
+
const what = cleanText(finding?.what);
|
|
83
|
+
const why = cleanText(finding?.why);
|
|
84
|
+
const fix = cleanText(finding?.fix);
|
|
85
|
+
const line =
|
|
86
|
+
typeof finding?.line === 'number' && Number.isInteger(finding.line) && finding.line > 0
|
|
87
|
+
? finding.line
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
if (!SEVERITY_HEADERS[severity] || !title || !file || !what || !why || !fix) {
|
|
91
|
+
return { ok: false, reason: 'structured output contains an invalid finding' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
normalizedFindings.push({ severity, title, file, line, what, why, fix });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
value: {
|
|
100
|
+
summary,
|
|
101
|
+
findings: normalizedFindings,
|
|
102
|
+
overallAssessment,
|
|
103
|
+
overallRationale,
|
|
104
|
+
notes,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function renderStructuredReview(review, { model }) {
|
|
110
|
+
const lines = ['## Summary', escapeMarkdownText(review.summary), '', '## Findings'];
|
|
111
|
+
|
|
112
|
+
if (review.findings.length === 0) {
|
|
113
|
+
lines.push('No confirmed issues found after reviewing the diff and surrounding code.');
|
|
114
|
+
} else {
|
|
115
|
+
for (const severity of SEVERITY_ORDER) {
|
|
116
|
+
const findings = review.findings.filter((finding) => finding.severity === severity);
|
|
117
|
+
if (findings.length === 0) continue;
|
|
118
|
+
|
|
119
|
+
lines.push(SEVERITY_HEADERS[severity]);
|
|
120
|
+
for (const finding of findings) {
|
|
121
|
+
const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
|
|
122
|
+
lines.push(`- **${renderCode(location)} — ${escapeMarkdownText(finding.title)}**`);
|
|
123
|
+
lines.push(` Problem: ${escapeMarkdownText(finding.what)}`);
|
|
124
|
+
lines.push(` Why it matters: ${escapeMarkdownText(finding.why)}`);
|
|
125
|
+
lines.push(` Suggested fix: ${escapeMarkdownText(finding.fix)}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push('');
|
|
128
|
+
}
|
|
129
|
+
if (lines[lines.length - 1] === '') {
|
|
130
|
+
lines.pop();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (review.notes.length > 0) {
|
|
135
|
+
lines.push('', '## Notes');
|
|
136
|
+
for (const note of review.notes) {
|
|
137
|
+
lines.push(`- ${escapeMarkdownText(note)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
lines.push(
|
|
142
|
+
'',
|
|
143
|
+
'## Overall Assessment',
|
|
144
|
+
`**${ASSESSMENTS[review.overallAssessment]}** — ${escapeMarkdownText(review.overallRationale)}`,
|
|
145
|
+
'',
|
|
146
|
+
`> Reviewed by \`${model}\``
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function renderIncompleteReview({ model, reason, runUrl, runtimeTools, turnsUsed }) {
|
|
153
|
+
const lines = [
|
|
154
|
+
'## AI Review Incomplete',
|
|
155
|
+
'',
|
|
156
|
+
'Claude did not return validated structured review output, so this workflow did not publish raw scratch text.',
|
|
157
|
+
'',
|
|
158
|
+
`- Reason: ${escapeMarkdownText(reason)}`,
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
if (runtimeTools?.length) {
|
|
162
|
+
lines.push(`- Runtime tools: ${runtimeTools.map(renderCode).join(', ')}`);
|
|
163
|
+
}
|
|
164
|
+
if (typeof turnsUsed === 'number') {
|
|
165
|
+
lines.push(`- Turns used: ${turnsUsed}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
lines.push('', `Re-run \`/review\` or inspect [the workflow run](${runUrl}).`, '', `> Reviewed by \`${model}\``);
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function writeReviewFromEnv(env = process.env) {
|
|
173
|
+
const outputFile = env.AI_REVIEW_OUTPUT_FILE || 'pr_review.md';
|
|
174
|
+
const model = env.AI_REVIEW_MODEL || 'unknown-model';
|
|
175
|
+
const runUrl = env.AI_REVIEW_RUN_URL || '#';
|
|
176
|
+
const validation = normalizeStructuredOutput(env.AI_REVIEW_STRUCTURED_OUTPUT);
|
|
177
|
+
const metadata = readExecutionMetadata(env.AI_REVIEW_EXECUTION_FILE);
|
|
178
|
+
const content = validation.ok
|
|
179
|
+
? renderStructuredReview(validation.value, { model })
|
|
180
|
+
: renderIncompleteReview({
|
|
181
|
+
model,
|
|
182
|
+
reason: validation.reason,
|
|
183
|
+
runUrl,
|
|
184
|
+
runtimeTools: metadata.runtimeTools,
|
|
185
|
+
turnsUsed: metadata.turnsUsed,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
189
|
+
fs.writeFileSync(outputFile, `${content}\n`, 'utf8');
|
|
190
|
+
|
|
191
|
+
if (!validation.ok) {
|
|
192
|
+
console.warn(`::warning::AI review output normalization fell back to incomplete comment: ${validation.reason}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { usedFallback: !validation.ok, content };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const isMain =
|
|
199
|
+
process.argv[1] &&
|
|
200
|
+
path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
201
|
+
|
|
202
|
+
if (isMain) {
|
|
203
|
+
writeReviewFromEnv();
|
|
204
|
+
}
|