feat: add automated bug report generator with Claude Agent SDK
Add npm run bug-report command that: - Collects comprehensive system diagnostics (versions, platform, worker status, logs, config) - Prompts for issue description with multiline input support - Auto-translates foreign languages to English - Generates formatted GitHub issue using Claude Agent SDK - Streams character count with animated progress - Auto-sanitizes paths for privacy - Automatically opens GitHub issue form with pre-filled title and body - Saves timestamped report locally Usage: npm run bug-report # Interactive bug report npm run bug-report --no-logs # Skip logs for privacy npm run bug-report --verbose # Show all diagnostics npm run bug-report --help # Show help Files: - scripts/bug-report/cli.ts - Interactive CLI entry point - scripts/bug-report/index.ts - Core logic with Agent SDK - scripts/bug-report/collector.ts - System diagnostics collector - package.json - Added bug-report script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
+2
-1
@@ -47,7 +47,8 @@
|
||||
"changelog:generate": "node scripts/generate-changelog.js",
|
||||
"usage:analyze": "node scripts/analyze-usage.js",
|
||||
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)",
|
||||
"translate-readme": "npx tsx scripts/translate-readme/cli.ts -v README.md zh ko ja"
|
||||
"translate-readme": "npx tsx scripts/translate-readme/cli.ts -v README.md zh ko ja",
|
||||
"bug-report": "npx tsx scripts/bug-report/cli.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.1.67",
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { generateBugReport } from "./index.ts";
|
||||
import { collectDiagnostics } from "./collector.ts";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import * as os from "os";
|
||||
import * as readline from "readline";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
interface CliArgs {
|
||||
output?: string;
|
||||
verbose: boolean;
|
||||
noLogs: boolean;
|
||||
help: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(): CliArgs {
|
||||
const args = process.argv.slice(2);
|
||||
const parsed: CliArgs = {
|
||||
verbose: false,
|
||||
noLogs: false,
|
||||
help: false,
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
switch (arg) {
|
||||
case "-h":
|
||||
case "--help":
|
||||
parsed.help = true;
|
||||
break;
|
||||
case "-v":
|
||||
case "--verbose":
|
||||
parsed.verbose = true;
|
||||
break;
|
||||
case "--no-logs":
|
||||
parsed.noLogs = true;
|
||||
break;
|
||||
case "-o":
|
||||
case "--output":
|
||||
parsed.output = args[++i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function printHelp(): void {
|
||||
console.log(`
|
||||
bug-report - Generate bug reports for claude-mem
|
||||
|
||||
USAGE:
|
||||
npm run bug-report [options]
|
||||
|
||||
OPTIONS:
|
||||
-o, --output <file> Save report to file (default: stdout + timestamped file)
|
||||
-v, --verbose Show all collected diagnostics
|
||||
--no-logs Skip log collection (for privacy)
|
||||
-h, --help Show this help message
|
||||
|
||||
DESCRIPTION:
|
||||
This script collects system diagnostics, prompts you for issue details,
|
||||
and generates a formatted GitHub issue for claude-mem using the Claude Agent SDK.
|
||||
|
||||
The generated report will be saved to ~/bug-report-YYYY-MM-DD-HHMMSS.md
|
||||
and displayed in your terminal for easy copy-pasting to GitHub.
|
||||
|
||||
EXAMPLES:
|
||||
# Generate a bug report interactively
|
||||
npm run bug-report
|
||||
|
||||
# Generate without including logs (for privacy)
|
||||
npm run bug-report --no-logs
|
||||
|
||||
# Save to a specific file
|
||||
npm run bug-report --output ~/my-bug-report.md
|
||||
|
||||
# Show all diagnostic details during collection
|
||||
npm run bug-report --verbose
|
||||
`);
|
||||
}
|
||||
|
||||
async function promptUser(question: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function promptMultiline(prompt: string): Promise<string> {
|
||||
console.log(prompt);
|
||||
console.log("(Press Enter on an empty line to finish)\n");
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const lines: string[] = [];
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.on("line", (line) => {
|
||||
// Empty line means we're done
|
||||
if (line.trim() === "" && lines.length > 0) {
|
||||
rl.close();
|
||||
resolve(lines.join("\n"));
|
||||
} else if (line.trim() !== "") {
|
||||
// Only add non-empty lines (or preserve empty lines in the middle)
|
||||
lines.push(line);
|
||||
}
|
||||
});
|
||||
|
||||
rl.on("close", () => {
|
||||
resolve(lines.join("\n"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs();
|
||||
|
||||
if (args.help) {
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("🌎 Leave report in ANY language, and it will auto translate to English\n");
|
||||
console.log("🔍 Collecting system diagnostics...");
|
||||
|
||||
// Collect diagnostics
|
||||
const diagnostics = await collectDiagnostics({
|
||||
includeLogs: !args.noLogs,
|
||||
});
|
||||
|
||||
console.log("✓ Version information collected");
|
||||
console.log("✓ Platform details collected");
|
||||
console.log("✓ Worker status checked");
|
||||
if (!args.noLogs) {
|
||||
console.log(
|
||||
`✓ Logs extracted (last ${diagnostics.logs.workerLog.length + diagnostics.logs.silentLog.length} lines)`
|
||||
);
|
||||
}
|
||||
console.log("✓ Configuration loaded\n");
|
||||
|
||||
// Show summary
|
||||
console.log("📋 System Summary:");
|
||||
console.log(` Claude-mem: v${diagnostics.versions.claudeMem}`);
|
||||
console.log(` Claude Code: ${diagnostics.versions.claudeCode}`);
|
||||
console.log(
|
||||
` Platform: ${diagnostics.platform.osVersion} (${diagnostics.platform.arch})`
|
||||
);
|
||||
console.log(
|
||||
` Worker: ${diagnostics.worker.running ? `Running (PID ${diagnostics.worker.pid}, port ${diagnostics.worker.port})` : "Not running"}\n`
|
||||
);
|
||||
|
||||
if (args.verbose) {
|
||||
console.log("📊 Detailed Diagnostics:");
|
||||
console.log(JSON.stringify(diagnostics, null, 2));
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Prompt for issue details
|
||||
const issueDescription = await promptMultiline(
|
||||
"Please describe the issue you're experiencing:"
|
||||
);
|
||||
|
||||
if (!issueDescription.trim()) {
|
||||
console.error("❌ Issue description is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log();
|
||||
const expectedBehavior = await promptMultiline(
|
||||
"Expected behavior (leave blank to skip):"
|
||||
);
|
||||
|
||||
console.log();
|
||||
const stepsToReproduce = await promptMultiline(
|
||||
"Steps to reproduce (leave blank to skip):"
|
||||
);
|
||||
|
||||
console.log();
|
||||
const confirm = await promptUser(
|
||||
"Generate bug report? (y/n): "
|
||||
);
|
||||
|
||||
if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
|
||||
console.log("❌ Bug report generation cancelled");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("\n🤖 Generating bug report with Claude...");
|
||||
|
||||
// Generate the bug report
|
||||
const result = await generateBugReport({
|
||||
issueDescription,
|
||||
expectedBehavior: expectedBehavior.trim() || undefined,
|
||||
stepsToReproduce: stepsToReproduce.trim() || undefined,
|
||||
includeLogs: !args.noLogs,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
console.error("❌ Failed to generate bug report:", result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("✓ Issue formatted successfully\n");
|
||||
|
||||
// Generate output file path
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/:/g, "")
|
||||
.replace(/\..+/, "")
|
||||
.replace("T", "-");
|
||||
const defaultOutputPath = path.join(
|
||||
os.homedir(),
|
||||
`bug-report-${timestamp}.md`
|
||||
);
|
||||
const outputPath = args.output || defaultOutputPath;
|
||||
|
||||
// Save to file
|
||||
await fs.writeFile(outputPath, result.body, "utf-8");
|
||||
|
||||
// Build GitHub URL with pre-filled title and body
|
||||
const encodedTitle = encodeURIComponent(result.title);
|
||||
const encodedBody = encodeURIComponent(result.body);
|
||||
const githubUrl = `https://github.com/thedotmack/claude-mem/issues/new?title=${encodedTitle}&body=${encodedBody}`;
|
||||
|
||||
// Display the report
|
||||
console.log("─".repeat(60));
|
||||
console.log("📋 BUG REPORT GENERATED");
|
||||
console.log("─".repeat(60));
|
||||
console.log();
|
||||
console.log(result.body);
|
||||
console.log();
|
||||
console.log("─".repeat(60));
|
||||
console.log("Suggested labels: bug, needs-triage");
|
||||
console.log(`Report saved to: ${outputPath}`);
|
||||
console.log("─".repeat(60));
|
||||
console.log();
|
||||
|
||||
// Open GitHub issue in browser
|
||||
console.log("🌐 Opening GitHub issue form in your browser...");
|
||||
try {
|
||||
const openCommand =
|
||||
process.platform === "darwin"
|
||||
? "open"
|
||||
: process.platform === "win32"
|
||||
? "start"
|
||||
: "xdg-open";
|
||||
|
||||
await execAsync(`${openCommand} "${githubUrl}"`);
|
||||
console.log("✓ Browser opened successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to open browser. Please visit:");
|
||||
console.error(githubUrl);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,364 @@
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import * as os from "os";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface SystemDiagnostics {
|
||||
versions: {
|
||||
claudeMem: string;
|
||||
claudeCode: string;
|
||||
node: string;
|
||||
bun: string;
|
||||
};
|
||||
platform: {
|
||||
os: string;
|
||||
osVersion: string;
|
||||
arch: string;
|
||||
};
|
||||
paths: {
|
||||
pluginPath: string;
|
||||
dataDir: string;
|
||||
cwd: string;
|
||||
isDevMode: boolean;
|
||||
};
|
||||
worker: {
|
||||
running: boolean;
|
||||
pid?: number;
|
||||
port?: number;
|
||||
uptime?: number;
|
||||
version?: string;
|
||||
health?: any;
|
||||
stats?: any;
|
||||
};
|
||||
logs: {
|
||||
workerLog: string[];
|
||||
silentLog: string[];
|
||||
};
|
||||
database: {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
size?: number;
|
||||
counts?: {
|
||||
observations: number;
|
||||
sessions: number;
|
||||
summaries: number;
|
||||
};
|
||||
};
|
||||
config: {
|
||||
settingsPath: string;
|
||||
settingsExist: boolean;
|
||||
settings?: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizePath(filePath: string): string {
|
||||
const homeDir = os.homedir();
|
||||
return filePath.replace(homeDir, "~");
|
||||
}
|
||||
|
||||
async function getClaudememVersion(): Promise<string> {
|
||||
try {
|
||||
const packageJsonPath = path.join(process.cwd(), "package.json");
|
||||
const content = await fs.readFile(packageJsonPath, "utf-8");
|
||||
const pkg = JSON.parse(content);
|
||||
return pkg.version || "unknown";
|
||||
} catch (error) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async function getClaudeCodeVersion(): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync("claude --version");
|
||||
return stdout.trim();
|
||||
} catch (error) {
|
||||
return "not installed or not in PATH";
|
||||
}
|
||||
}
|
||||
|
||||
async function getBunVersion(): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync("bun --version");
|
||||
return stdout.trim();
|
||||
} catch (error) {
|
||||
return "not installed";
|
||||
}
|
||||
}
|
||||
|
||||
async function getOsVersion(): Promise<string> {
|
||||
try {
|
||||
if (process.platform === "darwin") {
|
||||
const { stdout } = await execAsync("sw_vers -productVersion");
|
||||
return `macOS ${stdout.trim()}`;
|
||||
} else if (process.platform === "linux") {
|
||||
const { stdout } = await execAsync("uname -sr");
|
||||
return stdout.trim();
|
||||
} else if (process.platform === "win32") {
|
||||
const { stdout } = await execAsync("ver");
|
||||
return stdout.trim();
|
||||
}
|
||||
return "unknown";
|
||||
} catch (error) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
async function checkWorkerHealth(port: number): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getWorkerStats(port: number): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/api/stats`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function readPidFile(dataDir: string): Promise<any> {
|
||||
try {
|
||||
const pidPath = path.join(dataDir, "worker.pid");
|
||||
const content = await fs.readFile(pidPath, "utf-8");
|
||||
return JSON.parse(content);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function readLogLines(logPath: string, lines: number): Promise<string[]> {
|
||||
try {
|
||||
const content = await fs.readFile(logPath, "utf-8");
|
||||
const allLines = content.split("\n").filter((line) => line.trim());
|
||||
return allLines.slice(-lines);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getSettings(
|
||||
dataDir: string
|
||||
): Promise<{ exists: boolean; settings?: Record<string, any> }> {
|
||||
try {
|
||||
const settingsPath = path.join(dataDir, "settings.json");
|
||||
const content = await fs.readFile(settingsPath, "utf-8");
|
||||
const settings = JSON.parse(content);
|
||||
return { exists: true, settings };
|
||||
} catch (error) {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
async function getDatabaseInfo(
|
||||
dataDir: string
|
||||
): Promise<{ exists: boolean; size?: number }> {
|
||||
try {
|
||||
const dbPath = path.join(dataDir, "claude-mem.db");
|
||||
const stats = await fs.stat(dbPath);
|
||||
return { exists: true, size: stats.size };
|
||||
} catch (error) {
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectDiagnostics(
|
||||
options: { includeLogs?: boolean } = {}
|
||||
): Promise<SystemDiagnostics> {
|
||||
const homeDir = os.homedir();
|
||||
const dataDir = path.join(homeDir, ".claude-mem");
|
||||
const pluginPath = path.join(
|
||||
homeDir,
|
||||
".claude",
|
||||
"plugins",
|
||||
"marketplaces",
|
||||
"thedotmack"
|
||||
);
|
||||
const cwd = process.cwd();
|
||||
const isDevMode = cwd.includes("claude-mem") && !cwd.includes(".claude");
|
||||
|
||||
// Collect version information
|
||||
const [claudeMem, claudeCode, bun, osVersion] = await Promise.all([
|
||||
getClaudememVersion(),
|
||||
getClaudeCodeVersion(),
|
||||
getBunVersion(),
|
||||
getOsVersion(),
|
||||
]);
|
||||
|
||||
const versions = {
|
||||
claudeMem,
|
||||
claudeCode,
|
||||
node: process.version,
|
||||
bun,
|
||||
};
|
||||
|
||||
const platform = {
|
||||
os: process.platform,
|
||||
osVersion,
|
||||
arch: process.arch,
|
||||
};
|
||||
|
||||
const paths = {
|
||||
pluginPath: sanitizePath(pluginPath),
|
||||
dataDir: sanitizePath(dataDir),
|
||||
cwd: sanitizePath(cwd),
|
||||
isDevMode,
|
||||
};
|
||||
|
||||
// Check worker status
|
||||
const pidInfo = await readPidFile(dataDir);
|
||||
const workerPort = pidInfo?.port || 37777;
|
||||
|
||||
const [health, stats] = await Promise.all([
|
||||
checkWorkerHealth(workerPort),
|
||||
getWorkerStats(workerPort),
|
||||
]);
|
||||
|
||||
const worker = {
|
||||
running: health !== null,
|
||||
pid: pidInfo?.pid,
|
||||
port: workerPort,
|
||||
uptime: stats?.worker?.uptime,
|
||||
version: stats?.worker?.version,
|
||||
health,
|
||||
stats,
|
||||
};
|
||||
|
||||
// Collect logs if requested
|
||||
let workerLog: string[] = [];
|
||||
let silentLog: string[] = [];
|
||||
|
||||
if (options.includeLogs !== false) {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
const workerLogPath = path.join(dataDir, "logs", `worker-${today}.log`);
|
||||
const silentLogPath = path.join(dataDir, "silent.log");
|
||||
|
||||
[workerLog, silentLog] = await Promise.all([
|
||||
readLogLines(workerLogPath, 50),
|
||||
readLogLines(silentLogPath, 50),
|
||||
]);
|
||||
}
|
||||
|
||||
const logs = {
|
||||
workerLog: workerLog.map(sanitizePath),
|
||||
silentLog: silentLog.map(sanitizePath),
|
||||
};
|
||||
|
||||
// Database info
|
||||
const dbInfo = await getDatabaseInfo(dataDir);
|
||||
const database = {
|
||||
path: sanitizePath(path.join(dataDir, "claude-mem.db")),
|
||||
exists: dbInfo.exists,
|
||||
size: dbInfo.size,
|
||||
// TODO: Add table counts if we want to query the database
|
||||
};
|
||||
|
||||
// Configuration
|
||||
const settingsInfo = await getSettings(dataDir);
|
||||
const config = {
|
||||
settingsPath: sanitizePath(path.join(dataDir, "settings.json")),
|
||||
settingsExist: settingsInfo.exists,
|
||||
settings: settingsInfo.settings,
|
||||
};
|
||||
|
||||
return {
|
||||
versions,
|
||||
platform,
|
||||
paths,
|
||||
worker,
|
||||
logs,
|
||||
database,
|
||||
config,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatDiagnostics(diagnostics: SystemDiagnostics): string {
|
||||
let output = "";
|
||||
|
||||
output += "## Environment\n\n";
|
||||
output += `- **Claude-mem**: ${diagnostics.versions.claudeMem}\n`;
|
||||
output += `- **Claude Code**: ${diagnostics.versions.claudeCode}\n`;
|
||||
output += `- **Node.js**: ${diagnostics.versions.node}\n`;
|
||||
output += `- **Bun**: ${diagnostics.versions.bun}\n`;
|
||||
output += `- **OS**: ${diagnostics.platform.osVersion} (${diagnostics.platform.arch})\n`;
|
||||
output += `- **Platform**: ${diagnostics.platform.os}\n\n`;
|
||||
|
||||
output += "## Paths\n\n";
|
||||
output += `- **Plugin**: ${diagnostics.paths.pluginPath}\n`;
|
||||
output += `- **Data Directory**: ${diagnostics.paths.dataDir}\n`;
|
||||
output += `- **Current Directory**: ${diagnostics.paths.cwd}\n`;
|
||||
output += `- **Dev Mode**: ${diagnostics.paths.isDevMode ? "Yes" : "No"}\n\n`;
|
||||
|
||||
output += "## Worker Status\n\n";
|
||||
output += `- **Running**: ${diagnostics.worker.running ? "Yes" : "No"}\n`;
|
||||
if (diagnostics.worker.running) {
|
||||
output += `- **PID**: ${diagnostics.worker.pid || "unknown"}\n`;
|
||||
output += `- **Port**: ${diagnostics.worker.port}\n`;
|
||||
if (diagnostics.worker.uptime !== undefined) {
|
||||
const uptimeMinutes = Math.floor(diagnostics.worker.uptime / 60);
|
||||
output += `- **Uptime**: ${uptimeMinutes} minutes\n`;
|
||||
}
|
||||
if (diagnostics.worker.stats) {
|
||||
output += `- **Active Sessions**: ${diagnostics.worker.stats.worker?.activeSessions || 0}\n`;
|
||||
output += `- **SSE Clients**: ${diagnostics.worker.stats.worker?.sseClients || 0}\n`;
|
||||
}
|
||||
}
|
||||
output += "\n";
|
||||
|
||||
output += "## Database\n\n";
|
||||
output += `- **Path**: ${diagnostics.database.path}\n`;
|
||||
output += `- **Exists**: ${diagnostics.database.exists ? "Yes" : "No"}\n`;
|
||||
if (diagnostics.database.size) {
|
||||
const sizeKB = (diagnostics.database.size / 1024).toFixed(2);
|
||||
output += `- **Size**: ${sizeKB} KB\n`;
|
||||
}
|
||||
output += "\n";
|
||||
|
||||
output += "## Configuration\n\n";
|
||||
output += `- **Settings File**: ${diagnostics.config.settingsPath}\n`;
|
||||
output += `- **Settings Exist**: ${diagnostics.config.settingsExist ? "Yes" : "No"}\n`;
|
||||
if (diagnostics.config.settings) {
|
||||
output += "- **Key Settings**:\n";
|
||||
const keySettings = [
|
||||
"CLAUDE_MEM_MODEL",
|
||||
"CLAUDE_MEM_WORKER_PORT",
|
||||
"CLAUDE_MEM_WORKER_HOST",
|
||||
"CLAUDE_MEM_LOG_LEVEL",
|
||||
"CLAUDE_MEM_CONTEXT_OBSERVATIONS",
|
||||
];
|
||||
for (const key of keySettings) {
|
||||
if (diagnostics.config.settings[key]) {
|
||||
output += ` - ${key}: ${diagnostics.config.settings[key]}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
output += "\n";
|
||||
|
||||
// Add logs if present
|
||||
if (diagnostics.logs.workerLog.length > 0) {
|
||||
output += "## Recent Worker Logs (Last 50 Lines)\n\n";
|
||||
output += "```\n";
|
||||
output += diagnostics.logs.workerLog.join("\n");
|
||||
output += "\n```\n\n";
|
||||
}
|
||||
|
||||
if (diagnostics.logs.silentLog.length > 0) {
|
||||
output += "## Silent Debug Log (Last 50 Lines)\n\n";
|
||||
output += "```\n";
|
||||
output += diagnostics.logs.silentLog.join("\n");
|
||||
output += "\n```\n\n";
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
query,
|
||||
type SDKMessage,
|
||||
type SDKResultMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import {
|
||||
collectDiagnostics,
|
||||
formatDiagnostics,
|
||||
type SystemDiagnostics,
|
||||
} from "./collector.ts";
|
||||
|
||||
export interface BugReportInput {
|
||||
issueDescription: string;
|
||||
expectedBehavior?: string;
|
||||
stepsToReproduce?: string;
|
||||
includeLogs?: boolean;
|
||||
}
|
||||
|
||||
export interface BugReportResult {
|
||||
title: string;
|
||||
body: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function generateBugReport(
|
||||
input: BugReportInput
|
||||
): Promise<BugReportResult> {
|
||||
try {
|
||||
// Collect system diagnostics
|
||||
const diagnostics = await collectDiagnostics({
|
||||
includeLogs: input.includeLogs !== false,
|
||||
});
|
||||
|
||||
const formattedDiagnostics = formatDiagnostics(diagnostics);
|
||||
|
||||
// Build the prompt
|
||||
const prompt = buildPrompt(
|
||||
formattedDiagnostics,
|
||||
input.issueDescription,
|
||||
input.expectedBehavior,
|
||||
input.stepsToReproduce
|
||||
);
|
||||
|
||||
// Use Agent SDK to generate formatted issue
|
||||
let generatedMarkdown = "";
|
||||
let charCount = 0;
|
||||
const startTime = Date.now();
|
||||
|
||||
const stream = query({
|
||||
prompt,
|
||||
options: {
|
||||
model: "sonnet",
|
||||
systemPrompt: `You are a GitHub issue formatter. Format bug reports clearly and professionally.`,
|
||||
permissionMode: "bypassPermissions",
|
||||
allowDangerouslySkipPermissions: true,
|
||||
includePartialMessages: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Progress spinner frames
|
||||
const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
let spinnerIdx = 0;
|
||||
|
||||
// Stream the response
|
||||
for await (const message of stream) {
|
||||
if (message.type === "stream_event") {
|
||||
const event = message.event as { type: string; delta?: { type: string; text?: string } };
|
||||
if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
|
||||
generatedMarkdown += event.delta.text;
|
||||
charCount += event.delta.text.length;
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
const spinner = spinnerFrames[spinnerIdx++ % spinnerFrames.length];
|
||||
process.stdout.write(`\r ${spinner} Generating... ${charCount} chars (${elapsed}s)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle full assistant messages (fallback)
|
||||
if (message.type === "assistant") {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === "text" && !generatedMarkdown) {
|
||||
generatedMarkdown = block.text;
|
||||
charCount = generatedMarkdown.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle result
|
||||
if (message.type === "result") {
|
||||
const result = message as SDKResultMessage;
|
||||
if (result.subtype === "success" && !generatedMarkdown && result.result) {
|
||||
generatedMarkdown = result.result;
|
||||
charCount = generatedMarkdown.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the progress line
|
||||
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
||||
|
||||
// Extract title from markdown (first heading)
|
||||
const titleMatch = generatedMarkdown.match(/^#\s+(.+)$/m);
|
||||
const title = titleMatch ? titleMatch[1] : "Bug Report";
|
||||
|
||||
return {
|
||||
title,
|
||||
body: generatedMarkdown,
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
// Fallback to template-based generation
|
||||
console.error("Agent SDK failed, using template fallback:", error);
|
||||
return generateTemplateFallback(input);
|
||||
}
|
||||
}
|
||||
|
||||
function buildPrompt(
|
||||
diagnostics: string,
|
||||
issueDescription: string,
|
||||
expectedBehavior?: string,
|
||||
stepsToReproduce?: string
|
||||
): string {
|
||||
let prompt = `You are a GitHub issue formatter. Given system diagnostics and a user's bug description, create a well-structured GitHub issue for the claude-mem repository.
|
||||
|
||||
SYSTEM DIAGNOSTICS:
|
||||
${diagnostics}
|
||||
|
||||
USER DESCRIPTION:
|
||||
${issueDescription}
|
||||
`;
|
||||
|
||||
if (expectedBehavior) {
|
||||
prompt += `\nEXPECTED BEHAVIOR:
|
||||
${expectedBehavior}
|
||||
`;
|
||||
}
|
||||
|
||||
if (stepsToReproduce) {
|
||||
prompt += `\nSTEPS TO REPRODUCE:
|
||||
${stepsToReproduce}
|
||||
`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
IMPORTANT: If any part of the user's description is in a language other than English, translate it to English while preserving technical accuracy and meaning.
|
||||
|
||||
Create a GitHub issue with:
|
||||
1. Clear, descriptive title (max 80 chars) in English - start with a single # heading
|
||||
2. Problem statement summarizing the issue in English
|
||||
3. Environment section (versions, platform) from the diagnostics
|
||||
4. Steps to reproduce (if provided) in English
|
||||
5. Expected vs actual behavior in English
|
||||
6. Relevant logs (formatted as code blocks) if present in diagnostics
|
||||
7. Any additional context that would help diagnose the issue
|
||||
|
||||
Format the output as valid GitHub Markdown. Make sure the title is a single # heading at the very top.
|
||||
Do NOT add meta-commentary like "Here's a formatted issue" - just output the raw markdown.
|
||||
All content must be in English for the GitHub issue.
|
||||
`;
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
async function generateTemplateFallback(
|
||||
input: BugReportInput
|
||||
): Promise<BugReportResult> {
|
||||
const diagnostics = await collectDiagnostics({
|
||||
includeLogs: input.includeLogs !== false,
|
||||
});
|
||||
const formattedDiagnostics = formatDiagnostics(diagnostics);
|
||||
|
||||
let body = `# Bug Report\n\n`;
|
||||
body += `## Description\n\n`;
|
||||
body += `${input.issueDescription}\n\n`;
|
||||
|
||||
if (input.expectedBehavior) {
|
||||
body += `## Expected Behavior\n\n`;
|
||||
body += `${input.expectedBehavior}\n\n`;
|
||||
}
|
||||
|
||||
if (input.stepsToReproduce) {
|
||||
body += `## Steps to Reproduce\n\n`;
|
||||
body += `${input.stepsToReproduce}\n\n`;
|
||||
}
|
||||
|
||||
body += formattedDiagnostics;
|
||||
|
||||
return {
|
||||
title: "Bug Report",
|
||||
body,
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user