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,481 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import { parseSession, findLatestSession, formatConversationContext, } from "./session.js";
|
|
5
|
+
import { getAllModifiedFiles } from "./git.js";
|
|
6
|
+
import { getDependenciesForFiles, getDependentsForFiles, isWithinProject, } from "./imports.js";
|
|
7
|
+
import { findTestFilesForFiles } from "./tests.js";
|
|
8
|
+
import { findTypeFilesForFiles } from "./types.js";
|
|
9
|
+
import { estimateTokens, BUDGET_ALLOCATION } from "../utils/tokens.js";
|
|
10
|
+
/**
|
|
11
|
+
* Expand tilde in paths to home directory
|
|
12
|
+
*/
|
|
13
|
+
function expandTilde(filePath) {
|
|
14
|
+
if (filePath.startsWith("~/")) {
|
|
15
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
16
|
+
}
|
|
17
|
+
return filePath;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Sensitive paths that should never be included, even when explicitly requested
|
|
21
|
+
*/
|
|
22
|
+
const SENSITIVE_PATH_PATTERNS = [
|
|
23
|
+
// Version control
|
|
24
|
+
/[/\\]\.git[/\\]?/i,
|
|
25
|
+
// SSH and encryption keys
|
|
26
|
+
/[/\\]\.ssh[/\\]?/i,
|
|
27
|
+
/[/\\]\.gnupg[/\\]?/i,
|
|
28
|
+
/[/\\]\.gpg[/\\]?/i,
|
|
29
|
+
/[/\\]id_rsa/i,
|
|
30
|
+
/[/\\]id_ed25519/i,
|
|
31
|
+
/[/\\]id_ecdsa/i,
|
|
32
|
+
/[/\\]\.pem$/i,
|
|
33
|
+
/[/\\]\.key$/i,
|
|
34
|
+
// Cloud provider configs
|
|
35
|
+
/[/\\]\.aws[/\\]?/i,
|
|
36
|
+
/[/\\]\.config[/\\](?:gcloud|gh|hub)[/\\]?/i,
|
|
37
|
+
/[/\\]\.kube[/\\]?/i,
|
|
38
|
+
/[/\\]\.docker[/\\]config\.json$/i,
|
|
39
|
+
// Package manager auth
|
|
40
|
+
/[/\\]\.netrc$/i,
|
|
41
|
+
/[/\\]\.npmrc$/i,
|
|
42
|
+
/[/\\]\.pypirc$/i,
|
|
43
|
+
// Credential files
|
|
44
|
+
/[/\\]credentials\.json$/i,
|
|
45
|
+
/[/\\]service[-_]?account.*\.json$/i,
|
|
46
|
+
/[/\\]\.credentials$/i,
|
|
47
|
+
/secrets\.(json|ya?ml)$/i,
|
|
48
|
+
// Environment files (commonly contain secrets)
|
|
49
|
+
/[/\\]\.env($|\.[^/\\]*$)/i, // .env, .env.local, .env.production, etc.
|
|
50
|
+
// Terraform/IaC secrets
|
|
51
|
+
/\.tfvars$/i,
|
|
52
|
+
/terraform\.tfstate/i,
|
|
53
|
+
// Kubernetes secrets
|
|
54
|
+
/secret\.ya?ml$/i,
|
|
55
|
+
// Shell history (may contain commands with secrets)
|
|
56
|
+
/\.(bash|zsh|sh)_history$/i,
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Check if a path points to a sensitive location
|
|
60
|
+
*/
|
|
61
|
+
function isSensitivePath(filePath) {
|
|
62
|
+
const normalized = path.normalize(filePath);
|
|
63
|
+
return SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
64
|
+
}
|
|
65
|
+
/** Maximum directory recursion depth to prevent stack overflow from deep/circular structures */
|
|
66
|
+
const MAX_EXPAND_DEPTH = 10;
|
|
67
|
+
/**
|
|
68
|
+
* Expand a path to a list of files (handles directories recursively)
|
|
69
|
+
* Returns object with files and any blocked paths
|
|
70
|
+
*/
|
|
71
|
+
function expandPath(inputPath, projectPath, options, depth = 0) {
|
|
72
|
+
const result = { files: [], blocked: [] };
|
|
73
|
+
// Guard against excessively deep directory structures
|
|
74
|
+
if (depth >= MAX_EXPAND_DEPTH) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
// Expand tilde
|
|
78
|
+
let expandedPath = expandTilde(inputPath);
|
|
79
|
+
// Make relative paths absolute (relative to project)
|
|
80
|
+
if (!path.isAbsolute(expandedPath)) {
|
|
81
|
+
expandedPath = path.join(projectPath, expandedPath);
|
|
82
|
+
}
|
|
83
|
+
// Normalize the path
|
|
84
|
+
expandedPath = path.normalize(expandedPath);
|
|
85
|
+
// Block sensitive paths
|
|
86
|
+
if (isSensitivePath(expandedPath)) {
|
|
87
|
+
result.blocked.push({ path: expandedPath, reason: "sensitive_path" });
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
if (!fs.existsSync(expandedPath)) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
// Resolve symlinks to prevent escaping to sensitive locations
|
|
94
|
+
let realPath;
|
|
95
|
+
try {
|
|
96
|
+
realPath = fs.realpathSync(expandedPath);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Can't resolve path, skip it
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
// Check the resolved path against sensitive patterns
|
|
103
|
+
if (isSensitivePath(realPath)) {
|
|
104
|
+
result.blocked.push({ path: expandedPath, reason: "sensitive_path" });
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
// Check if file is outside project bounds (unless allowExternalFiles is true)
|
|
108
|
+
if (!options?.allowExternalFiles && !isWithinProject(realPath, projectPath)) {
|
|
109
|
+
result.blocked.push({
|
|
110
|
+
path: expandedPath,
|
|
111
|
+
reason: "outside_project_requires_allowExternalFiles",
|
|
112
|
+
});
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
const stat = fs.statSync(realPath);
|
|
116
|
+
if (stat.isFile()) {
|
|
117
|
+
result.files.push(realPath);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
if (stat.isDirectory()) {
|
|
121
|
+
const entries = fs.readdirSync(realPath, { withFileTypes: true });
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
// Skip hidden files and common non-code directories
|
|
124
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const fullPath = path.join(realPath, entry.name);
|
|
128
|
+
// Resolve symlinks for each entry
|
|
129
|
+
let entryRealPath;
|
|
130
|
+
try {
|
|
131
|
+
entryRealPath = fs.realpathSync(fullPath);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
// Check resolved path for sensitivity
|
|
137
|
+
if (isSensitivePath(entryRealPath)) {
|
|
138
|
+
result.blocked.push({ path: fullPath, reason: "sensitive_path" });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Check project bounds for entries too
|
|
142
|
+
if (!options?.allowExternalFiles &&
|
|
143
|
+
!isWithinProject(entryRealPath, projectPath)) {
|
|
144
|
+
result.blocked.push({
|
|
145
|
+
path: fullPath,
|
|
146
|
+
reason: "outside_project_requires_allowExternalFiles",
|
|
147
|
+
});
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const entryStat = fs.statSync(entryRealPath);
|
|
151
|
+
if (entryStat.isFile()) {
|
|
152
|
+
result.files.push(entryRealPath);
|
|
153
|
+
}
|
|
154
|
+
else if (entryStat.isDirectory()) {
|
|
155
|
+
// Recursively expand subdirectories
|
|
156
|
+
const subResult = expandPath(entryRealPath, projectPath, options, depth + 1);
|
|
157
|
+
result.files.push(...subResult.files);
|
|
158
|
+
result.blocked.push(...subResult.blocked);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Read a file and create a FileEntry
|
|
166
|
+
*/
|
|
167
|
+
function readFileEntry(filePath, category, existingContent) {
|
|
168
|
+
try {
|
|
169
|
+
const content = existingContent || fs.readFileSync(filePath, "utf-8");
|
|
170
|
+
return {
|
|
171
|
+
path: filePath,
|
|
172
|
+
content,
|
|
173
|
+
category,
|
|
174
|
+
tokenEstimate: estimateTokens(content),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Collect and bundle all context for review
|
|
183
|
+
*/
|
|
184
|
+
export async function bundleContext(options) {
|
|
185
|
+
const { projectPath, sessionId, includeConversation = true, includeDependencies = true, includeDependents = true, includeTests = true, includeTypes = true, includeFiles = [], allowExternalFiles = false, maxTokens = 100000, } = options;
|
|
186
|
+
const bundle = {
|
|
187
|
+
conversationContext: "",
|
|
188
|
+
files: [],
|
|
189
|
+
omittedFiles: [],
|
|
190
|
+
totalTokens: 0,
|
|
191
|
+
categories: {
|
|
192
|
+
session: 0,
|
|
193
|
+
git: 0,
|
|
194
|
+
dependency: 0,
|
|
195
|
+
dependent: 0,
|
|
196
|
+
test: 0,
|
|
197
|
+
type: 0,
|
|
198
|
+
explicit: 0,
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
// Track files we've already added
|
|
202
|
+
const addedFiles = new Set();
|
|
203
|
+
// 1. Get session context first to know conversation size
|
|
204
|
+
let sessionContext = null;
|
|
205
|
+
const sid = sessionId || findLatestSession(projectPath);
|
|
206
|
+
if (sid) {
|
|
207
|
+
sessionContext = parseSession(projectPath, sid);
|
|
208
|
+
}
|
|
209
|
+
// Add conversation context and calculate remaining budget
|
|
210
|
+
let remainingBudget = maxTokens;
|
|
211
|
+
if (includeConversation && sessionContext) {
|
|
212
|
+
bundle.conversationContext = formatConversationContext(sessionContext);
|
|
213
|
+
const conversationTokens = estimateTokens(bundle.conversationContext);
|
|
214
|
+
bundle.totalTokens += conversationTokens;
|
|
215
|
+
remainingBudget -= conversationTokens;
|
|
216
|
+
}
|
|
217
|
+
// Calculate budget for each category from remaining tokens
|
|
218
|
+
const budgets = {
|
|
219
|
+
explicit: Math.floor(remainingBudget * BUDGET_ALLOCATION.explicit),
|
|
220
|
+
session: Math.floor(remainingBudget * BUDGET_ALLOCATION.session),
|
|
221
|
+
git: Math.floor(remainingBudget * BUDGET_ALLOCATION.git),
|
|
222
|
+
dependency: Math.floor(remainingBudget * BUDGET_ALLOCATION.dependency),
|
|
223
|
+
dependent: Math.floor(remainingBudget * BUDGET_ALLOCATION.dependent),
|
|
224
|
+
test: Math.floor(remainingBudget * BUDGET_ALLOCATION.test),
|
|
225
|
+
type: Math.floor(remainingBudget * BUDGET_ALLOCATION.type),
|
|
226
|
+
};
|
|
227
|
+
// Helper to add files within a budget
|
|
228
|
+
const addFilesWithBudget = (files, category, budget, options) => {
|
|
229
|
+
let used = 0;
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
// Bounds check: skip files outside project (unless explicitly included)
|
|
232
|
+
if (!options?.skipBoundsCheck && !isWithinProject(file.path, projectPath)) {
|
|
233
|
+
bundle.omittedFiles.push({
|
|
234
|
+
path: file.path,
|
|
235
|
+
category,
|
|
236
|
+
tokenEstimate: file.tokenEstimate,
|
|
237
|
+
reason: "outside_project",
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (used + file.tokenEstimate > budget) {
|
|
242
|
+
// Track omitted files that exceeded budget
|
|
243
|
+
bundle.omittedFiles.push({
|
|
244
|
+
path: file.path,
|
|
245
|
+
category,
|
|
246
|
+
tokenEstimate: file.tokenEstimate,
|
|
247
|
+
reason: "budget_exceeded",
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!addedFiles.has(file.path)) {
|
|
252
|
+
bundle.files.push(file);
|
|
253
|
+
addedFiles.add(file.path);
|
|
254
|
+
used += file.tokenEstimate;
|
|
255
|
+
bundle.categories[category] += file.tokenEstimate;
|
|
256
|
+
bundle.totalTokens += file.tokenEstimate;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
// 1a. Process explicitly included files first (highest priority)
|
|
261
|
+
if (includeFiles.length > 0) {
|
|
262
|
+
const explicitFiles = [];
|
|
263
|
+
for (const inputPath of includeFiles) {
|
|
264
|
+
const { files: expandedPaths, blocked } = expandPath(inputPath, projectPath, {
|
|
265
|
+
allowExternalFiles,
|
|
266
|
+
});
|
|
267
|
+
// Track blocked paths (sensitive or outside project)
|
|
268
|
+
for (const blockedFile of blocked) {
|
|
269
|
+
bundle.omittedFiles.push({
|
|
270
|
+
path: blockedFile.path,
|
|
271
|
+
category: "explicit",
|
|
272
|
+
tokenEstimate: 0,
|
|
273
|
+
reason: blockedFile.reason,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
for (const filePath of expandedPaths) {
|
|
277
|
+
const entry = readFileEntry(filePath, "explicit");
|
|
278
|
+
if (entry) {
|
|
279
|
+
explicitFiles.push(entry);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Explicit files skip bounds check since expandPath already checked
|
|
284
|
+
addFilesWithBudget(explicitFiles, "explicit", budgets.explicit, { skipBoundsCheck: true });
|
|
285
|
+
}
|
|
286
|
+
// 2. Collect session files (files Claude read/edited/wrote)
|
|
287
|
+
const sessionFiles = [];
|
|
288
|
+
const modifiedFiles = [];
|
|
289
|
+
if (sessionContext) {
|
|
290
|
+
// Files from session - use content Claude saw if available
|
|
291
|
+
const allSessionFiles = [
|
|
292
|
+
...sessionContext.filesWritten,
|
|
293
|
+
...sessionContext.filesEdited,
|
|
294
|
+
...sessionContext.filesRead,
|
|
295
|
+
];
|
|
296
|
+
for (const filePath of allSessionFiles) {
|
|
297
|
+
const cachedContent = sessionContext.fileContents.get(filePath);
|
|
298
|
+
const entry = readFileEntry(filePath, "session", cachedContent);
|
|
299
|
+
if (entry) {
|
|
300
|
+
sessionFiles.push(entry);
|
|
301
|
+
if (sessionContext.filesWritten.includes(filePath) ||
|
|
302
|
+
sessionContext.filesEdited.includes(filePath)) {
|
|
303
|
+
modifiedFiles.push(filePath);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
addFilesWithBudget(sessionFiles, "session", budgets.session);
|
|
309
|
+
// 3. Git changes not in session
|
|
310
|
+
const gitChangedFiles = getAllModifiedFiles(projectPath);
|
|
311
|
+
const gitFiles = [];
|
|
312
|
+
for (const filePath of gitChangedFiles) {
|
|
313
|
+
if (!addedFiles.has(filePath)) {
|
|
314
|
+
const entry = readFileEntry(filePath, "git");
|
|
315
|
+
if (entry) {
|
|
316
|
+
gitFiles.push(entry);
|
|
317
|
+
modifiedFiles.push(filePath);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
addFilesWithBudget(gitFiles, "git", budgets.git);
|
|
322
|
+
// 4. Dependencies (files imported by modified files)
|
|
323
|
+
if (includeDependencies && modifiedFiles.length > 0) {
|
|
324
|
+
const deps = getDependenciesForFiles(modifiedFiles, projectPath);
|
|
325
|
+
const depFiles = [];
|
|
326
|
+
for (const dep of deps) {
|
|
327
|
+
if (!addedFiles.has(dep)) {
|
|
328
|
+
const entry = readFileEntry(dep, "dependency");
|
|
329
|
+
if (entry) {
|
|
330
|
+
depFiles.push(entry);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Sort by token count (smaller files first to fit more)
|
|
335
|
+
depFiles.sort((a, b) => a.tokenEstimate - b.tokenEstimate);
|
|
336
|
+
addFilesWithBudget(depFiles, "dependency", budgets.dependency);
|
|
337
|
+
}
|
|
338
|
+
// 5. Dependents (files that import modified files)
|
|
339
|
+
if (includeDependents && modifiedFiles.length > 0) {
|
|
340
|
+
const dependents = await getDependentsForFiles(modifiedFiles, projectPath);
|
|
341
|
+
const depFiles = [];
|
|
342
|
+
for (const dep of dependents) {
|
|
343
|
+
if (!addedFiles.has(dep)) {
|
|
344
|
+
const entry = readFileEntry(dep, "dependent");
|
|
345
|
+
if (entry) {
|
|
346
|
+
depFiles.push(entry);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
depFiles.sort((a, b) => a.tokenEstimate - b.tokenEstimate);
|
|
351
|
+
addFilesWithBudget(depFiles, "dependent", budgets.dependent);
|
|
352
|
+
}
|
|
353
|
+
// 6. Test files
|
|
354
|
+
if (includeTests && modifiedFiles.length > 0) {
|
|
355
|
+
const tests = findTestFilesForFiles(modifiedFiles, projectPath);
|
|
356
|
+
const testFiles = [];
|
|
357
|
+
for (const test of tests) {
|
|
358
|
+
if (!addedFiles.has(test)) {
|
|
359
|
+
const entry = readFileEntry(test, "test");
|
|
360
|
+
if (entry) {
|
|
361
|
+
testFiles.push(entry);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
addFilesWithBudget(testFiles, "test", budgets.test);
|
|
366
|
+
}
|
|
367
|
+
// 7. Type files
|
|
368
|
+
if (includeTypes && modifiedFiles.length > 0) {
|
|
369
|
+
const types = await findTypeFilesForFiles(modifiedFiles, projectPath);
|
|
370
|
+
const typeFiles = [];
|
|
371
|
+
for (const typeFile of types) {
|
|
372
|
+
if (!addedFiles.has(typeFile)) {
|
|
373
|
+
const entry = readFileEntry(typeFile, "type");
|
|
374
|
+
if (entry) {
|
|
375
|
+
typeFiles.push(entry);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
addFilesWithBudget(typeFiles, "type", budgets.type);
|
|
380
|
+
}
|
|
381
|
+
return bundle;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Format the bundle as markdown for the reviewer
|
|
385
|
+
*/
|
|
386
|
+
export function formatBundleAsMarkdown(bundle, projectPath) {
|
|
387
|
+
const lines = [];
|
|
388
|
+
// Conversation context
|
|
389
|
+
if (bundle.conversationContext) {
|
|
390
|
+
lines.push(bundle.conversationContext);
|
|
391
|
+
lines.push("---\n");
|
|
392
|
+
}
|
|
393
|
+
// Group files by category
|
|
394
|
+
const categories = {
|
|
395
|
+
explicit: [],
|
|
396
|
+
session: [],
|
|
397
|
+
git: [],
|
|
398
|
+
dependency: [],
|
|
399
|
+
dependent: [],
|
|
400
|
+
test: [],
|
|
401
|
+
type: [],
|
|
402
|
+
};
|
|
403
|
+
for (const file of bundle.files) {
|
|
404
|
+
categories[file.category].push(file);
|
|
405
|
+
}
|
|
406
|
+
const categoryLabels = {
|
|
407
|
+
explicit: "Explicitly Included Files",
|
|
408
|
+
session: "Modified Files (from Claude session)",
|
|
409
|
+
git: "Additional Git Changes",
|
|
410
|
+
dependency: "Dependencies (files imported by modified code)",
|
|
411
|
+
dependent: "Dependents (files that import modified code)",
|
|
412
|
+
test: "Related Tests",
|
|
413
|
+
type: "Type Definitions",
|
|
414
|
+
};
|
|
415
|
+
for (const [category, files] of Object.entries(categories)) {
|
|
416
|
+
if (files.length === 0)
|
|
417
|
+
continue;
|
|
418
|
+
lines.push(`## ${categoryLabels[category]}\n`);
|
|
419
|
+
for (const file of files) {
|
|
420
|
+
const relativePath = path.relative(projectPath, file.path);
|
|
421
|
+
const ext = path.extname(file.path).slice(1) || "txt";
|
|
422
|
+
lines.push(`### ${relativePath}\n`);
|
|
423
|
+
lines.push("```" + ext);
|
|
424
|
+
lines.push(file.content);
|
|
425
|
+
lines.push("```\n");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Summary
|
|
429
|
+
lines.push("---\n");
|
|
430
|
+
lines.push("## Context Summary\n");
|
|
431
|
+
lines.push(`- **Total files:** ${bundle.files.length}`);
|
|
432
|
+
lines.push(`- **Estimated tokens:** ${bundle.totalTokens.toLocaleString()}`);
|
|
433
|
+
lines.push(`- **Breakdown:**`);
|
|
434
|
+
for (const [cat, tokens] of Object.entries(bundle.categories)) {
|
|
435
|
+
if (tokens > 0) {
|
|
436
|
+
lines.push(` - ${cat}: ${tokens.toLocaleString()} tokens`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Report omitted files
|
|
440
|
+
if (bundle.omittedFiles.length > 0) {
|
|
441
|
+
lines.push("");
|
|
442
|
+
lines.push("### Omitted Files\n");
|
|
443
|
+
lines.push("The following files were not included due to token budget constraints or security restrictions:\n");
|
|
444
|
+
const byReason = {
|
|
445
|
+
budget_exceeded: bundle.omittedFiles.filter((f) => f.reason === "budget_exceeded"),
|
|
446
|
+
outside_project: bundle.omittedFiles.filter((f) => f.reason === "outside_project"),
|
|
447
|
+
sensitive_path: bundle.omittedFiles.filter((f) => f.reason === "sensitive_path"),
|
|
448
|
+
outside_project_requires_allowExternalFiles: bundle.omittedFiles.filter((f) => f.reason === "outside_project_requires_allowExternalFiles"),
|
|
449
|
+
};
|
|
450
|
+
if (byReason.sensitive_path.length > 0) {
|
|
451
|
+
lines.push("**Blocked (sensitive path):**");
|
|
452
|
+
for (const file of byReason.sensitive_path) {
|
|
453
|
+
lines.push(`- ${file.path}`);
|
|
454
|
+
}
|
|
455
|
+
lines.push("");
|
|
456
|
+
}
|
|
457
|
+
if (byReason.outside_project_requires_allowExternalFiles.length > 0) {
|
|
458
|
+
lines.push("**Blocked (outside project - set allowExternalFiles: true to include):**");
|
|
459
|
+
for (const file of byReason.outside_project_requires_allowExternalFiles) {
|
|
460
|
+
lines.push(`- ${file.path}`);
|
|
461
|
+
}
|
|
462
|
+
lines.push("");
|
|
463
|
+
}
|
|
464
|
+
if (byReason.budget_exceeded.length > 0) {
|
|
465
|
+
lines.push("**Budget exceeded:**");
|
|
466
|
+
for (const file of byReason.budget_exceeded) {
|
|
467
|
+
const relativePath = path.relative(projectPath, file.path);
|
|
468
|
+
lines.push(`- ${relativePath} (${file.category}, ~${file.tokenEstimate.toLocaleString()} tokens)`);
|
|
469
|
+
}
|
|
470
|
+
lines.push("");
|
|
471
|
+
}
|
|
472
|
+
if (byReason.outside_project.length > 0) {
|
|
473
|
+
lines.push("**Outside project bounds:**");
|
|
474
|
+
for (const file of byReason.outside_project) {
|
|
475
|
+
lines.push(`- ${file.path} (${file.category})`);
|
|
476
|
+
}
|
|
477
|
+
lines.push("");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return lines.join("\n");
|
|
481
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|