gitreviewpilot 0.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/LICENSE +22 -0
- package/README.md +234 -0
- package/dist/commands/ask.d.ts +3 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +32 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/changes.d.ts +3 -0
- package/dist/commands/changes.d.ts.map +1 -0
- package/dist/commands/changes.js +57 -0
- package/dist/commands/changes.js.map +1 -0
- package/dist/commands/install.d.ts +3 -0
- package/dist/commands/install.d.ts.map +1 -0
- package/dist/commands/install.js +86 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/pr.d.ts +3 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +53 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/review.d.ts +3 -0
- package/dist/commands/review.d.ts.map +1 -0
- package/dist/commands/review.js +45 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +16 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/config/env.d.ts +3 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +20 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/gitreviewpilotConfig.d.ts +16 -0
- package/dist/config/gitreviewpilotConfig.d.ts.map +1 -0
- package/dist/config/gitreviewpilotConfig.js +78 -0
- package/dist/config/gitreviewpilotConfig.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +56 -0
- package/dist/index.js.map +1 -0
- package/dist/services/ai.service.d.ts +60 -0
- package/dist/services/ai.service.d.ts.map +1 -0
- package/dist/services/ai.service.js +396 -0
- package/dist/services/ai.service.js.map +1 -0
- package/dist/services/askService.d.ts +12 -0
- package/dist/services/askService.d.ts.map +1 -0
- package/dist/services/askService.js +68 -0
- package/dist/services/askService.js.map +1 -0
- package/dist/services/git.service.d.ts +12 -0
- package/dist/services/git.service.d.ts.map +1 -0
- package/dist/services/git.service.js +61 -0
- package/dist/services/git.service.js.map +1 -0
- package/dist/services/github.service.d.ts +46 -0
- package/dist/services/github.service.d.ts.map +1 -0
- package/dist/services/github.service.js +169 -0
- package/dist/services/github.service.js.map +1 -0
- package/dist/services/prReviewService.d.ts +29 -0
- package/dist/services/prReviewService.d.ts.map +1 -0
- package/dist/services/prReviewService.js +107 -0
- package/dist/services/prReviewService.js.map +1 -0
- package/dist/services/prService.d.ts +12 -0
- package/dist/services/prService.d.ts.map +1 -0
- package/dist/services/prService.js +8 -0
- package/dist/services/prService.js.map +1 -0
- package/dist/services/reviewErrors.d.ts +7 -0
- package/dist/services/reviewErrors.d.ts.map +1 -0
- package/dist/services/reviewErrors.js +13 -0
- package/dist/services/reviewErrors.js.map +1 -0
- package/dist/services/reviewService.d.ts +16 -0
- package/dist/services/reviewService.d.ts.map +1 -0
- package/dist/services/reviewService.js +109 -0
- package/dist/services/reviewService.js.map +1 -0
- package/dist/utils/cli.d.ts +16 -0
- package/dist/utils/cli.d.ts.map +1 -0
- package/dist/utils/cli.js +108 -0
- package/dist/utils/cli.js.map +1 -0
- package/dist/utils/diff.d.ts +21 -0
- package/dist/utils/diff.d.ts.map +1 -0
- package/dist/utils/diff.js +105 -0
- package/dist/utils/diff.js.map +1 -0
- package/dist/utils/formatter.d.ts +11 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +186 -0
- package/dist/utils/formatter.js.map +1 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/repoContext.d.ts +26 -0
- package/dist/utils/repoContext.d.ts.map +1 -0
- package/dist/utils/repoContext.js +136 -0
- package/dist/utils/repoContext.js.map +1 -0
- package/dist/utils/spinner.d.ts +3 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +5 -0
- package/dist/utils/spinner.js.map +1 -0
- package/dist/utils/structuredReview.d.ts +21 -0
- package/dist/utils/structuredReview.d.ts.map +1 -0
- package/dist/utils/structuredReview.js +59 -0
- package/dist/utils/structuredReview.js.map +1 -0
- package/gitreviewpilot.config.json +3 -0
- package/package.json +37 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
let loaded = false;
|
|
4
|
+
export function loadEnv() {
|
|
5
|
+
if (loaded)
|
|
6
|
+
return;
|
|
7
|
+
const result = dotenv.config({ quiet: true });
|
|
8
|
+
loaded = true;
|
|
9
|
+
if (result.error) {
|
|
10
|
+
// No .env is a normal case in production; keep it non-fatal.
|
|
11
|
+
logger.debug(`dotenv not loaded: ${result.error.message}`);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
logger.debug("dotenv loaded");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function getEnv(name) {
|
|
18
|
+
return process.env[name];
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/config/env.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,IAAI,MAAM,GAAG,KAAK,CAAC;AAEnB,MAAM,UAAU,OAAO;IACrB,IAAI,MAAM;QAAE,OAAO;IACnB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,MAAM,GAAG,IAAI,CAAC;IAEd,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,6DAA6D;QAC7D,MAAM,CAAC,KAAK,CAAC,sBAAsB,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7D,CAAC;SAAM,CAAC;QACN,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IAChC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY;IACjC,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type GitReviewPilotConfig = {
|
|
2
|
+
/**
|
|
3
|
+
* High-level areas to prioritize in AI output.
|
|
4
|
+
* Treated as an ordered list (earlier = higher priority).
|
|
5
|
+
*/
|
|
6
|
+
focus: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare const DEFAULT_GITREVIEWPILOT_CONFIG: GitReviewPilotConfig;
|
|
9
|
+
export declare function loadConfig(cwd?: string): Promise<GitReviewPilotConfig>;
|
|
10
|
+
/**
|
|
11
|
+
* Returns the cached config if already loaded, otherwise returns defaults.
|
|
12
|
+
* Prefer calling `loadConfig()` during app startup to avoid I/O mid-command.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getConfig(): GitReviewPilotConfig;
|
|
15
|
+
export declare function focusDirectiveForPrompt(defaultFocusText: string): string;
|
|
16
|
+
//# sourceMappingURL=gitreviewpilotConfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitreviewpilotConfig.d.ts","sourceRoot":"","sources":["../../src/config/gitreviewpilotConfig.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,6BAA6B,EAAE,oBAE3C,CAAC;AAwCF,wBAAsB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAuB3F;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,oBAAoB,CAEhD;AAED,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAIxE"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
4
|
+
export const DEFAULT_GITREVIEWPILOT_CONFIG = {
|
|
5
|
+
focus: ["correctness", "security", "performance", "maintainability"]
|
|
6
|
+
};
|
|
7
|
+
function isObject(value) {
|
|
8
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function normalizeStringArray(value) {
|
|
11
|
+
if (!Array.isArray(value))
|
|
12
|
+
return undefined;
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const v of value) {
|
|
15
|
+
if (typeof v !== "string")
|
|
16
|
+
continue;
|
|
17
|
+
const trimmed = v.trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
continue;
|
|
20
|
+
if (!out.includes(trimmed))
|
|
21
|
+
out.push(trimmed);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function parseConfigJson(jsonText) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = JSON.parse(jsonText);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
logger.warn(`Invalid gitreviewpilot.config.json (failed to parse JSON): ${err instanceof Error ? err.message : String(err)}`);
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (!isObject(parsed)) {
|
|
35
|
+
logger.warn("Invalid gitreviewpilot.config.json (expected a JSON object).");
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const focus = normalizeStringArray(parsed.focus) ?? DEFAULT_GITREVIEWPILOT_CONFIG.focus;
|
|
39
|
+
return { ...DEFAULT_GITREVIEWPILOT_CONFIG, focus };
|
|
40
|
+
}
|
|
41
|
+
let cachedConfig;
|
|
42
|
+
export async function loadConfig(cwd = process.cwd()) {
|
|
43
|
+
if (cachedConfig)
|
|
44
|
+
return cachedConfig;
|
|
45
|
+
const configPath = path.join(cwd, "gitreviewpilot.config.json");
|
|
46
|
+
try {
|
|
47
|
+
const text = await readFile(configPath, "utf8");
|
|
48
|
+
const parsed = parseConfigJson(text);
|
|
49
|
+
cachedConfig = parsed ?? DEFAULT_GITREVIEWPILOT_CONFIG;
|
|
50
|
+
logger.debug(`config loaded: ${configPath}`);
|
|
51
|
+
return cachedConfig;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
// Missing config file is normal; default config should apply silently.
|
|
55
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
56
|
+
cachedConfig = DEFAULT_GITREVIEWPILOT_CONFIG;
|
|
57
|
+
logger.debug(`config not found, using defaults: ${configPath}`);
|
|
58
|
+
return cachedConfig;
|
|
59
|
+
}
|
|
60
|
+
cachedConfig = DEFAULT_GITREVIEWPILOT_CONFIG;
|
|
61
|
+
logger.warn(`Failed to load gitreviewpilot.config.json, using defaults: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
return cachedConfig;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Returns the cached config if already loaded, otherwise returns defaults.
|
|
67
|
+
* Prefer calling `loadConfig()` during app startup to avoid I/O mid-command.
|
|
68
|
+
*/
|
|
69
|
+
export function getConfig() {
|
|
70
|
+
return cachedConfig ?? DEFAULT_GITREVIEWPILOT_CONFIG;
|
|
71
|
+
}
|
|
72
|
+
export function focusDirectiveForPrompt(defaultFocusText) {
|
|
73
|
+
const focus = getConfig().focus;
|
|
74
|
+
if (!focus.length)
|
|
75
|
+
return defaultFocusText;
|
|
76
|
+
return `Focus areas (priority order): ${focus.join(", ")}.`;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=gitreviewpilotConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitreviewpilotConfig.js","sourceRoot":"","sources":["../../src/config/gitreviewpilotConfig.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { loadEnv } from "./config/env.js";
|
|
5
|
+
import { loadConfig } from "./config/gitreviewpilotConfig.js";
|
|
6
|
+
import { registerAskCommand } from "./commands/ask.js";
|
|
7
|
+
import { registerPrCommand } from "./commands/pr.js";
|
|
8
|
+
import { registerReviewCommand } from "./commands/review.js";
|
|
9
|
+
import { logger } from "./utils/logger.js";
|
|
10
|
+
import { printUserFacingError } from "./utils/cli.js";
|
|
11
|
+
import { registerVersionCommand } from "./commands/version.js";
|
|
12
|
+
import { registerChangesCommand } from "./commands/changes.js";
|
|
13
|
+
import { registerInstallCommand } from "./commands/install.js";
|
|
14
|
+
loadEnv();
|
|
15
|
+
await loadConfig();
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const pkg = require("../package.json");
|
|
18
|
+
const program = new Command()
|
|
19
|
+
.name("gitreviewpilot")
|
|
20
|
+
.description("GitReviewPilot — review diffs, inspect PRs, and ask questions about your codebase.")
|
|
21
|
+
.version(pkg.version ?? "0.0.0")
|
|
22
|
+
.addHelpText("after", "\nExamples:\n gitreviewpilot review README.md\n gitreviewpilot pr 123 --repo vercel/next.js\n gitreviewpilot changes\n gitreviewpilot ask \"Where is config loaded?\"\n gitreviewpilot version\n");
|
|
23
|
+
registerReviewCommand(program);
|
|
24
|
+
registerPrCommand(program);
|
|
25
|
+
registerAskCommand(program);
|
|
26
|
+
registerChangesCommand(program);
|
|
27
|
+
registerInstallCommand(program);
|
|
28
|
+
registerVersionCommand(program);
|
|
29
|
+
program.configureHelp({
|
|
30
|
+
sortSubcommands: true,
|
|
31
|
+
sortOptions: true
|
|
32
|
+
});
|
|
33
|
+
// Commander prints its own "error: ..." lines. We override stderr output so we can
|
|
34
|
+
// present consistent, friendly errors from a single place.
|
|
35
|
+
const silenceCommanderErrOutput = (cmd) => {
|
|
36
|
+
cmd.configureOutput({ writeErr: () => { } });
|
|
37
|
+
cmd.exitOverride();
|
|
38
|
+
for (const sub of cmd.commands)
|
|
39
|
+
silenceCommanderErrOutput(sub);
|
|
40
|
+
};
|
|
41
|
+
silenceCommanderErrOutput(program);
|
|
42
|
+
program.showHelpAfterError(true);
|
|
43
|
+
program.showSuggestionAfterError(true);
|
|
44
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
45
|
+
// Commander uses exceptions for help output when exitOverride() is enabled.
|
|
46
|
+
// Treat `--help`/help output as success.
|
|
47
|
+
if (err instanceof CommanderError && (err.code === "commander.helpDisplayed" || err.message === "(outputHelp)")) {
|
|
48
|
+
process.exitCode = 0;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
printUserFacingError(err);
|
|
52
|
+
// Still log the raw message for users piping logs via GITREVIEWPILOT_LOG_LEVEL=debug.
|
|
53
|
+
logger.debug(err instanceof Error ? err.stack ?? err.message : String(err));
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type AnalyzeOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Override the default model via code (env still applies).
|
|
4
|
+
* If omitted, uses provider defaults / env.
|
|
5
|
+
*/
|
|
6
|
+
model?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Timeout in milliseconds for the HTTP request.
|
|
9
|
+
* If omitted, uses provider defaults / env.
|
|
10
|
+
*/
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Override API base URL (useful for proxies / local servers).
|
|
14
|
+
*/
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Override API key.
|
|
18
|
+
*/
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Override provider selection.
|
|
22
|
+
*/
|
|
23
|
+
provider?: LlmProvider;
|
|
24
|
+
};
|
|
25
|
+
export type LlmProvider = "openai-compatible" | "gemini" | "anthropic";
|
|
26
|
+
export declare class LlmApiError extends Error {
|
|
27
|
+
readonly provider: LlmProvider;
|
|
28
|
+
readonly status: number | undefined;
|
|
29
|
+
readonly retryAfterMs: number | undefined;
|
|
30
|
+
readonly details: string | undefined;
|
|
31
|
+
constructor(args: {
|
|
32
|
+
provider: LlmProvider;
|
|
33
|
+
message: string;
|
|
34
|
+
status?: number;
|
|
35
|
+
retryAfterMs?: number;
|
|
36
|
+
details?: string;
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export declare function buildPRReviewPrompt(diff: string): string;
|
|
41
|
+
export declare function buildFileReviewPrompt(code: string): string;
|
|
42
|
+
export declare function buildRepoQueryPrompt(context: string, question: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* LLM-agnostic analysis entrypoint.
|
|
45
|
+
*
|
|
46
|
+
* Default provider is "openai-compatible", which works with OpenAI and many
|
|
47
|
+
* OpenAI-compatible servers (e.g. local Ollama / proxies), using env vars:
|
|
48
|
+
*
|
|
49
|
+
* Preferred:
|
|
50
|
+
* - GITREVIEWPILOT_LLM_PROVIDER=openai-compatible
|
|
51
|
+
* - GITREVIEWPILOT_LLM_BASE_URL=https://api.openai.com
|
|
52
|
+
* - GITREVIEWPILOT_LLM_API_KEY=...
|
|
53
|
+
* - GITREVIEWPILOT_LLM_MODEL=...
|
|
54
|
+
* - GITREVIEWPILOT_LLM_TIMEOUT_MS=30000
|
|
55
|
+
*
|
|
56
|
+
* Back-compat (also supported):
|
|
57
|
+
* - OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL / OPENAI_TIMEOUT_MS
|
|
58
|
+
*/
|
|
59
|
+
export declare function analyze(prompt: string, options?: AnalyzeOptions): Promise<string>;
|
|
60
|
+
//# sourceMappingURL=ai.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai.service.d.ts","sourceRoot":"","sources":["../../src/services/ai.service.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,mBAAmB,GAAG,QAAQ,GAAG,WAAW,CAAC;AAQvE,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;gBAEzB,IAAI,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;CASF;AA6FD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcxD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAa1D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiB9E;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CAW3F"}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { focusDirectiveForPrompt } from "../config/gitreviewpilotConfig.js";
|
|
2
|
+
export class LlmApiError extends Error {
|
|
3
|
+
provider;
|
|
4
|
+
status;
|
|
5
|
+
retryAfterMs;
|
|
6
|
+
details;
|
|
7
|
+
constructor(args) {
|
|
8
|
+
super(args.message);
|
|
9
|
+
this.name = "LlmApiError";
|
|
10
|
+
this.provider = args.provider;
|
|
11
|
+
this.status = args.status;
|
|
12
|
+
this.retryAfterMs = args.retryAfterMs;
|
|
13
|
+
this.details = args.details;
|
|
14
|
+
if (args.cause !== undefined)
|
|
15
|
+
this.cause = args.cause;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function parsePositiveInt(value, fallback) {
|
|
19
|
+
if (!value)
|
|
20
|
+
return fallback;
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
|
|
23
|
+
}
|
|
24
|
+
function getEnvFirst(...names) {
|
|
25
|
+
for (const name of names) {
|
|
26
|
+
const value = process.env[name];
|
|
27
|
+
if (value && value.trim())
|
|
28
|
+
return value.trim();
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function normalizeBaseUrl(url) {
|
|
33
|
+
return url.replace(/\/+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
function isLocalhostBaseUrl(url) {
|
|
36
|
+
try {
|
|
37
|
+
const u = new URL(url);
|
|
38
|
+
return u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1";
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function safeJsonParse(text) {
|
|
45
|
+
try {
|
|
46
|
+
return text ? JSON.parse(text) : undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function parseRetryAfterMs(res) {
|
|
53
|
+
const ra = res.headers.get("retry-after");
|
|
54
|
+
if (!ra)
|
|
55
|
+
return undefined;
|
|
56
|
+
const seconds = Number(ra);
|
|
57
|
+
if (Number.isFinite(seconds))
|
|
58
|
+
return Math.max(0, seconds) * 1000;
|
|
59
|
+
const dateMs = Date.parse(ra);
|
|
60
|
+
if (Number.isFinite(dateMs))
|
|
61
|
+
return Math.max(0, dateMs - Date.now());
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
async function fetchTextWithTimeout(input, init, timeoutMs) {
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(input, { ...init, signal: controller.signal });
|
|
69
|
+
const text = await res.text();
|
|
70
|
+
return { res, text };
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
clearTimeout(timeout);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function toTimeoutError(provider, timeoutMs, cause) {
|
|
77
|
+
return new LlmApiError({
|
|
78
|
+
provider,
|
|
79
|
+
message: `LLM request timed out after ${timeoutMs}ms.`,
|
|
80
|
+
cause
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function missingKeyError(provider, hint) {
|
|
84
|
+
return new LlmApiError({
|
|
85
|
+
provider,
|
|
86
|
+
message: `Missing API key. ${hint}`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function withFormatRules(task) {
|
|
90
|
+
return [
|
|
91
|
+
"You are a senior software engineer.",
|
|
92
|
+
"Keep the response concise and actionable.",
|
|
93
|
+
"Follow the exact output format below (use numbered headings):",
|
|
94
|
+
"",
|
|
95
|
+
"1. Critical Issues",
|
|
96
|
+
"2. Improvements",
|
|
97
|
+
"3. Suggestions",
|
|
98
|
+
"4. Summary",
|
|
99
|
+
"",
|
|
100
|
+
task.trim()
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
export function buildPRReviewPrompt(diff) {
|
|
104
|
+
const focusLine = focusDirectiveForPrompt("Focus on correctness, security, performance, and maintainability.");
|
|
105
|
+
return withFormatRules([
|
|
106
|
+
"Review the following pull request diff.",
|
|
107
|
+
focusLine,
|
|
108
|
+
"If something is unknown from the diff, state assumptions briefly.",
|
|
109
|
+
"",
|
|
110
|
+
"Diff:",
|
|
111
|
+
"```diff",
|
|
112
|
+
diff.trim(),
|
|
113
|
+
"```"
|
|
114
|
+
].join("\n"));
|
|
115
|
+
}
|
|
116
|
+
export function buildFileReviewPrompt(code) {
|
|
117
|
+
const focusLine = focusDirectiveForPrompt("Focus on correctness, security, readability, and edge cases.");
|
|
118
|
+
return withFormatRules([
|
|
119
|
+
"Review the following code file.",
|
|
120
|
+
focusLine,
|
|
121
|
+
"",
|
|
122
|
+
"Code:",
|
|
123
|
+
"```",
|
|
124
|
+
code.trim(),
|
|
125
|
+
"```"
|
|
126
|
+
].join("\n"));
|
|
127
|
+
}
|
|
128
|
+
export function buildRepoQueryPrompt(context, question) {
|
|
129
|
+
const focusLine = focusDirectiveForPrompt("Prioritize correctness and clarity, and keep the answer focused on what the context supports.");
|
|
130
|
+
return withFormatRules([
|
|
131
|
+
"Answer the question using only the provided repository context.",
|
|
132
|
+
focusLine,
|
|
133
|
+
"If context is insufficient, say what's missing and propose next steps.",
|
|
134
|
+
"",
|
|
135
|
+
"Context:",
|
|
136
|
+
"```",
|
|
137
|
+
context.trim(),
|
|
138
|
+
"```",
|
|
139
|
+
"",
|
|
140
|
+
"Question:",
|
|
141
|
+
question.trim()
|
|
142
|
+
].join("\n"));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* LLM-agnostic analysis entrypoint.
|
|
146
|
+
*
|
|
147
|
+
* Default provider is "openai-compatible", which works with OpenAI and many
|
|
148
|
+
* OpenAI-compatible servers (e.g. local Ollama / proxies), using env vars:
|
|
149
|
+
*
|
|
150
|
+
* Preferred:
|
|
151
|
+
* - GITREVIEWPILOT_LLM_PROVIDER=openai-compatible
|
|
152
|
+
* - GITREVIEWPILOT_LLM_BASE_URL=https://api.openai.com
|
|
153
|
+
* - GITREVIEWPILOT_LLM_API_KEY=...
|
|
154
|
+
* - GITREVIEWPILOT_LLM_MODEL=...
|
|
155
|
+
* - GITREVIEWPILOT_LLM_TIMEOUT_MS=30000
|
|
156
|
+
*
|
|
157
|
+
* Back-compat (also supported):
|
|
158
|
+
* - OPENAI_BASE_URL / OPENAI_API_KEY / OPENAI_MODEL / OPENAI_TIMEOUT_MS
|
|
159
|
+
*/
|
|
160
|
+
export async function analyze(prompt, options = {}) {
|
|
161
|
+
const provider = options.provider ??
|
|
162
|
+
getEnvFirst("GITREVIEWPILOT_LLM_PROVIDER") ??
|
|
163
|
+
"openai-compatible";
|
|
164
|
+
const client = PROVIDERS[provider];
|
|
165
|
+
if (!client) {
|
|
166
|
+
throw new LlmApiError({ provider, message: `Unsupported LLM provider: ${provider}` });
|
|
167
|
+
}
|
|
168
|
+
return client.analyze(prompt, options);
|
|
169
|
+
}
|
|
170
|
+
class OpenAICompatibleClient {
|
|
171
|
+
defaultBaseUrl() {
|
|
172
|
+
return normalizeBaseUrl(getEnvFirst("GITREVIEWPILOT_LLM_BASE_URL", "OPENAI_BASE_URL") ?? "https://api.openai.com");
|
|
173
|
+
}
|
|
174
|
+
defaultModel() {
|
|
175
|
+
return getEnvFirst("GITREVIEWPILOT_LLM_MODEL", "OPENAI_MODEL") ?? "gpt-4o-mini";
|
|
176
|
+
}
|
|
177
|
+
defaultTimeoutMs() {
|
|
178
|
+
return parsePositiveInt(getEnvFirst("GITREVIEWPILOT_LLM_TIMEOUT_MS", "OPENAI_TIMEOUT_MS"), 30_000);
|
|
179
|
+
}
|
|
180
|
+
resolveApiKey(baseUrl, override) {
|
|
181
|
+
if (override && override.trim())
|
|
182
|
+
return override.trim();
|
|
183
|
+
const key = getEnvFirst("GITREVIEWPILOT_LLM_API_KEY", "OPENAI_API_KEY");
|
|
184
|
+
if (key)
|
|
185
|
+
return key;
|
|
186
|
+
// Allow local OpenAI-compatible servers that don't require a key.
|
|
187
|
+
if (isLocalhostBaseUrl(baseUrl))
|
|
188
|
+
return "ollama";
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
async analyze(prompt, options = {}) {
|
|
192
|
+
const provider = "openai-compatible";
|
|
193
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? this.defaultBaseUrl());
|
|
194
|
+
const model = options.model ?? this.defaultModel();
|
|
195
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs();
|
|
196
|
+
const apiKey = this.resolveApiKey(baseUrl, options.apiKey);
|
|
197
|
+
if (!apiKey) {
|
|
198
|
+
throw missingKeyError(provider, "Set GITREVIEWPILOT_LLM_API_KEY (preferred) or OPENAI_API_KEY, or use a localhost base URL.");
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const { res, text } = await fetchTextWithTimeout(`${baseUrl}/v1/chat/completions`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: {
|
|
204
|
+
Authorization: `Bearer ${apiKey}`,
|
|
205
|
+
"Content-Type": "application/json"
|
|
206
|
+
},
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
model,
|
|
209
|
+
temperature: 0.2,
|
|
210
|
+
messages: [{ role: "user", content: prompt }]
|
|
211
|
+
})
|
|
212
|
+
}, timeoutMs);
|
|
213
|
+
const data = safeJsonParse(text);
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
const retryAfterMs = parseRetryAfterMs(res);
|
|
216
|
+
throw new LlmApiError({
|
|
217
|
+
provider,
|
|
218
|
+
status: res.status,
|
|
219
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
220
|
+
message: `LLM API error (${res.status}): ${data?.error?.message ?? (text ? text.slice(0, 500) : "Request failed")}`,
|
|
221
|
+
...(text ? { details: text } : {})
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
const content = data?.choices?.[0]?.message?.content ?? "";
|
|
225
|
+
const trimmed = content.trim();
|
|
226
|
+
if (!trimmed) {
|
|
227
|
+
throw new LlmApiError({ provider, status: res.status, message: "LLM returned an empty response." });
|
|
228
|
+
}
|
|
229
|
+
return trimmed;
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
if (err instanceof DOMException && err.name === "AbortError")
|
|
233
|
+
throw toTimeoutError(provider, timeoutMs, err);
|
|
234
|
+
if (err instanceof LlmApiError)
|
|
235
|
+
throw err;
|
|
236
|
+
if (err instanceof Error) {
|
|
237
|
+
throw new LlmApiError({ provider, message: err.message, cause: err });
|
|
238
|
+
}
|
|
239
|
+
throw new LlmApiError({ provider, message: String(err) });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
class GeminiClient {
|
|
244
|
+
defaultBaseUrl() {
|
|
245
|
+
return normalizeBaseUrl(getEnvFirst("GITREVIEWPILOT_LLM_BASE_URL", "GEMINI_BASE_URL") ??
|
|
246
|
+
"https://generativelanguage.googleapis.com");
|
|
247
|
+
}
|
|
248
|
+
defaultApiVersion() {
|
|
249
|
+
return getEnvFirst("GITREVIEWPILOT_GEMINI_API_VERSION") ?? "v1";
|
|
250
|
+
}
|
|
251
|
+
defaultModel() {
|
|
252
|
+
// Gemini REST expects "models/<modelName>"
|
|
253
|
+
const raw = getEnvFirst("GITREVIEWPILOT_LLM_MODEL", "GEMINI_MODEL") ?? "gemini-2.0-flash";
|
|
254
|
+
return raw.startsWith("models/") ? raw : `models/${raw}`;
|
|
255
|
+
}
|
|
256
|
+
defaultTimeoutMs() {
|
|
257
|
+
return parsePositiveInt(getEnvFirst("GITREVIEWPILOT_LLM_TIMEOUT_MS", "GEMINI_TIMEOUT_MS"), 30_000);
|
|
258
|
+
}
|
|
259
|
+
resolveApiKey(override) {
|
|
260
|
+
if (override && override.trim())
|
|
261
|
+
return override.trim();
|
|
262
|
+
return getEnvFirst("GITREVIEWPILOT_LLM_API_KEY", "GEMINI_API_KEY");
|
|
263
|
+
}
|
|
264
|
+
async analyze(prompt, options = {}) {
|
|
265
|
+
const provider = "gemini";
|
|
266
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? this.defaultBaseUrl());
|
|
267
|
+
const apiVersion = this.defaultApiVersion();
|
|
268
|
+
const model = options.model
|
|
269
|
+
? options.model.startsWith("models/")
|
|
270
|
+
? options.model
|
|
271
|
+
: `models/${options.model}`
|
|
272
|
+
: this.defaultModel();
|
|
273
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs();
|
|
274
|
+
const apiKey = this.resolveApiKey(options.apiKey);
|
|
275
|
+
if (!apiKey) {
|
|
276
|
+
throw missingKeyError(provider, "Set GITREVIEWPILOT_LLM_API_KEY (preferred) or GEMINI_API_KEY.");
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const url = new URL(`${baseUrl}/${apiVersion}/${model}:generateContent`);
|
|
280
|
+
url.searchParams.set("key", apiKey);
|
|
281
|
+
const { res, text } = await fetchTextWithTimeout(url, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: { "Content-Type": "application/json" },
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
contents: [{ role: "user", parts: [{ text: prompt }] }],
|
|
286
|
+
generationConfig: { temperature: 0.2 }
|
|
287
|
+
})
|
|
288
|
+
}, timeoutMs);
|
|
289
|
+
const data = safeJsonParse(text);
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
const retryAfterMs = parseRetryAfterMs(res);
|
|
292
|
+
throw new LlmApiError({
|
|
293
|
+
provider,
|
|
294
|
+
status: res.status,
|
|
295
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
296
|
+
message: `LLM API error (${res.status}): ${data?.error?.message ?? (text ? text.slice(0, 500) : "Request failed")}`,
|
|
297
|
+
...(text ? { details: text } : {})
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const parts = data?.candidates?.[0]?.content?.parts ?? [];
|
|
301
|
+
const content = parts.map((p) => p.text ?? "").join("").trim();
|
|
302
|
+
if (!content) {
|
|
303
|
+
throw new LlmApiError({ provider, status: res.status, message: "LLM returned an empty response." });
|
|
304
|
+
}
|
|
305
|
+
return content;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (err instanceof DOMException && err.name === "AbortError")
|
|
309
|
+
throw toTimeoutError(provider, timeoutMs, err);
|
|
310
|
+
if (err instanceof LlmApiError)
|
|
311
|
+
throw err;
|
|
312
|
+
if (err instanceof Error) {
|
|
313
|
+
throw new LlmApiError({ provider, message: err.message, cause: err });
|
|
314
|
+
}
|
|
315
|
+
throw new LlmApiError({ provider, message: String(err) });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
class AnthropicClient {
|
|
320
|
+
defaultBaseUrl() {
|
|
321
|
+
return normalizeBaseUrl(getEnvFirst("GITREVIEWPILOT_LLM_BASE_URL", "ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com");
|
|
322
|
+
}
|
|
323
|
+
defaultModel() {
|
|
324
|
+
return getEnvFirst("GITREVIEWPILOT_LLM_MODEL", "ANTHROPIC_MODEL") ?? "claude-3-5-sonnet-latest";
|
|
325
|
+
}
|
|
326
|
+
defaultTimeoutMs() {
|
|
327
|
+
return parsePositiveInt(getEnvFirst("GITREVIEWPILOT_LLM_TIMEOUT_MS", "ANTHROPIC_TIMEOUT_MS"), 30_000);
|
|
328
|
+
}
|
|
329
|
+
resolveApiKey(override) {
|
|
330
|
+
if (override && override.trim())
|
|
331
|
+
return override.trim();
|
|
332
|
+
return getEnvFirst("GITREVIEWPILOT_LLM_API_KEY", "ANTHROPIC_API_KEY");
|
|
333
|
+
}
|
|
334
|
+
async analyze(prompt, options = {}) {
|
|
335
|
+
const provider = "anthropic";
|
|
336
|
+
const baseUrl = normalizeBaseUrl(options.baseUrl ?? this.defaultBaseUrl());
|
|
337
|
+
const model = options.model ?? this.defaultModel();
|
|
338
|
+
const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs();
|
|
339
|
+
const apiKey = this.resolveApiKey(options.apiKey);
|
|
340
|
+
if (!apiKey) {
|
|
341
|
+
throw missingKeyError(provider, "Set GITREVIEWPILOT_LLM_API_KEY (preferred) or ANTHROPIC_API_KEY.");
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
const { res, text } = await fetchTextWithTimeout(`${baseUrl}/v1/messages`, {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: {
|
|
347
|
+
"Content-Type": "application/json",
|
|
348
|
+
"x-api-key": apiKey,
|
|
349
|
+
"anthropic-version": getEnvFirst("ANTHROPIC_VERSION") ?? "2023-06-01"
|
|
350
|
+
},
|
|
351
|
+
body: JSON.stringify({
|
|
352
|
+
model,
|
|
353
|
+
max_tokens: 800,
|
|
354
|
+
temperature: 0.2,
|
|
355
|
+
messages: [{ role: "user", content: prompt }]
|
|
356
|
+
})
|
|
357
|
+
}, timeoutMs);
|
|
358
|
+
const data = safeJsonParse(text);
|
|
359
|
+
if (!res.ok) {
|
|
360
|
+
const retryAfterMs = parseRetryAfterMs(res);
|
|
361
|
+
throw new LlmApiError({
|
|
362
|
+
provider,
|
|
363
|
+
status: res.status,
|
|
364
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
365
|
+
message: `LLM API error (${res.status}): ${data?.error?.message ?? (text ? text.slice(0, 500) : "Request failed")}`,
|
|
366
|
+
...(text ? { details: text } : {})
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const content = (data?.content ?? [])
|
|
370
|
+
.filter((p) => (p.type ?? "text") === "text")
|
|
371
|
+
.map((p) => p.text ?? "")
|
|
372
|
+
.join("")
|
|
373
|
+
.trim();
|
|
374
|
+
if (!content) {
|
|
375
|
+
throw new LlmApiError({ provider, status: res.status, message: "LLM returned an empty response." });
|
|
376
|
+
}
|
|
377
|
+
return content;
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
if (err instanceof DOMException && err.name === "AbortError")
|
|
381
|
+
throw toTimeoutError(provider, timeoutMs, err);
|
|
382
|
+
if (err instanceof LlmApiError)
|
|
383
|
+
throw err;
|
|
384
|
+
if (err instanceof Error) {
|
|
385
|
+
throw new LlmApiError({ provider, message: err.message, cause: err });
|
|
386
|
+
}
|
|
387
|
+
throw new LlmApiError({ provider, message: String(err) });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const PROVIDERS = {
|
|
392
|
+
"openai-compatible": new OpenAICompatibleClient(),
|
|
393
|
+
gemini: new GeminiClient(),
|
|
394
|
+
anthropic: new AnthropicClient()
|
|
395
|
+
};
|
|
396
|
+
//# sourceMappingURL=ai.service.js.map
|