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.
- package/README.md +323 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +84 -0
- package/dist/context/bundler.d.ts +51 -0
- package/dist/context/bundler.js +481 -0
- package/dist/context/bundler.test.d.ts +1 -0
- package/dist/context/bundler.test.js +275 -0
- package/dist/context/git.d.ts +21 -0
- package/dist/context/git.js +102 -0
- package/dist/context/imports.d.ts +43 -0
- package/dist/context/imports.js +197 -0
- package/dist/context/imports.test.d.ts +1 -0
- package/dist/context/imports.test.js +147 -0
- package/dist/context/index.d.ts +6 -0
- package/dist/context/index.js +6 -0
- package/dist/context/session.d.ts +35 -0
- package/dist/context/session.js +317 -0
- package/dist/context/tests.d.ts +13 -0
- package/dist/context/tests.js +83 -0
- package/dist/context/types.d.ts +16 -0
- package/dist/context/types.js +100 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/output/writer.d.ts +34 -0
- package/dist/output/writer.js +162 -0
- package/dist/output/writer.test.d.ts +1 -0
- package/dist/output/writer.test.js +175 -0
- package/dist/providers/base.d.ts +24 -0
- package/dist/providers/base.js +77 -0
- package/dist/providers/base.test.d.ts +1 -0
- package/dist/providers/base.test.js +91 -0
- package/dist/providers/gemini.d.ts +8 -0
- package/dist/providers/gemini.js +43 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +31 -0
- package/dist/providers/openai.d.ts +8 -0
- package/dist/providers/openai.js +39 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +181 -0
- package/dist/test-utils.d.ts +71 -0
- package/dist/test-utils.js +136 -0
- package/dist/tools/review.d.ts +76 -0
- package/dist/tools/review.js +199 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/tokens.d.ts +26 -0
- package/dist/utils/tokens.js +27 -0
- package/package.json +61 -0
- package/scripts/install-config.js +51 -0
- package/second-opinion.skill.md +34 -0
- 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
|
+
}
|
package/dist/server.d.ts
ADDED
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>;
|