edsger 0.2.2 → 0.2.3
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/dist/phases/code-implementation/analyzer.js +4 -4
- package/dist/phases/feature-analysis/analyzer.js +7 -1
- package/dist/phases/pull-request/creator.js +8 -8
- package/dist/phases/technical-design/analyzer.js +7 -1
- package/dist/prompts/formatters.d.ts +17 -4
- package/dist/prompts/formatters.js +41 -12
- package/dist/utils/image-downloader.d.ts +32 -0
- package/dist/utils/image-downloader.js +144 -0
- package/dist/utils/image-processor.d.ts +5 -0
- package/dist/utils/image-processor.js +55 -0
- package/package.json +1 -1
|
@@ -285,7 +285,7 @@ CRITICAL: Checklists are not optional suggestions - they are mandatory quality g
|
|
|
285
285
|
**Important Rules**:
|
|
286
286
|
1. **Git Workflow**:
|
|
287
287
|
- ALWAYS pull latest changes from main before creating branch: git pull origin ${baseBranch} --rebase
|
|
288
|
-
- ALWAYS create a new branch before making changes: git checkout -b
|
|
288
|
+
- ALWAYS create a new branch before making changes: git checkout -b dev/<feature-id>
|
|
289
289
|
- Use descriptive commit messages that explain what and why
|
|
290
290
|
- Handle pre-commit checks properly: fix issues first, use workarounds only when necessary
|
|
291
291
|
|
|
@@ -323,7 +323,7 @@ You MUST end your response with a JSON object containing the implementation resu
|
|
|
323
323
|
{
|
|
324
324
|
"implementation_result": {
|
|
325
325
|
"feature_id": "FEATURE_ID_PLACEHOLDER",
|
|
326
|
-
"branch_name": "
|
|
326
|
+
"branch_name": "dev/FEATURE_ID",
|
|
327
327
|
"files_modified": ["file1.ts", "file2.tsx"],
|
|
328
328
|
"commit_hash": "abc123...",
|
|
329
329
|
"summary": "Brief description of what was implemented",
|
|
@@ -409,7 +409,7 @@ Follow this systematic approach:
|
|
|
409
409
|
\`git pull origin ${baseBranch} --rebase\`
|
|
410
410
|
|
|
411
411
|
2. **CREATE BRANCH**: Create a new git branch from ${baseBranch}:
|
|
412
|
-
\`git checkout -b
|
|
412
|
+
\`git checkout -b dev/${featureId}\`
|
|
413
413
|
|
|
414
414
|
2. **ANALYZE CODEBASE**: Study the existing codebase structure to understand:
|
|
415
415
|
- Technology stack and architecture
|
|
@@ -459,7 +459,7 @@ const parseImplementationResponse = (response, featureId) => {
|
|
|
459
459
|
if (parsed.implementation_result) {
|
|
460
460
|
const result = parsed.implementation_result;
|
|
461
461
|
return {
|
|
462
|
-
branchName: result.branch_name || `
|
|
462
|
+
branchName: result.branch_name || `dev/${featureId}`,
|
|
463
463
|
filesModified: result.files_modified || [],
|
|
464
464
|
commitHash: result.commit_hash || '',
|
|
465
465
|
summary: result.summary || '',
|
|
@@ -30,7 +30,13 @@ export const analyzeFeatureWithMCP = async (options, config, checklistContext) =
|
|
|
30
30
|
}
|
|
31
31
|
const context = await fetchFeatureAnalysisContext(mcpServerUrl, mcpToken, featureId, verbose);
|
|
32
32
|
const systemPrompt = createFeatureAnalysisSystemPrompt(config, mcpServerUrl, mcpToken, featureId);
|
|
33
|
-
const contextInfo = formatFeatureAnalysisContext(context);
|
|
33
|
+
const { content: contextInfo, downloadedImages } = await formatFeatureAnalysisContext(context);
|
|
34
|
+
if (verbose && downloadedImages.length > 0) {
|
|
35
|
+
logInfo(`Downloaded ${downloadedImages.length} images for Claude Code:`);
|
|
36
|
+
downloadedImages.forEach((img) => {
|
|
37
|
+
logInfo(` - ${img.url} -> ${img.localPath}`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
34
40
|
// Add checklist context to the analysis prompt
|
|
35
41
|
let finalContextInfo = contextInfo;
|
|
36
42
|
if (checklistContext && checklistContext.checklists.length > 0) {
|
|
@@ -108,28 +108,28 @@ export async function createPullRequest(config, feature) {
|
|
|
108
108
|
if (currentBranch.startsWith('dev/')) {
|
|
109
109
|
const featureId = currentBranch.replace('dev/', '');
|
|
110
110
|
targetBranch = `feat/${featureId}`;
|
|
111
|
-
// Create the feat/ branch from
|
|
111
|
+
// Create the feat/ branch from base branch (main)
|
|
112
112
|
if (verbose) {
|
|
113
|
-
console.log(`📝 Creating target branch: ${targetBranch} from ${
|
|
113
|
+
console.log(`📝 Creating target branch: ${targetBranch} from ${baseBranch}`);
|
|
114
114
|
}
|
|
115
115
|
try {
|
|
116
116
|
// Push current branch to remote first
|
|
117
117
|
pushBranch(currentBranch, verbose);
|
|
118
|
-
// Get the
|
|
119
|
-
const { data:
|
|
118
|
+
// Get the base branch (main) SHA to create feat/ branch from
|
|
119
|
+
const { data: baseBranchData } = await octokit.repos.getBranch({
|
|
120
120
|
owner,
|
|
121
121
|
repo,
|
|
122
|
-
branch:
|
|
122
|
+
branch: baseBranch,
|
|
123
123
|
});
|
|
124
|
-
// Create the feat/ branch
|
|
124
|
+
// Create the feat/ branch from main
|
|
125
125
|
await octokit.git.createRef({
|
|
126
126
|
owner,
|
|
127
127
|
repo,
|
|
128
128
|
ref: `refs/heads/${targetBranch}`,
|
|
129
|
-
sha:
|
|
129
|
+
sha: baseBranchData.commit.sha,
|
|
130
130
|
});
|
|
131
131
|
if (verbose) {
|
|
132
|
-
console.log(`✅ Created target branch: ${targetBranch}`);
|
|
132
|
+
console.log(`✅ Created target branch: ${targetBranch} from ${baseBranch}`);
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
catch (error) {
|
|
@@ -29,7 +29,13 @@ export const generateTechnicalDesign = async (options, config, checklistContext)
|
|
|
29
29
|
}
|
|
30
30
|
const context = await fetchTechnicalDesignContext(mcpServerUrl, mcpToken, featureId, verbose);
|
|
31
31
|
const systemPrompt = createTechnicalDesignSystemPrompt(config, mcpServerUrl, mcpToken, featureId);
|
|
32
|
-
const contextInfo = formatTechnicalDesignContext(context);
|
|
32
|
+
const { content: contextInfo, downloadedImages } = await formatTechnicalDesignContext(context);
|
|
33
|
+
if (verbose && downloadedImages.length > 0) {
|
|
34
|
+
logInfo(`Downloaded ${downloadedImages.length} images for Claude Code:`);
|
|
35
|
+
downloadedImages.forEach((img) => {
|
|
36
|
+
logInfo(` - ${img.url} -> ${img.localPath}`);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
33
39
|
// Add checklist context to the design prompt
|
|
34
40
|
let finalContextInfo = contextInfo;
|
|
35
41
|
if (checklistContext && checklistContext.checklists.length > 0) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UserStory, TestCase } from '../types/features.js';
|
|
2
|
+
import { type DownloadedImage } from '../utils/image-downloader.js';
|
|
2
3
|
import { TechnicalDesignContext } from '../phases/technical-design/context-fetcher.js';
|
|
3
4
|
import { FeatureAnalysisContext } from '../phases/feature-analysis/context-fetcher.js';
|
|
4
5
|
import { CodeImplementationContext } from '../phases/code-implementation/context-fetcher.js';
|
|
@@ -14,16 +15,28 @@ export declare function formatTestCases(cases: TestCase[]): string;
|
|
|
14
15
|
/**
|
|
15
16
|
* Format technical design context for Claude Code prompts
|
|
16
17
|
*/
|
|
17
|
-
export declare function formatTechnicalDesignContext(context: TechnicalDesignContext):
|
|
18
|
+
export declare function formatTechnicalDesignContext(context: TechnicalDesignContext): Promise<{
|
|
19
|
+
content: string;
|
|
20
|
+
downloadedImages: DownloadedImage[];
|
|
21
|
+
}>;
|
|
18
22
|
/**
|
|
19
23
|
* Format feature analysis context for Claude Code prompts
|
|
20
24
|
*/
|
|
21
|
-
export declare function formatFeatureAnalysisContext(context: FeatureAnalysisContext):
|
|
25
|
+
export declare function formatFeatureAnalysisContext(context: FeatureAnalysisContext): Promise<{
|
|
26
|
+
content: string;
|
|
27
|
+
downloadedImages: DownloadedImage[];
|
|
28
|
+
}>;
|
|
22
29
|
/**
|
|
23
30
|
* Format code implementation context for Claude Code prompts
|
|
24
31
|
*/
|
|
25
|
-
export declare function formatCodeImplementationContext(context: CodeImplementationContext):
|
|
32
|
+
export declare function formatCodeImplementationContext(context: CodeImplementationContext): Promise<{
|
|
33
|
+
content: string;
|
|
34
|
+
downloadedImages: DownloadedImage[];
|
|
35
|
+
}>;
|
|
26
36
|
/**
|
|
27
37
|
* Format functional testing context for Claude Code prompts
|
|
28
38
|
*/
|
|
29
|
-
export declare function formatFunctionalTestingContext(context: FunctionalTestingContext):
|
|
39
|
+
export declare function formatFunctionalTestingContext(context: FunctionalTestingContext): Promise<{
|
|
40
|
+
content: string;
|
|
41
|
+
downloadedImages: DownloadedImage[];
|
|
42
|
+
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { downloadImagesForClaudeCode, } from '../utils/image-downloader.js';
|
|
1
2
|
/**
|
|
2
3
|
* Format user stories for display in prompts
|
|
3
4
|
*/
|
|
@@ -23,13 +24,19 @@ export function formatTestCases(cases) {
|
|
|
23
24
|
/**
|
|
24
25
|
* Format technical design context for Claude Code prompts
|
|
25
26
|
*/
|
|
26
|
-
export function formatTechnicalDesignContext(context) {
|
|
27
|
-
|
|
27
|
+
export async function formatTechnicalDesignContext(context) {
|
|
28
|
+
const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
|
|
29
|
+
);
|
|
30
|
+
const content = `# Technical Design Context
|
|
28
31
|
|
|
29
32
|
## Feature Information
|
|
30
33
|
- **ID**: ${context.feature.id}
|
|
31
34
|
- **Name**: ${context.feature.name}
|
|
32
|
-
- **Description**:
|
|
35
|
+
- **Description**:
|
|
36
|
+
${processedMarkdown}
|
|
37
|
+
|
|
38
|
+
${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
|
|
39
|
+
|
|
33
40
|
- **Current Status**: ${context.feature.status}
|
|
34
41
|
|
|
35
42
|
## Product Information
|
|
@@ -49,17 +56,24 @@ ${context.feature.technical_design || 'No existing technical design found'}
|
|
|
49
56
|
---
|
|
50
57
|
|
|
51
58
|
**Design Instructions**: Based on the above feature requirements, user stories, and test cases, create or enhance the comprehensive technical design. Focus on system architecture, component design, database schema, API specifications, security considerations, and implementation strategy.`;
|
|
59
|
+
return { content, downloadedImages };
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
62
|
* Format feature analysis context for Claude Code prompts
|
|
55
63
|
*/
|
|
56
|
-
export function formatFeatureAnalysisContext(context) {
|
|
57
|
-
|
|
64
|
+
export async function formatFeatureAnalysisContext(context) {
|
|
65
|
+
const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
|
|
66
|
+
);
|
|
67
|
+
const content = `# Feature Analysis Context
|
|
58
68
|
|
|
59
69
|
## Feature Information
|
|
60
70
|
- **ID**: ${context.feature.id}
|
|
61
71
|
- **Name**: ${context.feature.name}
|
|
62
|
-
- **Description**:
|
|
72
|
+
- **Description**:
|
|
73
|
+
${processedMarkdown}
|
|
74
|
+
|
|
75
|
+
${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
|
|
76
|
+
|
|
63
77
|
- **Current Status**: ${context.feature.status}
|
|
64
78
|
|
|
65
79
|
## Product Information
|
|
@@ -79,17 +93,24 @@ ${context.feature.technical_design || 'No technical design available yet'}
|
|
|
79
93
|
---
|
|
80
94
|
|
|
81
95
|
**Analysis Instructions**: Based on the above feature information and existing user stories/test cases, conduct comprehensive business analysis to identify gaps and create additional user stories and test cases that add business value.`;
|
|
96
|
+
return { content, downloadedImages };
|
|
82
97
|
}
|
|
83
98
|
/**
|
|
84
99
|
* Format code implementation context for Claude Code prompts
|
|
85
100
|
*/
|
|
86
|
-
export function formatCodeImplementationContext(context) {
|
|
87
|
-
|
|
101
|
+
export async function formatCodeImplementationContext(context) {
|
|
102
|
+
const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
|
|
103
|
+
);
|
|
104
|
+
const content = `# Code Implementation Context
|
|
88
105
|
|
|
89
106
|
## Feature Information
|
|
90
107
|
- **ID**: ${context.feature.id}
|
|
91
108
|
- **Name**: ${context.feature.name}
|
|
92
|
-
- **Description**:
|
|
109
|
+
- **Description**:
|
|
110
|
+
${processedMarkdown}
|
|
111
|
+
|
|
112
|
+
${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
|
|
113
|
+
|
|
93
114
|
- **Current Status**: ${context.feature.status}
|
|
94
115
|
|
|
95
116
|
## Product Information
|
|
@@ -109,17 +130,24 @@ ${context.technical_design || 'No technical design available - implement based o
|
|
|
109
130
|
---
|
|
110
131
|
|
|
111
132
|
**Implementation Instructions**: Based on the above requirements, user stories, test cases, and technical design, implement the complete feature functionality. Ensure all user stories are implemented and all test cases can pass.`;
|
|
133
|
+
return { content, downloadedImages };
|
|
112
134
|
}
|
|
113
135
|
/**
|
|
114
136
|
* Format functional testing context for Claude Code prompts
|
|
115
137
|
*/
|
|
116
|
-
export function formatFunctionalTestingContext(context) {
|
|
117
|
-
|
|
138
|
+
export async function formatFunctionalTestingContext(context) {
|
|
139
|
+
const { processedMarkdown, downloadedImages } = await downloadImagesForClaudeCode(context.feature.description || 'No description provided', context.feature.id // Use feature ID for directory naming
|
|
140
|
+
);
|
|
141
|
+
const content = `# Functional Testing Context
|
|
118
142
|
|
|
119
143
|
## Feature Information
|
|
120
144
|
- **ID**: ${context.feature.id}
|
|
121
145
|
- **Name**: ${context.feature.name}
|
|
122
|
-
- **Description**:
|
|
146
|
+
- **Description**:
|
|
147
|
+
${processedMarkdown}
|
|
148
|
+
|
|
149
|
+
${downloadedImages.length > 0 ? '**IMPORTANT**: The description contains images that have been downloaded locally. Please use the Read tool to view these images to fully understand the requirements.' : ''}
|
|
150
|
+
|
|
123
151
|
- **Current Status**: ${context.feature.status}
|
|
124
152
|
|
|
125
153
|
## Product Information
|
|
@@ -136,4 +164,5 @@ ${formatTestCases(context.test_cases)}
|
|
|
136
164
|
---
|
|
137
165
|
|
|
138
166
|
**Testing Instructions**: The feature has been implemented. Execute comprehensive functional testing using headless Playwright to verify all user stories work correctly and all test cases pass. Test both positive and negative scenarios.`;
|
|
167
|
+
return { content, downloadedImages };
|
|
139
168
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Downloaded image info
|
|
3
|
+
*/
|
|
4
|
+
export interface DownloadedImage {
|
|
5
|
+
url: string;
|
|
6
|
+
localPath: string;
|
|
7
|
+
alt: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Set current feature ID for image downloads
|
|
11
|
+
* Should be called at the start of pipeline execution
|
|
12
|
+
*/
|
|
13
|
+
export declare function setCurrentFeatureId(featureId: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Reset image cache (useful for testing)
|
|
16
|
+
*/
|
|
17
|
+
export declare function resetImageCache(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Download images from URLs to temporary directory for Claude Code to read
|
|
20
|
+
* Uses cache to avoid downloading same image multiple times
|
|
21
|
+
*
|
|
22
|
+
* @param markdown - Markdown content with image URLs
|
|
23
|
+
* @param featureId - Feature ID for directory naming
|
|
24
|
+
*/
|
|
25
|
+
export declare function downloadImagesForClaudeCode(markdown: string, featureId?: string): Promise<{
|
|
26
|
+
processedMarkdown: string;
|
|
27
|
+
downloadedImages: DownloadedImage[];
|
|
28
|
+
}>;
|
|
29
|
+
/**
|
|
30
|
+
* Clean markdown by removing width attributes
|
|
31
|
+
*/
|
|
32
|
+
export declare function cleanMarkdownForClaudeCode(markdown: string): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createWriteStream } from 'fs';
|
|
2
|
+
import { mkdir, access } from 'fs/promises';
|
|
3
|
+
import { tmpdir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { pipeline } from 'stream/promises';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import { logInfo, logError } from './logger.js';
|
|
8
|
+
// Cache for downloaded images in current session
|
|
9
|
+
// Key: image URL, Value: local path
|
|
10
|
+
const imageCache = new Map();
|
|
11
|
+
// Current feature ID for directory naming
|
|
12
|
+
let currentFeatureId = null;
|
|
13
|
+
/**
|
|
14
|
+
* Generate hash from URL to use as filename
|
|
15
|
+
* This ensures same URL always maps to same file
|
|
16
|
+
*/
|
|
17
|
+
function hashUrl(url) {
|
|
18
|
+
return createHash('md5').update(url).digest('hex');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get or create temp directory for current feature
|
|
22
|
+
*/
|
|
23
|
+
function getTempDir(featureId) {
|
|
24
|
+
return join(tmpdir(), 'claude-code-images', `feature-${featureId}`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Set current feature ID for image downloads
|
|
28
|
+
* Should be called at the start of pipeline execution
|
|
29
|
+
*/
|
|
30
|
+
export function setCurrentFeatureId(featureId) {
|
|
31
|
+
if (currentFeatureId !== featureId) {
|
|
32
|
+
// Clear cache when switching to different feature
|
|
33
|
+
imageCache.clear();
|
|
34
|
+
currentFeatureId = featureId;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Reset image cache (useful for testing)
|
|
39
|
+
*/
|
|
40
|
+
export function resetImageCache() {
|
|
41
|
+
imageCache.clear();
|
|
42
|
+
currentFeatureId = null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Download images from URLs to temporary directory for Claude Code to read
|
|
46
|
+
* Uses cache to avoid downloading same image multiple times
|
|
47
|
+
*
|
|
48
|
+
* @param markdown - Markdown content with image URLs
|
|
49
|
+
* @param featureId - Feature ID for directory naming
|
|
50
|
+
*/
|
|
51
|
+
export async function downloadImagesForClaudeCode(markdown, featureId) {
|
|
52
|
+
if (!markdown) {
|
|
53
|
+
return { processedMarkdown: markdown, downloadedImages: [] };
|
|
54
|
+
}
|
|
55
|
+
// Use provided featureId or fall back to currentFeatureId or timestamp
|
|
56
|
+
const effectiveFeatureId = featureId || currentFeatureId || Date.now().toString();
|
|
57
|
+
// Extract all image URLs from markdown
|
|
58
|
+
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)(?:\{width=\d+\})?/g;
|
|
59
|
+
const matches = Array.from(markdown.matchAll(imageRegex));
|
|
60
|
+
if (matches.length === 0) {
|
|
61
|
+
return { processedMarkdown: markdown, downloadedImages: [] };
|
|
62
|
+
}
|
|
63
|
+
// Create temp directory for this feature
|
|
64
|
+
const tempDir = getTempDir(effectiveFeatureId);
|
|
65
|
+
await mkdir(tempDir, { recursive: true });
|
|
66
|
+
const downloadedImages = [];
|
|
67
|
+
let processedMarkdown = markdown;
|
|
68
|
+
// Download each image (or use cached version)
|
|
69
|
+
for (let i = 0; i < matches.length; i++) {
|
|
70
|
+
const match = matches[i];
|
|
71
|
+
const alt = match[1] || '';
|
|
72
|
+
const url = match[2];
|
|
73
|
+
try {
|
|
74
|
+
// Check if image is already cached
|
|
75
|
+
let localPath = imageCache.get(url);
|
|
76
|
+
if (localPath) {
|
|
77
|
+
// Verify cached file still exists
|
|
78
|
+
try {
|
|
79
|
+
await access(localPath);
|
|
80
|
+
logInfo(`Using cached image: ${url}`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Cached file no longer exists, remove from cache
|
|
84
|
+
imageCache.delete(url);
|
|
85
|
+
localPath = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!localPath) {
|
|
89
|
+
// Download image
|
|
90
|
+
logInfo(`Downloading image ${i + 1}/${matches.length}: ${url}`);
|
|
91
|
+
const response = await fetch(url);
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
logError(`Failed to download image: ${response.statusText}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Determine file extension from URL or content-type
|
|
97
|
+
let extension = 'png';
|
|
98
|
+
const urlExt = url.split('.').pop()?.toLowerCase();
|
|
99
|
+
if (urlExt && ['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(urlExt)) {
|
|
100
|
+
extension = urlExt;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const contentType = response.headers.get('content-type');
|
|
104
|
+
if (contentType?.includes('jpeg'))
|
|
105
|
+
extension = 'jpg';
|
|
106
|
+
else if (contentType?.includes('png'))
|
|
107
|
+
extension = 'png';
|
|
108
|
+
else if (contentType?.includes('gif'))
|
|
109
|
+
extension = 'gif';
|
|
110
|
+
else if (contentType?.includes('webp'))
|
|
111
|
+
extension = 'webp';
|
|
112
|
+
}
|
|
113
|
+
// Use hash of URL as filename (ensures same URL = same file)
|
|
114
|
+
const urlHash = hashUrl(url);
|
|
115
|
+
const filename = `${urlHash}.${extension}`;
|
|
116
|
+
localPath = join(tempDir, filename);
|
|
117
|
+
// Use Node.js streams to save the file
|
|
118
|
+
const fileStream = createWriteStream(localPath);
|
|
119
|
+
if (response.body) {
|
|
120
|
+
await pipeline(response.body, fileStream);
|
|
121
|
+
}
|
|
122
|
+
// Cache the downloaded image
|
|
123
|
+
imageCache.set(url, localPath);
|
|
124
|
+
logInfo(`Downloaded to: ${localPath}`);
|
|
125
|
+
}
|
|
126
|
+
downloadedImages.push({ url, localPath, alt });
|
|
127
|
+
// Replace URL with local path in markdown
|
|
128
|
+
processedMarkdown = processedMarkdown.replace(match[0], ``);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logError(`Failed to download image ${url}: ${error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { processedMarkdown, downloadedImages };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Clean markdown by removing width attributes
|
|
138
|
+
*/
|
|
139
|
+
export function cleanMarkdownForClaudeCode(markdown) {
|
|
140
|
+
if (!markdown)
|
|
141
|
+
return markdown;
|
|
142
|
+
// Remove {width=600} syntax to keep pure markdown
|
|
143
|
+
return markdown.replace(/!\[([^\]]*)\]\(([^)]+)\)\{width=\d+\}/g, '');
|
|
144
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert HTML description to Markdown format
|
|
3
|
+
* Preserves image URLs so Claude Code can view them
|
|
4
|
+
*/
|
|
5
|
+
export function htmlToMarkdown(html) {
|
|
6
|
+
if (!html)
|
|
7
|
+
return '';
|
|
8
|
+
return (html
|
|
9
|
+
// Convert images to markdown format with original URL
|
|
10
|
+
.replace(/<img[^>]+src="([^">]+)"[^>]*alt="([^">]*)"[^>]*>/gi, '')
|
|
11
|
+
.replace(/<img[^>]+src="([^">]+)"[^>]*>/gi, '')
|
|
12
|
+
// Convert links
|
|
13
|
+
.replace(/<a[^>]+href="([^">]+)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
|
14
|
+
// Convert headings
|
|
15
|
+
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '\n# $1\n')
|
|
16
|
+
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '\n## $1\n')
|
|
17
|
+
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '\n### $1\n')
|
|
18
|
+
.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '\n#### $1\n')
|
|
19
|
+
.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '\n##### $1\n')
|
|
20
|
+
.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '\n###### $1\n')
|
|
21
|
+
// Convert lists
|
|
22
|
+
.replace(/<ul[^>]*>/gi, '\n')
|
|
23
|
+
.replace(/<\/ul>/gi, '\n')
|
|
24
|
+
.replace(/<ol[^>]*>/gi, '\n')
|
|
25
|
+
.replace(/<\/ol>/gi, '\n')
|
|
26
|
+
.replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
|
|
27
|
+
// Convert paragraphs
|
|
28
|
+
.replace(/<\/p>/gi, '\n\n')
|
|
29
|
+
.replace(/<p[^>]*>/gi, '')
|
|
30
|
+
// Convert line breaks
|
|
31
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
32
|
+
// Convert text formatting
|
|
33
|
+
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
|
34
|
+
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
|
|
35
|
+
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
|
36
|
+
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
|
|
37
|
+
.replace(/<u[^>]*>(.*?)<\/u>/gi, '__$1__')
|
|
38
|
+
// Convert code
|
|
39
|
+
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
|
|
40
|
+
.replace(/<pre[^>]*>(.*?)<\/pre>/gi, '\n```\n$1\n```\n')
|
|
41
|
+
// Convert blockquotes
|
|
42
|
+
.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gi, '\n> $1\n')
|
|
43
|
+
// Remove remaining HTML tags
|
|
44
|
+
.replace(/<[^>]+>/g, '')
|
|
45
|
+
// Decode HTML entities
|
|
46
|
+
.replace(/</g, '<')
|
|
47
|
+
.replace(/>/g, '>')
|
|
48
|
+
.replace(/&/g, '&')
|
|
49
|
+
.replace(/"/g, '"')
|
|
50
|
+
.replace(/'/g, "'")
|
|
51
|
+
.replace(/ /g, ' ')
|
|
52
|
+
// Clean up extra whitespace
|
|
53
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
54
|
+
.trim());
|
|
55
|
+
}
|