@j0hanz/code-assistant 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +437 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +72 -0
- package/dist/lib/concurrency.d.ts +13 -0
- package/dist/lib/concurrency.js +77 -0
- package/dist/lib/config.d.ts +43 -0
- package/dist/lib/config.js +87 -0
- package/dist/lib/diff.d.ts +49 -0
- package/dist/lib/diff.js +241 -0
- package/dist/lib/errors.d.ts +8 -0
- package/dist/lib/errors.js +69 -0
- package/dist/lib/format.d.ts +14 -0
- package/dist/lib/format.js +33 -0
- package/dist/lib/gemini.d.ts +45 -0
- package/dist/lib/gemini.js +833 -0
- package/dist/lib/progress.d.ts +72 -0
- package/dist/lib/progress.js +204 -0
- package/dist/lib/tools.d.ts +274 -0
- package/dist/lib/tools.js +646 -0
- package/dist/prompts/index.d.ts +11 -0
- package/dist/prompts/index.js +96 -0
- package/dist/resources/index.d.ts +12 -0
- package/dist/resources/index.js +115 -0
- package/dist/resources/instructions.d.ts +1 -0
- package/dist/resources/instructions.js +71 -0
- package/dist/resources/server-config.d.ts +1 -0
- package/dist/resources/server-config.js +75 -0
- package/dist/resources/tool-catalog.d.ts +1 -0
- package/dist/resources/tool-catalog.js +30 -0
- package/dist/resources/tool-info.d.ts +5 -0
- package/dist/resources/tool-info.js +105 -0
- package/dist/resources/workflows.d.ts +1 -0
- package/dist/resources/workflows.js +59 -0
- package/dist/schemas/inputs.d.ts +21 -0
- package/dist/schemas/inputs.js +46 -0
- package/dist/schemas/outputs.d.ts +121 -0
- package/dist/schemas/outputs.js +162 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +88 -0
- package/dist/tools/analyze-complexity.d.ts +2 -0
- package/dist/tools/analyze-complexity.js +50 -0
- package/dist/tools/analyze-pr-impact.d.ts +2 -0
- package/dist/tools/analyze-pr-impact.js +62 -0
- package/dist/tools/detect-api-breaking.d.ts +2 -0
- package/dist/tools/detect-api-breaking.js +49 -0
- package/dist/tools/generate-diff.d.ts +2 -0
- package/dist/tools/generate-diff.js +140 -0
- package/dist/tools/generate-review-summary.d.ts +2 -0
- package/dist/tools/generate-review-summary.js +71 -0
- package/dist/tools/generate-test-plan.d.ts +2 -0
- package/dist/tools/generate-test-plan.js +67 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +19 -0
- package/package.json +79 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
function parsePositiveInteger(value) {
|
|
2
|
+
const normalized = value.trim();
|
|
3
|
+
if (normalized.length === 0) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
7
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
8
|
+
}
|
|
9
|
+
function resolveEnvInt(envVar, defaultValue) {
|
|
10
|
+
const envValue = process.env[envVar] ?? '';
|
|
11
|
+
return parsePositiveInteger(envValue) ?? defaultValue;
|
|
12
|
+
}
|
|
13
|
+
/** Creates a cached integer value from an environment variable, with a default fallback. */
|
|
14
|
+
export function createCachedEnvInt(envVar, defaultValue) {
|
|
15
|
+
let cached;
|
|
16
|
+
return {
|
|
17
|
+
get() {
|
|
18
|
+
if (cached !== undefined) {
|
|
19
|
+
return cached;
|
|
20
|
+
}
|
|
21
|
+
cached = resolveEnvInt(envVar, defaultValue);
|
|
22
|
+
return cached;
|
|
23
|
+
},
|
|
24
|
+
reset() {
|
|
25
|
+
cached = undefined;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Fast, cost-effective model for summarization and light analysis. */
|
|
30
|
+
export const FLASH_MODEL = 'gemini-3-flash-preview';
|
|
31
|
+
/** Default language hint. */
|
|
32
|
+
export const DEFAULT_LANGUAGE = 'detect';
|
|
33
|
+
/** Default test-framework hint. */
|
|
34
|
+
export const DEFAULT_FRAMEWORK = 'detect';
|
|
35
|
+
/** Extended timeout for deep analysis calls (ms). */
|
|
36
|
+
export const DEFAULT_TIMEOUT_EXTENDED_MS = 120_000;
|
|
37
|
+
export const MODEL_TIMEOUT_MS = Object.freeze({
|
|
38
|
+
extended: DEFAULT_TIMEOUT_EXTENDED_MS,
|
|
39
|
+
});
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Budgets (Thinking & Output)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
const THINKING_LEVELS = {
|
|
44
|
+
/** Minimal thinking for triage/classification. */
|
|
45
|
+
flashTriage: 'minimal',
|
|
46
|
+
/** Medium thinking for analysis tasks. */
|
|
47
|
+
flash: 'medium',
|
|
48
|
+
/** High thinking for deep review and patches. */
|
|
49
|
+
flashHigh: 'high',
|
|
50
|
+
};
|
|
51
|
+
/** Thinking level for Flash triage. */
|
|
52
|
+
export const FLASH_TRIAGE_THINKING_LEVEL = THINKING_LEVELS.flashTriage;
|
|
53
|
+
/** Thinking level for Flash analysis. */
|
|
54
|
+
export const FLASH_THINKING_LEVEL = THINKING_LEVELS.flash;
|
|
55
|
+
/** Thinking level for Flash deep analysis. */
|
|
56
|
+
export const FLASH_HIGH_THINKING_LEVEL = THINKING_LEVELS.flashHigh;
|
|
57
|
+
// Output token caps for various tools. Set to a high default to avoid cutting off important information, but can be adjusted as needed.
|
|
58
|
+
const DEFAULT_OUTPUT_CAP = 65_536;
|
|
59
|
+
/** Output cap for Flash API breaking-change detection. */
|
|
60
|
+
export const FLASH_API_BREAKING_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
61
|
+
/** Output cap for Flash complexity analysis. */
|
|
62
|
+
export const FLASH_COMPLEXITY_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
63
|
+
/** Output cap for Flash test-plan generation. */
|
|
64
|
+
export const FLASH_TEST_PLAN_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
65
|
+
/** Output cap for Flash triage tools. */
|
|
66
|
+
export const FLASH_TRIAGE_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
67
|
+
/** Output cap for Flash patch generation. */
|
|
68
|
+
export const FLASH_PATCH_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
69
|
+
/** Output cap for Flash deep review findings. */
|
|
70
|
+
export const FLASH_REVIEW_MAX_OUTPUT_TOKENS = DEFAULT_OUTPUT_CAP;
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Temperatures
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
const TOOL_TEMPERATURE = {
|
|
75
|
+
analysis: 1.0, // Gemini 3 recommends 1.0 for all tasks
|
|
76
|
+
creative: 1.0, // Gemini 3 recommends 1.0 for all tasks
|
|
77
|
+
patch: 1.0, // Gemini 3 recommends 1.0 for all tasks
|
|
78
|
+
triage: 1.0, // Gemini 3 recommends 1.0 for all tasks
|
|
79
|
+
};
|
|
80
|
+
/** Temperature for analytical tools. */
|
|
81
|
+
export const ANALYSIS_TEMPERATURE = TOOL_TEMPERATURE.analysis;
|
|
82
|
+
/** Temperature for creative synthesis (test plans). */
|
|
83
|
+
export const CREATIVE_TEMPERATURE = TOOL_TEMPERATURE.creative;
|
|
84
|
+
/** Temperature for code patch generation. */
|
|
85
|
+
export const PATCH_TEMPERATURE = TOOL_TEMPERATURE.patch;
|
|
86
|
+
/** Temperature for triage/classification tools. */
|
|
87
|
+
export const TRIAGE_TEMPERATURE = TOOL_TEMPERATURE.triage;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { File as ParsedFile } from 'parse-diff';
|
|
3
|
+
import { createErrorToolResponse } from './tools.js';
|
|
4
|
+
export type { ParsedFile };
|
|
5
|
+
export declare function getMaxDiffChars(): number;
|
|
6
|
+
export declare function resetMaxDiffCharsCacheForTesting(): void;
|
|
7
|
+
export declare function exceedsDiffBudget(diff: string): boolean;
|
|
8
|
+
export declare function getDiffBudgetError(diffLength: number, maxChars?: number): string;
|
|
9
|
+
export declare function validateDiffBudget(diff: string): ReturnType<typeof createErrorToolResponse> | undefined;
|
|
10
|
+
export declare const NOISY_EXCLUDE_PATHSPECS: readonly [":(exclude)package-lock.json", ":(exclude)yarn.lock", ":(exclude)pnpm-lock.yaml", ":(exclude)bun.lockb", ":(exclude)*.lock", ":(exclude)dist/", ":(exclude)build/", ":(exclude)out/", ":(exclude).next/", ":(exclude)coverage/", ":(exclude)*.min.js", ":(exclude)*.min.css", ":(exclude)*.map"];
|
|
11
|
+
export declare function cleanDiff(raw: string): string;
|
|
12
|
+
export declare function isEmptyDiff(diff: string): boolean;
|
|
13
|
+
export declare const EMPTY_DIFF_STATS: Readonly<DiffStats>;
|
|
14
|
+
export interface DiffStats {
|
|
15
|
+
files: number;
|
|
16
|
+
added: number;
|
|
17
|
+
deleted: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseDiffFiles(diff: string): ParsedFile[];
|
|
20
|
+
export declare function computeDiffStatsAndSummaryFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
21
|
+
stats: DiffStats;
|
|
22
|
+
summary: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function computeDiffStatsAndPathsFromFiles(files: readonly ParsedFile[]): Readonly<{
|
|
25
|
+
stats: DiffStats;
|
|
26
|
+
paths: string[];
|
|
27
|
+
}>;
|
|
28
|
+
export declare function extractChangedPathsFromFiles(files: readonly ParsedFile[]): string[];
|
|
29
|
+
export declare function extractChangedPaths(diff: string): string[];
|
|
30
|
+
export declare function computeDiffStatsFromFiles(files: readonly ParsedFile[]): Readonly<DiffStats>;
|
|
31
|
+
export declare function computeDiffStats(diff: string): Readonly<DiffStats>;
|
|
32
|
+
export declare function formatFileSummary(files: ParsedFile[]): string;
|
|
33
|
+
export declare const DIFF_RESOURCE_URI = "diff://current";
|
|
34
|
+
export declare const diffStaleWarningMs: import("./config.js").CachedEnvInt;
|
|
35
|
+
export interface DiffSlot {
|
|
36
|
+
diff: string;
|
|
37
|
+
parsedFiles: readonly ParsedFile[];
|
|
38
|
+
stats: DiffStats;
|
|
39
|
+
generatedAt: string;
|
|
40
|
+
mode: string;
|
|
41
|
+
}
|
|
42
|
+
/** Binds diff resource notifications to the currently active server instance. */
|
|
43
|
+
export declare function initDiffStore(server: McpServer): void;
|
|
44
|
+
export declare function storeDiff(data: DiffSlot, key?: string): void;
|
|
45
|
+
export declare function getDiff(key?: string): DiffSlot | undefined;
|
|
46
|
+
export declare function hasDiff(key?: string): boolean;
|
|
47
|
+
/** Test-only: directly set or clear the diff slot without emitting resource-updated. */
|
|
48
|
+
export declare function setDiffForTesting(data: DiffSlot | undefined, key?: string): void;
|
|
49
|
+
export declare function createNoDiffError(): ReturnType<typeof createErrorToolResponse>;
|
package/dist/lib/diff.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import parseDiff from 'parse-diff';
|
|
2
|
+
import { createCachedEnvInt } from './config.js';
|
|
3
|
+
import { formatUsNumber } from './format.js';
|
|
4
|
+
import { createErrorToolResponse } from './tools.js';
|
|
5
|
+
// --- Diff Budget ---
|
|
6
|
+
const DEFAULT_MAX_DIFF_CHARS = 120_000;
|
|
7
|
+
const MAX_DIFF_CHARS_ENV_VAR = 'MAX_DIFF_CHARS';
|
|
8
|
+
const diffCharsConfig = createCachedEnvInt(MAX_DIFF_CHARS_ENV_VAR, DEFAULT_MAX_DIFF_CHARS);
|
|
9
|
+
export function getMaxDiffChars() {
|
|
10
|
+
return diffCharsConfig.get();
|
|
11
|
+
}
|
|
12
|
+
export function resetMaxDiffCharsCacheForTesting() {
|
|
13
|
+
diffCharsConfig.reset();
|
|
14
|
+
}
|
|
15
|
+
export function exceedsDiffBudget(diff) {
|
|
16
|
+
return diff.length > getMaxDiffChars();
|
|
17
|
+
}
|
|
18
|
+
export function getDiffBudgetError(diffLength, maxChars = getMaxDiffChars()) {
|
|
19
|
+
return `diff exceeds max allowed size (${formatUsNumber(diffLength)} chars > ${formatUsNumber(maxChars)} chars)`;
|
|
20
|
+
}
|
|
21
|
+
const BUDGET_ERROR_META = { retryable: false, kind: 'budget' };
|
|
22
|
+
export function validateDiffBudget(diff) {
|
|
23
|
+
const providedChars = diff.length;
|
|
24
|
+
const maxChars = getMaxDiffChars();
|
|
25
|
+
if (providedChars <= maxChars) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return createErrorToolResponse('E_INPUT_TOO_LARGE', getDiffBudgetError(providedChars, maxChars), { providedChars, maxChars }, BUDGET_ERROR_META);
|
|
29
|
+
}
|
|
30
|
+
// --- Diff Cleaner ---
|
|
31
|
+
export const NOISY_EXCLUDE_PATHSPECS = [
|
|
32
|
+
':(exclude)package-lock.json',
|
|
33
|
+
':(exclude)yarn.lock',
|
|
34
|
+
':(exclude)pnpm-lock.yaml',
|
|
35
|
+
':(exclude)bun.lockb',
|
|
36
|
+
':(exclude)*.lock',
|
|
37
|
+
':(exclude)dist/',
|
|
38
|
+
':(exclude)build/',
|
|
39
|
+
':(exclude)out/',
|
|
40
|
+
':(exclude).next/',
|
|
41
|
+
':(exclude)coverage/',
|
|
42
|
+
':(exclude)*.min.js',
|
|
43
|
+
':(exclude)*.min.css',
|
|
44
|
+
':(exclude)*.map',
|
|
45
|
+
];
|
|
46
|
+
const BINARY_FILE_LINE = /^Binary files .+ differ$/m;
|
|
47
|
+
const GIT_BINARY_PATCH = /^GIT binary patch/m;
|
|
48
|
+
const HAS_HUNK = /^@@/m;
|
|
49
|
+
const HAS_OLD_MODE = /^old mode /m;
|
|
50
|
+
function shouldKeepSection(section) {
|
|
51
|
+
return (Boolean(section.trim()) &&
|
|
52
|
+
!BINARY_FILE_LINE.test(section) &&
|
|
53
|
+
!GIT_BINARY_PATCH.test(section) &&
|
|
54
|
+
(!HAS_OLD_MODE.test(section) || HAS_HUNK.test(section)));
|
|
55
|
+
}
|
|
56
|
+
function processSection(raw, start, end, sections) {
|
|
57
|
+
if (end > start) {
|
|
58
|
+
const section = raw.slice(start, end);
|
|
59
|
+
if (shouldKeepSection(section)) {
|
|
60
|
+
sections.push(section);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function extractAllSections(raw, sections, firstIndex) {
|
|
65
|
+
let lastIndex = 0;
|
|
66
|
+
let nextIndex = firstIndex;
|
|
67
|
+
while (nextIndex !== -1) {
|
|
68
|
+
const matchIndex = nextIndex === 0 ? 0 : nextIndex + 1; // +1 to skip \n
|
|
69
|
+
processSection(raw, lastIndex, matchIndex, sections);
|
|
70
|
+
lastIndex = matchIndex;
|
|
71
|
+
nextIndex = raw.indexOf('\ndiff --git ', lastIndex);
|
|
72
|
+
}
|
|
73
|
+
processSection(raw, lastIndex, raw.length, sections);
|
|
74
|
+
}
|
|
75
|
+
export function cleanDiff(raw) {
|
|
76
|
+
if (!raw)
|
|
77
|
+
return '';
|
|
78
|
+
const sections = [];
|
|
79
|
+
const nextIndex = raw.startsWith('diff --git ')
|
|
80
|
+
? 0
|
|
81
|
+
: raw.indexOf('\ndiff --git ');
|
|
82
|
+
if (nextIndex === -1) {
|
|
83
|
+
processSection(raw, 0, raw.length, sections);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
extractAllSections(raw, sections, nextIndex);
|
|
87
|
+
}
|
|
88
|
+
return sections.join('').trim();
|
|
89
|
+
}
|
|
90
|
+
export function isEmptyDiff(diff) {
|
|
91
|
+
return diff.trim().length === 0;
|
|
92
|
+
}
|
|
93
|
+
// --- Diff Parser ---
|
|
94
|
+
const UNKNOWN_PATH = 'unknown';
|
|
95
|
+
const NO_FILES_CHANGED = 'No files changed.';
|
|
96
|
+
const EMPTY_PATHS = [];
|
|
97
|
+
const MAX_SUMMARY_FILES = 40;
|
|
98
|
+
export const EMPTY_DIFF_STATS = Object.freeze({
|
|
99
|
+
files: 0,
|
|
100
|
+
added: 0,
|
|
101
|
+
deleted: 0,
|
|
102
|
+
});
|
|
103
|
+
const PATH_SORTER = (left, right) => left.localeCompare(right);
|
|
104
|
+
export function parseDiffFiles(diff) {
|
|
105
|
+
return diff ? parseDiff(diff) : [];
|
|
106
|
+
}
|
|
107
|
+
function cleanPath(path) {
|
|
108
|
+
if (path.startsWith('a/') || path.startsWith('b/')) {
|
|
109
|
+
return path.slice(2);
|
|
110
|
+
}
|
|
111
|
+
return path;
|
|
112
|
+
}
|
|
113
|
+
function resolveChangedPath(file) {
|
|
114
|
+
if (file.to && file.to !== '/dev/null')
|
|
115
|
+
return cleanPath(file.to);
|
|
116
|
+
if (file.from && file.from !== '/dev/null')
|
|
117
|
+
return cleanPath(file.from);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
function sortPaths(paths) {
|
|
121
|
+
return Array.from(paths).sort(PATH_SORTER);
|
|
122
|
+
}
|
|
123
|
+
function isNoFiles(files) {
|
|
124
|
+
return files.length === 0;
|
|
125
|
+
}
|
|
126
|
+
function calculateStats(files) {
|
|
127
|
+
let added = 0;
|
|
128
|
+
let deleted = 0;
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
added += file.additions;
|
|
131
|
+
deleted += file.deletions;
|
|
132
|
+
}
|
|
133
|
+
return { files: files.length, added, deleted };
|
|
134
|
+
}
|
|
135
|
+
function getUniquePaths(files) {
|
|
136
|
+
const paths = new Set();
|
|
137
|
+
for (const file of files) {
|
|
138
|
+
const path = resolveChangedPath(file);
|
|
139
|
+
if (path)
|
|
140
|
+
paths.add(path);
|
|
141
|
+
}
|
|
142
|
+
return paths;
|
|
143
|
+
}
|
|
144
|
+
function buildFileSummaryList(files, maxFiles = 40) {
|
|
145
|
+
return files.slice(0, maxFiles).map((file) => {
|
|
146
|
+
const path = resolveChangedPath(file) ?? UNKNOWN_PATH;
|
|
147
|
+
return `${path} (+${Math.max(0, file.additions)} -${Math.max(0, file.deletions)})`;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export function computeDiffStatsAndSummaryFromFiles(files) {
|
|
151
|
+
if (isNoFiles(files)) {
|
|
152
|
+
return { stats: EMPTY_DIFF_STATS, summary: NO_FILES_CHANGED };
|
|
153
|
+
}
|
|
154
|
+
const stats = calculateStats(files);
|
|
155
|
+
const summaries = buildFileSummaryList(files, MAX_SUMMARY_FILES);
|
|
156
|
+
if (files.length > MAX_SUMMARY_FILES) {
|
|
157
|
+
summaries.push(`... and ${files.length - MAX_SUMMARY_FILES} more files`);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
stats,
|
|
161
|
+
summary: `${summaries.join(', ')} [${stats.files} files, +${stats.added} -${Math.abs(stats.deleted)}]`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function computeDiffStatsAndPathsFromFiles(files) {
|
|
165
|
+
if (isNoFiles(files)) {
|
|
166
|
+
return { stats: EMPTY_DIFF_STATS, paths: EMPTY_PATHS };
|
|
167
|
+
}
|
|
168
|
+
const stats = calculateStats(files);
|
|
169
|
+
const paths = sortPaths(getUniquePaths(files));
|
|
170
|
+
return { stats, paths };
|
|
171
|
+
}
|
|
172
|
+
export function extractChangedPathsFromFiles(files) {
|
|
173
|
+
if (isNoFiles(files))
|
|
174
|
+
return EMPTY_PATHS;
|
|
175
|
+
return sortPaths(getUniquePaths(files));
|
|
176
|
+
}
|
|
177
|
+
export function extractChangedPaths(diff) {
|
|
178
|
+
return extractChangedPathsFromFiles(parseDiffFiles(diff));
|
|
179
|
+
}
|
|
180
|
+
export function computeDiffStatsFromFiles(files) {
|
|
181
|
+
if (isNoFiles(files))
|
|
182
|
+
return EMPTY_DIFF_STATS;
|
|
183
|
+
return calculateStats(files);
|
|
184
|
+
}
|
|
185
|
+
export function computeDiffStats(diff) {
|
|
186
|
+
return computeDiffStatsFromFiles(parseDiffFiles(diff));
|
|
187
|
+
}
|
|
188
|
+
export function formatFileSummary(files) {
|
|
189
|
+
return computeDiffStatsAndSummaryFromFiles(files).summary;
|
|
190
|
+
}
|
|
191
|
+
export const DIFF_RESOURCE_URI = 'diff://current';
|
|
192
|
+
const diffCacheTtlMs = createCachedEnvInt('DIFF_CACHE_TTL_MS', 60 * 60 * 1_000 // 1 hour default
|
|
193
|
+
);
|
|
194
|
+
export const diffStaleWarningMs = createCachedEnvInt('DIFF_STALE_WARNING_MS', 5 * 60 * 1_000 // 5 minutes default
|
|
195
|
+
);
|
|
196
|
+
const diffSlots = new Map();
|
|
197
|
+
let sendResourceUpdated;
|
|
198
|
+
function setDiffSlot(key, data) {
|
|
199
|
+
if (data) {
|
|
200
|
+
diffSlots.set(key, data);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
diffSlots.delete(key);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function notifyDiffUpdated() {
|
|
207
|
+
void sendResourceUpdated?.({ uri: DIFF_RESOURCE_URI }).catch(() => {
|
|
208
|
+
// Ignore errors sending resource-updated, which can happen if the server is not fully initialized yet.
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** Binds diff resource notifications to the currently active server instance. */
|
|
212
|
+
export function initDiffStore(server) {
|
|
213
|
+
sendResourceUpdated = (params) => server.server.sendResourceUpdated(params);
|
|
214
|
+
}
|
|
215
|
+
export function storeDiff(data, key = process.cwd()) {
|
|
216
|
+
setDiffSlot(key, data);
|
|
217
|
+
notifyDiffUpdated();
|
|
218
|
+
}
|
|
219
|
+
export function getDiff(key = process.cwd()) {
|
|
220
|
+
const slot = diffSlots.get(key);
|
|
221
|
+
if (!slot) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
const age = Date.now() - new Date(slot.generatedAt).getTime();
|
|
225
|
+
if (age > diffCacheTtlMs.get()) {
|
|
226
|
+
diffSlots.delete(key);
|
|
227
|
+
notifyDiffUpdated();
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
return slot;
|
|
231
|
+
}
|
|
232
|
+
export function hasDiff(key = process.cwd()) {
|
|
233
|
+
return getDiff(key) !== undefined;
|
|
234
|
+
}
|
|
235
|
+
/** Test-only: directly set or clear the diff slot without emitting resource-updated. */
|
|
236
|
+
export function setDiffForTesting(data, key = process.cwd()) {
|
|
237
|
+
setDiffSlot(key, data);
|
|
238
|
+
}
|
|
239
|
+
export function createNoDiffError() {
|
|
240
|
+
return createErrorToolResponse('E_NO_DIFF', 'No diff cached. You must call the generate_diff tool before using any review tool. Run generate_diff with mode="unstaged" or mode="staged" to capture the current branch changes, then retry this tool.', undefined, { retryable: false, kind: 'validation' });
|
|
241
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ErrorMeta } from './tools.js';
|
|
2
|
+
/** Matches transient upstream provider failures that are typically safe to retry. */
|
|
3
|
+
export declare const RETRYABLE_UPSTREAM_ERROR_PATTERN: RegExp;
|
|
4
|
+
export declare function toRecord(value: unknown): Record<string, unknown> | undefined;
|
|
5
|
+
export declare function getErrorMessage(error: unknown): string;
|
|
6
|
+
declare const CANCELLED_ERROR_PATTERN: RegExp;
|
|
7
|
+
export { CANCELLED_ERROR_PATTERN };
|
|
8
|
+
export declare function classifyErrorMeta(error: unknown, message: string): ErrorMeta;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** Matches transient upstream provider failures that are typically safe to retry. */
|
|
4
|
+
export const RETRYABLE_UPSTREAM_ERROR_PATTERN = /(429|500|502|503|504|rate.?limit|quota|overload|unavailable|gateway|timeout|timed.out|connection|reset|econn|enotfound|temporary|transient)/i;
|
|
5
|
+
function isObjectRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null;
|
|
7
|
+
}
|
|
8
|
+
export function toRecord(value) {
|
|
9
|
+
if (!isObjectRecord(value)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function getStringProperty(value, key) {
|
|
15
|
+
if (!isObjectRecord(value) || !(key in value)) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const property = value[key];
|
|
19
|
+
return typeof property === 'string' ? property : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function getErrorMessage(error) {
|
|
22
|
+
const message = getStringProperty(error, 'message');
|
|
23
|
+
if (message !== undefined) {
|
|
24
|
+
return message;
|
|
25
|
+
}
|
|
26
|
+
if (typeof error === 'string') {
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
return inspect(error, { depth: 3, breakLength: 120 });
|
|
30
|
+
}
|
|
31
|
+
const CANCELLED_ERROR_PATTERN = /cancelled|canceled/i;
|
|
32
|
+
const TIMEOUT_ERROR_PATTERN = /timed out|timeout/i;
|
|
33
|
+
const BUDGET_ERROR_PATTERN = /exceeds limit|max allowed size|input too large/i;
|
|
34
|
+
const BUSY_ERROR_PATTERN = /too many concurrent/i;
|
|
35
|
+
const VALIDATION_ERROR_PATTERN = /validation/i;
|
|
36
|
+
export { CANCELLED_ERROR_PATTERN };
|
|
37
|
+
const ERROR_CLASSIFIERS = [
|
|
38
|
+
{
|
|
39
|
+
pattern: CANCELLED_ERROR_PATTERN,
|
|
40
|
+
meta: { kind: 'cancelled', retryable: false },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: TIMEOUT_ERROR_PATTERN,
|
|
44
|
+
meta: { kind: 'timeout', retryable: true },
|
|
45
|
+
},
|
|
46
|
+
{ pattern: BUDGET_ERROR_PATTERN, meta: { kind: 'budget', retryable: false } },
|
|
47
|
+
{ pattern: BUSY_ERROR_PATTERN, meta: { kind: 'busy', retryable: true } },
|
|
48
|
+
{
|
|
49
|
+
pattern: RETRYABLE_UPSTREAM_ERROR_PATTERN,
|
|
50
|
+
meta: { kind: 'upstream', retryable: true },
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
export function classifyErrorMeta(error, message) {
|
|
54
|
+
if (error instanceof z.ZodError || VALIDATION_ERROR_PATTERN.test(message)) {
|
|
55
|
+
return {
|
|
56
|
+
kind: 'validation',
|
|
57
|
+
retryable: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
for (const { pattern, meta } of ERROR_CLASSIFIERS) {
|
|
61
|
+
if (pattern.test(message)) {
|
|
62
|
+
return meta;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
kind: 'internal',
|
|
67
|
+
retryable: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function formatOptionalLine(label: string, value: string | number | undefined): string;
|
|
2
|
+
export interface OptionalLineEntry {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string | number | undefined;
|
|
5
|
+
}
|
|
6
|
+
export declare function formatOptionalLines(entries: readonly OptionalLineEntry[]): string;
|
|
7
|
+
export declare function formatLanguageSegment(language: string | undefined): string;
|
|
8
|
+
export declare function buildLanguageDiffPrompt(language: string | undefined, diff: string, conclusion: string): string;
|
|
9
|
+
export declare function formatCountLabel(count: number, singular: string, plural: string): string;
|
|
10
|
+
export declare function formatUsNumber(value: number): string;
|
|
11
|
+
export declare function formatTimeoutSeconds(timeoutMs: number): string;
|
|
12
|
+
export declare function formatThinkingLevel(thinkingLevel: string | undefined, fallback?: string): string;
|
|
13
|
+
export declare function toBulletedList(lines: readonly string[]): string;
|
|
14
|
+
export declare function toInlineCode(value: string): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function formatOptionalLine(label, value) {
|
|
2
|
+
return value === undefined ? '' : `\n${label}: ${value}`;
|
|
3
|
+
}
|
|
4
|
+
export function formatOptionalLines(entries) {
|
|
5
|
+
return entries
|
|
6
|
+
.map((entry) => formatOptionalLine(entry.label, entry.value))
|
|
7
|
+
.join('');
|
|
8
|
+
}
|
|
9
|
+
export function formatLanguageSegment(language) {
|
|
10
|
+
return formatOptionalLine('Language', language);
|
|
11
|
+
}
|
|
12
|
+
export function buildLanguageDiffPrompt(language, diff, conclusion) {
|
|
13
|
+
return `${formatLanguageSegment(language)}\nDiff:\n${diff}\n\n${conclusion}`.trimStart();
|
|
14
|
+
}
|
|
15
|
+
export function formatCountLabel(count, singular, plural) {
|
|
16
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
17
|
+
}
|
|
18
|
+
const usNumberFormatter = new Intl.NumberFormat('en-US');
|
|
19
|
+
export function formatUsNumber(value) {
|
|
20
|
+
return usNumberFormatter.format(value);
|
|
21
|
+
}
|
|
22
|
+
export function formatTimeoutSeconds(timeoutMs) {
|
|
23
|
+
return `${Math.round(timeoutMs / 1_000)}s`;
|
|
24
|
+
}
|
|
25
|
+
export function formatThinkingLevel(thinkingLevel, fallback = '-') {
|
|
26
|
+
return thinkingLevel ?? fallback;
|
|
27
|
+
}
|
|
28
|
+
export function toBulletedList(lines) {
|
|
29
|
+
return lines.map((line) => `- ${line}`).join('\n');
|
|
30
|
+
}
|
|
31
|
+
export function toInlineCode(value) {
|
|
32
|
+
return `\`${value}\``;
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { GoogleGenAI } from '@google/genai';
|
|
3
|
+
export type JsonObject = Record<string, unknown>;
|
|
4
|
+
export type GeminiLogHandler = (level: string, data: unknown) => Promise<void>;
|
|
5
|
+
export type GeminiThinkingLevel = 'minimal' | 'low' | 'medium' | 'high';
|
|
6
|
+
export interface GeminiRequestExecutionOptions {
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
temperature?: number;
|
|
10
|
+
maxOutputTokens?: number;
|
|
11
|
+
thinkingLevel?: GeminiThinkingLevel;
|
|
12
|
+
includeThoughts?: boolean;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
onLog?: GeminiLogHandler;
|
|
15
|
+
responseKeyOrdering?: readonly string[];
|
|
16
|
+
batchMode?: 'off' | 'inline';
|
|
17
|
+
}
|
|
18
|
+
export interface GeminiStructuredRequestOptions extends GeminiRequestExecutionOptions {
|
|
19
|
+
model?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface GeminiStructuredRequest extends GeminiStructuredRequestOptions {
|
|
22
|
+
systemInstruction?: string;
|
|
23
|
+
prompt: string;
|
|
24
|
+
responseSchema: Readonly<JsonObject>;
|
|
25
|
+
}
|
|
26
|
+
type JsonRecord = Record<string, unknown>;
|
|
27
|
+
export declare function stripJsonSchemaConstraints(schema: JsonRecord): JsonRecord;
|
|
28
|
+
export declare const RETRYABLE_NUMERIC_CODES: Set<number>;
|
|
29
|
+
export declare const RETRYABLE_TRANSIENT_CODES: Set<string>;
|
|
30
|
+
export declare function toUpperStringCode(candidate: unknown): string | undefined;
|
|
31
|
+
export declare function getNumericErrorCode(error: unknown): number | undefined;
|
|
32
|
+
export declare function shouldRetry(error: unknown): boolean;
|
|
33
|
+
export declare function getRetryDelayMs(attempt: number): number;
|
|
34
|
+
export declare function canRetryAttempt(attempt: number, maxRetries: number, error: unknown): boolean;
|
|
35
|
+
export declare const geminiEvents: EventEmitter<[never]>;
|
|
36
|
+
export declare function getCurrentRequestId(): string;
|
|
37
|
+
export declare function setClientForTesting(client: GoogleGenAI): void;
|
|
38
|
+
export declare function getGeminiQueueSnapshot(): {
|
|
39
|
+
activeWaiters: number;
|
|
40
|
+
activeCalls: number;
|
|
41
|
+
activeBatchWaiters: number;
|
|
42
|
+
activeBatchCalls: number;
|
|
43
|
+
};
|
|
44
|
+
export declare function generateStructuredJson(request: GeminiStructuredRequest): Promise<unknown>;
|
|
45
|
+
export {};
|