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,8 @@
1
+ import { ReviewProvider, ReviewRequest, ReviewResponse } from "./base.js";
2
+ export declare class GeminiProvider implements ReviewProvider {
3
+ name: string;
4
+ private client;
5
+ private model;
6
+ constructor(apiKey: string, model?: string);
7
+ review(request: ReviewRequest): Promise<ReviewResponse>;
8
+ }
@@ -0,0 +1,43 @@
1
+ import { GoogleGenerativeAI } from "@google/generative-ai";
2
+ import { buildReviewPrompt, getSystemPrompt, } from "./base.js";
3
+ export class GeminiProvider {
4
+ name = "gemini";
5
+ client;
6
+ model;
7
+ constructor(apiKey, model = "gemini-2.0-flash-exp") {
8
+ this.client = new GoogleGenerativeAI(apiKey);
9
+ this.model = model;
10
+ }
11
+ async review(request) {
12
+ const prompt = buildReviewPrompt(request);
13
+ const systemInstruction = getSystemPrompt(!!request.task);
14
+ const model = this.client.getGenerativeModel({
15
+ model: this.model,
16
+ systemInstruction,
17
+ });
18
+ const result = await model.generateContent({
19
+ contents: [
20
+ {
21
+ role: "user",
22
+ parts: [{ text: prompt }],
23
+ },
24
+ ],
25
+ generationConfig: {
26
+ maxOutputTokens: 8192,
27
+ temperature: 0.3, // Lower temperature for more focused reviews
28
+ },
29
+ });
30
+ const response = result.response;
31
+ const text = response.text();
32
+ // Get token usage if available
33
+ const usage = response.usageMetadata;
34
+ const tokensUsed = usage
35
+ ? (usage.promptTokenCount || 0) + (usage.candidatesTokenCount || 0)
36
+ : undefined;
37
+ return {
38
+ review: text,
39
+ model: this.model,
40
+ tokensUsed,
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,8 @@
1
+ import { Config } from "../config.js";
2
+ import { ReviewProvider } from "./base.js";
3
+ export * from "./base.js";
4
+ export * from "./gemini.js";
5
+ export * from "./openai.js";
6
+ export type ProviderName = "gemini" | "openai";
7
+ export declare function createProvider(name: ProviderName, config: Config): ReviewProvider;
8
+ export declare function getAvailableProviders(config: Config): ProviderName[];
@@ -0,0 +1,31 @@
1
+ import { GeminiProvider } from "./gemini.js";
2
+ import { OpenAIProvider } from "./openai.js";
3
+ export * from "./base.js";
4
+ export * from "./gemini.js";
5
+ export * from "./openai.js";
6
+ export function createProvider(name, config) {
7
+ switch (name) {
8
+ case "gemini":
9
+ if (!config.geminiApiKey) {
10
+ throw new Error("GEMINI_API_KEY is required for Gemini provider");
11
+ }
12
+ return new GeminiProvider(config.geminiApiKey, config.geminiModel);
13
+ case "openai":
14
+ if (!config.openaiApiKey) {
15
+ throw new Error("OPENAI_API_KEY is required for OpenAI provider");
16
+ }
17
+ return new OpenAIProvider(config.openaiApiKey, config.openaiModel);
18
+ default:
19
+ throw new Error(`Unknown provider: ${name}`);
20
+ }
21
+ }
22
+ export function getAvailableProviders(config) {
23
+ const providers = [];
24
+ if (config.geminiApiKey) {
25
+ providers.push("gemini");
26
+ }
27
+ if (config.openaiApiKey) {
28
+ providers.push("openai");
29
+ }
30
+ return providers;
31
+ }
@@ -0,0 +1,8 @@
1
+ import { ReviewProvider, ReviewRequest, ReviewResponse } from "./base.js";
2
+ export declare class OpenAIProvider implements ReviewProvider {
3
+ name: string;
4
+ private client;
5
+ private model;
6
+ constructor(apiKey: string, model?: string);
7
+ review(request: ReviewRequest): Promise<ReviewResponse>;
8
+ }
@@ -0,0 +1,39 @@
1
+ import OpenAI from "openai";
2
+ import { buildReviewPrompt, getSystemPrompt, } from "./base.js";
3
+ export class OpenAIProvider {
4
+ name = "openai";
5
+ client;
6
+ model;
7
+ constructor(apiKey, model = "gpt-4o") {
8
+ this.client = new OpenAI({ apiKey });
9
+ this.model = model;
10
+ }
11
+ async review(request) {
12
+ const prompt = buildReviewPrompt(request);
13
+ const systemPrompt = getSystemPrompt(!!request.task);
14
+ const response = await this.client.chat.completions.create({
15
+ model: this.model,
16
+ messages: [
17
+ {
18
+ role: "system",
19
+ content: systemPrompt,
20
+ },
21
+ {
22
+ role: "user",
23
+ content: prompt,
24
+ },
25
+ ],
26
+ max_completion_tokens: 8192,
27
+ temperature: 0.3,
28
+ });
29
+ const text = response.choices[0]?.message?.content || "";
30
+ const tokensUsed = response.usage
31
+ ? response.usage.prompt_tokens + response.usage.completion_tokens
32
+ : undefined;
33
+ return {
34
+ review: text,
35
+ model: this.model,
36
+ tokensUsed,
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ export declare function createServer(): Server;
3
+ export declare function runServer(): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,181 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { SecondOpinionInputSchema, executeReview, } from "./tools/review.js";
5
+ import { loadConfig } from "./config.js";
6
+ import { getAvailableProviders } from "./providers/index.js";
7
+ export function createServer() {
8
+ const server = new Server({
9
+ name: "second-opinion",
10
+ version: "0.1.0",
11
+ }, {
12
+ capabilities: {
13
+ tools: {},
14
+ },
15
+ });
16
+ // List available tools
17
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
18
+ const config = loadConfig();
19
+ const providers = getAvailableProviders(config);
20
+ const providerList = providers.length > 0 ? providers.join(", ") : "none configured";
21
+ return {
22
+ tools: [
23
+ {
24
+ name: "second_opinion",
25
+ description: `Get an async code review from an external LLM. Available providers: ${providerList}.
26
+
27
+ This tool:
28
+ 1. Reads context from your Claude Code session (files read/edited, conversation)
29
+ 2. Analyzes dependencies, dependents, tests, and type definitions
30
+ 3. Sends the bundled context to Gemini or GPT for review
31
+ 4. Writes the review to a markdown file in your project
32
+
33
+ The reviewer sees the same context Claude had, plus related code for full understanding.`,
34
+ inputSchema: {
35
+ type: "object",
36
+ properties: {
37
+ provider: {
38
+ type: "string",
39
+ enum: ["gemini", "openai"],
40
+ description: "Which LLM to use for the review",
41
+ },
42
+ projectPath: {
43
+ type: "string",
44
+ description: "Absolute path to the project being reviewed",
45
+ },
46
+ sessionId: {
47
+ type: "string",
48
+ description: "Claude Code session ID (defaults to most recent)",
49
+ },
50
+ includeConversation: {
51
+ type: "boolean",
52
+ default: true,
53
+ description: "Include conversation context from Claude session",
54
+ },
55
+ includeDependencies: {
56
+ type: "boolean",
57
+ default: true,
58
+ description: "Include files imported by modified files",
59
+ },
60
+ includeDependents: {
61
+ type: "boolean",
62
+ default: true,
63
+ description: "Include files that import modified files",
64
+ },
65
+ includeTests: {
66
+ type: "boolean",
67
+ default: true,
68
+ description: "Include corresponding test files",
69
+ },
70
+ includeTypes: {
71
+ type: "boolean",
72
+ default: true,
73
+ description: "Include referenced type definitions",
74
+ },
75
+ maxTokens: {
76
+ type: "number",
77
+ default: 100000,
78
+ description: "Maximum tokens for context",
79
+ },
80
+ sessionName: {
81
+ type: "string",
82
+ description: "Name for this review (used in output filename)",
83
+ },
84
+ customPrompt: {
85
+ type: "string",
86
+ description: "Additional instructions for the reviewer",
87
+ },
88
+ focusAreas: {
89
+ type: "array",
90
+ items: { type: "string" },
91
+ description: "Specific areas to focus on",
92
+ },
93
+ includeFiles: {
94
+ type: "array",
95
+ items: { type: "string" },
96
+ description: "Additional files or folders to include (supports ~ and relative paths)",
97
+ },
98
+ allowExternalFiles: {
99
+ type: "boolean",
100
+ default: false,
101
+ description: "Allow including files outside the project directory. Required when includeFiles contains paths outside the project.",
102
+ },
103
+ dryRun: {
104
+ type: "boolean",
105
+ default: false,
106
+ description: "If true, return a preview of what would be sent without calling the external API",
107
+ },
108
+ },
109
+ required: ["provider", "projectPath"],
110
+ },
111
+ },
112
+ ],
113
+ };
114
+ });
115
+ // Handle tool calls
116
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
117
+ if (request.params.name !== "second_opinion") {
118
+ throw new Error(`Unknown tool: ${request.params.name}`);
119
+ }
120
+ try {
121
+ // Validate input
122
+ const input = SecondOpinionInputSchema.parse(request.params.arguments);
123
+ // Execute the review
124
+ const result = await executeReview(input);
125
+ // Handle dry run response
126
+ if (result.dryRun) {
127
+ return {
128
+ content: [
129
+ {
130
+ type: "text",
131
+ text: JSON.stringify({
132
+ dryRun: true,
133
+ provider: result.provider,
134
+ summary: result.summary,
135
+ totalTokens: result.totalTokens,
136
+ }, null, 2),
137
+ },
138
+ ],
139
+ };
140
+ }
141
+ // Return success response for actual review
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify({
147
+ success: true,
148
+ reviewFile: result.reviewFile,
149
+ egressManifestFile: result.egressManifestFile,
150
+ provider: result.provider,
151
+ model: result.model,
152
+ filesReviewed: result.filesReviewed,
153
+ contextTokens: result.contextTokens,
154
+ tokensUsed: result.tokensUsed,
155
+ summary: result.summary,
156
+ reviewPreview: result.review.substring(0, 500) + (result.review.length > 500 ? "..." : ""),
157
+ }, null, 2),
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: JSON.stringify({ success: false, error: message }, null, 2),
169
+ },
170
+ ],
171
+ isError: true,
172
+ };
173
+ }
174
+ });
175
+ return server;
176
+ }
177
+ export async function runServer() {
178
+ const server = createServer();
179
+ const transport = new StdioServerTransport();
180
+ await server.connect(transport);
181
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Create a temporary directory for tests
3
+ */
4
+ export declare function createTempDir(prefix?: string): string;
5
+ /**
6
+ * Clean up a temporary directory
7
+ */
8
+ export declare function cleanupTempDir(dir: string): void;
9
+ /**
10
+ * Create a project structure from a file tree specification
11
+ *
12
+ * @example
13
+ * createProjectStructure(tmpDir, {
14
+ * "src/index.ts": "export const main = 1;",
15
+ * "src/utils/helper.ts": "export function helper() {}",
16
+ * "package.json": '{"name": "test"}'
17
+ * });
18
+ */
19
+ export declare function createProjectStructure(baseDir: string, files: Record<string, string>): void;
20
+ /**
21
+ * Session message for mock session data
22
+ */
23
+ export interface MockSessionMessage {
24
+ type: "user" | "assistant";
25
+ content: string | object;
26
+ timestamp?: string;
27
+ toolUses?: MockToolUse[];
28
+ }
29
+ export interface MockToolUse {
30
+ id: string;
31
+ name: string;
32
+ input: Record<string, unknown>;
33
+ }
34
+ export interface MockToolResult {
35
+ tool_use_id: string;
36
+ content: string;
37
+ }
38
+ /**
39
+ * Create mock session JSONL data
40
+ *
41
+ * @example
42
+ * const jsonl = createMockSession([
43
+ * { type: "user", content: "Help me with this code" },
44
+ * { type: "assistant", content: "I'll help you.", toolUses: [...] }
45
+ * ]);
46
+ */
47
+ export declare function createMockSession(messages: MockSessionMessage[]): string;
48
+ /**
49
+ * Create a mock tool result entry
50
+ */
51
+ export declare function createMockToolResult(toolUseId: string, content: string): string;
52
+ /**
53
+ * Create a sessions-index.json file
54
+ */
55
+ export interface MockSessionIndexEntry {
56
+ sessionId: string;
57
+ firstPrompt?: string;
58
+ projectPath?: string;
59
+ modified?: string;
60
+ }
61
+ export declare function createMockSessionIndex(projectDir: string, entries: MockSessionIndexEntry[], originalPath?: string): void;
62
+ /**
63
+ * Initialize a git repo in a directory (for git tests)
64
+ * Uses execSync with static arguments - safe for test utilities
65
+ */
66
+ export declare function initGitRepo(dir: string): void;
67
+ /**
68
+ * Make a git commit in a directory
69
+ * Uses execSync with static arguments - safe for test utilities
70
+ */
71
+ export declare function gitCommit(dir: string, message?: string): void;
@@ -0,0 +1,136 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { execSync } from "child_process";
5
+ /**
6
+ * Create a temporary directory for tests
7
+ */
8
+ export function createTempDir(prefix = "test") {
9
+ const dir = path.join(os.tmpdir(), `second-opinion-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
10
+ fs.mkdirSync(dir, { recursive: true });
11
+ return dir;
12
+ }
13
+ /**
14
+ * Clean up a temporary directory
15
+ */
16
+ export function cleanupTempDir(dir) {
17
+ if (dir.startsWith(os.tmpdir())) {
18
+ fs.rmSync(dir, { recursive: true, force: true });
19
+ }
20
+ }
21
+ /**
22
+ * Create a project structure from a file tree specification
23
+ *
24
+ * @example
25
+ * createProjectStructure(tmpDir, {
26
+ * "src/index.ts": "export const main = 1;",
27
+ * "src/utils/helper.ts": "export function helper() {}",
28
+ * "package.json": '{"name": "test"}'
29
+ * });
30
+ */
31
+ export function createProjectStructure(baseDir, files) {
32
+ for (const [relativePath, content] of Object.entries(files)) {
33
+ const fullPath = path.join(baseDir, relativePath);
34
+ const dir = path.dirname(fullPath);
35
+ if (!fs.existsSync(dir)) {
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ fs.writeFileSync(fullPath, content, "utf-8");
39
+ }
40
+ }
41
+ /**
42
+ * Create mock session JSONL data
43
+ *
44
+ * @example
45
+ * const jsonl = createMockSession([
46
+ * { type: "user", content: "Help me with this code" },
47
+ * { type: "assistant", content: "I'll help you.", toolUses: [...] }
48
+ * ]);
49
+ */
50
+ export function createMockSession(messages) {
51
+ const lines = [];
52
+ const baseTime = Date.now();
53
+ for (let i = 0; i < messages.length; i++) {
54
+ const msg = messages[i];
55
+ const timestamp = msg.timestamp || new Date(baseTime + i * 1000).toISOString();
56
+ if (msg.type === "user") {
57
+ const entry = {
58
+ type: "user",
59
+ message: {
60
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
61
+ },
62
+ timestamp,
63
+ };
64
+ lines.push(JSON.stringify(entry));
65
+ }
66
+ else {
67
+ const contentBlocks = [];
68
+ // Add text content if it's a string
69
+ if (typeof msg.content === "string" && msg.content.length > 0) {
70
+ contentBlocks.push({ type: "text", text: msg.content });
71
+ }
72
+ // Add tool uses if present
73
+ if (msg.toolUses) {
74
+ for (const toolUse of msg.toolUses) {
75
+ contentBlocks.push({
76
+ type: "tool_use",
77
+ id: toolUse.id,
78
+ name: toolUse.name,
79
+ input: toolUse.input,
80
+ });
81
+ }
82
+ }
83
+ const entry = {
84
+ type: "assistant",
85
+ message: {
86
+ content: contentBlocks,
87
+ },
88
+ timestamp,
89
+ };
90
+ lines.push(JSON.stringify(entry));
91
+ }
92
+ }
93
+ return lines.join("\n");
94
+ }
95
+ /**
96
+ * Create a mock tool result entry
97
+ */
98
+ export function createMockToolResult(toolUseId, content) {
99
+ const entry = {
100
+ type: "tool_result",
101
+ tool_use_id: toolUseId,
102
+ content,
103
+ };
104
+ return JSON.stringify(entry);
105
+ }
106
+ export function createMockSessionIndex(projectDir, entries, originalPath) {
107
+ const index = {
108
+ entries: entries.map((e) => ({
109
+ sessionId: e.sessionId,
110
+ fullPath: path.join(projectDir, `${e.sessionId}.jsonl`),
111
+ firstPrompt: e.firstPrompt || "Test prompt",
112
+ projectPath: e.projectPath,
113
+ modified: e.modified || new Date().toISOString(),
114
+ })),
115
+ originalPath,
116
+ };
117
+ fs.writeFileSync(path.join(projectDir, "sessions-index.json"), JSON.stringify(index, null, 2));
118
+ }
119
+ /**
120
+ * Initialize a git repo in a directory (for git tests)
121
+ * Uses execSync with static arguments - safe for test utilities
122
+ */
123
+ export function initGitRepo(dir) {
124
+ execSync("git init", { cwd: dir, stdio: "pipe" });
125
+ execSync("git config user.email 'test@test.com'", { cwd: dir, stdio: "pipe" });
126
+ execSync("git config user.name 'Test'", { cwd: dir, stdio: "pipe" });
127
+ }
128
+ /**
129
+ * Make a git commit in a directory
130
+ * Uses execSync with static arguments - safe for test utilities
131
+ */
132
+ export function gitCommit(dir, message = "Initial commit") {
133
+ execSync("git add -A", { cwd: dir, stdio: "pipe" });
134
+ // Use array-based command to safely handle the message
135
+ execSync(`git commit -m "${message.replace(/"/g, '\\"')}" --allow-empty`, { cwd: dir, stdio: "pipe" });
136
+ }
@@ -0,0 +1,76 @@
1
+ import { z } from "zod";
2
+ import { EgressSummary } from "../output/writer.js";
3
+ export declare const SecondOpinionInputSchema: z.ZodObject<{
4
+ provider: z.ZodEnum<["gemini", "openai"]>;
5
+ projectPath: z.ZodString;
6
+ task: z.ZodOptional<z.ZodString>;
7
+ sessionId: z.ZodOptional<z.ZodString>;
8
+ includeFiles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
9
+ allowExternalFiles: z.ZodDefault<z.ZodBoolean>;
10
+ includeConversation: z.ZodDefault<z.ZodBoolean>;
11
+ includeDependencies: z.ZodDefault<z.ZodBoolean>;
12
+ includeDependents: z.ZodDefault<z.ZodBoolean>;
13
+ includeTests: z.ZodDefault<z.ZodBoolean>;
14
+ includeTypes: z.ZodDefault<z.ZodBoolean>;
15
+ maxTokens: z.ZodDefault<z.ZodNumber>;
16
+ sessionName: z.ZodOptional<z.ZodString>;
17
+ customPrompt: z.ZodOptional<z.ZodString>;
18
+ focusAreas: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
19
+ dryRun: z.ZodDefault<z.ZodBoolean>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ allowExternalFiles: boolean;
22
+ projectPath: string;
23
+ includeConversation: boolean;
24
+ includeDependencies: boolean;
25
+ includeDependents: boolean;
26
+ includeTests: boolean;
27
+ includeTypes: boolean;
28
+ maxTokens: number;
29
+ provider: "gemini" | "openai";
30
+ dryRun: boolean;
31
+ sessionId?: string | undefined;
32
+ includeFiles?: string[] | undefined;
33
+ task?: string | undefined;
34
+ sessionName?: string | undefined;
35
+ customPrompt?: string | undefined;
36
+ focusAreas?: string[] | undefined;
37
+ }, {
38
+ projectPath: string;
39
+ provider: "gemini" | "openai";
40
+ allowExternalFiles?: boolean | undefined;
41
+ sessionId?: string | undefined;
42
+ includeConversation?: boolean | undefined;
43
+ includeDependencies?: boolean | undefined;
44
+ includeDependents?: boolean | undefined;
45
+ includeTests?: boolean | undefined;
46
+ includeTypes?: boolean | undefined;
47
+ includeFiles?: string[] | undefined;
48
+ maxTokens?: number | undefined;
49
+ task?: string | undefined;
50
+ sessionName?: string | undefined;
51
+ customPrompt?: string | undefined;
52
+ focusAreas?: string[] | undefined;
53
+ dryRun?: boolean | undefined;
54
+ }>;
55
+ export type SecondOpinionInput = z.infer<typeof SecondOpinionInputSchema>;
56
+ export type { EgressSummary } from "../output/writer.js";
57
+ export interface SecondOpinionDryRunOutput {
58
+ dryRun: true;
59
+ provider: string;
60
+ summary: EgressSummary;
61
+ totalTokens: number;
62
+ }
63
+ export interface SecondOpinionOutput {
64
+ dryRun?: false;
65
+ review: string;
66
+ reviewFile: string;
67
+ egressManifestFile: string;
68
+ provider: string;
69
+ model: string;
70
+ tokensUsed?: number;
71
+ timestamp: string;
72
+ filesReviewed: number;
73
+ contextTokens: number;
74
+ summary: EgressSummary;
75
+ }
76
+ export declare function executeReview(input: SecondOpinionInput): Promise<SecondOpinionOutput | SecondOpinionDryRunOutput>;