[WIP] feat: add mini-muse - AI web app generator example#1
[WIP] feat: add mini-muse - AI web app generator example#1jerryliang64 wants to merge 9 commits intomasterfrom
Conversation
Mini-Muse is a fullstack AI agent example that generates complete React + Tailwind web applications from natural language descriptions. Tech stack: - Backend: Egg.js 3 + TypeScript - Frontend: React 18 + Vite + Tailwind CSS - AI: Anthropic Claude API with tool use Features: - AI agent loop with structured tool calls - Real-time progress streaming via SSE - Live preview and project download
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughThis pull request establishes a complete full-stack AI-powered web application generator using Claude. It introduces backend infrastructure with Egg.js and TypeScript, a React-based frontend with Tailwind CSS, AI orchestration via the Claude Agent SDK, a comprehensive tool system for code generation and project management, and supporting services for project preview and session tracking. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client (Browser)
participant API as API Server<br/>(Egg.js)
participant Orchestrator as Orchestrator<br/>Service
participant Claude as Claude Agent<br/>SDK
participant Tools as Tool System<br/>(MCP)
participant FileSystem as File System<br/>& OSS
Client->>API: POST /api/v1/runs/stream<br/>(description, appName)
API->>Orchestrator: agentLoop(params)
Orchestrator->>Claude: Create streaming query<br/>(systemPrompt, userPrompt)
Orchestrator-->>Client: Stream: thread.run.created<br/>(threadId)
loop Agent Iteration
Claude->>Tools: Request tool execution<br/>(analyze_requirements,<br/>plan_architecture, etc.)
Tools->>FileSystem: Create files, read,<br/>validate project
Tools-->>Claude: Tool result (success/fail)
Orchestrator-->>Client: Stream: tool_use,<br/>tool_result events
end
Claude->>Claude: Generate code files
Claude->>Tools: create_component,<br/>create_page, create_config
Tools->>FileSystem: Write generated code
Orchestrator-->>Client: Stream: file_created<br/>events
Claude-->>Orchestrator: Completion signal
Orchestrator-->>Client: Stream: completed event
Client->>API: GET /api/v1/projects/{threadId}/files
API->>FileSystem: List generated files
API-->>Client: File list
Client->>Client: Render FileTree,<br/>CodePreview, Preview
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub. |
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a comprehensive example application, 'Mini-Muse', demonstrating an AI-powered web application generator. It provides a full-stack solution where users can describe a desired web application in natural language, and an AI agent, powered by Claude, will generate the complete project. The system supports a structured generation workflow, real-time progress updates, and interactive features for reviewing, previewing, and iteratively modifying the generated code, showcasing advanced AI integration in development tools. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces an impressive full-stack example application, mini-muse, which functions as an AI-powered web app generator. The backend is built with Egg.js and the frontend with React. The overall architecture is well-documented and follows a clear separation of concerns. The use of an AI agent with a tool-based workflow is a great demonstration.
My review focuses on improving security, type safety, and error handling. I've identified a few high-severity security issues in the default configuration, such as a hardcoded secret key and disabled CSRF protection. I've also pointed out several places where type safety could be improved (e.g., using specific types instead of unknown[] and avoiding unsafe casts) and where error handling could be more robust by logging errors instead of swallowing them in empty catch blocks. These changes will make the example more secure and resilient.
mini-muse/app/service/taskManager.ts
Outdated
| outputDir: string; | ||
| filesCreated: string[]; | ||
| progress: ProgressEvent[]; | ||
| messages: unknown[]; |
There was a problem hiding this comment.
Using unknown[] for the messages property is not type-safe and leads to unsafe casting in other parts of the application (e.g., orchestrator.ts). You should use a specific type for the messages to leverage TypeScript's type safety. I'd recommend importing the Message type from ./aiClient and using it here.
| messages: unknown[]; | |
| messages: Message[]; |
mini-muse/config/config.default.ts
Outdated
| export default () => { | ||
| const config = {} as PowerPartial<EggAppConfig>; | ||
|
|
||
| config.keys = 'mini-muse-secret-key'; |
There was a problem hiding this comment.
Hardcoding secret keys in a default configuration file is a significant security risk, as it could be accidentally committed to version control. This key should be loaded from environment variables or a per-environment configuration file that is not checked into git. The .env file should be added to .gitignore.
| config.keys = 'mini-muse-secret-key'; | |
| config.keys = process.env.EGG_KEYS || 'mini-muse-secret-key'; |
| csrf: { | ||
| enable: false, | ||
| }, |
There was a problem hiding this comment.
Disabling CSRF protection is a security risk for any web application that handles state-changing requests from a browser. Since this application serves a UI, it's recommended to keep CSRF protection enabled. If you are using token-based authentication for your API, ensure that it's correctly implemented for all state-changing endpoints.
enable: true,
mini-muse/app/controller/generate.ts
Outdated
| } catch { | ||
| ctx.body = { | ||
| success: true, | ||
| data: { | ||
| taskId: task.id, | ||
| files: [], | ||
| }, | ||
| }; | ||
| } |
There was a problem hiding this comment.
This catch block swallows any error from listFiles and returns a successful response with an empty array. This can be misleading for the client, as it hides potential issues like file system permissions errors. The error should be logged, and an appropriate error response should be sent to the client.
} catch (err) {
ctx.logger.error('Failed to list files for task %s: %s', taskId, err);
ctx.status = 500;
ctx.body = {
success: false,
error: 'Failed to retrieve file list.',
};
}| - Has clean, readable formatting | ||
| - Includes meaningful comments for complex logic | ||
| - Uses semantic HTML and accessible patterns | ||
| - Implements responsive design with CSS modules or styled-components |
There was a problem hiding this comment.
The prompt specifies using CSS modules or styled-components, but the generated project uses Tailwind CSS. The prompt should be updated to reflect the actual technology stack being used to avoid confusion and ensure the AI generates code consistent with the project's setup.
| - Implements responsive design with CSS modules or styled-components | |
| - Implements responsive design with Tailwind CSS |
| input: Record<string, unknown>, | ||
| _outputDir: string | ||
| ): Promise<ToolResult> { | ||
| const data = input as unknown as AnalyzeRequirementsInput; |
There was a problem hiding this comment.
Using an unsafe type cast as unknown as AnalyzeRequirementsInput is risky. If the input from the AI doesn't match the expected schema, it can lead to runtime errors. A more robust approach is to use a validation library like Zod to parse and validate the input. This would ensure type safety and provide clear errors if the input is invalid. This feedback applies to all other tool implementations in this PR that use similar unsafe casts.
|
|
||
| for (const file of tsFiles) { | ||
| const content = await readFile(file); | ||
| const importMatches = content.matchAll(/import\s+.*?\s+from\s+['"]([^'"]+)['"]/g); |
There was a problem hiding this comment.
Using regular expressions to parse code for validating imports is quite fragile. It can easily fail with multi-line imports, comments, or string literals that look like imports. For a more robust solution, consider using a proper code parser like the TypeScript Compiler API or a dedicated tool like dependency-cruiser. For an example project this might be acceptable, but it's worth noting this limitation.
| } catch { | ||
| return code; | ||
| } |
There was a problem hiding this comment.
The catch block for prettier.format is empty, which means formatting errors will be silently ignored, and unformatted code will be returned. This can make debugging difficult. You should at least log the error to be aware of any formatting issues.
} catch (err) {
console.error('Prettier formatting failed:', err);
return code;
}| setPreviewStarting(false); | ||
| if (pollRef.current) clearInterval(pollRef.current); | ||
| } | ||
| } catch { /* ignore */ } |
There was a problem hiding this comment.
This empty catch block ignores errors that might occur while polling for the preview status. Swallowing errors makes debugging very difficult. At a minimum, the error should be logged to the console.
| } catch { /* ignore */ } | |
| } catch (err) { console.error('Failed to poll preview status:', err); } |
There was a problem hiding this comment.
Actionable comments posted: 18
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (6)
mini-muse/docs/design.md-194-194 (1)
194-194:⚠️ Potential issue | 🟡 MinorAdd fence languages for markdownlint compliance (MD040).
Line 194, Line 262, and Line 286 open fenced blocks without language identifiers.
📝 Proposed fix
-``` +```text frontend/src/ ... -``` +``` -``` +```bash # Option 1: Direct Anthropic API ANTHROPIC_API_KEY=sk-ant-your-api-key-here ... -``` +``` -``` +```text mini-muse/ ... -``` +```Also applies to: 262-262, 286-286
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/docs/design.md` at line 194, Three fenced code blocks are missing language identifiers; add appropriate fence languages to satisfy markdownlint MD040 by changing the opening fences near the snippets that include "frontend/src/" to ```text, the block that begins with "# Option 1: Direct Anthropic API" to ```bash, and the block showing "mini-muse/" to ```text so each fenced block has a language label.mini-muse/frontend/src/pages/ResultPage.tsx-11-29 (1)
11-29:⚠️ Potential issue | 🟡 MinorHandle missing
taskIdexplicitly to avoid stuck loading and invalid URL generation.If
taskIdis absent, Line [24] returns early andloadingnever flips; Line [114] still force-assertstaskId!.Suggested fix
export default function ResultPage() { const { taskId } = useParams<{ taskId: string }>(); @@ + if (!taskId) { + return <div className="text-center text-red-500 py-12">Invalid task id</div>; + } + useEffect(() => { - if (!taskId) return; getFiles(taskId).then((f) => { setFiles(f); setLoading(false); @@ - href={getDownloadUrl(taskId!)} + href={getDownloadUrl(taskId)}Also applies to: 114-114
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/pages/ResultPage.tsx` around lines 11 - 29, The effect and other code assume taskId exists and leave loading true or call taskId! later; update the useEffect in ResultPage to explicitly handle a missing taskId by setting setLoading(false) and either navigate to a safe route or show an error (i.e., when !taskId do setLoading(false) and navigate('/somewhere') or set an error state), and audit all uses of the non-null assertion taskId! (e.g., places building URLs or calling getFiles/getContent) to guard them with if (!taskId) return/throw so no invalid URL is generated or background polling starts without an id; ensure pollRef and any functions that reference taskId check taskId before using it.mini-muse/frontend/src/components/FileTree.tsx-19-19 (1)
19-19:⚠️ Potential issue | 🟡 MinorAvoid mutating
fileswhile building the tree.Line [19] sorts
filesin place, which mutates the prop array and can create parent-state side effects.Suggested fix
- for (const file of files.sort()) { + const sortedFiles = [...files].sort(); + for (const file of sortedFiles) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/components/FileTree.tsx` at line 19, The loop in the FileTree component currently calls files.sort(), which mutates the incoming prop array; to fix, iterate over a sorted copy instead (e.g., create a shallow copy via slice() or spread and then sort) so the prop is not mutated—replace the iteration over files.sort() with iteration over a copied-and-sorted array (use the FileTree component and the files variable to locate the change).mini-muse/app/service/orchestrator.ts-67-72 (1)
67-72:⚠️ Potential issue | 🟡 MinorPotential runtime error if
task.messagesis undefined.In
modify(), line 68 spreadstask.messageswhich may be undefined if the task was never successfully run or if it errored before completion. This would cause a runtime error.🐛 Suggested fix
// Restore previous messages and append modification instruction - const messages: Message[] = [...task.messages as Message[]]; + const messages: Message[] = [...(task.messages as Message[] || [])];Alternatively, add a guard:
+ if (!task.messages || task.messages.length === 0) { + throw new Error('Cannot modify task: no previous messages found'); + } const messages: Message[] = [...task.messages as Message[]];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/orchestrator.ts` around lines 67 - 72, In modify(), spreading task.messages can throw if task.messages is undefined; update the code that builds messages to safely handle that by using a default empty array or an explicit guard before spreading (e.g., const messages: Message[] = [...(task.messages ?? [])]) so that messages.push({... role:'user', content: createModifyPrompt(instruction, relativeFiles) }) always operates on a defined array; ensure you update any type annotations or null checks in modify() to reflect that task.messages may be undefined.mini-muse/app/controller/generate.ts-262-271 (1)
262-271:⚠️ Potential issue | 🟡 MinorPotential HTTP header injection via
appName.The
appNameis user-provided (defaulting to 'my-app') and used directly in theContent-Dispositionheader (line 264). Special characters like newlines or quotes could cause header injection or malformed headers.🛡️ Suggested fix to sanitize filename
+ // Sanitize appName for use in Content-Disposition header + const safeAppName = task.appName.replace(/[^a-zA-Z0-9_-]/g, '_'); ctx.res.writeHead(200, { 'Content-Type': 'application/zip', - 'Content-Disposition': `attachment; filename="${task.appName}.zip"`, + 'Content-Disposition': `attachment; filename="${safeAppName}.zip"`, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/controller/generate.ts` around lines 262 - 271, The Content-Disposition header uses the user-controlled task.appName directly in ctx.res.writeHead which can allow header injection; sanitize and validate task.appName before using it in the header (e.g., strip CR/LF, quotes, path separators, and any unsafe characters, collapse whitespace, and fallback to a safe default like "my-app" if the result is empty), then use the sanitized name when building the header and the archive.directory call; update references in this block (task.appName, ctx.res.writeHead, and archive.directory) to use the sanitized variable to prevent injection and malformed headers.mini-muse/app/service/taskManager.ts-85-88 (1)
85-88:⚠️ Potential issue | 🟡 MinorUnsafe type assertion could cause runtime errors.
The
event.data as string[]assertion doesn't validate the actual type. Ifevent.datais not an array of strings, spreading it intofilesCreatedwill fail or produce unexpected results.🛡️ Proposed fix with runtime validation
if (event.type === 'file_created' && event.data) { - const files = event.data as string[]; - task.filesCreated.push(...files); + const files = event.data; + if (Array.isArray(files)) { + task.filesCreated.push(...files.filter((f): f is string => typeof f === 'string')); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/taskManager.ts` around lines 85 - 88, The code unsafely asserts event.data as string[] and spreads it into task.filesCreated; replace that with a runtime check: verify Array.isArray(event.data), map/filter to keep only items where typeof item === 'string' (or coerce to string if intended), and then push those validated strings into task.filesCreated (e.g., const files = (Array.isArray(event.data) ? event.data.filter(i => typeof i === 'string') : []); task.filesCreated.push(...files)). Update the block handling event.type === 'file_created' in taskManager.ts to gracefully handle non-array or mixed-type event.data rather than using the unsafe cast.
🧹 Nitpick comments (25)
mini-muse/frontend/src/types/index.ts (1)
30-33: Consider using a status union type for consistency.
CreateTaskResponse.statusis typed asstring, whileTaskSummary.statususes a union type'pending' | 'running' | 'completed' | 'failed'. Using the same union would provide better type safety.💡 Optional: Use consistent status typing
export interface CreateTaskResponse { taskId: string; - status: string; + status: 'pending' | 'running' | 'completed' | 'failed'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/types/index.ts` around lines 30 - 33, CreateTaskResponse.status is currently a plain string while TaskSummary.status uses the union 'pending' | 'running' | 'completed' | 'failed'; change CreateTaskResponse to use the same union for status (or extract a shared TaskStatus type and use it for both CreateTaskResponse.status and TaskSummary.status) so both types share the identical union for consistent type safety.mini-muse/frontend/vite.config.ts (1)
10-14: Consider addingchangeOriginto the proxy configuration.Adding
changeOrigin: truecan help with CORS and ensures theHostheader is rewritten to match the target. This is particularly useful if you add authentication or host-based routing later.💡 Optional: Enhanced proxy configuration
server: { proxy: { - '/api': 'http://127.0.0.1:7001', + '/api': { + target: 'http://127.0.0.1:7001', + changeOrigin: true, + }, }, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/vite.config.ts` around lines 10 - 14, Update the Vite dev server proxy for the '/api' entry to include changeOrigin: true so the Host header is rewritten to the target; locate the server.proxy configuration in vite.config.ts (the '/api' proxy entry) and change the simple string target into an object with target: 'http://127.0.0.1:7001' and changeOrigin: true to ensure proper CORS/host behavior (optionally add secure or other proxy options later).mini-muse/frontend/package.json (1)
11-25: Consider pinning dependency versions or adding engines field.The caret ranges (
^) are fine for an example project, but consider:
- Adding an
enginesfield to document the Node.js version requirement (README mentions>= 18)- Adding
npm-shrinkwrap.jsonor committingpackage-lock.jsonif reproducibility is important💡 Optional: Add engines field
"version": "1.0.0", "type": "module", + "engines": { + "node": ">=18" + }, "scripts": {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/package.json` around lines 11 - 25, Add an "engines" field to package.json to document the Node.js version required (e.g., matching README's ">= 18") and either commit the generated lockfile (package-lock.json) or add an npm-shrinkwrap.json to the repo for reproducible installs; update the package.json near the existing "dependencies" and "devDependencies" entries to include the "engines" key and ensure package-lock.json (or npm-shrinkwrap.json) is present in version control.mini-muse/README.md (1)
42-56: Consider a single root “build-all” command for production flow.Line 55 depends on Line 45 having run first. A single script reduces deployment mistakes.
♻️ Suggested change
# mini-muse/package.json "scripts": { "dev": "egg-bin dev --ts", "build": "tsc", + "build:all": "cd frontend && npm run build && cd .. && npm run build", "start": "egg-scripts start --ts", "test": "egg-bin test --ts" }# mini-muse/README.md -# Production -npm run build && npm start +# Production +npm run build:all && npm start🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/README.md` around lines 42 - 56, Add a new top-level npm script (e.g., "build-all") that runs the frontend build then the root production build so production deploys don't depend on manual ordering; update package.json scripts to include "build-all" which executes the frontend build step (the command currently run via "cd frontend && npm run build") followed by the existing root "build" and ensure "npm start" still runs the server; update README instructions to reference "npm run build-all && npm start" for the Production flow and keep "npm run dev" unchanged for Development.mini-muse/app/lib/prompts.ts (1)
86-96: Escape fenced-block delimiters in dynamic prompt content.If
existingFilesorinstructioncontains ``` it can break the prompt format and degrade tool behavior.🧩 Suggested hardening
export function createModifyPrompt(instruction: string, existingFiles: string[]): string { + const sanitize = (value: string) => value.replace(/```/g, '\\`\\`\\`'); + const safeFiles = existingFiles.map(sanitize); + const safeInstruction = sanitize(instruction); + return `The project has been generated with the following file structure: \`\`\` -${existingFiles.join('\n')} +${safeFiles.join('\n')} \`\`\` The user wants to make the following modification: --- -${instruction} +${safeInstruction} ---🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/prompts.ts` around lines 86 - 96, The template inserts raw existingFiles and instruction into a fenced code block; if either contains ``` it breaks the prompt format, so add a sanitize helper (e.g., sanitize) that escapes triple-backticks (replace /```/g with '\`\\`\\`' or equivalent), produce safeFiles = existingFiles.map(sanitize) and safeInstruction = sanitize(instruction), and use ${safeFiles.join('\n')} and ${safeInstruction} in the returned template instead of ${existingFiles.join('\n')} and ${instruction}; ensure the helper is applied wherever these dynamic values are interpolated in the prompt generation function.mini-muse/app/middleware/errorHandler.ts (1)
17-21: Consider sanitizing error messages for 500 errors.Exposing
error.messagedirectly in the response could leak implementation details in production. For non-client errors (500s), consider using a generic message.💡 Suggestion
ctx.status = error.status || 500; ctx.body = { success: false, - error: error.message || 'Internal Server Error', + error: error.status && error.status < 500 + ? (error.message || 'Request Error') + : 'Internal Server Error', };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/middleware/errorHandler.ts` around lines 17 - 21, The current error handler sets ctx.body.error to error.message which can leak details for server errors; update the middleware (where ctx.status is set) to only return the real error.message for client errors (status in 400-499) and replace messages for 500/unknown errors with a generic string like "Internal Server Error"; keep full error details logged on the server (e.g., via logger or console.error) but do not include them in ctx.body when ctx.status >= 500 so production responses are sanitized.mini-muse/app/lib/tools/reader/readFile.ts (1)
50-54: Consider preserving the original error for debugging.The catch block assumes all errors are "file not found", but other errors like permission denied could occur. Logging or including the actual error would help debugging.
💡 Suggestion
- } catch { + } catch (err) { + const errorCode = (err as NodeJS.ErrnoException).code; return { success: false, - error: `File not found: ${data.filePath}`, + error: errorCode === 'ENOENT' + ? `File not found: ${data.filePath}` + : `Failed to read file: ${data.filePath}`, }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/reader/readFile.ts` around lines 50 - 54, In the readFile catch block (in readFile.ts) capture the thrown error (e.g., catch (err)) instead of a bare catch and preserve its information in the returned object: keep success: false but change error to a concise message and add a new field (e.g., originalError or errorDetails) containing err.message or err.stack so callers and logs can see the real cause (permission denied, EACCES, etc.); update any callers if needed to read the new field and/or log the original error for debugging.mini-muse/app/service/tools.ts (2)
10-12: Unsafe cast ofnametoToolName.The cast
name as ToolNamebypasses type safety. WhileexecuteToolhandles unknown tools gracefully at runtime, consider validating the name or documenting this behavior.💡 Optional: Add validation
+import { getToolDefinitions, executeTool, ToolName, ToolResult, isValidToolName } from '../lib/tools/registry'; + async execute(name: string, input: Record<string, unknown>, outputDir: string): Promise<ToolResult> { + if (!isValidToolName(name)) { + return { success: false, error: `Unknown tool: ${name}` }; + } return executeTool(name as ToolName, input, outputDir); }This requires adding
isValidToolNameto the registry.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/tools.ts` around lines 10 - 12, The current implementation in execute (async execute(name: string, ...)) unsafely casts name to ToolName before calling executeTool; replace the cast with a proper validation: check the provided name against the tool registry (implement or call isValidToolName) and only call executeTool(name as ToolName, ...) after the check, otherwise return a rejected Promise or throw a descriptive error (or log and return a ToolResult indicating unknown tool). Update or add isValidToolName in the registry and use it from execute, or change the execute signature to accept ToolName if callers can guarantee the type.
1-4: Consider consolidatingToolDefinitionto a single source of truth.
ToolDefinitionis defined in bothaiClient.tsandregistry.ts(see relevant snippets). The service imports fromaiClient.tsbutgetToolDefinitions()returns fromregistry.ts. If these interfaces diverge, type mismatches could occur.Consider exporting
ToolDefinitionfrom a single location (e.g.,registry.ts) and importing it elsewhere.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/tools.ts` around lines 1 - 4, ToolDefinition is declared in multiple modules causing potential type divergence between the service import and getToolDefinitions() return; consolidate to a single source by exporting ToolDefinition from the registry module (the same module that defines getToolDefinitions()/executeTool/ToolName/ToolResult) and update aiClient.ts and mini-muse/app/service/tools.ts to import ToolDefinition from that registry export; ensure you remove or re-export any duplicate declaration (or make aiClient re-export registry's ToolDefinition) so all usages (getToolDefinitions, executeTool, and the Service code) reference the same type symbol ToolDefinition.mini-muse/frontend/src/components/ProgressLog.tsx (1)
8-26: Missing styles and labels formodify_startedandmodify_completedevent types.The
ProgressEventtype includes'modify_started'and'modify_completed'(seemini-muse/frontend/src/types/index.ts), but these are not present in thetypeStylesandtypeLabelsmaps. They'll fall back to defaults, but explicit handling would improve UX.💡 Suggestion
const typeStyles: Record<string, string> = { status: 'text-blue-600', thinking: 'text-gray-500', tool_call: 'text-purple-600', tool_result: 'text-green-600', file_created: 'text-emerald-600', completed: 'text-green-700 font-medium', error: 'text-red-600 font-medium', + modify_started: 'text-amber-600', + modify_completed: 'text-amber-700 font-medium', }; const typeLabels: Record<string, string> = { status: 'STATUS', thinking: 'THINKING', tool_call: 'TOOL', tool_result: 'RESULT', file_created: 'FILE', completed: 'DONE', error: 'ERROR', + modify_started: 'MODIFY', + modify_completed: 'MODIFIED', };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/components/ProgressLog.tsx` around lines 8 - 26, Add explicit mappings for the missing ProgressEvent types by updating the typeStyles and typeLabels objects to include entries for "modify_started" and "modify_completed" so they don't fall back to defaults; e.g., add semantic class names (such as 'text-yellow-600' or 'text-amber-600' for modify_started and 'text-amber-700 font-medium' for modify_completed) to typeStyles and matching labels like "MODIFY" or "MODIFY_DONE" to typeLabels, ensuring the keys exactly match the ProgressEvent variants so components using typeStyles/typeLabels render the correct text and classes.mini-muse/frontend/src/pages/ProgressPage.tsx (1)
44-52: Edge case: No status indicator shown when stream ends unexpectedly.If the SSE stream ends (
donebecomes true) without a'completed'or'error'event, none of the three status indicator conditions will match, leaving the status area empty. Consider handling this edge case.💡 Suggestion
{!done && !failed && ( <div className="w-4 h-4 rounded-full bg-indigo-500 animate-pulse" /> )} + {done && !isCompleted && !failed && ( + <div className="w-4 h-4 rounded-full bg-yellow-500" /> + )} {isCompleted && ( <div className="w-4 h-4 rounded-full bg-green-500" /> )}And update the status text:
<span className="font-medium text-gray-700"> - {failed ? 'Generation Failed' : isCompleted ? 'Generation Complete' : 'Generating...'} + {failed ? 'Generation Failed' : isCompleted ? 'Generation Complete' : done ? 'Stream Ended' : 'Generating...'} </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/pages/ProgressPage.tsx` around lines 44 - 52, The status area can be empty when the SSE stream ends (done) without emitting 'completed' or 'error'; update the render logic in ProgressPage.tsx to handle the edge case by adding a condition (e.g., done && !isCompleted && !failed) that shows a distinct indicator (amber/gray) and corresponding status text for "stream ended unexpectedly" or "stopped" so users see a terminal state; you can also set or map an explicit state flag instead of relying solely on done to make the intent clear.mini-muse/config/config.default.ts (1)
14-14: Model identifier is valid.claude-sonnet-4-20250514is Anthropic's current Claude Sonnet 4 model and is available on the Anthropic API. Consider making this configurable via environment variable for better flexibility in different environments.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/config/config.default.ts` at line 14, The config currently hardcodes model: 'claude-sonnet-4-20250514'; change it to read from an environment variable (e.g., process.env.ANTHROPIC_MODEL or similar) with 'claude-sonnet-4-20250514' as the fallback default so different environments can override the model; update the exported config object in config.default.ts (the model property) to use the env var fallback pattern and update any README or comments if present to document the new ANTHROPIC_MODEL variable name.mini-muse/app/lib/tools/generator/createComponent.ts (2)
78-81: Consider validating thedirectoryinput to prevent path traversal.The
directoryfield is user-controlled and directly joined into the path without sanitization. While the output is constrained tooutputDir/src/components/..., a malicious input like../../etccould write outside the intended directory.For consistency with
deleteFile.tsandreadFile.ts(which include path traversal checks), consider adding a similar guard here.🛡️ Suggested validation
// Determine component directory + if (data.directory && (data.directory.includes('..') || path.isAbsolute(data.directory))) { + return { + success: false, + error: 'Invalid directory: path traversal not allowed', + }; + } const baseDir = data.directory ? path.join(outputDir, 'src', 'components', data.directory, componentName) : path.join(outputDir, 'src', 'components', componentName);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/generator/createComponent.ts` around lines 78 - 81, The code builds baseDir from user-controlled data.directory without sanitization, allowing path traversal; update createComponent.ts to validate/sanitize data.directory before using it in path.join (same approach as deleteFile.ts/readFile.ts): check for presence of ".." or absolute paths and normalize the joined path, then verify the resulting baseDir is a subpath of the intended root (outputDir/src/components) before writing; apply this guard around the baseDir computation and use the safe baseDir in the rest of the createComponent flow.
62-73: Missing error handling for file system operations.The function performs multiple file writes (lines 86, 93, 103) without try-catch, so any I/O error will propagate as an unhandled exception. Other tools in this PR (e.g.,
deleteFile,readProjectFile) wrap operations and return structured error results.♻️ Suggested fix to add error handling
export async function createComponent( input: Record<string, unknown>, outputDir: string ): Promise<ToolResult> { const data = input as unknown as CreateComponentInput; if (!data.name || !data.componentCode) { return { success: false, error: 'name and componentCode are required', }; } + try { const componentName = toPascalCase(data.name); const filesCreated: string[] = []; // ... rest of implementation ... return { success: true, message: `Created component: ${componentName}`, filesCreated, data: { componentName, directory: baseDir, }, }; + } catch (error) { + return { + success: false, + error: `Failed to create component: ${error instanceof Error ? error.message : String(error)}`, + }; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/generator/createComponent.ts` around lines 62 - 73, createComponent currently performs multiple filesystem writes (the write operations that create the component files and update index) without error handling; wrap the file I/O in a try/catch around the region that performs the writes in createComponent and on any thrown error return a ToolResult with success: false and a descriptive error string (include the caught error.message). Ensure the catch covers all write operations (the same block that writes componentCode, CSS/TSX files, and the index update) so failures are returned as structured errors rather than letting exceptions propagate.mini-muse/app/lib/tools/registry.ts (1)
13-21: DuplicateToolDefinitioninterface.The
ToolDefinitioninterface is also defined inmini-muse/app/service/aiClient.ts(lines 31-39) with an identical structure. Consider exporting from a single location and importing where needed to avoid drift.♻️ Suggested consolidation
Export from registry.ts (or a shared types file) and import in aiClient.ts:
// In aiClient.ts import { ToolDefinition } from '../lib/tools/registry';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/registry.ts` around lines 13 - 21, Duplicate ToolDefinition interface exists; remove the duplicate definition from the aiClient.ts module and instead import the single exported ToolDefinition from the registry module (or move ToolDefinition to a new shared types module and export it from there), update aiClient.ts to import { ToolDefinition } from the registry/shared-types export, and ensure all references to ToolDefinition (the interface name) remain valid.mini-muse/app/controller/generate.ts (2)
111-120: Direct mutation oftask.statusbypasses service layer.Line 113 directly sets
task.status = 'running', bypassingtaskManagerwhich may have additional logic for state transitions. This could lead to inconsistent state if the service tracks status changes.♻️ Consider using a service method
- // Reset status to running - task.status = 'running'; + // Reset status to running via service + ctx.service.taskManager.setTaskStatus(taskId, 'running');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/controller/generate.ts` around lines 111 - 120, The code directly mutates task.status and task.chatHistory; instead call the task service methods (e.g., taskManager.updateStatus or taskManager.setTaskStatus) to set status to 'running' and use a taskManager.appendChatEntry/ addChatHistoryEntry method to add the user message (preserving role, content, timestamp) so any business rules/side effects in taskManager run; replace direct assignments to task.status and task.chatHistory.push with those service calls (use task.id or the task object as the identifier per the taskManager API).
268-271: Missing error handling for archive streaming.If
archive.directory()orarchive.finalize()throws (e.g., directory doesn't exist), the error will propagate unhandled and the response may be left in an inconsistent state.♻️ Suggested error handling
const archive = archiver('zip', { zlib: { level: 9 } }); + archive.on('error', (err) => { + ctx.logger.error('Archive error for task %s: %s', taskId, err.message); + ctx.res.end(); + }); archive.pipe(ctx.res); archive.directory(task.outputDir, task.appName); await archive.finalize();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/controller/generate.ts` around lines 268 - 271, Wrap the archiver streaming code in a try/catch and add error event handlers so streaming failures from archive.directory or archive.finalize are caught and the HTTP response is properly closed; specifically, attach an 'error' listener to the archive to handle stream errors, guard the calls to archive.directory(task.outputDir, task.appName) and await archive.finalize() inside the try block, and in the catch (or on error) call ctx.status and ctx.body (or ctx.res.end) to return a clear error and ensure ctx.res is ended so the response doesn't hang.mini-muse/app/lib/tools/generator/createPage.ts (2)
40-91: Significant code duplication withcreateComponent.ts.Both
createPageandcreateComponentshare nearly identical logic for:
- Formatting and writing main TSX file
- Formatting and writing optional CSS module
- Generating barrel
index.tswith conditional default export handlingConsider extracting a shared helper function (e.g.,
createModuleWithStyle) to reduce duplication and ensure consistent behavior across generators.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/generator/createPage.ts` around lines 40 - 91, Extract shared file-creation logic into a helper (e.g., createModuleWithStyle) that both createPage and createComponent call: the helper should accept parameters like moduleName, outputDir (or parent path), mainCode (tsx string), styleCode (optional), and route/metadata info, perform toPascalCase(moduleName), create the module directory, format mainCode with formatCode('typescript') and styleCode with formatCode('css') as needed, write the main .tsx and optional .module.css via writeFile, detect default export via /export\s+default\b/.test(mainCode) and generate the index.ts barrel accordingly, and return filesCreated plus module metadata (name, directory, etc.); then replace the duplicated logic in createPage (and createComponent) to call this helper and reuse its returned filesCreated/metadata.
40-51: Same missing error handling pattern ascreateComponent.ts.File operations at lines 62, 69, and 79 lack try-catch wrapping, so I/O errors will propagate as unhandled exceptions rather than returning a structured
ToolResultwithsuccess: false.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/generator/createPage.ts` around lines 40 - 51, The createPage function is missing try/catch around its file I/O calls so filesystem errors will throw instead of returning a ToolResult; wrap the file write and directory operations in a try/catch (same pattern used in createComponent.ts) so any thrown error is caught and you return { success: false, error: String(err) } (include contextual text like "failed to create page" plus err.message). Locate the I/O calls in createPage that use outputDir and data.pageCode (the write/mkdir operations) and move them into the try block, and in the catch return a structured ToolResult with success:false and the error details.mini-muse/app/lib/tools/validator/validateProject.ts (2)
166-172: Regex for detectinganytype may produce false positives.The regex
/:\s*any\b/gwill match occurrences inside comments and string literals (e.g.,// type: any exampleor"type: any"). For a validation tool, this could report spurious issues.A more robust approach would use a parser, but that may be overkill. Consider documenting this limitation or using a slightly more restrictive pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/validator/validateProject.ts` around lines 166 - 172, The current detection using the regex /:\s*any\b/g inside validateProject.ts (where anyMatches is computed) can match comments and string literals; update the logic to avoid false positives by either (1) using a simple pre-filter that strips or ignores comments and string literals before running the regex, or (2) tightening the pattern to only match TypeScript-like annotations (for example require a preceding identifier or punctuation like /\b[a-zA-Z0-9_$]+\s*:\s*any\b/ or include lookbehind to avoid quotes/slash), or (3) document the limitation in the validation note if you prefer not to change detection; apply this change to the block that sets anyMatches and adjust the issue message generation accordingly.
104-137: Import resolution doesn't handle@/path alias.The import validator skips external packages but also skips
@/aliases (line 112). This means aliased imports won't be validated even though they're local paths. If the generated projects use@/aliases, unresolved aliased imports will be silently ignored.Consider adding resolution logic for the
@/alias (typically maps tosrc/) if you want comprehensive import validation.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/lib/tools/validator/validateProject.ts` around lines 104 - 137, The validator currently skips imports that start with '@/' so aliased local imports are ignored; update the loop in validateProject.ts (inside the tsFiles for-loop where importPath is computed) to resolve '@/...' to the project source directory and validate it like a relative path: when importPath startsWith('@/'), map it to a filesystem path (e.g. join outputDir and 'src' with the remainder of importPath), compute a targetPath from that mapping, then run the same extensions check (the extensions array and fileExists loop used for relative imports) and push the same error message using relativePath if no file is found; keep existing behavior for external packages and for './'/'../' relative paths.mini-muse/app/service/orchestrator.ts (1)
219-226: Path separator replacement may fail on Windows.Line 222 uses
f.replace(outputDir + '/', '')which assumes Unix-style path separators. On Windows,outputDirwould use backslashes, causing the replacement to fail.Consider using
path.relative()for cross-platform compatibility:♻️ Suggested fix
if (result.filesCreated && result.filesCreated.length > 0) { ctx.service.taskManager.updateProgress(taskId, { type: 'file_created', - message: `Created: ${result.filesCreated.map(f => f.replace(outputDir + '/', '')).join(', ')}`, + message: `Created: ${result.filesCreated.map(f => path.relative(outputDir, f)).join(', ')}`, data: result.filesCreated, timestamp: Date.now(), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/orchestrator.ts` around lines 219 - 226, The code uses f.replace(outputDir + '/', '') which breaks on Windows due to backslashes; replace that logic to compute a cross-platform relative path (e.g., use path.relative(outputDir, f) on each entry in result.filesCreated) and use the relative path in the message and data; also ensure the Node 'path' module is imported and, if you need consistent separators in the message, normalize the resulting relative path (e.g., replace backslashes) before joining.mini-muse/frontend/src/services/api.ts (2)
5-14: Consider checking HTTP status before parsing JSON.The functions don't check
res.okbefore attempting to parse JSON. If the server returns a non-2xx response with a non-JSON body (e.g., HTML error page),res.json()will throw an unhelpful parsing error.♻️ Suggested improvement
export async function createTask(description: string, appName: string): Promise<CreateTaskResponse> { const res = await fetch(`${BASE_URL}/generate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ description, appName }), }); + if (!res.ok) { + throw new Error(`Request failed with status ${res.status}`); + } const json: ApiResponse<CreateTaskResponse> = await res.json(); if (!json.success || !json.data) throw new Error(json.error || 'Failed to create task'); return json.data; }This pattern should be applied consistently across all API functions.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/services/api.ts` around lines 5 - 14, The createTask function should check the HTTP response status before calling res.json to avoid parse errors on non-JSON error pages: after awaiting fetch in createTask, verify res.ok and if false attempt to read and include a safe error payload (e.g., try res.json() in a try/catch or fallback to res.text()) and then throw a meaningful Error containing res.status/res.statusText and the body; apply this same pattern to other API functions that call res.json so they always handle non-2xx responses gracefully and include status and body in the thrown error.
27-29: Silent error swallowing hides debugging issues.The empty catch block silently ignores JSON parse errors. While ignoring malformed events may be intentional, completely swallowing errors makes debugging difficult when things go wrong.
♻️ Consider logging parse failures
} catch { - // ignore parse errors + // Log parse errors in development for debugging + if (import.meta.env.DEV) { + console.warn('Failed to parse SSE event:', e.data); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/services/api.ts` around lines 27 - 29, The empty catch block that swallows JSON parse errors in the API service should be changed to log the parse failure instead of silently ignoring it: in the catch after the JSON.parse call (the lone catch { ... } in the api service), capture the error and the raw payload and emit a debug/warn log entry (e.g., include error.message and the raw event string) so malformed events are visible while keeping the existing flow (do not rethrow unless intended).mini-muse/app/service/taskManager.ts (1)
32-37: In-memory task storage lacks cleanup mechanism and persistence.The
tasksMap will grow indefinitely as tasks are created but never removed. This leads to unbounded memory growth over time. Additionally, all tasks are lost on server restart.Consider adding:
- A TTL-based cleanup for completed/failed tasks
- Optional persistence if task history matters
♻️ Example cleanup mechanism
// Singleton task store shared across all service instances const tasks = new Map<string, TaskInfo>(); + +// Optional: Clean up old completed/failed tasks periodically +const TASK_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours + +setInterval(() => { + const now = Date.now(); + for (const [id, task] of tasks) { + if ((task.status === 'completed' || task.status === 'failed') && + now - task.createdAt > TASK_TTL_MS) { + tasks.delete(id); + } + } +}, 60 * 60 * 1000); // Check hourly🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/taskManager.ts` around lines 32 - 37, The tasks Map (tasks) holding TaskInfo entries has no cleanup or persistence; add a TTL cleanup and optional persistence: extend TaskInfo with timestamps (createdAt, finishedAt) and a status field if not present, implement a configurable cleanup loop (e.g., a setInterval in taskManager) that scans tasks and deletes entries whose status is completed/failed and finishedAt + TTL < now, and ensure task lifecycle points (where tasks are created/updated—reference the functions/methods that set TaskInfo in the tasks Map) update finishedAt/status so the cleaner can work; additionally add an optional persistence adapter hook (simple JSON file flush/load or pluggable interface) invoked on startup to load existing tasks and periodically/on-change to persist, controlled by configuration.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (50)
mini-muse/.env.examplemini-muse/CLAUDE.mdmini-muse/README.mdmini-muse/app/controller/generate.tsmini-muse/app/controller/home.tsmini-muse/app/lib/prompts.tsmini-muse/app/lib/tools/analyzer/analyzeRequirements.tsmini-muse/app/lib/tools/analyzer/planArchitecture.tsmini-muse/app/lib/tools/generator/createComponent.tsmini-muse/app/lib/tools/generator/createConfig.tsmini-muse/app/lib/tools/generator/createFile.tsmini-muse/app/lib/tools/generator/createHook.tsmini-muse/app/lib/tools/generator/createPage.tsmini-muse/app/lib/tools/generator/createStyle.tsmini-muse/app/lib/tools/generator/deleteFile.tsmini-muse/app/lib/tools/reader/readFile.tsmini-muse/app/lib/tools/registry.tsmini-muse/app/lib/tools/validator/validateProject.tsmini-muse/app/lib/utils.tsmini-muse/app/middleware/errorHandler.tsmini-muse/app/router.tsmini-muse/app/service/aiClient.tsmini-muse/app/service/orchestrator.tsmini-muse/app/service/preview.tsmini-muse/app/service/taskManager.tsmini-muse/app/service/tools.tsmini-muse/config/config.default.tsmini-muse/config/config.local.tsmini-muse/config/plugin.tsmini-muse/docs/design.mdmini-muse/frontend/index.htmlmini-muse/frontend/package.jsonmini-muse/frontend/postcss.config.jsmini-muse/frontend/src/App.tsxmini-muse/frontend/src/components/ChatPanel.tsxmini-muse/frontend/src/components/CodePreview.tsxmini-muse/frontend/src/components/FileTree.tsxmini-muse/frontend/src/components/ProgressLog.tsxmini-muse/frontend/src/index.cssmini-muse/frontend/src/main.tsxmini-muse/frontend/src/pages/HomePage.tsxmini-muse/frontend/src/pages/ProgressPage.tsxmini-muse/frontend/src/pages/ResultPage.tsxmini-muse/frontend/src/services/api.tsmini-muse/frontend/src/types/index.tsmini-muse/frontend/tailwind.config.jsmini-muse/frontend/tsconfig.jsonmini-muse/frontend/vite.config.tsmini-muse/package.jsonmini-muse/tsconfig.json
mini-muse/app/controller/home.ts
Outdated
| try { | ||
| const html = await fs.readFile(indexPath, 'utf-8'); | ||
| ctx.type = 'text/html'; | ||
| ctx.body = html; | ||
| } catch { | ||
| ctx.body = 'Mini-Muse is running. Please build the frontend first: cd frontend && npm run build'; | ||
| } |
There was a problem hiding this comment.
Return an explicit error status when frontend entry cannot be served.
The failure branch sends a success-shaped response body without setting an error status, which hides deployment/runtime problems.
🔧 Proposed fix
- } catch {
- ctx.body = 'Mini-Muse is running. Please build the frontend first: cd frontend && npm run build';
+ } catch (error) {
+ ctx.logger.error({ err: error }, 'Failed to read app/public/index.html');
+ ctx.status = 503;
+ ctx.type = 'text/plain';
+ ctx.body = 'Mini-Muse is running. Please build the frontend first: cd frontend && npm run build';
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| const html = await fs.readFile(indexPath, 'utf-8'); | |
| ctx.type = 'text/html'; | |
| ctx.body = html; | |
| } catch { | |
| ctx.body = 'Mini-Muse is running. Please build the frontend first: cd frontend && npm run build'; | |
| } | |
| try { | |
| const html = await fs.readFile(indexPath, 'utf-8'); | |
| ctx.type = 'text/html'; | |
| ctx.body = html; | |
| } catch (error) { | |
| ctx.logger.error({ err: error }, 'Failed to read app/public/index.html'); | |
| ctx.status = 503; | |
| ctx.type = 'text/plain'; | |
| ctx.body = 'Mini-Muse is running. Please build the frontend first: cd frontend && npm run build'; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/controller/home.ts` around lines 10 - 16, The catch block in
the handler that reads indexPath using fs.readFile currently returns a plaintext
success-shaped body without an HTTP error status; update the catch to set an
appropriate error status (e.g., ctx.status = 500 or 404 depending on semantics)
before setting ctx.type and ctx.body so callers see a non-2xx response when
fs.readFile(indexPath, 'utf-8') fails; modify the try/catch around fs.readFile,
referencing indexPath, ctx.type, and ctx.body to add the status line and keep
the informative message.
| - Implements responsive design with CSS modules or styled-components | ||
|
|
||
| ## Technology Stack | ||
|
|
||
| Always generate projects using: | ||
| - **Build Tool**: Vite | ||
| - **Framework**: React 18+ | ||
| - **Language**: TypeScript (strict mode) | ||
| - **Styling**: CSS Modules (*.module.css) | ||
| - **Routing**: React Router v6 (if multiple pages) |
There was a problem hiding this comment.
Align styling instructions with the project’s Tailwind stack.
Line 48 and Lines 56-57 instruct CSS Modules/styled-components, but this project is documented and scaffolded around Tailwind. This can produce inconsistent or non-runnable output.
🧭 Proposed fix
- - Implements responsive design with CSS modules or styled-components
+ - Implements responsive design with Tailwind CSS utility classes
...
- - **Styling**: CSS Modules (*.module.css)
+ - **Styling**: Tailwind CSS📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - Implements responsive design with CSS modules or styled-components | |
| ## Technology Stack | |
| Always generate projects using: | |
| - **Build Tool**: Vite | |
| - **Framework**: React 18+ | |
| - **Language**: TypeScript (strict mode) | |
| - **Styling**: CSS Modules (*.module.css) | |
| - **Routing**: React Router v6 (if multiple pages) | |
| - Implements responsive design with Tailwind CSS utility classes | |
| ## Technology Stack | |
| Always generate projects using: | |
| - **Build Tool**: Vite | |
| - **Framework**: React 18+ | |
| - **Language**: TypeScript (strict mode) | |
| - **Styling**: Tailwind CSS | |
| - **Routing**: React Router v6 (if multiple pages) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/prompts.ts` around lines 48 - 57, The styling instructions
currently reference "Styling: CSS Modules (*.module.css)" and a note about
"responsive design with CSS modules or styled-components", which conflicts with
this project's Tailwind-based scaffold; update the prompt text to replace any
"CSS Modules", "styled-components" or related responsive guidance with
Tailwind-specific guidance (e.g., "Styling: Tailwind CSS (utility-first,
configured with postcss/tailwind.config.js)" and remove mentions of CSS
Modules/styled-components) so all generated projects and docs consistently use
Tailwind classes and configuration; locate the block containing the "Styling"
line and the responsive-design sentence and change them accordingly.
| if (!data.appName || !data.pages || !data.components || !data.dataModels) { | ||
| return { | ||
| success: false, | ||
| error: 'Missing required fields in requirements analysis', | ||
| }; | ||
| } | ||
|
|
||
| // Return the analyzed requirements for the next phase | ||
| return { | ||
| success: true, | ||
| message: `Analyzed requirements for "${data.appTitle}": ${data.pages.length} pages, ${data.components.length} components, ${data.dataModels.length} data models`, | ||
| data: { | ||
| appName: data.appName, | ||
| appTitle: data.appTitle, | ||
| pages: data.pages, | ||
| components: data.components, | ||
| dataModels: data.dataModels, | ||
| features: data.features, | ||
| hooks: data.hooks || [], |
There was a problem hiding this comment.
Validate all required fields and array shapes before building the result.
Line 139 does not enforce description, appTitle, or features, and it does not confirm array types. Line 149 then assumes .length exists, which can break or propagate malformed state.
🛠️ Proposed fix
export async function analyzeRequirements(
input: Record<string, unknown>,
_outputDir: string
): Promise<ToolResult> {
const data = input as unknown as AnalyzeRequirementsInput;
// Validate required fields
- if (!data.appName || !data.pages || !data.components || !data.dataModels) {
+ if (
+ !data.description ||
+ !data.appName ||
+ !data.appTitle ||
+ !Array.isArray(data.pages) ||
+ !Array.isArray(data.components) ||
+ !Array.isArray(data.dataModels) ||
+ !Array.isArray(data.features)
+ ) {
return {
success: false,
error: 'Missing required fields in requirements analysis',
};
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/tools/analyzer/analyzeRequirements.ts` around lines 139 -
157, The current validation in the analyzeRequirements function only checks
appName/pages/components/dataModels but misses required fields (description,
appTitle, features) and doesn't verify that pages/components/dataModels/features
are arrays before using .length; update the initial guard to assert presence of
description, appTitle, and features and verify Array.isArray(...) for pages,
components, dataModels and features (and that they have non-negative lengths),
returning the existing error shape if any check fails; also make the result
message robust by using a safe fallback (e.g., data.appTitle || data.appName)
when interpolating the title and ensure hooks defaults remain as hooks || [].
| // Format code if possible | ||
| let content = data.content; | ||
| if (fileType) { | ||
| content = await formatCode(content, fileType); | ||
| } | ||
|
|
||
| // Determine the full path | ||
| let fullPath: string; | ||
| if (data.filePath.startsWith('src/')) { | ||
| fullPath = path.join(outputDir, data.filePath); | ||
| } else { | ||
| fullPath = path.join(outputDir, 'src', data.filePath); | ||
| } | ||
|
|
||
| await writeFile(fullPath, content); | ||
|
|
||
| return { | ||
| success: true, | ||
| message: `Created file: ${data.filePath}`, | ||
| filesCreated: [fullPath], | ||
| }; |
There was a problem hiding this comment.
Normalize write/format failures to ToolResult instead of throwing.
The tool API returns ToolResult, but errors from formatting/writing currently bubble out and can break orchestration flow.
Suggested fix
- // Format code if possible
- let content = data.content;
- if (fileType) {
- content = await formatCode(content, fileType);
- }
+ try {
+ // Format code if possible
+ let content = data.content;
+ if (fileType) {
+ content = await formatCode(content, fileType);
+ }
- await writeFile(fullPath, content);
+ await writeFile(fullPath, content);
- return {
- success: true,
- message: `Created file: ${data.filePath}`,
- filesCreated: [fullPath],
- };
+ return {
+ success: true,
+ message: `Created file: ${data.filePath}`,
+ filesCreated: [fullPath],
+ };
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return { success: false, error: `Failed to create file: ${msg}` };
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/tools/generator/createFile.ts` around lines 57 - 77, The
createFile flow currently lets errors from formatCode and writeFile escape; wrap
the formatCode call and the await writeFile(fullPath, content) in try/catch
blocks (or a single try around both) and return a ToolResult object on failure
instead of throwing: catch the error from formatCode (when calling
formatCode(content, fileType)) and from writeFile(fullPath, content) and return
{ success: false, message: `Failed to create ${data.filePath}: ${err.message ||
String(err)}`, filesCreated: [] } so callers always get a ToolResult; keep
success path returning the existing success result.
| // Determine the full path | ||
| let fullPath: string; | ||
| if (data.filePath.startsWith('src/')) { | ||
| fullPath = path.join(outputDir, data.filePath); | ||
| } else { | ||
| fullPath = path.join(outputDir, 'src', data.filePath); | ||
| } | ||
|
|
||
| await writeFile(fullPath, content); |
There was a problem hiding this comment.
Block path traversal before writing files.
Lines [63-69] allow ../ and absolute path escape; Line [71] then writes outside the project root.
Suggested fix
- // Determine the full path
+ const rootDir = path.resolve(outputDir);
+ // Determine the full path
let fullPath: string;
if (data.filePath.startsWith('src/')) {
- fullPath = path.join(outputDir, data.filePath);
+ fullPath = path.resolve(rootDir, data.filePath);
} else {
- fullPath = path.join(outputDir, 'src', data.filePath);
+ fullPath = path.resolve(rootDir, 'src', data.filePath);
+ }
+
+ if (fullPath !== rootDir && !fullPath.startsWith(rootDir + path.sep)) {
+ return {
+ success: false,
+ error: 'Access denied: path traversal detected',
+ };
}
await writeFile(fullPath, content);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Determine the full path | |
| let fullPath: string; | |
| if (data.filePath.startsWith('src/')) { | |
| fullPath = path.join(outputDir, data.filePath); | |
| } else { | |
| fullPath = path.join(outputDir, 'src', data.filePath); | |
| } | |
| await writeFile(fullPath, content); | |
| const rootDir = path.resolve(outputDir); | |
| // Determine the full path | |
| let fullPath: string; | |
| if (data.filePath.startsWith('src/')) { | |
| fullPath = path.resolve(rootDir, data.filePath); | |
| } else { | |
| fullPath = path.resolve(rootDir, 'src', data.filePath); | |
| } | |
| if (fullPath !== rootDir && !fullPath.startsWith(rootDir + path.sep)) { | |
| return { | |
| success: false, | |
| error: 'Access denied: path traversal detected', | |
| }; | |
| } | |
| await writeFile(fullPath, content); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/tools/generator/createFile.ts` around lines 63 - 71, The
current logic in createFile.ts builds fullPath from data.filePath and then calls
writeFile, allowing directory traversal or absolute escapes; fix by normalizing
and validating the target path before writing: first reject/normalize any input
that is absolute or contains '..' segments (data.filePath), then construct the
final resolvedPath with path.resolve(outputDir, data.filePath) or
path.resolve(outputDir, 'src', data.filePath) depending on the src prefix, and
verify path.relative(outputDir, resolvedPath) does not start with '..' (or
ensure resolvedPath.startsWith(path.resolve(outputDir))). Only call writeFile
with the validated resolvedPath; reference symbols: data.filePath, fullPath (or
resolvedPath), outputDir, and writeFile.
mini-muse/config/config.default.ts
Outdated
| export default () => { | ||
| const config = {} as PowerPartial<EggAppConfig>; | ||
|
|
||
| config.keys = 'mini-muse-secret-key'; |
There was a problem hiding this comment.
Hardcoded secret key should use environment variable.
Using a hardcoded secret key in config is a security risk. In production, this key is used for signing cookies and sessions. Consider reading from environment variable with a fallback only for development.
🔒 Proposed fix
- config.keys = 'mini-muse-secret-key';
+ config.keys = process.env.EGG_SERVER_SECRET || 'mini-muse-secret-key-dev';Add EGG_SERVER_SECRET to .env.example as well.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| config.keys = 'mini-muse-secret-key'; | |
| config.keys = process.env.EGG_SERVER_SECRET || 'mini-muse-secret-key-dev'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/config/config.default.ts` at line 9, The config currently hardcodes
the session/cookie secret via config.keys = 'mini-muse-secret-key'; replace this
by reading from an environment variable (use process.env.EGG_SERVER_SECRET or
similar) with a safe development fallback only when NODE_ENV !== 'production',
and assign that value to config.keys; also add EGG_SERVER_SECRET to .env.example
so deploys know to set it. Ensure the referenced symbol is config.keys and the
env var name is EGG_SERVER_SECRET.
| unsubscribeRef.current = subscribeToProgress( | ||
| taskId, | ||
| (event) => { | ||
| const ev = event as ProgressEvent; | ||
| // Filter out events from before the modification started | ||
| if (ev.timestamp < modifyStartTimeRef.current) return; | ||
| setModifyEvents(prev => [...prev, ev]); | ||
|
|
||
| if (ev.type === 'completed' || ev.type === 'modify_completed') { | ||
| // Refresh chat history, files, and selected file content | ||
| getChatHistory(taskId).then(history => { | ||
| setChatHistory(history); | ||
| setModifyEvents([]); | ||
| setIsModifying(false); | ||
| }); | ||
|
|
||
| getFiles(taskId).then(onFilesUpdated); | ||
|
|
||
| if (selectedFile) { | ||
| getFileContent(taskId, selectedFile).then(content => { | ||
| onFileContentUpdated(selectedFile, content); | ||
| }).catch(() => {}); | ||
| } | ||
| } | ||
|
|
||
| if (ev.type === 'error') { | ||
| setIsModifying(false); | ||
| } | ||
| }, | ||
| () => { | ||
| // SSE connection closed | ||
| } | ||
| ); |
There was a problem hiding this comment.
Close previous/current SSE subscriptions to prevent leaks and duplicate streams.
Lines [60-92] overwrite unsubscribeRef.current without closing existing subscriptions, and completion/error paths never unsubscribe.
Suggested fix
- unsubscribeRef.current = subscribeToProgress(
+ unsubscribeRef.current?.();
+ unsubscribeRef.current = subscribeToProgress(
taskId,
(event) => {
@@
if (ev.type === 'completed' || ev.type === 'modify_completed') {
+ unsubscribeRef.current?.();
+ unsubscribeRef.current = null;
// Refresh chat history, files, and selected file content
@@
if (ev.type === 'error') {
+ unsubscribeRef.current?.();
+ unsubscribeRef.current = null;
setIsModifying(false);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| unsubscribeRef.current = subscribeToProgress( | |
| taskId, | |
| (event) => { | |
| const ev = event as ProgressEvent; | |
| // Filter out events from before the modification started | |
| if (ev.timestamp < modifyStartTimeRef.current) return; | |
| setModifyEvents(prev => [...prev, ev]); | |
| if (ev.type === 'completed' || ev.type === 'modify_completed') { | |
| // Refresh chat history, files, and selected file content | |
| getChatHistory(taskId).then(history => { | |
| setChatHistory(history); | |
| setModifyEvents([]); | |
| setIsModifying(false); | |
| }); | |
| getFiles(taskId).then(onFilesUpdated); | |
| if (selectedFile) { | |
| getFileContent(taskId, selectedFile).then(content => { | |
| onFileContentUpdated(selectedFile, content); | |
| }).catch(() => {}); | |
| } | |
| } | |
| if (ev.type === 'error') { | |
| setIsModifying(false); | |
| } | |
| }, | |
| () => { | |
| // SSE connection closed | |
| } | |
| ); | |
| unsubscribeRef.current?.(); | |
| unsubscribeRef.current = subscribeToProgress( | |
| taskId, | |
| (event) => { | |
| const ev = event as ProgressEvent; | |
| // Filter out events from before the modification started | |
| if (ev.timestamp < modifyStartTimeRef.current) return; | |
| setModifyEvents(prev => [...prev, ev]); | |
| if (ev.type === 'completed' || ev.type === 'modify_completed') { | |
| unsubscribeRef.current?.(); | |
| unsubscribeRef.current = null; | |
| // Refresh chat history, files, and selected file content | |
| getChatHistory(taskId).then(history => { | |
| setChatHistory(history); | |
| setModifyEvents([]); | |
| setIsModifying(false); | |
| }); | |
| getFiles(taskId).then(onFilesUpdated); | |
| if (selectedFile) { | |
| getFileContent(taskId, selectedFile).then(content => { | |
| onFileContentUpdated(selectedFile, content); | |
| }).catch(() => {}); | |
| } | |
| } | |
| if (ev.type === 'error') { | |
| unsubscribeRef.current?.(); | |
| unsubscribeRef.current = null; | |
| setIsModifying(false); | |
| } | |
| }, | |
| () => { | |
| // SSE connection closed | |
| } | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/components/ChatPanel.tsx` around lines 60 - 92, The
SSE subscription logic overwrites unsubscribeRef.current from
subscribeToProgress without closing any previous subscription and never
unsubscribes on completion/error or when the SSE closes; before assigning
unsubscribeRef.current call and await/clear any existing unsubscribe (if
present) and set it to null, and ensure the callback paths (ev.type ===
'completed' / 'modify_completed', ev.type === 'error', and the
subscribeToProgress close callback) invoke unsubscribeRef.current() and then
clear it; update usages around subscribeToProgress, unsubscribeRef.current,
setIsModifying, getChatHistory, getFiles, and getFileContent to reliably clean
up the SSE handle.
| getChatHistory(taskId).then(history => { | ||
| setChatHistory(history); | ||
| setModifyEvents([]); | ||
| setIsModifying(false); | ||
| }); |
There was a problem hiding this comment.
Don’t gate setIsModifying(false) on successful history refresh.
If getChatHistory fails, Line [73] is never reached and the panel can remain stuck in modifying mode.
Suggested fix
- getChatHistory(taskId).then(history => {
- setChatHistory(history);
- setModifyEvents([]);
- setIsModifying(false);
- });
+ getChatHistory(taskId)
+ .then(history => {
+ setChatHistory(history);
+ })
+ .catch(() => {})
+ .finally(() => {
+ setModifyEvents([]);
+ setIsModifying(false);
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| getChatHistory(taskId).then(history => { | |
| setChatHistory(history); | |
| setModifyEvents([]); | |
| setIsModifying(false); | |
| }); | |
| getChatHistory(taskId) | |
| .then(history => { | |
| setChatHistory(history); | |
| }) | |
| .catch(() => {}) | |
| .finally(() => { | |
| setModifyEvents([]); | |
| setIsModifying(false); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/components/ChatPanel.tsx` around lines 70 - 74, Ensure
setIsModifying(false) runs regardless of getChatHistory outcome by moving it out
of the success-only then branch; update the usage around getChatHistory(taskId)
(where setChatHistory and setModifyEvents are called) to handle errors and
always call setIsModifying(false) (e.g., use .catch(...) to handle failures and
a .finally(...) to call setIsModifying(false) or ensure setIsModifying(false) is
invoked in both then and catch paths) so the panel cannot remain stuck in
modifying mode.
| const root: TreeNode[] = []; | ||
|
|
||
| for (const file of files.sort()) { | ||
| const parts = file.split('/'); |
There was a problem hiding this comment.
Normalize path separators before splitting.
Line [20] assumes /. If backend runs on Windows, path.relative may return \, and the tree will flatten incorrectly.
Suggested fix
- const parts = file.split('/');
+ const normalized = file.replace(/\\/g, '/');
+ const parts = normalized.split('/');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parts = file.split('/'); | |
| const normalized = file.replace(/\\/g, '/'); | |
| const parts = normalized.split('/'); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/components/FileTree.tsx` at line 20, The code splits
file paths using a hardcoded '/' which breaks when path.relative returns Windows
backslashes; normalize separators before splitting by converting backslashes to
forward slashes (or use Node's path.sep logic) so the variable parts =
file.split('/') receives consistent separators—update the logic around the
variable parts (where file is derived, e.g., in the FileTree component) to
replace backslashes with '/' (or normalize the path) then split, ensuring
consistent tree structure across platforms.
| @tailwind base; | ||
| @tailwind components; | ||
| @tailwind utilities; | ||
|
|
||
| body { | ||
| @apply bg-gray-50 text-gray-900; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, check for lint configuration files
echo "=== Searching for lint config files ==="
fd -HI 'biome.json|.biome.json|stylelint.config.*|.stylelintrc*' . 2>/dev/null || echo "No config files found"
echo -e "\n=== Checking project structure ==="
ls -la mini-muse/frontend/ 2>/dev/null | head -20 || echo "Directory not found"
echo -e "\n=== Checking for CSS-related config ==="
find . -maxdepth 3 -type f \( -name "package.json" -o -name "*lint*" -o -name "*tailwind*" \) 2>/dev/null | grep -E "(package.json|lint|tailwind)" | head -20
echo -e "\n=== Checking frontend directory structure ==="
find mini-muse/frontend -maxdepth 2 -type f 2>/dev/null | head -30
echo -e "\n=== Looking for postcss config ==="
fd -HI 'postcss.config.*|tailwind.config.*' . 2>/dev/null || echo "No postcss/tailwind config found"Repository: eggjs/agent-examples
Length of output: 1371
🏁 Script executed:
#!/bin/bash
echo "=== .stylelintrc.json content ==="
cat -n .stylelintrc.json
echo -e "\n=== biome.json content (CSS section) ==="
cat -n biome.json | grep -A 30 -i "css\|lint"
echo -e "\n=== Full biome.json ==="
cat -n biome.json
echo -e "\n=== mini-muse/frontend/postcss.config.js ==="
cat -n mini-muse/frontend/postcss.config.js
echo -e "\n=== mini-muse/frontend/tailwind.config.js ==="
head -50 mini-muse/frontend/tailwind.config.js | cat -nRepository: eggjs/agent-examples
Length of output: 5523
Configure StyleLint and Biome to recognize Tailwind directives.
The .stylelintrc.json extends stylelint-config-standard-scss, which treats @tailwind, @apply, and @layer as unknown at-rules and will fail linting. Add Tailwind support by either:
- Installing and using
stylelint-config-tailwindcssin the StyleLint config, or - Explicitly disabling the
at-rule-no-unknownrule in.stylelintrc.jsonfor Tailwind at-rules
Biome's CSS linter (enabled in biome.json) also lacks Tailwind directive support.
🧰 Tools
🪛 Biome (2.4.4)
[error] 6-6: Tailwind-specific syntax is disabled.
(parse)
[error] 1-1: Unexpected unknown at-rule: tailwind
(lint/suspicious/noUnknownAtRules)
[error] 2-2: Unexpected unknown at-rule: tailwind
(lint/suspicious/noUnknownAtRules)
[error] 3-3: Unexpected unknown at-rule: tailwind
(lint/suspicious/noUnknownAtRules)
🪛 Stylelint (17.3.0)
[error] 1-1: Unexpected unknown at-rule "@tailwind" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
[error] 2-2: Unexpected unknown at-rule "@tailwind" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
[error] 3-3: Unexpected unknown at-rule "@tailwind" (scss/at-rule-no-unknown)
(scss/at-rule-no-unknown)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/index.css` around lines 1 - 6, Style linting fails
because Tailwind at-rules like `@tailwind` and `@apply` in index.css are
unrecognized; update your linter configs to accept Tailwind directives by either
adding the Tailwind StyleLint preset (stylelint-config-tailwindcss) to your
StyleLint extends or by disabling the at-rule-no-unknown rule for Tailwind
at-rules in .stylelintrc.json (allow `@tailwind`, `@apply`, `@layer`), and likewise
configure biome.json’s CSS linter to ignore or support those at-rules so
`@tailwind`, `@apply` and `@layer` no longer trigger errors.
…t API - Replace traditional Egg.js controllers/services with tegg decorators - Backend: @AgentController (auto Thread/Run API), @HTTPController (projects), @SingletonProto (services) - Frontend: fetch+ReadableStream SSE parsing, taskId→threadId migration - Storage: OSSAgentStore replaces in-memory TaskManager - Orchestrator: async generator yielding AgentStreamMessage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix config exports: use defineConfig/plain objects instead of removed EggPlugin/EggAppConfig types - Add "type": "module" to enable ts-node/esm loader for dynamic import() of .ts files - Update tsconfig to module:esnext/bundler with ts-node ESM experimental resolver - Create app/service/package.json as tegg module to avoid controller double-registration - Fix DI inject names to match class names (aiClientService, toolsService, etc.) - Rename HomeController to SpaFallbackController to avoid name collision with egg built-in Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add response validation in AiClientService for empty/malformed API responses - Filter out 'thinking' content blocks that some models (e.g. GLM-4.7) return - Only include system/tools params when non-empty to avoid proxy issues - Restore .gitignore (was missing from tracked files) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (3)
mini-muse/app/service/aiClient.ts (1)
74-80:⚠️ Potential issue | 🟠 MajorFail fast when Anthropic credentials are missing.
Line 76 can construct the client with both
apiKeyandauthTokenundefined, so configuration errors only surface on the first live request. Reject that case here with a clear error instead.Possible fix
private getClient(): Anthropic { if (!this.client) { + const apiKey = process.env.ANTHROPIC_API_KEY; + const authToken = process.env.ANTHROPIC_AUTH_TOKEN; + if (!apiKey && !authToken) { + throw new Error( + 'Missing Anthropic credentials: set ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN' + ); + } + this.client = new Anthropic({ baseURL: process.env.ANTHROPIC_BASE_URL || undefined, - authToken: process.env.ANTHROPIC_AUTH_TOKEN || undefined, - apiKey: process.env.ANTHROPIC_API_KEY || undefined, + authToken: authToken || undefined, + apiKey: apiKey || undefined, }); } return this.client; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/aiClient.ts` around lines 74 - 80, The getClient method currently allows constructing an Anthropic client with both apiKey and authToken undefined; update getClient to fail fast by checking process.env.ANTHROPIC_API_KEY and process.env.ANTHROPIC_AUTH_TOKEN (and/or existing this.client) before instantiating Anthropic and throw a clear Error if neither credential is provided, so the code in getClient (which sets this.client = new Anthropic(...)) never creates a client with missing credentials and surfaces configuration problems immediately.mini-muse/app/service/preview.ts (2)
73-83:⚠️ Potential issue | 🟠 MajorMake the npm registry configurable and bound the install time.
Hardcoding
registry.npmmirror.commakes preview startup region-specific, and this Promise can hang forever on a stalled install. Read the registry and install timeout from config, and kill the child when the timeout expires.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/preview.ts` around lines 73 - 83, Replace the hardcoded registry and unbounded install Promise in the npm install block: read registry and installTimeout from config (e.g., preview config) and set env npm_config_registry to that value when spawning child in the Promise inside the function that calls spawn; start a timer (setTimeout) using installTimeout that, when fired, kills the child process (child.kill()) and rejects the Promise with a timeout Error; when the child emits 'close' or 'error' clear the timer and resolve/reject appropriately; ensure the rejection includes the exit code or timeout reason so callers of this Promise can handle failures.
44-46:⚠️ Potential issue | 🟠 MajorPersist failed preview state on both startup rejection and later process exit.
The outer catch only logs rejected startup, so install failures can stay stuck at
installing; the close handler also ignores exits once the preview becamerunning, so dead servers can still report healthy. Persistinfo.status = 'failed'andinfo.errorin both places sogetStatus()can surface the failure.Also applies to: 126-132
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/preview.ts` around lines 44 - 46, When installAndStart(threadId, outputDir, info) rejects or the preview process later exits abnormally, update and persist the preview info so getStatus() can surface failures: in the .catch handler for installAndStart set info.status = 'failed' and info.error (e.g., err.message or err.stack) before persisting/committing the info, and likewise in the process close/exit handler detect non-zero/abnormal exits and set info.status = 'failed' and info.error to the exit reason/details and persist; ensure you reference the same info object used by getStatus() so the status change is visible.
🧹 Nitpick comments (3)
mini-muse/docs/design.md (1)
220-235: Add language specifiers to fenced code blocks.Per markdownlint, the following code blocks are missing language specifiers:
- Line 220: Directory structure (use
textorplaintext)- Line 255: SSE event format (use
textorhttp)- Line 366: Directory structure (use
textorplaintext)📝 Proposed fix
-``` +```text frontend/src/ ├── pages/-``` +```text event: thread.run.created data: {"id":"run_xxx","threadId":"thread_xxx","status":"queued",...}-``` +```text mini-muse/ ├── app/Also applies to: 255-267, 366-399
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/docs/design.md` around lines 220 - 235, Several fenced code blocks in design.md (the directory listings and the SSE event example) lack language specifiers; edit the three blocks that show the frontend/mini-muse directory trees and the SSE event format (the blocks around the directory structure at the top and bottom and the SSE event example in the middle) and add appropriate language identifiers such as "text" or "plaintext" for the tree blocks and "text" or "http" for the SSE event block so markdownlint warnings are resolved.mini-muse/frontend/src/pages/ResultPage.tsx (1)
81-84: Consider HTTPS/HTTP mixed content when page is served over HTTPS.The preview URL is constructed with
http://which will cause mixed content issues if the main app is served over HTTPS. For local development this is typically fine, but in production deployments the iframe may be blocked.💡 Optional: Use same protocol as current page
const previewUrl = preview.port - ? `http://${window.location.hostname}:${preview.port}` + ? `${window.location.protocol}//${window.location.hostname}:${preview.port}` : '';Note: This only works if the preview server also supports HTTPS, which may not be the case for Vite dev server without additional configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/pages/ResultPage.tsx` around lines 81 - 84, The preview URL currently hardcodes "http://" which causes mixed-content failures when the app is served over HTTPS; change the construction of previewUrl (the code using preview.port and window.location.hostname) to use the current page protocol by prefixing with window.location.protocol + '//' so the iframe uses the same protocol as the host page (i.e., build the URL from window.location.protocol, window.location.hostname and preview.port), keeping the empty-string fallback when preview.port is falsy.mini-muse/frontend/src/services/api.ts (1)
175-184: Weak type safety and potential timestamp assumption issue.
- The function uses
anytypes extensively which bypasses TypeScript's type checking.- Line 182 assumes
createdAtis in seconds (multiplies by 1000), but this should be verified against the backend's actual timestamp format.💡 Proposed improvement
+interface ThreadMessage { + role: string; + content?: Array<{ text?: { value?: string } }>; + createdAt: number; +} + +interface ThreadResponse { + messages?: ThreadMessage[]; +} + export async function getChatHistory(threadId: string): Promise<{ role: string; content: string; timestamp: number }[]> { - const thread = await getThread(threadId); + const thread: ThreadResponse = await getThread(threadId); if (!thread?.messages) return []; - return thread.messages.map((msg: any) => ({ + return thread.messages.map((msg) => ({ role: msg.role, - content: msg.content?.map((c: any) => c.text?.value || '').join('') || '', + content: msg.content?.map((c) => c.text?.value || '').join('') || '', timestamp: msg.createdAt * 1000, })); }Please verify that the backend returns
createdAtas Unix timestamp in seconds (not milliseconds).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/services/api.ts` around lines 175 - 184, The getChatHistory function currently uses unsafe any types and assumes msg.createdAt is seconds; replace the any usages by defining proper types/interfaces for the thread and message objects (e.g., Thread { messages: Message[] } and Message with role, content array items, and createdAt), update getChatHistory to use those types when calling getThread, validate msg.createdAt (check typeof and value range) before multiplying by 1000 and only convert when the backend returns seconds (avoid double-conversion for milliseconds), and replace the content mapping to safely handle undefined/null content items (use explicit checks on msg.content and content[i].text.value) instead of any so TypeScript can catch errors; also verify with the backend whether createdAt is in seconds or milliseconds and change the conversion accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mini-muse/app/controller/HomeController.ts`:
- Around line 5-15: The SPA fallback currently matches all GET routes
(HTTPMethod path '/*') and lives in SpaFallbackController.index; add a guard at
the start of the index method to skip serving index.html for API routes: check
ctx.request.path (or ctx.path) and if it startsWith '/api' then set an
appropriate API-style response (e.g., ctx.status = 404; ctx.body = { error: 'Not
Found' }; return) so API GETs are not swallowed by the SPA fallback;
alternatively, adjust the HTTPMethod path to explicitly exclude '/api' paths if
your router supports that.
In `@mini-muse/app/controller/MuseAgentController.ts`:
- Around line 13-27: In createStore(), validate required OSS configuration env
vars (at minimum OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET and OSS_BUCKET;
optionally OSS_REGION/OSS_ENDPOINT) before constructing
OSSObject/OSSObjectStorageClient/OSSAgentStore: check each required variable is
non-empty and if any are missing, throw or return a descriptive error (e.g.,
"missing OSS_ACCESS_KEY_ID") to fail fast rather than building a client with
empty credentials; perform this validation at the top of the createStore()
method and only construct OSSObject, OSSObjectStorageClient, and OSSAgentStore
when all validations pass.
- Around line 33-37: The execRun implementation assumes input.input.messages[0]
exists and that content parts have a text field; validate input in execRun
before using userMessage: check that input?.input?.messages is an array with at
least one element, that the first message has a content property which is either
a string or an array, and if it's an array ensure every element has a text
string; if any of these checks fail, throw or return a controlled bad-request
error (e.g., HTTP 400 / a specific error) rather than letting
userMessage/description access throw. Ensure you update references to
userMessage and description to use the validated values.
In `@mini-muse/app/controller/ProjectController.ts`:
- Around line 23-24: The threadId interpolation allows path traversal; add a
shared helper resolveProjectDir(threadId) that does: base =
path.resolve('output'), projectDir = path.resolve(base, threadId), rel =
path.relative(base, projectDir) and reject when rel === '' ||
path.isAbsolute(rel) || rel.startsWith('..'); use this helper wherever project
paths are built (the files method and the other similar blocks referenced around
lines 43-46, 62-64, 83-86) and apply the same path.relative check to any derived
filePath before reading/zipping to ensure filePath remains inside the resolved
projectDir.
In `@mini-muse/app/service/orchestrator.ts`:
- Around line 52-62: The loop currently only checks signal?.aborted before
awaiting heavy operations, so cancellation arriving during createMessage or tool
execution is ignored; modify the loop to propagate the AbortSignal into
in-flight work by passing the signal into
this.aiClientService.createMessage(...) (and any tool execution calls referenced
in this file), and after each await check signal?.aborted and immediately return
or throw to stop further processing and side-effects; update usages of
createMessage and tool execution helpers so they accept an AbortSignal parameter
and respect it during their async work (e.g., cancel network requests and stop
file mutations).
In `@mini-muse/app/service/preview.ts`:
- Around line 73-95: The code currently spawns untrusted project processes
directly (spawn('npm', ['install'], ...) and spawn('npx', ['vite', ...]) in
start()), which is an RCE risk and leaves stale entries in the previews map on
failure; update start() to run all untrusted project commands inside an isolated
sandbox (e.g., Docker/nsjail/other container runtime) instead of host spawn, or
at minimum run npm install with scripts disabled (npm ci / --ignore-scripts)
while you implement containerization, make the registry configurable via a
passed-in option instead of hardcoding npm_config_registry, add a timeout (e.g.,
5 minutes) to the npm install promise and to starting the vite server, and
ensure any failure path/catch in start() cleans up the previews map and resets
info.status/info.port to avoid stale state (reference symbols: start(), spawn
calls, npm install block, npx vite child, outputDir, findFreePort, previews map,
info.status, info.port).
In `@mini-muse/frontend/src/pages/ProgressPage.tsx`:
- Around line 24-46: Race when cleanup runs before startRun's promise resolves:
create an AbortController before calling startRun, assign cancelRef.current =
controller.abort.bind(controller) immediately, and pass controller.signal into
startRun (update startRun signature to accept an optional signal and use it
inside the SSE/fetch logic); this ensures the cleanup callback can synchronously
abort the in-flight run even if startRun's returned cancel function hasn't
resolved yet.
In `@mini-muse/frontend/src/pages/ResultPage.tsx`:
- Around line 113-118: The anchor uses a non-null assertion on threadId
(threadId!) which can cause a runtime error if threadId is undefined; update the
ResultPage JSX so the "Download ZIP" anchor is only rendered when threadId is
defined (e.g., wrap the <a ...> that calls getDownloadUrl(threadId) in a
conditional check like if (threadId) or render a disabled fallback), or
alternatively compute a safe href (fallback '#' or disable) and avoid using the
non-null assertion; locate the getDownloadUrl usage and the Download ZIP anchor
in ResultPage.tsx and remove the threadId! assertion.
In `@mini-muse/frontend/src/services/api.ts`:
- Around line 55-59: The code currently may call onDone() twice (once after the
read loop and once when handleSSEEvent processes a 'done' event); to fix, create
a single-use wrapper or boolean guard (e.g., doneCalled = false or a once(fn)
wrapper) and use that wrapped callback everywhere you currently call onDone,
including in handleSSEEvent and after the stream loop (instead of calling onDone
directly); ensure the guard is checked/set before invoking so the 'done' event
and the stream-close path cannot both trigger the real onDone implementation.
- Line 25: The code currently uses a non-null assertion on response.body when
creating the reader (const reader = response.body!.getReader()), which can throw
if body is null; update the logic to guard against a missing body by checking
response.body before calling getReader(), and handle the null case (e.g., throw
a descriptive error or return an empty/closed stream) so caller code won't
encounter a runtime exception when response.body is absent.
---
Duplicate comments:
In `@mini-muse/app/service/aiClient.ts`:
- Around line 74-80: The getClient method currently allows constructing an
Anthropic client with both apiKey and authToken undefined; update getClient to
fail fast by checking process.env.ANTHROPIC_API_KEY and
process.env.ANTHROPIC_AUTH_TOKEN (and/or existing this.client) before
instantiating Anthropic and throw a clear Error if neither credential is
provided, so the code in getClient (which sets this.client = new Anthropic(...))
never creates a client with missing credentials and surfaces configuration
problems immediately.
In `@mini-muse/app/service/preview.ts`:
- Around line 73-83: Replace the hardcoded registry and unbounded install
Promise in the npm install block: read registry and installTimeout from config
(e.g., preview config) and set env npm_config_registry to that value when
spawning child in the Promise inside the function that calls spawn; start a
timer (setTimeout) using installTimeout that, when fired, kills the child
process (child.kill()) and rejects the Promise with a timeout Error; when the
child emits 'close' or 'error' clear the timer and resolve/reject appropriately;
ensure the rejection includes the exit code or timeout reason so callers of this
Promise can handle failures.
- Around line 44-46: When installAndStart(threadId, outputDir, info) rejects or
the preview process later exits abnormally, update and persist the preview info
so getStatus() can surface failures: in the .catch handler for installAndStart
set info.status = 'failed' and info.error (e.g., err.message or err.stack)
before persisting/committing the info, and likewise in the process close/exit
handler detect non-zero/abnormal exits and set info.status = 'failed' and
info.error to the exit reason/details and persist; ensure you reference the same
info object used by getStatus() so the status change is visible.
---
Nitpick comments:
In `@mini-muse/docs/design.md`:
- Around line 220-235: Several fenced code blocks in design.md (the directory
listings and the SSE event example) lack language specifiers; edit the three
blocks that show the frontend/mini-muse directory trees and the SSE event format
(the blocks around the directory structure at the top and bottom and the SSE
event example in the middle) and add appropriate language identifiers such as
"text" or "plaintext" for the tree blocks and "text" or "http" for the SSE event
block so markdownlint warnings are resolved.
In `@mini-muse/frontend/src/pages/ResultPage.tsx`:
- Around line 81-84: The preview URL currently hardcodes "http://" which causes
mixed-content failures when the app is served over HTTPS; change the
construction of previewUrl (the code using preview.port and
window.location.hostname) to use the current page protocol by prefixing with
window.location.protocol + '//' so the iframe uses the same protocol as the host
page (i.e., build the URL from window.location.protocol,
window.location.hostname and preview.port), keeping the empty-string fallback
when preview.port is falsy.
In `@mini-muse/frontend/src/services/api.ts`:
- Around line 175-184: The getChatHistory function currently uses unsafe any
types and assumes msg.createdAt is seconds; replace the any usages by defining
proper types/interfaces for the thread and message objects (e.g., Thread {
messages: Message[] } and Message with role, content array items, and
createdAt), update getChatHistory to use those types when calling getThread,
validate msg.createdAt (check typeof and value range) before multiplying by 1000
and only convert when the backend returns seconds (avoid double-conversion for
milliseconds), and replace the content mapping to safely handle undefined/null
content items (use explicit checks on msg.content and content[i].text.value)
instead of any so TypeScript can catch errors; also verify with the backend
whether createdAt is in seconds or milliseconds and change the conversion
accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8f2ea815-abc8-48bb-8c1e-f3ac5178365b
📒 Files selected for processing (23)
mini-muse/.env.examplemini-muse/.gitignoremini-muse/app/controller/HomeController.tsmini-muse/app/controller/MuseAgentController.tsmini-muse/app/controller/ProjectController.tsmini-muse/app/service/aiClient.tsmini-muse/app/service/orchestrator.tsmini-muse/app/service/package.jsonmini-muse/app/service/preview.tsmini-muse/app/service/tools.tsmini-muse/config/config.default.tsmini-muse/config/config.local.tsmini-muse/config/plugin.tsmini-muse/docs/design.mdmini-muse/frontend/src/App.tsxmini-muse/frontend/src/components/ChatPanel.tsxmini-muse/frontend/src/pages/HomePage.tsxmini-muse/frontend/src/pages/ProgressPage.tsxmini-muse/frontend/src/pages/ResultPage.tsxmini-muse/frontend/src/services/api.tsmini-muse/frontend/src/types/index.tsmini-muse/package.jsonmini-muse/tsconfig.json
✅ Files skipped from review due to trivial changes (1)
- mini-muse/.gitignore
🚧 Files skipped from review as they are similar to previous changes (9)
- mini-muse/package.json
- mini-muse/config/config.local.ts
- mini-muse/config/config.default.ts
- mini-muse/config/plugin.ts
- mini-muse/frontend/src/types/index.ts
- mini-muse/app/service/tools.ts
- mini-muse/tsconfig.json
- mini-muse/frontend/src/components/ChatPanel.tsx
- mini-muse/frontend/src/App.tsx
| @HTTPController({ path: '/' }) | ||
| export class SpaFallbackController { | ||
| @HTTPMethod({ method: HTTPMethodEnum.GET, path: '/*' }) | ||
| async index(@HTTPContext() ctx: any) { | ||
| const indexPath = path.join(process.cwd(), 'app/public/index.html'); | ||
| try { | ||
| const html = await fs.readFile(indexPath, 'utf-8'); | ||
| ctx.type = 'text/html'; | ||
| return html; | ||
| } catch { | ||
| return 'Mini-Muse is running. Please build the frontend first.'; |
There was a problem hiding this comment.
Keep the SPA fallback off /api/*.
This catch-all GET route also matches API URLs, so unknown GET /api/... requests will return HTML instead of a JSON 404, and route registration order can also shadow real GET controllers. Guard /api before serving index.html, or narrow the fallback to non-API paths only.
Possible fix
`@HTTPController`({ path: '/' })
export class SpaFallbackController {
`@HTTPMethod`({ method: HTTPMethodEnum.GET, path: '/*' })
async index(`@HTTPContext`() ctx: any) {
+ if (ctx.path === '/api' || ctx.path.startsWith('/api/')) {
+ ctx.throw(404, 'Not Found');
+ }
+
const indexPath = path.join(process.cwd(), 'app/public/index.html');
try {
const html = await fs.readFile(indexPath, 'utf-8');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/controller/HomeController.ts` around lines 5 - 15, The SPA
fallback currently matches all GET routes (HTTPMethod path '/*') and lives in
SpaFallbackController.index; add a guard at the start of the index method to
skip serving index.html for API routes: check ctx.request.path (or ctx.path) and
if it startsWith '/api' then set an appropriate API-style response (e.g.,
ctx.status = 404; ctx.body = { error: 'Not Found' }; return) so API GETs are not
swallowed by the SPA fallback; alternatively, adjust the HTTPMethod path to
explicitly exclude '/api' paths if your router supports that.
| async createStore(): Promise<AgentStore> { | ||
| const endpoint = process.env.OSS_ENDPOINT || `https://${process.env.OSS_REGION || 'oss-cn-hangzhou'}.aliyuncs.com`; | ||
|
|
||
| const ossClient = new OSSObject({ | ||
| endpoint, | ||
| accessKeyId: process.env.OSS_ACCESS_KEY_ID || '', | ||
| accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '', | ||
| bucket: process.env.OSS_BUCKET || '', | ||
| }); | ||
|
|
||
| const storageClient = new OSSObjectStorageClient(ossClient); | ||
| const store = new OSSAgentStore({ | ||
| client: storageClient, | ||
| prefix: process.env.OSS_PREFIX || 'mini-muse/', | ||
| }); |
There was a problem hiding this comment.
Validate OSS config before constructing the store.
This can build the OSS client with empty credentials or bucket names, so misconfiguration only shows up later on the first persistence call. Fail fast here with a clear error if the required OSS env vars are missing.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/controller/MuseAgentController.ts` around lines 13 - 27, In
createStore(), validate required OSS configuration env vars (at minimum
OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET and OSS_BUCKET; optionally
OSS_REGION/OSS_ENDPOINT) before constructing
OSSObject/OSSObjectStorageClient/OSSAgentStore: check each required variable is
non-empty and if any are missing, throw or return a descriptive error (e.g.,
"missing OSS_ACCESS_KEY_ID") to fail fast rather than building a client with
empty credentials; perform this validation at the top of the createStore()
method and only construct OSSObject, OSSObjectStorageClient, and OSSAgentStore
when all validations pass.
| async *execRun(input: CreateRunInput, signal?: AbortSignal): AsyncGenerator<AgentStreamMessage> { | ||
| const userMessage = input.input.messages[0]; | ||
| const description = typeof userMessage.content === 'string' | ||
| ? userMessage.content | ||
| : userMessage.content.map((p: { type: string; text: string }) => p.text).join(''); |
There was a problem hiding this comment.
Reject empty or non-text run inputs up front.
Line 34 assumes at least one message exists and that every content part has a text field. Malformed requests will throw here instead of returning a controlled bad-request error.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/controller/MuseAgentController.ts` around lines 33 - 37, The
execRun implementation assumes input.input.messages[0] exists and that content
parts have a text field; validate input in execRun before using userMessage:
check that input?.input?.messages is an array with at least one element, that
the first message has a content property which is either a string or an array,
and if it's an array ensure every element has a text string; if any of these
checks fail, throw or return a controlled bad-request error (e.g., HTTP 400 / a
specific error) rather than letting userMessage/description access throw. Ensure
you update references to userMessage and description to use the validated
values.
| async files(@HTTPParam({ name: 'threadId' }) threadId: string) { | ||
| const outputDir = `./output/${threadId}`; |
There was a problem hiding this comment.
Harden threadId and filePath path resolution.
threadId is interpolated directly into ./output/${threadId}, so values like .. escape the output root, and the startsWith() check on filePath is bypassable by sibling-prefix paths such as ../app2/... when the project dir is app. That exposes arbitrary file reads/zips and lets the preview endpoint run commands outside the intended project directory.
Use a shared resolveProjectDir() helper that resolves against path.resolve('output') and rejects any path.relative() that is empty, absolute, or starts with .., then apply the same path.relative() check to filePath.
Also applies to: 43-46, 62-64, 83-86
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/controller/ProjectController.ts` around lines 23 - 24, The
threadId interpolation allows path traversal; add a shared helper
resolveProjectDir(threadId) that does: base = path.resolve('output'), projectDir
= path.resolve(base, threadId), rel = path.relative(base, projectDir) and reject
when rel === '' || path.isAbsolute(rel) || rel.startsWith('..'); use this helper
wherever project paths are built (the files method and the other similar blocks
referenced around lines 43-46, 62-64, 83-86) and apply the same path.relative
check to any derived filePath before reading/zipping to ensure filePath remains
inside the resolved projectDir.
| await new Promise<void>((resolve, reject) => { | ||
| const child = spawn('npm', ['install'], { | ||
| cwd: outputDir, | ||
| stdio: 'pipe', | ||
| env: { ...process.env, npm_config_registry: 'https://registry.npmmirror.com' }, | ||
| }); | ||
| child.on('close', (code) => { | ||
| if (code === 0) resolve(); | ||
| else reject(new Error(`npm install failed with code ${code}`)); | ||
| }); | ||
| child.on('error', reject); | ||
| }); | ||
|
|
||
| // Step 2: Find a free port and start vite dev server | ||
| info.status = 'starting'; | ||
| const port = await findFreePort(); | ||
| info.port = port; | ||
|
|
||
| const child = spawn('npx', ['vite', '--port', String(port), '--host', '0.0.0.0'], { | ||
| cwd: outputDir, | ||
| stdio: 'pipe', | ||
| env: { ...process.env }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Preview execution path:"
sed -n '71,95p' mini-muse/app/service/preview.ts
echo
echo "Generated runtime files / config writers:"
rg -n -C2 "package\\.json|vite\\.config|create_config|create_file|writeFile\\(" mini-muse
echo
echo "Sandboxing or script suppression hooks:"
rg -n -C2 "ignore-scripts|docker|container|sandbox|nsjail|firejail|chroot|vm2" mini-museRepository: eggjs/agent-examples
Length of output: 15290
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Preview service process handlers and error handling ==="
sed -n '50,150p' mini-muse/app/service/preview.ts
echo
echo "=== PreviewInfo state management and module-level storage ==="
rg -n -B3 -A3 "PreviewInfo|previewMap|previewState" mini-muse/app/service/preview.ts | head -50Repository: eggjs/agent-examples
Length of output: 3933
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Catch block in start() method and error handling ==="
sed -n '32,50p' mini-muse/app/service/preview.ts
echo
echo "=== Full start() method including catch ==="
sed -n '30,60p' mini-muse/app/service/preview.tsRepository: eggjs/agent-examples
Length of output: 1573
Don't execute generated projects on the host—this is a critical RCE vulnerability.
npm install and npx vite are spawned directly inside AI-generated project directories. The AI can inject arbitrary code into package.json (postinstall scripts), vite.config.ts (plugins), or index.html that executes on the server with full process privileges. No sandboxing, file validation, or script suppression (e.g., --ignore-scripts) is present. This must be isolated using containers or a sandbox (Docker, nsjail, vm2, etc.) before release.
Additionally, the catch block in start() only logs errors without cleaning up the previews map, so failed installations leave stale state that blocks retries. Implement proper state cleanup on startup failure.
Finally, use a timeout for npm install (e.g., 5 minutes) and make the registry configurable instead of hardcoded.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/service/preview.ts` around lines 73 - 95, The code currently
spawns untrusted project processes directly (spawn('npm', ['install'], ...) and
spawn('npx', ['vite', ...]) in start()), which is an RCE risk and leaves stale
entries in the previews map on failure; update start() to run all untrusted
project commands inside an isolated sandbox (e.g., Docker/nsjail/other container
runtime) instead of host spawn, or at minimum run npm install with scripts
disabled (npm ci / --ignore-scripts) while you implement containerization, make
the registry configurable via a passed-in option instead of hardcoding
npm_config_registry, add a timeout (e.g., 5 minutes) to the npm install promise
and to starting the vite server, and ensure any failure path/catch in start()
cleans up the previews map and resets info.status/info.port to avoid stale state
(reference symbols: start(), spawn calls, npm install block, npx vite child,
outputDir, findFreePort, previews map, info.status, info.port).
| startRun( | ||
| description, | ||
| appName || 'my-app', | ||
| (event) => { | ||
| setEvents(prev => [...prev, event]); | ||
| if (event.type === 'error') setFailed(true); | ||
| }, | ||
| (threadId) => { | ||
| threadIdRef.current = threadId; | ||
| }, | ||
| () => setDone(true) | ||
| ).then(cancel => { | ||
| cancelRef.current = cancel; | ||
| }).catch(err => { | ||
| setEvents(prev => [...prev, { type: 'error', message: err.message, timestamp: Date.now() }]); | ||
| setFailed(true); | ||
| setDone(true); | ||
| }); | ||
|
|
||
| return () => { | ||
| cancelRef.current?.(); | ||
| }; | ||
| }, [description, appName, navigate]); |
There was a problem hiding this comment.
Potential race condition: cleanup may run before cancelRef is assigned.
The startRun function is async and returns a cancel function via .then(). If the component unmounts before the promise resolves, the cleanup function at line 44 will find cancelRef.current still null, leaving the SSE stream uncancelled.
🛡️ Proposed fix using AbortController directly
+ const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
if (!description) {
navigate('/');
return;
}
+ const controller = new AbortController();
+ abortRef.current = controller;
+
startRun(
description,
appName || 'my-app',
(event) => {
setEvents(prev => [...prev, event]);
if (event.type === 'error') setFailed(true);
},
(threadId) => {
threadIdRef.current = threadId;
},
- () => setDone(true)
- ).then(cancel => {
- cancelRef.current = cancel;
- }).catch(err => {
+ () => setDone(true),
+ undefined,
+ controller.signal // Pass signal to startRun
+ ).catch(err => {
+ if (controller.signal.aborted) return; // Ignore if aborted
setEvents(prev => [...prev, { type: 'error', message: err.message, timestamp: Date.now() }]);
setFailed(true);
setDone(true);
});
return () => {
- cancelRef.current?.();
+ controller.abort();
};
}, [description, appName, navigate]);This would require modifying startRun to accept an optional signal parameter, but provides more reliable cleanup.
Alternatively, you could store the AbortController reference immediately before calling startRun, since startRun internally creates one and returns the abort function.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/pages/ProgressPage.tsx` around lines 24 - 46, Race
when cleanup runs before startRun's promise resolves: create an AbortController
before calling startRun, assign cancelRef.current =
controller.abort.bind(controller) immediately, and pass controller.signal into
startRun (update startRun signature to accept an optional signal and use it
inside the SSE/fetch logic); this ensures the cleanup callback can synchronously
abort the in-flight run even if startRun's returned cancel function hasn't
resolved yet.
| <a | ||
| href={getDownloadUrl(threadId!)} | ||
| className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors inline-flex items-center gap-2" | ||
| > | ||
| Download ZIP | ||
| </a> |
There was a problem hiding this comment.
Non-null assertion on threadId may cause runtime error.
At line 114, threadId! assumes threadId is always defined when the download link is rendered. However, threadId comes from useParams and could be undefined. While the parent conditional chatOpen && threadId on line 230 guards the ChatPanel, the download link is always rendered.
🛡️ Proposed fix to add a guard or fallback
<a
- href={getDownloadUrl(threadId!)}
+ href={threadId ? getDownloadUrl(threadId) : '#'}
className="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors inline-flex items-center gap-2"
+ onClick={(e) => { if (!threadId) e.preventDefault(); }}
>
Download ZIP
</a>Alternatively, conditionally render the download button only when threadId is defined.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/pages/ResultPage.tsx` around lines 113 - 118, The
anchor uses a non-null assertion on threadId (threadId!) which can cause a
runtime error if threadId is undefined; update the ResultPage JSX so the
"Download ZIP" anchor is only rendered when threadId is defined (e.g., wrap the
<a ...> that calls getDownloadUrl(threadId) in a conditional check like if
(threadId) or render a disabled fallback), or alternatively compute a safe href
(fallback '#' or disable) and avoid using the non-null assertion; locate the
getDownloadUrl usage and the Download ZIP anchor in ResultPage.tsx and remove
the threadId! assertion.
| onThreadId: (threadId: string) => void, | ||
| onDone: () => void | ||
| ): Promise<void> { | ||
| const reader = response.body!.getReader(); |
There was a problem hiding this comment.
Non-null assertion on response.body could cause runtime error.
At line 25, response.body! assumes the body is always present. While this is typically true for successful fetch responses, it's safer to add a guard.
🛡️ Proposed fix
async function parseSSEStream(
response: Response,
onEvent: (event: ProgressEvent) => void,
onThreadId: (threadId: string) => void,
onDone: () => void
): Promise<void> {
+ if (!response.body) {
+ onDone();
+ return;
+ }
- const reader = response.body!.getReader();
+ const reader = response.body.getReader();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/services/api.ts` at line 25, The code currently uses a
non-null assertion on response.body when creating the reader (const reader =
response.body!.getReader()), which can throw if body is null; update the logic
to guard against a missing body by checking response.body before calling
getReader(), and handle the null case (e.g., throw a descriptive error or return
an empty/closed stream) so caller code won't encounter a runtime exception when
response.body is absent.
| } finally { | ||
| reader.releaseLock(); | ||
| } | ||
| onDone(); | ||
| } |
There was a problem hiding this comment.
Potential double onDone invocation.
onDone() is called both at line 58 (after stream ends) and at line 88 (when 'done' event is received). If the server sends a 'done' event before closing the stream, onDone will be called twice, potentially causing duplicate state updates in consumers.
🛡️ Proposed fix using a flag
async function parseSSEStream(
response: Response,
onEvent: (event: ProgressEvent) => void,
onThreadId: (threadId: string) => void,
onDone: () => void
): Promise<void> {
+ let doneCalled = false;
+ const callDone = () => {
+ if (!doneCalled) {
+ doneCalled = true;
+ onDone();
+ }
+ };
// ... in handleSSEEvent calls, pass callDone instead of onDone
// ... and at line 58, call callDone() instead of onDone()Or pass a wrapped callback to handleSSEEvent and use it at line 58.
Also applies to: 87-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/frontend/src/services/api.ts` around lines 55 - 59, The code
currently may call onDone() twice (once after the read loop and once when
handleSSEEvent processes a 'done' event); to fix, create a single-use wrapper or
boolean guard (e.g., doneCalled = false or a once(fn) wrapper) and use that
wrapped callback everywhere you currently call onDone, including in
handleSSEEvent and after the stream loop (instead of calling onDone directly);
ensure the guard is checked/set before invoking so the 'done' event and the
stream-close path cannot both trigger the real onDone implementation.
…t-sdk - Replace manual agentic loop with Agent SDK's `query()` function - Create `mcpTools.ts` to register 11 custom tools as MCP tools via `tool()` + `createSdkMcpServer()` from the Agent SDK - Remove `AiClientService` (no longer needed, Agent SDK manages API calls) - Simplify `OrchestratorService` to consume `query()` async iterator - Update `ToolsService` to remove dependency on deleted `aiClient.ts` - Update design docs to reflect new architecture Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now see their previous generation sessions on the HomePage and click to navigate directly to the result page. Session records (threadId, appName, description, status) are saved to localStorage when a run starts and updated on completion/failure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Map CLAUDE_MODEL to ANTHROPIC_MODEL for Agent SDK (Claude CLI) - Set CLAUDE_CODE_OFFLINE_MODE and disable telemetry to prevent CLI from making external network calls - Ensure ~/.claude.json has onboarding completed - Create outputDir before query() to fix spawn ENOENT error - Pass model, cwd, and stderr options to query() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (7)
mini-muse/app/service/tools.ts (1)
6-7: Accept ToolName type in execute() parameter to preserve compile-time safety.Line 6 accepts any string but line 7 immediately casts it with
as ToolName. The function signature ofexecuteTool(from registry.ts:133) already expectsToolName, so the cast only suppresses TypeScript warnings for invalid tool names. WhileexecuteTooldoes handle unknown names at runtime (returning an errorToolResult), the public API should validate tool names at the boundary rather than silently accepting any string and failing later.Change
name: stringtoname: ToolNameon line 6 to align the type contract with the called function and prevent typos in callers.♻️ Proposed fix
export class ToolsService { - async execute(name: string, input: Record<string, unknown>, outputDir: string): Promise<ToolResult> { - return executeTool(name as ToolName, input, outputDir); + async execute(name: ToolName, input: Record<string, unknown>, outputDir: string): Promise<ToolResult> { + return executeTool(name, input, outputDir); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/tools.ts` around lines 6 - 7, Change the execute() function signature to accept a ToolName instead of string to preserve compile-time safety: update the parameter type in execute (in mini-muse/app/service/tools.ts) from name: string to name: ToolName so you can call executeTool(name, input, outputDir) without casting; this aligns the public API with executeTool (registry.ts:133) and prevents callers from passing invalid tool names.mini-muse/frontend/src/pages/HomePage.tsx (2)
99-105: Addaria-labelfor better screen reader support.The delete button relies on
titlefor accessibility context, but screen readers may not announce title attributes consistently. Adding an explicitaria-labelimproves accessibility.♿ Suggested improvement
<button onClick={(e) => { e.stopPropagation(); handleDelete(s.threadId); }} className="text-gray-400 hover:text-red-500 transition-colors text-sm" title="Delete" + aria-label={`Delete ${s.appName} session`} > × </button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/pages/HomePage.tsx` around lines 99 - 105, The delete button in HomePage.tsx currently uses title="Delete" but lacks an explicit accessibility label; update the button element that calls handleDelete(s.threadId) to include an aria-label (e.g., aria-label="Delete thread") so screen readers announce its purpose reliably while keeping the existing title and onClick behavior unchanged.
24-29:setLoading(true)has no visible effect before navigation.The
setLoading(true)on line 27 triggers a state update, butnavigate()is called synchronously on line 28. The component will unmount before React can re-render with the loading state, so the "Creating..." button text will never display.If the intent is purely to disable double-submission during the brief navigation, the current approach works. If you want visible feedback, consider adding a small delay or removing the loading state entirely since the navigation is immediate.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/pages/HomePage.tsx` around lines 24 - 29, In handleSubmit, setLoading(true) is being set immediately before synchronous navigate so the component unmounts before the render shows the "Creating..." state; either remove the setLoading call and rely on immediate navigation to prevent double submits, or if you want visible feedback, after calling setLoading(true) await a short timer (e.g. await new Promise(r => setTimeout(r, 50))) before calling navigate('/progress', { state: { description: description.trim(), appName: appName.trim() || 'my-app' } }); so the Loading state (managed by setLoading) can render; reference handleSubmit, setLoading, and navigate when making the change.mini-muse/frontend/src/pages/ProgressPage.tsx (1)
75-80: Consider handling the edge case wheredoneis true without a terminal event.If the SSE stream ends unexpectedly (e.g., network interruption) without emitting a
completedorerrorevent, the component will show no status indicator since:
- Line 75:
!done && !failed→ false- Line 76:
isCompleted→ false- Line 77:
failed→ falseThis is unlikely in normal operation but could leave users without visual feedback in edge cases.
💡 Defensive fallback indicator
{!done && !failed && <div className="w-4 h-4 rounded-full bg-indigo-500 animate-pulse" />} {isCompleted && <div className="w-4 h-4 rounded-full bg-green-500" />} {failed && <div className="w-4 h-4 rounded-full bg-red-500" />} + {done && !isCompleted && !failed && <div className="w-4 h-4 rounded-full bg-gray-400" />} <span className="font-medium text-gray-700"> - {failed ? 'Generation Failed' : isCompleted ? 'Generation Complete' : 'Generating...'} + {failed ? 'Generation Failed' : isCompleted ? 'Generation Complete' : done ? 'Finished' : 'Generating...'} </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/pages/ProgressPage.tsx` around lines 75 - 80, The component currently shows no indicator when done is true but neither isCompleted nor failed is set; add a defensive fallback for that edge case by rendering a neutral/notice indicator when (done && !isCompleted && !failed) and update the status text logic to handle that case (e.g., show "Generation Interrupted" or similar). Modify the JSX around the existing indicators (the lines referencing done, isCompleted, failed in ProgressPage.tsx) to include a new conditional branch for (done && !isCompleted && !failed) that renders a distinct colored dot (e.g., yellow) and adjusts the span text to cover this fallback state.mini-muse/frontend/src/services/sessionHistory.ts (1)
32-44: Consider adding a session limit to prevent unbounded localStorage growth.While localStorage has its own size limits, sessions will accumulate indefinitely over time. Consider pruning old sessions to maintain a reasonable history size.
💡 Optional: Limit stored sessions
+const MAX_SESSIONS = 50; + export function saveSession(record: SessionRecord): void { const sessions = getSessions(); const idx = sessions.findIndex(s => s.threadId === record.threadId); if (idx >= 0) { sessions[idx] = record; } else { sessions.unshift(record); } + // Keep only the most recent sessions + const trimmed = sessions.slice(0, MAX_SESSIONS); - localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions)); + localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed)); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/frontend/src/services/sessionHistory.ts` around lines 32 - 44, Add a max-session cap to prevent unbounded localStorage growth by introducing a constant (e.g. MAX_SESSIONS = 100) and a small helper to prune sessions to that size (pruneSessions(sessions): return most-recent MAX_SESSIONS). Call the pruner before persisting to localStorage in any place that writes STORAGE_KEY (e.g. updateSessionStatus and other save paths that call localStorage.setItem), using getSessions() to load and relying on a timestamp/ordering field if available (or array order) to keep the newest entries. Ensure you update only the in-memory sessions array and then persist the pruned array via localStorage.setItem(STORAGE_KEY, JSON.stringify(prunedSessions)).mini-muse/app/service/orchestrator.ts (2)
22-47: Global state mutation on eachagentLoopinvocation.
ensureClaudeEnv()is called on everyagentLoop()call, repeatedly mutatingprocess.envand checking/writing~/.claude.json. This is idempotent but wasteful. Consider running this once at service initialization or module load.Additionally, the
accessSync+writeFileSyncpattern is not atomic—concurrentagentLoopinvocations could race here. The synchronous I/O also blocks the event loop.♻️ Proposed refactor: run once at module load with async I/O
+import { access, writeFile } from 'fs/promises'; + +let claudeEnvInitialized = false; + /** Ensure Claude CLI env vars and config are ready for Agent SDK */ -function ensureClaudeEnv() { +async function ensureClaudeEnv() { + if (claudeEnvInitialized) return; + claudeEnvInitialized = true; + // Agent SDK (Claude CLI) reads ANTHROPIC_MODEL, not CLAUDE_MODEL if (process.env.CLAUDE_MODEL && !process.env.ANTHROPIC_MODEL) { process.env.ANTHROPIC_MODEL = process.env.CLAUDE_MODEL; } // ... env vars unchanged ... const claudeConfigPath = path.join(os.homedir(), '.claude.json'); try { - fs.accessSync(claudeConfigPath); - } catch { - fs.writeFileSync(claudeConfigPath, JSON.stringify({ + await access(claudeConfigPath); + } catch { + await writeFile(claudeConfigPath, JSON.stringify({ // ... - }, null, 2)); + }, null, 2), { flag: 'wx' }); // 'wx' fails if file exists (atomic) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/orchestrator.ts` around lines 22 - 47, ensureClaudeEnv() is mutating process.env and doing synchronous file I/O every time agentLoop() runs; move this to run once at module initialization (or service startup) and remove calls from agentLoop. Change ensureClaudeEnv to an async initializer (e.g., ensureClaudeEnvAsync) that only sets env vars when they are unset (check process.env.ANTHROPIC_MODEL before assigning) and uses fs.promises to create the claudeConfigPath file atomically (fs.promises.writeFile with flag 'wx' or open with 'wx' and handle EEXIST) instead of accessSync/writeFileSync to avoid races and blocking the event loop; call this initializer once from module load or the top-level service init instead of inside agentLoop().
62-65: Silent error suppression may hide legitimate failures.The empty
catchblock at line 65 assumes any error means "directory might not exist," but it could mask permission errors, disk issues, or other problems. Consider logging at debug level or narrowing the catch to specific error codes.🛡️ Proposed fix to handle specific errors
try { const fileList = await listFiles(outputDir); relativeFiles = fileList.map(f => path.relative(outputDir, f)); - } catch { /* directory might not exist */ } + } catch (err) { + // ENOENT is expected for new projects; log other errors + if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { + console.warn('[orchestrator] listFiles failed:', err); + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/app/service/orchestrator.ts` around lines 62 - 65, The empty catch around the listFiles call hides real failures; update the try/catch where you await listFiles(outputDir) (which sets relativeFiles by mapping path.relative) to only suppress "directory not found" cases and otherwise log the error at debug or error level—either check the error.code (e.g., ENOENT) before swallowing or rethrow; include context (outputDir and the function name/orchestrator) in the log so permission or disk errors are visible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mini-muse/app/lib/tools/mcpTools.ts`:
- Around line 107-115: The create_file tool (museTool 'create_file' in
mcpTools.ts) and other file-writing tools (e.g., createComponent, createPage,
createHook handlers that accept filePath or directory) lack path traversal
protection; update each tool to resolve the user-supplied path against the
server output base (use path.resolve(outputDir, userInput)) and reject any path
where the resolved fullPath does not start with path.resolve(outputDir); on
rejection return a failure result (e.g., { success: false, error: 'Access
denied: path traversal detected' }) and do this validation before
writing/creating files so ../ escapes cannot reach outside the intended
directory.
- Around line 1-4: The import of AnyZodRawShape from
'@anthropic-ai/claude-agent-sdk' is invalid; remove that import and either omit
the explicit type constraint (let TypeScript infer it) or import ZodRawShape
from 'zod' instead and use that where needed; update usage sites related to tool
or createSdkMcpServer so they no longer reference AnyZodRawShape (or switch to
z.ZodRawShape) while keeping the rest of imports (tool, createSdkMcpServer, z,
executeTool, ToolName) unchanged.
In `@mini-muse/app/service/orchestrator.ts`:
- Around line 87-88: The configuration currently sets permissionMode:
'bypassPermissions' and allowDangerouslySkipPermissions: true which grants
unrestricted FS access; change this by removing or disabling these flags and
enforce sandboxing and path validation in the orchestrator and tools: restrict
allowed operations to the configured outputDir, validate and canonicalize target
paths (reject paths that escape outputDir via .. or symlinks), and ensure any
tool methods (e.g., file read/write methods in the orchestrator or tool
implementations) check that resolved paths are inside outputDir before
performing IO; if bypass is required for tests, gate it behind an explicit
opt-in env flag and add logging/auditing when enabled.
In `@mini-muse/docs/design.md`:
- Around line 248-260: The example SSE code block in docs/design.md lacks a
fenced-code language specifier; update the block that shows the events (the
lines beginning with "event: thread.run.created", "event: thread.message.delta",
"event: thread.run.completed", "event: done") to start with a fenced code tag
including a language identifier (e.g., ```text or a custom tag) and close with
the matching ``` so markdown linters recognize it and readability is improved.
- Around line 211-227: The fenced code block in mini-muse/docs/design.md
containing the frontend directory tree lacks a language specifier; update that
fenced block (the triple-backtick that encloses the directory listing) to
include a language such as text or plaintext so Markdown renderers treat it as
plain text (e.g., change ``` to ```text) while keeping the existing directory
content (frontend/src/, pages/, components/, services/, types/, etc.) unchanged.
- Around line 357-388: The fenced code block showing the project directory tree
(the block beginning with "mini-muse/" and the ASCII tree under it) needs a
language specifier; update the opening triple backticks to ```text so the
directory structure block is ```text ... ``` for consistent syntax highlighting
in the docs/design.md file.
---
Nitpick comments:
In `@mini-muse/app/service/orchestrator.ts`:
- Around line 22-47: ensureClaudeEnv() is mutating process.env and doing
synchronous file I/O every time agentLoop() runs; move this to run once at
module initialization (or service startup) and remove calls from agentLoop.
Change ensureClaudeEnv to an async initializer (e.g., ensureClaudeEnvAsync) that
only sets env vars when they are unset (check process.env.ANTHROPIC_MODEL before
assigning) and uses fs.promises to create the claudeConfigPath file atomically
(fs.promises.writeFile with flag 'wx' or open with 'wx' and handle EEXIST)
instead of accessSync/writeFileSync to avoid races and blocking the event loop;
call this initializer once from module load or the top-level service init
instead of inside agentLoop().
- Around line 62-65: The empty catch around the listFiles call hides real
failures; update the try/catch where you await listFiles(outputDir) (which sets
relativeFiles by mapping path.relative) to only suppress "directory not found"
cases and otherwise log the error at debug or error level—either check the
error.code (e.g., ENOENT) before swallowing or rethrow; include context
(outputDir and the function name/orchestrator) in the log so permission or disk
errors are visible.
In `@mini-muse/app/service/tools.ts`:
- Around line 6-7: Change the execute() function signature to accept a ToolName
instead of string to preserve compile-time safety: update the parameter type in
execute (in mini-muse/app/service/tools.ts) from name: string to name: ToolName
so you can call executeTool(name, input, outputDir) without casting; this aligns
the public API with executeTool (registry.ts:133) and prevents callers from
passing invalid tool names.
In `@mini-muse/frontend/src/pages/HomePage.tsx`:
- Around line 99-105: The delete button in HomePage.tsx currently uses
title="Delete" but lacks an explicit accessibility label; update the button
element that calls handleDelete(s.threadId) to include an aria-label (e.g.,
aria-label="Delete thread") so screen readers announce its purpose reliably
while keeping the existing title and onClick behavior unchanged.
- Around line 24-29: In handleSubmit, setLoading(true) is being set immediately
before synchronous navigate so the component unmounts before the render shows
the "Creating..." state; either remove the setLoading call and rely on immediate
navigation to prevent double submits, or if you want visible feedback, after
calling setLoading(true) await a short timer (e.g. await new Promise(r =>
setTimeout(r, 50))) before calling navigate('/progress', { state: { description:
description.trim(), appName: appName.trim() || 'my-app' } }); so the Loading
state (managed by setLoading) can render; reference handleSubmit, setLoading,
and navigate when making the change.
In `@mini-muse/frontend/src/pages/ProgressPage.tsx`:
- Around line 75-80: The component currently shows no indicator when done is
true but neither isCompleted nor failed is set; add a defensive fallback for
that edge case by rendering a neutral/notice indicator when (done &&
!isCompleted && !failed) and update the status text logic to handle that case
(e.g., show "Generation Interrupted" or similar). Modify the JSX around the
existing indicators (the lines referencing done, isCompleted, failed in
ProgressPage.tsx) to include a new conditional branch for (done && !isCompleted
&& !failed) that renders a distinct colored dot (e.g., yellow) and adjusts the
span text to cover this fallback state.
In `@mini-muse/frontend/src/services/sessionHistory.ts`:
- Around line 32-44: Add a max-session cap to prevent unbounded localStorage
growth by introducing a constant (e.g. MAX_SESSIONS = 100) and a small helper to
prune sessions to that size (pruneSessions(sessions): return most-recent
MAX_SESSIONS). Call the pruner before persisting to localStorage in any place
that writes STORAGE_KEY (e.g. updateSessionStatus and other save paths that call
localStorage.setItem), using getSessions() to load and relying on a
timestamp/ordering field if available (or array order) to keep the newest
entries. Ensure you update only the in-memory sessions array and then persist
the pruned array via localStorage.setItem(STORAGE_KEY,
JSON.stringify(prunedSessions)).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 04f8cdd2-a8d8-436b-abd6-f5b9d34e479a
📒 Files selected for processing (8)
mini-muse/app/lib/tools/mcpTools.tsmini-muse/app/service/orchestrator.tsmini-muse/app/service/tools.tsmini-muse/docs/design.mdmini-muse/frontend/src/pages/HomePage.tsxmini-muse/frontend/src/pages/ProgressPage.tsxmini-muse/frontend/src/services/sessionHistory.tsmini-muse/package.json
🚧 Files skipped from review as they are similar to previous changes (1)
- mini-muse/package.json
mini-muse/app/lib/tools/mcpTools.ts
Outdated
| import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk'; | ||
| import type { AnyZodRawShape } from '@anthropic-ai/claude-agent-sdk'; | ||
| import { z } from 'zod'; | ||
| import { executeTool, ToolName } from './registry'; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
@anthropic-ai/claude-agent-sdk npm package API documentation tool createSdkMcpServer
💡 Result:
createSdkMcpServer is documented in Anthropic’s Claude Agent SDK (TypeScript) reference and used in the Custom Tools guide. [1], [2]
Signature (TypeScript)
function createSdkMcpServer(options: {
name: string;
version?: string;
tools?: Array<SdkMcpToolDefinition<any>>;
}): McpSdkServerConfigWithInstance;It “creates an MCP server instance that runs in the same process as your application.” [1]
Typical usage (custom tools)
- Define tools with
tool(...), then create an in-process MCP server:
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
const customServer = createSdkMcpServer({
name: "my-custom-tools",
version: "1.0.0",
tools: [
tool("get_weather", "Get current temperature", { latitude: z.number(), longitude: z.number() }, async (args) => {
return { content: [{ type: "text", text: `...` }] };
}),
],
});[2]
- Pass the server to
query()viaoptions.mcpServers(as an object/dictionary). Custom MCP tools require streaming input mode (i.e.,promptmust be anAsyncIterable, not a plain string). [2]
Tool naming when exposed to Claude
MCP tool names follow: mcp__{server_name}__{tool_name} (e.g., mcp__my-custom-tools__get_weather). [2]
Sources: Anthropic Claude Agent SDK docs—TypeScript reference [1] and Custom Tools guide [2].
🏁 Script executed:
# First, let's check the full content of the mcpTools.ts file, focusing on where AnyZodRawShape is used
head -50 mini-muse/app/lib/tools/mcpTools.tsRepository: eggjs/agent-examples
Length of output: 2328
🌐 Web query:
@anthropic-ai/claude-agent-sdk AnyZodRawShape type export TypeScript
💡 Result:
In the Claude Agent SDK TypeScript docs, tool() is typed as:
function tool<Schema extends AnyZodRawShape>(...)(docs.claude.com)
However, AnyZodRawShape is not documented in the “Types” section there, so if you’re trying to reference that constraint in your own TypeScript types, the practical options are:
- Use Zod’s raw-shape type (most common)
import { z, type ZodRawShape } from "zod";
type MyShape = ZodRawShape;- Or define a Zod-3/4-compatible alias yourself
import { z } from "zod";
// Works as a “raw shape” constraint for both Zod 3/4-style shapes
export type AnyZodRawShape = Record<string, z.ZodTypeAny>;In many cases you don’t need to name the type at all—tool() will infer Schema from the inputSchema you pass. (docs.claude.com)
Citations:
- 1: https://docs.claude.com/en/docs/agent-sdk/typescript
- 2: https://docs.claude.com/en/docs/agent-sdk/typescript
AnyZodRawShape is not a public export from @anthropic-ai/claude-agent-sdk.
The SDK's tool() function uses this type internally, but it does not expose AnyZodRawShape in its public API. Remove the import on line 2 or use ZodRawShape from zod instead. Most commonly, the type constraint can be omitted entirely and inferred automatically by TypeScript.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/tools/mcpTools.ts` around lines 1 - 4, The import of
AnyZodRawShape from '@anthropic-ai/claude-agent-sdk' is invalid; remove that
import and either omit the explicit type constraint (let TypeScript infer it) or
import ZodRawShape from 'zod' instead and use that where needed; update usage
sites related to tool or createSdkMcpServer so they no longer reference
AnyZodRawShape (or switch to z.ZodRawShape) while keeping the rest of imports
(tool, createSdkMcpServer, z, executeTool, ToolName) unchanged.
mini-muse/app/lib/tools/mcpTools.ts
Outdated
| const createFile = museTool( | ||
| 'create_file', | ||
| 'Create a generic file with specified content. Use for type definitions, utilities, constants, or any file that does not fit other specialized tools.', | ||
| { | ||
| filePath: z.string().describe('Path relative to src/ directory (e.g., "types/index.ts", "utils/helpers.ts")'), | ||
| content: z.string().describe('The complete file content'), | ||
| fileType: z.enum(['typescript', 'css', 'json']).optional().describe('File type for formatting'), | ||
| }, | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check which tool implementations have path traversal protection
echo "=== Checking for path traversal validation in tool handlers ==="
# Search for the secure pattern (path.resolve + startsWith check)
echo -e "\n--- Tools WITH path traversal protection ---"
rg -l "startsWith.*resolve" mini-muse/app/lib/tools/
echo -e "\n--- Tools using path.join (potentially unsafe) ---"
rg -n "path\.join\(outputDir" mini-muse/app/lib/tools/generator/
echo -e "\n--- All file-writing operations ---"
rg -n "writeFile|fs\.write" mini-muse/app/lib/tools/Repository: eggjs/agent-examples
Length of output: 3511
🏁 Script executed:
cat -n mini-muse/app/lib/tools/generator/deleteFile.tsRepository: eggjs/agent-examples
Length of output: 1621
🏁 Script executed:
cat -n mini-muse/app/lib/tools/generator/createFile.tsRepository: eggjs/agent-examples
Length of output: 2529
🏁 Script executed:
cat -n mini-muse/app/lib/tools/reader/readFile.tsRepository: eggjs/agent-examples
Length of output: 1703
🏁 Script executed:
cat -n mini-muse/app/lib/tools/generator/createComponent.ts | head -90Repository: eggjs/agent-examples
Length of output: 3367
🏁 Script executed:
cat -n mini-muse/app/lib/tools/generator/createPage.ts | head -80Repository: eggjs/agent-examples
Length of output: 3040
🏁 Script executed:
cat -n mini-muse/app/lib/tools/generator/createHook.ts | head -80Repository: eggjs/agent-examples
Length of output: 2628
Path traversal vulnerability in file-creation tools.
The filePath and directory parameters in file-writing tools accept user-controlled paths without consistent validation. Comparison with deleteFile.ts reveals the gap:
deleteFile.tsandreadFile.tscorrectly validate usingpath.resolve()+startsWith()check (preventing../escapes)createFile.tsusespath.join()without traversal validation; checking onlystartsWith('src/')allows attacks likefilePath='src/../../../etc/passwd'to escapeoutputDircreateComponent.tsaccepts unvalidateddirectoryparameter with the same riskcreatePage.tsandcreateHook.tshave marginal protection viatoPascalCase/toCamelCasenormalization, but lack explicit path validation
Apply the secure validation pattern from deleteFile.ts to all file-writing tools:
const fullPath = path.resolve(outputDir, userInput);
if (!fullPath.startsWith(path.resolve(outputDir))) {
return { success: false, error: 'Access denied: path traversal detected' };
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/tools/mcpTools.ts` around lines 107 - 115, The create_file
tool (museTool 'create_file' in mcpTools.ts) and other file-writing tools (e.g.,
createComponent, createPage, createHook handlers that accept filePath or
directory) lack path traversal protection; update each tool to resolve the
user-supplied path against the server output base (use path.resolve(outputDir,
userInput)) and reject any path where the resolved fullPath does not start with
path.resolve(outputDir); on rejection return a failure result (e.g., { success:
false, error: 'Access denied: path traversal detected' }) and do this validation
before writing/creating files so ../ escapes cannot reach outside the intended
directory.
| permissionMode: 'bypassPermissions', | ||
| allowDangerouslySkipPermissions: true, |
There was a problem hiding this comment.
Security: bypassPermissions + allowDangerouslySkipPermissions grants the agent unrestricted file system access.
These options disable all permission checks, allowing the agent to read/write any files the process can access. Ensure this is intentional and that outputDir is properly sandboxed. Consider adding path validation in tool implementations to restrict operations to outputDir.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/service/orchestrator.ts` around lines 87 - 88, The
configuration currently sets permissionMode: 'bypassPermissions' and
allowDangerouslySkipPermissions: true which grants unrestricted FS access;
change this by removing or disabling these flags and enforce sandboxing and path
validation in the orchestrator and tools: restrict allowed operations to the
configured outputDir, validate and canonicalize target paths (reject paths that
escape outputDir via .. or symlinks), and ensure any tool methods (e.g., file
read/write methods in the orchestrator or tool implementations) check that
resolved paths are inside outputDir before performing IO; if bypass is required
for tests, gate it behind an explicit opt-in env flag and add logging/auditing
when enabled.
| ``` | ||
| frontend/src/ | ||
| ├── pages/ | ||
| │ ├── HomePage.tsx # 输入表单 + 历史会话列表 (localStorage) | ||
| │ ├── ProgressPage.tsx # SSE 实时进度展示 + 会话记录持久化 | ||
| │ └── ResultPage.tsx # Code/Preview 双标签页 + 可折叠聊天面板 | ||
| ├── components/ | ||
| │ ├── FileTree.tsx # 递归文件树 | ||
| │ ├── CodePreview.tsx # 带行号的代码展示 | ||
| │ ├── ProgressLog.tsx # 进度日志滚动列表 | ||
| │ └── ChatPanel.tsx # 迭代修改聊天面板 | ||
| ├── services/ | ||
| │ ├── api.ts # API 封装 (fetch + ReadableStream SSE 解析) | ||
| │ └── sessionHistory.ts # 历史会话管理 (localStorage CRUD) | ||
| └── types/ | ||
| └── index.ts # 共享类型 (RunObject, ThreadObject, ProgressEvent 等) | ||
| ``` |
There was a problem hiding this comment.
Add language specifier to fenced code block.
Static analysis flagged this code block as missing a language specifier. For directory structures, use text or plaintext for consistent rendering across Markdown processors.
📝 Proposed fix
-```
+```text
frontend/src/
├── pages/
...📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| frontend/src/ | |
| ├── pages/ | |
| │ ├── HomePage.tsx # 输入表单 + 历史会话列表 (localStorage) | |
| │ ├── ProgressPage.tsx # SSE 实时进度展示 + 会话记录持久化 | |
| │ └── ResultPage.tsx # Code/Preview 双标签页 + 可折叠聊天面板 | |
| ├── components/ | |
| │ ├── FileTree.tsx # 递归文件树 | |
| │ ├── CodePreview.tsx # 带行号的代码展示 | |
| │ ├── ProgressLog.tsx # 进度日志滚动列表 | |
| │ └── ChatPanel.tsx # 迭代修改聊天面板 | |
| ├── services/ | |
| │ ├── api.ts # API 封装 (fetch + ReadableStream SSE 解析) | |
| │ └── sessionHistory.ts # 历史会话管理 (localStorage CRUD) | |
| └── types/ | |
| └── index.ts # 共享类型 (RunObject, ThreadObject, ProgressEvent 等) | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 211-211: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/docs/design.md` around lines 211 - 227, The fenced code block in
mini-muse/docs/design.md containing the frontend directory tree lacks a language
specifier; update that fenced block (the triple-backtick that encloses the
directory listing) to include a language such as text or plaintext so Markdown
renderers treat it as plain text (e.g., change ``` to ```text) while keeping the
existing directory content (frontend/src/, pages/, components/, services/,
types/, etc.) unchanged.
| ``` | ||
| event: thread.run.created | ||
| data: {"id":"run_xxx","threadId":"thread_xxx","status":"queued",...} | ||
|
|
||
| event: thread.message.delta | ||
| data: {"id":"msg_xxx","delta":{"content":[{"type":"text","text":{"value":"[status] Processing...","annotations":[]}}]}} | ||
|
|
||
| event: thread.run.completed | ||
| data: {"id":"run_xxx","status":"completed",...} | ||
|
|
||
| event: done | ||
| data: "[DONE]" | ||
| ``` |
There was a problem hiding this comment.
Add language specifier to SSE example code block.
Use text or a custom identifier to satisfy markdown linting and improve readability.
📝 Proposed fix
-```
+```text
event: thread.run.created
data: {"id":"run_xxx","threadId":"thread_xxx","status":"queued",...}
...🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 248-248: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/docs/design.md` around lines 248 - 260, The example SSE code block
in docs/design.md lacks a fenced-code language specifier; update the block that
shows the events (the lines beginning with "event: thread.run.created", "event:
thread.message.delta", "event: thread.run.completed", "event: done") to start
with a fenced code tag including a language identifier (e.g., ```text or a
custom tag) and close with the matching ``` so markdown linters recognize it and
readability is improved.
| ``` | ||
| mini-muse/ | ||
| ├── app/ | ||
| │ ├── controller/ # tegg 装饰器控制器 | ||
| │ │ ├── MuseAgentController.ts # @AgentController - 标准 Agent API | ||
| │ │ ├── ProjectController.ts # @HTTPController - 文件/预览 API | ||
| │ │ └── HomeController.ts # @HTTPController - SPA fallback | ||
| │ ├── service/ # tegg @SingletonProto 服务 | ||
| │ │ ├── orchestrator.ts # Agent 主循环 (claude-agent-sdk query) | ||
| │ │ ├── tools.ts # 工具调用入口 | ||
| │ │ └── preview.ts # 预览服务器管理 | ||
| │ ├── middleware/ | ||
| │ │ └── errorHandler.ts # API 错误统一处理 | ||
| │ ├── lib/ # 纯业务逻辑 (框架无关) | ||
| │ │ ├── prompts.ts | ||
| │ │ ├── utils.ts | ||
| │ │ └── tools/ # 11 个 AI 工具 | ||
| │ ├── public/ # 前端构建产物 | ||
| │ └── module.json # tegg 模块声明 | ||
| ├── config/ # Egg.js + tegg 配置 | ||
| │ ├── config.default.ts | ||
| │ ├── config.local.ts | ||
| │ └── plugin.ts | ||
| ├── frontend/ # React SPA 源码 | ||
| │ └── src/ | ||
| ├── docs/ | ||
| │ └── design.md # 本文档 | ||
| ├── .env # 环境变量 (不提交) | ||
| ├── .env.example # 环境变量模板 | ||
| ├── CLAUDE.md # Claude Code 项目规范 | ||
| ├── package.json | ||
| └── tsconfig.json |
There was a problem hiding this comment.
Add language specifier to directory structure code block.
Same issue as above—add text language specifier for consistency.
📝 Proposed fix
-```
+```text
mini-muse/
├── app/
...📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| mini-muse/ | |
| ├── app/ | |
| │ ├── controller/ # tegg 装饰器控制器 | |
| │ │ ├── MuseAgentController.ts # @AgentController - 标准 Agent API | |
| │ │ ├── ProjectController.ts # @HTTPController - 文件/预览 API | |
| │ │ └── HomeController.ts # @HTTPController - SPA fallback | |
| │ ├── service/ # tegg @SingletonProto 服务 | |
| │ │ ├── orchestrator.ts # Agent 主循环 (claude-agent-sdk query) | |
| │ │ ├── tools.ts # 工具调用入口 | |
| │ │ └── preview.ts # 预览服务器管理 | |
| │ ├── middleware/ | |
| │ │ └── errorHandler.ts # API 错误统一处理 | |
| │ ├── lib/ # 纯业务逻辑 (框架无关) | |
| │ │ ├── prompts.ts | |
| │ │ ├── utils.ts | |
| │ │ └── tools/ # 11 个 AI 工具 | |
| │ ├── public/ # 前端构建产物 | |
| │ └── module.json # tegg 模块声明 | |
| ├── config/ # Egg.js + tegg 配置 | |
| │ ├── config.default.ts | |
| │ ├── config.local.ts | |
| │ └── plugin.ts | |
| ├── frontend/ # React SPA 源码 | |
| │ └── src/ | |
| ├── docs/ | |
| │ └── design.md # 本文档 | |
| ├── .env # 环境变量 (不提交) | |
| ├── .env.example # 环境变量模板 | |
| ├── CLAUDE.md # Claude Code 项目规范 | |
| ├── package.json | |
| └── tsconfig.json |
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 357-357: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/docs/design.md` around lines 357 - 388, The fenced code block
showing the project directory tree (the block beginning with "mini-muse/" and
the ASCII tree under it) needs a language specifier; update the opening triple
backticks to ```text so the directory structure block is ```text ... ``` for
consistent syntax highlighting in the docs/design.md file.
…th DI Move all 11 tool definitions from mcpTools.ts into ToolsService as class properties using tool() from Agent SDK. Add getMcpServers()/getAllowedTools() methods following wohuguiagent pattern. OrchestratorService now uses @Inject for ToolsService and @LifecyclePostInject for env init. Delete redundant mcpTools.ts and clean up registry.ts (remove unused schemas/definitions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix singleton concurrency issue: outputDir was stored as mutable state on the singleton ToolsService, causing concurrent requests to overwrite each other's output directory. Now getMcpServers(outputDir) accepts the directory as a parameter and creates tool instances bound to it. - Remove dead exports: xxxSchema and xxxInput from all 11 tool files are no longer imported after the Zod migration, saving ~430 lines. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
mini-muse/docs/design.md (3)
259-271:⚠️ Potential issue | 🟡 MinorAdd language specifier to SSE example code block.
Use
textor a custom identifier to satisfy markdown linting and improve readability.📝 Proposed fix
-``` +```text event: thread.run.created data: {"id":"run_xxx","threadId":"thread_xxx","status":"queued",...} ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/docs/design.md` around lines 259 - 271, The markdown SSE example block lacks a fenced-code language specifier; update the triple-backtick fence that wraps the events (containing lines like "event: thread.run.created", "event: thread.message.delta", "event: thread.run.completed", and "event: done") to include a language tag such as "text" (e.g., change ``` to ```text) so the code block satisfies markdown linting and improves readability.
368-400:⚠️ Potential issue | 🟡 MinorAdd language specifier to directory structure code block.
Same issue as above—add
textlanguage specifier for consistency.📝 Proposed fix
-``` +```text mini-muse/ ├── app/ ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/docs/design.md` around lines 368 - 400, The fenced code block showing the project directory tree in design.md is missing a language specifier; update the triple-backtick opening fence that precedes the mini-muse/ tree to use ```text so the block is consistently highlighted (e.g., change ``` to ```text for the directory structure block in docs/design.md).
222-238:⚠️ Potential issue | 🟡 MinorAdd language specifier to fenced code block.
Static analysis flagged this code block as missing a language specifier. For directory structures, use
textorplaintextfor consistent rendering across Markdown processors.📝 Proposed fix
-``` +```text frontend/src/ ├── pages/ ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@mini-muse/docs/design.md` around lines 222 - 238, Update the fenced code block in mini-muse/docs/design.md that shows the frontend/src/ directory tree to include a language specifier (e.g., change the opening "```" to "```text" or "```plaintext") so Markdown renderers treat the directory structure correctly; locate the block containing "frontend/src/" and the list of pages/components/services/types and replace the fence accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@mini-muse/app/lib/tools/registry.ts`:
- Around line 50-63: The dispatcher executeTool currently calls the selected
executor directly and lets exceptions propagate; wrap the executor invocation in
a try/catch inside executeTool (referencing executeTool and toolExecutors) so
any thrown error is caught and you return a ToolResult with success: false and
an error message (include the caught error message/string). Preserve the
existing handling for unknown tools, and ensure the catch returns a ToolResult
rather than rethrowing so callers always receive a ToolResult.
In `@mini-muse/app/service/tools.ts`:
- Around line 17-21: The tool wrapper currently calls executeTool with
this.outputDir even when outputDir is empty, causing filesystem tools to operate
in the process CWD; update the async handler returned by tool (the anonymous
async (args) => { ... } block) to validate this.outputDir (or this?.outputDir)
up front and throw a clear error (e.g., "outputDir not initialized") if falsy
before calling executeTool(name, args, this.outputDir) so execution fails fast
and prevents accidental reads/writes outside the intended workspace.
- Around line 8-18: ToolsService is a singleton but currently stores per-run
state in the private outputDir and setOutputDir, which causes concurrent runs to
clobber each other; make ToolsService stateless by removing the outputDir field
and setOutputDir, and change createTool (and wherever you construct tool(...)
handlers) so the handler captures the per-run directory via a parameter/closure
or reads it from the invocation args instead of this.outputDir — specifically
update createTool, the tool(...) factory calls, and executeTool usage so
executeTool(name, args, outputDir) receives a run-scoped outputDir supplied when
the tool is registered or invoked (reference: ToolsService, setOutputDir,
createTool, outputDir, tool, executeTool).
---
Duplicate comments:
In `@mini-muse/docs/design.md`:
- Around line 259-271: The markdown SSE example block lacks a fenced-code
language specifier; update the triple-backtick fence that wraps the events
(containing lines like "event: thread.run.created", "event:
thread.message.delta", "event: thread.run.completed", and "event: done") to
include a language tag such as "text" (e.g., change ``` to ```text) so the code
block satisfies markdown linting and improves readability.
- Around line 368-400: The fenced code block showing the project directory tree
in design.md is missing a language specifier; update the triple-backtick opening
fence that precedes the mini-muse/ tree to use ```text so the block is
consistently highlighted (e.g., change ``` to ```text for the directory
structure block in docs/design.md).
- Around line 222-238: Update the fenced code block in mini-muse/docs/design.md
that shows the frontend/src/ directory tree to include a language specifier
(e.g., change the opening "```" to "```text" or "```plaintext") so Markdown
renderers treat the directory structure correctly; locate the block containing
"frontend/src/" and the list of pages/components/services/types and replace the
fence accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3858f6c4-ad66-49b6-8046-a77a3563810a
📒 Files selected for processing (4)
mini-muse/app/lib/tools/registry.tsmini-muse/app/service/orchestrator.tsmini-muse/app/service/tools.tsmini-muse/docs/design.md
🚧 Files skipped from review as they are similar to previous changes (1)
- mini-muse/app/service/orchestrator.ts
| export async function executeTool( | ||
| name: ToolName, | ||
| input: Record<string, unknown>, | ||
| outputDir: string | ||
| ): Promise<ToolResult> { | ||
| const executor = toolExecutors[name]; | ||
| if (!executor) { | ||
| return { | ||
| success: false, | ||
| error: `Unknown tool: ${name}`, | ||
| }; | ||
| } | ||
|
|
||
| return executor(input, outputDir); |
There was a problem hiding this comment.
Catch executor exceptions in the dispatcher.
A few executors do raw formatting/filesystem work and can throw instead of returning ToolResult (for example, mini-muse/app/lib/tools/generator/createFile.ts, Lines 34-78, and mini-muse/app/lib/tools/generator/createConfig.ts, Lines 140-205). Letting that escape here will fail the whole MCP call instead of returning a recoverable tool error.
Proposed fix
export async function executeTool(
name: ToolName,
input: Record<string, unknown>,
outputDir: string
): Promise<ToolResult> {
const executor = toolExecutors[name];
if (!executor) {
return {
success: false,
error: `Unknown tool: ${name}`,
};
}
- return executor(input, outputDir);
+ try {
+ return await executor(input, outputDir);
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : 'Tool execution failed',
+ };
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function executeTool( | |
| name: ToolName, | |
| input: Record<string, unknown>, | |
| outputDir: string | |
| ): Promise<ToolResult> { | |
| const executor = toolExecutors[name]; | |
| if (!executor) { | |
| return { | |
| success: false, | |
| error: `Unknown tool: ${name}`, | |
| }; | |
| } | |
| return executor(input, outputDir); | |
| export async function executeTool( | |
| name: ToolName, | |
| input: Record<string, unknown>, | |
| outputDir: string | |
| ): Promise<ToolResult> { | |
| const executor = toolExecutors[name]; | |
| if (!executor) { | |
| return { | |
| success: false, | |
| error: `Unknown tool: ${name}`, | |
| }; | |
| } | |
| try { | |
| return await executor(input, outputDir); | |
| } catch (error) { | |
| return { | |
| success: false, | |
| error: error instanceof Error ? error.message : 'Tool execution failed', | |
| }; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/lib/tools/registry.ts` around lines 50 - 63, The dispatcher
executeTool currently calls the selected executor directly and lets exceptions
propagate; wrap the executor invocation in a try/catch inside executeTool
(referencing executeTool and toolExecutors) so any thrown error is caught and
you return a ToolResult with success: false and an error message (include the
caught error message/string). Preserve the existing handling for unknown tools,
and ensure the catch returns a ToolResult rather than rethrowing so callers
always receive a ToolResult.
mini-muse/app/service/tools.ts
Outdated
| return tool(name, description, inputSchema, async (args) => { | ||
| const result = await executeTool(name, args as Record<string, unknown>, this.outputDir); | ||
| return { | ||
| content: [{ type: 'text' as const, text: JSON.stringify(result) }], | ||
| }; |
There was a problem hiding this comment.
Fail fast when outputDir has not been initialized.
Right now an empty outputDir falls through to the executors. For the filesystem-backed tools, that means reads/writes/deletes resolve relative to the server's current working directory instead of an isolated generated workspace.
Proposed fix
private createTool(name: ToolName, description: string, inputSchema: Parameters<typeof tool>[2]) {
return tool(name, description, inputSchema, async (args) => {
+ if (!this.outputDir) {
+ return {
+ content: [{
+ type: 'text' as const,
+ text: JSON.stringify({
+ success: false,
+ error: 'outputDir is not initialized',
+ }),
+ }],
+ };
+ }
const result = await executeTool(name, args as Record<string, unknown>, this.outputDir);
return {
content: [{ type: 'text' as const, text: JSON.stringify(result) }],
};
});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@mini-muse/app/service/tools.ts` around lines 17 - 21, The tool wrapper
currently calls executeTool with this.outputDir even when outputDir is empty,
causing filesystem tools to operate in the process CWD; update the async handler
returned by tool (the anonymous async (args) => { ... } block) to validate
this.outputDir (or this?.outputDir) up front and throw a clear error (e.g.,
"outputDir not initialized") if falsy before calling executeTool(name, args,
this.outputDir) so execution fails fast and prevents accidental reads/writes
outside the intended workspace.
Summary
Project Structure
Key Features
Checklist
.env.exampleprovided as templatedocs/design.md🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Chores