second-opinion-mcp 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.
Files changed (51) hide show
  1. package/README.md +323 -0
  2. package/dist/config.d.ts +31 -0
  3. package/dist/config.js +84 -0
  4. package/dist/context/bundler.d.ts +51 -0
  5. package/dist/context/bundler.js +481 -0
  6. package/dist/context/bundler.test.d.ts +1 -0
  7. package/dist/context/bundler.test.js +275 -0
  8. package/dist/context/git.d.ts +21 -0
  9. package/dist/context/git.js +102 -0
  10. package/dist/context/imports.d.ts +43 -0
  11. package/dist/context/imports.js +197 -0
  12. package/dist/context/imports.test.d.ts +1 -0
  13. package/dist/context/imports.test.js +147 -0
  14. package/dist/context/index.d.ts +6 -0
  15. package/dist/context/index.js +6 -0
  16. package/dist/context/session.d.ts +35 -0
  17. package/dist/context/session.js +317 -0
  18. package/dist/context/tests.d.ts +13 -0
  19. package/dist/context/tests.js +83 -0
  20. package/dist/context/types.d.ts +16 -0
  21. package/dist/context/types.js +100 -0
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +6 -0
  24. package/dist/output/writer.d.ts +34 -0
  25. package/dist/output/writer.js +162 -0
  26. package/dist/output/writer.test.d.ts +1 -0
  27. package/dist/output/writer.test.js +175 -0
  28. package/dist/providers/base.d.ts +24 -0
  29. package/dist/providers/base.js +77 -0
  30. package/dist/providers/base.test.d.ts +1 -0
  31. package/dist/providers/base.test.js +91 -0
  32. package/dist/providers/gemini.d.ts +8 -0
  33. package/dist/providers/gemini.js +43 -0
  34. package/dist/providers/index.d.ts +8 -0
  35. package/dist/providers/index.js +31 -0
  36. package/dist/providers/openai.d.ts +8 -0
  37. package/dist/providers/openai.js +39 -0
  38. package/dist/server.d.ts +3 -0
  39. package/dist/server.js +181 -0
  40. package/dist/test-utils.d.ts +71 -0
  41. package/dist/test-utils.js +136 -0
  42. package/dist/tools/review.d.ts +76 -0
  43. package/dist/tools/review.js +199 -0
  44. package/dist/utils/index.d.ts +1 -0
  45. package/dist/utils/index.js +1 -0
  46. package/dist/utils/tokens.d.ts +26 -0
  47. package/dist/utils/tokens.js +27 -0
  48. package/package.json +61 -0
  49. package/scripts/install-config.js +51 -0
  50. package/second-opinion.skill.md +34 -0
  51. package/templates/second-opinion.md +54 -0
@@ -0,0 +1,100 @@
1
+ import * as fs from "fs";
2
+ import { glob } from "glob";
3
+ import { resolveImportPath } from "./imports.js";
4
+ // Patterns for type imports
5
+ const TYPE_IMPORT_PATTERNS = [
6
+ // import type { X } from 'y'
7
+ /import\s+type\s+\{[^}]+\}\s+from\s+['"]([^'"]+)['"]/g,
8
+ // import { type X } from 'y'
9
+ /import\s+\{[^}]*type\s+\w+[^}]*\}\s+from\s+['"]([^'"]+)['"]/g,
10
+ // Regular imports from type-ish locations
11
+ /import\s+(?:[\w*{}\s,]+\s+from\s+)?['"]([^'"]*(?:types?|interfaces?|models?)[^'"]*)['"]/gi,
12
+ ];
13
+ // Common type file/directory patterns
14
+ const TYPE_LOCATIONS = [
15
+ "types",
16
+ "types/**",
17
+ "interfaces",
18
+ "interfaces/**",
19
+ "models",
20
+ "models/**",
21
+ "@types",
22
+ "@types/**",
23
+ "src/types",
24
+ "src/types/**",
25
+ "src/interfaces",
26
+ "src/interfaces/**",
27
+ "**/*.d.ts",
28
+ ];
29
+ /**
30
+ * Extract type-related imports from file content
31
+ */
32
+ export function extractTypeImports(content) {
33
+ const typeImports = new Set();
34
+ for (const pattern of TYPE_IMPORT_PATTERNS) {
35
+ pattern.lastIndex = 0;
36
+ let match;
37
+ while ((match = pattern.exec(content)) !== null) {
38
+ typeImports.add(match[1]);
39
+ }
40
+ }
41
+ return Array.from(typeImports);
42
+ }
43
+ /**
44
+ * Check if a file path looks like a type definition file
45
+ */
46
+ export function isTypeFile(filePath) {
47
+ const normalized = filePath.toLowerCase();
48
+ return (normalized.endsWith(".d.ts") ||
49
+ normalized.includes("/types/") ||
50
+ normalized.includes("/interfaces/") ||
51
+ normalized.includes("/models/") ||
52
+ normalized.includes("/@types/") ||
53
+ /\/types?\.(ts|js)$/.test(normalized) ||
54
+ /\/interfaces?\.(ts|js)$/.test(normalized));
55
+ }
56
+ /**
57
+ * Find all type definition files in a project
58
+ */
59
+ export async function findAllTypeFiles(projectPath) {
60
+ const typeFiles = await glob(TYPE_LOCATIONS, {
61
+ cwd: projectPath,
62
+ absolute: true,
63
+ nodir: true,
64
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"],
65
+ });
66
+ return typeFiles.filter((f) => /\.(ts|tsx|js|jsx)$/.test(f));
67
+ }
68
+ /**
69
+ * Find type files that are imported by the given files
70
+ */
71
+ export async function findTypeFilesForFiles(files, projectPath) {
72
+ const typeFiles = new Set();
73
+ // Get all type files in the project
74
+ const allTypes = await findAllTypeFiles(projectPath);
75
+ const typeFileSet = new Set(allTypes);
76
+ for (const file of files) {
77
+ if (!fs.existsSync(file))
78
+ continue;
79
+ try {
80
+ const content = fs.readFileSync(file, "utf-8");
81
+ const typeImports = extractTypeImports(content);
82
+ // Also check regular imports that resolve to type files
83
+ const allImportPattern = /import\s+(?:[\w*{}\s,]+\s+from\s+)?['"]([^'"]+)['"]/g;
84
+ let match;
85
+ while ((match = allImportPattern.exec(content)) !== null) {
86
+ const importPath = match[1];
87
+ const resolved = resolveImportPath(importPath, file, projectPath);
88
+ if (resolved && (typeFileSet.has(resolved) || isTypeFile(resolved))) {
89
+ if (!files.includes(resolved)) {
90
+ typeFiles.add(resolved);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ // Skip files that can't be read
97
+ }
98
+ }
99
+ return Array.from(typeFiles);
100
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runServer } from "./server.js";
3
+ runServer().catch((error) => {
4
+ console.error("Fatal error:", error);
5
+ process.exit(1);
6
+ });
@@ -0,0 +1,34 @@
1
+ export interface ReviewMetadata {
2
+ sessionName: string;
3
+ provider: string;
4
+ model: string;
5
+ timestamp: string;
6
+ filesReviewed: string[];
7
+ tokensUsed?: number;
8
+ task?: string;
9
+ }
10
+ export interface EgressSummary {
11
+ projectFilesSent: number;
12
+ projectFilePaths: string[];
13
+ externalFilesSent: number;
14
+ externalFilePaths: string[];
15
+ externalLocations: string[];
16
+ blockedFiles: {
17
+ path: string;
18
+ reason: string;
19
+ }[];
20
+ provider: string;
21
+ }
22
+ /**
23
+ * Write a review/response to a markdown file
24
+ */
25
+ export declare function writeReview(projectPath: string, reviewsDir: string, metadata: ReviewMetadata, review: string): string;
26
+ /**
27
+ * Derive a session name from the conversation context
28
+ */
29
+ export declare function deriveSessionName(conversationContext: string, fallback?: string): string;
30
+ /**
31
+ * Write an egress manifest JSON file for audit trail
32
+ * This records exactly what files were sent to the external LLM
33
+ */
34
+ export declare function writeEgressManifest(projectPath: string, reviewsDir: string, metadata: ReviewMetadata, egressData: EgressSummary): string;
@@ -0,0 +1,162 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ /**
4
+ * Generate a slug from a session name
5
+ */
6
+ function slugify(text) {
7
+ return text
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9]+/g, "-")
10
+ .replace(/^-+|-+$/g, "")
11
+ .substring(0, 50);
12
+ }
13
+ /**
14
+ * Validate that reviewsDir is safe (relative, no traversal)
15
+ */
16
+ function validateReviewsDir(reviewsDir) {
17
+ // Must be relative
18
+ if (path.isAbsolute(reviewsDir)) {
19
+ throw new Error(`reviewsDir must be relative, got: ${reviewsDir}`);
20
+ }
21
+ // Must not contain path traversal
22
+ if (reviewsDir.includes("..")) {
23
+ throw new Error(`reviewsDir contains path traversal: ${reviewsDir}`);
24
+ }
25
+ // Normalize and verify no traversal after normalization
26
+ const normalized = path.normalize(reviewsDir);
27
+ if (normalized.startsWith("..")) {
28
+ throw new Error(`reviewsDir resolves outside project: ${reviewsDir}`);
29
+ }
30
+ }
31
+ /**
32
+ * Derive a short task slug from a task description
33
+ */
34
+ function deriveTaskSlug(task) {
35
+ // Take first few meaningful words
36
+ const words = task
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9\s]/g, " ")
39
+ .split(/\s+/)
40
+ .filter((w) => w.length > 2)
41
+ .slice(0, 4);
42
+ if (words.length === 0) {
43
+ return "task";
44
+ }
45
+ return words.join("-").substring(0, 30);
46
+ }
47
+ /**
48
+ * Generate base filename (without extension) for output files
49
+ */
50
+ function generateBaseFilename(metadata) {
51
+ const sessionSlug = slugify(metadata.sessionName);
52
+ if (metadata.task) {
53
+ const taskSlug = deriveTaskSlug(metadata.task);
54
+ return `${sessionSlug}.${metadata.provider}.${taskSlug}`;
55
+ }
56
+ return `${sessionSlug}.${metadata.provider}.review`;
57
+ }
58
+ /**
59
+ * Ensure output directory exists and return its path
60
+ */
61
+ function ensureOutputDir(projectPath, reviewsDir) {
62
+ validateReviewsDir(reviewsDir);
63
+ const outputDirPath = path.join(projectPath, reviewsDir);
64
+ if (!fs.existsSync(outputDirPath)) {
65
+ fs.mkdirSync(outputDirPath, { recursive: true });
66
+ }
67
+ return outputDirPath;
68
+ }
69
+ /**
70
+ * Write a review/response to a markdown file
71
+ */
72
+ export function writeReview(projectPath, reviewsDir, metadata, review) {
73
+ const outputDirPath = ensureOutputDir(projectPath, reviewsDir);
74
+ const filePath = path.join(outputDirPath, `${generateBaseFilename(metadata)}.md`);
75
+ // Build the document
76
+ const lines = [];
77
+ // Header based on task or review
78
+ if (metadata.task) {
79
+ lines.push(`# Second Opinion - ${metadata.sessionName}`);
80
+ lines.push("");
81
+ lines.push(`**Task:** ${metadata.task.substring(0, 200)}${metadata.task.length > 200 ? "..." : ""}`);
82
+ }
83
+ else {
84
+ lines.push(`# Code Review - ${metadata.sessionName}`);
85
+ }
86
+ lines.push("");
87
+ lines.push(`**Provider:** ${metadata.provider} (${metadata.model})`);
88
+ lines.push(`**Date:** ${metadata.timestamp}`);
89
+ if (metadata.tokensUsed) {
90
+ lines.push(`**Tokens Used:** ${metadata.tokensUsed.toLocaleString()}`);
91
+ }
92
+ lines.push("");
93
+ if (metadata.filesReviewed.length > 0) {
94
+ lines.push("## Files Analyzed");
95
+ lines.push("");
96
+ for (const file of metadata.filesReviewed) {
97
+ const relativePath = path.relative(projectPath, file);
98
+ lines.push(`- ${relativePath}`);
99
+ }
100
+ lines.push("");
101
+ }
102
+ lines.push("---");
103
+ lines.push("");
104
+ lines.push(review);
105
+ lines.push("");
106
+ lines.push("---");
107
+ lines.push("*Generated by second-opinion MCP server*");
108
+ // Write the file
109
+ fs.writeFileSync(filePath, lines.join("\n"));
110
+ return filePath;
111
+ }
112
+ /**
113
+ * Derive a session name from the conversation context
114
+ */
115
+ export function deriveSessionName(conversationContext, fallback = "review") {
116
+ // Try to extract from the first user message
117
+ const userMatch = conversationContext.match(/\*\*User\*\*:\n(.+?)(?:\n|$)/);
118
+ if (userMatch) {
119
+ const firstMessage = userMatch[1].trim();
120
+ // Take first 50 chars, clean up
121
+ const name = firstMessage
122
+ .substring(0, 50)
123
+ .replace(/[^a-zA-Z0-9\s-]/g, "")
124
+ .trim();
125
+ if (name.length > 3) {
126
+ return name;
127
+ }
128
+ }
129
+ return fallback;
130
+ }
131
+ /**
132
+ * Write an egress manifest JSON file for audit trail
133
+ * This records exactly what files were sent to the external LLM
134
+ */
135
+ export function writeEgressManifest(projectPath, reviewsDir, metadata, egressData) {
136
+ const outputDirPath = ensureOutputDir(projectPath, reviewsDir);
137
+ const filePath = path.join(outputDirPath, `${generateBaseFilename(metadata)}.egress.json`);
138
+ // Build the manifest
139
+ const manifest = {
140
+ timestamp: metadata.timestamp,
141
+ provider: egressData.provider,
142
+ model: metadata.model,
143
+ sessionName: metadata.sessionName,
144
+ task: metadata.task || null,
145
+ egress: {
146
+ projectFiles: {
147
+ count: egressData.projectFilesSent,
148
+ paths: egressData.projectFilePaths.map((p) => path.relative(projectPath, p)),
149
+ },
150
+ externalFiles: {
151
+ count: egressData.externalFilesSent,
152
+ paths: egressData.externalFilePaths,
153
+ locations: egressData.externalLocations,
154
+ },
155
+ blockedFiles: egressData.blockedFiles,
156
+ totalFilesSent: egressData.projectFilesSent + egressData.externalFilesSent,
157
+ },
158
+ };
159
+ // Write the file
160
+ fs.writeFileSync(filePath, JSON.stringify(manifest, null, 2));
161
+ return filePath;
162
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { writeReview, writeEgressManifest, deriveSessionName, } from "./writer.js";
6
+ describe("deriveSessionName", () => {
7
+ it("extracts name from first user message", () => {
8
+ const context = "## Conversation Context\n\n**User**:\nFix the login bug\n\n**Claude**:\nI'll look into it.";
9
+ expect(deriveSessionName(context)).toBe("Fix the login bug");
10
+ });
11
+ it("truncates long messages", () => {
12
+ const longMessage = "A".repeat(100);
13
+ const context = `**User**:\n${longMessage}\n`;
14
+ const result = deriveSessionName(context);
15
+ expect(result.length).toBeLessThanOrEqual(50);
16
+ });
17
+ it("returns fallback for empty context", () => {
18
+ expect(deriveSessionName("")).toBe("review");
19
+ expect(deriveSessionName("", "custom-fallback")).toBe("custom-fallback");
20
+ });
21
+ it("returns fallback for context without user message", () => {
22
+ const context = "**Claude**:\nHere's the review.";
23
+ expect(deriveSessionName(context)).toBe("review");
24
+ });
25
+ it("strips special characters from name", () => {
26
+ const context = "**User**:\nFix the `bug` in [auth]!\n";
27
+ const result = deriveSessionName(context);
28
+ expect(result).not.toContain("`");
29
+ expect(result).not.toContain("[");
30
+ expect(result).not.toContain("!");
31
+ });
32
+ });
33
+ describe("writeReview", () => {
34
+ const tmpDir = path.join(os.tmpdir(), "writer-test-" + Date.now());
35
+ beforeAll(() => {
36
+ fs.mkdirSync(tmpDir, { recursive: true });
37
+ });
38
+ afterAll(() => {
39
+ fs.rmSync(tmpDir, { recursive: true, force: true });
40
+ });
41
+ it("creates review file with correct filename for review", () => {
42
+ const metadata = {
43
+ sessionName: "Fix Login Bug",
44
+ provider: "gemini",
45
+ model: "gemini-2.0-flash-exp",
46
+ timestamp: "2024-01-01T00:00:00Z",
47
+ filesReviewed: [],
48
+ };
49
+ const filePath = writeReview(tmpDir, "reviews", metadata, "# Review\nLooks good!");
50
+ expect(fs.existsSync(filePath)).toBe(true);
51
+ expect(filePath).toContain("fix-login-bug.gemini.review.md");
52
+ });
53
+ it("creates review file with task slug when task provided", () => {
54
+ const metadata = {
55
+ sessionName: "Auth Refactor",
56
+ provider: "openai",
57
+ model: "gpt-4o",
58
+ timestamp: "2024-01-01T00:00:00Z",
59
+ filesReviewed: [],
60
+ task: "Check for security vulnerabilities in the authentication flow",
61
+ };
62
+ const filePath = writeReview(tmpDir, "reviews", metadata, "# Analysis\nNo issues found.");
63
+ expect(filePath).toContain("auth-refactor.openai.check-for-security");
64
+ expect(filePath.endsWith(".md")).toBe(true);
65
+ });
66
+ it("creates output directory if it doesn't exist", () => {
67
+ const metadata = {
68
+ sessionName: "Test",
69
+ provider: "gemini",
70
+ model: "gemini-2.0-flash-exp",
71
+ timestamp: "2024-01-01T00:00:00Z",
72
+ filesReviewed: [],
73
+ };
74
+ const filePath = writeReview(tmpDir, "new-dir", metadata, "Review content");
75
+ expect(fs.existsSync(path.join(tmpDir, "new-dir"))).toBe(true);
76
+ expect(fs.existsSync(filePath)).toBe(true);
77
+ });
78
+ it("includes metadata in review content", () => {
79
+ const metadata = {
80
+ sessionName: "Metadata Test",
81
+ provider: "gemini",
82
+ model: "gemini-2.0-flash-exp",
83
+ timestamp: "2024-01-01T00:00:00Z",
84
+ filesReviewed: [path.join(tmpDir, "src", "file.ts")],
85
+ tokensUsed: 1000,
86
+ };
87
+ const filePath = writeReview(tmpDir, "reviews", metadata, "Review body");
88
+ const content = fs.readFileSync(filePath, "utf-8");
89
+ expect(content).toContain("**Provider:** gemini (gemini-2.0-flash-exp)");
90
+ expect(content).toContain("**Date:** 2024-01-01T00:00:00Z");
91
+ expect(content).toContain("**Tokens Used:** 1,000");
92
+ expect(content).toContain("Review body");
93
+ });
94
+ it("throws on absolute reviewsDir", () => {
95
+ const metadata = {
96
+ sessionName: "Test",
97
+ provider: "gemini",
98
+ model: "gemini-2.0-flash-exp",
99
+ timestamp: "2024-01-01T00:00:00Z",
100
+ filesReviewed: [],
101
+ };
102
+ expect(() => writeReview(tmpDir, "/absolute/path", metadata, "content")).toThrow("reviewsDir must be relative");
103
+ });
104
+ it("throws on path traversal in reviewsDir", () => {
105
+ const metadata = {
106
+ sessionName: "Test",
107
+ provider: "gemini",
108
+ model: "gemini-2.0-flash-exp",
109
+ timestamp: "2024-01-01T00:00:00Z",
110
+ filesReviewed: [],
111
+ };
112
+ expect(() => writeReview(tmpDir, "../escape", metadata, "content")).toThrow("path traversal");
113
+ });
114
+ });
115
+ describe("writeEgressManifest", () => {
116
+ const tmpDir = path.join(os.tmpdir(), "egress-test-" + Date.now());
117
+ beforeAll(() => {
118
+ fs.mkdirSync(tmpDir, { recursive: true });
119
+ });
120
+ afterAll(() => {
121
+ fs.rmSync(tmpDir, { recursive: true, force: true });
122
+ });
123
+ it("creates egress manifest with correct structure", () => {
124
+ const metadata = {
125
+ sessionName: "Egress Test",
126
+ provider: "gemini",
127
+ model: "gemini-2.0-flash-exp",
128
+ timestamp: "2024-01-01T00:00:00Z",
129
+ filesReviewed: [],
130
+ };
131
+ const egressData = {
132
+ projectFilesSent: 5,
133
+ projectFilePaths: [
134
+ path.join(tmpDir, "src/a.ts"),
135
+ path.join(tmpDir, "src/b.ts"),
136
+ ],
137
+ externalFilesSent: 1,
138
+ externalFilePaths: ["/external/file.ts"],
139
+ externalLocations: ["/external"],
140
+ blockedFiles: [{ path: "/blocked/secret.ts", reason: "sensitive_path" }],
141
+ provider: "gemini",
142
+ };
143
+ const filePath = writeEgressManifest(tmpDir, "reviews", metadata, egressData);
144
+ expect(filePath).toContain("egress-test.gemini.review.egress.json");
145
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
146
+ expect(content.timestamp).toBe("2024-01-01T00:00:00Z");
147
+ expect(content.provider).toBe("gemini");
148
+ expect(content.egress.projectFiles.count).toBe(5);
149
+ expect(content.egress.externalFiles.count).toBe(1);
150
+ expect(content.egress.blockedFiles).toHaveLength(1);
151
+ expect(content.egress.totalFilesSent).toBe(6);
152
+ });
153
+ it("relativizes project file paths", () => {
154
+ const metadata = {
155
+ sessionName: "Path Test",
156
+ provider: "gemini",
157
+ model: "gemini-2.0-flash-exp",
158
+ timestamp: "2024-01-01T00:00:00Z",
159
+ filesReviewed: [],
160
+ };
161
+ const egressData = {
162
+ projectFilesSent: 1,
163
+ projectFilePaths: [path.join(tmpDir, "src", "file.ts")],
164
+ externalFilesSent: 0,
165
+ externalFilePaths: [],
166
+ externalLocations: [],
167
+ blockedFiles: [],
168
+ provider: "gemini",
169
+ };
170
+ const filePath = writeEgressManifest(tmpDir, "reviews", metadata, egressData);
171
+ const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
172
+ // Should be relative path, not absolute
173
+ expect(content.egress.projectFiles.paths[0]).toBe(path.join("src", "file.ts"));
174
+ });
175
+ });
@@ -0,0 +1,24 @@
1
+ export interface ReviewRequest {
2
+ instructions: string;
3
+ context: string;
4
+ task?: string;
5
+ focusAreas?: string[];
6
+ customPrompt?: string;
7
+ }
8
+ export interface ReviewResponse {
9
+ review: string;
10
+ model: string;
11
+ tokensUsed?: number;
12
+ }
13
+ export interface ReviewProvider {
14
+ name: string;
15
+ review(request: ReviewRequest): Promise<ReviewResponse>;
16
+ }
17
+ /**
18
+ * Get the system prompt based on whether a custom task is provided
19
+ */
20
+ export declare function getSystemPrompt(hasTask: boolean): string;
21
+ /**
22
+ * Build the full prompt for the LLM
23
+ */
24
+ export declare function buildReviewPrompt(request: ReviewRequest): string;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Get the system prompt based on whether a custom task is provided
3
+ */
4
+ export function getSystemPrompt(hasTask) {
5
+ return hasTask
6
+ ? "You are an expert software engineer. Complete the requested task thoroughly and provide clear, actionable output."
7
+ : "You are an expert software engineer performing a code review. Be thorough, constructive, and actionable.";
8
+ }
9
+ /**
10
+ * Build the full prompt for the LLM
11
+ */
12
+ export function buildReviewPrompt(request) {
13
+ const parts = [];
14
+ // When a task is provided, it becomes the primary objective
15
+ if (request.task) {
16
+ parts.push("# Task");
17
+ parts.push("");
18
+ parts.push(request.task);
19
+ parts.push("");
20
+ // Focus areas if specified
21
+ if (request.focusAreas && request.focusAreas.length > 0) {
22
+ parts.push("## Focus Areas");
23
+ parts.push("");
24
+ for (const area of request.focusAreas) {
25
+ parts.push(`- ${area}`);
26
+ }
27
+ parts.push("");
28
+ }
29
+ // Additional instructions if specified
30
+ if (request.customPrompt) {
31
+ parts.push("## Additional Instructions");
32
+ parts.push("");
33
+ parts.push(request.customPrompt);
34
+ parts.push("");
35
+ }
36
+ // Include instructions as reference material
37
+ if (request.instructions) {
38
+ parts.push("---");
39
+ parts.push("");
40
+ parts.push("## Reference Instructions");
41
+ parts.push("");
42
+ parts.push("*Use the following instructions as reference where relevant to your task:*");
43
+ parts.push("");
44
+ parts.push(request.instructions);
45
+ parts.push("");
46
+ }
47
+ }
48
+ else {
49
+ // Default: code review mode
50
+ parts.push(request.instructions);
51
+ parts.push("");
52
+ // Focus areas if specified
53
+ if (request.focusAreas && request.focusAreas.length > 0) {
54
+ parts.push("## Specific Focus Areas for This Review");
55
+ parts.push("");
56
+ for (const area of request.focusAreas) {
57
+ parts.push(`- ${area}`);
58
+ }
59
+ parts.push("");
60
+ }
61
+ // Custom prompt if specified (legacy support)
62
+ if (request.customPrompt) {
63
+ parts.push("## Additional Instructions");
64
+ parts.push("");
65
+ parts.push(request.customPrompt);
66
+ parts.push("");
67
+ }
68
+ }
69
+ // Separator
70
+ parts.push("---");
71
+ parts.push("");
72
+ parts.push("# Code Context");
73
+ parts.push("");
74
+ // The actual context
75
+ parts.push(request.context);
76
+ return parts.join("\n");
77
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildReviewPrompt, getSystemPrompt } from "./base.js";
3
+ describe("getSystemPrompt", () => {
4
+ it("returns task prompt when hasTask is true", () => {
5
+ const prompt = getSystemPrompt(true);
6
+ expect(prompt).toContain("Complete the requested task");
7
+ expect(prompt).not.toContain("code review");
8
+ });
9
+ it("returns review prompt when hasTask is false", () => {
10
+ const prompt = getSystemPrompt(false);
11
+ expect(prompt).toContain("code review");
12
+ expect(prompt).toContain("constructive");
13
+ });
14
+ });
15
+ describe("buildReviewPrompt", () => {
16
+ it("builds prompt with task as primary objective", () => {
17
+ const request = {
18
+ instructions: "Review guidelines here",
19
+ context: "# Code\n```ts\nconst x = 1;\n```",
20
+ task: "Analyze security vulnerabilities",
21
+ };
22
+ const prompt = buildReviewPrompt(request);
23
+ expect(prompt).toContain("# Task");
24
+ expect(prompt).toContain("Analyze security vulnerabilities");
25
+ expect(prompt).toContain("## Reference Instructions");
26
+ expect(prompt).toContain("# Code Context");
27
+ expect(prompt).toContain("const x = 1;");
28
+ });
29
+ it("builds prompt in review mode when no task", () => {
30
+ const request = {
31
+ instructions: "Review guidelines here",
32
+ context: "# Code\n```ts\nconst x = 1;\n```",
33
+ };
34
+ const prompt = buildReviewPrompt(request);
35
+ expect(prompt).not.toContain("# Task");
36
+ expect(prompt).toContain("Review guidelines here");
37
+ expect(prompt).toContain("# Code Context");
38
+ });
39
+ it("includes focus areas when provided", () => {
40
+ const request = {
41
+ instructions: "Guidelines",
42
+ context: "Code",
43
+ focusAreas: ["Security", "Performance"],
44
+ };
45
+ const prompt = buildReviewPrompt(request);
46
+ expect(prompt).toContain("Focus Areas");
47
+ expect(prompt).toContain("- Security");
48
+ expect(prompt).toContain("- Performance");
49
+ });
50
+ it("includes focus areas in task mode", () => {
51
+ const request = {
52
+ instructions: "Guidelines",
53
+ context: "Code",
54
+ task: "Analyze code",
55
+ focusAreas: ["Security"],
56
+ };
57
+ const prompt = buildReviewPrompt(request);
58
+ expect(prompt).toContain("## Focus Areas");
59
+ expect(prompt).toContain("- Security");
60
+ });
61
+ it("includes custom prompt as additional instructions", () => {
62
+ const request = {
63
+ instructions: "Guidelines",
64
+ context: "Code",
65
+ customPrompt: "Pay special attention to error handling",
66
+ };
67
+ const prompt = buildReviewPrompt(request);
68
+ expect(prompt).toContain("## Additional Instructions");
69
+ expect(prompt).toContain("Pay special attention to error handling");
70
+ });
71
+ it("includes custom prompt in task mode", () => {
72
+ const request = {
73
+ instructions: "Guidelines",
74
+ context: "Code",
75
+ task: "Review",
76
+ customPrompt: "Be concise",
77
+ };
78
+ const prompt = buildReviewPrompt(request);
79
+ expect(prompt).toContain("## Additional Instructions");
80
+ expect(prompt).toContain("Be concise");
81
+ });
82
+ it("separates sections with markdown separators", () => {
83
+ const request = {
84
+ instructions: "Guidelines",
85
+ context: "Code content here",
86
+ };
87
+ const prompt = buildReviewPrompt(request);
88
+ expect(prompt).toContain("---");
89
+ expect(prompt).toContain("# Code Context");
90
+ });
91
+ });