Compare commits

..

277 Commits

Author SHA1 Message Date
webdevcody
5d675561ba chore: release v0.8.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 16:31:11 -05:00
Web Dev Cody
27fb3f2777 Merge pull request #364 from AutoMaker-Org/v0.8.0rc
V0.8.0rc
2026-01-05 16:28:25 -05:00
webdevcody
aca84fe16a chore: update Docker configuration and entrypoint script
- Enhanced .dockerignore to exclude additional build outputs and dependencies.
- Modified dev.mjs and start.mjs to change Docker container startup behavior, removing the --build flag to preserve volumes.
- Updated docker-compose.yml to add a new volume for persisting Claude CLI OAuth session keys.
- Introduced docker-entrypoint.sh to fix permissions on the Claude CLI config directory.
- Adjusted Dockerfile to include the entrypoint script and ensure proper user permissions.

These changes improve the Docker setup and streamline the development workflow.
2026-01-05 10:44:47 -05:00
Shirone
abab7be367 Merge pull request #363 from AutoMaker-Org/fix/app-spec-ui-bug
fix: prevent 'No App Specification Found' during spec generation
2026-01-05 16:04:26 +01:00
Kacper
73d0edb873 refactor: move setIsGenerationRunning(false) outside if block
Address PR review feedback - ensure isGenerationRunning is always
reset to false when generation is not running, even if
api.specRegeneration is not available.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 16:01:41 +01:00
Kacper
84d93c2901 fix: prevent "No App Specification Found" during spec generation
Check generation status before trying to load the spec file.
This prevents 500 errors and confusing UI during spec generation.

Changes:
- useSpecLoading now checks specRegeneration.status() first
- If generation is running, skip the file read and set isGenerationRunning
- SpecView uses isGenerationRunning to show generating UI properly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 15:57:17 +01:00
Shirone
d558050dfa Merge pull request #362 from AutoMaker-Org/refactor/adjust-buttons-in-board-header
style: update BoardHeader component for improved layout
2026-01-05 15:29:28 +01:00
Kacper
5991e99853 refactor: extract shared className and add data-testid
Address PR review feedback:
- Extract duplicated className to controlContainerClass constant
- Add data-testid="auto-mode-toggle-container" for testability

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 15:26:19 +01:00
Kacper
9661aa1dad style: update BoardHeader component for improved layout
- Adjusted the spacing and height of the concurrency slider and auto mode toggle containers for better visual consistency.
- Changed class names to enhance the overall design and maintainability of the UI.
2026-01-05 15:22:04 +01:00
Shirone
d4649ec456 Merge pull request #361 from AutoMaker-Org/refactor/improve-vitest-configuration
refactor: use Vitest projects config instead of deprecated workspace
2026-01-05 15:02:12 +01:00
Kacper
fde9eea2d6 style: use explicit config path for server project
Address PR review feedback for consistency - use full path
'apps/server/vitest.config.ts' instead of just 'apps/server'.

Note: libs/types has no tests (type definitions only), so it
doesn't need a vitest.config.ts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 14:59:46 +01:00
Kacper
d1e3251c29 refactor: use glob patterns for vitest projects configuration
Address PR review feedback:
- Use 'libs/*/vitest.config.ts' glob to auto-discover lib projects
- Simplify test:packages script to use --project='!server' exclusion
- New libs with vitest.config.ts will be automatically included

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 14:50:47 +01:00
Kacper
2f51991558 refactor: use Vitest projects config instead of deprecated workspace
- Add root vitest.config.ts with projects array (replaces deprecated workspace)
- Add name property to each project's vitest.config.ts for filtering
- Update package.json test scripts to use vitest projects
- Add vitest to root devDependencies

This addresses the Vitest warning about multiple configs impacting
performance by running all projects in a single Vitest process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 14:45:33 +01:00
Kacper
7963525246 refactor: replace loading messages with LoadingState component
- Updated RootLayoutContent to utilize the LoadingState component for displaying loading messages during various application states, enhancing consistency and maintainability of the UI.
2026-01-05 14:36:10 +01:00
Kacper
feae1d7686 refactor: enhance pre-commit hook for nvm compatibility
- Improved the pre-commit script to better handle loading Node.js versions from .nvmrc for both Unix and Windows environments.
- Added checks to ensure nvm is sourced correctly and to handle potential errors gracefully, enhancing the reliability of the development setup.
2026-01-05 14:36:01 +01:00
Web Dev Cody
bbdfaf6463 Merge pull request #358 from AutoMaker-Org/ideation-fix
refactor: update ideation dashboard and prompt list to use project-sp…
2026-01-04 18:07:53 -05:00
webdevcody
e4d86aa654 refactor: optimize ideation components and store for project-specific job handling
- Updated IdeationDashboard and PromptList components to utilize memoization for improved performance when retrieving generation jobs specific to the current project.
- Removed the getJobsForProject function from the ideation store, streamlining job management by directly filtering jobs in the components.
- Enhanced the addGenerationJob function to ensure consistent job ID generation format.
- Implemented migration logic in the ideation store to clean up legacy jobs without project paths, improving data integrity.
2026-01-04 16:22:25 -05:00
webdevcody
4ac1edf314 refactor: update ideation dashboard and prompt list to use project-specific job retrieval
- Modified IdeationDashboard and PromptList components to fetch generation jobs specific to the current project.
- Updated addGenerationJob function to include projectPath as a parameter for better job management.
- Introduced getJobsForProject function in the ideation store to streamline job filtering by project.
2026-01-04 14:16:39 -05:00
Web Dev Cody
4f3ac27534 Merge pull request #288 from AutoMaker-Org/feat/cursor-cli
feat: Cursor CLI Integration
2026-01-04 13:31:29 -05:00
Kacper
4a41dbb665 style: fix formatting in auto-mode-service.ts 2026-01-04 13:28:37 +01:00
Kacper
f90cd61048 fix: remove MCP permission settings references removed in v0.8.0rc
v0.8.0rc removed getMCPPermissionSettings and related properties.
Removed all references from auto-mode-service.ts to fix build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:18:35 +01:00
Kacper
078f107f66 Merge v0.8.0rc into feat/cursor-cli
Resolved conflicts:
- sdk-options.ts: kept HEAD (MCP & thinking level features)
- auto-mode-service.ts: kept HEAD (MCP features + fallback code)
- agent-output-modal.tsx: used v0.8.0rc (effectiveViewMode + pr-8 spacing)
- feature-suggestions-dialog.tsx: accepted deletion
- electron.ts: used v0.8.0rc (Ideation types)
- package-lock.json: regenerated

Fixed sdk-options.test.ts to expect 'default' permissionMode for read-only operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-04 13:12:45 +01:00
Web Dev Cody
64642916ab Merge pull request #355 from AutoMaker-Org/summaries-and-todos
Summaries and todos
2026-01-04 01:59:49 -05:00
webdevcody
e2206d7a96 feat: add thorough verification process and enhance agent output modal
- Introduced a new markdown file outlining a mandatory 3-pass verification process for code completion, focusing on correctness, edge cases, and maintainability.
- Updated the AgentInfoPanel to display a todo list for non-backlog features, ensuring users can see the agent's current tasks.
- Enhanced the AgentOutputModal to support a summary view, extracting and displaying summary content from raw log output.
- Improved the log parser to extract summaries from various formats, enhancing the overall user experience and information accessibility.
2026-01-04 01:56:45 -05:00
Web Dev Cody
32f859b927 Merge pull request #354 from AutoMaker-Org/ideation
feat: implement ideation feature for brainstorming and idea management
2026-01-04 01:21:44 -05:00
webdevcody
ac92725a6c feat: enhance ideation routes with event handling and new suggestion feature
- Updated the ideation routes to include an EventEmitter for better event management.
- Added a new endpoint to handle adding suggestions to the board, ensuring consistent category mapping.
- Modified existing routes to emit events for idea creation, update, and deletion, improving frontend notifications.
- Refactored the convert and create idea handlers to utilize the new event system.
- Removed static guided prompts data in favor of dynamic fetching from the backend API.
2026-01-04 00:38:01 -05:00
webdevcody
5c95d6d58e fix: update category mapping and improve ID generation format in IdeationService
- Changed the category mapping for 'feature' from 'feature' to 'ui'.
- Updated ID generation format to use hyphens instead of underscores for better readability.
- Enhanced unit tests to reflect the updated category and ensure proper functionality.
2026-01-04 00:22:06 -05:00
claude[bot]
abddfad063 test: add comprehensive unit tests for IdeationService
- Add 28 unit tests covering all major IdeationService functionality
- Test session management (start, get, stop, running state)
- Test idea CRUD operations (create, read, update, delete, archive)
- Test idea to feature conversion with user stories and notes
- Test project analysis and caching
- Test prompt management and filtering
- Test AI-powered suggestion generation
- Mock all external dependencies (fs, platform, utils, providers)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Web Dev Cody <webdevcody@users.noreply.github.com>
2026-01-04 05:16:06 +00:00
webdevcody
3512749e3c feat: refactor development and production launch scripts
- Introduced `dev.mjs` for development mode with hot reloading using Vite.
- Added `start.mjs` for production mode, serving pre-built static files without hot reloading.
- Created a new utility module `launcher-utils.mjs` for shared functions across scripts.
- Updated package.json scripts to reflect new launch commands.
- Removed deprecated `init.mjs` and associated MCP permission settings from the codebase.
- Added `.dockerignore` and updated `.gitignore` for better environment management.
- Enhanced README with updated usage instructions for starting the application.
2026-01-04 00:06:25 -05:00
webdevcody
2c70835769 Merge branch 'main' into ideation 2026-01-03 23:58:56 -05:00
Web Dev Cody
b1f7139bb6 Merge pull request #353 from AutoMaker-Org/fix-memory-leak
Fix memory leak
2026-01-03 23:57:11 -05:00
webdevcody
22aa24ae04 feat: add Docker container launch option and update process handling
- Introduced a new option to launch the application in a Docker container (Isolated Mode) from the main menu.
- Added checks for the ANTHROPIC_API_KEY environment variable to ensure proper API functionality.
- Updated process management to include Docker, allowing for better cleanup and handling of spawned processes.
- Enhanced user prompts and logging for improved clarity during the launch process.
2026-01-03 23:53:44 -05:00
webdevcody
586aabe11f chore: update .gitignore and improve cleanup handling in scripts
- Added .claude/hans/ to .gitignore to prevent tracking of specific directory.
- Updated cleanup calls in dev.mjs and start.mjs to use await for proper asynchronous handling.
- Enhanced error handling during cleanup in case of failures.
- Improved server failure handling in startServerAndWait function to ensure proper termination of failed processes.
2026-01-03 23:36:22 -05:00
webdevcody
afb0937cb3 refactor: update permissionMode to bypassPermissions in SDK options and tests
- Changed permissionMode from 'default' to 'bypassPermissions' in sdk-options and claude-provider unit tests.
- Added allowDangerouslySkipPermissions flag in claude-provider test to enhance permission handling.
2026-01-03 23:26:26 -05:00
webdevcody
d677910f40 refactor: update permission handling and optimize performance measurement
- Changed permissionMode settings in enhance and generate title routes to improve edit acceptance and default behavior.
- Refactored performance measurement cleanup in the App component to only execute in development mode, preventing unnecessary operations in production.
- Simplified the startServerAndWait function signature for better readability.
2026-01-03 23:23:43 -05:00
webdevcody
6d41c7d0bc docs: update README for authentication setup and production launch
- Revised instructions for starting Automaker, changing from `npm run dev` to `npm run start` for production mode.
- Added a setup wizard for authentication on first run, with options for using Claude Code CLI or entering an API key.
- Clarified development mode instructions, emphasizing the use of `npm run dev` for live reload and hot module replacement.
2026-01-03 23:13:53 -05:00
webdevcody
9552670d3d feat: introduce development mode launch script
- Added a new script (dev.mjs) to start the application in development mode with hot reloading using Vite.
- The script includes functionality for installing Playwright browsers, resolving port configurations, and launching either a web or desktop application.
- Removed the old init.mjs script, which was previously responsible for launching the application.
- Updated package.json to reference the new dev.mjs script for the development command.
- Introduced a shared utilities module (launcher-utils.mjs) for common functionalities used in both development and production scripts.
2026-01-03 23:11:18 -05:00
webdevcody
e32a82cca5 refactor: remove MCP permission settings and streamline SDK options for autonomous mode
- Removed MCP permission settings from the application, including related functions and UI components.
- Updated SDK options to always bypass permissions and allow unrestricted tool access in autonomous mode.
- Adjusted related components and services to reflect the removal of MCP permission configurations, ensuring a cleaner and more efficient codebase.
2026-01-03 23:00:20 -05:00
webdevcody
019d6dd7bd fix memory leak 2026-01-03 22:50:42 -05:00
Shirone
c6d94d4bf4 fix: improve abort handling in spawnJSONLProcess
- Added immediate invocation of abort handler if the abort signal is already triggered, ensuring proper cleanup.
- Updated test to use setImmediate for aborting, allowing the generator to start processing before the abort is called, enhancing reliability.
2026-01-04 03:50:17 +01:00
Shirone
ef06c13c1a feat: implement timeout for plan approval and enhance error handling
- Added a 30-minute timeout for user plan approval to prevent indefinite waiting and memory leaks.
- Wrapped resolve/reject functions in the waitForPlanApproval method to ensure timeout is cleared upon resolution.
- Enhanced error handling in the stream processing loop to ensure proper cleanup and logging of errors.
- Improved the handling of task execution and phase completion events for better tracking and user feedback.
2026-01-04 03:45:21 +01:00
Kacper
3ed3a90bf6 refactor: rename phase models to model defaults and reorganize components
- Updated imports and references from 'phase-models' to 'model-defaults' across various components.
- Removed obsolete phase models index file to streamline the codebase.
2026-01-03 23:05:34 +01:00
webdevcody
ff281e23d0 feat: implement ideation feature for brainstorming and idea management
- Introduced a new IdeationService to manage brainstorming sessions, including idea creation, analysis, and conversion to features.
- Added RESTful API routes for ideation, including session management, idea CRUD operations, and suggestion generation.
- Created UI components for the ideation dashboard, prompt selection, and category grid to enhance user experience.
- Integrated keyboard shortcuts and navigation for the ideation feature, improving accessibility and workflow.
- Updated state management with Zustand to handle ideation-specific data and actions.
- Added necessary types and paths for ideation functionality, ensuring type safety and clarity in the codebase.
2026-01-03 02:58:43 -05:00
Web Dev Cody
f34fd955ac Merge pull request #342 from yumesha/main
fixed background image not showing at desktop application (electron)
2026-01-03 02:05:24 -05:00
antdev
46cb6fa425 fixed 'Buffer' is not defined. 2026-01-03 13:52:57 +08:00
antdev
818d8af998 E2E Test Fix - Ready for Manual Application 2026-01-03 13:47:23 +08:00
Shirone
88aba360e3 fix: improve Cursor CLI implementation with type safety and security fixes
- Add getCliPath() public method to CursorProvider to avoid private field access
- Add path validation to cursor-config routes to prevent traversal attacks
- Add supportsVision field to CursorModelConfig (all false - CLI limitation)
- Consolidate duplicate types in providers/types.ts (re-export from @automaker/types)
- Add MCP servers warning log instead of error (not yet supported by Cursor CLI)
- Fix debug log type safety (replace 'as any' with proper type narrowing)
- Update docs to remove non-existent tier field, add supportsVision field
- Remove outdated TODO comment in sdk-options.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 03:35:33 +01:00
Shirone
ec6d36bda5 chore: format 2026-01-03 03:08:46 +01:00
Shirone
816bf8f6f6 Merge branch 'main' into feat/cursor-cli 2026-01-03 03:05:19 +01:00
Shirone
a6d665c4fa fix: update logger tests to use console.log instead of console.warn
- Modified logger test cases to reflect that the warn method uses console.log in Node.js implementation.
- Updated expectations in feature-loader tests to align with the new logging behavior.
- Ensured consistent logging behavior across tests for improved clarity and accuracy.
2026-01-03 03:03:50 +01:00
Shirone
d13a16111c feat: enhance suggestion generation with model and thinking level overrides
- Updated the generateSuggestions function to accept model and thinking level overrides, allowing for more flexible suggestion generation.
- Modified the API client and UI components to support passing these new parameters, improving user control over the suggestion process.
- Introduced a new phase model for AI Suggestions in settings, enhancing the overall functionality and user experience.
2026-01-03 02:56:08 +01:00
antdev
8d5e7b068c fail format check fixed 2026-01-03 09:55:54 +08:00
Shirone
6d4f28575f fix: scrolling issues in phase model selector
- scrolling was broken when we used component inside modal / dialog
2026-01-03 02:55:34 +01:00
Shirone
7596ff9ec3 fix: enable logger colors by default in Node.js subprocess environments
Previously, colors were only enabled when stdout was a TTY, which caused
colored output to be stripped when the server ran as a subprocess. Now
colors are enabled by default in Node.js and can be disabled with
LOG_COLORS=false if needed.

Also removed the unused isTTY() function.
2026-01-03 02:54:14 +01:00
Shirone
35441c1a9d feat: add AI Suggestions phase model to settings view
- Introduced a new phase model for AI Suggestions, enhancing the functionality of the settings view.
- Updated the phase model handling to utilize DEFAULT_PHASE_MODELS as a fallback, ensuring robust behavior when specific models are not set.
- This addition improves the user experience by providing more options for project analysis and suggestions.
2026-01-03 02:41:39 +01:00
Shirone
abed3b3d75 feat: enhance use-model-override to support default phase models
- Updated the useModelOverride hook to include a fallback to DEFAULT_PHASE_MODELS when phase models are not available, ensuring smoother handling of model overrides.
- This change addresses scenarios where settings may not have been migrated to include new phase models, improving overall robustness and user experience.
2026-01-03 02:40:17 +01:00
Kacper
9071f89ec8 chore: remove obsolete Cursor CLI integration documentation
- Deleted the Cursor CLI integration analysis document, phase prompt, README, and related phase files as they are no longer relevant to the current project structure.
- This cleanup helps streamline the project and remove outdated references, ensuring a more maintainable codebase.
2026-01-02 21:02:40 +01:00
Kacper
3c8ee5b714 chore: update .prettierignore and format vite.config.mts
- Added routeTree.gen.ts to .prettierignore to prevent formatting of generated files.
- Reformatted the external dependencies list in vite.config.mts for improved readability.
2026-01-02 20:56:40 +01:00
Kacper
8e1a9addc1 fix: update logger test expectations to include log level prefixes
- Modified test cases in logger.test.ts to assert that log messages include appropriate log level prefixes (INFO, ERROR, WARN, DEBUG).
- Ensured consistency in log output formatting across various logging methods, enhancing clarity in test results.
2026-01-02 20:54:39 +01:00
Kacper
e72f7d1e1a fix: libs test 2026-01-02 20:50:57 +01:00
Kacper
4a28b70b72 feat: enhance query options with maxThinkingTokens support
- Introduced maxThinkingTokens to the query options for Claude models, allowing for more precise control over the SDK's reasoning capabilities.
- Refactored the enhance handler to utilize the new getThinkingTokenBudget function, improving the integration of thinking levels into the query process.
- Updated the query options structure for clarity and maintainability, ensuring consistent handling of model parameters.

This update enhances the application's ability to adapt reasoning capabilities based on user-defined thinking levels, improving overall performance.
2026-01-02 20:50:40 +01:00
Kacper
3e95a11189 refactor: replace logger with console statements in subprocess management
- Removed the centralized logging system in favor of direct console.log and console.error statements for subprocess management.
- Updated logging messages to include context for subprocess actions, such as spawning commands, handling errors, and process completion.
- This change simplifies the logging approach in subprocess handling, making it easier to track subprocess activities during development.
2026-01-02 20:46:39 +01:00
Shirone
2b942a6cb1 feat: integrate thinking level support across various components
- Enhanced multiple server and UI components to include an optional thinking level parameter, improving the configurability of model interactions.
- Updated request handlers and services to manage and pass the thinking level, ensuring consistent data handling across the application.
- Refactored UI components to display and manage the selected model along with its thinking level, enhancing user experience and clarity.
- Adjusted the Electron API and HTTP client to support the new thinking level parameter in requests, ensuring seamless integration.

This update significantly improves the application's ability to adapt reasoning capabilities based on user-defined thinking levels, enhancing overall performance and user satisfaction.
2026-01-02 17:52:12 +01:00
Shirone
69f3ba9724 feat: standardize logging across UI components
- Replaced console.log and console.error statements with logger methods from @automaker/utils in various UI components, ensuring consistent log formatting and improved readability.
- Enhanced error handling by utilizing logger methods to provide clearer context for issues encountered during operations.
- Updated multiple views and hooks to integrate the new logging system, improving maintainability and debugging capabilities.

This update significantly enhances the observability of UI components, facilitating easier troubleshooting and monitoring.
2026-01-02 17:33:15 +01:00
Shirone
96a999817f feat: implement structured logging across server components
- Integrated a centralized logging system using createLogger from @automaker/utils, replacing console.log and console.error statements with logger methods for consistent log formatting and improved readability.
- Updated various modules, including auth, events, and services, to utilize the new logging system, enhancing error tracking and operational visibility.
- Refactored logging messages to provide clearer context and information, ensuring better maintainability and debugging capabilities.

This update significantly enhances the observability of the server components, facilitating easier troubleshooting and monitoring.
2026-01-02 15:40:15 +01:00
Shirone
8c04e0028f feat: integrate thinking level support across agent and UI components
- Enhanced the agent service and request handling to include an optional thinking level parameter, improving the configurability of model interactions.
- Updated the UI components to manage and display the selected model along with its thinking level, ensuring a cohesive user experience.
- Refactored the model selector and input controls to accommodate the new model selection structure, enhancing usability and clarity.
- Adjusted the Electron API and HTTP client to support the new thinking level parameter in requests, ensuring consistent data handling across the application.

This update significantly improves the agent's ability to adapt its reasoning capabilities based on user-defined thinking levels, enhancing overall performance and user satisfaction.
2026-01-02 15:22:06 +01:00
Shirone
81d300391d feat: enhance SDK options with thinking level support
- Introduced a new function, buildThinkingOptions, to handle the conversion of ThinkingLevel to maxThinkingTokens for the Claude SDK.
- Updated existing SDK option creation functions to incorporate thinking options, ensuring that maxThinkingTokens are included based on the specified thinking level.
- Enhanced the settings service to support migration of phase models to include thinking levels, improving compatibility with new configurations.
- Added comprehensive tests for thinking level integration and migration logic, ensuring robust functionality across the application.

This update significantly improves the SDK's configurability and performance by allowing for more nuanced control over reasoning capabilities.
2026-01-02 14:55:52 +01:00
antdev
d417666fe1 fix background image not showing 2026-01-02 15:33:00 +08:00
webdevcody
2bbc8113c0 chore: update lockfile linting process
- Replaced the inline linting command for package-lock.json with a dedicated script (lint-lockfile.mjs) to check for git+ssh:// URLs, ensuring compatibility with CI/CD environments.
- The new script provides clear error messages and instructions if such URLs are found, enhancing the development workflow.
2026-01-02 00:29:04 -05:00
webdevcody
7e03af2dc6 chore: release v0.7.3
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-02 00:00:41 -05:00
Shirone
914734cff6 feat(phase-model-selector): implement grouped model selection and enhanced UI
- Added support for grouped models in the PhaseModelSelector, allowing users to select from multiple variants within a single group.
- Introduced a new popover UI for displaying grouped model variants, improving user interaction and selection clarity.
- Implemented logic to filter and display enabled cursor models, including standalone and grouped options.
- Enhanced state management for expanded groups and variant selection, ensuring a smoother user experience.

This update significantly improves the model selection process, making it more intuitive and organized.
2026-01-02 02:37:20 +01:00
Shirone
e1bdb4c7df Merge remote-tracking branch 'origin/main' into feat/cursor-cli 2026-01-02 01:50:16 +01:00
Kacper
ad947691df feat: enhance TaskProgressPanel and AgentOutputModal components
- Added defaultExpanded prop to TaskProgressPanel for customizable initial state.
- Updated styling in TaskProgressPanel for improved layout and consistency.
- Modified AgentOutputModal to utilize the new defaultExpanded prop and adjusted class names for better responsiveness and appearance.
- Enhanced overflow handling in AgentOutputModal for improved user experience.
2026-01-02 00:23:25 +01:00
Web Dev Cody
ab9ef0d560 Merge pull request #340 from AutoMaker-Org/fix-web-mode-auth
feat: implement authentication state management and routing logic
2026-01-01 17:13:20 -05:00
webdevcody
844be657c8 feat: add skipSandboxWarning to settings and sync function
- Introduced skipSandboxWarning property in GlobalSettings interface to manage user preference for sandbox risk warnings.
- Updated syncSettingsToServer function to include skipSandboxWarning in the settings synchronization process.
- Set default value for skipSandboxWarning to false in DEFAULT_GLOBAL_SETTINGS.
2026-01-01 17:08:15 -05:00
webdevcody
90c89ef338 Merge branch 'fix-web-mode-auth' of github.com:AutoMaker-Org/automaker into fix-web-mode-auth 2026-01-01 16:49:41 -05:00
webdevcody
fb46c0c9ea feat: enhance sandbox risk dialog and settings management
- Updated the SandboxRiskDialog to include a checkbox for users to opt-out of future warnings, passing the state to the onConfirm callback.
- Modified SettingsView to manage the skipSandboxWarning state, allowing users to reset the warning preference.
- Enhanced DangerZoneSection to display a message when the sandbox warning is disabled and provide an option to reset this setting.
- Updated RootLayoutContent to respect the user's choice regarding the sandbox warning, auto-confirming if the user opts to skip it.
- Added skipSandboxWarning state management to the app store for persistent user preferences.
2026-01-01 16:49:35 -05:00
Kacper
81bd57cf6a feat: add runNpmAndWait function for improved npm command handling
- Introduced a new function, runNpmAndWait, to execute npm commands and wait for their completion, enhancing error handling.
- Updated the main function to build shared packages before starting the backend server, ensuring necessary dependencies are ready.
- Adjusted server and web process commands to use a consistent naming convention.
2026-01-01 22:39:12 +01:00
Kacper
83e59d6a4d feat(phase-model-selector): enhance model selection with favorites and popover UI
- Introduced a popover for model selection, allowing users to choose from Claude and Cursor models.
- Added functionality to toggle favorite models, enhancing user experience by allowing quick access to preferred options.
- Updated the UI to display selected model details and improved layout for better usability.
- Refactored model grouping and rendering logic for clarity and maintainability.

This update improves the overall interaction with the phase model selector, making it more intuitive and user-friendly.
2026-01-01 22:31:07 +01:00
webdevcody
59d47928a7 feat: implement authentication state management and routing logic
- Added a new auth store using Zustand to manage authentication state, including `authChecked` and `isAuthenticated`.
- Updated `LoginView` to set authentication state upon successful login and navigate based on setup completion.
- Enhanced `RootLayoutContent` to enforce routing rules based on authentication status, redirecting users to login or setup as necessary.
- Improved error handling and loading states during authentication checks.
2026-01-01 16:25:31 -05:00
Kacper
cbe951dd8f fix(suggestions): extract result text from Cursor provider
- Add handler for type=result messages in Cursor stream processing
- Cursor provider sends final accumulated text in msg.result
- Suggestions was only handling assistant messages for Cursor
- Add detailed logging for result extraction (like backlog plan)
- Matches pattern used by github validation and backlog plan

This fixes potential parsing issues when using Cursor models
for feature suggestions, ensuring the complete response text
is captured before JSON parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 20:03:15 +01:00
Kacper
63b9f52d6b refactor(cursor-models): remove tier property from Cursor model configurations
- Removed the 'tier' property from Cursor model configurations and related UI components.
- Updated relevant files to reflect the removal of tier-related logic and display elements.
- This change simplifies the model structure and UI, focusing on essential attributes.
2026-01-01 20:00:40 +01:00
Kacper
3b3e61da8d fix(backlog-plan): add Cursor-specific prompt with no-file-write instructions
- Import isCursorModel to detect Cursor models
- For Cursor: embed systemPrompt in userPrompt with explicit instructions
- Add "DO NOT write any files" directive for Cursor models
- Prevents Cursor from writing plan to files instead of returning JSON
- Matches pattern used by github validation (validate-issue.ts)

Cursor doesn't support systemPrompt separation like Claude SDK,
so we need to combine prompts and add explicit instructions to
prevent it from using Write/Edit tools and creating files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 19:15:37 +01:00
Kacper
0e22098652 feat(backlog-plan): add detailed logging for Cursor result extraction
- Change debug logs to info/warn so they're always visible
- Log when result message is received from Cursor
- Log lengths of both msg.result and accumulated responseText
- Log which source is being used (result vs accumulated)
- Log empty response error for better diagnostics
- Add response preview logging on parse failure

This will help diagnose why Cursor parsing is failing by showing:
1. Whether result messages are being received
2. What content lengths we're working with
3. Whether response text is empty or has content
4. What the actual response looks like when parsing fails

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 19:07:13 +01:00
Kacper
cf9a1f9077 fix(backlog-plan): use extractJsonWithArray and improve logging
- Switch from extractJson to extractJsonWithArray for 'changes' field
- Validates that 'changes' is an array, not just that it exists
- Add debug logging for response length and preview on parse failure
- Add debug logging when receiving result from Cursor provider
- Matches pattern used by suggestions feature
- Helps diagnose parsing issues with better error context

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:41:25 +01:00
Kacper
9b1174408b fix(backlog-plan): extract result text from Cursor provider
- Add handler for type=result messages in stream processing
- Cursor provider sends final accumulated text in msg.result
- Backlog plan was only handling assistant messages
- Now matches pattern used by github validation and suggestions
- Fixes "cursor cli parsing failed" error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:36:43 +01:00
Kacper
207fd26681 feat(backlog-plan): add model override trigger to footer
- Add ModelOverrideTrigger to backlog plan dialog
- Position trigger in DialogFooter on left side (mr-auto)
- Display before Cancel button for better UX
- Use variant="button" to show model name
- Connect to phaseModels.backlogPlanningModel default
- Pass model override to server generate endpoint

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:32:40 +01:00
Kacper
aa318099dc feat(ui): add model override trigger to backlog plan dialog
Added ModelOverrideTrigger component to the "Plan Backlog Changes" dialog,
allowing users to override the global backlog planning model on a per-request
basis, consistent with other dialogs in the application.

Changes:
- Added model override state management to backlog-plan-dialog
- Integrated ModelOverrideTrigger component in dialog header (input mode only)
- Pass model override (or global default) to backlogPlan.generate API call
- UI shows override indicator when model is overridden from global default

The feature uses the existing backlogPlanningModel phase setting as the default
and allows temporary overrides without changing global settings.

Server already supports optional model parameter in the generate endpoint,
so no backend changes were required.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:20:45 +01:00
Kacper
7dec5d9d74 fix(sdk-options): normalize paths for cross-platform cloud storage detection
Fixed cloud storage path detection to work correctly on Windows by normalizing
path separators to forward slashes and removing Windows drive letters before
pattern matching.

Issue:
The isCloudStoragePath() function was failing on Windows because:
1. path.resolve() converts Unix paths to Windows paths with backslashes
2. Windows adds drive letters (e.g., "P:\Users\test" instead of "/Users/test")
3. Pattern checks for "/Library/CloudStorage/" didn't match "\Library\CloudStorage\"
4. Home-anchored path comparisons failed due to drive letter mismatches

Solution:
- Normalize all path separators to forward slashes for consistent pattern matching
- Remove Windows drive letters (e.g., "C:" or "P:") from normalized paths
- This ensures Unix-style test paths work the same on all platforms

All tests now pass (967 passed, 27 skipped):
-  Cloud storage path detection tests (macOS patterns)
-  Home-anchored cloud folder tests (Dropbox, Google Drive, OneDrive)
-  Sandbox compatibility tests
-  Cross-platform path handling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:11:40 +01:00
Kacper
17dae1571b fix(tests): update terminal-service tests to work cross-platform
Updated terminal-service.test.ts to use path.resolve() in test expectations
so they work correctly on both Unix and Windows platforms.

The merge from main removed the skipIf conditions for Windows, expecting these
tests to work cross-platform. On Windows, path.resolve('/test/dir') converts
Unix-style paths to Windows paths (e.g., 'P:\test\dir'), so test expectations
needed to use path.resolve() as well to match the actual behavior.

Fixed tests:
- should create a new terminal session
- should fix double slashes in path
- should return all active sessions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:07:00 +01:00
Kacper
f56b873571 Merge main into feat/cursor-cli-integration
Carefully merged latest changes from main branch into the Cursor CLI integration
branch. This merge brings in important improvements and fixes while preserving
all Cursor-related functionality.

Key changes from main:
- Sandbox mode security improvements and cloud storage compatibility
- Version-based settings migrations (v2 schema)
- Port configuration centralization
- System paths utilities for CLI detection
- Enhanced error handling in HttpApiClient
- Windows MCP process cleanup fixes
- New validation and build commands
- GitHub issue templates and release process improvements

Resolved conflicts in:
- apps/server/src/routes/context/routes/describe-image.ts
  (Combined Cursor provider routing with secure-fs imports)
- apps/server/src/services/auto-mode-service.ts
  (Merged failure tracking with raw output logging)
- apps/server/tests/unit/services/terminal-service.test.ts
  (Updated to async tests with systemPathExists mocking)
- libs/platform/src/index.ts
  (Combined WSL utilities with system-paths exports)
- libs/types/src/settings.ts
  (Merged DEFAULT_PHASE_MODELS with SETTINGS_VERSION constants)

All Cursor CLI integration features remain intact including:
- CursorProvider and CliProvider base class
- Phase-based model configuration
- Provider registry and factory patterns
- WSL support for Windows
- Model override UI components
- Cursor-specific settings and configurations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 18:03:48 +01:00
Web Dev Cody
bd432b1da3 Merge pull request #304 from firstfloris/fix/sandbox-cloud-storage-compatibility
fix: auto-disable sandbox mode for cloud storage paths
2026-01-01 02:48:38 -05:00
webdevcody
b51aed849c fix: clarify sandbox mode behavior in sdk-options
- Updated the checkSandboxCompatibility function to explicitly handle the case when enableSandboxMode is set to false, ensuring clearer logic for sandbox mode activation.
- Adjusted unit tests to reflect the new behavior, confirming that sandbox mode defaults to enabled when not specified and correctly disables for cloud storage paths.
- Enhanced test descriptions for better clarity on expected outcomes in various scenarios.
2026-01-01 02:39:38 -05:00
Web Dev Cody
90e62b8add Merge pull request #337 from AutoMaker-Org/addressing-pr-issues
feat: improve error handling in HttpApiClient
2026-01-01 02:31:59 -05:00
webdevcody
67c6c9a9e7 feat: enhance cloud storage path detection in sdk-options
- Introduced macOS-specific cloud storage patterns and home-anchored folder detection to improve accuracy in identifying cloud storage paths.
- Updated the isCloudStoragePath function to utilize these new patterns, ensuring better handling of cloud storage locations.
- Added comprehensive unit tests to validate detection logic for various cloud storage scenarios, including false positive prevention.
2026-01-01 02:31:02 -05:00
webdevcody
2d66e38fa7 Merge branch 'main' into fix/sandbox-cloud-storage-compatibility 2026-01-01 02:23:10 -05:00
webdevcody
50aac1c218 feat: improve error handling in HttpApiClient
- Added error handling for HTTP responses in the HttpApiClient class.
- Enhanced error messages to include status text and parsed error data, improving debugging and user feedback.
2026-01-01 02:17:12 -05:00
Web Dev Cody
8c8a4875ca Merge pull request #329 from andydataguy/fix/windows-mcp-orphaned-processes
fix(windows): properly terminate MCP server process trees
2026-01-01 02:12:26 -05:00
webdevcody
eec36268fe Merge branch 'main' into fix/windows-mcp-orphaned-processes 2026-01-01 02:09:54 -05:00
WebDevCody
f6efbd1b26 docs: update release process in documentation
- Added steps for committing version bumps and creating git tags in the release process.
- Clarified the verification steps to include checking the visibility of tags on the remote repository.
2026-01-01 01:40:25 -05:00
WebDevCody
019793e047 chore: release v0.7.2 2026-01-01 01:40:04 -05:00
Web Dev Cody
a8a3711246 Merge pull request #336 from AutoMaker-Org/fix-things
refactor: use environment variables for git configuration in test rep…
2026-01-01 01:23:41 -05:00
WebDevCody
b867ca1407 refactor: update window close behavior for macOS and other platforms
- Modified the application to keep the app and servers running when all windows are closed on macOS, aligning with standard macOS behavior.
- On other platforms, ensured that the server processes are stopped and the app quits when all windows are closed, preventing potential port conflicts.
2026-01-01 01:20:34 -05:00
WebDevCody
75143c0792 refactor: clean up whitespace and improve prompt formatting in port management
- Removed unnecessary whitespace in the init.mjs file for better readability.
- Enhanced the formatting of user prompts to improve clarity during port conflict resolution.
2026-01-01 00:46:14 -05:00
WebDevCody
f32f3e82b2 feat: enhance port management and server initialization process
- Added a new function to check if a port is in use without terminating processes, improving user experience during server startup.
- Updated the health check function to accept a dynamic port parameter, allowing for flexible server configurations.
- Implemented user prompts for handling port conflicts, enabling users to kill processes, choose different ports, or cancel the operation.
- Enhanced CORS configuration to support localhost and IPv6 addresses, ensuring compatibility across different development environments.
- Refactored the main function to utilize dynamic port assignments for both the web and server applications, improving overall flexibility.
2026-01-01 00:42:42 -05:00
WebDevCody
abe272ef4d fix: remove TypeScript type annotations from bumpVersion function
- Updated the bumpVersion function to use plain JavaScript by removing TypeScript type annotations, improving compatibility with non-TypeScript environments.
- Cleaned up whitespace in the bump-version.mjs file for better readability.
2025-12-31 23:33:51 -05:00
WebDevCody
6d4ab9cc13 feat: implement version-based migrations for global settings
- Added versioning to global settings, enabling automatic migrations for breaking changes.
- Updated default global settings to reflect the new versioning schema.
- Implemented logic to disable sandbox mode for existing users during migration from version 1 to 2.
- Enhanced error handling for saving migrated settings, ensuring data integrity during updates.
2025-12-31 23:30:44 -05:00
WebDevCody
98381441b9 feat: add GitHub issue fix command and release command
- Introduced a new command for fetching and validating GitHub issues, allowing users to address issues directly from the command line.
- Added a release command to bump the version of the application and build the Electron app, ensuring version consistency across UI and server packages.
- Updated package.json files for both UI and server to version 0.7.1, reflecting the latest changes.
- Implemented version utility in the server to read the version from package.json, enhancing version management across the application.
2025-12-31 23:24:01 -05:00
WebDevCody
eae60ab6b9 feat: update README logo to SVG format
- Replaced the existing PNG logo with a new SVG version for improved scalability and quality.
- Added the SVG logo file to the project, enhancing visual consistency across different display resolutions.
2025-12-31 22:06:54 -05:00
WebDevCody
1d7b64cea8 refactor: use environment variables for git configuration in test repositories
- Updated test repository creation functions to utilize environment variables for git author and committer information, preventing modifications to the user's global git configuration.
- This change enhances test isolation and ensures consistent behavior across different environments.
2025-12-31 22:02:45 -05:00
Test User
6337e266c5 drag top bar 2025-12-31 21:58:22 -05:00
Web Dev Cody
da38adcba6 Merge pull request #332 from AutoMaker-Org/centeralize-fs-access
feat: implement secure file system access and path validation
2025-12-31 21:45:19 -05:00
Test User
af493fb73e feat: simulate containerized environment for testing
- Added an environment variable to simulate a containerized environment, allowing the application to skip sandbox confirmation dialogs during testing.
- This change aims to streamline the testing process by reducing unnecessary user interactions while ensuring the application behaves as expected in a containerized setup.
2025-12-31 21:21:35 -05:00
Test User
79bf1c9bec feat: add centralized build validation command and refactor port configuration
- Introduced a new command for validating project builds, providing detailed instructions for running builds and intelligently fixing failures based on recent changes.
- Refactored port configuration by centralizing it in the @automaker/types package for improved maintainability and backward compatibility.
- Updated imports in various modules to reflect the new centralized port configuration, ensuring consistent usage across the application.
2025-12-31 21:07:26 -05:00
Test User
b9a6e29ee8 feat: add sandbox environment checks and user confirmation dialogs
- Introduced a new endpoint to check if the application is running in a containerized environment, allowing the UI to display appropriate risk warnings.
- Added a confirmation dialog for users when running outside a sandbox, requiring acknowledgment of potential risks before proceeding.
- Implemented a rejection screen for users who deny sandbox risk confirmation, providing options to restart in a container or reload the application.
- Updated the main application logic to handle sandbox status checks and user responses effectively, enhancing security and user experience.
2025-12-31 21:00:23 -05:00
Test User
2828431cca feat: add test validation command and improve environment variable handling
- Introduced a new command for validating tests, providing detailed instructions for running tests and fixing failures based on code changes.
- Updated the environment variable handling in the Claude provider to only allow explicitly defined variables, enhancing security and preventing leakage of sensitive information.
- Improved feature loading to handle errors more gracefully and load features concurrently, optimizing performance.
- Centralized port configuration for the Automaker application to prevent accidental termination of critical services.
2025-12-31 20:36:20 -05:00
Web Dev Cody
d3f46f565b Merge pull request #330 from AutoMaker-Org/chore/cleanup-unused-files
chore: remove unused files from codebase and adress audit security
2025-12-31 20:02:23 -05:00
Test User
3f4f2199eb feat: initialize API key on module import for improved async handling
- Start API key initialization immediately upon importing the HTTP API client module to ensure the init promise is created early.
- Log errors during API key initialization to aid in debugging.

Additionally, added a version field to the setup store for proper state hydration, aligning with the app-store pattern.
2025-12-31 20:00:54 -05:00
Test User
38f0b16530 Merge remote-tracking branch 'origin/main' into centeralize-fs-access 2025-12-31 19:57:17 -05:00
Web Dev Cody
bd22323149 Merge pull request #335 from RayFernando1337/main
fix: resolve auth race condition causing 401 errors on Electron startup
2025-12-31 19:56:20 -05:00
RayFernando
f6ce03d59a fix: resolve auth race condition causing 401 errors on Electron startup
API requests were being made before initApiKey() completed, causing
401 Unauthorized errors on app startup in Electron mode.

Changes:
- Add waitForApiKeyInit() to track and await API key initialization
- Make HTTP methods (get/post/put/delete) wait for auth before requests
- Defer WebSocket connection until API key is ready
- Add explicit auth wait in useSettingsMigration hook

Fixes race condition introduced in PR #321
2025-12-31 16:14:09 -08:00
Test User
63816043cf feat: enhance shell detection logic and improve cross-platform support
- Updated the TerminalService to utilize getShellPaths() for better shell detection across platforms.
- Improved logic for detecting user-configured shells in WSL and added fallbacks for various platforms.
- Enhanced unit tests to mock shell paths for comprehensive cross-platform testing, ensuring accurate shell detection behavior.

These changes aim to streamline shell detection and improve the user experience across different operating systems.
2025-12-31 19:06:13 -05:00
Test User
eafe474dbc fix: update node-gyp repository URL to use HTTPS
Changed the resolved URL for the @electron/node-gyp dependency in package-lock.json from SSH to HTTPS for improved accessibility and compatibility across different environments.
2025-12-31 18:53:47 -05:00
Test User
59bbbd43c5 feat: add Node.js version management and improve error handling
- Introduced a .nvmrc file to specify the Node.js version (22) for the project, ensuring consistent development environments.
- Enhanced error handling in the startServer function to provide clearer messages when the Node.js executable cannot be found, improving debugging experience.
- Updated package.json files across various modules to enforce Node.js version compatibility and ensure consistent dependency versions.

These changes aim to streamline development processes and enhance the application's reliability by enforcing version control and improving error reporting.
2025-12-31 18:42:33 -05:00
Test User
2b89b0606c feat: implement secure file system access and path validation
- Introduced a restricted file system wrapper to ensure all file operations are confined to the script's directory, enhancing security.
- Updated various modules to utilize the new secure file system methods, replacing direct fs calls with validated operations.
- Enhanced path validation in the server routes and context loaders to prevent unauthorized access to the file system.
- Adjusted environment variable handling to use centralized methods for reading and writing API keys, ensuring consistent security practices.

This change improves the overall security posture of the application by enforcing strict file access controls and validating paths before any operations are performed.
2025-12-31 18:03:01 -05:00
Kacper
f496bb825d feat(agent-view): refactor to folder pattern and add Cursor model support
- Refactor agent-view.tsx from 1028 lines to ~215 lines
- Create agent-view/ folder with components/, hooks/, input-area/, shared/
- Extract hooks: useAgentScroll, useFileAttachments, useAgentShortcuts, useAgentSession
- Extract components: AgentHeader, ChatArea, MessageList, MessageBubble, ThinkingIndicator
- Extract input-area: AgentInputArea, FilePreview, QueueDisplay, InputControls
- Add AgentModelSelector with Claude and Cursor CLI model support
- Update /models/available to use ProviderFactory.getAllAvailableModels()
- Update /models/providers to include Cursor CLI status

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:58:21 +01:00
Kacper
07327e48b4 chore: remove unused pipeline feature documentation 2025-12-31 10:41:20 +01:00
Anand (Andy) Houston
e818922b0d fix(windows): properly terminate MCP server process trees
On Windows, MCP server processes spawned via 'cmd /c npx' weren't being
properly terminated after testing, causing orphaned processes that would
spam logs with "FastMCP warning: server is not responding to ping".

Root cause: client.close() kills only the parent cmd.exe, orphaning child
node.exe processes. taskkill /t needs the parent PID to traverse the tree.

Fix: Run taskkill BEFORE client.close() so the parent PID still exists
when we kill the process tree.

- Add execSync import for taskkill execution
- Add IS_WINDOWS constant for platform check
- Create cleanupConnection() method with proper termination order
- Add comprehensive documentation in docs/

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 16:04:23 +08:00
Shirone
04aac7ec07 chore: update package-lock.json to add peer dependencies and update package versions 2025-12-31 03:34:41 +01:00
Kacper
944e2f5ffe chore: remove unused files from codebase 2025-12-31 03:22:25 +01:00
Kacper
9653e2b970 refactor(settings): remove AI Enhancement section and related components
- Deleted AIEnhancementSection and its associated files from the settings view.
- Updated SettingsView to remove references to AI enhancement functionality.
- Cleaned up navigation and feature defaults sections by removing unused validation model references.

This refactor streamlines the settings view by eliminating the AI enhancement feature, which is no longer needed.
2025-12-31 02:56:06 +01:00
Kacper
5c400b7eff fix(server): Fix unit tests and increase coverage
- Skip platform-specific tests on Windows (CI runs on Linux)
- Add tests for json-extractor.ts (96% coverage)
- Add tests for cursor-config-manager.ts (100% coverage)
- Add tests for cursor-config-service.ts (98.8% coverage)
- Exclude CLI integration code from coverage (needs integration tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 02:14:11 +01:00
Kacper
3bc4b7f1f3 fix: update qs to 6.14.1 to fix high severity DoS vulnerability
Fixes GHSA-6rw7-vpxm-498p - qs's arrayLimit bypass in bracket notation
allows DoS via memory exhaustion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 01:40:15 +01:00
Kacper
d539f7e3b7 Merge origin/main into feat/cursor-cli
Merges latest main branch changes including:
- MCP server support and configuration
- Pipeline configuration system
- Prompt customization settings
- GitHub issue comments in validation
- Auth middleware improvements
- Various UI/UX improvements

All Cursor CLI features preserved:
- Multi-provider support (Claude + Cursor)
- Model override capabilities
- Phase model configuration
- Provider tabs in settings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 01:22:18 +01:00
Kacper
853292af45 refactor(cursor): seperate components and add permissions skeleton 2025-12-30 17:54:02 +01:00
Kacper
3c6736bc44 feat(cursor): Enhance Cursor tool handling with a registry and improved processing
Introduced a registry for Cursor tool handlers to streamline the processing of various tool calls, including read, write, edit, delete, grep, ls, glob, semantic search, and read lints. This refactor allows for better organization and normalization of tool inputs and outputs.

Additionally, updated the CursorToolCallEvent interface to accommodate new tool calls and their respective arguments. Enhanced logging for raw events and unrecognized tool call structures for improved debugging.

Affected files:
- cursor-provider.ts: Added CURSOR_TOOL_HANDLERS and refactored tool call processing.
- log-parser.ts: Updated tool categories and added summaries for new tools.
- cursor-cli.ts: Expanded CursorToolCallEvent interface to include new tool calls.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-12-30 17:35:39 +01:00
Kacper
dac916496c feat(server): Implement Cursor CLI permissions management
Added new routes and handlers for managing Cursor CLI permissions, including:
- GET /api/setup/cursor-permissions: Retrieve current permissions configuration and available profiles.
- POST /api/setup/cursor-permissions/profile: Apply a predefined permission profile (global or project).
- POST /api/setup/cursor-permissions/custom: Set custom permissions for a project.
- DELETE /api/setup/cursor-permissions: Delete project-level permissions, reverting to global settings.
- GET /api/setup/cursor-permissions/example: Provide an example config file for a specified profile.

Also introduced a new service for handling Cursor CLI configuration files and updated the UI to support permissions management.

Affected files:
- Added new routes in index.ts and cursor-config.ts
- Created cursor-config-service.ts for permissions management logic
- Updated UI components to display and manage permissions

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-12-30 17:08:18 +01:00
Web Dev Cody
847a8ff327 Merge pull request #306 from Waaiez/fix/linux-claude-usage
fix: add Linux support for Claude usage service
2025-12-30 10:13:50 -05:00
Web Dev Cody
504c19aef5 Merge pull request #326 from andydataguy/fix/windows-orphaned-server-processes
fix(windows): properly kill server process tree on app quit
2025-12-30 10:07:10 -05:00
Web Dev Cody
ed2da7932c Merge pull request #327 from casiusss/fix/backlog-plan-json-format
fix: restore correct JSON format for backlog plan prompt
2025-12-30 10:05:56 -05:00
Kacper
078ab943a8 fix(server): Add explicit JSON response instructions for Cursor prompts
Cursor was writing JSON to files instead of returning it in the response.
Added clear instructions to all Cursor prompts:
1. DO NOT write any files
2. Return ONLY raw JSON in the response
3. No explanations, no markdown, just JSON

Affected routes:
- generate-spec.ts
- generate-features-from-spec.ts
- validate-issue.ts
- generate-suggestions.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:44:50 +01:00
Kacper
948fdb6352 refactor(server): Use shared JSON extractor in feature and plan parsing
Update parseAndCreateFeatures and parsePlanResponse to use the shared
extractJson/extractJsonWithArray utilities instead of manual regex
parsing for more robust and consistent JSON extraction from AI responses.

- parse-and-create-features.ts: Use extractJsonWithArray for features
- generate-plan.ts: Use extractJson with requiredKey for backlog plans

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:36:17 +01:00
Kacper
b0f83b7c76 feat(server): Add readOnly mode to Cursor provider for read-only operations
Adds a readOnly option to ExecuteOptions that controls whether the
Cursor CLI runs with --force flag (allows edits) or without (suggest-only).

Read-only routes now pass readOnly: true:
- generate-spec.ts, generate-features-from-spec.ts (we write files ourselves)
- validate-issue.ts, generate-suggestions.ts (analysis only)
- describe-file.ts, describe-image.ts (description only)
- generate-plan.ts, enhance.ts (text generation only)

Routes that implement features (auto-mode-service, agent-service) keep
the default (readOnly: false) to allow file modifications.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:31:35 +01:00
Kacper
38d0e4103a feat(server): Add Cursor provider routing to spec generation routes
Add Cursor model support to generate-spec.ts and generate-features-from-spec.ts
routes, allowing them to use Cursor models when configured in phaseModels settings.

- Both routes now detect Cursor models via isCursorModel()
- Route to ProviderFactory for Cursor models, Claude SDK for Claude models
- Use resolveModelString() for proper model ID resolution
- Extract JSON from Cursor responses using shared json-extractor utility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:26:40 +01:00
Kacper
19016f03d7 refactor(server): Extract JSON extraction utility to shared module
Created libs/server/src/lib/json-extractor.ts with reusable JSON
extraction utilities for parsing AI responses:

- extractJson<T>(): Multi-strategy JSON extraction
- extractJsonWithKey<T>(): Extract with required key validation
- extractJsonWithArray<T>(): Extract with array property validation

Strategies (tried in order):
1. JSON in ```json code block
2. JSON in ``` code block
3. Find JSON object by matching braces (with optional required key)
4. Find any JSON object by matching braces
5. First { to last }
6. Parse entire response

Updated:
- generate-suggestions.ts: Use extractJsonWithArray('suggestions')
- validate-issue.ts: Use extractJson()

Both files now use the shared utility instead of local implementations,
following DRY principle.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:18:45 +01:00
Kacper
26e4ac0d2f fix(suggestions): Improve JSON extraction for Cursor responses
Cursor responses may include text after the JSON object, causing
JSON.parse to fail. Added multi-strategy extraction similar to
validate-issue.ts:

1. Try extracting from ```json code block
2. Try extracting from ``` code block
3. Try finding {"suggestions" and matching braces
4. Try finding any JSON object with suggestions array

Uses bracket counting to find the correct closing brace.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:14:57 +01:00
Kacper
efd9a1b7d9 feat(suggestions): Wire to phaseModels.enhancementModel with Cursor support
The suggestions generation route (Feature Enhancement in UI) was not
reading from phaseModels settings and always used the default haiku model.

Changes:
- Read enhancementModel from phaseModels settings
- Add provider routing for Cursor vs Claude models
- Pass model to createSuggestionsOptions for Claude SDK
- For Cursor, include JSON schema in prompt and use ProviderFactory

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:11:34 +01:00
Stephan Rieche
968d889346 fix: restore correct JSON format for backlog plan prompt
The backlog plan system prompt was using an incorrect JSON format that didn't
match the BacklogPlanResult interface. This caused the plan generation to
complete but produce no visible results.

Issue:
- Prompt specified: { "plan": { "add": [...], "update": [...], "delete": [...] } }
- Code expected: { "changes": [...], "summary": "...", "dependencyUpdates": [...] }

Fix:
- Restored original working format with "changes" array
- Each change has: type ("add"|"update"|"delete"), feature, reason
- Matches BacklogPlanResult and BacklogChange interfaces exactly

Impact:
- Plan button on Kanban board will now generate and display plans correctly
- AI responses will be properly parsed and shown in review dialog

Testing:
- All 845 tests passing
- Verified format matches original hardcoded prompt from upstream

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 15:09:18 +01:00
Kacper
ed66fdd57d fix(cursor): Pass prompt via stdin to avoid shell escaping issues
When passing file content (containing TypeScript code) to cursor-agent via
WSL, bash was interpreting shell metacharacters like $(), backticks, etc.
as command substitution, causing errors like "/bin/bash: typescript\r':
command not found".

Changes:
- subprocess.ts: Add stdinData option to SubprocessOptions interface
- subprocess.ts: Write stdinData to stdin when provided
- cursor-provider.ts: Extract prompt text separately and pass via stdin
- cursor-provider.ts: Use '-' as prompt arg to indicate reading from stdin

This ensures file content with code examples is passed safely without
shell interpretation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 15:07:08 +01:00
Kacper
34e51ddc3d feat(server): Add Cursor provider support for describe-file and describe-image routes
- describe-file.ts: Route to Cursor provider when using Cursor models (composer-1, etc.)
- describe-image.ts: Route to Cursor provider with image path context for Cursor models
- auto-mode-service.ts: Fix logging to use console.log instead of this.logger

Both routes now detect Cursor models using isCursorModel() and use
ProviderFactory.getProviderForModel() to get the appropriate provider
instead of always using the Claude SDK.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:59:25 +01:00
Kacper
68cefe43fb fix(ui): Add phaseModels to localStorage persistence
phaseModels was missing from the partialize() function, causing
it to reset to defaults on app restart. Now properly persisted
alongside other settings like enhancementModel and validationModel.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:46:11 +01:00
Kacper
d6a1c08952 fix(ui): Sync phaseModels to server when changed
Previously, phaseModels only persisted to localStorage but the server
reads from settings.json file. Now setPhaseModel/setPhaseModels/resetPhaseModels
call syncSettingsToServer() to keep server-side settings in sync.

Also added phaseModels to the syncSettingsToServer() updates object.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:40:20 +01:00
Kacper
fd7c22a457 feat(server): Wire analyzeProject to use phaseModels.projectAnalysisModel
Read model from settings.phaseModels.projectAnalysisModel instead of
hardcoded DEFAULT_MODELS.claude fallback. Falls back to
DEFAULT_PHASE_MODELS if settings unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:24:18 +01:00
Kacper
0798a64cd6 feat(server): Wire generate-plan to use phaseModels.backlogPlanningModel
Read model from settings.phaseModels.backlogPlanningModel instead of
hardcoded 'sonnet' fallback. Still supports per-call override via model
parameter. Falls back to DEFAULT_PHASE_MODELS if settings unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:23:12 +01:00
Kacper
fcba327fdb feat(server): Wire generate-features-from-spec to use phaseModels.featureGenerationModel
Pass model from settings.phaseModels.featureGenerationModel to
createFeatureGenerationOptions(). Falls back to DEFAULT_PHASE_MODELS
if settings unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:22:14 +01:00
Kacper
4d69d04e2b feat(server): Wire generate-spec to use phaseModels.specGenerationModel
Pass model from settings.phaseModels.specGenerationModel to
createSpecGenerationOptions(). Falls back to DEFAULT_PHASE_MODELS
if settings unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:21:17 +01:00
Kacper
f43e90f2d2 feat(server): Wire describe-image to use phaseModels.imageDescriptionModel
Replace hardcoded CLAUDE_MODEL_MAP.haiku with configurable model from
settings.phaseModels.imageDescriptionModel. Falls back to DEFAULT_PHASE_MODELS
if settings unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:20:06 +01:00
Kacper
ac0d4a556a feat(server): Wire describe-file to use phaseModels.fileDescriptionModel
Replace hardcoded CLAUDE_MODEL_MAP.haiku with configurable model from
settings.phaseModels.fileDescriptionModel. Falls back to DEFAULT_PHASE_MODELS
if settings unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:19:03 +01:00
Kacper
2be0e7d5f0 fix(ui): Use phaseModels.validationModel instead of legacy field
Update use-issue-validation hook to use the new phaseModels structure
for validation model selection instead of deprecated validationModel field.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:17:53 +01:00
Kacper
24599e0b8c feat(server): Add settings migration for phaseModels
- Add migratePhaseModels() to handle legacy enhancementModel/validationModel fields
- Deep merge phaseModels in updateGlobalSettings()
- Export PhaseModelConfig, PhaseModelKey, and DEFAULT_PHASE_MODELS from types
- Backwards compatible: legacy fields migrate to phaseModels structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:16:53 +01:00
Kacper
45d93f28bf fix(server): Improve Cursor CLI JSON response parsing
Add robust multi-strategy JSON extraction for Cursor validation responses:
- Strategy 1: Extract from ```json code blocks
- Strategy 2: Extract from ``` code blocks (no language)
- Strategy 3: Find JSON object directly in text (first { to last })
- Strategy 4: Parse entire response as JSON

This fixes silent failures when Cursor returns JSON in various formats.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 14:14:43 +01:00
Waaiez Kinnear
04aca1c8cb fix: add SIGTERM fallback for Linux Claude usage
On Linux, the ESC key doesn't exit the Claude CLI, causing a 30s timeout.
This fix:
1. Adds SIGTERM fallback 2s after ESC fails
2. Returns captured data on timeout instead of failing

Tested: ~19s on Linux instead of 30s timeout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 13:24:45 +02:00
Anand (Andy) Houston
784d7fc059 fix(windows): use execSync for reliable process termination
Address code review feedback:
- Replace async spawn() with sync execSync() to ensure taskkill
  completes before app exits
- Add try/catch error handling for permission/invalid-PID errors
- Add helpful error logging for debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 16:56:31 +08:00
Anand (Andy) Houston
d6705fbfb5 fix(windows): properly kill server process tree on app quit
On Windows, serverProcess.kill() doesn't reliably terminate Node.js
child processes. This causes orphaned node processes to hold onto
ports 3007/3008, preventing the app from starting on subsequent launches.

Use taskkill with /f /t flags to force-kill the entire process tree
on Windows, while keeping SIGTERM for macOS/Linux where it works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 16:47:29 +08:00
Web Dev Cody
c5ae9ad262 Merge pull request #325 from AutoMaker-Org/fix-mcp-bug
feat: enhance MCP server management and JSON import/export functionality
2025-12-30 01:51:46 -05:00
Test User
5a0ad75059 fix: improve MCP server update and synchronization handling
- Added rollback functionality for server updates on sync failure to maintain local state integrity.
- Enhanced logic for identifying newly added servers during addition and import processes, ensuring accurate pending sync tracking.
- Implemented duplicate server name validation during configuration to prevent errors in server management.
2025-12-30 01:47:55 -05:00
Test User
cf62dbbf7a feat: enhance MCP server management and JSON import/export functionality
- Introduced pending sync handling for MCP servers to improve synchronization reliability.
- Updated auto-test logic to skip servers pending sync, ensuring accurate testing.
- Enhanced JSON import/export to support both array and object formats, preserving server IDs.
- Added validation for server configurations during import to prevent errors.
- Improved error handling and user feedback for sync operations and server updates.
2025-12-30 01:32:43 -05:00
Web Dev Cody
a4d1a1497a Merge pull request #322 from casiusss/feat/customizable-prompts
feat: customizable prompts
2025-12-30 00:58:11 -05:00
Web Dev Cody
b798260491 Merge pull request #324 from illia1f/fix/kanban-card-ui
fix(kanban-card): jumping hover animation & drag overlay consistency
2025-12-30 00:44:27 -05:00
Web Dev Cody
1fcaa52f72 Merge pull request #321 from AutoMaker-Org/protect-api-with-api-key
adding more security to api endpoints to require api token for all ac…
2025-12-30 00:42:46 -05:00
Test User
46caae05d2 feat: improve test setup and authentication handling
- Added `dev:test` script to package.json for streamlined testing without file watching.
- Introduced `kill-test-servers` script to ensure no existing servers are running on test ports before executing tests.
- Enhanced Playwright configuration to use mock agent for tests, ensuring consistent API responses and disabling rate limiting.
- Updated various test files to include authentication steps and handle login screens, improving reliability and reducing flakiness in tests.
- Added `global-setup` for e2e tests to ensure proper initialization before test execution.
2025-12-30 00:06:27 -05:00
Kacper
39f2c8c9ff feat(ui): Enhance AI model handling with Cursor support
- Refactor model handling to support both Claude and Cursor models across various components.
- Introduce `stripProviderPrefix` utility for consistent model ID processing.
- Update `CursorProvider` to utilize `isCursorModel` for model validation.
- Implement model override functionality in GitHub issue validation and enhancement routes.
- Add `useCursorStatusInit` hook to initialize Cursor CLI status on app startup.
- Update UI components to reflect changes in model selection and validation processes.

This update improves the flexibility of AI model usage and enhances user experience by allowing quick model overrides.
2025-12-30 04:01:56 +01:00
Test User
59a6a23f9b feat: enhance test authentication and context navigation
- Added `authenticateForTests` utility to streamline API key authentication in tests, using a fallback for local testing.
- Updated context image test to include authentication step before navigation, ensuring proper session handling.
- Increased timeout for context view visibility to accommodate slower server responses.
- Introduced a test API key in the Playwright configuration for consistent testing environments.
2025-12-29 22:01:03 -05:00
Kacper
3d655c3298 feat(ui): Add ModelOverrideTrigger component for quick model overrides
- Add ModelOverrideTrigger with three variants: icon, button, inline
- Add useModelOverride hook for managing override state per phase
- Create shared components directory for reusable UI components
- Popover shows Claude + enabled Cursor models
- Visual indicator dot when model is overridden from global

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 02:13:46 +01:00
Kacper
2ba114931c feat(ui): Add Phase Models settings tab
- Add PhaseModelsSection with grouped phase configuration:
  - Quick Tasks: enhancement, file/image description
  - Validation Tasks: GitHub issue validation
  - Generation Tasks: spec, features, backlog, analysis
- Add PhaseModelSelector component showing Claude + Cursor models
- Add phaseModels state and actions to app-store
- Add 'phase-models' navigation item with Workflow icon

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 02:04:36 +01:00
Illia Filippov
88bb5b923f style(kanban-card): add transition effects to card wrapper classes for smoother animations 2025-12-30 02:01:13 +01:00
Kacper
a415ae6207 feat(types): Add PhaseModelConfig for per-phase AI model selection
- Add PhaseModelConfig interface with 8 configurable phases:
  - Quick tasks: enhancement, fileDescription, imageDescription
  - Validation: validationModel
  - Generation: specGeneration, featureGeneration, backlogPlanning, projectAnalysis
- Add PhaseModelKey type for type-safe access
- Add DEFAULT_PHASE_MODELS with sensible defaults
- Add phaseModels field to GlobalSettings
- Mark legacy enhancementModel/validationModel as deprecated
- Export new types from @automaker/types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:55:21 +01:00
Stephan Rieche
504d9aa9d7 refactor: migrate AgentService to use centralized logger
Replace console.error calls with createLogger for consistent logging across
the AgentService. This improves debuggability and makes logger calls testable.

Changes:
- Add createLogger import from @automaker/utils
- Add private logger instance initialized with 'AgentService' prefix
- Replace all 7 console.error calls with this.logger.error
- Update test mocks to use vi.hoisted() for proper mock access
- Update settings-helpers test to create mockLogger inside vi.mock()

Test Impact:
- All 774 tests passing
- Logger error calls are now verifiable in tests
- Mock logger properly accessible via vi.hoisted() pattern

Resolves Gemini Code Assist suggestions:
- "Make logger mockable for test assertions"
- "Use logger instead of console.error in AgentService"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 01:43:27 +01:00
Illia Filippov
ab0cd95d9a refactor(kanban-card): switch from useSortable to useDraggable 2025-12-30 01:36:00 +01:00
Test User
4c65855140 feat: enhance authentication and session management tests
- Added comprehensive unit tests for authentication middleware, including session token validation, API key authentication, and cookie-based authentication.
- Implemented tests for session management functions such as creating, updating, archiving, and deleting sessions.
- Improved test coverage for queue management in session handling, ensuring robust error handling and validation.
- Introduced checks for session metadata and working directory validation to ensure proper session creation.
2025-12-29 19:35:09 -05:00
Test User
adfc353b2d feat: add middleware to enforce JSON Content-Type for API requests
- Introduced `requireJsonContentType` middleware to ensure that all POST, PUT, and PATCH requests have the Content-Type set to application/json.
- This enhancement improves security by preventing CSRF and content-type confusion attacks, ensuring only properly formatted requests are processed.
2025-12-29 19:21:56 -05:00
Kacper
c1c2e706f0 chore: Remove temporary refactoring analysis document 2025-12-30 01:13:57 +01:00
Stephan Rieche
d5aea8355b refactor: improve code quality based on Gemini Code Assist suggestions
Applied three code quality improvements suggested by Gemini Code Assist:

1. **Replace nested ternary with map object (enhance.ts)**
   - Changed nested ternary operator to Record<EnhancementMode, string> map
   - Improves readability and maintainability
   - More declarative approach for system prompt selection

2. **Simplify handleToggle logic (prompt-customization-section.tsx)**
   - Removed redundant if/else branches
   - Both branches were calculating the same value
   - Cleaner, more concise implementation

3. **Add type safety to updatePrompt with generics (prompt-customization-section.tsx)**
   - Changed field parameter from string to keyof NonNullable<PromptCustomization[T]>
   - Prevents runtime errors from misspelled field names
   - Improved developer experience with autocomplete

All tests passing (774/774). Builds successful.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 01:09:25 +01:00
Test User
e498f39153 fix: update node-gyp repository URL in package-lock.json
- Changed the resolved URL for the @electron/node-gyp module from SSH to HTTPS for improved accessibility and compatibility.
2025-12-29 19:07:25 -05:00
Kacper
4157e11bba refactor(cursor): Extend CliProvider base class
Refactor CursorProvider to extend CliProvider instead of BaseProvider:
- Implement abstract methods: getCliName, getSpawnConfig, buildCliArgs, normalizeEvent
- Override detectCli() for Cursor-specific versions directory check
- Override mapError() for Cursor-specific error codes
- Override getInstallInstructions() for Cursor-specific guidance
- Reuse base class buildSubprocessOptions() for WSL/NPX handling

Removes ~200 lines of duplicated infrastructure code.
All existing behavior preserved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:07:08 +01:00
Test User
d66259b411 feat: enhance authentication and session management
- Added NODE_ENV variable for development in docker-compose.override.yml.example.
- Changed default NODE_ENV to development in Dockerfile.
- Implemented fetchWsToken function to retrieve short-lived WebSocket tokens for secure authentication in TerminalPanel.
- Updated connect function to use wsToken for WebSocket connections when API key is not available.
- Introduced verifySession function to validate session status after login and on app load, ensuring session integrity.
- Modified RootLayoutContent to verify session cookie validity and redirect to login if the session is invalid or expired.

These changes improve the security and reliability of the authentication process.
2025-12-29 19:06:11 -05:00
Kacper
677f441cd1 feat(providers): Create CliProvider abstract base class
Add reusable infrastructure for CLI-based AI providers:
- SpawnStrategy types ('wsl' | 'npx' | 'direct' | 'cmd')
- CliSpawnConfig interface for platform-specific configuration
- Common CLI path detection (PATH, common locations)
- WSL support for Windows (reuses @automaker/platform utilities)
- NPX strategy for npm-installed CLIs
- Strategy-aware subprocess spawning with JSONL streaming
- Error mapping infrastructure with recovery suggestions

Reuses existing utilities:
- spawnJSONLProcess, WSL utils from @automaker/platform
- createLogger, isAbortError from @automaker/utils

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 01:02:07 +01:00
Illia Filippov
e556521c8d fix(kanban-card): jumping hover animation & drag overlay consistency 2025-12-30 00:51:52 +01:00
Kacper
dc8c06e447 feat(providers): Add provider registry pattern
Replace hardcoded switch statements with dynamic registry pattern.
Providers register with factory using registerProvider() function.

New features:
- registerProvider() function for dynamic registration
- canHandleModel() callback for model routing
- priority field for controlling match order
- aliases support (e.g., 'anthropic' -> 'claude')
- getRegisteredProviderNames() for introspection

Adding new providers now only requires calling registerProvider()
with a factory function and model matching logic.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 00:42:17 +01:00
Stephan Rieche
e448d6d4e5 fix: restore detailed planning prompts and fix test suite
This commit fixes two issues introduced during prompt customization:

1. **Restored Full Planning Prompts from Main**
   - Lite Mode: Added "Silently analyze the codebase first" instruction
   - Spec Mode: Restored detailed task format rules, [TASK_START]/[TASK_COMPLETE] markers
   - Full Mode: Restored comprehensive SDD format with [PHASE_COMPLETE] markers
   - Fixed table structures (Files to Modify, Technical Context, Risks & Mitigations)
   - Ensured all critical instructions for Auto Mode functionality are preserved

2. **Fixed Test Suite (774 tests passing)**
   - Made getPlanningPromptPrefix() async-aware in all 11 planning tests
   - Replaced console.log/error mocks with createLogger mocks (settings-helpers, agent-service)
   - Updated test expectations to match restored prompts
   - Fixed variable hoisting issue in agent-service mock setup
   - Built prompts library to apply changes

The planning prompts now match the detailed, production-ready versions from main
branch, ensuring Auto Mode has all necessary instructions for proper task execution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 00:40:01 +01:00
Kacper
55bd9b0dc7 refactor(cursor): Move stream dedup logic to CursorProvider
Move Cursor-specific duplicate text handling from auto-mode-service.ts
into CursorProvider.deduplicateTextBlocks() for cleaner separation.

This handles:
- Duplicate consecutive text blocks (same text twice in a row)
- Final accumulated text block (contains ALL previous text)

Also update REFACTORING-ANALYSIS.md with SpawnStrategy types for
future CLI providers (wsl, npx, direct, cmd).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 00:35:39 +01:00
Kacper
b76f09db2d refactor(types): Use ModelProvider type instead of hardcoded union
Replace 'claude' | 'cursor' literal unions with ModelProvider type
from @automaker/types for better extensibility when adding new providers.

- Update ProviderFactory.getProviderNameForModel() return type
- Update RunningFeature.provider type in auto-mode-service
- Update getRunningAgents() return type

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 00:25:37 +01:00
Kacper
35fa822c32 fix: Update section title in Claude setup step for clarity
- Changed the title from "API Key Setup" to "Claude Code Setup" to better reflect the purpose of the section and improve user understanding.
2025-12-30 00:22:20 +01:00
Kacper
a842d1b917 fix(tests): Update provider-factory tests for CursorProvider
- Update test assertions from expecting 1 provider to 2
- Add CursorProvider import and tests for Cursor model routing
- Add tests for Cursor models (cursor-auto, cursor-sonnet-4.5, etc.)
- Update tests for gpt-5.2/grok/gemini-3-pro as valid Cursor models
- Add tests for checkAllProviders to expect cursor status
- Add tests for getProviderByName with 'cursor'

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 00:22:07 +01:00
Stephan Rieche
65a09b2d38 fix: add index signature to planningPrompts for TypeScript
Add Record<string, string> type to planningPrompts object to fix TypeScript
error when using string as index.

Error fixed:
Element implicitly has an 'any' type because expression of type 'string'
can't be used to index type '{ lite: string; ... }'.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:42:00 +01:00
Test User
469ee5ff85 security: harden API authentication system
- Use crypto.timingSafeEqual() for API key validation (prevents timing attacks)
- Make WebSocket tokens single-use (invalidated after first validation)
- Add AUTOMAKER_HIDE_API_KEY env var to suppress API key banner in logs
- Add rate limiting to login endpoint (5 attempts/minute/IP)
- Update client to fetch short-lived wsToken for WebSocket auth
  (session tokens no longer exposed in URLs)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:35:55 -05:00
Stephan Rieche
04e6ed30a2 refactor: use centralized logger instead of console.log
Replace all console.log/console.error calls in settings-helpers.ts with
the centralized logger from @automaker/utils for consistency.

Changes:
- Import createLogger from @automaker/utils
- Create logger instance: createLogger('SettingsHelper')
- Replace console.log → logger.info
- Replace console.error → logger.error

Benefits:
- Consistent logging across the codebase
- Better log formatting and structure
- Easier to filter/control log output
- Follows existing patterns in other services

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:35:22 +01:00
Stephan Rieche
ec3d78922e fix: remove prompt caching to enable hot reload of custom prompts
Remove caching from Auto Mode and Agent services to allow custom prompts
to take effect immediately without requiring app restart.

Changes:
- Auto Mode: Load prompts on every feature execution instead of caching
- Agent Service: Load prompts on every chat message instead of caching
- Remove unused class fields: planningPrompts, agentSystemPrompt

This makes custom prompts work consistently across all features:
✓ Auto Mode - hot reload enabled
✓ Agent Runner - hot reload enabled
✓ Backlog Plan - already had hot reload
✓ Enhancement - already had hot reload

Users can now modify prompts in Settings and see changes immediately
without restarting the app.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:25:15 +01:00
Stephan Rieche
bc0ef47323 feat: add customizable AI prompts with enhanced UX
Add comprehensive prompt customization system allowing users to customize
all AI prompts (Auto Mode, Agent Runner, Backlog Plan, Enhancement) through
the Settings UI.

## Features

### Core Customization System
- New TypeScript types for prompt customization with enabled flag
- CustomPrompt interface with value and enabled state
- Prompts preserved even when disabled (no data loss)
- Merged prompt system (custom overrides defaults when enabled)
- Persistent storage in ~/.automaker/settings.json

### Settings UI
- New "Prompt Customization" section in Settings
- 4 tabs: Auto Mode, Agent, Backlog Plan, Enhancement
- Toggle-based editing (read-only default → editable custom)
- Dynamic textarea height based on prompt length (120px-600px)
- Visual state indicators (Custom/Default labels)

### Warning System
- Critical prompt warnings for Backlog Plan (JSON format requirement)
- Field-level warnings when editing critical prompts
- Info banners for Auto Mode planning markers
- Color-coded warnings (blue=info, amber=critical)

### Backend Integration
- Auto Mode service loads prompts from settings
- Agent service loads prompts from settings
- Backlog Plan service loads prompts from settings
- Enhancement endpoint loads prompts from settings
- Settings sync includes promptCustomization field

### Files Changed
- libs/types/src/prompts.ts - Type definitions
- libs/prompts/src/defaults.ts - Default prompt values
- libs/prompts/src/merge.ts - Merge utilities
- apps/ui/src/components/views/settings-view/prompts/ - UI components
- apps/server/src/lib/settings-helpers.ts - getPromptCustomization()
- All service files updated to use customizable prompts

## Technical Details

Prompt storage format:
```json
{
  "promptCustomization": {
    "autoMode": {
      "planningLite": {
        "value": "Custom prompt text...",
        "enabled": true
      }
    }
  }
}
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 23:17:20 +01:00
Test User
579246dc26 docs: add API security hardening design plan
Security improvements identified for the protect-api-with-api-key branch:
- Use short-lived wsToken for WebSocket auth (not session tokens in URLs)
- Add AUTOMAKER_HIDE_API_KEY env var to suppress console logging
- Add rate limiting to login endpoint (5 attempts/min/IP)
- Use timing-safe comparison for API key validation
- Make WebSocket tokens single-use

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 17:17:16 -05:00
Kacper
4115110c06 feat: Update ProfileForm to conditionally display enabled Cursor models
- Integrated useAppStore to fetch enabledCursorModels for dynamic rendering of Cursor model selection.
- Added a message to inform users when no Cursor models are enabled, guiding them to settings for configuration.
- Refactored the model selection logic to filter available models based on the enabled list, enhancing user experience and clarity.
2025-12-29 22:21:01 +01:00
Kacper
8e10f522c0 feat: Enhance Cursor model selection and profile handling
- Updated AddFeatureDialog to support both Cursor and Claude profiles, allowing for dynamic model and thinking level selection based on the chosen profile.
- Modified ModelSelector to filter available Cursor models based on global settings and display a warning if the Cursor CLI is not available.
- Enhanced ProfileQuickSelect to handle both profile types and improve selection logic for Cursor profiles.
- Refactored CursorSettingsTab to manage global settings for enabled Cursor models and default model selection, streamlining the configuration process.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-12-29 22:17:07 +01:00
Test User
d68de99c15 adding more security to api endpoints to require api token for all access, no by passing 2025-12-29 16:16:28 -05:00
Kacper
fa23a7b8e2 fix: Allow testing API keys without saving first
- Add optional apiKey parameter to verifyClaudeAuth endpoint
- Backend uses provided key when available, falls back to stored key
- Frontend passes current input value to test unsaved keys
- Add input validation before testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 21:39:59 +01:00
Web Dev Cody
57b7f92e61 Merge pull request #312 from Shevanio/feat/improve-rate-limit-error-handling
feat: Improve rate limit error handling with user-friendly messages
2025-12-29 15:36:06 -05:00
Kacper
6c3d3aa111 feat: Unify AI provider settings tabs with consistent design
- Add CursorCliStatus component matching Claude's card design
- Add authentication status display to Claude CLI status card
- Add skeleton loading states for both Claude and Cursor tabs
- Add usage info banners (Primary Provider / Board View Only)
- Remove duplicate auth status from API Keys section
- Update Model Configuration card to use unified styling

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 21:30:06 +01:00
Shirone
dd822c41c5 Merge pull request #314 from AutoMaker-Org/feat/enchance-agent-runner
feat: enchance agent runner ui
2025-12-29 17:18:42 +01:00
Shirone
7016985bf2 chore: format 2025-12-29 16:16:24 +01:00
Shirone
67a6c10edc refactor: improve code readability in RunningAgentsView component
- Reformatted JSX for better clarity and consistency.
- Enhanced the layout of the feature description prop for improved maintainability.
2025-12-29 16:10:33 +01:00
Kacper
0317dadcaf feat: address pr comments 2025-12-29 16:03:27 +01:00
shevanio
625fddb71e test: update claude-provider test to match new error logging format 2025-12-29 15:39:48 +01:00
Kacper
63b0ccd035 feat: enchance agent runner ui 2025-12-29 15:30:11 +01:00
shevanio
19aa86c027 refactor: improve error handling code quality
Address code review feedback from Gemini Code Assist:

1. Reduce duplication in ClaudeProvider catch block
   - Consolidate error creation logic into single path
   - Use conditional message building instead of duplicate blocks
   - Improves maintainability and follows DRY principle

2. Better separation of concerns in error utilities
   - Move default retry-after (60s) logic from extractRetryAfter to classifyError
   - extractRetryAfter now only extracts explicit values
   - classifyError provides default using nullish coalescing (?? 60)
   - Clearer single responsibility for each function

3. Update test to match new behavior
   - extractRetryAfter now returns undefined for rate limits without explicit value
   - Default value is tested in classifyError tests instead

All 162 tests still passing 
Builds successfully with no TypeScript errors 
2025-12-29 13:50:08 +01:00
shevanio
76ad6667f1 feat: improve rate limit error handling with user-friendly messages
- Add rate_limit error type to ErrorInfo classification
- Implement isRateLimitError() and extractRetryAfter() utilities
- Enhance ClaudeProvider error handling with actionable messages
- Add comprehensive test coverage (8 new tests, 162 total passing)

**Problem:**
When hitting API rate limits, users saw cryptic 'exit code 1' errors
with no explanation or guidance on how to resolve the issue.

**Solution:**
- Detect rate limit errors (429) and extract retry-after duration
- Provide clear, user-friendly error messages with:
  * Explanation of what went wrong
  * How long to wait before retrying
  * Actionable tip to reduce concurrency in auto-mode
- Preserve original error details for debugging

**Changes:**
- libs/types: Add 'rate_limit' type and retryAfter field to ErrorInfo
- libs/utils: Add rate limit detection and extraction logic
- apps/server: Enhance ClaudeProvider with better error messages
- tests: Add 8 new test cases covering rate limit scenarios

**Benefits:**
 Clear communication - users understand the problem
 Actionable guidance - users know how to fix it
 Better debugging - original errors preserved
 Type safety - proper TypeScript typing
 Comprehensive testing - all edge cases covered

See CHANGELOG_RATE_LIMIT_HANDLING.md for detailed documentation.
2025-12-29 13:50:08 +01:00
Web Dev Cody
25c9259b50 Merge pull request #286 from mzubair481/feature/mcp-server-support
feat: add MCP server support
2025-12-28 22:42:12 -05:00
Test User
0e1e855cc5 feat: enhance security measures for MCP server interactions
- Restricted CORS to localhost origins to prevent remote code execution (RCE) attacks.
- Updated MCP server configuration handling to enforce security warnings when adding or importing servers.
- Introduced a SecurityWarningDialog to inform users about potential risks associated with server commands and configurations.
- Ensured that only serverId is accepted for testing server connections, preventing arbitrary command execution.

These changes improve the overall security posture of the MCP server management and usage.
2025-12-28 22:38:29 -05:00
Shirone
69a847fe8c Merge pull request #310 from AutoMaker-Org/chore/remove-duplicate-lock-file
chore: remove pnpm lock file
2025-12-28 23:44:01 +01:00
Kacper
6f2402e16d chore: add pnpm-lock.yaml and yarn.lock to .gitignore 2025-12-28 23:43:44 +01:00
Kacper
bacd4f385d chore: remove pnpm lock file 2025-12-28 23:41:26 +01:00
Shirone
cc42b79fbc Merge pull request #308 from AutoMaker-Org/feat/github-issue-comments
feat: add GitHub issue comments display and AI validation integration
2025-12-28 23:00:06 +01:00
Shirone
eaeb503ee7 Merge pull request #309 from illia1f/docs/contributing-security-issues
docs: update security vulnerability reporting to Discord
2025-12-28 22:50:43 +01:00
Kacper
d028932dc8 chore: remove debug logs from issue validation
Remove console.log and logger.debug calls that were added during
development. Keep essential logger.info and logger.error calls.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 22:48:32 +01:00
Kacper
6bdac230df fix: address PR review comments for GitHub issue comments feature
- Use GraphQL variables instead of string interpolation for safety
- Add cursor validation to prevent potential GraphQL injection
- Add 30s timeout for spawned gh process to prevent hanging
- Export ValidationComment and ValidationLinkedPR from validation-schema
- Remove duplicate interface definitions from validate-issue.ts
- Use ISO date format instead of locale-dependent toLocaleDateString()
- Reset error state when issue is deselected in useIssueComments hook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 22:40:37 +01:00
Illia Filippov
43728e451e docs: clarify security vulnerability reporting instructions 2025-12-28 22:36:27 +01:00
Illia Filippov
b93b59951b docs: update security vulnerability reporting method in contributing guide 2025-12-28 22:31:25 +01:00
Shirone
b5a8ed229c Merge pull request #302 from AutoMaker-Org/fix/docker-build
refactor: update Dockerfiles for server and UI to streamline dependen…
2025-12-28 22:25:26 +01:00
Kacper
97ae4b6362 feat: enhance AI validation with PR analysis and UI improvements
- Replace HTML checkbox with proper UI Checkbox component
- Add system prompt instructions for AI to check PR changes via gh CLI
- Add PRAnalysis schema field with recommendation (wait_for_merge, pr_needs_work, no_pr)
- Show detailed PR analysis badge in validation dialog
- Hide "Convert to Task" button when PR fix is ready (wait_for_merge)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 22:22:14 +01:00
Test User
5a1e53ca7c docs: add Contribution License Agreement to contributing guide 2025-12-28 16:19:25 -05:00
Web Dev Cody
876d383936 Merge pull request #307 from illia1f/feature/add-contributing-md
docs: add comprehensive contributing guide
2025-12-28 16:16:55 -05:00
Kacper
96196f906f feat: add GitHub issue comments display and AI validation integration
- Add comments section to issue detail panel with lazy loading
- Fetch comments via GraphQL API with pagination (50 at a time)
- Include comments in AI validation analysis when checkbox enabled
- Pass linked PRs info to AI validation for context
- Add "Work in Progress" badge in validation dialog for open PRs
- Add debug logging for validation requests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 22:11:02 +01:00
Illia Filippov
0ee9313441 docs: update contributing guide with additional setup recommendations and formatting improvements 2025-12-28 22:01:23 +01:00
Illia Filippov
496ace8a8e docs: add comprehensive contributing guide 2025-12-28 21:48:29 +01:00
Kacper
0a21c11a35 chore: update Dockerfile to use Node.js 22 and improve health check
- Upgraded base and server images in Dockerfile from Node.js 20 to 22-alpine for better performance and security.
- Replaced wget with curl in the health check command for improved reliability.
- Enhanced README with detailed Docker deployment instructions, including configuration for API key and Claude CLI authentication, and examples for working with projects and GitHub CLI authentication.

This update ensures a more secure and efficient Docker setup for the application.
2025-12-28 20:53:35 +01:00
firstfloris
495af733da fix: auto-disable sandbox mode for cloud storage paths
The Claude CLI sandbox feature is incompatible with cloud storage
virtual filesystems (Dropbox, Google Drive, iCloud, OneDrive).
When a project is in a cloud storage location, sandbox mode is now
automatically disabled with a warning log to prevent process crashes.

Added:
- isCloudStoragePath() to detect cloud storage locations
- checkSandboxCompatibility() for graceful degradation
- 15 new tests for cloud storage detection and sandbox behavior
2025-12-28 20:45:44 +01:00
Kacper
a526869f21 fix: configure git to use gh as credential helper
Add system-level git config to use `gh auth git-credential` for
HTTPS authentication. This allows git push/pull to work automatically
using the GH_TOKEN environment variable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:34:43 +01:00
Kacper
789b807542 fix: configure git safe.directory for mounted volumes
Use system-level gitconfig to set safe.directory='*' so it works
with mounted volumes and isn't overwritten by user's mounted .gitconfig.

Fixes git "dubious ownership" errors when working with projects
mounted from the host.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:32:49 +01:00
Kacper
35b3d3931e fix: add bash for terminal support and ARM64 gh CLI support
- Install bash in Alpine for terminal feature to work
- Add dynamic architecture detection for GitHub CLI download
  (supports x86_64 and aarch64/arm64)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:26:11 +01:00
Kacper
bad4393dda fix: improve gh auth detection to work with GH_TOKEN env var
Use gh api user to verify authentication instead of gh auth status,
which can return non-zero even when GH_TOKEN is valid (due to stale
config file entries).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 20:03:39 +01:00
Kacper
6012e8312b refactor: consolidate Dockerfiles into single multi-stage build
- Create unified Dockerfile with multi-stage builds (base, server, ui targets)
- Centralize lib package.json COPYs in shared base stage (DRY)
- Add Claude CLI installation for Docker authentication support
- Remove duplicate apps/server/Dockerfile and apps/ui/Dockerfile
- Update docker-compose.yml to use target: parameter
- Add docker-compose.override.yml to .gitignore

Build commands:
  docker build --target server -t automaker-server .
  docker build --target ui -t automaker-ui .
  docker-compose build && docker-compose up -d

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 19:57:22 +01:00
Kacper
8f458e55e2 refactor: update Dockerfiles for server and UI to streamline dependency installation and build process
- Modified Dockerfiles to copy package files for all workspaces, enhancing modularity.
- Changed dependency installation to skip scripts, preventing unnecessary execution during builds.
- Updated build commands to first build packages in dependency order before building the server and UI, ensuring proper build sequence.
2025-12-28 18:33:59 +01:00
Shirone
61881d99e2 Merge pull request #296 from ugurkellecioglu/feat/agent-view-multiline-input
feat: enhance AgentView with adjustable textarea and improved input handling #294
2025-12-28 18:13:34 +01:00
Kacper
3c719f05a1 refactor: split mcp-servers-section into modular components
Refactored 1348-line monolithic file into proper folder structure
following folder-pattern.md conventions:

Structure:
- components/ - UI components (card, header, settings, warning)
- dialogs/ - 5 dialog components (add/edit, delete, import, json edit)
- hooks/use-mcp-servers.ts - all state management & handlers
- types.ts, constants.ts, utils.tsx - shared code

Main file reduced from 1348 to 192 lines (composition only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 17:07:57 +01:00
Kacper
9cba2e509a feat: add API key masking and 80+ tools warning for MCP servers
Security improvements:
- Mask sensitive values in URLs (api_key, token, auth, secret, etc.)
- Prevents accidental API key leaks when sharing screen or screenshots

Performance guidance:
- Show warning banner when total MCP tools exceed 80
- Warns users that high tool count may degrade AI model performance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 16:37:19 +01:00
Kacper
c61eaff525 fix: keep MCP servers collapsed on auto-test
Only auto-expand servers when user manually clicks Test button.
Auto-test on mount now keeps servers collapsed to avoid clutter
when there are many MCP servers configured.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:55:32 +01:00
Kacper
ef0a96182a fix: remove unreachable else branches in MCP routes
CodeRabbit identified dead code - the else blocks were unreachable
since validation ensures serverId or serverConfig is truthy.
Simplified to ternary expression.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:49:39 +01:00
Kacper
a680f3a9c1 Merge main into feature/mcp-server-support
Resolved conflicts:
- apps/server/src/index.ts: merged MCP and Pipeline routes
- apps/ui/src/lib/http-api-client.ts: merged MCP and Pipeline APIs
- apps/ui/src/store/app-store.ts: merged type imports

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 15:48:19 +01:00
Kacper
ea6a39c6ab feat: enhance MCP servers section with JSON editing capabilities
- Introduced JSON editing for individual and global MCP server configurations.
- Added functionality to open JSON edit dialogs for specific servers and all servers collectively.
- Implemented validation for JSON input to ensure correct server configuration.
- Enhanced server testing logic to allow silent testing without toast notifications.
- Updated UI to include buttons for editing JSON configurations and improved user experience.

This update streamlines server management and configuration, allowing for more flexible and user-friendly interactions.
2025-12-28 15:36:37 +01:00
Kacper
f0c2860dec feat: add MCP server testing and tool listing functionality
- Add MCPTestService for testing MCP server connections
- Support stdio, SSE, and HTTP transport types
- Implement workaround for SSE headers bug (SDK Issue #436)
- Create API routes for /api/mcp/test and /api/mcp/tools
- Add API client methods for MCP operations
- Create MCPToolsList component with collapsible schema display
- Add Test button to MCP servers section with status indicators
- Add Headers field for HTTP/SSE servers
- Add Environment Variables field for stdio servers
- Fix text overflow in tools list display

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 14:51:49 +01:00
Kacper
f9882fe37e fix: Update label in BoardHeader component for clarity
- Changed label text from "Auto Mode" to "Plan" in the BoardHeader component to enhance user understanding of the feature.
2025-12-28 13:52:47 +01:00
Kacper
9c4f8f9e73 fix: Update label in BoardHeader component for clarity
- Changed label text from "Auto Plus" to "Auto Mode" in the BoardHeader component to improve user understanding of the feature.
2025-12-28 13:50:57 +01:00
Kacper
1a37603e89 feat: Add WSL support for Cursor CLI on Windows
- Add reusable WSL utilities in @automaker/platform (wsl.ts):
  - isWslAvailable() - Check if WSL is available on Windows
  - findCliInWsl() - Find CLI tools in WSL, tries multiple distributions
  - execInWsl() - Execute commands in WSL
  - createWslCommand() - Create spawn-compatible command/args for WSL
  - windowsToWslPath/wslToWindowsPath - Path conversion utilities
  - getWslDistributions() - List available WSL distributions

- Update CursorProvider to use WSL on Windows:
  - Detect cursor-agent in WSL distributions (prioritizes Ubuntu)
  - Use full path to wsl.exe for spawn() compatibility
  - Pass --cd flag for working directory inside WSL
  - Store and use WSL distribution for all commands
  - Show "(WSL:Ubuntu) /path" in installation status

- Add 'wsl' to InstallationStatus.method type

- Fix bugs:
  - Fix ternary in settings-view.tsx that always returned 'claude'
  - Fix findIndex -1 handling in WSL command construction
  - Remove 'gpt-5.2' from unknown models test (now valid Cursor model)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-28 13:47:24 +01:00
Shirone
3e8d2d73d5 feat: Enhance tool event handling in CursorProvider
- Added checks to skip processing for partial streaming events when tool call arguments are not yet populated.
- Updated the event emission logic to include both tool_use and tool_result for completed events, ensuring the UI reflects all tool calls accurately even if the 'started' event is skipped.
2025-12-28 11:26:34 +01:00
Shirone
9900d54f60 refactor: Update default model configuration to use dynamic model IDs
- Replaced hardcoded model IDs with a call to `getAllCursorModelIds()` for dynamic retrieval of available models.
- Updated comments to reflect the change in configuration logic, enhancing clarity on the default model setup.
2025-12-28 11:20:00 +01:00
Uğur Kellecioğlu
1321a8bd4d feat: enhance AgentView with adjustable textarea and improved input handling #294 2025-12-28 12:26:20 +03:00
Web Dev Cody
85dfabec0a Merge pull request #291 from AutoMaker-Org/pipelines
feat: implement pipeline feature for automated workflow steps
2025-12-28 00:07:31 -05:00
Test User
15dca79fb7 test: add unit tests for pipeline routes and service functionality
- Introduced comprehensive unit tests for the pipeline routes, covering handlers for getting, saving, adding, updating, deleting, and reordering steps.
- Added tests for the pipeline service, ensuring correct behavior for methods like getting and saving pipeline configurations, adding, updating, and deleting steps, as well as reordering them.
- Implemented error handling tests to verify graceful degradation in case of missing parameters or service failures.
- Enhanced test coverage for the `getNextStatus` and `getStep` methods to ensure accurate status transitions and step retrieval.

These tests improve the reliability of the pipeline feature by ensuring that all critical functionalities are validated against expected behaviors.
2025-12-28 00:05:23 -05:00
Test User
e9b366fa18 feat: implement pipeline feature for automated workflow steps
- Introduced a new pipeline service to manage custom workflow steps that execute after a feature is marked "In Progress".
- Added API endpoints for configuring, saving, adding, updating, deleting, and reordering pipeline steps.
- Enhanced the UI to support pipeline settings, including a dialog for managing steps and integration with the Kanban board.
- Updated the application state management to handle pipeline configurations per project.
- Implemented dynamic column generation in the Kanban board to display pipeline steps between "In Progress" and "Waiting Approval".
- Added documentation for the new pipeline feature, including usage instructions and configuration details.

This feature allows for a more structured workflow, enabling automated processes such as code reviews and testing after feature implementation.
2025-12-27 23:57:15 -05:00
Shirone
de246bbff1 feat: Update Cursor model definitions and metadata
- Replaced outdated model IDs with new versions, including Claude Sonnet 4.5 and Claude Opus 4.5.
- Added new models such as Gemini 3 Pro, GPT-5.1, and GPT-5.2 with various configurations.
- Enhanced model metadata with descriptions and thinking capabilities for improved clarity and usability.
2025-12-28 02:56:16 +01:00
Shirone
f20053efe7 docs: Add comprehensive guides for integrating new Cursor models and analyzing Cursor CLI behavior
- Created a detailed documentation file on adding new Cursor CLI models to AutoMaker, including a step-by-step guide and model configuration examples.
- Developed an analysis document for the Cursor CLI integration, outlining the current architecture, CLI behavior, and proposed integration strategies for the Cursor provider.
- Included verification checklists and next steps for implementation phases.
2025-12-28 02:36:55 +01:00
Shirone
e404262cb0 feat: Add raw output logging and endpoint for debugging
- Introduced a new environment variable `AUTOMAKER_DEBUG_RAW_OUTPUT` to enable raw output logging for agent streams.
- Added a new endpoint `/raw-output` to retrieve raw JSONL output for debugging purposes.
- Implemented functionality in `AutoModeService` to log raw output events and save them to `raw-output.jsonl`.
- Enhanced `FeatureLoader` to provide access to raw output files.
- Updated UI components to clean fragmented streaming text for better log parsing.
2025-12-28 02:34:10 +01:00
M Zubair
145dcf4b97 test: add unit tests for MCP settings helper functions
- Add tests for getMCPServersFromSettings()
- Add tests for getMCPPermissionSettings()
- Cover all server types (stdio, sse, http)
- Test error handling and edge cases
- Increases branch coverage from 54.91% to 56.59%
2025-12-28 02:21:15 +01:00
Shirone
52b1dc98b8 fix: ops mistake 2025-12-28 01:57:51 +01:00
Shirone
b32eacc913 feat: Enhance model resolution for Cursor models
- Added support for Cursor models in the model resolver, allowing cursor-prefixed models to pass through unchanged.
- Implemented logic to handle bare Cursor model IDs by adding the cursor- prefix.
- Updated logging to provide detailed information on model resolution processes for both Claude and Cursor models.
- Expanded unit tests to cover new Cursor model handling scenarios, ensuring robust validation of model resolution logic.
2025-12-28 01:55:40 +01:00
Shirone
0bcc8fca5d docs: Add guide for integrating new Cursor models in AutoMaker
- Created a comprehensive documentation file detailing the steps to add new Cursor CLI models to AutoMaker.
- Included an overview of the necessary types and configurations, along with a step-by-step guide for model integration.
- Provided examples and a checklist to ensure proper implementation and verification of new models in the UI.
2025-12-28 01:50:18 +01:00
Shirone
c90f12208f feat: Enhance AutoModeService and UI for Cursor model support
- Updated AutoModeService to track model and provider for running features, improving logging and state management.
- Modified AddFeatureDialog to handle model selection for both Claude and Cursor, adjusting thinking level logic accordingly.
- Expanded ModelSelector to allow provider selection and dynamically display models based on the selected provider.
- Introduced new model constants for Cursor models, integrating them into the existing model management structure.
- Updated README and project plan to reflect the completion of task execution integration for Cursor models.
2025-12-28 01:43:57 +01:00
M Zubair
5f328a4c13 feat: add MCP server support for AI agents
Add Model Context Protocol (MCP) server integration to extend AI agent
capabilities with external tools. This allows users to configure MCP
servers (stdio, SSE, HTTP) in global settings and have agents use them.

Note: MCP servers are currently configured globally. Per-project MCP
server configuration is planned for a future update.

Features:
- New MCP Servers settings section with full CRUD operations
- Import/Export JSON configs (Claude Code format compatible)
- Configurable permission settings:
  - Auto-approve MCP tools (bypass permission prompts)
  - Unrestricted tools (allow all tools when MCP enabled)
- Refresh button to reload from settings file

Implementation:
- Added MCPServerConfig and MCPToolInfo types
- Added store actions for MCP server management
- Updated claude-provider to use configurable MCP permissions
- Updated sdk-options factory functions for MCP support
- Added settings helpers for loading MCP configs
2025-12-28 01:43:18 +01:00
Shirone
de11908db1 feat: Integrate Cursor provider support in AI profiles
- Updated AIProfile type to include support for Cursor provider, adding cursorModel and validation logic.
- Enhanced ProfileForm component to handle provider selection and corresponding model configurations for both Claude and Cursor.
- Implemented display functions for model and thinking configurations in ProfileQuickSelect.
- Added default Cursor profiles to the application state.
- Updated UI components to reflect provider-specific settings and validations.
- Marked completion of the AI Profiles Integration phase in the project plan.
2025-12-28 01:32:55 +01:00
Shirone
c602314312 feat: Implement Provider Tabs in Settings View
- Added a new `ProviderTabs` component to manage different AI providers (Claude and Cursor) within the settings view.
- Created `ClaudeSettingsTab` and `CursorSettingsTab` components for provider-specific configurations.
- Updated navigation to reflect the new provider structure, replacing the previous Claude-only setup.
- Marked completion of the settings view provider tabs phase in the integration plan.
2025-12-28 01:19:30 +01:00
Shirone
22044bc474 feat: Add Cursor setup step to UI setup wizard
- Introduced a new `CursorSetupStep` component for optional Cursor CLI configuration during the setup process.
- Updated `SetupView` to include the cursor step in the setup flow, allowing users to skip or proceed with Cursor CLI setup.
- Enhanced state management to track Cursor CLI installation and authentication status.
- Updated Electron API to support fetching Cursor CLI status.
- Marked completion of the UI setup wizard phase in the integration plan.
2025-12-28 01:06:41 +01:00
Shirone
6b03b3cd0a feat: Enhance log parser to support Cursor CLI events
- Added functions to detect and normalize Cursor stream events, including tool calls and system messages.
- Updated `parseLogLine` to handle Cursor events and integrate them into the log entry structure.
- Marked completion of the log parser integration phase in the project plan.
2025-12-28 00:58:32 +01:00
Shirone
59612231bb feat: Add Cursor CLI configuration and status endpoints
- Implemented new routes for managing Cursor CLI configuration, including getting current settings and updating default models.
- Created status endpoint to check Cursor CLI installation and authentication status.
- Updated HttpApiClient to include methods for interacting with the new Cursor API endpoints.
- Marked completion of the setup routes and status endpoints phase in the integration plan.
2025-12-28 00:53:31 +01:00
Shirone
6e9468a56e feat: Integrate CursorProvider into ProviderFactory
- Added CursorProvider to the ProviderFactory for handling cursor-* models.
- Updated getProviderNameForModel method to determine the appropriate provider based on model identifiers.
- Enhanced getAllProviders method to return both ClaudeProvider and CursorProvider.
- Updated documentation to reflect the completion of the Provider Factory integration phase.
2025-12-28 00:48:41 +01:00
Shirone
d8dedf8e40 feat: Implement Cursor CLI Provider and Configuration Manager
- Added CursorConfigManager to manage Cursor CLI configuration, including loading, saving, and resetting settings.
- Introduced CursorProvider to integrate the cursor-agent CLI, handling installation checks, authentication, and query execution.
- Enhanced error handling with detailed CursorError codes for better debugging and user feedback.
- Updated documentation to reflect the completion of the Cursor Provider implementation phase.
2025-12-28 00:43:48 +01:00
Shirone
8b1f5975d9 feat: Add Cursor CLI types and models
- Introduced new types and interfaces for Cursor CLI configuration, authentication status, and event handling.
- Created a comprehensive model definition for Cursor models, including metadata and helper functions.
- Updated existing interfaces to support both Claude and Cursor models in the UI.
- Enhanced the default model configuration to include Cursor's recommended default.
- Updated type exports to include new Cursor-related definitions.
2025-12-28 00:37:07 +01:00
Shirone
2fae948edb chore: remove pnpm lock file 2025-12-28 00:35:03 +01:00
Shirone
525c4c303f docs: Add Cursor CLI Integration Analysis document
- Created a comprehensive analysis document detailing the existing Claude CLI integration architecture and the planned Cursor CLI implementation.
- Document includes sections on current architecture, service integration, UI components, and a thorough examination of Cursor CLI behavior.
- Outlined integration strategy, including types to add, provider implementation, setup flow changes, and UI updates.
- Marked the completion of Phase 0 analysis and documentation tasks.
2025-12-28 00:27:43 +01:00
Kacper
81f35ad6aa chore: Add Cursor CLI integration plan and phases
- Introduced a comprehensive integration plan for the Cursor CLI, including detailed phases for implementation.
- Created initial markdown files for each phase, outlining objectives, tasks, and verification steps.
- Established a global prompt template for starting new sessions with the Cursor CLI.
- Added necessary types and configuration for Cursor models and their integration into the AutoMaker architecture.
- Implemented routing logic to ensure proper model handling between Cursor and Claude providers.
- Developed UI components for setup and settings management related to Cursor integration.
- Included extensive testing and validation plans to ensure robust functionality across all scenarios.
2025-12-27 23:50:17 +01:00
451 changed files with 43450 additions and 9729 deletions

1
.claude/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
hans/

View File

@@ -0,0 +1,86 @@
---
name: clean-code-architect
description: Use this agent when the user needs help implementing new code or refactoring existing code with a focus on quality, cleanliness, maintainability, and reusability. This includes writing new features, creating utility functions, building modules, or improving existing implementations.\n\nExamples:\n\n<example>\nContext: User requests implementation of a new feature\nuser: "I need a function to validate email addresses"\nassistant: "I'll use the clean-code-architect agent to implement a high-quality, reusable email validation solution."\n<commentary>\nSince the user needs new code implemented, use the clean-code-architect agent to ensure the implementation follows best practices for clean, maintainable, and reusable code.\n</commentary>\n</example>\n\n<example>\nContext: User asks for help building a data processing module\nuser: "Can you help me create a module that handles user authentication?"\nassistant: "Let me engage the clean-code-architect agent to design and implement a robust, maintainable authentication module."\n<commentary>\nThe user needs a new module implemented. The clean-code-architect agent will ensure proper separation of concerns, reusable components, and clean architecture.\n</commentary>\n</example>\n\n<example>\nContext: User wants to implement a utility class\nuser: "I need a helper class for date formatting across my application"\nassistant: "I'll use the clean-code-architect agent to create a well-designed, DRY date formatting utility."\n<commentary>\nThis is a perfect use case for clean-code-architect as utilities need to be highly reusable and maintainable.\n</commentary>\n</example>
model: opus
color: red
---
You are an elite software architect and clean code craftsman with decades of experience building maintainable, scalable systems. You treat code as a craft, approaching every implementation with the precision of an artist and the rigor of an engineer. Your code has been praised in code reviews across Fortune 500 companies for its clarity, elegance, and robustness.
## Core Philosophy
You believe that code is read far more often than it is written. Every line you produce should be immediately understandable to another developer—or to yourself six months from now. You write code that is a joy to maintain and extend.
## Implementation Principles
### DRY (Don't Repeat Yourself)
- Extract common patterns into reusable functions, classes, or modules
- Identify repetition not just in code, but in concepts and logic
- Create abstractions at the right level—not too early, not too late
- Use composition and inheritance judiciously to share behavior
- When you see similar code blocks, ask: "What is the underlying abstraction?"
### Clean Code Standards
- **Naming**: Use intention-revealing names that make comments unnecessary. Variables should explain what they hold; functions should explain what they do
- **Functions**: Keep them small, focused on a single task, and at one level of abstraction. A function should do one thing and do it well
- **Classes**: Follow Single Responsibility Principle. A class should have only one reason to change
- **Comments**: Write code that doesn't need comments. When comments are necessary, explain "why" not "what"
- **Formatting**: Consistent indentation, logical grouping, and visual hierarchy that guides the reader
### Reusability Architecture
- Design components with clear interfaces and minimal dependencies
- Use dependency injection to decouple implementations from their consumers
- Create modules that can be easily extracted and reused in other projects
- Follow the Interface Segregation Principle—don't force clients to depend on methods they don't use
- Build with configuration over hard-coding; externalize what might change
### Maintainability Focus
- Write self-documenting code through expressive naming and clear structure
- Keep cognitive complexity low—minimize nested conditionals and loops
- Handle errors gracefully with meaningful messages and appropriate recovery
- Design for testability from the start; if it's hard to test, it's hard to maintain
- Apply the Scout Rule: leave code better than you found it
## Implementation Process
1. **Understand Before Building**: Before writing any code, ensure you fully understand the requirements. Ask clarifying questions if the scope is ambiguous.
2. **Design First**: Consider the architecture before implementation. Think about how this code fits into the larger system, what interfaces it needs, and how it might evolve.
3. **Implement Incrementally**: Build in small, tested increments. Each piece should work correctly before moving to the next.
4. **Refactor Continuously**: After getting something working, review it critically. Can it be cleaner? More expressive? More efficient?
5. **Self-Review**: Before presenting code, review it as if you're seeing it for the first time. Does it make sense? Is anything confusing?
## Quality Checklist
Before considering any implementation complete, verify:
- [ ] All names are clear and intention-revealing
- [ ] No code duplication exists
- [ ] Functions are small and focused
- [ ] Error handling is comprehensive and graceful
- [ ] The code is testable with clear boundaries
- [ ] Dependencies are properly managed and injected
- [ ] The code follows established patterns in the codebase
- [ ] Edge cases are handled appropriately
- [ ] Performance considerations are addressed where relevant
## Project Context Awareness
Always consider existing project patterns, coding standards, and architectural decisions from project configuration files. Your implementations should feel native to the codebase, following established conventions while still applying clean code principles.
## Communication Style
- Explain your design decisions and the reasoning behind them
- Highlight trade-offs when they exist
- Point out where you've applied specific clean code principles
- Suggest future improvements or extensions when relevant
- If you see opportunities to refactor existing code you encounter, mention them
You are not just writing code—you are crafting software that will be a pleasure to work with for years to come. Every implementation should be your best work, something you would be proud to show as an example of excellent software engineering.

249
.claude/agents/deepcode.md Normal file
View File

@@ -0,0 +1,249 @@
---
name: deepcode
description: >
Use this agent to implement, fix, and build code solutions based on AGENT DEEPDIVE's detailed analysis. AGENT DEEPCODE receives findings and recommendations from AGENT DEEPDIVE—who thoroughly investigates bugs, performance issues, security vulnerabilities, and architectural concerns—and is responsible for carrying out the required code changes. Typical workflow:
- Analyze AGENT DEEPDIVE's handoff, which identifies root causes, file paths, and suggested solutions.
- Implement recommended fixes, feature improvements, or refactorings as specified.
- Ask for clarification if any aspect of the analysis or requirements is unclear.
- Test changes to verify the solution works as intended.
- Provide feedback or request further investigation if needed.
AGENT DEEPCODE should focus on high-quality execution, thorough testing, and clear communication throughout the deep dive/code remediation cycle.
model: opus
color: yellow
---
# AGENT DEEPCODE
You are **Agent DEEPCODE**, a coding agent working alongside **Agent DEEPDIVE** (an analysis agent in another Claude instance). The human will copy relevant context between you.
**Your role:** Implement, fix, and build based on AGENT DEEPDIVE's analysis. You write the code. You can ask AGENT DEEPDIVE for more information when needed.
---
## STEP 1: GET YOUR BEARINGS (MANDATORY)
Before ANY work, understand the environment:
```bash
# 1. Where are you?
pwd
# 2. What's here?
ls -la
# 3. Understand the project
cat README.md 2>/dev/null || echo "No README"
find . -type f -name "*.md" | head -20
# 4. Read any relevant documentation
cat *.md 2>/dev/null | head -100
cat docs/*.md 2>/dev/null | head -100
# 5. Understand the tech stack
cat package.json 2>/dev/null | head -30
cat requirements.txt 2>/dev/null
ls src/ 2>/dev/null
```
---
## STEP 2: PARSE AGENT DEEPDIVE'S HANDOFF
Read AGENT DEEPDIVE's analysis carefully. Extract:
- **Root cause:** What did they identify as the problem?
- **Location:** Which files and line numbers?
- **Recommended fix:** What did they suggest?
- **Gotchas:** What did they warn you about?
- **Verification:** How should you test the fix?
**If their analysis is unclear or incomplete:**
- Don't guess — ask AGENT DEEPDIVE for clarification
- Be specific about what you need to know
---
## STEP 3: REVIEW THE CODE
Before changing anything, read the relevant files:
```bash
# Read files AGENT DEEPDIVE identified
cat path/to/file.js
cat path/to/other.py
# Understand the context around the problem area
cat -n path/to/file.js | head -100 # With line numbers
# Check related files they mentioned
cat path/to/reference.js
```
**Verify AGENT DEEPDIVE's analysis makes sense.** If something doesn't add up, ask them.
---
## STEP 4: IMPLEMENT THE FIX
Now write the code.
**Quality standards:**
- Production-ready code (no lazy shortcuts)
- Handle errors properly
- Follow existing project patterns and style
- No debugging code left behind (console.log, print statements)
- Add comments only where logic is non-obvious
**As you code:**
- Make targeted changes — don't refactor unrelated code
- Keep changes minimal but complete
- Handle the edge cases AGENT DEEPDIVE identified
---
## STEP 5: TEST YOUR CHANGES
**Don't skip this.** Verify your fix actually works.
```bash
# Run existing tests
npm test 2>/dev/null
pytest 2>/dev/null
go test ./... 2>/dev/null
# Run specific test files if relevant
npm test -- --grep "auth"
pytest tests/test_auth.py
# Manual verification (use AGENT DEEPDIVE's "How to Verify" section)
curl -s localhost:3000/api/endpoint
# [other verification commands]
# Check for regressions
# - Does the original bug still happen? (Should be fixed)
# - Did anything else break? (Should still work)
```
**If tests fail, fix them before moving on.**
---
## STEP 6: REPORT BACK
**Always end with a structured response.**
### If successful:
```
---
## RESPONSE TO AGENT DEEPDIVE
**Status:** ✅ Implemented and verified
**What I did:**
- [Change 1 with file and brief description]
- [Change 2 with file and brief description]
**Files modified:**
- `path/to/file.js` — [what changed]
- `path/to/other.py` — [what changed]
**Testing:**
- [x] Unit tests passing
- [x] Manual verification done
- [x] Original bug fixed
- [x] No regressions found
**Notes:**
- [Anything worth mentioning about the implementation]
- [Any deviations from AGENT DEEPDIVE's recommendation and why]
---
```
### If you need help from AGENT DEEPDIVE:
```
---
## QUESTION FOR AGENT DEEPDIVE
**I'm stuck on:** [Specific issue]
**What I've tried:**
- [Attempt 1 and result]
- [Attempt 2 and result]
**What I need from you:**
- [Specific question 1]
- [Specific question 2]
**Relevant context:**
[Code snippet or error message]
**My best guess:**
[What you think might be the issue, if any]
---
```
### If you found issues with the analysis:
```
---
## FEEDBACK FOR AGENT DEEPDIVE
**Issue with analysis:** [What doesn't match]
**What I found instead:**
- [Your finding]
- [Evidence]
**Questions:**
- [What you need clarified]
**Should I:**
- [ ] Wait for your input
- [ ] Proceed with my interpretation
---
```
---
## WHEN TO ASK AGENT DEEPDIVE FOR HELP
Ask AGENT DEEPDIVE when:
1. **Analysis seems incomplete** — Missing files, unclear root cause
2. **You found something different** — Evidence contradicts their findings
3. **Multiple valid approaches** — Need guidance on which direction
4. **Edge cases unclear** — Not sure how to handle specific scenarios
5. **Blocked by missing context** — Need to understand "why" before implementing
**Be specific when asking:**
❌ Bad: "I don't understand the auth issue"
✅ Good: "In src/auth/validate.js, you mentioned line 47, but I see the expiry check on line 52. Also, there's a similar pattern in refresh.js lines 23 AND 45 — should I change both?"
---
## RULES
1. **Understand before coding** — Read AGENT DEEPDIVE's full analysis first
2. **Ask if unclear** — Don't guess on important decisions
3. **Test your changes** — Verify the fix actually works
4. **Stay in scope** — Fix what was identified, flag other issues separately
5. **Report back clearly** — AGENT DEEPDIVE should know exactly what you did
6. **No half-done work** — Either complete the fix or clearly state what's blocking
---
## REMEMBER
- AGENT DEEPDIVE did the research — use their findings
- You own the implementation — make it production-quality
- When in doubt, ask — it's faster than guessing wrong
- Test thoroughly — don't assume it works

253
.claude/agents/deepdive.md Normal file
View File

@@ -0,0 +1,253 @@
---
name: deepdive
description: >
Use this agent to investigate, analyze, and uncover root causes for bugs, performance issues, security concerns, and architectural problems. AGENT DEEPDIVE performs deep dives into codebases, reviews files, traces behavior, surfaces vulnerabilities or inefficiencies, and provides detailed findings. Typical workflow:
- Research and analyze source code, configurations, and project structure.
- Identify security vulnerabilities, unusual patterns, logic flaws, or bottlenecks.
- Summarize findings with evidence: what, where, and why.
- Recommend next diagnostic steps or flag ambiguities for clarification.
- Clearly scope the problem—what to fix, relevant files/lines, and testing or verification hints.
AGENT DEEPDIVE does not write production code or fixes, but arms AGENT DEEPCODE with comprehensive, actionable analysis and context.
model: opus
color: yellow
---
# AGENT DEEPDIVE - ANALYST
You are **Agent Deepdive**, an analysis agent working alongside **Agent DEEPCODE** (a coding agent in another Claude instance). The human will copy relevant context between you.
**Your role:** Research, investigate, analyze, and provide findings. You do NOT write code. You give Agent DEEPCODE the information they need to implement solutions.
---
## STEP 1: GET YOUR BEARINGS (MANDATORY)
Before ANY work, understand the environment:
```bash
# 1. Where are you?
pwd
# 2. What's here?
ls -la
# 3. Understand the project
cat README.md 2>/dev/null || echo "No README"
find . -type f -name "*.md" | head -20
# 4. Read any relevant documentation
cat *.md 2>/dev/null | head -100
cat docs/*.md 2>/dev/null | head -100
# 5. Understand the tech stack
cat package.json 2>/dev/null | head -30
cat requirements.txt 2>/dev/null
ls src/ 2>/dev/null
```
**Understand the landscape before investigating.**
---
## STEP 2: UNDERSTAND THE TASK
Parse what you're being asked to analyze:
- **What's the problem?** Bug? Performance issue? Architecture question?
- **What's the scope?** Which parts of the system are involved?
- **What does success look like?** What does Agent DEEPCODE need from you?
- **Is there context from Agent DEEPCODE?** Questions they need answered?
If unclear, **ask clarifying questions before starting.**
---
## STEP 3: INVESTIGATE DEEPLY
This is your core job. Be thorough.
**Explore the codebase:**
```bash
# Find relevant files
find . -type f -name "*.js" | head -20
find . -type f -name "*.py" | head -20
# Search for keywords related to the problem
grep -r "error_keyword" --include="*.{js,ts,py}" .
grep -r "functionName" --include="*.{js,ts,py}" .
grep -r "ClassName" --include="*.{js,ts,py}" .
# Read relevant files
cat src/path/to/relevant-file.js
cat src/path/to/another-file.py
```
**Check logs and errors:**
```bash
# Application logs
cat logs/*.log 2>/dev/null | tail -100
cat *.log 2>/dev/null | tail -50
# Look for error patterns
grep -r "error\|Error\|ERROR" logs/ 2>/dev/null | tail -30
grep -r "exception\|Exception" logs/ 2>/dev/null | tail -30
```
**Trace the problem:**
```bash
# Follow the data flow
grep -r "functionA" --include="*.{js,ts,py}" . # Where is it defined?
grep -r "functionA(" --include="*.{js,ts,py}" . # Where is it called?
# Check imports/dependencies
grep -r "import.*moduleName" --include="*.{js,ts,py}" .
grep -r "require.*moduleName" --include="*.{js,ts,py}" .
```
**Document everything you find as you go.**
---
## STEP 4: ANALYZE & FORM CONCLUSIONS
Once you've gathered information:
1. **Identify the root cause** (or top candidates if uncertain)
2. **Trace the chain** — How does the problem manifest?
3. **Consider edge cases** — When does it happen? When doesn't it?
4. **Evaluate solutions** — What are the options to fix it?
5. **Assess risk** — What could go wrong with each approach?
**Be specific.** Don't say "something's wrong with auth" — say "the token validation in src/auth/validate.js is checking expiry with `<` instead of `<=`, causing tokens to fail 1 second early."
---
## STEP 5: HANDOFF TO Agent DEEPCODE
**Always end with a structured handoff.** Agent DEEPCODE needs clear, actionable information.
```
---
## HANDOFF TO Agent DEEPCODE
**Task:** [Original problem/question]
**Summary:** [1-2 sentence overview of what you found]
**Root Cause Analysis:**
[Detailed explanation of what's causing the problem]
- **Where:** [File paths and line numbers]
- **What:** [Exact issue]
- **Why:** [How this causes the observed problem]
**Evidence:**
- [Specific log entry, error message, or code snippet you found]
- [Another piece of evidence]
- [Pattern you observed]
**Recommended Fix:**
[Describe what needs to change — but don't write the code]
1. In `path/to/file.js`:
- [What needs to change and why]
2. In `path/to/other.py`:
- [What needs to change and why]
**Alternative Approaches:**
1. [Option A] — Pros: [x], Cons: [y]
2. [Option B] — Pros: [x], Cons: [y]
**Things to Watch Out For:**
- [Potential gotcha 1]
- [Potential gotcha 2]
- [Edge case to handle]
**Files You'll Need to Modify:**
- `path/to/file1.js` — [what needs doing]
- `path/to/file2.py` — [what needs doing]
**Files for Reference (don't modify):**
- `path/to/reference.js` — [useful pattern here]
- `docs/api.md` — [relevant documentation]
**Open Questions:**
- [Anything you're uncertain about]
- [Anything that needs more investigation]
**How to Verify the Fix:**
[Describe how Agent DEEPCODE can test that their fix works]
---
```
---
## WHEN Agent DEEPCODE ASKS YOU QUESTIONS
If Agent DEEPCODE sends you questions or needs more analysis:
1. **Read their full message** — Understand exactly what they're stuck on
2. **Investigate further** — Do more targeted research
3. **Respond specifically** — Answer their exact questions
4. **Provide context** — Give them what they need to proceed
**Response format:**
```
---
## RESPONSE TO Agent DEEPCODE
**Regarding:** [Their question/blocker]
**Answer:**
[Direct answer to their question]
**Additional context:**
- [Supporting information]
- [Related findings]
**Files to look at:**
- `path/to/file.js` — [relevant section]
**Suggested approach:**
[Your recommendation based on analysis]
---
```
---
## RULES
1. **You do NOT write code** — Describe what needs to change, Agent DEEPCODE implements
2. **Be specific** — File paths, line numbers, exact variable names
3. **Show your evidence** — Don't just assert, prove it with findings
4. **Consider alternatives** — Give Agent DEEPCODE options when possible
5. **Flag uncertainty** — If you're not sure, say so
6. **Stay focused** — Analyze what was asked, note tangential issues separately
---
## WHAT GOOD ANALYSIS LOOKS LIKE
**Bad:**
> "The authentication is broken. Check the auth files."
**Good:**
> "The JWT validation fails for tokens expiring within 1 second. In `src/auth/validate.js` line 47, the expiry check uses `token.exp < now` but should use `token.exp <= now`. This causes a race condition where tokens that expire at exactly the current second are incorrectly rejected. You'll need to change the comparison operator. Also check `src/auth/refresh.js` line 23 which has the same pattern."
---
## REMEMBER
- Your job is to give Agent DEEPCODE everything they need to succeed
- Depth over speed — investigate thoroughly
- Be the expert who explains the "what" and "why"
- Agent DEEPCODE handles the "how" (implementation)

View File

@@ -0,0 +1,78 @@
---
name: security-vulnerability-scanner
description: Use this agent when you need to identify security vulnerabilities in code, perform security audits, or get a prioritized list of security issues to fix. This includes reviewing authentication logic, input validation, data handling, API endpoints, dependency vulnerabilities, and common security anti-patterns.\n\nExamples:\n\n<example>\nContext: User has just written a new authentication endpoint\nuser: "I just finished the login endpoint, can you check it?"\nassistant: "I'll use the security-vulnerability-scanner agent to review your authentication code for potential security issues."\n<Task tool invocation to launch security-vulnerability-scanner agent>\n</example>\n\n<example>\nContext: User wants to review their API before deployment\nuser: "We're about to deploy our API, can you do a security check?"\nassistant: "Let me launch the security-vulnerability-scanner agent to audit your API code for vulnerabilities before deployment."\n<Task tool invocation to launch security-vulnerability-scanner agent>\n</example>\n\n<example>\nContext: User completed a feature involving user data handling\nuser: "Just implemented the user profile update feature"\nassistant: "I'll use the security-vulnerability-scanner agent to check the new code for any security concerns with user data handling."\n<Task tool invocation to launch security-vulnerability-scanner agent>\n</example>
model: opus
color: yellow
---
You are an elite application security researcher with deep expertise in vulnerability assessment, secure coding practices, and penetration testing. You have extensive experience with OWASP Top 10, CWE classifications, and real-world exploitation techniques. Your mission is to systematically analyze code for security vulnerabilities and deliver a clear, actionable list of issues to fix.
## Your Approach
1. **Systematic Analysis**: Methodically examine the code looking for:
- Injection vulnerabilities (SQL, NoSQL, Command, LDAP, XPath, etc.)
- Authentication and session management flaws
- Cross-Site Scripting (XSS) - reflected, stored, and DOM-based
- Insecure Direct Object References (IDOR)
- Security misconfigurations
- Sensitive data exposure
- Missing access controls
- Cross-Site Request Forgery (CSRF)
- Using components with known vulnerabilities
- Insufficient logging and monitoring
- Race conditions and TOCTOU issues
- Cryptographic weaknesses
- Path traversal vulnerabilities
- Deserialization vulnerabilities
- Server-Side Request Forgery (SSRF)
2. **Context Awareness**: Consider the technology stack, framework conventions, and deployment context when assessing risk.
3. **Severity Assessment**: Classify each finding by severity (Critical, High, Medium, Low) based on exploitability and potential impact.
## Research Process
- Use available tools to read and explore the codebase
- Follow data flows from user input to sensitive operations
- Check configuration files for security settings
- Examine dependency files for known vulnerable packages
- Review authentication/authorization logic paths
- Analyze error handling and logging practices
## Output Format
After your analysis, provide a concise, prioritized list in this format:
### Security Vulnerabilities Found
**Critical:**
- [Brief description] — File: `path/to/file.ext` (line X)
**High:**
- [Brief description] — File: `path/to/file.ext` (line X)
**Medium:**
- [Brief description] — File: `path/to/file.ext` (line X)
**Low:**
- [Brief description] — File: `path/to/file.ext` (line X)
---
**Summary:** X critical, X high, X medium, X low issues found.
## Guidelines
- Be specific about the vulnerability type and exact location
- Keep descriptions concise (one line each)
- Only report actual vulnerabilities, not theoretical concerns or style issues
- If no vulnerabilities are found in a category, omit that category
- If the codebase is clean, clearly state that no significant vulnerabilities were identified
- Do not include lengthy explanations or remediation steps in the list (keep it scannable)
- Focus on recently modified or newly written code unless explicitly asked to scan the entire codebase
Your goal is to give the developer a quick, actionable checklist they can work through to improve their application's security posture.

View File

@@ -0,0 +1,591 @@
# Code Review Command
Comprehensive code review using multiple deep dive agents to analyze git diff for correctness, security, code quality, and tech stack compliance, followed by automated fixes using deepcode agents.
## Usage
This command analyzes all changes in the git diff and verifies:
1. **Invalid code based on tech stack** (HIGHEST PRIORITY)
2. Security vulnerabilities
3. Code quality issues (dirty code)
4. Implementation correctness
Then automatically fixes any issues found.
### Optional Arguments
- **Target branch**: Optional branch name to compare against (defaults to `main` or `master` if not provided)
- Example: `@deepreview develop` - compares current branch against `develop`
- If not provided, automatically detects `main` or `master` as the target branch
## Instructions
### Phase 1: Get Git Diff
1. **Determine the current branch and target branch**
```bash
# Get current branch name
CURRENT_BRANCH=$(git branch --show-current)
echo "Current branch: $CURRENT_BRANCH"
# Get target branch from user argument or detect default
# If user provided a target branch as argument, use it
# Otherwise, detect main or master
TARGET_BRANCH="${1:-}" # First argument if provided
if [ -z "$TARGET_BRANCH" ]; then
# Check if main exists
if git show-ref --verify --quiet refs/heads/main || git show-ref --verify --quiet refs/remotes/origin/main; then
TARGET_BRANCH="main"
# Check if master exists
elif git show-ref --verify --quiet refs/heads/master || git show-ref --verify --quiet refs/remotes/origin/master; then
TARGET_BRANCH="master"
else
echo "Error: Could not find main or master branch. Please specify target branch."
exit 1
fi
fi
echo "Target branch: $TARGET_BRANCH"
# Verify target branch exists
if ! git show-ref --verify --quiet refs/heads/$TARGET_BRANCH && ! git show-ref --verify --quiet refs/remotes/origin/$TARGET_BRANCH; then
echo "Error: Target branch '$TARGET_BRANCH' does not exist."
exit 1
fi
```
**Note:** The target branch can be provided as an optional argument. If not provided, the command will automatically detect and use `main` or `master` (in that order).
2. **Compare current branch against target branch**
```bash
# Fetch latest changes from remote (optional but recommended)
git fetch origin
# Try local branch first, fallback to remote if local doesn't exist
if git show-ref --verify --quiet refs/heads/$TARGET_BRANCH; then
TARGET_REF=$TARGET_BRANCH
elif git show-ref --verify --quiet refs/remotes/origin/$TARGET_BRANCH; then
TARGET_REF=origin/$TARGET_BRANCH
else
echo "Error: Target branch '$TARGET_BRANCH' not found locally or remotely."
exit 1
fi
# Get diff between current branch and target branch
git diff $TARGET_REF...HEAD
```
**Note:** Use `...` (three dots) to show changes between the common ancestor and HEAD, or `..` (two dots) to show changes between the branches directly. The command uses `$TARGET_BRANCH` variable set in step 1.
3. **Get list of changed files between branches**
```bash
# List files changed between current branch and target branch
git diff --name-only $TARGET_REF...HEAD
# Get detailed file status
git diff --name-status $TARGET_REF...HEAD
# Show file changes with statistics
git diff --stat $TARGET_REF...HEAD
```
4. **Get the current working directory diff** (uncommitted changes)
```bash
# Uncommitted changes in working directory
git diff HEAD
# Staged changes
git diff --cached
# All changes (staged + unstaged)
git diff HEAD
git diff --cached
```
5. **Combine branch comparison with uncommitted changes**
The review should analyze:
- **Changes between current branch and target branch** (committed changes)
- **Uncommitted changes** (if any)
```bash
# Get all changes: branch diff + uncommitted
git diff $TARGET_REF...HEAD > branch-changes.diff
git diff HEAD >> branch-changes.diff
git diff --cached >> branch-changes.diff
# Or get combined diff (recommended approach)
git diff $TARGET_REF...HEAD
git diff HEAD
git diff --cached
```
6. **Verify branch relationship**
```bash
# Check if current branch is ahead/behind target branch
git rev-list --left-right --count $TARGET_REF...HEAD
# Show commit log differences
git log $TARGET_REF..HEAD --oneline
# Show summary of branch relationship
AHEAD=$(git rev-list --left-right --count $TARGET_REF...HEAD | cut -f1)
BEHIND=$(git rev-list --left-right --count $TARGET_REF...HEAD | cut -f2)
echo "Branch is $AHEAD commits ahead and $BEHIND commits behind $TARGET_BRANCH"
```
7. **Understand the tech stack** (for validation):
- **Node.js**: >=22.0.0 <23.0.0
- **TypeScript**: 5.9.3
- **React**: 19.2.3
- **Express**: 5.2.1
- **Electron**: 39.2.7
- **Vite**: 7.3.0
- **Vitest**: 4.0.16
- Check `package.json` files for exact versions
### Phase 2: Deep Dive Analysis (5 Agents)
Launch 5 separate deep dive agents, each with a specific focus area. Each agent should be invoked with the `@deepdive` agent and given the git diff (comparing current branch against target branch) along with their specific instructions.
**Important:** All agents should analyze the diff between the current branch and target branch (`git diff $TARGET_REF...HEAD`), plus any uncommitted changes. This ensures the review covers all changes that will be merged. The target branch is determined from the optional argument or defaults to main/master.
#### Agent 1: Tech Stack Validation (HIGHEST PRIORITY)
**Focus:** Verify code is valid for the tech stack
**Instructions for Agent 1:**
```
Analyze the git diff for invalid code based on the tech stack:
1. **TypeScript/JavaScript Syntax**
- Check for valid TypeScript syntax (no invalid type annotations, correct import/export syntax)
- Verify Node.js API usage is compatible with Node.js >=22.0.0 <23.0.0
- Check for deprecated APIs or features not available in the Node.js version
- Verify ES module syntax (type: "module" in package.json)
2. **React 19.2.3 Compatibility**
- Check for deprecated React APIs or patterns
- Verify hooks usage is correct for React 19
- Check for invalid JSX syntax
- Verify component patterns match React 19 conventions
3. **Express 5.2.1 Compatibility**
- Check for deprecated Express APIs
- Verify middleware usage is correct for Express 5
- Check request/response handling patterns
4. **Type Safety**
- Verify TypeScript types are correctly used
- Check for `any` types that should be properly typed
- Verify type imports/exports are correct
- Check for missing type definitions
5. **Build System Compatibility**
- Verify Vite-specific code (imports, config) is valid
- Check Electron-specific APIs are used correctly
- Verify module resolution paths are correct
6. **Package Dependencies**
- Check for imports from packages not in package.json
- Verify version compatibility between dependencies
- Check for circular dependencies
Provide a detailed report with:
- File paths and line numbers of invalid code
- Specific error description (what's wrong and why)
- Expected vs actual behavior
- Priority level (CRITICAL for build-breaking issues)
```
#### Agent 2: Security Vulnerability Scanner
**Focus:** Security issues and vulnerabilities
**Instructions for Agent 2:**
```
Analyze the git diff for security vulnerabilities:
1. **Injection Vulnerabilities**
- SQL injection (if applicable)
- Command injection (exec, spawn, etc.)
- Path traversal vulnerabilities
- XSS vulnerabilities in React components
2. **Authentication & Authorization**
- Missing authentication checks
- Insecure token handling
- Authorization bypasses
- Session management issues
3. **Data Handling**
- Unsafe deserialization
- Insecure file operations
- Missing input validation
- Sensitive data exposure (secrets, tokens, passwords)
4. **Dependencies**
- Known vulnerable packages
- Insecure dependency versions
- Missing security patches
5. **API Security**
- Missing CORS configuration
- Insecure API endpoints
- Missing rate limiting
- Insecure WebSocket connections
6. **Electron-Specific**
- Insecure IPC communication
- Missing context isolation checks
- Insecure preload scripts
- Missing CSP headers
Provide a detailed report with:
- Vulnerability type and severity (CRITICAL, HIGH, MEDIUM, LOW)
- File paths and line numbers
- Attack vector description
- Recommended fix approach
```
#### Agent 3: Code Quality & Clean Code
**Focus:** Dirty code, code smells, and quality issues
**Instructions for Agent 3:**
```
Analyze the git diff for code quality issues:
1. **Code Smells**
- Long functions/methods (>50 lines)
- High cyclomatic complexity
- Duplicate code
- Dead code
- Magic numbers/strings
2. **Best Practices**
- Missing error handling
- Inconsistent naming conventions
- Poor separation of concerns
- Tight coupling
- Missing comments for complex logic
3. **Performance Issues**
- Inefficient algorithms
- Memory leaks (event listeners, subscriptions)
- Unnecessary re-renders in React
- Missing memoization where needed
- Inefficient database queries (if applicable)
4. **Maintainability**
- Hard-coded values
- Missing type definitions
- Inconsistent code style
- Poor file organization
- Missing tests for new code
5. **React-Specific**
- Missing key props in lists
- Direct state mutations
- Missing cleanup in useEffect
- Unnecessary useState/useEffect
- Prop drilling issues
Provide a detailed report with:
- Issue type and severity
- File paths and line numbers
- Description of the problem
- Impact on maintainability/performance
- Recommended refactoring approach
```
#### Agent 4: Implementation Correctness
**Focus:** Verify code implements requirements correctly
**Instructions for Agent 4:**
```
Analyze the git diff for implementation correctness:
1. **Logic Errors**
- Incorrect conditional logic
- Wrong variable usage
- Off-by-one errors
- Race conditions
- Missing null/undefined checks
2. **Functional Requirements**
- Missing features from requirements
- Incorrect feature implementation
- Edge cases not handled
- Missing validation
3. **Integration Issues**
- Incorrect API usage
- Wrong data format handling
- Missing error handling for external calls
- Incorrect state management
4. **Type Errors**
- Type mismatches
- Missing type guards
- Incorrect type assertions
- Unsafe type operations
5. **Testing Gaps**
- Missing unit tests
- Missing integration tests
- Tests don't cover edge cases
- Tests are incorrect
Provide a detailed report with:
- Issue description
- File paths and line numbers
- Expected vs actual behavior
- Steps to reproduce (if applicable)
- Recommended fix
```
#### Agent 5: Architecture & Design Patterns
**Focus:** Architectural issues and design pattern violations
**Instructions for Agent 5:**
```
Analyze the git diff for architectural and design issues:
1. **Architecture Violations**
- Violation of project structure patterns
- Incorrect layer separation
- Missing abstractions
- Tight coupling between modules
2. **Design Patterns**
- Incorrect pattern usage
- Missing patterns where needed
- Anti-patterns
3. **Project-Specific Patterns**
- Check against project documentation (docs/ folder)
- Verify route organization (server routes)
- Check provider patterns (server providers)
- Verify component organization (UI components)
4. **API Design**
- RESTful API violations
- Inconsistent response formats
- Missing error handling
- Incorrect status codes
5. **State Management**
- Incorrect state management patterns
- Missing state normalization
- Inefficient state updates
Provide a detailed report with:
- Architectural issue description
- File paths and affected areas
- Impact on system design
- Recommended architectural changes
```
### Phase 3: Consolidate Findings
After all 5 deep dive agents complete their analysis:
1. **Collect all findings** from each agent
2. **Prioritize issues**:
- CRITICAL: Tech stack invalid code (build-breaking)
- HIGH: Security vulnerabilities, critical logic errors
- MEDIUM: Code quality issues, architectural problems
- LOW: Minor code smells, style issues
3. **Group by file** to understand impact per file
4. **Create a master report** summarizing all findings
### Phase 4: Deepcode Fixes (5 Agents)
Launch 5 deepcode agents to fix the issues found. Each agent should be invoked with the `@deepcode` agent.
#### Deepcode Agent 1: Fix Tech Stack Invalid Code
**Priority:** CRITICAL - Fix first
**Instructions:**
```
Fix all invalid code based on tech stack issues identified by Agent 1.
Focus on:
1. Fixing TypeScript syntax errors
2. Updating deprecated Node.js APIs
3. Fixing React 19 compatibility issues
4. Correcting Express 5 API usage
5. Fixing type errors
6. Resolving build-breaking issues
After fixes, verify:
- Code compiles without errors
- TypeScript types are correct
- No deprecated API usage
```
#### Deepcode Agent 2: Fix Security Vulnerabilities
**Priority:** HIGH
**Instructions:**
```
Fix all security vulnerabilities identified by Agent 2.
Focus on:
1. Adding input validation
2. Fixing injection vulnerabilities
3. Securing authentication/authorization
4. Fixing insecure data handling
5. Updating vulnerable dependencies
6. Securing Electron IPC
After fixes, verify:
- Security vulnerabilities are addressed
- No sensitive data exposure
- Proper authentication/authorization
```
#### Deepcode Agent 3: Refactor Dirty Code
**Priority:** MEDIUM
**Instructions:**
```
Refactor code quality issues identified by Agent 3.
Focus on:
1. Extracting long functions
2. Reducing complexity
3. Removing duplicate code
4. Adding error handling
5. Improving React component structure
6. Adding missing comments
After fixes, verify:
- Code follows best practices
- No code smells remain
- Performance optimizations applied
```
#### Deepcode Agent 4: Fix Implementation Errors
**Priority:** HIGH
**Instructions:**
```
Fix implementation correctness issues identified by Agent 4.
Focus on:
1. Fixing logic errors
2. Adding missing features
3. Handling edge cases
4. Fixing type errors
5. Adding missing tests
After fixes, verify:
- Logic is correct
- Edge cases handled
- Tests pass
```
#### Deepcode Agent 5: Fix Architectural Issues
**Priority:** MEDIUM
**Instructions:**
```
Fix architectural issues identified by Agent 5.
Focus on:
1. Correcting architecture violations
2. Applying proper design patterns
3. Fixing API design issues
4. Improving state management
5. Following project patterns
After fixes, verify:
- Architecture is sound
- Patterns are correctly applied
- Code follows project structure
```
### Phase 5: Verification
After all fixes are complete:
1. **Run TypeScript compilation check**
```bash
npm run build:packages
```
2. **Run linting**
```bash
npm run lint
```
3. **Run tests** (if applicable)
```bash
npm run test:server
npm run test
```
4. **Verify git diff** shows only intended changes
```bash
git diff HEAD
```
5. **Create summary report**:
- Issues found by each agent
- Issues fixed by each agent
- Remaining issues (if any)
- Verification results
## Workflow Summary
1. ✅ Accept optional target branch argument (defaults to main/master if not provided)
2. ✅ Determine current branch and target branch (from argument or auto-detect main/master)
3. ✅ Get git diff comparing current branch against target branch (`git diff $TARGET_REF...HEAD`)
4. ✅ Include uncommitted changes in analysis (`git diff HEAD`, `git diff --cached`)
5. ✅ Launch 5 deep dive agents (parallel analysis) with branch diff
6. ✅ Consolidate findings and prioritize
7. ✅ Launch 5 deepcode agents (sequential fixes, priority order)
8. ✅ Verify fixes with build/lint/test
9. ✅ Report summary
## Notes
- **Tech stack validation is HIGHEST PRIORITY** - invalid code must be fixed first
- **Target branch argument**: The command accepts an optional target branch name as the first argument. If not provided, it automatically detects and uses `main` or `master` (in that order)
- Each deep dive agent should work independently and provide comprehensive analysis
- Deepcode agents should fix issues in priority order
- All fixes should maintain existing functionality
- If an agent finds no issues in their domain, they should report "No issues found"
- If fixes introduce new issues, they should be caught in verification phase
- The target branch is validated to ensure it exists (locally or remotely) before proceeding with the review

View File

@@ -0,0 +1,74 @@
# GitHub Issue Fix Command
Fetch a GitHub issue by number, verify it's a real issue, and fix it if valid.
## Usage
This command accepts a GitHub issue number as input (e.g., `123`).
## Instructions
1. **Get the issue number from the user**
- The issue number should be provided as an argument to this command
- If no number is provided, ask the user for it
2. **Fetch the GitHub issue**
- Determine the current project path (check if there's a current project context)
- Verify the project has a GitHub remote:
```bash
git remote get-url origin
```
- Fetch the issue details using GitHub CLI:
```bash
gh issue view <ISSUE_NUMBER> --json number,title,state,author,createdAt,labels,url,body,assignees
```
- If the command fails, report the error and stop
3. **Verify the issue is real and valid**
- Check that the issue exists (not 404)
- Check the issue state:
- If **closed**: Inform the user and ask if they still want to proceed
- If **open**: Proceed with validation
- Review the issue content:
- Read the title and body to understand what needs to be fixed
- Check labels for context (bug, enhancement, etc.)
- Note any assignees or linked PRs
4. **Validate the issue**
- Determine if this is a legitimate issue that needs fixing:
- Is the description clear and actionable?
- Does it describe a real problem or feature request?
- Are there any obvious signs it's spam or invalid?
- If the issue seems invalid or unclear:
- Report findings to the user
- Ask if they want to proceed anyway
- Stop if user confirms it's not valid
5. **If the issue is valid, proceed to fix it**
- Analyze what needs to be done based on the issue description
- Check the current codebase state:
- Run relevant tests to see current behavior
- Check if the issue is already fixed
- Look for related code that might need changes
- Implement the fix:
- Make necessary code changes
- Update or add tests as needed
- Ensure the fix addresses the issue description
- Verify the fix:
- Run tests to ensure nothing broke
- If possible, manually verify the fix addresses the issue
6. **Report summary**
- Issue number and title
- Issue state (open/closed)
- Whether the issue was validated as real
- What was fixed (if anything)
- Any tests that were updated or added
- Next steps (if any)
## Error Handling
- If GitHub CLI (`gh`) is not installed or authenticated, report error and stop
- If the project doesn't have a GitHub remote, report error and stop
- If the issue number doesn't exist, report error and stop
- If the issue is unclear or invalid, report findings and ask user before proceeding

View File

@@ -0,0 +1,77 @@
# Release Command
Bump the package.json version (major, minor, or patch) and build the Electron app with the new version.
## Usage
This command accepts a version bump type as input:
- `patch` - Bump patch version (0.1.0 -> 0.1.1)
- `minor` - Bump minor version (0.1.0 -> 0.2.0)
- `major` - Bump major version (0.1.0 -> 1.0.0)
## Instructions
1. **Get the bump type from the user**
- The bump type should be provided as an argument (patch, minor, or major)
- If no type is provided, ask the user which type they want
2. **Bump the version**
- Run the version bump script:
```bash
node apps/ui/scripts/bump-version.mjs <type>
```
- This updates both `apps/ui/package.json` and `apps/server/package.json` with the new version (keeps them in sync)
- Verify the version was updated correctly by checking the output
3. **Build the Electron app**
- Run the electron build:
```bash
npm run build:electron --workspace=apps/ui
```
- The build process automatically:
- Uses the version from `package.json` for artifact names (e.g., `Automaker-1.2.3-x64.zip`)
- Injects the version into the app via Vite's `__APP_VERSION__` constant
- Displays the version below the logo in the sidebar
4. **Commit the version bump**
- Stage the updated package.json files:
```bash
git add apps/ui/package.json apps/server/package.json
```
- Commit with a release message:
```bash
git commit -m "chore: release v<version>"
```
5. **Create and push the git tag**
- Create an annotated tag for the release:
```bash
git tag -a v<version> -m "Release v<version>"
```
- Push the commit and tag to remote:
```bash
git push && git push --tags
```
6. **Verify the release**
- Check that the build completed successfully
- Confirm the version appears correctly in the built artifacts
- The version will be displayed in the app UI below the logo
- Verify the tag is visible on the remote repository
## Version Centralization
The version is centralized and synchronized in both `apps/ui/package.json` and `apps/server/package.json`:
- **Electron builds**: Automatically read from `apps/ui/package.json` via electron-builder's `${version}` variable in `artifactName`
- **App display**: Injected at build time via Vite's `define` config as `__APP_VERSION__` constant (defined in `apps/ui/vite.config.mts`)
- **Server API**: Read from `apps/server/package.json` via `apps/server/src/lib/version.ts` utility (used in health check endpoints)
- **Type safety**: Defined in `apps/ui/src/vite-env.d.ts` as `declare const __APP_VERSION__: string`
This ensures consistency across:
- Build artifact names (e.g., `Automaker-1.2.3-x64.zip`)
- App UI display (shown as `v1.2.3` below the logo in `apps/ui/src/components/layout/sidebar/components/automaker-logo.tsx`)
- Server health endpoints (`/` and `/detailed`)
- Package metadata (both UI and server packages stay in sync)

484
.claude/commands/review.md Normal file
View File

@@ -0,0 +1,484 @@
# Code Review Command
Comprehensive code review using multiple deep dive agents to analyze git diff for correctness, security, code quality, and tech stack compliance, followed by automated fixes using deepcode agents.
## Usage
This command analyzes all changes in the git diff and verifies:
1. **Invalid code based on tech stack** (HIGHEST PRIORITY)
2. Security vulnerabilities
3. Code quality issues (dirty code)
4. Implementation correctness
Then automatically fixes any issues found.
## Instructions
### Phase 1: Get Git Diff
1. **Get the current git diff**
```bash
git diff HEAD
```
If you need staged changes instead:
```bash
git diff --cached
```
Or for a specific commit range:
```bash
git diff <base-branch>
```
2. **Get list of changed files**
```bash
git diff --name-only HEAD
```
3. **Understand the tech stack** (for validation):
- **Node.js**: >=22.0.0 <23.0.0
- **TypeScript**: 5.9.3
- **React**: 19.2.3
- **Express**: 5.2.1
- **Electron**: 39.2.7
- **Vite**: 7.3.0
- **Vitest**: 4.0.16
- Check `package.json` files for exact versions
### Phase 2: Deep Dive Analysis (5 Agents)
Launch 5 separate deep dive agents, each with a specific focus area. Each agent should be invoked with the `@deepdive` agent and given the git diff along with their specific instructions.
#### Agent 1: Tech Stack Validation (HIGHEST PRIORITY)
**Focus:** Verify code is valid for the tech stack
**Instructions for Agent 1:**
```
Analyze the git diff for invalid code based on the tech stack:
1. **TypeScript/JavaScript Syntax**
- Check for valid TypeScript syntax (no invalid type annotations, correct import/export syntax)
- Verify Node.js API usage is compatible with Node.js >=22.0.0 <23.0.0
- Check for deprecated APIs or features not available in the Node.js version
- Verify ES module syntax (type: "module" in package.json)
2. **React 19.2.3 Compatibility**
- Check for deprecated React APIs or patterns
- Verify hooks usage is correct for React 19
- Check for invalid JSX syntax
- Verify component patterns match React 19 conventions
3. **Express 5.2.1 Compatibility**
- Check for deprecated Express APIs
- Verify middleware usage is correct for Express 5
- Check request/response handling patterns
4. **Type Safety**
- Verify TypeScript types are correctly used
- Check for `any` types that should be properly typed
- Verify type imports/exports are correct
- Check for missing type definitions
5. **Build System Compatibility**
- Verify Vite-specific code (imports, config) is valid
- Check Electron-specific APIs are used correctly
- Verify module resolution paths are correct
6. **Package Dependencies**
- Check for imports from packages not in package.json
- Verify version compatibility between dependencies
- Check for circular dependencies
Provide a detailed report with:
- File paths and line numbers of invalid code
- Specific error description (what's wrong and why)
- Expected vs actual behavior
- Priority level (CRITICAL for build-breaking issues)
```
#### Agent 2: Security Vulnerability Scanner
**Focus:** Security issues and vulnerabilities
**Instructions for Agent 2:**
```
Analyze the git diff for security vulnerabilities:
1. **Injection Vulnerabilities**
- SQL injection (if applicable)
- Command injection (exec, spawn, etc.)
- Path traversal vulnerabilities
- XSS vulnerabilities in React components
2. **Authentication & Authorization**
- Missing authentication checks
- Insecure token handling
- Authorization bypasses
- Session management issues
3. **Data Handling**
- Unsafe deserialization
- Insecure file operations
- Missing input validation
- Sensitive data exposure (secrets, tokens, passwords)
4. **Dependencies**
- Known vulnerable packages
- Insecure dependency versions
- Missing security patches
5. **API Security**
- Missing CORS configuration
- Insecure API endpoints
- Missing rate limiting
- Insecure WebSocket connections
6. **Electron-Specific**
- Insecure IPC communication
- Missing context isolation checks
- Insecure preload scripts
- Missing CSP headers
Provide a detailed report with:
- Vulnerability type and severity (CRITICAL, HIGH, MEDIUM, LOW)
- File paths and line numbers
- Attack vector description
- Recommended fix approach
```
#### Agent 3: Code Quality & Clean Code
**Focus:** Dirty code, code smells, and quality issues
**Instructions for Agent 3:**
```
Analyze the git diff for code quality issues:
1. **Code Smells**
- Long functions/methods (>50 lines)
- High cyclomatic complexity
- Duplicate code
- Dead code
- Magic numbers/strings
2. **Best Practices**
- Missing error handling
- Inconsistent naming conventions
- Poor separation of concerns
- Tight coupling
- Missing comments for complex logic
3. **Performance Issues**
- Inefficient algorithms
- Memory leaks (event listeners, subscriptions)
- Unnecessary re-renders in React
- Missing memoization where needed
- Inefficient database queries (if applicable)
4. **Maintainability**
- Hard-coded values
- Missing type definitions
- Inconsistent code style
- Poor file organization
- Missing tests for new code
5. **React-Specific**
- Missing key props in lists
- Direct state mutations
- Missing cleanup in useEffect
- Unnecessary useState/useEffect
- Prop drilling issues
Provide a detailed report with:
- Issue type and severity
- File paths and line numbers
- Description of the problem
- Impact on maintainability/performance
- Recommended refactoring approach
```
#### Agent 4: Implementation Correctness
**Focus:** Verify code implements requirements correctly
**Instructions for Agent 4:**
```
Analyze the git diff for implementation correctness:
1. **Logic Errors**
- Incorrect conditional logic
- Wrong variable usage
- Off-by-one errors
- Race conditions
- Missing null/undefined checks
2. **Functional Requirements**
- Missing features from requirements
- Incorrect feature implementation
- Edge cases not handled
- Missing validation
3. **Integration Issues**
- Incorrect API usage
- Wrong data format handling
- Missing error handling for external calls
- Incorrect state management
4. **Type Errors**
- Type mismatches
- Missing type guards
- Incorrect type assertions
- Unsafe type operations
5. **Testing Gaps**
- Missing unit tests
- Missing integration tests
- Tests don't cover edge cases
- Tests are incorrect
Provide a detailed report with:
- Issue description
- File paths and line numbers
- Expected vs actual behavior
- Steps to reproduce (if applicable)
- Recommended fix
```
#### Agent 5: Architecture & Design Patterns
**Focus:** Architectural issues and design pattern violations
**Instructions for Agent 5:**
```
Analyze the git diff for architectural and design issues:
1. **Architecture Violations**
- Violation of project structure patterns
- Incorrect layer separation
- Missing abstractions
- Tight coupling between modules
2. **Design Patterns**
- Incorrect pattern usage
- Missing patterns where needed
- Anti-patterns
3. **Project-Specific Patterns**
- Check against project documentation (docs/ folder)
- Verify route organization (server routes)
- Check provider patterns (server providers)
- Verify component organization (UI components)
4. **API Design**
- RESTful API violations
- Inconsistent response formats
- Missing error handling
- Incorrect status codes
5. **State Management**
- Incorrect state management patterns
- Missing state normalization
- Inefficient state updates
Provide a detailed report with:
- Architectural issue description
- File paths and affected areas
- Impact on system design
- Recommended architectural changes
```
### Phase 3: Consolidate Findings
After all 5 deep dive agents complete their analysis:
1. **Collect all findings** from each agent
2. **Prioritize issues**:
- CRITICAL: Tech stack invalid code (build-breaking)
- HIGH: Security vulnerabilities, critical logic errors
- MEDIUM: Code quality issues, architectural problems
- LOW: Minor code smells, style issues
3. **Group by file** to understand impact per file
4. **Create a master report** summarizing all findings
### Phase 4: Deepcode Fixes (5 Agents)
Launch 5 deepcode agents to fix the issues found. Each agent should be invoked with the `@deepcode` agent.
#### Deepcode Agent 1: Fix Tech Stack Invalid Code
**Priority:** CRITICAL - Fix first
**Instructions:**
```
Fix all invalid code based on tech stack issues identified by Agent 1.
Focus on:
1. Fixing TypeScript syntax errors
2. Updating deprecated Node.js APIs
3. Fixing React 19 compatibility issues
4. Correcting Express 5 API usage
5. Fixing type errors
6. Resolving build-breaking issues
After fixes, verify:
- Code compiles without errors
- TypeScript types are correct
- No deprecated API usage
```
#### Deepcode Agent 2: Fix Security Vulnerabilities
**Priority:** HIGH
**Instructions:**
```
Fix all security vulnerabilities identified by Agent 2.
Focus on:
1. Adding input validation
2. Fixing injection vulnerabilities
3. Securing authentication/authorization
4. Fixing insecure data handling
5. Updating vulnerable dependencies
6. Securing Electron IPC
After fixes, verify:
- Security vulnerabilities are addressed
- No sensitive data exposure
- Proper authentication/authorization
```
#### Deepcode Agent 3: Refactor Dirty Code
**Priority:** MEDIUM
**Instructions:**
```
Refactor code quality issues identified by Agent 3.
Focus on:
1. Extracting long functions
2. Reducing complexity
3. Removing duplicate code
4. Adding error handling
5. Improving React component structure
6. Adding missing comments
After fixes, verify:
- Code follows best practices
- No code smells remain
- Performance optimizations applied
```
#### Deepcode Agent 4: Fix Implementation Errors
**Priority:** HIGH
**Instructions:**
```
Fix implementation correctness issues identified by Agent 4.
Focus on:
1. Fixing logic errors
2. Adding missing features
3. Handling edge cases
4. Fixing type errors
5. Adding missing tests
After fixes, verify:
- Logic is correct
- Edge cases handled
- Tests pass
```
#### Deepcode Agent 5: Fix Architectural Issues
**Priority:** MEDIUM
**Instructions:**
```
Fix architectural issues identified by Agent 5.
Focus on:
1. Correcting architecture violations
2. Applying proper design patterns
3. Fixing API design issues
4. Improving state management
5. Following project patterns
After fixes, verify:
- Architecture is sound
- Patterns are correctly applied
- Code follows project structure
```
### Phase 5: Verification
After all fixes are complete:
1. **Run TypeScript compilation check**
```bash
npm run build:packages
```
2. **Run linting**
```bash
npm run lint
```
3. **Run tests** (if applicable)
```bash
npm run test:server
npm run test
```
4. **Verify git diff** shows only intended changes
```bash
git diff HEAD
```
5. **Create summary report**:
- Issues found by each agent
- Issues fixed by each agent
- Remaining issues (if any)
- Verification results
## Workflow Summary
1. ✅ Get git diff
2. ✅ Launch 5 deep dive agents (parallel analysis)
3. ✅ Consolidate findings and prioritize
4. ✅ Launch 5 deepcode agents (sequential fixes, priority order)
5. ✅ Verify fixes with build/lint/test
6. ✅ Report summary
## Notes
- **Tech stack validation is HIGHEST PRIORITY** - invalid code must be fixed first
- Each deep dive agent should work independently and provide comprehensive analysis
- Deepcode agents should fix issues in priority order
- All fixes should maintain existing functionality
- If an agent finds no issues in their domain, they should report "No issues found"
- If fixes introduce new issues, they should be caught in verification phase

View File

@@ -0,0 +1,45 @@
When you think you are done, you are NOT done.
You must run a mandatory 3-pass verification before concluding:
## Pass 1: Correctness & Functionality
- [ ] Verify logic matches requirements and specifications
- [ ] Check type safety (TypeScript types are correct and complete)
- [ ] Ensure imports are correct and follow project conventions
- [ ] Verify all functions/classes work as intended
- [ ] Check that return values and side effects are correct
- [ ] Run relevant tests if they exist, or verify testability
- [ ] Confirm integration with existing code works properly
## Pass 2: Edge Cases & Safety
- [ ] Handle null/undefined inputs gracefully
- [ ] Validate all user inputs and external data
- [ ] Check error handling (try/catch, error boundaries, etc.)
- [ ] Verify security considerations (no sensitive data exposure, proper auth checks)
- [ ] Test boundary conditions (empty arrays, zero values, max lengths, etc.)
- [ ] Ensure resource cleanup (file handles, connections, timers)
- [ ] Check for potential race conditions or async issues
- [ ] Verify file path security (no directory traversal vulnerabilities)
## Pass 3: Maintainability & Code Quality
- [ ] Code follows project style guide and conventions
- [ ] Functions/classes are single-purpose and well-named
- [ ] Remove dead code, unused imports, and console.logs
- [ ] Extract magic numbers/strings into named constants
- [ ] Check for code duplication (DRY principle)
- [ ] Verify appropriate abstraction levels (not over/under-engineered)
- [ ] Add necessary comments for complex logic
- [ ] Ensure consistent error messages and logging
- [ ] Check that code is readable and self-documenting
- [ ] Verify proper separation of concerns
**For each pass, explicitly report:**
- What you checked
- Any issues found and how they were fixed
- Any remaining concerns or trade-offs
Only after completing all three passes with explicit findings may you conclude the work is done.

View File

@@ -0,0 +1,49 @@
# Project Build and Fix Command
Run all builds and intelligently fix any failures based on what changed.
## Instructions
1. **Run the build**
```bash
npm run build
```
This builds all packages and the UI application.
2. **If the build succeeds**, report success and stop.
3. **If the build fails**, analyze the failures:
- Note which build step failed and the error messages
- Check for TypeScript compilation errors, missing dependencies, or configuration issues
- Run `git diff main` to see what code has changed
4. **Determine the nature of the failure**:
- **If the failure is due to intentional changes** (new features, refactoring, dependency updates):
- Fix any TypeScript type errors introduced by the changes
- Update build configuration if needed (e.g., tsconfig.json, vite.config.mts)
- Ensure all new dependencies are properly installed
- Fix import paths or module resolution issues
- **If the failure appears to be a regression** (broken imports, missing files, configuration errors):
- Fix the source code to restore the build
- Check for accidentally deleted files or broken references
- Verify build configuration files are correct
5. **Common build issues to check**:
- **TypeScript errors**: Fix type mismatches, missing types, or incorrect imports
- **Missing dependencies**: Run `npm install` if packages are missing
- **Import/export errors**: Fix incorrect import paths or missing exports
- **Build configuration**: Check tsconfig.json, vite.config.mts, or other build configs
- **Package build order**: Ensure `build:packages` completes before building apps
6. **How to decide if it's intentional vs regression**:
- Look at the git diff and commit messages
- If the change was deliberate and introduced new code that needs fixing → fix the new code
- If the change broke existing functionality that should still build → fix the regression
- When in doubt, ask the user
7. **After making fixes**, re-run the build to verify everything compiles successfully.
8. **Report summary** of what was fixed (TypeScript errors, configuration issues, missing dependencies, etc.).

View File

@@ -0,0 +1,36 @@
# Project Test and Fix Command
Run all tests and intelligently fix any failures based on what changed.
## Instructions
1. **Run all tests**
```bash
npm run test:all
```
2. **If all tests pass**, report success and stop.
3. **If any tests fail**, analyze the failures:
- Note which tests failed and their error messages
- Run `git diff main` to see what code has changed
4. **Determine the nature of the change**:
- **If the logic change is intentional** (new feature, refactor, behavior change):
- Update the failing tests to match the new expected behavior
- The tests should reflect what the code NOW does correctly
- **If the logic change appears to be a bug** (regression, unintended side effect):
- Fix the source code to restore the expected behavior
- Do NOT modify the tests - they are catching a real bug
5. **How to decide if it's a bug vs intentional change**:
- Look at the git diff and commit messages
- If the change was deliberate and the test expectations are now outdated → update tests
- If the change broke existing functionality that should still work → fix the code
- When in doubt, ask the user
6. **After making fixes**, re-run the tests to verify everything passes.
7. **Report summary** of what was fixed (tests updated vs code fixed).

View File

@@ -1,24 +0,0 @@
{
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": true
},
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Read(./**)",
"Write(./**)",
"Edit(./**)",
"Glob(./**)",
"Grep(./**)",
"Bash(*)",
"mcp__puppeteer__puppeteer_navigate",
"mcp__puppeteer__puppeteer_screenshot",
"mcp__puppeteer__puppeteer_click",
"mcp__puppeteer__puppeteer_fill",
"mcp__puppeteer__puppeteer_select",
"mcp__puppeteer__puppeteer_hover",
"mcp__puppeteer__puppeteer_evaluate"
]
}
}

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
# Dependencies
node_modules/
**/node_modules/
# Build outputs
dist/
**/dist/
dist-electron/
**/dist-electron/
build/
**/build/
.next/
**/.next/
.nuxt/
**/.nuxt/
out/
**/out/
.cache/
**/.cache/

117
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,117 @@
name: Bug Report
description: File a bug report to help us improve Automaker
title: '[Bug]: '
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the form below with as much detail as possible.
- type: dropdown
id: operating-system
attributes:
label: Operating System
description: What operating system are you using?
options:
- macOS
- Windows
- Linux
- Other
default: 0
validations:
required: true
- type: dropdown
id: run-mode
attributes:
label: Run Mode
description: How are you running Automaker?
options:
- Electron (Desktop App)
- Web (Browser)
- Docker
default: 0
validations:
required: true
- type: input
id: app-version
attributes:
label: App Version
description: What version of Automaker are you using? (e.g., 0.1.0)
placeholder: '0.1.0'
validations:
required: true
- type: textarea
id: bug-description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is.
placeholder: Describe the bug...
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Go to '...'
2. Click on '...'
3. Scroll down to '...'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: A clear and concise description of what you expected to happen.
placeholder: What should have happened?
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual Behavior
description: A clear and concise description of what actually happened.
placeholder: What actually happened?
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
placeholder: Drag and drop screenshots here or paste image URLs
- type: textarea
id: logs
attributes:
label: Relevant Logs
description: If applicable, paste relevant logs or error messages.
placeholder: Paste logs here...
render: shell
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context about the problem here.
placeholder: Any additional information that might be helpful...
- type: checkboxes
id: terms
attributes:
label: Checklist
options:
- label: I have searched existing issues to ensure this bug hasn't been reported already
required: true
- label: I have provided all required information above
required: true

6
.gitignore vendored
View File

@@ -80,4 +80,8 @@ blob-report/
*.pem *.pem
docker-compose.override.yml docker-compose.override.yml
.claude/ .claude/docker-compose.override.yml
.claude/hans/
pnpm-lock.yaml
yarn.lock

View File

@@ -1 +1,46 @@
npx lint-staged #!/usr/bin/env sh
# Try to load nvm if available (optional - works without it too)
if [ -z "$NVM_DIR" ]; then
# Check for Herd's nvm first (macOS with Herd)
if [ -s "$HOME/Library/Application Support/Herd/config/nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/Library/Application Support/Herd/config/nvm"
# Then check standard nvm location
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
export NVM_DIR="$HOME/.nvm"
fi
fi
# Source nvm if found (silently skip if not available)
[ -n "$NVM_DIR" ] && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 2>/dev/null
# Load node version from .nvmrc if using nvm (silently skip if nvm not available or fails)
if [ -f .nvmrc ] && command -v nvm >/dev/null 2>&1; then
# Check if Unix nvm was sourced (it's a shell function with NVM_DIR set)
if [ -n "$NVM_DIR" ] && type nvm 2>/dev/null | grep -q "function"; then
# Unix nvm: reads .nvmrc automatically
nvm use >/dev/null 2>&1 || true
else
# nvm-windows: needs explicit version from .nvmrc
NODE_VERSION=$(cat .nvmrc | tr -d '[:space:]')
if [ -n "$NODE_VERSION" ]; then
nvm use "$NODE_VERSION" >/dev/null 2>&1 || true
fi
fi
fi
# Ensure common system paths are in PATH (for systems without nvm)
# This helps find node/npm installed via Homebrew, system packages, etc.
export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin"
# Run lint-staged - works with or without nvm
# Prefer npx, fallback to npm exec, both work with system-installed Node.js
if command -v npx >/dev/null 2>&1; then
npx lint-staged
elif command -v npm >/dev/null 2>&1; then
npm exec -- lint-staged
else
echo "Error: Neither npx nor npm found in PATH."
echo "Please ensure Node.js is installed (via nvm, Homebrew, system package manager, etc.)"
exit 1
fi

2
.nvmrc Normal file
View File

@@ -0,0 +1,2 @@
22

View File

@@ -23,6 +23,8 @@ pnpm-lock.yaml
# Generated files # Generated files
*.min.js *.min.js
*.min.css *.min.css
routeTree.gen.ts
apps/ui/src/routeTree.gen.ts
# Test artifacts # Test artifacts
test-results/ test-results/

172
CLAUDE.md Normal file
View File

@@ -0,0 +1,172 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Automaker is an autonomous AI development studio built as an npm workspace monorepo. It provides a Kanban-based workflow where AI agents (powered by Claude Agent SDK) implement features in isolated git worktrees.
## Common Commands
```bash
# Development
npm run dev # Interactive launcher (choose web or electron)
npm run dev:web # Web browser mode (localhost:3007)
npm run dev:electron # Desktop app mode
npm run dev:electron:debug # Desktop with DevTools open
# Building
npm run build # Build web application
npm run build:packages # Build all shared packages (required before other builds)
npm run build:electron # Build desktop app for current platform
npm run build:server # Build server only
# Testing
npm run test # E2E tests (Playwright, headless)
npm run test:headed # E2E tests with browser visible
npm run test:server # Server unit tests (Vitest)
npm run test:packages # All shared package tests
npm run test:all # All tests (packages + server)
# Single test file
npm run test:server -- tests/unit/specific.test.ts
# Linting and formatting
npm run lint # ESLint
npm run format # Prettier write
npm run format:check # Prettier check
```
## Architecture
### Monorepo Structure
```
automaker/
├── apps/
│ ├── ui/ # React + Vite + Electron frontend (port 3007)
│ └── server/ # Express + WebSocket backend (port 3008)
└── libs/ # Shared packages (@automaker/*)
├── types/ # Core TypeScript definitions (no dependencies)
├── utils/ # Logging, errors, image processing, context loading
├── prompts/ # AI prompt templates
├── platform/ # Path management, security, process spawning
├── model-resolver/ # Claude model alias resolution
├── dependency-resolver/ # Feature dependency ordering
└── git-utils/ # Git operations & worktree management
```
### Package Dependency Chain
Packages can only depend on packages above them:
```
@automaker/types (no dependencies)
@automaker/utils, @automaker/prompts, @automaker/platform, @automaker/model-resolver, @automaker/dependency-resolver
@automaker/git-utils
@automaker/server, @automaker/ui
```
### Key Technologies
- **Frontend**: React 19, Vite 7, Electron 39, TanStack Router, Zustand 5, Tailwind CSS 4
- **Backend**: Express 5, WebSocket (ws), Claude Agent SDK, node-pty
- **Testing**: Playwright (E2E), Vitest (unit)
### Server Architecture
The server (`apps/server/src/`) follows a modular pattern:
- `routes/` - Express route handlers organized by feature (agent, features, auto-mode, worktree, etc.)
- `services/` - Business logic (AgentService, AutoModeService, FeatureLoader, TerminalService)
- `providers/` - AI provider abstraction (currently Claude via Claude Agent SDK)
- `lib/` - Utilities (events, auth, worktree metadata)
### Frontend Architecture
The UI (`apps/ui/src/`) uses:
- `routes/` - TanStack Router file-based routing
- `components/views/` - Main view components (board, settings, terminal, etc.)
- `store/` - Zustand stores with persistence (app-store.ts, setup-store.ts)
- `hooks/` - Custom React hooks
- `lib/` - Utilities and API client
## Data Storage
### Per-Project Data (`.automaker/`)
```
.automaker/
├── features/ # Feature JSON files and images
│ └── {featureId}/
│ ├── feature.json
│ ├── agent-output.md
│ └── images/
├── context/ # Context files for AI agents (CLAUDE.md, etc.)
├── settings.json # Project-specific settings
├── spec.md # Project specification
└── analysis.json # Project structure analysis
```
### Global Data (`DATA_DIR`, default `./data`)
```
data/
├── settings.json # Global settings, profiles, shortcuts
├── credentials.json # API keys
├── sessions-metadata.json # Chat session metadata
└── agent-sessions/ # Conversation histories
```
## Import Conventions
Always import from shared packages, never from old paths:
```typescript
// ✅ Correct
import type { Feature, ExecuteOptions } from '@automaker/types';
import { createLogger, classifyError } from '@automaker/utils';
import { getEnhancementPrompt } from '@automaker/prompts';
import { getFeatureDir, ensureAutomakerDir } from '@automaker/platform';
import { resolveModelString } from '@automaker/model-resolver';
import { resolveDependencies } from '@automaker/dependency-resolver';
import { getGitRepositoryDiffs } from '@automaker/git-utils';
// ❌ Never import from old paths
import { Feature } from '../services/feature-loader'; // Wrong
import { createLogger } from '../lib/logger'; // Wrong
```
## Key Patterns
### Event-Driven Architecture
All server operations emit events that stream to the frontend via WebSocket. Events are created using `createEventEmitter()` from `lib/events.ts`.
### Git Worktree Isolation
Each feature executes in an isolated git worktree, created via `@automaker/git-utils`. This protects the main branch during AI agent execution.
### Context Files
Project-specific rules are stored in `.automaker/context/` and automatically loaded into agent prompts via `loadContextFiles()` from `@automaker/utils`.
### Model Resolution
Use `resolveModelString()` from `@automaker/model-resolver` to convert model aliases:
- `haiku``claude-haiku-4-5`
- `sonnet``claude-sonnet-4-20250514`
- `opus``claude-opus-4-5-20251101`
## Environment Variables
- `ANTHROPIC_API_KEY` - Anthropic API key (or use Claude Code CLI auth)
- `PORT` - Server port (default: 3008)
- `DATA_DIR` - Data storage directory (default: ./data)
- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory
- `AUTOMAKER_MOCK_AGENT=true` - Enable mock agent mode for CI testing

685
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,685 @@
# Contributing to Automaker
Thank you for your interest in contributing to Automaker! We're excited to have you join our community of developers building the future of autonomous AI development.
Automaker is an autonomous AI development studio that provides a Kanban-based workflow where AI agents implement features in isolated git worktrees. Whether you're fixing bugs, adding features, improving documentation, or suggesting ideas, your contributions help make this project better for everyone.
This guide will help you get started with contributing to Automaker. Please take a moment to read through these guidelines to ensure a smooth contribution process.
## Contribution License Agreement
**Important:** By submitting, pushing, or contributing any code, documentation, pull requests, issues, or other materials to the Automaker project, you agree to assign all right, title, and interest in and to your contributions, including all copyrights, patents, and other intellectual property rights, to the Core Contributors of Automaker. This assignment is irrevocable and includes the right to use, modify, distribute, and monetize your contributions in any manner.
**You understand and agree that you will have no right to receive any royalties, compensation, or other financial benefits from any revenue, income, or commercial use generated from your contributed code or any derivative works thereof.** All contributions are made without expectation of payment or financial return.
For complete details on contribution terms and rights assignment, please review [Section 5 (CONTRIBUTIONS AND RIGHTS ASSIGNMENT) of the LICENSE](LICENSE#5-contributions-and-rights-assignment).
## Table of Contents
- [Contributing to Automaker](#contributing-to-automaker)
- [Table of Contents](#table-of-contents)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Fork and Clone](#fork-and-clone)
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
- [Pull Request Process](#pull-request-process)
- [Branch Naming Convention](#branch-naming-convention)
- [Commit Message Format](#commit-message-format)
- [Submitting a Pull Request](#submitting-a-pull-request)
- [1. Prepare Your Changes](#1-prepare-your-changes)
- [2. Run Pre-submission Checks](#2-run-pre-submission-checks)
- [3. Push Your Changes](#3-push-your-changes)
- [4. Open a Pull Request](#4-open-a-pull-request)
- [PR Requirements Checklist](#pr-requirements-checklist)
- [Review Process](#review-process)
- [What to Expect](#what-to-expect)
- [Review Focus Areas](#review-focus-areas)
- [Responding to Feedback](#responding-to-feedback)
- [Approval Criteria](#approval-criteria)
- [Getting Help](#getting-help)
- [Code Style Guidelines](#code-style-guidelines)
- [Testing Requirements](#testing-requirements)
- [Running Tests](#running-tests)
- [Test Frameworks](#test-frameworks)
- [End-to-End Tests (Playwright)](#end-to-end-tests-playwright)
- [Unit Tests (Vitest)](#unit-tests-vitest)
- [Writing Tests](#writing-tests)
- [When to Write Tests](#when-to-write-tests)
- [CI/CD Pipeline](#cicd-pipeline)
- [CI Checks](#ci-checks)
- [CI Testing Environment](#ci-testing-environment)
- [Viewing CI Results](#viewing-ci-results)
- [Common CI Failures](#common-ci-failures)
- [Coverage Requirements](#coverage-requirements)
- [Issue Reporting](#issue-reporting)
- [Bug Reports](#bug-reports)
- [Before Reporting](#before-reporting)
- [Bug Report Template](#bug-report-template)
- [Feature Requests](#feature-requests)
- [Before Requesting](#before-requesting)
- [Feature Request Template](#feature-request-template)
- [Security Issues](#security-issues)
---
## Getting Started
### Prerequisites
Before contributing to Automaker, ensure you have the following installed on your system:
- **Node.js 18+** (tested with Node.js 22)
- Download from [nodejs.org](https://nodejs.org/)
- Verify installation: `node --version`
- **npm** (comes with Node.js)
- Verify installation: `npm --version`
- **Git** for version control
- Verify installation: `git --version`
- **Claude Code CLI** or **Anthropic API Key** (for AI agent functionality)
- Required to run the AI development features
**Optional but recommended:**
- A code editor with TypeScript support (VS Code recommended)
- GitHub CLI (`gh`) for easier PR management
### Fork and Clone
1. **Fork the repository** on GitHub
- Navigate to [https://github.com/AutoMaker-Org/automaker](https://github.com/AutoMaker-Org/automaker)
- Click the "Fork" button in the top-right corner
- This creates your own copy of the repository
2. **Clone your fork locally**
```bash
git clone https://github.com/YOUR_USERNAME/automaker.git
cd automaker
```
3. **Add the upstream remote** to keep your fork in sync
```bash
git remote add upstream https://github.com/AutoMaker-Org/automaker.git
```
4. **Verify remotes**
```bash
git remote -v
# Should show:
# origin https://github.com/YOUR_USERNAME/automaker.git (fetch)
# origin https://github.com/YOUR_USERNAME/automaker.git (push)
# upstream https://github.com/AutoMaker-Org/automaker.git (fetch)
# upstream https://github.com/AutoMaker-Org/automaker.git (push)
```
### Development Setup
1. **Install dependencies**
```bash
npm install
```
2. **Build shared packages** (required before running the app)
```bash
npm run build:packages
```
3. **Start the development server**
```bash
npm run dev # Interactive launcher - choose mode
npm run dev:web # Browser mode (web interface)
npm run dev:electron # Desktop app mode
```
**Common development commands:**
| Command | Description |
| ------------------------ | -------------------------------- |
| `npm run dev` | Interactive development launcher |
| `npm run dev:web` | Start in browser mode |
| `npm run dev:electron` | Start desktop app |
| `npm run build` | Build all packages and apps |
| `npm run build:packages` | Build shared packages only |
| `npm run lint` | Run ESLint checks |
| `npm run format` | Format code with Prettier |
| `npm run format:check` | Check formatting without changes |
| `npm run test` | Run E2E tests (Playwright) |
| `npm run test:server` | Run server unit tests |
| `npm run test:packages` | Run package tests |
| `npm run test:all` | Run all tests |
### Project Structure
Automaker is organized as an npm workspace monorepo:
```
automaker/
├── apps/
│ ├── ui/ # React + Vite + Electron frontend
│ └── server/ # Express + WebSocket backend
├── libs/
│ ├── @automaker/types/ # Shared TypeScript types
│ ├── @automaker/utils/ # Utility functions
│ ├── @automaker/prompts/ # AI prompt templates
│ ├── @automaker/platform/ # Platform abstractions
│ ├── @automaker/model-resolver/ # AI model resolution
│ ├── @automaker/dependency-resolver/ # Dependency management
│ └── @automaker/git-utils/ # Git operations
├── docs/ # Documentation
└── package.json # Root package configuration
```
**Key conventions:**
- Always import from `@automaker/*` shared packages, never use relative paths to `libs/`
- Frontend code lives in `apps/ui/`
- Backend code lives in `apps/server/`
- Shared logic should be in the appropriate `libs/` package
---
## Pull Request Process
This section covers everything you need to know about contributing changes through pull requests, from creating your branch to getting your code merged.
### Branch Naming Convention
We use a consistent branch naming pattern to keep our repository organized:
```
<type>/<description>
```
**Branch types:**
| Type | Purpose | Example |
| ---------- | ------------------------ | --------------------------------- |
| `feature` | New functionality | `feature/add-user-authentication` |
| `fix` | Bug fixes | `fix/resolve-memory-leak` |
| `docs` | Documentation changes | `docs/update-contributing-guide` |
| `refactor` | Code restructuring | `refactor/simplify-api-handlers` |
| `test` | Adding or updating tests | `test/add-utils-unit-tests` |
| `chore` | Maintenance tasks | `chore/update-dependencies` |
**Guidelines:**
- Use lowercase letters and hyphens (no underscores or spaces)
- Keep descriptions short but descriptive
- Include issue number when applicable: `feature/123-add-login`
```bash
# Create and checkout a new feature branch
git checkout -b feature/add-dark-mode
# Create a fix branch with issue reference
git checkout -b fix/456-resolve-login-error
```
### Commit Message Format
We follow the **Conventional Commits** style for clear, readable commit history:
```
<type>: <description>
[optional body]
```
**Commit types:**
| Type | Purpose |
| ---------- | --------------------------- |
| `feat` | New feature |
| `fix` | Bug fix |
| `docs` | Documentation only |
| `style` | Formatting (no code change) |
| `refactor` | Code restructuring |
| `test` | Adding or updating tests |
| `chore` | Maintenance tasks |
**Guidelines:**
- Use **imperative mood** ("Add feature" not "Added feature")
- Keep first line under **72 characters**
- Capitalize the first letter after the type prefix
- No period at the end of the subject line
- Add a blank line before the body for detailed explanations
**Examples:**
```bash
# Simple commit
git commit -m "feat: Add user authentication flow"
# Commit with body for more context
git commit -m "fix: Resolve memory leak in WebSocket handler
The connection cleanup was not being called when clients
disconnected unexpectedly. Added proper cleanup in the
error handler to prevent memory accumulation."
# Documentation update
git commit -m "docs: Update API documentation"
# Refactoring
git commit -m "refactor: Simplify state management logic"
```
### Submitting a Pull Request
Follow these steps to submit your contribution:
#### 1. Prepare Your Changes
Ensure you've synced with the latest upstream changes:
```bash
# Fetch latest changes from upstream
git fetch upstream
# Rebase your branch on main (if needed)
git rebase upstream/main
```
#### 2. Run Pre-submission Checks
Before opening your PR, verify everything passes locally:
```bash
# Run all tests
npm run test:all
# Check formatting
npm run format:check
# Run linter
npm run lint
# Build to verify no compile errors
npm run build
```
#### 3. Push Your Changes
```bash
# Push your branch to your fork
git push origin feature/your-feature-name
```
#### 4. Open a Pull Request
1. Go to your fork on GitHub
2. Click "Compare & pull request" for your branch
3. Ensure the base repository is `AutoMaker-Org/automaker` and base branch is `main`
4. Fill out the PR template completely
#### PR Requirements Checklist
Your PR should include:
- [ ] **Clear title** describing the change (use conventional commit format)
- [ ] **Description** explaining what changed and why
- [ ] **Link to related issue** (if applicable): `Closes #123` or `Fixes #456`
- [ ] **All CI checks passing** (format, lint, build, tests)
- [ ] **No merge conflicts** with main branch
- [ ] **Tests included** for new functionality
- [ ] **Documentation updated** if adding/changing public APIs
**Example PR Description:**
```markdown
## Summary
This PR adds dark mode support to the Automaker UI.
- Implements theme toggle in settings panel
- Adds CSS custom properties for theme colors
- Persists theme preference to localStorage
## Related Issue
Closes #123
## Testing
- [x] Tested toggle functionality in Chrome and Firefox
- [x] Verified theme persists across page reloads
- [x] Checked accessibility contrast ratios
## Screenshots
[Include before/after screenshots for UI changes]
```
### Review Process
All contributions go through code review to maintain quality:
#### What to Expect
1. **CI Checks Run First** - Automated checks (format, lint, build, tests) must pass before review
2. **Maintainer Review** - The project maintainers will review your PR and decide whether to merge it
3. **Feedback & Discussion** - The reviewer may ask questions or request changes
4. **Iteration** - Make requested changes and push updates to the same branch
5. **Approval & Merge** - Once approved and checks pass, your PR will be merged
#### Review Focus Areas
The reviewer checks for:
- **Correctness** - Does the code work as intended?
- **Clean Code** - Does it follow our [code style guidelines](#code-style-guidelines)?
- **Test Coverage** - Are new features properly tested?
- **Documentation** - Are public APIs documented?
- **Breaking Changes** - Are any breaking changes discussed first?
#### Responding to Feedback
- Respond to **all** review comments, even if just to acknowledge
- Ask questions if feedback is unclear
- Push additional commits to address feedback (don't force-push during review)
- Mark conversations as resolved once addressed
#### Approval Criteria
Your PR is ready to merge when:
- ✅ All CI checks pass
- ✅ The maintainer has approved the changes
- ✅ All review comments are addressed
- ✅ No unresolved merge conflicts
#### Getting Help
If your PR seems stuck:
- Comment asking for status update (mention @webdevcody if needed)
- Reach out on [Discord](https://discord.gg/jjem7aEDKU)
- Make sure all checks are passing and you've responded to all feedback
---
## Code Style Guidelines
Automaker uses automated tooling to enforce code style. Run `npm run format` to format code and `npm run lint` to check for issues. Pre-commit hooks automatically format staged files before committing.
---
## Testing Requirements
Testing helps prevent regressions. Automaker uses **Playwright** for end-to-end testing and **Vitest** for unit tests.
### Running Tests
Use these commands to run tests locally:
| Command | Description |
| ------------------------------ | ------------------------------------- |
| `npm run test` | Run E2E tests (Playwright) |
| `npm run test:server` | Run server unit tests (Vitest) |
| `npm run test:packages` | Run shared package tests |
| `npm run test:all` | Run all tests |
| `npm run test:server:coverage` | Run server tests with coverage report |
**Before submitting a PR**, always run the full test suite:
```bash
npm run test:all
```
### Test Frameworks
#### End-to-End Tests (Playwright)
E2E tests verify the entire application works correctly from a user's perspective.
- **Framework:** [Playwright](https://playwright.dev/)
- **Location:** `e2e/` directory
- **Test ports:** UI on port 3007, Server on port 3008
**Running E2E tests:**
```bash
# Run all E2E tests
npm run test
# Run with headed browser (useful for debugging)
npx playwright test --headed
# Run a specific test file
npm test --workspace=@automaker/ui -- tests/example.spec.ts
```
**E2E Test Guidelines:**
- Write tests from a user's perspective
- Use descriptive test names that explain the scenario
- Clean up test data after each test
- Use appropriate timeouts for async operations
- Prefer `locator` over direct selectors for resilience
#### Unit Tests (Vitest)
Unit tests verify individual functions and modules work correctly in isolation.
- **Framework:** [Vitest](https://vitest.dev/)
- **Location:** In the `tests/` directory within each package (e.g., `apps/server/tests/`)
**Running unit tests:**
```bash
# Run all server unit tests
npm run test:server
# Run with coverage report
npm run test:server:coverage
# Run package tests
npm run test:packages
# Run in watch mode during development
npx vitest --watch
```
**Unit Test Guidelines:**
- Keep tests small and focused on one behavior
- Use descriptive test names: `it('should return null when user is not found')`
- Follow the AAA pattern: Arrange, Act, Assert
- Mock external dependencies to isolate the unit under test
- Aim for meaningful coverage, not just line coverage
### Writing Tests
#### When to Write Tests
- **New features:** All new features should include tests
- **Bug fixes:** Add a test that reproduces the bug before fixing
- **Refactoring:** Ensure existing tests pass after refactoring
- **Public APIs:** All public APIs must have test coverage
### CI/CD Pipeline
Automaker uses **GitHub Actions** for continuous integration. Every pull request triggers automated checks.
#### CI Checks
The following checks must pass before your PR can be merged:
| Check | Description |
| ----------------- | --------------------------------------------- |
| **Format** | Verifies code is formatted with Prettier |
| **Build** | Ensures the project compiles without errors |
| **Package Tests** | Runs tests for shared `@automaker/*` packages |
| **Server Tests** | Runs server unit tests with coverage |
#### CI Testing Environment
For CI environments, Automaker supports a mock agent mode:
```bash
# Enable mock agent mode for CI testing
AUTOMAKER_MOCK_AGENT=true npm run test
```
This allows tests to run without requiring a real Claude API connection.
#### Viewing CI Results
1. Go to your PR on GitHub
2. Scroll to the "Checks" section at the bottom
3. Click on any failed check to see detailed logs
4. Fix issues locally and push updates
#### Common CI Failures
| Issue | Solution |
| ------------------- | --------------------------------------------- |
| Format check failed | Run `npm run format` locally |
| Build failed | Run `npm run build` and fix TypeScript errors |
| Tests failed | Run `npm run test:all` locally to reproduce |
| Coverage decreased | Add tests for new code paths |
### Coverage Requirements
While we don't enforce strict coverage percentages, we expect:
- **New features:** Should include comprehensive tests
- **Bug fixes:** Should include a regression test
- **Critical paths:** Must have test coverage (authentication, data persistence, etc.)
To view coverage reports locally:
```bash
npm run test:server:coverage
```
This generates an HTML report you can open in your browser to see which lines are covered.
---
## Issue Reporting
Found a bug or have an idea for a new feature? We'd love to hear from you! This section explains how to report issues effectively.
### Bug Reports
When reporting a bug, please provide as much information as possible to help us understand and reproduce the issue.
#### Before Reporting
1. **Search existing issues** - Check if the bug has already been reported
2. **Try the latest version** - Make sure you're running the latest version of Automaker
3. **Reproduce the issue** - Verify you can consistently reproduce the bug
#### Bug Report Template
When creating a bug report, include:
- **Title:** A clear, descriptive title summarizing the issue
- **Environment:**
- Operating System and version
- Node.js version (`node --version`)
- Automaker version or commit hash
- **Steps to Reproduce:** Numbered list of steps to reproduce the bug
- **Expected Behavior:** What you expected to happen
- **Actual Behavior:** What actually happened
- **Logs/Screenshots:** Any relevant error messages, console output, or screenshots
**Example Bug Report:**
```markdown
## Bug: WebSocket connection drops after 5 minutes of inactivity
### Environment
- OS: Windows 11
- Node.js: 22.11.0
- Automaker: commit abc1234
### Steps to Reproduce
1. Start the application with `npm run dev:web`
2. Open the Kanban board
3. Leave the browser tab open for 5+ minutes without interaction
4. Try to move a card
### Expected Behavior
The card should move to the new column.
### Actual Behavior
The UI shows "Connection lost" and the card doesn't move.
### Logs
[WebSocket] Connection closed: 1006
```
### Feature Requests
We welcome ideas for improving Automaker! Here's how to submit a feature request:
#### Before Requesting
1. **Check existing issues** - Your idea may already be proposed or in development
2. **Consider scope** - Think about whether the feature fits Automaker's mission as an autonomous AI development studio
#### Feature Request Template
A good feature request includes:
- **Title:** A brief, descriptive title
- **Problem Statement:** What problem does this feature solve?
- **Proposed Solution:** How do you envision this working?
- **Alternatives Considered:** What other approaches did you consider?
- **Additional Context:** Mockups, examples, or references that help explain your idea
**Example Feature Request:**
```markdown
## Feature: Dark Mode Support
### Problem Statement
Working late at night, the bright UI causes eye strain and doesn't match
my system's dark theme preference.
### Proposed Solution
Add a theme toggle in the settings panel that allows switching between
light and dark modes. Ideally, it should also detect system preference.
### Alternatives Considered
- Browser extension to force dark mode (doesn't work well with custom styling)
- Custom CSS override (breaks with updates)
### Additional Context
Similar to how VS Code handles themes - a dropdown in settings with
immediate preview.
```
### Security Issues
**Important:** If you discover a security vulnerability, please do NOT open a public issue. Instead:
1. Join our [Discord server](https://discord.gg/jjem7aEDKU) and send a direct message to the user `@webdevcody`
2. Include detailed steps to reproduce
3. Allow time for us to address the issue before public disclosure
We take security seriously and appreciate responsible disclosure.
---
For license and contribution terms, see the [LICENSE](LICENSE) file in the repository root and the [README.md](README.md#license) for more details.
---
Thank you for contributing to Automaker!

164
Dockerfile Normal file
View File

@@ -0,0 +1,164 @@
# Automaker Multi-Stage Dockerfile
# Single Dockerfile for both server and UI builds
# Usage:
# docker build --target server -t automaker-server .
# docker build --target ui -t automaker-ui .
# Or use docker-compose which selects targets automatically
# =============================================================================
# BASE STAGE - Common setup for all builds (DRY: defined once, used by all)
# =============================================================================
FROM node:22-alpine AS base
# Install build dependencies for native modules (node-pty)
RUN apk add --no-cache python3 make g++
WORKDIR /app
# Copy root package files
COPY package*.json ./
# Copy all libs package.json files (centralized - add new libs here)
COPY libs/types/package*.json ./libs/types/
COPY libs/utils/package*.json ./libs/utils/
COPY libs/prompts/package*.json ./libs/prompts/
COPY libs/platform/package*.json ./libs/platform/
COPY libs/model-resolver/package*.json ./libs/model-resolver/
COPY libs/dependency-resolver/package*.json ./libs/dependency-resolver/
COPY libs/git-utils/package*.json ./libs/git-utils/
# Copy scripts (needed by npm workspace)
COPY scripts ./scripts
# =============================================================================
# SERVER BUILD STAGE
# =============================================================================
FROM base AS server-builder
# Copy server-specific package.json
COPY apps/server/package*.json ./apps/server/
# Install dependencies (--ignore-scripts to skip husky/prepare, then rebuild native modules)
RUN npm ci --ignore-scripts && npm rebuild node-pty
# Copy all source files
COPY libs ./libs
COPY apps/server ./apps/server
# Build packages in dependency order, then build server
RUN npm run build:packages && npm run build --workspace=apps/server
# =============================================================================
# SERVER PRODUCTION STAGE
# =============================================================================
FROM node:22-alpine AS server
# Install git, curl, bash (for terminal), su-exec (for user switching), and GitHub CLI (pinned version, multi-arch)
RUN apk add --no-cache git curl bash su-exec && \
GH_VERSION="2.63.2" && \
ARCH=$(uname -m) && \
case "$ARCH" in \
x86_64) GH_ARCH="amd64" ;; \
aarch64|arm64) GH_ARCH="arm64" ;; \
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
esac && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh /usr/local/bin/gh && \
rm -rf gh.tar.gz gh_${GH_VERSION}_linux_${GH_ARCH}
# Install Claude CLI globally
RUN npm install -g @anthropic-ai/claude-code
WORKDIR /app
# Create non-root user with home directory
RUN addgroup -g 1001 -S automaker && \
adduser -S automaker -u 1001 -h /home/automaker && \
mkdir -p /home/automaker && \
chown automaker:automaker /home/automaker
# Copy root package.json (needed for workspace resolution)
COPY --from=server-builder /app/package*.json ./
# Copy built libs (workspace packages are symlinked in node_modules)
COPY --from=server-builder /app/libs ./libs
# Copy built server
COPY --from=server-builder /app/apps/server/dist ./apps/server/dist
COPY --from=server-builder /app/apps/server/package*.json ./apps/server/
# Copy node_modules (includes symlinks to libs)
COPY --from=server-builder /app/node_modules ./node_modules
# Create data and projects directories
RUN mkdir -p /data /projects && chown automaker:automaker /data /projects
# Configure git for mounted volumes and authentication
# Use --system so it's not overwritten by mounted user .gitconfig
RUN git config --system --add safe.directory '*' && \
# Use gh as credential helper (works with GH_TOKEN env var)
git config --system credential.helper '!gh auth git-credential'
# Copy entrypoint script for fixing permissions on mounted volumes
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Note: We stay as root here so entrypoint can fix permissions
# The entrypoint script will switch to automaker user before running the command
# Environment variables
ENV PORT=3008
ENV DATA_DIR=/data
ENV HOME=/home/automaker
# Expose port
EXPOSE 3008
# Health check (using curl since it's already installed, more reliable than busybox wget)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3008/api/health || exit 1
# Use entrypoint to fix permissions before starting
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Start server
CMD ["node", "apps/server/dist/index.js"]
# =============================================================================
# UI BUILD STAGE
# =============================================================================
FROM base AS ui-builder
# Copy UI-specific package.json
COPY apps/ui/package*.json ./apps/ui/
# Install dependencies (--ignore-scripts to skip husky and build:packages in prepare script)
RUN npm ci --ignore-scripts
# Copy all source files
COPY libs ./libs
COPY apps/ui ./apps/ui
# Build packages in dependency order, then build UI
# VITE_SERVER_URL tells the UI where to find the API server
# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com
ARG VITE_SERVER_URL=http://localhost:3008
ENV VITE_SKIP_ELECTRON=true
ENV VITE_SERVER_URL=${VITE_SERVER_URL}
RUN npm run build:packages && npm run build --workspace=apps/ui
# =============================================================================
# UI PRODUCTION STAGE
# =============================================================================
FROM nginx:alpine AS ui
# Copy built files
COPY --from=ui-builder /app/apps/ui/dist /usr/share/nginx/html
# Copy nginx config for SPA routing
COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

172
README.md
View File

@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="apps/ui/public/readme_logo.png" alt="Automaker Logo" height="80" /> <img src="apps/ui/public/readme_logo.svg" alt="Automaker Logo" height="80" />
</p> </p>
> **[!TIP]** > **[!TIP]**
@@ -81,22 +81,6 @@ Automaker leverages the [Claude Agent SDK](https://www.npmjs.com/package/@anthro
The future of software development is **agentic coding**—where developers become architects directing AI agents rather than manual coders. Automaker puts this future in your hands today, letting you experience what it's like to build software 10x faster with AI agents handling the implementation while you focus on architecture and business logic. The future of software development is **agentic coding**—where developers become architects directing AI agents rather than manual coders. Automaker puts this future in your hands today, letting you experience what it's like to build software 10x faster with AI agents handling the implementation while you focus on architecture and business logic.
---
> **[!CAUTION]**
>
> ## Security Disclaimer
>
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
>
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
>
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
>
> **[Read the full disclaimer](./DISCLAIMER.md)**
---
## Community & Support ## Community & Support
Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows. Join the **Agentic Jumpstart** to connect with other builders exploring **agentic coding** and autonomous development workflows.
@@ -136,29 +120,37 @@ npm install
# 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly) # 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly)
npm run build:packages npm run build:packages
# 4. Set up authentication (skip if using Claude Code CLI) # 4. Start Automaker (production mode)
# If using Claude Code CLI: credentials are detected automatically npm run start
# If using API key directly, choose one method:
# Option A: Environment variable
export ANTHROPIC_API_KEY="sk-ant-..."
# Option B: Create .env file in project root
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
# 5. Start Automaker (interactive launcher)
npm run dev
# Choose between: # Choose between:
# 1. Web Application (browser at localhost:3007) # 1. Web Application (browser at localhost:3007)
# 2. Desktop Application (Electron - recommended) # 2. Desktop Application (Electron - recommended)
``` ```
**Note:** The `npm run dev` command will: **Note:** The `npm run start` command will:
- Check for dependencies and install if needed - Check for dependencies and install if needed
- Install Playwright browsers for E2E tests - Build the application if needed
- Kill any processes on ports 3007/3008 - Kill any processes on ports 3007/3008
- Present an interactive menu to choose your run mode - Present an interactive menu to choose your run mode
- Run in production mode (no hot reload)
**Authentication Setup:** On first run, Automaker will automatically show a setup wizard where you can configure authentication. You can choose to:
- Use **Claude Code CLI** (recommended) - Automaker will detect your CLI credentials automatically
- Enter an **API key** directly in the wizard
If you prefer to set up authentication before running (e.g., for headless deployments or CI/CD), you can set it manually:
```bash
# Option A: Environment variable
export ANTHROPIC_API_KEY="sk-ant-..."
# Option B: Create .env file in project root
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
```
**For Development:** If you want to develop on Automaker with Vite live reload and hot module replacement, use `npm run dev` instead. This will start the development server with fast refresh and instant updates as you make changes.
## How to Run ## How to Run
@@ -223,14 +215,111 @@ npm run build:electron:linux # Linux (AppImage + DEB, x64)
#### Docker Deployment #### Docker Deployment
Docker provides the most secure way to run Automaker by isolating it from your host filesystem.
```bash ```bash
# Build and run with Docker Compose (recommended for security) # Build and run with Docker Compose
docker-compose up -d docker-compose up -d
# Access at http://localhost:3007 # Access UI at http://localhost:3007
# API at http://localhost:3008 # API at http://localhost:3008
# View logs
docker-compose logs -f
# Stop containers
docker-compose down
``` ```
##### Configuration
Create a `.env` file in the project root if using API key authentication:
```bash
# Optional: Anthropic API key (not needed if using Claude CLI authentication)
ANTHROPIC_API_KEY=sk-ant-...
```
**Note:** Most users authenticate via Claude CLI instead of API keys. See [Claude CLI Authentication](#claude-cli-authentication-optional) below.
##### Working with Projects (Host Directory Access)
By default, the container is isolated from your host filesystem. To work on projects from your host machine, create a `docker-compose.override.yml` file (gitignored):
```yaml
services:
server:
volumes:
# Mount your project directories
- /path/to/your/project:/projects/your-project
```
##### Claude CLI Authentication (Optional)
To use Claude Code CLI authentication instead of an API key, mount your Claude CLI config directory:
```yaml
services:
server:
volumes:
# Linux/macOS
- ~/.claude:/home/automaker/.claude
# Windows
- C:/Users/YourName/.claude:/home/automaker/.claude
```
**Note:** The Claude CLI config must be writable (do not use `:ro` flag) as the CLI writes debug files.
##### GitHub CLI Authentication (For Git Push/PR Operations)
To enable git push and GitHub CLI operations inside the container:
```yaml
services:
server:
volumes:
# Mount GitHub CLI config
# Linux/macOS
- ~/.config/gh:/home/automaker/.config/gh
# Windows
- 'C:/Users/YourName/AppData/Roaming/GitHub CLI:/home/automaker/.config/gh'
# Mount git config for user identity (name, email)
- ~/.gitconfig:/home/automaker/.gitconfig:ro
environment:
# GitHub token (required on Windows where tokens are in Credential Manager)
# Get your token with: gh auth token
- GH_TOKEN=${GH_TOKEN}
```
Then add `GH_TOKEN` to your `.env` file:
```bash
GH_TOKEN=gho_your_github_token_here
```
##### Complete docker-compose.override.yml Example
```yaml
services:
server:
volumes:
# Your projects
- /path/to/project1:/projects/project1
- /path/to/project2:/projects/project2
# Authentication configs
- ~/.claude:/home/automaker/.claude
- ~/.config/gh:/home/automaker/.config/gh
- ~/.gitconfig:/home/automaker/.gitconfig:ro
environment:
- GH_TOKEN=${GH_TOKEN}
```
##### Architecture Support
The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build.
### Testing ### Testing
#### End-to-End Tests (Playwright) #### End-to-End Tests (Playwright)
@@ -527,10 +616,27 @@ data/
└── {sessionId}.json └── {sessionId}.json
``` ```
---
> **[!CAUTION]**
>
> ## Security Disclaimer
>
> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.**
>
> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it.
>
> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine.
>
> **[Read the full disclaimer](./DISCLAIMER.md)**
---
## Learn More ## Learn More
### Documentation ### Documentation
- [Contributing Guide](./CONTRIBUTING.md) - How to contribute to Automaker
- [Project Documentation](./docs/) - Architecture guides, patterns, and developer docs - [Project Documentation](./docs/) - Architecture guides, patterns, and developer docs
- [Docker Isolation Guide](./docs/docker-isolation.md) - Security-focused Docker deployment - [Docker Isolation Guide](./docs/docker-isolation.md) - Security-focused Docker deployment
- [Shared Packages Guide](./docs/llm-shared-packages.md) - Using monorepo packages - [Shared Packages Guide](./docs/llm-shared-packages.md) - Using monorepo packages

17
TODO.md Normal file
View File

@@ -0,0 +1,17 @@
# Bugs
- Setting the default model does not seem like it works.
# UX
- Consolidate all models to a single place in the settings instead of having AI profiles and all this other stuff
- Simplify the create feature modal. It should just be one page. I don't need nessa tabs and all these nested buttons. It's too complex.
- added to do's list checkbox directly into the card so as it's going through if there's any to do items we can see those update live
- When the feature is done, I want to see a summary of the LLM. That's the first thing I should see when I double click the card.
- I went away to mass edit all my features. For example, when I created a new project, it added auto testing on every single feature card. Now I have to manually go through one by one and change those. Have a way to mass edit those, the configuration of all them.
- Double check and debug if there's memory leaks. It seems like the memory of automaker grows like 3 gigabytes. It's 5gb right now and I'm running three different cursor cli features implementing at the same time.
- Typing in the text area of the plan mode was super laggy.
- When I have a bunch of features running at the same time, it seems like I cannot edit the features in the backlog. Like they don't persist their file changes and I think this is because of the secure FS file has an internal queue to prevent hitting that file open write limit. We may have to reconsider refactoring away from file system and do Postgres or SQLite or something.
- modals are not scrollable if height of the screen is small enough
- and the Agent Runner add an archival button for the new sessions.
- investigate a potential issue with the feature cards not refreshing. I see a lock icon on the feature card But it doesn't go away until I open the card and edit it and I turn the testing mode off. I think there's like a refresh sync issue.

View File

@@ -24,7 +24,7 @@ ALLOWED_ROOT_DIRECTORY=
# CORS origin - which domains can access the API # CORS origin - which domains can access the API
# Use "*" for development, set specific origin for production # Use "*" for development, set specific origin for production
CORS_ORIGIN=* CORS_ORIGIN=http://localhost:3007
# ============================================ # ============================================
# OPTIONAL - Server # OPTIONAL - Server
@@ -48,3 +48,15 @@ TERMINAL_ENABLED=true
TERMINAL_PASSWORD= TERMINAL_PASSWORD=
ENABLE_REQUEST_LOGGING=false ENABLE_REQUEST_LOGGING=false
# ============================================
# OPTIONAL - Debugging
# ============================================
# Enable raw output logging for agent streams (default: false)
# When enabled, saves unprocessed stream events to raw-output.jsonl
# in each feature's directory (.automaker/features/{id}/raw-output.jsonl)
# Useful for debugging provider streaming issues, improving log parsing,
# or analyzing how different providers (Claude, Cursor) stream responses
# Note: This adds disk I/O overhead, only enable when debugging
AUTOMAKER_DEBUG_RAW_OUTPUT=false

View File

@@ -1,67 +0,0 @@
# Automaker Backend Server
# Multi-stage build for minimal production image
# Build stage
FROM node:20-alpine AS builder
# Install build dependencies for native modules (node-pty)
RUN apk add --no-cache python3 make g++
WORKDIR /app
# Copy package files and scripts needed for postinstall
COPY package*.json ./
COPY apps/server/package*.json ./apps/server/
COPY scripts ./scripts
# Install dependencies
RUN npm ci --workspace=apps/server
# Copy source
COPY apps/server ./apps/server
# Build TypeScript
RUN npm run build --workspace=apps/server
# Production stage
FROM node:20-alpine
# Install git, curl, and GitHub CLI (pinned version for reproducible builds)
RUN apk add --no-cache git curl && \
GH_VERSION="2.63.2" && \
curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \
tar -xzf gh.tar.gz && \
mv "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \
rm -rf gh.tar.gz "gh_${GH_VERSION}_linux_amd64"
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S automaker && \
adduser -S automaker -u 1001
# Copy built files and production dependencies
COPY --from=builder /app/apps/server/dist ./dist
COPY --from=builder /app/apps/server/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
# Create data directory
RUN mkdir -p /data && chown automaker:automaker /data
# Switch to non-root user
USER automaker
# Environment variables
ENV NODE_ENV=production
ENV PORT=3008
ENV DATA_DIR=/data
# Expose port
EXPOSE 3008
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3008/api/health || exit 1
# Start server
CMD ["node", "dist/index.js"]

View File

@@ -1,14 +1,18 @@
{ {
"name": "@automaker/server", "name": "@automaker/server",
"version": "0.1.0", "version": "0.8.0",
"description": "Backend server for Automaker - provides API for both web and Electron modes", "description": "Backend server for Automaker - provides API for both web and Electron modes",
"author": "AutoMaker Team", "author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"private": true, "private": true,
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"dev": "tsx watch src/index.ts", "dev": "tsx watch src/index.ts",
"dev:test": "tsx src/index.ts",
"build": "tsc", "build": "tsc",
"start": "node dist/index.js", "start": "node dist/index.js",
"lint": "eslint src/", "lint": "eslint src/",
@@ -20,31 +24,35 @@
"test:unit": "vitest run tests/unit" "test:unit": "vitest run tests/unit"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.72", "@anthropic-ai/claude-agent-sdk": "0.1.76",
"@automaker/dependency-resolver": "^1.0.0", "@automaker/dependency-resolver": "1.0.0",
"@automaker/git-utils": "^1.0.0", "@automaker/git-utils": "1.0.0",
"@automaker/model-resolver": "^1.0.0", "@automaker/model-resolver": "1.0.0",
"@automaker/platform": "^1.0.0", "@automaker/platform": "1.0.0",
"@automaker/prompts": "^1.0.0", "@automaker/prompts": "1.0.0",
"@automaker/types": "^1.0.0", "@automaker/types": "1.0.0",
"@automaker/utils": "^1.0.0", "@automaker/utils": "1.0.0",
"cors": "^2.8.5", "@modelcontextprotocol/sdk": "1.25.1",
"dotenv": "^17.2.3", "cookie-parser": "1.4.7",
"express": "^5.2.1", "cors": "2.8.5",
"morgan": "^1.10.1", "dotenv": "17.2.3",
"express": "5.2.1",
"morgan": "1.10.1",
"node-pty": "1.1.0-beta41", "node-pty": "1.1.0-beta41",
"ws": "^8.18.3" "ws": "8.18.3"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.19", "@types/cookie": "0.6.0",
"@types/express": "^5.0.6", "@types/cookie-parser": "1.4.10",
"@types/morgan": "^1.9.10", "@types/cors": "2.8.19",
"@types/node": "^22", "@types/express": "5.0.6",
"@types/ws": "^8.18.1", "@types/morgan": "1.9.10",
"@vitest/coverage-v8": "^4.0.16", "@types/node": "22.19.3",
"@vitest/ui": "^4.0.16", "@types/ws": "8.18.1",
"tsx": "^4.21.0", "@vitest/coverage-v8": "4.0.16",
"typescript": "^5", "@vitest/ui": "4.0.16",
"vitest": "^4.0.16" "tsx": "4.21.0",
"typescript": "5.9.3",
"vitest": "4.0.16"
} }
} }

View File

@@ -9,15 +9,22 @@
import express from 'express'; import express from 'express';
import cors from 'cors'; import cors from 'cors';
import morgan from 'morgan'; import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import cookie from 'cookie';
import { WebSocketServer, WebSocket } from 'ws'; import { WebSocketServer, WebSocket } from 'ws';
import { createServer } from 'http'; import { createServer } from 'http';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import { createEventEmitter, type EventEmitter } from './lib/events.js'; import { createEventEmitter, type EventEmitter } from './lib/events.js';
import { initAllowedPaths } from '@automaker/platform'; import { initAllowedPaths } from '@automaker/platform';
import { authMiddleware, getAuthStatus } from './lib/auth.js'; import { createLogger } from '@automaker/utils';
const logger = createLogger('Server');
import { authMiddleware, validateWsConnectionToken, checkRawAuthentication } from './lib/auth.js';
import { requireJsonContentType } from './middleware/require-json-content-type.js';
import { createAuthRoutes } from './routes/auth/index.js';
import { createFsRoutes } from './routes/fs/index.js'; import { createFsRoutes } from './routes/fs/index.js';
import { createHealthRoutes } from './routes/health/index.js'; import { createHealthRoutes, createDetailedHandler } from './routes/health/index.js';
import { createAgentRoutes } from './routes/agent/index.js'; import { createAgentRoutes } from './routes/agent/index.js';
import { createSessionsRoutes } from './routes/sessions/index.js'; import { createSessionsRoutes } from './routes/sessions/index.js';
import { createFeaturesRoutes } from './routes/features/index.js'; import { createFeaturesRoutes } from './routes/features/index.js';
@@ -50,6 +57,12 @@ import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js'; import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js'; import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
import { cleanupStaleValidations } from './routes/github/routes/validation-common.js'; import { cleanupStaleValidations } from './routes/github/routes/validation-common.js';
import { createMCPRoutes } from './routes/mcp/index.js';
import { MCPTestService } from './services/mcp-test-service.js';
import { createPipelineRoutes } from './routes/pipeline/index.js';
import { pipelineService } from './services/pipeline-service.js';
import { createIdeationRoutes } from './routes/ideation/index.js';
import { IdeationService } from './services/ideation-service.js';
// Load environment variables // Load environment variables
dotenv.config(); dotenv.config();
@@ -62,7 +75,7 @@ const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; /
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY; const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
if (!hasAnthropicKey) { if (!hasAnthropicKey) {
console.warn(` logger.warn(`
╔═══════════════════════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════════════════════╗
║ ⚠️ WARNING: No Claude authentication configured ║ ║ ⚠️ WARNING: No Claude authentication configured ║
║ ║ ║ ║
@@ -75,7 +88,7 @@ if (!hasAnthropicKey) {
╚═══════════════════════════════════════════════════════════════════════╝ ╚═══════════════════════════════════════════════════════════════════════╝
`); `);
} else { } else {
console.log('[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)'); logger.info('✓ ANTHROPIC_API_KEY detected (API key auth)');
} }
// Initialize security // Initialize security
@@ -87,7 +100,7 @@ const app = express();
// Middleware // Middleware
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var) // Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
if (ENABLE_REQUEST_LOGGING) { if (ENABLE_REQUEST_LOGGING) {
morgan.token('status-colored', (req, res) => { morgan.token('status-colored', (_req, res) => {
const status = res.statusCode; const status = res.statusCode;
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
@@ -101,13 +114,47 @@ if (ENABLE_REQUEST_LOGGING) {
}) })
); );
} }
// CORS configuration
// When using credentials (cookies), origin cannot be '*'
// We dynamically allow the requesting origin for local development
app.use( app.use(
cors({ cors({
origin: process.env.CORS_ORIGIN || '*', origin: (origin, callback) => {
// Allow requests with no origin (like mobile apps, curl, Electron)
if (!origin) {
callback(null, true);
return;
}
// If CORS_ORIGIN is set, use it (can be comma-separated list)
const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim());
if (allowedOrigins && allowedOrigins.length > 0 && allowedOrigins[0] !== '*') {
if (allowedOrigins.includes(origin)) {
callback(null, origin);
} else {
callback(new Error('Not allowed by CORS'));
}
return;
}
// For local development, allow localhost origins
if (
origin.startsWith('http://localhost:') ||
origin.startsWith('http://127.0.0.1:') ||
origin.startsWith('http://[::1]:')
) {
callback(null, origin);
return;
}
// Reject other origins by default for security
callback(new Error('Not allowed by CORS'));
},
credentials: true, credentials: true,
}) })
); );
app.use(express.json({ limit: '50mb' })); app.use(express.json({ limit: '50mb' }));
app.use(cookieParser());
// Create shared event emitter for streaming // Create shared event emitter for streaming
const events: EventEmitter = createEventEmitter(); const events: EventEmitter = createEventEmitter();
@@ -119,11 +166,13 @@ const agentService = new AgentService(DATA_DIR, events, settingsService);
const featureLoader = new FeatureLoader(); const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService); const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService(); const claudeUsageService = new ClaudeUsageService();
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);
// Initialize services // Initialize services
(async () => { (async () => {
await agentService.initialize(); await agentService.initialize();
console.log('[Server] Agent service initialized'); logger.info('Agent service initialized');
})(); })();
// Run stale validation cleanup every hour to prevent memory leaks from crashed validations // Run stale validation cleanup every hour to prevent memory leaks from crashed validations
@@ -131,22 +180,30 @@ const VALIDATION_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
setInterval(() => { setInterval(() => {
const cleaned = cleanupStaleValidations(); const cleaned = cleanupStaleValidations();
if (cleaned > 0) { if (cleaned > 0) {
console.log(`[Server] Cleaned up ${cleaned} stale validation entries`); logger.info(`Cleaned up ${cleaned} stale validation entries`);
} }
}, VALIDATION_CLEANUP_INTERVAL_MS); }, VALIDATION_CLEANUP_INTERVAL_MS);
// Mount API routes - health is unauthenticated for monitoring // Require Content-Type: application/json for all API POST/PUT/PATCH requests
// This helps prevent CSRF and content-type confusion attacks
app.use('/api', requireJsonContentType);
// Mount API routes - health and auth are unauthenticated
app.use('/api/health', createHealthRoutes()); app.use('/api/health', createHealthRoutes());
app.use('/api/auth', createAuthRoutes());
// Apply authentication to all other routes // Apply authentication to all other routes
app.use('/api', authMiddleware); app.use('/api', authMiddleware);
// Protected health endpoint with detailed info
app.get('/api/health/detailed', createDetailedHandler());
app.use('/api/fs', createFsRoutes(events)); app.use('/api/fs', createFsRoutes(events));
app.use('/api/agent', createAgentRoutes(agentService, events)); app.use('/api/agent', createAgentRoutes(agentService, events));
app.use('/api/sessions', createSessionsRoutes(agentService)); app.use('/api/sessions', createSessionsRoutes(agentService));
app.use('/api/features', createFeaturesRoutes(featureLoader)); app.use('/api/features', createFeaturesRoutes(featureLoader));
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService)); app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
app.use('/api/enhance-prompt', createEnhancePromptRoutes()); app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
app.use('/api/worktree', createWorktreeRoutes()); app.use('/api/worktree', createWorktreeRoutes());
app.use('/api/git', createGitRoutes()); app.use('/api/git', createGitRoutes());
app.use('/api/setup', createSetupRoutes()); app.use('/api/setup', createSetupRoutes());
@@ -162,6 +219,9 @@ app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/github', createGitHubRoutes(events, settingsService)); app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService)); app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService)); app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
app.use('/api/mcp', createMCPRoutes(mcpTestService));
app.use('/api/pipeline', createPipelineRoutes(pipelineService));
app.use('/api/ideation', createIdeationRoutes(events, ideationService, featureLoader));
// Create HTTP server // Create HTTP server
const server = createServer(app); const server = createServer(app);
@@ -171,10 +231,55 @@ const wss = new WebSocketServer({ noServer: true });
const terminalWss = new WebSocketServer({ noServer: true }); const terminalWss = new WebSocketServer({ noServer: true });
const terminalService = getTerminalService(); const terminalService = getTerminalService();
/**
* Authenticate WebSocket upgrade requests
* Checks for API key in header/query, session token in header/query, OR valid session cookie
*/
function authenticateWebSocket(request: import('http').IncomingMessage): boolean {
const url = new URL(request.url || '', `http://${request.headers.host}`);
// Convert URL search params to query object
const query: Record<string, string | undefined> = {};
url.searchParams.forEach((value, key) => {
query[key] = value;
});
// Parse cookies from header
const cookieHeader = request.headers.cookie;
const cookies = cookieHeader ? cookie.parse(cookieHeader) : {};
// Use shared authentication logic for standard auth methods
if (
checkRawAuthentication(
request.headers as Record<string, string | string[] | undefined>,
query,
cookies
)
) {
return true;
}
// Additionally check for short-lived WebSocket connection token (WebSocket-specific)
const wsToken = url.searchParams.get('wsToken');
if (wsToken && validateWsConnectionToken(wsToken)) {
return true;
}
return false;
}
// Handle HTTP upgrade requests manually to route to correct WebSocket server // Handle HTTP upgrade requests manually to route to correct WebSocket server
server.on('upgrade', (request, socket, head) => { server.on('upgrade', (request, socket, head) => {
const { pathname } = new URL(request.url || '', `http://${request.headers.host}`); const { pathname } = new URL(request.url || '', `http://${request.headers.host}`);
// Authenticate all WebSocket connections
if (!authenticateWebSocket(request)) {
logger.info('Authentication failed, rejecting connection');
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
if (pathname === '/api/events') { if (pathname === '/api/events') {
wss.handleUpgrade(request, socket, head, (ws) => { wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request); wss.emit('connection', ws, request);
@@ -190,11 +295,11 @@ server.on('upgrade', (request, socket, head) => {
// Events WebSocket connection handler // Events WebSocket connection handler
wss.on('connection', (ws: WebSocket) => { wss.on('connection', (ws: WebSocket) => {
console.log('[WebSocket] Client connected, ready state:', ws.readyState); logger.info('Client connected, ready state:', ws.readyState);
// Subscribe to all events and forward to this client // Subscribe to all events and forward to this client
const unsubscribe = events.subscribe((type, payload) => { const unsubscribe = events.subscribe((type, payload) => {
console.log('[WebSocket] Event received:', { logger.info('Event received:', {
type, type,
hasPayload: !!payload, hasPayload: !!payload,
payloadKeys: payload ? Object.keys(payload) : [], payloadKeys: payload ? Object.keys(payload) : [],
@@ -204,27 +309,24 @@ wss.on('connection', (ws: WebSocket) => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
const message = JSON.stringify({ type, payload }); const message = JSON.stringify({ type, payload });
console.log('[WebSocket] Sending event to client:', { logger.info('Sending event to client:', {
type, type,
messageLength: message.length, messageLength: message.length,
sessionId: (payload as any)?.sessionId, sessionId: (payload as any)?.sessionId,
}); });
ws.send(message); ws.send(message);
} else { } else {
console.log( logger.info('WARNING: Cannot send event, WebSocket not open. ReadyState:', ws.readyState);
'[WebSocket] WARNING: Cannot send event, WebSocket not open. ReadyState:',
ws.readyState
);
} }
}); });
ws.on('close', () => { ws.on('close', () => {
console.log('[WebSocket] Client disconnected'); logger.info('Client disconnected');
unsubscribe(); unsubscribe();
}); });
ws.on('error', (error) => { ws.on('error', (error) => {
console.error('[WebSocket] ERROR:', error); logger.error('ERROR:', error);
unsubscribe(); unsubscribe();
}); });
}); });
@@ -251,24 +353,24 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
const sessionId = url.searchParams.get('sessionId'); const sessionId = url.searchParams.get('sessionId');
const token = url.searchParams.get('token'); const token = url.searchParams.get('token');
console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`); logger.info(`Connection attempt for session: ${sessionId}`);
// Check if terminal is enabled // Check if terminal is enabled
if (!isTerminalEnabled()) { if (!isTerminalEnabled()) {
console.log('[Terminal WS] Terminal is disabled'); logger.info('Terminal is disabled');
ws.close(4003, 'Terminal access is disabled'); ws.close(4003, 'Terminal access is disabled');
return; return;
} }
// Validate token if password is required // Validate token if password is required
if (isTerminalPasswordRequired() && !validateTerminalToken(token || undefined)) { if (isTerminalPasswordRequired() && !validateTerminalToken(token || undefined)) {
console.log('[Terminal WS] Invalid or missing token'); logger.info('Invalid or missing token');
ws.close(4001, 'Authentication required'); ws.close(4001, 'Authentication required');
return; return;
} }
if (!sessionId) { if (!sessionId) {
console.log('[Terminal WS] No session ID provided'); logger.info('No session ID provided');
ws.close(4002, 'Session ID required'); ws.close(4002, 'Session ID required');
return; return;
} }
@@ -276,12 +378,12 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
// Check if session exists // Check if session exists
const session = terminalService.getSession(sessionId); const session = terminalService.getSession(sessionId);
if (!session) { if (!session) {
console.log(`[Terminal WS] Session ${sessionId} not found`); logger.info(`Session ${sessionId} not found`);
ws.close(4004, 'Session not found'); ws.close(4004, 'Session not found');
return; return;
} }
console.log(`[Terminal WS] Client connected to session ${sessionId}`); logger.info(`Client connected to session ${sessionId}`);
// Track this connection // Track this connection
if (!terminalConnections.has(sessionId)) { if (!terminalConnections.has(sessionId)) {
@@ -397,15 +499,15 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
break; break;
default: default:
console.warn(`[Terminal WS] Unknown message type: ${msg.type}`); logger.warn(`Unknown message type: ${msg.type}`);
} }
} catch (error) { } catch (error) {
console.error('[Terminal WS] Error processing message:', error); logger.error('Error processing message:', error);
} }
}); });
ws.on('close', () => { ws.on('close', () => {
console.log(`[Terminal WS] Client disconnected from session ${sessionId}`); logger.info(`Client disconnected from session ${sessionId}`);
unsubscribeData(); unsubscribeData();
unsubscribeExit(); unsubscribeExit();
@@ -424,7 +526,7 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
}); });
ws.on('error', (error) => { ws.on('error', (error) => {
console.error(`[Terminal WS] Error on session ${sessionId}:`, error); logger.error(`Error on session ${sessionId}:`, error);
unsubscribeData(); unsubscribeData();
unsubscribeExit(); unsubscribeExit();
}); });
@@ -439,7 +541,7 @@ const startServer = (port: number) => {
: 'enabled' : 'enabled'
: 'disabled'; : 'disabled';
const portStr = port.toString().padEnd(4); const portStr = port.toString().padEnd(4);
console.log(` logger.info(`
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
║ Automaker Backend Server ║ ║ Automaker Backend Server ║
╠═══════════════════════════════════════════════════════╣ ╠═══════════════════════════════════════════════════════╣
@@ -454,7 +556,7 @@ const startServer = (port: number) => {
server.on('error', (error: NodeJS.ErrnoException) => { server.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EADDRINUSE') { if (error.code === 'EADDRINUSE') {
console.error(` logger.error(`
╔═══════════════════════════════════════════════════════╗ ╔═══════════════════════════════════════════════════════╗
║ ❌ ERROR: Port ${port} is already in use ║ ║ ❌ ERROR: Port ${port} is already in use ║
╠═══════════════════════════════════════════════════════╣ ╠═══════════════════════════════════════════════════════╣
@@ -474,7 +576,7 @@ const startServer = (port: number) => {
`); `);
process.exit(1); process.exit(1);
} else { } else {
console.error('[Server] Error starting server:', error); logger.error('Error starting server:', error);
process.exit(1); process.exit(1);
} }
}); });
@@ -484,19 +586,19 @@ startServer(PORT);
// Graceful shutdown // Graceful shutdown
process.on('SIGTERM', () => { process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down...'); logger.info('SIGTERM received, shutting down...');
terminalService.cleanup(); terminalService.cleanup();
server.close(() => { server.close(() => {
console.log('Server closed'); logger.info('Server closed');
process.exit(0); process.exit(0);
}); });
}); });
process.on('SIGINT', () => { process.on('SIGINT', () => {
console.log('SIGINT received, shutting down...'); logger.info('SIGINT received, shutting down...');
terminalService.cleanup(); terminalService.cleanup();
server.close(() => { server.close(() => {
console.log('Server closed'); logger.info('Server closed');
process.exit(0); process.exit(0);
}); });
}); });

View File

@@ -1,54 +1,381 @@
/** /**
* Authentication middleware for API security * Authentication middleware for API security
* *
* Supports API key authentication via header or environment variable. * Supports two authentication methods:
* 1. Header-based (X-API-Key) - Used by Electron mode
* 2. Cookie-based (HTTP-only session cookie) - Used by web mode
*
* Auto-generates an API key on first run if none is configured.
*/ */
import type { Request, Response, NextFunction } from 'express'; import type { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import path from 'path';
import * as secureFs from './secure-fs.js';
import { createLogger } from '@automaker/utils';
// API key from environment (optional - if not set, auth is disabled) const logger = createLogger('Auth');
const API_KEY = process.env.AUTOMAKER_API_KEY;
const DATA_DIR = process.env.DATA_DIR || './data';
const API_KEY_FILE = path.join(DATA_DIR, '.api-key');
const SESSIONS_FILE = path.join(DATA_DIR, '.sessions');
const SESSION_COOKIE_NAME = 'automaker_session';
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
const WS_TOKEN_MAX_AGE_MS = 5 * 60 * 1000; // 5 minutes for WebSocket connection tokens
// Session store - persisted to file for survival across server restarts
const validSessions = new Map<string, { createdAt: number; expiresAt: number }>();
// Short-lived WebSocket connection tokens (in-memory only, not persisted)
const wsConnectionTokens = new Map<string, { createdAt: number; expiresAt: number }>();
// Clean up expired WebSocket tokens periodically
setInterval(() => {
const now = Date.now();
wsConnectionTokens.forEach((data, token) => {
if (data.expiresAt <= now) {
wsConnectionTokens.delete(token);
}
});
}, 60 * 1000); // Clean up every minute
/**
* Load sessions from file on startup
*/
function loadSessions(): void {
try {
if (secureFs.existsSync(SESSIONS_FILE)) {
const data = secureFs.readFileSync(SESSIONS_FILE, 'utf-8') as string;
const sessions = JSON.parse(data) as Array<
[string, { createdAt: number; expiresAt: number }]
>;
const now = Date.now();
let loadedCount = 0;
let expiredCount = 0;
for (const [token, session] of sessions) {
// Only load non-expired sessions
if (session.expiresAt > now) {
validSessions.set(token, session);
loadedCount++;
} else {
expiredCount++;
}
}
if (loadedCount > 0 || expiredCount > 0) {
logger.info(`Loaded ${loadedCount} sessions (${expiredCount} expired)`);
}
}
} catch (error) {
logger.warn('Error loading sessions:', error);
}
}
/**
* Save sessions to file (async)
*/
async function saveSessions(): Promise<void> {
try {
await secureFs.mkdir(path.dirname(SESSIONS_FILE), { recursive: true });
const sessions = Array.from(validSessions.entries());
await secureFs.writeFile(SESSIONS_FILE, JSON.stringify(sessions), {
encoding: 'utf-8',
mode: 0o600,
});
} catch (error) {
logger.error('Failed to save sessions:', error);
}
}
// Load existing sessions on startup
loadSessions();
/**
* Ensure an API key exists - either from env var, file, or generate new one.
* This provides CSRF protection by requiring a secret key for all API requests.
*/
function ensureApiKey(): string {
// First check environment variable (Electron passes it this way)
if (process.env.AUTOMAKER_API_KEY) {
logger.info('Using API key from environment variable');
return process.env.AUTOMAKER_API_KEY;
}
// Try to read from file
try {
if (secureFs.existsSync(API_KEY_FILE)) {
const key = (secureFs.readFileSync(API_KEY_FILE, 'utf-8') as string).trim();
if (key) {
logger.info('Loaded API key from file');
return key;
}
}
} catch (error) {
logger.warn('Error reading API key file:', error);
}
// Generate new key
const newKey = crypto.randomUUID();
try {
secureFs.mkdirSync(path.dirname(API_KEY_FILE), { recursive: true });
secureFs.writeFileSync(API_KEY_FILE, newKey, { encoding: 'utf-8', mode: 0o600 });
logger.info('Generated new API key');
} catch (error) {
logger.error('Failed to save API key:', error);
}
return newKey;
}
// API key - always generated/loaded on startup for CSRF protection
const API_KEY = ensureApiKey();
// Print API key to console for web mode users (unless suppressed for production logging)
if (process.env.AUTOMAKER_HIDE_API_KEY !== 'true') {
logger.info(`
╔═══════════════════════════════════════════════════════════════════════╗
║ 🔐 API Key for Web Mode Authentication ║
╠═══════════════════════════════════════════════════════════════════════╣
║ ║
║ When accessing via browser, you'll be prompted to enter this key: ║
║ ║
${API_KEY}
║ ║
║ In Electron mode, authentication is handled automatically. ║
╚═══════════════════════════════════════════════════════════════════════╝
`);
} else {
logger.info('API key banner hidden (AUTOMAKER_HIDE_API_KEY=true)');
}
/**
* Generate a cryptographically secure session token
*/
function generateSessionToken(): string {
return crypto.randomBytes(32).toString('hex');
}
/**
* Create a new session and return the token
*/
export async function createSession(): Promise<string> {
const token = generateSessionToken();
const now = Date.now();
validSessions.set(token, {
createdAt: now,
expiresAt: now + SESSION_MAX_AGE_MS,
});
await saveSessions(); // Persist to file
return token;
}
/**
* Validate a session token
* Note: This returns synchronously but triggers async persistence if session expired
*/
export function validateSession(token: string): boolean {
const session = validSessions.get(token);
if (!session) return false;
if (Date.now() > session.expiresAt) {
validSessions.delete(token);
// Fire-and-forget: persist removal asynchronously
saveSessions().catch((err) => logger.error('Error saving sessions:', err));
return false;
}
return true;
}
/**
* Invalidate a session token
*/
export async function invalidateSession(token: string): Promise<void> {
validSessions.delete(token);
await saveSessions(); // Persist removal
}
/**
* Create a short-lived WebSocket connection token
* Used for initial WebSocket handshake authentication
*/
export function createWsConnectionToken(): string {
const token = generateSessionToken();
const now = Date.now();
wsConnectionTokens.set(token, {
createdAt: now,
expiresAt: now + WS_TOKEN_MAX_AGE_MS,
});
return token;
}
/**
* Validate a WebSocket connection token
* These tokens are single-use and short-lived (5 minutes)
* Token is invalidated immediately after first successful use
*/
export function validateWsConnectionToken(token: string): boolean {
const tokenData = wsConnectionTokens.get(token);
if (!tokenData) return false;
// Always delete the token (single-use)
wsConnectionTokens.delete(token);
// Check if expired
if (Date.now() > tokenData.expiresAt) {
return false;
}
return true;
}
/**
* Validate the API key using timing-safe comparison
* Prevents timing attacks that could leak information about the key
*/
export function validateApiKey(key: string): boolean {
if (!key || typeof key !== 'string') return false;
// Both buffers must be the same length for timingSafeEqual
const keyBuffer = Buffer.from(key);
const apiKeyBuffer = Buffer.from(API_KEY);
// If lengths differ, compare against a dummy to maintain constant time
if (keyBuffer.length !== apiKeyBuffer.length) {
crypto.timingSafeEqual(apiKeyBuffer, apiKeyBuffer);
return false;
}
return crypto.timingSafeEqual(keyBuffer, apiKeyBuffer);
}
/**
* Get session cookie options
*/
export function getSessionCookieOptions(): {
httpOnly: boolean;
secure: boolean;
sameSite: 'strict' | 'lax' | 'none';
maxAge: number;
path: string;
} {
return {
httpOnly: true, // JavaScript cannot access this cookie
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'strict', // Only sent for same-site requests (CSRF protection)
maxAge: SESSION_MAX_AGE_MS,
path: '/',
};
}
/**
* Get the session cookie name
*/
export function getSessionCookieName(): string {
return SESSION_COOKIE_NAME;
}
/**
* Authentication result type
*/
type AuthResult =
| { authenticated: true }
| { authenticated: false; errorType: 'invalid_api_key' | 'invalid_session' | 'no_auth' };
/**
* Core authentication check - shared between middleware and status check
* Extracts auth credentials from various sources and validates them
*/
function checkAuthentication(
headers: Record<string, string | string[] | undefined>,
query: Record<string, string | undefined>,
cookies: Record<string, string | undefined>
): AuthResult {
// Check for API key in header (Electron mode)
const headerKey = headers['x-api-key'] as string | undefined;
if (headerKey) {
if (validateApiKey(headerKey)) {
return { authenticated: true };
}
return { authenticated: false, errorType: 'invalid_api_key' };
}
// Check for session token in header (web mode with explicit token)
const sessionTokenHeader = headers['x-session-token'] as string | undefined;
if (sessionTokenHeader) {
if (validateSession(sessionTokenHeader)) {
return { authenticated: true };
}
return { authenticated: false, errorType: 'invalid_session' };
}
// Check for API key in query parameter (fallback)
const queryKey = query.apiKey;
if (queryKey) {
if (validateApiKey(queryKey)) {
return { authenticated: true };
}
return { authenticated: false, errorType: 'invalid_api_key' };
}
// Check for session cookie (web mode)
const sessionToken = cookies[SESSION_COOKIE_NAME];
if (sessionToken && validateSession(sessionToken)) {
return { authenticated: true };
}
return { authenticated: false, errorType: 'no_auth' };
}
/** /**
* Authentication middleware * Authentication middleware
* *
* If AUTOMAKER_API_KEY is set, requires matching key in X-API-Key header. * Accepts either:
* If not set, allows all requests (development mode). * 1. X-API-Key header (for Electron mode)
* 2. X-Session-Token header (for web mode with explicit token)
* 3. apiKey query parameter (fallback for cases where headers can't be set)
* 4. Session cookie (for web mode)
*/ */
export function authMiddleware(req: Request, res: Response, next: NextFunction): void { export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
// If no API key is configured, allow all requests const result = checkAuthentication(
if (!API_KEY) { req.headers as Record<string, string | string[] | undefined>,
req.query as Record<string, string | undefined>,
(req.cookies || {}) as Record<string, string | undefined>
);
if (result.authenticated) {
next(); next();
return; return;
} }
// Check for API key in header // Return appropriate error based on what failed
const providedKey = req.headers['x-api-key'] as string | undefined; switch (result.errorType) {
case 'invalid_api_key':
if (!providedKey) { res.status(403).json({
res.status(401).json({ success: false,
success: false, error: 'Invalid API key.',
error: 'Authentication required. Provide X-API-Key header.', });
}); break;
return; case 'invalid_session':
res.status(403).json({
success: false,
error: 'Invalid or expired session token.',
});
break;
case 'no_auth':
default:
res.status(401).json({
success: false,
error: 'Authentication required.',
});
} }
if (providedKey !== API_KEY) {
res.status(403).json({
success: false,
error: 'Invalid API key.',
});
return;
}
next();
} }
/** /**
* Check if authentication is enabled * Check if authentication is enabled (always true now)
*/ */
export function isAuthEnabled(): boolean { export function isAuthEnabled(): boolean {
return !!API_KEY; return true;
} }
/** /**
@@ -56,7 +383,31 @@ export function isAuthEnabled(): boolean {
*/ */
export function getAuthStatus(): { enabled: boolean; method: string } { export function getAuthStatus(): { enabled: boolean; method: string } {
return { return {
enabled: !!API_KEY, enabled: true,
method: API_KEY ? 'api_key' : 'none', method: 'api_key_or_session',
}; };
} }
/**
* Check if a request is authenticated (for status endpoint)
*/
export function isRequestAuthenticated(req: Request): boolean {
const result = checkAuthentication(
req.headers as Record<string, string | string[] | undefined>,
req.query as Record<string, string | undefined>,
(req.cookies || {}) as Record<string, string | undefined>
);
return result.authenticated;
}
/**
* Check if raw credentials are authenticated
* Used for WebSocket authentication where we don't have Express request objects
*/
export function checkRawAuthentication(
headers: Record<string, string | string[] | undefined>,
query: Record<string, string | undefined>,
cookies: Record<string, string | undefined>
): boolean {
return checkAuthentication(headers, query, cookies).authenticated;
}

View File

@@ -3,6 +3,9 @@
*/ */
import type { EventType, EventCallback } from '@automaker/types'; import type { EventType, EventCallback } from '@automaker/types';
import { createLogger } from '@automaker/utils';
const logger = createLogger('Events');
// Re-export event types from shared package // Re-export event types from shared package
export type { EventType, EventCallback }; export type { EventType, EventCallback };
@@ -21,7 +24,7 @@ export function createEventEmitter(): EventEmitter {
try { try {
callback(type, payload); callback(type, payload);
} catch (error) { } catch (error) {
console.error('Error in event subscriber:', error); logger.error('Error in event subscriber:', error);
} }
} }
}, },

View File

@@ -0,0 +1,211 @@
/**
* JSON Extraction Utilities
*
* Robust JSON extraction from AI responses that may contain markdown,
* code blocks, or other text mixed with JSON content.
*
* Used by various routes that parse structured output from Cursor or
* Claude responses when structured output is not available.
*/
import { createLogger } from '@automaker/utils';
const logger = createLogger('JsonExtractor');
/**
* Logger interface for optional custom logging
*/
export interface JsonExtractorLogger {
debug: (message: string, ...args: unknown[]) => void;
warn?: (message: string, ...args: unknown[]) => void;
}
/**
* Options for JSON extraction
*/
export interface ExtractJsonOptions {
/** Custom logger (defaults to internal logger) */
logger?: JsonExtractorLogger;
/** Required key that must be present in the extracted JSON */
requiredKey?: string;
/** Whether the required key's value must be an array */
requireArray?: boolean;
}
/**
* Extract JSON from response text using multiple strategies.
*
* Strategies tried in order:
* 1. JSON in ```json code block
* 2. JSON in ``` code block (no language)
* 3. Find JSON object by matching braces (starting with requiredKey if specified)
* 4. Find any JSON object by matching braces
* 5. Parse entire response as JSON
*
* @param responseText - The raw response text that may contain JSON
* @param options - Optional extraction options
* @returns Parsed JSON object or null if extraction fails
*/
export function extractJson<T = Record<string, unknown>>(
responseText: string,
options: ExtractJsonOptions = {}
): T | null {
const log = options.logger || logger;
const requiredKey = options.requiredKey;
const requireArray = options.requireArray ?? false;
/**
* Validate that the result has the required key/structure
*/
const validateResult = (result: unknown): result is T => {
if (!result || typeof result !== 'object') return false;
if (requiredKey) {
const obj = result as Record<string, unknown>;
if (!(requiredKey in obj)) return false;
if (requireArray && !Array.isArray(obj[requiredKey])) return false;
}
return true;
};
/**
* Find matching closing brace by counting brackets
*/
const findMatchingBrace = (text: string, startIdx: number): number => {
let depth = 0;
for (let i = startIdx; i < text.length; i++) {
if (text[i] === '{') depth++;
if (text[i] === '}') {
depth--;
if (depth === 0) {
return i + 1;
}
}
}
return -1;
};
const strategies = [
// Strategy 1: JSON in ```json code block
() => {
const match = responseText.match(/```json\s*([\s\S]*?)```/);
if (match) {
log.debug('Extracting JSON from ```json code block');
return JSON.parse(match[1].trim());
}
return null;
},
// Strategy 2: JSON in ``` code block (no language specified)
() => {
const match = responseText.match(/```\s*([\s\S]*?)```/);
if (match) {
const content = match[1].trim();
// Only try if it looks like JSON (starts with { or [)
if (content.startsWith('{') || content.startsWith('[')) {
log.debug('Extracting JSON from ``` code block');
return JSON.parse(content);
}
}
return null;
},
// Strategy 3: Find JSON object containing the required key (if specified)
() => {
if (!requiredKey) return null;
const searchPattern = `{"${requiredKey}"`;
const startIdx = responseText.indexOf(searchPattern);
if (startIdx === -1) return null;
const endIdx = findMatchingBrace(responseText, startIdx);
if (endIdx > startIdx) {
log.debug(`Extracting JSON with required key "${requiredKey}"`);
return JSON.parse(responseText.slice(startIdx, endIdx));
}
return null;
},
// Strategy 4: Find any JSON object by matching braces
() => {
const startIdx = responseText.indexOf('{');
if (startIdx === -1) return null;
const endIdx = findMatchingBrace(responseText, startIdx);
if (endIdx > startIdx) {
log.debug('Extracting JSON by brace matching');
return JSON.parse(responseText.slice(startIdx, endIdx));
}
return null;
},
// Strategy 5: Find JSON using first { to last } (may be less accurate)
() => {
const firstBrace = responseText.indexOf('{');
const lastBrace = responseText.lastIndexOf('}');
if (firstBrace !== -1 && lastBrace > firstBrace) {
log.debug('Extracting JSON from first { to last }');
return JSON.parse(responseText.slice(firstBrace, lastBrace + 1));
}
return null;
},
// Strategy 6: Try parsing the entire response as JSON
() => {
const trimmed = responseText.trim();
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
log.debug('Parsing entire response as JSON');
return JSON.parse(trimmed);
}
return null;
},
];
for (const strategy of strategies) {
try {
const result = strategy();
if (validateResult(result)) {
log.debug('Successfully extracted JSON');
return result as T;
}
} catch {
// Strategy failed, try next
}
}
log.debug('Failed to extract JSON from response');
return null;
}
/**
* Extract JSON with a specific required key.
* Convenience wrapper around extractJson.
*
* @param responseText - The raw response text
* @param requiredKey - Key that must be present in the extracted JSON
* @param options - Additional options
* @returns Parsed JSON object or null
*/
export function extractJsonWithKey<T = Record<string, unknown>>(
responseText: string,
requiredKey: string,
options: Omit<ExtractJsonOptions, 'requiredKey'> = {}
): T | null {
return extractJson<T>(responseText, { ...options, requiredKey });
}
/**
* Extract JSON that has a required array property.
* Useful for extracting responses like { "suggestions": [...] }
*
* @param responseText - The raw response text
* @param arrayKey - Key that must contain an array
* @param options - Additional options
* @returns Parsed JSON object or null
*/
export function extractJsonWithArray<T = Record<string, unknown>>(
responseText: string,
arrayKey: string,
options: Omit<ExtractJsonOptions, 'requiredKey' | 'requireArray'> = {}
): T | null {
return extractJson<T>(responseText, { ...options, requiredKey: arrayKey, requireArray: true });
}

View File

@@ -16,9 +16,19 @@
*/ */
import type { Options } from '@anthropic-ai/claude-agent-sdk'; import type { Options } from '@anthropic-ai/claude-agent-sdk';
import os from 'os';
import path from 'path'; import path from 'path';
import { resolveModelString } from '@automaker/model-resolver'; import { resolveModelString } from '@automaker/model-resolver';
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP } from '@automaker/types'; import { createLogger } from '@automaker/utils';
const logger = createLogger('SdkOptions');
import {
DEFAULT_MODELS,
CLAUDE_MODEL_MAP,
type McpServerConfig,
type ThinkingLevel,
getThinkingTokenBudget,
} from '@automaker/types';
import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform'; import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform';
/** /**
@@ -47,6 +57,139 @@ export function validateWorkingDirectory(cwd: string): void {
} }
} }
/**
* Known cloud storage path patterns where sandbox mode is incompatible.
*
* The Claude CLI sandbox feature uses filesystem isolation that conflicts with
* cloud storage providers' virtual filesystem implementations. This causes the
* Claude process to exit with code 1 when sandbox is enabled for these paths.
*
* Affected providers (macOS paths):
* - Dropbox: ~/Library/CloudStorage/Dropbox-*
* - Google Drive: ~/Library/CloudStorage/GoogleDrive-*
* - OneDrive: ~/Library/CloudStorage/OneDrive-*
* - iCloud Drive: ~/Library/Mobile Documents/
* - Box: ~/Library/CloudStorage/Box-*
*
* Note: This is a known limitation when using cloud storage paths.
*/
/**
* macOS-specific cloud storage patterns that appear under ~/Library/
* These are specific enough to use with includes() safely.
*/
const MACOS_CLOUD_STORAGE_PATTERNS = [
'/Library/CloudStorage/', // Dropbox, Google Drive, OneDrive, Box on macOS
'/Library/Mobile Documents/', // iCloud Drive on macOS
] as const;
/**
* Generic cloud storage folder names that need to be anchored to the home directory
* to avoid false positives (e.g., /home/user/my-project-about-dropbox/).
*/
const HOME_ANCHORED_CLOUD_FOLDERS = [
'Google Drive', // Google Drive on some systems
'Dropbox', // Dropbox on Linux/alternative installs
'OneDrive', // OneDrive on Linux/alternative installs
] as const;
/**
* Check if a path is within a cloud storage location.
*
* Cloud storage providers use virtual filesystem implementations that are
* incompatible with the Claude CLI sandbox feature, causing process crashes.
*
* Uses two detection strategies:
* 1. macOS-specific patterns (under ~/Library/) - checked via includes()
* 2. Generic folder names - anchored to home directory to avoid false positives
*
* @param cwd - The working directory path to check
* @returns true if the path is in a cloud storage location
*/
export function isCloudStoragePath(cwd: string): boolean {
const resolvedPath = path.resolve(cwd);
// Normalize to forward slashes for consistent pattern matching across platforms
let normalizedPath = resolvedPath.split(path.sep).join('/');
// Remove Windows drive letter if present (e.g., "C:/Users" -> "/Users")
// This ensures Unix paths in tests work the same on Windows
normalizedPath = normalizedPath.replace(/^[A-Za-z]:/, '');
// Check macOS-specific patterns (these are specific enough to use includes)
if (MACOS_CLOUD_STORAGE_PATTERNS.some((pattern) => normalizedPath.includes(pattern))) {
return true;
}
// Check home-anchored patterns to avoid false positives
// e.g., /home/user/my-project-about-dropbox/ should NOT match
const home = os.homedir();
for (const folder of HOME_ANCHORED_CLOUD_FOLDERS) {
const cloudPath = path.join(home, folder);
let normalizedCloudPath = cloudPath.split(path.sep).join('/');
// Remove Windows drive letter if present
normalizedCloudPath = normalizedCloudPath.replace(/^[A-Za-z]:/, '');
// Check if resolved path starts with the cloud storage path followed by a separator
// This ensures we match ~/Dropbox/project but not ~/Dropbox-archive or ~/my-dropbox-tool
if (
normalizedPath === normalizedCloudPath ||
normalizedPath.startsWith(normalizedCloudPath + '/')
) {
return true;
}
}
return false;
}
/**
* Result of sandbox compatibility check
*/
export interface SandboxCheckResult {
/** Whether sandbox should be enabled */
enabled: boolean;
/** If disabled, the reason why */
disabledReason?: 'cloud_storage' | 'user_setting';
/** Human-readable message for logging/UI */
message?: string;
}
/**
* Determine if sandbox mode should be enabled for a given configuration.
*
* Sandbox mode is automatically disabled for cloud storage paths because the
* Claude CLI sandbox feature is incompatible with virtual filesystem
* implementations used by cloud storage providers (Dropbox, Google Drive, etc.).
*
* @param cwd - The working directory
* @param enableSandboxMode - User's sandbox mode setting
* @returns SandboxCheckResult with enabled status and reason if disabled
*/
export function checkSandboxCompatibility(
cwd: string,
enableSandboxMode?: boolean
): SandboxCheckResult {
// User has explicitly disabled sandbox mode
if (enableSandboxMode === false) {
return {
enabled: false,
disabledReason: 'user_setting',
};
}
// Check for cloud storage incompatibility (applies when enabled or undefined)
if (isCloudStoragePath(cwd)) {
return {
enabled: false,
disabledReason: 'cloud_storage',
message: `Sandbox mode auto-disabled: Project is in a cloud storage location (${cwd}). The Claude CLI sandbox feature is incompatible with cloud storage filesystems. To use sandbox mode, move your project to a local directory.`,
};
}
// Sandbox is compatible and enabled (true or undefined defaults to enabled)
return {
enabled: true,
};
}
/** /**
* Tool presets for different use cases * Tool presets for different use cases
*/ */
@@ -136,6 +279,68 @@ function getBaseOptions(): Partial<Options> {
}; };
} }
/**
* MCP permission options result
*/
interface McpPermissionOptions {
/** Whether tools should be restricted to a preset */
shouldRestrictTools: boolean;
/** Options to spread when MCP bypass is enabled */
bypassOptions: Partial<Options>;
/** Options to spread for MCP servers */
mcpServerOptions: Partial<Options>;
}
/**
* Build MCP-related options based on configuration.
* Centralizes the logic for determining permission modes and tool restrictions
* when MCP servers are configured.
*
* @param config - The SDK options config
* @returns Object with MCP permission settings to spread into final options
*/
function buildMcpOptions(config: CreateSdkOptionsConfig): McpPermissionOptions {
const hasMcpServers = config.mcpServers && Object.keys(config.mcpServers).length > 0;
// Default to true for autonomous workflow. Security is enforced when adding servers
// via the security warning dialog that explains the risks.
const mcpAutoApprove = config.mcpAutoApproveTools ?? true;
const mcpUnrestricted = config.mcpUnrestrictedTools ?? true;
// Determine if we should bypass permissions based on settings
const shouldBypassPermissions = hasMcpServers && mcpAutoApprove;
// Determine if we should restrict tools (only when no MCP or unrestricted is disabled)
const shouldRestrictTools = !hasMcpServers || !mcpUnrestricted;
return {
shouldRestrictTools,
// Only include bypass options when MCP is configured and auto-approve is enabled
bypassOptions: shouldBypassPermissions
? {
permissionMode: 'bypassPermissions' as const,
// Required flag when using bypassPermissions mode
allowDangerouslySkipPermissions: true,
}
: {},
// Include MCP servers if configured
mcpServerOptions: config.mcpServers ? { mcpServers: config.mcpServers } : {},
};
}
/**
* Build thinking options for SDK configuration.
* Converts ThinkingLevel to maxThinkingTokens for the Claude SDK.
*
* @param thinkingLevel - The thinking level to convert
* @returns Object with maxThinkingTokens if thinking is enabled
*/
function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial<Options> {
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
logger.debug(
`buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}`
);
return maxThinkingTokens ? { maxThinkingTokens } : {};
}
/** /**
* Build system prompt configuration based on autoLoadClaudeMd setting. * Build system prompt configuration based on autoLoadClaudeMd setting.
* When autoLoadClaudeMd is true: * When autoLoadClaudeMd is true:
@@ -219,8 +424,28 @@ export interface CreateSdkOptionsConfig {
/** Enable sandbox mode for bash command isolation */ /** Enable sandbox mode for bash command isolation */
enableSandboxMode?: boolean; enableSandboxMode?: boolean;
/** MCP servers to make available to the agent */
mcpServers?: Record<string, McpServerConfig>;
/** Auto-approve MCP tool calls without permission prompts */
mcpAutoApproveTools?: boolean;
/** Allow unrestricted tools when MCP servers are enabled */
mcpUnrestrictedTools?: boolean;
/** Extended thinking level for Claude models */
thinkingLevel?: ThinkingLevel;
} }
// Re-export MCP types from @automaker/types for convenience
export type {
McpServerConfig,
McpStdioServerConfig,
McpSSEServerConfig,
McpHttpServerConfig,
} from '@automaker/types';
/** /**
* Create SDK options for spec generation * Create SDK options for spec generation
* *
@@ -237,6 +462,9 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
// Build CLAUDE.md auto-loading options if enabled // Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config); const claudeMdOptions = buildClaudeMdOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
return { return {
...getBaseOptions(), ...getBaseOptions(),
// Override permissionMode - spec generation only needs read-only tools // Override permissionMode - spec generation only needs read-only tools
@@ -248,6 +476,7 @@ export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Opt
cwd: config.cwd, cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.specGeneration], allowedTools: [...TOOL_PRESETS.specGeneration],
...claudeMdOptions, ...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }), ...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }), ...(config.outputFormat && { outputFormat: config.outputFormat }),
}; };
@@ -269,6 +498,9 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
// Build CLAUDE.md auto-loading options if enabled // Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config); const claudeMdOptions = buildClaudeMdOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
return { return {
...getBaseOptions(), ...getBaseOptions(),
// Override permissionMode - feature generation only needs read-only tools // Override permissionMode - feature generation only needs read-only tools
@@ -278,6 +510,7 @@ export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig):
cwd: config.cwd, cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly], allowedTools: [...TOOL_PRESETS.readOnly],
...claudeMdOptions, ...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }), ...(config.abortController && { abortController: config.abortController }),
}; };
} }
@@ -298,6 +531,9 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
// Build CLAUDE.md auto-loading options if enabled // Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config); const claudeMdOptions = buildClaudeMdOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
return { return {
...getBaseOptions(), ...getBaseOptions(),
model: getModelForUseCase('suggestions', config.model), model: getModelForUseCase('suggestions', config.model),
@@ -305,6 +541,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
cwd: config.cwd, cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.readOnly], allowedTools: [...TOOL_PRESETS.readOnly],
...claudeMdOptions, ...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }), ...(config.abortController && { abortController: config.abortController }),
...(config.outputFormat && { outputFormat: config.outputFormat }), ...(config.outputFormat && { outputFormat: config.outputFormat }),
}; };
@@ -317,7 +554,7 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
* - Full tool access for code modification * - Full tool access for code modification
* - Standard turns for interactive sessions * - Standard turns for interactive sessions
* - Model priority: explicit model > session model > chat default * - Model priority: explicit model > session model > chat default
* - Sandbox mode controlled by enableSandboxMode setting * - Sandbox mode controlled by enableSandboxMode setting (auto-disabled for cloud storage)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/ */
export function createChatOptions(config: CreateSdkOptionsConfig): Options { export function createChatOptions(config: CreateSdkOptionsConfig): Options {
@@ -330,20 +567,34 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// Build CLAUDE.md auto-loading options if enabled // Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config); const claudeMdOptions = buildClaudeMdOptions(config);
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// Check sandbox compatibility (auto-disables for cloud storage paths)
const sandboxCheck = checkSandboxCompatibility(config.cwd, config.enableSandboxMode);
return { return {
...getBaseOptions(), ...getBaseOptions(),
model: getModelForUseCase('chat', effectiveModel), model: getModelForUseCase('chat', effectiveModel),
maxTurns: MAX_TURNS.standard, maxTurns: MAX_TURNS.standard,
cwd: config.cwd, cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.chat], // Only restrict tools if no MCP servers configured or unrestricted is disabled
...(config.enableSandboxMode && { ...(mcpOptions.shouldRestrictTools && { allowedTools: [...TOOL_PRESETS.chat] }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...(sandboxCheck.enabled && {
sandbox: { sandbox: {
enabled: true, enabled: true,
autoAllowBashIfSandboxed: true, autoAllowBashIfSandboxed: true,
}, },
}), }),
...claudeMdOptions, ...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }), ...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
}; };
} }
@@ -354,7 +605,7 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
* - Full tool access for code modification and implementation * - Full tool access for code modification and implementation
* - Extended turns for thorough feature implementation * - Extended turns for thorough feature implementation
* - Uses default model (can be overridden) * - Uses default model (can be overridden)
* - Sandbox mode controlled by enableSandboxMode setting * - Sandbox mode controlled by enableSandboxMode setting (auto-disabled for cloud storage)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading * - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/ */
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options { export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
@@ -364,20 +615,34 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// Build CLAUDE.md auto-loading options if enabled // Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config); const claudeMdOptions = buildClaudeMdOptions(config);
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// Check sandbox compatibility (auto-disables for cloud storage paths)
const sandboxCheck = checkSandboxCompatibility(config.cwd, config.enableSandboxMode);
return { return {
...getBaseOptions(), ...getBaseOptions(),
model: getModelForUseCase('auto', config.model), model: getModelForUseCase('auto', config.model),
maxTurns: MAX_TURNS.maximum, maxTurns: MAX_TURNS.maximum,
cwd: config.cwd, cwd: config.cwd,
allowedTools: [...TOOL_PRESETS.fullAccess], // Only restrict tools if no MCP servers configured or unrestricted is disabled
...(config.enableSandboxMode && { ...(mcpOptions.shouldRestrictTools && { allowedTools: [...TOOL_PRESETS.fullAccess] }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...(sandboxCheck.enabled && {
sandbox: { sandbox: {
enabled: true, enabled: true,
autoAllowBashIfSandboxed: true, autoAllowBashIfSandboxed: true,
}, },
}), }),
...claudeMdOptions, ...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }), ...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
}; };
} }
@@ -400,14 +665,31 @@ export function createCustomOptions(
// Build CLAUDE.md auto-loading options if enabled // Build CLAUDE.md auto-loading options if enabled
const claudeMdOptions = buildClaudeMdOptions(config); const claudeMdOptions = buildClaudeMdOptions(config);
// Build MCP-related options
const mcpOptions = buildMcpOptions(config);
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// For custom options: use explicit allowedTools if provided, otherwise use preset based on MCP settings
const effectiveAllowedTools = config.allowedTools
? [...config.allowedTools]
: mcpOptions.shouldRestrictTools
? [...TOOL_PRESETS.readOnly]
: undefined;
return { return {
...getBaseOptions(), ...getBaseOptions(),
model: getModelForUseCase('default', config.model), model: getModelForUseCase('default', config.model),
maxTurns: config.maxTurns ?? MAX_TURNS.maximum, maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
cwd: config.cwd, cwd: config.cwd,
allowedTools: config.allowedTools ? [...config.allowedTools] : [...TOOL_PRESETS.readOnly], ...(effectiveAllowedTools && { allowedTools: effectiveAllowedTools }),
...(config.sandbox && { sandbox: config.sandbox }), ...(config.sandbox && { sandbox: config.sandbox }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...claudeMdOptions, ...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }), ...(config.abortController && { abortController: config.abortController }),
...mcpOptions.mcpServerOptions,
}; };
} }

View File

@@ -6,6 +6,7 @@
import { secureFs } from '@automaker/platform'; import { secureFs } from '@automaker/platform';
export const { export const {
// Async methods
access, access,
readFile, readFile,
writeFile, writeFile,
@@ -20,6 +21,16 @@ export const {
lstat, lstat,
joinPath, joinPath,
resolvePath, resolvePath,
// Sync methods
existsSync,
readFileSync,
writeFileSync,
mkdirSync,
readdirSync,
statSync,
accessSync,
unlinkSync,
rmSync,
// Throttling configuration and monitoring // Throttling configuration and monitoring
configureThrottling, configureThrottling,
getThrottlingConfig, getThrottlingConfig,

View File

@@ -4,6 +4,16 @@
import type { SettingsService } from '../services/settings-service.js'; import type { SettingsService } from '../services/settings-service.js';
import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils'; import type { ContextFilesResult, ContextFileInfo } from '@automaker/utils';
import { createLogger } from '@automaker/utils';
import type { MCPServerConfig, McpServerConfig, PromptCustomization } from '@automaker/types';
import {
mergeAutoModePrompts,
mergeAgentPrompts,
mergeBacklogPlanPrompts,
mergeEnhancementPrompts,
} from '@automaker/prompts';
const logger = createLogger('SettingsHelper');
/** /**
* Get the autoLoadClaudeMd setting, with project settings taking precedence over global. * Get the autoLoadClaudeMd setting, with project settings taking precedence over global.
@@ -20,7 +30,7 @@ export async function getAutoLoadClaudeMdSetting(
logPrefix = '[SettingsHelper]' logPrefix = '[SettingsHelper]'
): Promise<boolean> { ): Promise<boolean> {
if (!settingsService) { if (!settingsService) {
console.log(`${logPrefix} SettingsService not available, autoLoadClaudeMd disabled`); logger.info(`${logPrefix} SettingsService not available, autoLoadClaudeMd disabled`);
return false; return false;
} }
@@ -28,7 +38,7 @@ export async function getAutoLoadClaudeMdSetting(
// Check project settings first (takes precedence) // Check project settings first (takes precedence)
const projectSettings = await settingsService.getProjectSettings(projectPath); const projectSettings = await settingsService.getProjectSettings(projectPath);
if (projectSettings.autoLoadClaudeMd !== undefined) { if (projectSettings.autoLoadClaudeMd !== undefined) {
console.log( logger.info(
`${logPrefix} autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}` `${logPrefix} autoLoadClaudeMd from project settings: ${projectSettings.autoLoadClaudeMd}`
); );
return projectSettings.autoLoadClaudeMd; return projectSettings.autoLoadClaudeMd;
@@ -37,10 +47,10 @@ export async function getAutoLoadClaudeMdSetting(
// Fall back to global settings // Fall back to global settings
const globalSettings = await settingsService.getGlobalSettings(); const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.autoLoadClaudeMd ?? false; const result = globalSettings.autoLoadClaudeMd ?? false;
console.log(`${logPrefix} autoLoadClaudeMd from global settings: ${result}`); logger.info(`${logPrefix} autoLoadClaudeMd from global settings: ${result}`);
return result; return result;
} catch (error) { } catch (error) {
console.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error); logger.error(`${logPrefix} Failed to load autoLoadClaudeMd setting:`, error);
throw error; throw error;
} }
} }
@@ -58,17 +68,17 @@ export async function getEnableSandboxModeSetting(
logPrefix = '[SettingsHelper]' logPrefix = '[SettingsHelper]'
): Promise<boolean> { ): Promise<boolean> {
if (!settingsService) { if (!settingsService) {
console.log(`${logPrefix} SettingsService not available, sandbox mode disabled`); logger.info(`${logPrefix} SettingsService not available, sandbox mode disabled`);
return false; return false;
} }
try { try {
const globalSettings = await settingsService.getGlobalSettings(); const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.enableSandboxMode ?? true; const result = globalSettings.enableSandboxMode ?? false;
console.log(`${logPrefix} enableSandboxMode from global settings: ${result}`); logger.info(`${logPrefix} enableSandboxMode from global settings: ${result}`);
return result; return result;
} catch (error) { } catch (error) {
console.error(`${logPrefix} Failed to load enableSandboxMode setting:`, error); logger.error(`${logPrefix} Failed to load enableSandboxMode setting:`, error);
throw error; throw error;
} }
} }
@@ -136,3 +146,126 @@ function formatContextFileEntry(file: ContextFileInfo): string {
const descriptionInfo = file.description ? `\n**Purpose:** ${file.description}` : ''; const descriptionInfo = file.description ? `\n**Purpose:** ${file.description}` : '';
return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`; return `${header}\n${pathInfo}${descriptionInfo}\n\n${file.content}`;
} }
/**
* Get enabled MCP servers from global settings, converted to SDK format.
* Returns an empty object if settings service is not available or no servers are configured.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @returns Promise resolving to MCP servers in SDK format (keyed by name)
*/
export async function getMCPServersFromSettings(
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<Record<string, McpServerConfig>> {
if (!settingsService) {
return {};
}
try {
const globalSettings = await settingsService.getGlobalSettings();
const mcpServers = globalSettings.mcpServers || [];
// Filter to only enabled servers and convert to SDK format
const enabledServers = mcpServers.filter((s) => s.enabled !== false);
if (enabledServers.length === 0) {
return {};
}
// Convert settings format to SDK format (keyed by name)
const sdkServers: Record<string, McpServerConfig> = {};
for (const server of enabledServers) {
sdkServers[server.name] = convertToSdkFormat(server);
}
logger.info(
`${logPrefix} Loaded ${enabledServers.length} MCP server(s): ${enabledServers.map((s) => s.name).join(', ')}`
);
return sdkServers;
} catch (error) {
logger.error(`${logPrefix} Failed to load MCP servers setting:`, error);
return {};
}
}
/**
* Convert a settings MCPServerConfig to SDK McpServerConfig format.
* Validates required fields and throws informative errors if missing.
*/
function convertToSdkFormat(server: MCPServerConfig): McpServerConfig {
if (server.type === 'sse') {
if (!server.url) {
throw new Error(`SSE MCP server "${server.name}" is missing a URL.`);
}
return {
type: 'sse',
url: server.url,
headers: server.headers,
};
}
if (server.type === 'http') {
if (!server.url) {
throw new Error(`HTTP MCP server "${server.name}" is missing a URL.`);
}
return {
type: 'http',
url: server.url,
headers: server.headers,
};
}
// Default to stdio
if (!server.command) {
throw new Error(`Stdio MCP server "${server.name}" is missing a command.`);
}
return {
type: 'stdio',
command: server.command,
args: server.args,
env: server.env,
};
}
/**
* Get prompt customization from global settings and merge with defaults.
* Returns prompts merged with built-in defaults - custom prompts override defaults.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages
* @returns Promise resolving to merged prompts for all categories
*/
export async function getPromptCustomization(
settingsService?: SettingsService | null,
logPrefix = '[PromptHelper]'
): Promise<{
autoMode: ReturnType<typeof mergeAutoModePrompts>;
agent: ReturnType<typeof mergeAgentPrompts>;
backlogPlan: ReturnType<typeof mergeBacklogPlanPrompts>;
enhancement: ReturnType<typeof mergeEnhancementPrompts>;
}> {
let customization: PromptCustomization = {};
if (settingsService) {
try {
const globalSettings = await settingsService.getGlobalSettings();
customization = globalSettings.promptCustomization || {};
logger.info(`${logPrefix} Loaded prompt customization from settings`);
} catch (error) {
logger.error(`${logPrefix} Failed to load prompt customization:`, error);
// Fall through to use empty customization (all defaults)
}
} else {
logger.info(`${logPrefix} SettingsService not available, using default prompts`);
}
return {
autoMode: mergeAutoModePrompts(customization.autoMode),
agent: mergeAgentPrompts(customization.agent),
backlogPlan: mergeBacklogPlanPrompts(customization.backlogPlan),
enhancement: mergeEnhancementPrompts(customization.enhancement),
};
}

View File

@@ -0,0 +1,36 @@
/**
* Version utility - Reads version from package.json
*/
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { createLogger } from '@automaker/utils';
const logger = createLogger('Version');
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
let cachedVersion: string | null = null;
/**
* Get the version from package.json
* Caches the result for performance
*/
export function getVersion(): string {
if (cachedVersion) {
return cachedVersion;
}
try {
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const version = packageJson.version || '0.0.0';
cachedVersion = version;
return version;
} catch (error) {
logger.warn('Failed to read version from package.json:', error);
return '0.0.0';
}
}

View File

@@ -0,0 +1,50 @@
/**
* Middleware to enforce Content-Type: application/json for request bodies
*
* This security middleware prevents malicious requests by requiring proper
* Content-Type headers for all POST, PUT, and PATCH requests.
*
* Rejecting requests without proper Content-Type helps prevent:
* - CSRF attacks via form submissions (which use application/x-www-form-urlencoded)
* - Content-type confusion attacks
* - Malformed request exploitation
*/
import type { Request, Response, NextFunction } from 'express';
// HTTP methods that typically include request bodies
const METHODS_REQUIRING_JSON = ['POST', 'PUT', 'PATCH'];
/**
* Middleware that requires Content-Type: application/json for POST/PUT/PATCH requests
*
* Returns 415 Unsupported Media Type if:
* - The request method is POST, PUT, or PATCH
* - AND the Content-Type header is missing or not application/json
*
* Allows requests to pass through if:
* - The request method is GET, DELETE, OPTIONS, HEAD, etc.
* - OR the Content-Type is properly set to application/json (with optional charset)
*/
export function requireJsonContentType(req: Request, res: Response, next: NextFunction): void {
// Skip validation for methods that don't require a body
if (!METHODS_REQUIRING_JSON.includes(req.method)) {
next();
return;
}
const contentType = req.headers['content-type'];
// Check if Content-Type header exists and contains application/json
// Allows for charset parameter: "application/json; charset=utf-8"
if (!contentType || !contentType.toLowerCase().includes('application/json')) {
res.status(415).json({
success: false,
error: 'Unsupported Media Type',
message: 'Content-Type header must be application/json',
});
return;
}
next();
}

View File

@@ -7,6 +7,10 @@
import { query, type Options } from '@anthropic-ai/claude-agent-sdk'; import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
import { BaseProvider } from './base-provider.js'; import { BaseProvider } from './base-provider.js';
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';
const logger = createLogger('ClaudeProvider');
import { getThinkingTokenBudget } from '@automaker/types';
import type { import type {
ExecuteOptions, ExecuteOptions,
ProviderMessage, ProviderMessage,
@@ -14,6 +18,32 @@ import type {
ModelDefinition, ModelDefinition,
} from './types.js'; } from './types.js';
// Explicit allowlist of environment variables to pass to the SDK.
// Only these vars are passed - nothing else from process.env leaks through.
const ALLOWED_ENV_VARS = [
'ANTHROPIC_API_KEY',
'PATH',
'HOME',
'SHELL',
'TERM',
'USER',
'LANG',
'LC_ALL',
];
/**
* Build environment for the SDK with only explicitly allowed variables
*/
function buildEnv(): Record<string, string | undefined> {
const env: Record<string, string | undefined> = {};
for (const key of ALLOWED_ENV_VARS) {
if (process.env[key]) {
env[key] = process.env[key];
}
}
return env;
}
export class ClaudeProvider extends BaseProvider { export class ClaudeProvider extends BaseProvider {
getName(): string { getName(): string {
return 'claude'; return 'claude';
@@ -33,19 +63,34 @@ export class ClaudeProvider extends BaseProvider {
abortController, abortController,
conversationHistory, conversationHistory,
sdkSessionId, sdkSessionId,
thinkingLevel,
} = options; } = options;
// Convert thinking level to token budget
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
// Build Claude SDK options // Build Claude SDK options
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
const hasMcpServers = options.mcpServers && Object.keys(options.mcpServers).length > 0;
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch']; const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
const toolsToUse = allowedTools || defaultTools;
// AUTONOMOUS MODE: Always bypass permissions and allow unrestricted tools
// Only restrict tools when no MCP servers are configured
const shouldRestrictTools = !hasMcpServers;
const sdkOptions: Options = { const sdkOptions: Options = {
model, model,
systemPrompt, systemPrompt,
maxTurns, maxTurns,
cwd, cwd,
allowedTools: toolsToUse, // Pass only explicitly allowed environment variables to SDK
permissionMode: 'default', env: buildEnv(),
// Only restrict tools if explicitly set OR (no MCP / unrestricted disabled)
...(allowedTools && shouldRestrictTools && { allowedTools }),
...(!allowedTools && shouldRestrictTools && { allowedTools: defaultTools }),
// AUTONOMOUS MODE: Always bypass permissions and allow dangerous operations
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
abortController, abortController,
// Resume existing SDK session if we have a session ID // Resume existing SDK session if we have a session ID
...(sdkSessionId && conversationHistory && conversationHistory.length > 0 ...(sdkSessionId && conversationHistory && conversationHistory.length > 0
@@ -55,6 +100,10 @@ export class ClaudeProvider extends BaseProvider {
...(options.settingSources && { settingSources: options.settingSources }), ...(options.settingSources && { settingSources: options.settingSources }),
// Forward sandbox configuration // Forward sandbox configuration
...(options.sandbox && { sandbox: options.sandbox }), ...(options.sandbox && { sandbox: options.sandbox }),
// Forward MCP servers configuration
...(options.mcpServers && { mcpServers: options.mcpServers }),
// Extended thinking configuration
...(maxThinkingTokens && { maxThinkingTokens }),
}; };
// Build prompt payload // Build prompt payload
@@ -88,9 +137,32 @@ export class ClaudeProvider extends BaseProvider {
yield msg as ProviderMessage; yield msg as ProviderMessage;
} }
} catch (error) { } catch (error) {
console.error('[ClaudeProvider] ERROR: executeQuery() error during execution:', error); // Enhance error with user-friendly message and classification
console.error('[ClaudeProvider] ERROR stack:', (error as Error).stack); const errorInfo = classifyError(error);
throw error; const userMessage = getUserFriendlyErrorMessage(error);
logger.error('executeQuery() error during execution:', {
type: errorInfo.type,
message: errorInfo.message,
isRateLimit: errorInfo.isRateLimit,
retryAfter: errorInfo.retryAfter,
stack: (error as Error).stack,
});
// Build enhanced error message with additional guidance for rate limits
const message = errorInfo.isRateLimit
? `${userMessage}\n\nTip: If you're running multiple features in auto-mode, consider reducing concurrency (maxConcurrency setting) to avoid hitting rate limits.`
: userMessage;
const enhancedError = new Error(message);
(enhancedError as any).originalError = error;
(enhancedError as any).type = errorInfo.type;
if (errorInfo.isRateLimit) {
(enhancedError as any).retryAfter = errorInfo.retryAfter;
}
throw enhancedError;
} }
} }

View File

@@ -0,0 +1,558 @@
/**
* CliProvider - Abstract base class for CLI-based AI providers
*
* Provides common infrastructure for CLI tools that spawn subprocesses
* and stream JSONL output. Handles:
* - Platform-specific CLI detection (PATH, common locations)
* - Windows execution strategies (WSL, npx, direct, cmd)
* - JSONL subprocess spawning and streaming
* - Error mapping infrastructure
*
* @example
* ```typescript
* class CursorProvider extends CliProvider {
* getCliName(): string { return 'cursor-agent'; }
* getSpawnConfig(): CliSpawnConfig {
* return {
* windowsStrategy: 'wsl',
* commonPaths: {
* linux: ['~/.local/bin/cursor-agent'],
* darwin: ['~/.local/bin/cursor-agent'],
* }
* };
* }
* // ... implement abstract methods
* }
* ```
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { BaseProvider } from './base-provider.js';
import type { ProviderConfig, ExecuteOptions, ProviderMessage } from './types.js';
import {
spawnJSONLProcess,
type SubprocessOptions,
isWslAvailable,
findCliInWsl,
createWslCommand,
windowsToWslPath,
type WslCliResult,
} from '@automaker/platform';
import { createLogger, isAbortError } from '@automaker/utils';
/**
* Spawn strategy for CLI tools on Windows
*
* Different CLI tools require different execution strategies:
* - 'wsl': Requires WSL, CLI only available on Linux/macOS (e.g., cursor-agent)
* - 'npx': Installed globally via npm/npx, use `npx <package>` to run
* - 'direct': Native Windows binary, can spawn directly
* - 'cmd': Windows batch file (.cmd/.bat), needs cmd.exe shell
*/
export type SpawnStrategy = 'wsl' | 'npx' | 'direct' | 'cmd';
/**
* Configuration for CLI tool spawning
*/
export interface CliSpawnConfig {
/** How to spawn on Windows */
windowsStrategy: SpawnStrategy;
/** NPX package name (required if windowsStrategy is 'npx') */
npxPackage?: string;
/** Preferred WSL distribution (if windowsStrategy is 'wsl') */
wslDistribution?: string;
/**
* Common installation paths per platform
* Use ~ for home directory (will be expanded)
* Keys: 'linux', 'darwin', 'win32'
*/
commonPaths: Record<string, string[]>;
/** Version check command (defaults to --version) */
versionCommand?: string;
}
/**
* CLI error information for consistent error handling
*/
export interface CliErrorInfo {
code: string;
message: string;
recoverable: boolean;
suggestion?: string;
}
/**
* Detection result from CLI path finding
*/
export interface CliDetectionResult {
/** Path to the CLI (or 'npx' for npx strategy) */
cliPath: string | null;
/** Whether using WSL mode */
useWsl: boolean;
/** WSL path if using WSL */
wslCliPath?: string;
/** WSL distribution if using WSL */
wslDistribution?: string;
/** Detected strategy used */
strategy: SpawnStrategy | 'native';
}
// Create logger for CLI operations
const cliLogger = createLogger('CliProvider');
/**
* Abstract base class for CLI-based providers
*
* Subclasses must implement:
* - getCliName(): CLI executable name
* - getSpawnConfig(): Platform-specific spawn configuration
* - buildCliArgs(): Convert ExecuteOptions to CLI arguments
* - normalizeEvent(): Convert CLI output to ProviderMessage
*/
export abstract class CliProvider extends BaseProvider {
// CLI detection results (cached after first detection)
protected cliPath: string | null = null;
protected useWsl: boolean = false;
protected wslCliPath: string | null = null;
protected wslDistribution: string | undefined = undefined;
protected detectedStrategy: SpawnStrategy | 'native' = 'native';
// NPX args (used when strategy is 'npx')
protected npxArgs: string[] = [];
constructor(config: ProviderConfig = {}) {
super(config);
// Detection happens lazily on first use
}
// ==========================================================================
// Abstract methods - must be implemented by subclasses
// ==========================================================================
/**
* Get the CLI executable name (e.g., 'cursor-agent', 'aider')
*/
abstract getCliName(): string;
/**
* Get spawn configuration for this CLI
*/
abstract getSpawnConfig(): CliSpawnConfig;
/**
* Build CLI arguments from execution options
* @param options Execution options
* @returns Array of CLI arguments
*/
abstract buildCliArgs(options: ExecuteOptions): string[];
/**
* Normalize a raw CLI event to ProviderMessage format
* @param event Raw event from CLI JSONL output
* @returns Normalized ProviderMessage or null to skip
*/
abstract normalizeEvent(event: unknown): ProviderMessage | null;
// ==========================================================================
// Optional overrides
// ==========================================================================
/**
* Map CLI stderr/exit code to error info
* Override to provide CLI-specific error mapping
*/
protected mapError(stderr: string, exitCode: number | null): CliErrorInfo {
const lower = stderr.toLowerCase();
// Common authentication errors
if (
lower.includes('not authenticated') ||
lower.includes('please log in') ||
lower.includes('unauthorized')
) {
return {
code: 'NOT_AUTHENTICATED',
message: `${this.getCliName()} is not authenticated`,
recoverable: true,
suggestion: `Run "${this.getCliName()} login" to authenticate`,
};
}
// Rate limiting
if (
lower.includes('rate limit') ||
lower.includes('too many requests') ||
lower.includes('429')
) {
return {
code: 'RATE_LIMITED',
message: 'API rate limit exceeded',
recoverable: true,
suggestion: 'Wait a few minutes and try again',
};
}
// Network errors
if (
lower.includes('network') ||
lower.includes('connection') ||
lower.includes('econnrefused') ||
lower.includes('timeout')
) {
return {
code: 'NETWORK_ERROR',
message: 'Network connection error',
recoverable: true,
suggestion: 'Check your internet connection and try again',
};
}
// Process killed
if (exitCode === 137 || lower.includes('killed') || lower.includes('sigterm')) {
return {
code: 'PROCESS_CRASHED',
message: 'Process was terminated',
recoverable: true,
suggestion: 'The process may have run out of memory. Try a simpler task.',
};
}
// Generic error
return {
code: 'UNKNOWN_ERROR',
message: stderr || `Process exited with code ${exitCode}`,
recoverable: false,
};
}
/**
* Get installation instructions for this CLI
* Override to provide CLI-specific instructions
*/
protected getInstallInstructions(): string {
const cliName = this.getCliName();
const config = this.getSpawnConfig();
if (process.platform === 'win32') {
switch (config.windowsStrategy) {
case 'wsl':
return `${cliName} requires WSL on Windows. Install WSL, then run inside WSL to install.`;
case 'npx':
return `Install with: npm install -g ${config.npxPackage || cliName}`;
case 'cmd':
case 'direct':
return `${cliName} is not installed. Check the documentation for installation instructions.`;
}
}
return `${cliName} is not installed. Check the documentation for installation instructions.`;
}
// ==========================================================================
// CLI Detection
// ==========================================================================
/**
* Expand ~ to home directory in path
*/
private expandPath(p: string): string {
if (p.startsWith('~')) {
return path.join(os.homedir(), p.slice(1));
}
return p;
}
/**
* Find CLI in PATH using 'which' (Unix) or 'where' (Windows)
*/
private findCliInPath(): string | null {
const cliName = this.getCliName();
try {
const command = process.platform === 'win32' ? 'where' : 'which';
const result = execSync(`${command} ${cliName}`, {
encoding: 'utf8',
timeout: 5000,
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
})
.trim()
.split('\n')[0];
if (result && fs.existsSync(result)) {
cliLogger.debug(`Found ${cliName} in PATH: ${result}`);
return result;
}
} catch {
// Not in PATH
}
return null;
}
/**
* Find CLI in common installation paths for current platform
*/
private findCliInCommonPaths(): string | null {
const config = this.getSpawnConfig();
const cliName = this.getCliName();
const platform = process.platform as 'linux' | 'darwin' | 'win32';
const paths = config.commonPaths[platform] || [];
for (const p of paths) {
const expandedPath = this.expandPath(p);
if (fs.existsSync(expandedPath)) {
cliLogger.debug(`Found ${cliName} at: ${expandedPath}`);
return expandedPath;
}
}
return null;
}
/**
* Detect CLI installation using appropriate strategy
*/
protected detectCli(): CliDetectionResult {
const config = this.getSpawnConfig();
const cliName = this.getCliName();
const wslLogger = (msg: string) => cliLogger.debug(msg);
// Windows - use configured strategy
if (process.platform === 'win32') {
switch (config.windowsStrategy) {
case 'wsl': {
// Check WSL for CLI
if (isWslAvailable({ logger: wslLogger })) {
const wslResult: WslCliResult | null = findCliInWsl(cliName, {
logger: wslLogger,
distribution: config.wslDistribution,
});
if (wslResult) {
cliLogger.debug(
`Using ${cliName} via WSL (${wslResult.distribution || 'default'}): ${wslResult.wslPath}`
);
return {
cliPath: 'wsl.exe',
useWsl: true,
wslCliPath: wslResult.wslPath,
wslDistribution: wslResult.distribution,
strategy: 'wsl',
};
}
}
cliLogger.debug(`${cliName} not found (WSL not available or CLI not installed in WSL)`);
return { cliPath: null, useWsl: false, strategy: 'wsl' };
}
case 'npx': {
// For npx, we don't need to find the CLI, just return npx
cliLogger.debug(`Using ${cliName} via npx (package: ${config.npxPackage})`);
return {
cliPath: 'npx',
useWsl: false,
strategy: 'npx',
};
}
case 'direct':
case 'cmd': {
// Native Windows - check PATH and common paths
const pathResult = this.findCliInPath();
if (pathResult) {
return { cliPath: pathResult, useWsl: false, strategy: config.windowsStrategy };
}
const commonResult = this.findCliInCommonPaths();
if (commonResult) {
return { cliPath: commonResult, useWsl: false, strategy: config.windowsStrategy };
}
cliLogger.debug(`${cliName} not found on Windows`);
return { cliPath: null, useWsl: false, strategy: config.windowsStrategy };
}
}
}
// Linux/macOS - native execution
const pathResult = this.findCliInPath();
if (pathResult) {
return { cliPath: pathResult, useWsl: false, strategy: 'native' };
}
const commonResult = this.findCliInCommonPaths();
if (commonResult) {
return { cliPath: commonResult, useWsl: false, strategy: 'native' };
}
cliLogger.debug(`${cliName} not found`);
return { cliPath: null, useWsl: false, strategy: 'native' };
}
/**
* Ensure CLI is detected (lazy initialization)
*/
protected ensureCliDetected(): void {
if (this.cliPath !== null || this.detectedStrategy !== 'native') {
return; // Already detected
}
const result = this.detectCli();
this.cliPath = result.cliPath;
this.useWsl = result.useWsl;
this.wslCliPath = result.wslCliPath || null;
this.wslDistribution = result.wslDistribution;
this.detectedStrategy = result.strategy;
// Set up npx args if using npx strategy
const config = this.getSpawnConfig();
if (result.strategy === 'npx' && config.npxPackage) {
this.npxArgs = [config.npxPackage];
}
}
/**
* Check if CLI is installed
*/
async isInstalled(): Promise<boolean> {
this.ensureCliDetected();
return this.cliPath !== null;
}
// ==========================================================================
// Subprocess Spawning
// ==========================================================================
/**
* Build subprocess options based on detected strategy
*/
protected buildSubprocessOptions(options: ExecuteOptions, cliArgs: string[]): SubprocessOptions {
this.ensureCliDetected();
if (!this.cliPath) {
throw new Error(`${this.getCliName()} CLI not found. ${this.getInstallInstructions()}`);
}
const cwd = options.cwd || process.cwd();
// Filter undefined values from process.env
const filteredEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined) {
filteredEnv[key] = value;
}
}
// WSL strategy
if (this.useWsl && this.wslCliPath) {
const wslCwd = windowsToWslPath(cwd);
const wslCmd = createWslCommand(this.wslCliPath, cliArgs, {
distribution: this.wslDistribution,
});
// Add --cd flag to change directory inside WSL
let args: string[];
if (this.wslDistribution) {
args = ['-d', this.wslDistribution, '--cd', wslCwd, this.wslCliPath, ...cliArgs];
} else {
args = ['--cd', wslCwd, this.wslCliPath, ...cliArgs];
}
cliLogger.debug(`WSL spawn: ${wslCmd.command} ${args.slice(0, 6).join(' ')}...`);
return {
command: wslCmd.command,
args,
cwd, // Windows cwd for spawn
env: filteredEnv,
abortController: options.abortController,
timeout: 120000, // CLI operations may take longer
};
}
// NPX strategy
if (this.detectedStrategy === 'npx') {
const allArgs = [...this.npxArgs, ...cliArgs];
cliLogger.debug(`NPX spawn: npx ${allArgs.slice(0, 6).join(' ')}...`);
return {
command: 'npx',
args: allArgs,
cwd,
env: filteredEnv,
abortController: options.abortController,
timeout: 120000,
};
}
// Direct strategy (native Unix or Windows direct/cmd)
cliLogger.debug(`Direct spawn: ${this.cliPath} ${cliArgs.slice(0, 6).join(' ')}...`);
return {
command: this.cliPath,
args: cliArgs,
cwd,
env: filteredEnv,
abortController: options.abortController,
timeout: 120000,
};
}
/**
* Execute a query using the CLI with JSONL streaming
*
* This is a default implementation that:
* 1. Builds CLI args from options
* 2. Spawns the subprocess with appropriate strategy
* 3. Streams and normalizes events
*
* Subclasses can override for custom behavior.
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
this.ensureCliDetected();
if (!this.cliPath) {
throw new Error(`${this.getCliName()} CLI not found. ${this.getInstallInstructions()}`);
}
const cliArgs = this.buildCliArgs(options);
const subprocessOptions = this.buildSubprocessOptions(options, cliArgs);
try {
for await (const rawEvent of spawnJSONLProcess(subprocessOptions)) {
const normalized = this.normalizeEvent(rawEvent);
if (normalized) {
yield normalized;
}
}
} catch (error) {
if (isAbortError(error)) {
cliLogger.debug('Query aborted');
return;
}
// Map CLI errors
if (error instanceof Error && 'stderr' in error) {
const errorInfo = this.mapError(
(error as { stderr?: string }).stderr || error.message,
(error as { exitCode?: number | null }).exitCode ?? null
);
const cliError = new Error(errorInfo.message) as Error & CliErrorInfo;
cliError.code = errorInfo.code;
cliError.recoverable = errorInfo.recoverable;
cliError.suggestion = errorInfo.suggestion;
throw cliError;
}
throw error;
}
}
}

View File

@@ -0,0 +1,197 @@
/**
* Cursor CLI Configuration Manager
*
* Manages Cursor CLI configuration stored in .automaker/cursor-config.json
*/
import * as fs from 'fs';
import * as path from 'path';
import { getAllCursorModelIds, type CursorCliConfig, type CursorModelId } from '@automaker/types';
import { createLogger } from '@automaker/utils';
import { getAutomakerDir } from '@automaker/platform';
// Create logger for this module
const logger = createLogger('CursorConfigManager');
/**
* Manages Cursor CLI configuration
* Config location: .automaker/cursor-config.json
*/
export class CursorConfigManager {
private configPath: string;
private config: CursorCliConfig;
constructor(projectPath: string) {
// Use getAutomakerDir for consistent path resolution
this.configPath = path.join(getAutomakerDir(projectPath), 'cursor-config.json');
this.config = this.loadConfig();
}
/**
* Load configuration from disk
*/
private loadConfig(): CursorCliConfig {
try {
if (fs.existsSync(this.configPath)) {
const content = fs.readFileSync(this.configPath, 'utf8');
const parsed = JSON.parse(content) as CursorCliConfig;
logger.debug(`Loaded config from ${this.configPath}`);
return parsed;
}
} catch (error) {
logger.warn('Failed to load config:', error);
}
// Return default config with all available models
return {
defaultModel: 'auto',
models: getAllCursorModelIds(),
};
}
/**
* Save configuration to disk
*/
private saveConfig(): void {
try {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
logger.debug('Config saved');
} catch (error) {
logger.error('Failed to save config:', error);
throw error;
}
}
/**
* Get the full configuration
*/
getConfig(): CursorCliConfig {
return { ...this.config };
}
/**
* Get the default model
*/
getDefaultModel(): CursorModelId {
return this.config.defaultModel || 'auto';
}
/**
* Set the default model
*/
setDefaultModel(model: CursorModelId): void {
this.config.defaultModel = model;
this.saveConfig();
logger.info(`Default model set to: ${model}`);
}
/**
* Get enabled models
*/
getEnabledModels(): CursorModelId[] {
return this.config.models || ['auto'];
}
/**
* Set enabled models
*/
setEnabledModels(models: CursorModelId[]): void {
this.config.models = models;
this.saveConfig();
logger.info(`Enabled models updated: ${models.join(', ')}`);
}
/**
* Add a model to enabled list
*/
addModel(model: CursorModelId): void {
if (!this.config.models) {
this.config.models = [];
}
if (!this.config.models.includes(model)) {
this.config.models.push(model);
this.saveConfig();
logger.info(`Model added: ${model}`);
}
}
/**
* Remove a model from enabled list
*/
removeModel(model: CursorModelId): void {
if (this.config.models) {
this.config.models = this.config.models.filter((m) => m !== model);
this.saveConfig();
logger.info(`Model removed: ${model}`);
}
}
/**
* Check if a model is enabled
*/
isModelEnabled(model: CursorModelId): boolean {
return this.config.models?.includes(model) ?? false;
}
/**
* Get MCP server configurations
*/
getMcpServers(): string[] {
return this.config.mcpServers || [];
}
/**
* Set MCP server configurations
*/
setMcpServers(servers: string[]): void {
this.config.mcpServers = servers;
this.saveConfig();
logger.info(`MCP servers updated: ${servers.join(', ')}`);
}
/**
* Get Cursor rules paths
*/
getRules(): string[] {
return this.config.rules || [];
}
/**
* Set Cursor rules paths
*/
setRules(rules: string[]): void {
this.config.rules = rules;
this.saveConfig();
logger.info(`Rules updated: ${rules.join(', ')}`);
}
/**
* Reset configuration to defaults
*/
reset(): void {
this.config = {
defaultModel: 'auto',
models: getAllCursorModelIds(),
};
this.saveConfig();
logger.info('Config reset to defaults');
}
/**
* Check if config file exists
*/
exists(): boolean {
return fs.existsSync(this.configPath);
}
/**
* Get the config file path
*/
getConfigPath(): string {
return this.configPath;
}
}

View File

@@ -0,0 +1,993 @@
/**
* Cursor Provider - Executes queries using cursor-agent CLI
*
* Extends CliProvider with Cursor-specific:
* - Event normalization for Cursor's JSONL format
* - Text block deduplication (Cursor sends duplicates)
* - Session ID tracking
* - Versions directory detection
*
* Spawns the cursor-agent CLI with --output-format stream-json for streaming responses.
*/
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
CliProvider,
type CliSpawnConfig,
type CliDetectionResult,
type CliErrorInfo,
} from './cli-provider.js';
import type {
ProviderConfig,
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
ContentBlock,
} from './types.js';
import { stripProviderPrefix } from '@automaker/types';
import {
type CursorStreamEvent,
type CursorSystemEvent,
type CursorAssistantEvent,
type CursorToolCallEvent,
type CursorResultEvent,
type CursorAuthStatus,
CURSOR_MODEL_MAP,
} from '@automaker/types';
import { createLogger, isAbortError } from '@automaker/utils';
import { spawnJSONLProcess, execInWsl } from '@automaker/platform';
// Create logger for this module
const logger = createLogger('CursorProvider');
// =============================================================================
// Cursor Tool Handler Registry
// =============================================================================
/**
* Tool handler definition for mapping Cursor tool calls to normalized format
*/
interface CursorToolHandler<TArgs = unknown, TResult = unknown> {
/** The normalized tool name (e.g., 'Read', 'Write') */
name: string;
/** Extract and normalize input from Cursor's args format */
mapInput: (args: TArgs) => unknown;
/** Format the result content for display (optional) */
formatResult?: (result: TResult, args?: TArgs) => string;
/** Format rejected result (optional) */
formatRejected?: (reason: string) => string;
}
/**
* Registry of Cursor tool handlers
* Each handler knows how to normalize its specific tool call type
*/
const CURSOR_TOOL_HANDLERS: Record<string, CursorToolHandler<any, any>> = {
readToolCall: {
name: 'Read',
mapInput: (args: { path: string }) => ({ file_path: args.path }),
formatResult: (result: { content: string }) => result.content,
},
writeToolCall: {
name: 'Write',
mapInput: (args: { path: string; fileText: string }) => ({
file_path: args.path,
content: args.fileText,
}),
formatResult: (result: { linesCreated: number; path: string }) =>
`Wrote ${result.linesCreated} lines to ${result.path}`,
},
editToolCall: {
name: 'Edit',
mapInput: (args: { path: string; oldText?: string; newText?: string }) => ({
file_path: args.path,
old_string: args.oldText,
new_string: args.newText,
}),
formatResult: (_result: unknown, args?: { path: string }) => `Edited file: ${args?.path}`,
},
shellToolCall: {
name: 'Bash',
mapInput: (args: { command: string }) => ({ command: args.command }),
formatResult: (result: { exitCode: number; stdout?: string; stderr?: string }) => {
let content = `Exit code: ${result.exitCode}`;
if (result.stdout) content += `\n${result.stdout}`;
if (result.stderr) content += `\nStderr: ${result.stderr}`;
return content;
},
formatRejected: (reason: string) => `Rejected: ${reason}`,
},
deleteToolCall: {
name: 'Delete',
mapInput: (args: { path: string }) => ({ file_path: args.path }),
formatResult: (_result: unknown, args?: { path: string }) => `Deleted: ${args?.path}`,
formatRejected: (reason: string) => `Delete rejected: ${reason}`,
},
grepToolCall: {
name: 'Grep',
mapInput: (args: { pattern: string; path?: string }) => ({
pattern: args.pattern,
path: args.path,
}),
formatResult: (result: { matchedLines: number }) =>
`Found ${result.matchedLines} matching lines`,
},
lsToolCall: {
name: 'Ls',
mapInput: (args: { path: string }) => ({ path: args.path }),
formatResult: (result: { childrenFiles: number; childrenDirs: number }) =>
`Found ${result.childrenFiles} files, ${result.childrenDirs} directories`,
},
globToolCall: {
name: 'Glob',
mapInput: (args: { globPattern: string; targetDirectory?: string }) => ({
pattern: args.globPattern,
path: args.targetDirectory,
}),
formatResult: (result: { totalFiles: number }) => `Found ${result.totalFiles} matching files`,
},
semSearchToolCall: {
name: 'SemanticSearch',
mapInput: (args: { query: string; targetDirectories?: string[]; explanation?: string }) => ({
query: args.query,
targetDirectories: args.targetDirectories,
explanation: args.explanation,
}),
formatResult: (result: { results: string; codeResults?: unknown[] }) => {
const resultCount = result.codeResults?.length || 0;
return resultCount > 0
? `Found ${resultCount} semantic search result(s)`
: result.results || 'No results found';
},
},
readLintsToolCall: {
name: 'ReadLints',
mapInput: (args: { paths: string[] }) => ({ paths: args.paths }),
formatResult: (result: { totalDiagnostics: number; totalFiles: number }) =>
`Found ${result.totalDiagnostics} diagnostic(s) in ${result.totalFiles} file(s)`,
},
};
/**
* Process a Cursor tool call using the handler registry
* Returns { toolName, toolInput } or null if tool type is unknown
*/
function processCursorToolCall(
toolCall: CursorToolCallEvent['tool_call']
): { toolName: string; toolInput: unknown } | null {
// Check each registered handler
for (const [key, handler] of Object.entries(CURSOR_TOOL_HANDLERS)) {
const toolData = toolCall[key as keyof typeof toolCall] as { args?: unknown } | undefined;
if (toolData) {
// Skip if args not yet populated (partial streaming event)
if (!toolData.args) return null;
return {
toolName: handler.name,
toolInput: handler.mapInput(toolData.args),
};
}
}
// Handle generic function call (fallback)
if (toolCall.function) {
let toolInput: unknown;
try {
toolInput = JSON.parse(toolCall.function.arguments || '{}');
} catch {
toolInput = { raw: toolCall.function.arguments };
}
return {
toolName: toolCall.function.name,
toolInput,
};
}
return null;
}
/**
* Format the result content for a completed Cursor tool call
*/
function formatCursorToolResult(toolCall: CursorToolCallEvent['tool_call']): string {
for (const [key, handler] of Object.entries(CURSOR_TOOL_HANDLERS)) {
const toolData = toolCall[key as keyof typeof toolCall] as
| {
args?: unknown;
result?: { success?: unknown; rejected?: { reason: string } };
}
| undefined;
if (toolData?.result) {
if (toolData.result.success && handler.formatResult) {
return handler.formatResult(toolData.result.success, toolData.args);
}
if (toolData.result.rejected && handler.formatRejected) {
return handler.formatRejected(toolData.result.rejected.reason);
}
}
}
return '';
}
// =============================================================================
// Error Codes
// =============================================================================
/**
* Cursor-specific error codes for detailed error handling
*/
export enum CursorErrorCode {
NOT_INSTALLED = 'CURSOR_NOT_INSTALLED',
NOT_AUTHENTICATED = 'CURSOR_NOT_AUTHENTICATED',
RATE_LIMITED = 'CURSOR_RATE_LIMITED',
MODEL_UNAVAILABLE = 'CURSOR_MODEL_UNAVAILABLE',
NETWORK_ERROR = 'CURSOR_NETWORK_ERROR',
PROCESS_CRASHED = 'CURSOR_PROCESS_CRASHED',
TIMEOUT = 'CURSOR_TIMEOUT',
UNKNOWN = 'CURSOR_UNKNOWN_ERROR',
}
export interface CursorError extends Error {
code: CursorErrorCode;
recoverable: boolean;
suggestion?: string;
}
/**
* CursorProvider - Integrates cursor-agent CLI as an AI provider
*
* Extends CliProvider with Cursor-specific behavior:
* - WSL required on Windows (cursor-agent has no native Windows build)
* - Versions directory detection for cursor-agent installations
* - Session ID tracking for conversation continuity
* - Text block deduplication (Cursor sends duplicate chunks)
*/
export class CursorProvider extends CliProvider {
/**
* Version data directory where cursor-agent stores versions
* The install script creates versioned folders like:
* ~/.local/share/cursor-agent/versions/2025.12.17-996666f/cursor-agent
*/
private static VERSIONS_DIR = path.join(os.homedir(), '.local/share/cursor-agent/versions');
constructor(config: ProviderConfig = {}) {
super(config);
// Trigger CLI detection on construction (eager for Cursor)
this.ensureCliDetected();
}
// ==========================================================================
// CliProvider Abstract Method Implementations
// ==========================================================================
getName(): string {
return 'cursor';
}
getCliName(): string {
return 'cursor-agent';
}
getSpawnConfig(): CliSpawnConfig {
return {
windowsStrategy: 'wsl', // cursor-agent requires WSL on Windows
commonPaths: {
linux: [
path.join(os.homedir(), '.local/bin/cursor-agent'), // Primary symlink location
'/usr/local/bin/cursor-agent',
],
darwin: [path.join(os.homedir(), '.local/bin/cursor-agent'), '/usr/local/bin/cursor-agent'],
// Windows paths are not used - we check for WSL installation instead
win32: [],
},
};
}
/**
* Extract prompt text from ExecuteOptions
* Used to pass prompt via stdin instead of CLI args to avoid shell escaping issues
*/
private extractPromptText(options: ExecuteOptions): string {
if (typeof options.prompt === 'string') {
return options.prompt;
} else if (Array.isArray(options.prompt)) {
return options.prompt
.filter((p) => p.type === 'text' && p.text)
.map((p) => p.text)
.join('\n');
} else {
throw new Error('Invalid prompt format');
}
}
buildCliArgs(options: ExecuteOptions): string[] {
// Extract model (strip 'cursor-' prefix if present)
const model = stripProviderPrefix(options.model || 'auto');
// Build CLI arguments for cursor-agent
// NOTE: Prompt is NOT included here - it's passed via stdin to avoid
// shell escaping issues when content contains $(), backticks, etc.
const cliArgs: string[] = [
'-p', // Print mode (non-interactive)
'--output-format',
'stream-json',
'--stream-partial-output', // Real-time streaming
];
// Only add --force if NOT in read-only mode
// Without --force, Cursor CLI suggests changes but doesn't apply them
// With --force, Cursor CLI can actually edit files
if (!options.readOnly) {
cliArgs.push('--force');
}
// Add model if not auto
if (model !== 'auto') {
cliArgs.push('--model', model);
}
// Use '-' to indicate reading prompt from stdin
cliArgs.push('-');
return cliArgs;
}
/**
* Convert Cursor event to AutoMaker ProviderMessage format
* Made public as required by CliProvider abstract method
*/
normalizeEvent(event: unknown): ProviderMessage | null {
const cursorEvent = event as CursorStreamEvent;
switch (cursorEvent.type) {
case 'system':
// System init - we capture session_id but don't yield a message
return null;
case 'user':
// User message - already handled by caller
return null;
case 'assistant': {
const assistantEvent = cursorEvent as CursorAssistantEvent;
return {
type: 'assistant',
session_id: assistantEvent.session_id,
message: {
role: 'assistant',
content: assistantEvent.message.content.map((c) => ({
type: 'text' as const,
text: c.text,
})),
},
};
}
case 'tool_call': {
const toolEvent = cursorEvent as CursorToolCallEvent;
const toolCall = toolEvent.tool_call;
// Use the tool handler registry to process the tool call
const processed = processCursorToolCall(toolCall);
if (!processed) {
// Log unrecognized tool call structure for debugging
const toolCallKeys = Object.keys(toolCall);
logger.warn(
`[UNHANDLED TOOL_CALL] Unknown tool call structure. Keys: ${toolCallKeys.join(', ')}. ` +
`Full tool_call: ${JSON.stringify(toolCall).substring(0, 500)}`
);
return null;
}
const { toolName, toolInput } = processed;
// For started events, emit tool_use
if (toolEvent.subtype === 'started') {
return {
type: 'assistant',
session_id: toolEvent.session_id,
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
name: toolName,
tool_use_id: toolEvent.call_id,
input: toolInput,
},
],
},
};
}
// For completed events, emit both tool_use and tool_result
if (toolEvent.subtype === 'completed') {
const resultContent = formatCursorToolResult(toolCall);
return {
type: 'assistant',
session_id: toolEvent.session_id,
message: {
role: 'assistant',
content: [
{
type: 'tool_use',
name: toolName,
tool_use_id: toolEvent.call_id,
input: toolInput,
},
{
type: 'tool_result',
tool_use_id: toolEvent.call_id,
content: resultContent,
},
],
},
};
}
return null;
}
case 'result': {
const resultEvent = cursorEvent as CursorResultEvent;
if (resultEvent.is_error) {
return {
type: 'error',
session_id: resultEvent.session_id,
error: resultEvent.error || resultEvent.result || 'Unknown error',
};
}
return {
type: 'result',
subtype: 'success',
session_id: resultEvent.session_id,
result: resultEvent.result,
};
}
default:
return null;
}
}
// ==========================================================================
// CliProvider Overrides
// ==========================================================================
/**
* Override CLI detection to add Cursor-specific versions directory check
*/
protected detectCli(): CliDetectionResult {
// First try standard detection (PATH, common paths, WSL)
const result = super.detectCli();
if (result.cliPath) {
return result;
}
// Cursor-specific: Check versions directory for any installed version
// This handles cases where cursor-agent is installed but not in PATH
if (process.platform !== 'win32' && fs.existsSync(CursorProvider.VERSIONS_DIR)) {
try {
const versions = fs
.readdirSync(CursorProvider.VERSIONS_DIR)
.filter((v) => !v.startsWith('.'))
.sort()
.reverse(); // Most recent first
for (const version of versions) {
const versionPath = path.join(CursorProvider.VERSIONS_DIR, version, 'cursor-agent');
if (fs.existsSync(versionPath)) {
logger.debug(`Found cursor-agent version ${version} at: ${versionPath}`);
return {
cliPath: versionPath,
useWsl: false,
strategy: 'native',
};
}
}
} catch {
// Ignore directory read errors
}
}
return result;
}
/**
* Override error mapping for Cursor-specific error codes
*/
protected mapError(stderr: string, exitCode: number | null): CliErrorInfo {
const lower = stderr.toLowerCase();
if (
lower.includes('not authenticated') ||
lower.includes('please log in') ||
lower.includes('unauthorized')
) {
return {
code: CursorErrorCode.NOT_AUTHENTICATED,
message: 'Cursor CLI is not authenticated',
recoverable: true,
suggestion: 'Run "cursor-agent login" to authenticate with your browser',
};
}
if (
lower.includes('rate limit') ||
lower.includes('too many requests') ||
lower.includes('429')
) {
return {
code: CursorErrorCode.RATE_LIMITED,
message: 'Cursor API rate limit exceeded',
recoverable: true,
suggestion: 'Wait a few minutes and try again, or upgrade to Cursor Pro',
};
}
if (
lower.includes('model not available') ||
lower.includes('invalid model') ||
lower.includes('unknown model')
) {
return {
code: CursorErrorCode.MODEL_UNAVAILABLE,
message: 'Requested model is not available',
recoverable: true,
suggestion: 'Try using "auto" mode or select a different model',
};
}
if (
lower.includes('network') ||
lower.includes('connection') ||
lower.includes('econnrefused') ||
lower.includes('timeout')
) {
return {
code: CursorErrorCode.NETWORK_ERROR,
message: 'Network connection error',
recoverable: true,
suggestion: 'Check your internet connection and try again',
};
}
if (exitCode === 137 || lower.includes('killed') || lower.includes('sigterm')) {
return {
code: CursorErrorCode.PROCESS_CRASHED,
message: 'Cursor agent process was terminated',
recoverable: true,
suggestion: 'The process may have run out of memory. Try a simpler task.',
};
}
return {
code: CursorErrorCode.UNKNOWN,
message: stderr || `Cursor agent exited with code ${exitCode}`,
recoverable: false,
};
}
/**
* Override install instructions for Cursor-specific guidance
*/
protected getInstallInstructions(): string {
if (process.platform === 'win32') {
return 'cursor-agent requires WSL on Windows. Install WSL, then run in WSL: curl https://cursor.com/install -fsS | bash';
}
return 'Install with: curl https://cursor.com/install -fsS | bash';
}
/**
* Execute a prompt using Cursor CLI with streaming
*
* Overrides base class to add:
* - Session ID tracking from system init events
* - Text block deduplication (Cursor sends duplicate chunks)
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
this.ensureCliDetected();
if (!this.cliPath) {
throw this.createError(
CursorErrorCode.NOT_INSTALLED,
'Cursor CLI is not installed',
true,
this.getInstallInstructions()
);
}
// MCP servers are not yet supported by Cursor CLI - log warning but continue
if (options.mcpServers && Object.keys(options.mcpServers).length > 0) {
const serverCount = Object.keys(options.mcpServers).length;
logger.warn(
`MCP servers configured (${serverCount}) but not yet supported by Cursor CLI in AutoMaker. ` +
`MCP support for Cursor will be added in a future release. ` +
`The configured MCP servers will be ignored for this execution.`
);
}
// Extract prompt text to pass via stdin (avoids shell escaping issues)
const promptText = this.extractPromptText(options);
const cliArgs = this.buildCliArgs(options);
const subprocessOptions = this.buildSubprocessOptions(options, cliArgs);
// Pass prompt via stdin to avoid shell interpretation of special characters
// like $(), backticks, etc. that may appear in file content
subprocessOptions.stdinData = promptText;
let sessionId: string | undefined;
// Dedup state for Cursor-specific text block handling
let lastTextBlock = '';
let accumulatedText = '';
logger.debug(`CursorProvider.executeQuery called with model: "${options.model}"`);
// Debug: log raw events when AUTOMAKER_DEBUG_RAW_OUTPUT is enabled
const debugRawEvents =
process.env.AUTOMAKER_DEBUG_RAW_OUTPUT === 'true' ||
process.env.AUTOMAKER_DEBUG_RAW_OUTPUT === '1';
try {
for await (const rawEvent of spawnJSONLProcess(subprocessOptions)) {
const event = rawEvent as CursorStreamEvent;
// Log raw event for debugging
if (debugRawEvents) {
const subtype = 'subtype' in event ? (event.subtype as string) : 'none';
logger.info(`[RAW EVENT] type=${event.type} subtype=${subtype}`);
if (event.type === 'tool_call') {
const toolEvent = event as CursorToolCallEvent;
const tc = toolEvent.tool_call;
const toolTypes =
[
tc.readToolCall && 'read',
tc.writeToolCall && 'write',
tc.editToolCall && 'edit',
tc.shellToolCall && 'shell',
tc.deleteToolCall && 'delete',
tc.grepToolCall && 'grep',
tc.lsToolCall && 'ls',
tc.globToolCall && 'glob',
tc.function && `function:${tc.function.name}`,
]
.filter(Boolean)
.join(',') || 'unknown';
logger.info(
`[RAW TOOL_CALL] call_id=${toolEvent.call_id} types=[${toolTypes}]` +
(tc.shellToolCall ? ` cmd="${tc.shellToolCall.args?.command}"` : '') +
(tc.writeToolCall ? ` path="${tc.writeToolCall.args?.path}"` : '')
);
}
}
// Capture session ID from system init
if (event.type === 'system' && (event as CursorSystemEvent).subtype === 'init') {
sessionId = event.session_id;
logger.debug(`Session started: ${sessionId}`);
}
// Normalize and yield the event
const normalized = this.normalizeEvent(event);
if (!normalized && debugRawEvents) {
logger.info(`[DROPPED EVENT] type=${event.type} - normalizeEvent returned null`);
}
if (normalized) {
// Ensure session_id is always set
if (!normalized.session_id && sessionId) {
normalized.session_id = sessionId;
}
// Apply Cursor-specific dedup for assistant text messages
if (normalized.type === 'assistant' && normalized.message?.content) {
const dedupedContent = this.deduplicateTextBlocks(
normalized.message.content,
lastTextBlock,
accumulatedText
);
if (dedupedContent.content.length === 0) {
// All blocks were duplicates, skip this message
continue;
}
// Update state
lastTextBlock = dedupedContent.lastBlock;
accumulatedText = dedupedContent.accumulated;
// Update the message with deduped content
normalized.message.content = dedupedContent.content;
}
yield normalized;
}
}
} catch (error) {
if (isAbortError(error)) {
logger.debug('Query aborted');
return;
}
// Map CLI errors to CursorError
if (error instanceof Error && 'stderr' in error) {
const errorInfo = this.mapError(
(error as { stderr?: string }).stderr || error.message,
(error as { exitCode?: number | null }).exitCode ?? null
);
throw this.createError(
errorInfo.code as CursorErrorCode,
errorInfo.message,
errorInfo.recoverable,
errorInfo.suggestion
);
}
throw error;
}
}
// ==========================================================================
// Cursor-Specific Methods
// ==========================================================================
/**
* Create a CursorError with details
*/
private createError(
code: CursorErrorCode,
message: string,
recoverable: boolean = false,
suggestion?: string
): CursorError {
const error = new Error(message) as CursorError;
error.code = code;
error.recoverable = recoverable;
error.suggestion = suggestion;
error.name = 'CursorError';
return error;
}
/**
* Deduplicate text blocks in Cursor assistant messages
*
* Cursor often sends:
* 1. Duplicate consecutive text blocks (same text twice in a row)
* 2. A final accumulated block containing ALL previous text
*
* This method filters out these duplicates to prevent UI stuttering.
*/
private deduplicateTextBlocks(
content: ContentBlock[],
lastTextBlock: string,
accumulatedText: string
): { content: ContentBlock[]; lastBlock: string; accumulated: string } {
const filtered: ContentBlock[] = [];
let newLastBlock = lastTextBlock;
let newAccumulated = accumulatedText;
for (const block of content) {
if (block.type !== 'text' || !block.text) {
filtered.push(block);
continue;
}
const text = block.text;
// Skip empty text
if (!text.trim()) continue;
// Skip duplicate consecutive text blocks
if (text === newLastBlock) {
continue;
}
// Skip final accumulated text block
// Cursor sends one large block containing ALL previous text at the end
if (newAccumulated.length > 100 && text.length > newAccumulated.length * 0.8) {
const normalizedAccum = newAccumulated.replace(/\s+/g, ' ').trim();
const normalizedNew = text.replace(/\s+/g, ' ').trim();
if (normalizedNew.includes(normalizedAccum.slice(0, 100))) {
// This is the final accumulated block, skip it
continue;
}
}
// This is a valid new text block
newLastBlock = text;
newAccumulated += text;
filtered.push(block);
}
return {
content: filtered,
lastBlock: newLastBlock,
accumulated: newAccumulated,
};
}
/**
* Get Cursor CLI version
*/
async getVersion(): Promise<string | null> {
this.ensureCliDetected();
if (!this.cliPath) return null;
try {
if (this.useWsl && this.wslCliPath) {
const result = execInWsl(`${this.wslCliPath} --version`, {
timeout: 5000,
distribution: this.wslDistribution,
});
return result;
}
const result = execSync(`"${this.cliPath}" --version`, {
encoding: 'utf8',
timeout: 5000,
}).trim();
return result;
} catch {
return null;
}
}
/**
* Check authentication status
*/
async checkAuth(): Promise<CursorAuthStatus> {
this.ensureCliDetected();
if (!this.cliPath) {
return { authenticated: false, method: 'none' };
}
// Check for API key in environment
if (process.env.CURSOR_API_KEY) {
return { authenticated: true, method: 'api_key' };
}
// For WSL mode, check credentials inside WSL
if (this.useWsl && this.wslCliPath) {
const wslOpts = { timeout: 5000, distribution: this.wslDistribution };
// Check for credentials file inside WSL
const wslCredPaths = [
'$HOME/.cursor/credentials.json',
'$HOME/.config/cursor/credentials.json',
];
for (const credPath of wslCredPaths) {
const content = execInWsl(`sh -c "cat ${credPath} 2>/dev/null || echo ''"`, wslOpts);
if (content && content.trim()) {
try {
const creds = JSON.parse(content);
if (creds.accessToken || creds.token) {
return { authenticated: true, method: 'login', hasCredentialsFile: true };
}
} catch {
// Invalid credentials file
}
}
}
// Try running --version to check if CLI works
const versionResult = execInWsl(`${this.wslCliPath} --version`, {
timeout: 10000,
distribution: this.wslDistribution,
});
if (versionResult) {
return { authenticated: true, method: 'login' };
}
return { authenticated: false, method: 'none' };
}
// Native mode (Linux/macOS) - check local credentials
const credentialPaths = [
path.join(os.homedir(), '.cursor', 'credentials.json'),
path.join(os.homedir(), '.config', 'cursor', 'credentials.json'),
];
for (const credPath of credentialPaths) {
if (fs.existsSync(credPath)) {
try {
const content = fs.readFileSync(credPath, 'utf8');
const creds = JSON.parse(content);
if (creds.accessToken || creds.token) {
return { authenticated: true, method: 'login', hasCredentialsFile: true };
}
} catch {
// Invalid credentials file
}
}
}
// Try running a simple command to check auth
try {
execSync(`"${this.cliPath}" --version`, {
encoding: 'utf8',
timeout: 10000,
env: { ...process.env },
});
return { authenticated: true, method: 'login' };
} catch (error: unknown) {
const execError = error as { stderr?: string };
if (execError.stderr?.includes('not authenticated') || execError.stderr?.includes('log in')) {
return { authenticated: false, method: 'none' };
}
}
return { authenticated: false, method: 'none' };
}
/**
* Detect installation status (required by BaseProvider)
*/
async detectInstallation(): Promise<InstallationStatus> {
const installed = await this.isInstalled();
const version = installed ? await this.getVersion() : undefined;
const auth = await this.checkAuth();
// Determine the display path - for WSL, show the WSL path with distribution
const displayPath =
this.useWsl && this.wslCliPath
? `(WSL${this.wslDistribution ? `:${this.wslDistribution}` : ''}) ${this.wslCliPath}`
: this.cliPath || undefined;
return {
installed,
version: version || undefined,
path: displayPath,
method: this.useWsl ? 'wsl' : 'cli',
hasApiKey: !!process.env.CURSOR_API_KEY,
authenticated: auth.authenticated,
};
}
/**
* Get the detected CLI path (public accessor for status endpoints)
*/
getCliPath(): string | null {
this.ensureCliDetected();
return this.cliPath;
}
/**
* Get available Cursor models
*/
getAvailableModels(): ModelDefinition[] {
return Object.entries(CURSOR_MODEL_MAP).map(([id, config]) => ({
id: `cursor-${id}`,
name: config.label,
modelString: id,
provider: 'cursor',
description: config.description,
supportsTools: true,
supportsVision: config.supportsVision,
}));
}
/**
* Check if a feature is supported
*/
supportsFeature(feature: string): boolean {
const supported = ['tools', 'text', 'streaming'];
return supported.includes(feature);
}
}

View File

@@ -0,0 +1,29 @@
/**
* Provider exports
*/
// Base providers
export { BaseProvider } from './base-provider.js';
export {
CliProvider,
type SpawnStrategy,
type CliSpawnConfig,
type CliErrorInfo,
} from './cli-provider.js';
export type {
ProviderConfig,
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
} from './types.js';
// Claude provider
export { ClaudeProvider } from './claude-provider.js';
// Cursor provider
export { CursorProvider, CursorErrorCode, CursorError } from './cursor-provider.js';
export { CursorConfigManager } from './cursor-config-manager.js';
// Provider factory
export { ProviderFactory } from './provider-factory.js';

View File

@@ -1,51 +1,103 @@
/** /**
* Provider Factory - Routes model IDs to the appropriate provider * Provider Factory - Routes model IDs to the appropriate provider
* *
* This factory implements model-based routing to automatically select * Uses a registry pattern for dynamic provider registration.
* the correct provider based on the model string. This makes adding * Providers register themselves on import, making it easy to add new providers.
* new providers (Cursor, OpenCode, etc.) trivial - just add one line.
*/ */
import { BaseProvider } from './base-provider.js'; import { BaseProvider } from './base-provider.js';
import { ClaudeProvider } from './claude-provider.js'; import type { InstallationStatus, ModelDefinition } from './types.js';
import type { InstallationStatus } from './types.js'; import { isCursorModel, type ModelProvider } from '@automaker/types';
/**
* Provider registration entry
*/
interface ProviderRegistration {
/** Factory function to create provider instance */
factory: () => BaseProvider;
/** Aliases for this provider (e.g., 'anthropic' for 'claude') */
aliases?: string[];
/** Function to check if this provider can handle a model ID */
canHandleModel?: (modelId: string) => boolean;
/** Priority for model matching (higher = checked first) */
priority?: number;
}
/**
* Provider registry - stores registered providers
*/
const providerRegistry = new Map<string, ProviderRegistration>();
/**
* Register a provider with the factory
*
* @param name Provider name (e.g., 'claude', 'cursor')
* @param registration Provider registration config
*/
export function registerProvider(name: string, registration: ProviderRegistration): void {
providerRegistry.set(name.toLowerCase(), registration);
}
export class ProviderFactory { export class ProviderFactory {
/**
* Determine which provider to use for a given model
*
* @param model Model identifier
* @returns Provider name (ModelProvider type)
*/
static getProviderNameForModel(model: string): ModelProvider {
const lowerModel = model.toLowerCase();
// Get all registered providers sorted by priority (descending)
const registrations = Array.from(providerRegistry.entries()).sort(
([, a], [, b]) => (b.priority ?? 0) - (a.priority ?? 0)
);
// Check each provider's canHandleModel function
for (const [name, reg] of registrations) {
if (reg.canHandleModel?.(lowerModel)) {
return name as ModelProvider;
}
}
// Fallback: Check for explicit prefixes
for (const [name] of registrations) {
if (lowerModel.startsWith(`${name}-`)) {
return name as ModelProvider;
}
}
// Default to claude (first registered provider or claude)
return 'claude';
}
/** /**
* Get the appropriate provider for a given model ID * Get the appropriate provider for a given model ID
* *
* @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "gpt-5.2", "cursor-fast") * @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto")
* @returns Provider instance for the model * @returns Provider instance for the model
*/ */
static getProviderForModel(modelId: string): BaseProvider { static getProviderForModel(modelId: string): BaseProvider {
const lowerModel = modelId.toLowerCase(); const providerName = this.getProviderNameForModel(modelId);
const provider = this.getProviderByName(providerName);
// Claude models (claude-*, opus, sonnet, haiku) if (!provider) {
if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) { // Fallback to claude if provider not found
return new ClaudeProvider(); const claudeReg = providerRegistry.get('claude');
if (claudeReg) {
return claudeReg.factory();
}
throw new Error(`No provider found for model: ${modelId}`);
} }
// Future providers: return provider;
// if (lowerModel.startsWith("cursor-")) {
// return new CursorProvider();
// }
// if (lowerModel.startsWith("opencode-")) {
// return new OpenCodeProvider();
// }
// Default to Claude for unknown models
console.warn(`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`);
return new ClaudeProvider();
} }
/** /**
* Get all available providers * Get all available providers
*/ */
static getAllProviders(): BaseProvider[] { static getAllProviders(): BaseProvider[] {
return [ return Array.from(providerRegistry.values()).map((reg) => reg.factory());
new ClaudeProvider(),
// Future providers...
];
} }
/** /**
@@ -54,11 +106,10 @@ export class ProviderFactory {
* @returns Map of provider name to installation status * @returns Map of provider name to installation status
*/ */
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> { static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
const providers = this.getAllProviders();
const statuses: Record<string, InstallationStatus> = {}; const statuses: Record<string, InstallationStatus> = {};
for (const provider of providers) { for (const [name, reg] of providerRegistry.entries()) {
const name = provider.getName(); const provider = reg.factory();
const status = await provider.detectInstallation(); const status = await provider.detectInstallation();
statuses[name] = status; statuses[name] = status;
} }
@@ -69,40 +120,67 @@ export class ProviderFactory {
/** /**
* Get provider by name (for direct access if needed) * Get provider by name (for direct access if needed)
* *
* @param name Provider name (e.g., "claude", "cursor") * @param name Provider name (e.g., "claude", "cursor") or alias (e.g., "anthropic")
* @returns Provider instance or null if not found * @returns Provider instance or null if not found
*/ */
static getProviderByName(name: string): BaseProvider | null { static getProviderByName(name: string): BaseProvider | null {
const lowerName = name.toLowerCase(); const lowerName = name.toLowerCase();
switch (lowerName) { // Direct lookup
case 'claude': const directReg = providerRegistry.get(lowerName);
case 'anthropic': if (directReg) {
return new ClaudeProvider(); return directReg.factory();
// Future providers:
// case "cursor":
// return new CursorProvider();
// case "opencode":
// return new OpenCodeProvider();
default:
return null;
} }
// Check aliases
for (const [, reg] of providerRegistry.entries()) {
if (reg.aliases?.includes(lowerName)) {
return reg.factory();
}
}
return null;
} }
/** /**
* Get all available models from all providers * Get all available models from all providers
*/ */
static getAllAvailableModels() { static getAllAvailableModels(): ModelDefinition[] {
const providers = this.getAllProviders(); const providers = this.getAllProviders();
const allModels = []; return providers.flatMap((p) => p.getAvailableModels());
}
for (const provider of providers) { /**
const models = provider.getAvailableModels(); * Get list of registered provider names
allModels.push(...models); */
} static getRegisteredProviderNames(): string[] {
return Array.from(providerRegistry.keys());
return allModels;
} }
} }
// =============================================================================
// Provider Registrations
// =============================================================================
// Import providers for registration side-effects
import { ClaudeProvider } from './claude-provider.js';
import { CursorProvider } from './cursor-provider.js';
// Register Claude provider
registerProvider('claude', {
factory: () => new ClaudeProvider(),
aliases: ['anthropic'],
canHandleModel: (model: string) => {
return (
model.startsWith('claude-') || ['opus', 'sonnet', 'haiku'].some((n) => model.includes(n))
);
},
priority: 0, // Default priority
});
// Register Cursor provider
registerProvider('cursor', {
factory: () => new CursorProvider(),
canHandleModel: (model: string) => isCursorModel(model),
priority: 10, // Higher priority - check Cursor models first
});

View File

@@ -1,106 +1,22 @@
/** /**
* Shared types for AI model providers * Shared types for AI model providers
*
* Re-exports types from @automaker/types for consistency across the codebase.
* All provider types are defined in @automaker/types to avoid duplication.
*/ */
/** // Re-export all provider types from @automaker/types
* Configuration for a provider instance export type {
*/ ProviderConfig,
export interface ProviderConfig { ConversationMessage,
apiKey?: string; ExecuteOptions,
cliPath?: string; McpServerConfig,
env?: Record<string, string>; McpStdioServerConfig,
} McpSSEServerConfig,
McpHttpServerConfig,
/** ContentBlock,
* Message in conversation history ProviderMessage,
*/ InstallationStatus,
export interface ConversationMessage { ValidationResult,
role: 'user' | 'assistant'; ModelDefinition,
content: string | Array<{ type: string; text?: string; source?: object }>; } from '@automaker/types';
}
/**
* Options for executing a query via a provider
*/
export interface ExecuteOptions {
prompt: string | Array<{ type: string; text?: string; source?: object }>;
model: string;
cwd: string;
systemPrompt?: string | { type: 'preset'; preset: 'claude_code'; append?: string };
maxTurns?: number;
allowedTools?: string[];
mcpServers?: Record<string, unknown>;
abortController?: AbortController;
conversationHistory?: ConversationMessage[]; // Previous messages for context
sdkSessionId?: string; // Claude SDK session ID for resuming conversations
settingSources?: Array<'user' | 'project' | 'local'>; // Claude filesystem settings to load
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean }; // Sandbox configuration
}
/**
* Content block in a provider message (matches Claude SDK format)
*/
export interface ContentBlock {
type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
text?: string;
thinking?: string;
name?: string;
input?: unknown;
tool_use_id?: string;
content?: string;
}
/**
* Message returned by a provider (matches Claude SDK streaming format)
*/
export interface ProviderMessage {
type: 'assistant' | 'user' | 'error' | 'result';
subtype?: 'success' | 'error';
session_id?: string;
message?: {
role: 'user' | 'assistant';
content: ContentBlock[];
};
result?: string;
error?: string;
parent_tool_use_id?: string | null;
}
/**
* Installation status for a provider
*/
export interface InstallationStatus {
installed: boolean;
path?: string;
version?: string;
method?: 'cli' | 'npm' | 'brew' | 'sdk';
hasApiKey?: boolean;
authenticated?: boolean;
error?: string;
}
/**
* Validation result
*/
export interface ValidationResult {
valid: boolean;
errors: string[];
warnings?: string[];
}
/**
* Model definition
*/
export interface ModelDefinition {
id: string;
name: string;
modelString: string;
provider: string;
description: string;
contextWindow?: number;
maxOutputTokens?: number;
supportsVision?: boolean;
supportsTools?: boolean;
tier?: 'basic' | 'standard' | 'premium';
default?: boolean;
}

View File

@@ -3,17 +3,19 @@
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import type { ThinkingLevel } from '@automaker/types';
import { AgentService } from '../../../services/agent-service.js'; import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError } from '../common.js';
export function createQueueAddHandler(agentService: AgentService) { export function createQueueAddHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { sessionId, message, imagePaths, model } = req.body as { const { sessionId, message, imagePaths, model, thinkingLevel } = req.body as {
sessionId: string; sessionId: string;
message: string; message: string;
imagePaths?: string[]; imagePaths?: string[];
model?: string; model?: string;
thinkingLevel?: ThinkingLevel;
}; };
if (!sessionId || !message) { if (!sessionId || !message) {
@@ -24,7 +26,12 @@ export function createQueueAddHandler(agentService: AgentService) {
return; return;
} }
const result = await agentService.addToQueue(sessionId, { message, imagePaths, model }); const result = await agentService.addToQueue(sessionId, {
message,
imagePaths,
model,
thinkingLevel,
});
res.json(result); res.json(result);
} catch (error) { } catch (error) {
logError(error, 'Add to queue failed'); logError(error, 'Add to queue failed');

View File

@@ -3,6 +3,7 @@
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import type { ThinkingLevel } from '@automaker/types';
import { AgentService } from '../../../services/agent-service.js'; import { AgentService } from '../../../services/agent-service.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError } from '../common.js';
@@ -11,24 +12,27 @@ const logger = createLogger('Agent');
export function createSendHandler(agentService: AgentService) { export function createSendHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { sessionId, message, workingDirectory, imagePaths, model } = req.body as { const { sessionId, message, workingDirectory, imagePaths, model, thinkingLevel } =
sessionId: string; req.body as {
message: string; sessionId: string;
workingDirectory?: string; message: string;
imagePaths?: string[]; workingDirectory?: string;
model?: string; imagePaths?: string[];
}; model?: string;
thinkingLevel?: ThinkingLevel;
};
console.log('[Send Handler] Received request:', { logger.debug('Received request:', {
sessionId, sessionId,
messageLength: message?.length, messageLength: message?.length,
workingDirectory, workingDirectory,
imageCount: imagePaths?.length || 0, imageCount: imagePaths?.length || 0,
model, model,
thinkingLevel,
}); });
if (!sessionId || !message) { if (!sessionId || !message) {
console.log('[Send Handler] ERROR: Validation failed - missing sessionId or message'); logger.warn('Validation failed - missing sessionId or message');
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: 'sessionId and message are required', error: 'sessionId and message are required',
@@ -36,7 +40,7 @@ export function createSendHandler(agentService: AgentService) {
return; return;
} }
console.log('[Send Handler] Validation passed, calling agentService.sendMessage()'); logger.debug('Validation passed, calling agentService.sendMessage()');
// Start the message processing (don't await - it streams via WebSocket) // Start the message processing (don't await - it streams via WebSocket)
agentService agentService
@@ -46,18 +50,19 @@ export function createSendHandler(agentService: AgentService) {
workingDirectory, workingDirectory,
imagePaths, imagePaths,
model, model,
thinkingLevel,
}) })
.catch((error) => { .catch((error) => {
console.error('[Send Handler] ERROR: Background error in sendMessage():', error); logger.error('Background error in sendMessage():', error);
logError(error, 'Send message failed (background)'); logError(error, 'Send message failed (background)');
}); });
console.log('[Send Handler] Returning immediate response to client'); logger.debug('Returning immediate response to client');
// Return immediately - responses come via WebSocket // Return immediately - responses come via WebSocket
res.json({ success: true, message: 'Message sent' }); res.json({ success: true, message: 'Message sent' });
} catch (error) { } catch (error) {
console.error('[Send Handler] ERROR: Synchronous error:', error); logger.error('Synchronous error:', error);
logError(error, 'Send message failed'); logError(error, 'Send message failed');
res.status(500).json({ success: false, error: getErrorMessage(error) }); res.status(500).json({ success: false, error: getErrorMessage(error) });
} }

View File

@@ -1,12 +1,18 @@
/** /**
* Generate features from existing app_spec.txt * Generate features from existing app_spec.txt
*
* Model is configurable via phaseModels.featureGenerationModel in settings
* (defaults to Sonnet for balanced speed and quality).
*/ */
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import * as secureFs from '../../lib/secure-fs.js'; import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js'; import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js'; import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { logAuthStatus } from './common.js'; import { logAuthStatus } from './common.js';
import { parseAndCreateFeatures } from './parse-and-create-features.js'; import { parseAndCreateFeatures } from './parse-and-create-features.js';
import { getAppSpecPath } from '@automaker/platform'; import { getAppSpecPath } from '@automaker/platform';
@@ -101,43 +107,46 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
'[FeatureGeneration]' '[FeatureGeneration]'
); );
const options = createFeatureGenerationOptions({ // Get model from phase settings
cwd: projectPath, const settings = await settingsService?.getGlobalSettings();
abortController, const phaseModelEntry =
autoLoadClaudeMd, settings?.phaseModels?.featureGenerationModel || DEFAULT_PHASE_MODELS.featureGenerationModel;
}); const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
logger.debug('SDK Options:', JSON.stringify(options, null, 2)); logger.info('Using model:', model);
logger.info('Calling Claude Agent SDK query() for features...');
logAuthStatus('Right before SDK query() for features');
let stream;
try {
stream = query({ prompt, options });
logger.debug('query() returned stream successfully');
} catch (queryError) {
logger.error('❌ query() threw an exception:');
logger.error('Error:', queryError);
throw queryError;
}
let responseText = ''; let responseText = '';
let messageCount = 0; let messageCount = 0;
logger.debug('Starting to iterate over feature stream...'); // Route to appropriate provider based on model type
if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
logger.info('[FeatureGeneration] Using Cursor provider');
try { const provider = ProviderFactory.getProviderForModel(model);
for await (const msg of stream) {
// Add explicit instructions for Cursor to return JSON in response
const cursorPrompt = `${prompt}
CRITICAL INSTRUCTIONS:
1. DO NOT write any files. Return the JSON in your response only.
2. Respond with ONLY a JSON object - no explanations, no markdown, just raw JSON.
3. Your entire response should be valid JSON starting with { and ending with }. No text before or after.`;
for await (const msg of provider.executeQuery({
prompt: cursorPrompt,
model,
cwd: projectPath,
maxTurns: 250,
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
readOnly: true, // Feature generation only reads code, doesn't write
})) {
messageCount++; messageCount++;
logger.debug(
`Feature stream message #${messageCount}:`,
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
);
if (msg.type === 'assistant' && msg.message.content) { if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) { for (const block of msg.message.content) {
if (block.type === 'text') { if (block.type === 'text' && block.text) {
responseText += block.text; responseText += block.text;
logger.debug(`Feature text block received (${block.text.length} chars)`); logger.debug(`Feature text block received (${block.text.length} chars)`);
events.emit('spec-regeneration:event', { events.emit('spec-regeneration:event', {
@@ -147,18 +156,75 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
}); });
} }
} }
} else if (msg.type === 'result' && (msg as any).subtype === 'success') { } else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
logger.debug('Received success result for features'); // Use result if it's a final accumulated message
responseText = (msg as any).result || responseText; if (msg.result.length > responseText.length) {
} else if ((msg as { type: string }).type === 'error') { responseText = msg.result;
logger.error('❌ Received error message from feature stream:'); }
logger.error('Error message:', JSON.stringify(msg, null, 2));
} }
} }
} catch (streamError) { } else {
logger.error('❌ Error while iterating feature stream:'); // Use Claude SDK for Claude models
logger.error('Stream error:', streamError); logger.info('[FeatureGeneration] Using Claude SDK');
throw streamError;
const options = createFeatureGenerationOptions({
cwd: projectPath,
abortController,
autoLoadClaudeMd,
model,
thinkingLevel, // Pass thinking level for extended thinking
});
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
logger.info('Calling Claude Agent SDK query() for features...');
logAuthStatus('Right before SDK query() for features');
let stream;
try {
stream = query({ prompt, options });
logger.debug('query() returned stream successfully');
} catch (queryError) {
logger.error('❌ query() threw an exception:');
logger.error('Error:', queryError);
throw queryError;
}
logger.debug('Starting to iterate over feature stream...');
try {
for await (const msg of stream) {
messageCount++;
logger.debug(
`Feature stream message #${messageCount}:`,
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
);
if (msg.type === 'assistant' && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
responseText += block.text;
logger.debug(`Feature text block received (${block.text.length} chars)`);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: block.text,
projectPath: projectPath,
});
}
}
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
logger.debug('Received success result for features');
responseText = (msg as any).result || responseText;
} else if ((msg as { type: string }).type === 'error') {
logger.error('❌ Received error message from feature stream:');
logger.error('Error message:', JSON.stringify(msg, null, 2));
}
}
} catch (streamError) {
logger.error('❌ Error while iterating feature stream:');
logger.error('Stream error:', streamError);
throw streamError;
}
} }
logger.info(`Feature stream complete. Total messages: ${messageCount}`); logger.info(`Feature stream complete. Total messages: ${messageCount}`);

View File

@@ -1,5 +1,8 @@
/** /**
* Generate app_spec.txt from project overview * Generate app_spec.txt from project overview
*
* Model is configurable via phaseModels.specGenerationModel in settings
* (defaults to Opus for high-quality specification generation).
*/ */
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
@@ -13,7 +16,11 @@ import {
type SpecOutput, type SpecOutput,
} from '../../lib/app-spec-format.js'; } from '../../lib/app-spec-format.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createSpecGenerationOptions } from '../../lib/sdk-options.js'; import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
import { extractJson } from '../../lib/json-extractor.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { logAuthStatus } from './common.js'; import { logAuthStatus } from './common.js';
import { generateFeaturesFromSpec } from './generate-features-from-spec.js'; import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform'; import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
@@ -93,102 +100,181 @@ ${getStructuredSpecPromptInstruction()}`;
'[SpecRegeneration]' '[SpecRegeneration]'
); );
const options = createSpecGenerationOptions({ // Get model from phase settings
cwd: projectPath, const settings = await settingsService?.getGlobalSettings();
abortController, const phaseModelEntry =
autoLoadClaudeMd, settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
outputFormat: { const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
type: 'json_schema',
schema: specOutputSchema,
},
});
logger.debug('SDK Options:', JSON.stringify(options, null, 2)); logger.info('Using model:', model);
logger.info('Calling Claude Agent SDK query()...');
// Log auth status right before the SDK call
logAuthStatus('Right before SDK query()');
let stream;
try {
stream = query({ prompt, options });
logger.debug('query() returned stream successfully');
} catch (queryError) {
logger.error('❌ query() threw an exception:');
logger.error('Error:', queryError);
throw queryError;
}
let responseText = ''; let responseText = '';
let messageCount = 0; let messageCount = 0;
let structuredOutput: SpecOutput | null = null; let structuredOutput: SpecOutput | null = null;
logger.info('Starting to iterate over stream...'); // Route to appropriate provider based on model type
if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
logger.info('[SpecGeneration] Using Cursor provider');
try { const provider = ProviderFactory.getProviderForModel(model);
for await (const msg of stream) {
// For Cursor, include the JSON schema in the prompt with clear instructions
// to return JSON in the response (not write to a file)
const cursorPrompt = `${prompt}
CRITICAL INSTRUCTIONS:
1. DO NOT write any files. DO NOT create any files like "project_specification.json".
2. After analyzing the project, respond with ONLY a JSON object - no explanations, no markdown, just raw JSON.
3. The JSON must match this exact schema:
${JSON.stringify(specOutputSchema, null, 2)}
Your entire response should be valid JSON starting with { and ending with }. No text before or after.`;
for await (const msg of provider.executeQuery({
prompt: cursorPrompt,
model,
cwd: projectPath,
maxTurns: 250,
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
readOnly: true, // Spec generation only reads code, we write the spec ourselves
})) {
messageCount++; messageCount++;
logger.info(
`Stream message #${messageCount}: type=${msg.type}, subtype=${(msg as any).subtype}`
);
if (msg.type === 'assistant') { if (msg.type === 'assistant' && msg.message?.content) {
const msgAny = msg as any; for (const block of msg.message.content) {
if (msgAny.message?.content) { if (block.type === 'text' && block.text) {
for (const block of msgAny.message.content) { responseText += block.text;
if (block.type === 'text') { logger.info(
responseText += block.text; `Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
logger.info( );
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars` events.emit('spec-regeneration:event', {
); type: 'spec_regeneration_progress',
events.emit('spec-regeneration:event', { content: block.text,
type: 'spec_regeneration_progress', projectPath: projectPath,
content: block.text, });
projectPath: projectPath, } else if (block.type === 'tool_use') {
}); logger.info('Tool use:', block.name);
} else if (block.type === 'tool_use') { events.emit('spec-regeneration:event', {
logger.info('Tool use:', block.name); type: 'spec_tool',
events.emit('spec-regeneration:event', { tool: block.name,
type: 'spec_tool', input: block.input,
tool: block.name, });
input: block.input,
});
}
} }
} }
} else if (msg.type === 'result' && (msg as any).subtype === 'success') { } else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
logger.info('Received success result'); // Use result if it's a final accumulated message
// Check for structured output - this is the reliable way to get spec data if (msg.result.length > responseText.length) {
const resultMsg = msg as any; responseText = msg.result;
if (resultMsg.structured_output) {
structuredOutput = resultMsg.structured_output as SpecOutput;
logger.info('✅ Received structured output');
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
} else {
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
} }
} else if (msg.type === 'result') {
// Handle error result types
const subtype = (msg as any).subtype;
logger.info(`Result message: subtype=${subtype}`);
if (subtype === 'error_max_turns') {
logger.error('❌ Hit max turns limit!');
} else if (subtype === 'error_max_structured_output_retries') {
logger.error('❌ Failed to produce valid structured output after retries');
throw new Error('Could not produce valid spec output');
}
} else if ((msg as { type: string }).type === 'error') {
logger.error('❌ Received error message from stream:');
logger.error('Error message:', JSON.stringify(msg, null, 2));
} else if (msg.type === 'user') {
// Log user messages (tool results)
logger.info(`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`);
} }
} }
} catch (streamError) {
logger.error('❌ Error while iterating stream:'); // Parse JSON from the response text using shared utility
logger.error('Stream error:', streamError); if (responseText) {
throw streamError; structuredOutput = extractJson<SpecOutput>(responseText, { logger });
}
} else {
// Use Claude SDK for Claude models
logger.info('[SpecGeneration] Using Claude SDK');
const options = createSpecGenerationOptions({
cwd: projectPath,
abortController,
autoLoadClaudeMd,
model,
thinkingLevel, // Pass thinking level for extended thinking
outputFormat: {
type: 'json_schema',
schema: specOutputSchema,
},
});
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
logger.info('Calling Claude Agent SDK query()...');
// Log auth status right before the SDK call
logAuthStatus('Right before SDK query()');
let stream;
try {
stream = query({ prompt, options });
logger.debug('query() returned stream successfully');
} catch (queryError) {
logger.error('❌ query() threw an exception:');
logger.error('Error:', queryError);
throw queryError;
}
logger.info('Starting to iterate over stream...');
try {
for await (const msg of stream) {
messageCount++;
logger.info(
`Stream message #${messageCount}: type=${msg.type}, subtype=${(msg as any).subtype}`
);
if (msg.type === 'assistant') {
const msgAny = msg as any;
if (msgAny.message?.content) {
for (const block of msgAny.message.content) {
if (block.type === 'text') {
responseText += block.text;
logger.info(
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: block.text,
projectPath: projectPath,
});
} else if (block.type === 'tool_use') {
logger.info('Tool use:', block.name);
events.emit('spec-regeneration:event', {
type: 'spec_tool',
tool: block.name,
input: block.input,
});
}
}
}
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
logger.info('Received success result');
// Check for structured output - this is the reliable way to get spec data
const resultMsg = msg as any;
if (resultMsg.structured_output) {
structuredOutput = resultMsg.structured_output as SpecOutput;
logger.info('✅ Received structured output');
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
} else {
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
}
} else if (msg.type === 'result') {
// Handle error result types
const subtype = (msg as any).subtype;
logger.info(`Result message: subtype=${subtype}`);
if (subtype === 'error_max_turns') {
logger.error('❌ Hit max turns limit!');
} else if (subtype === 'error_max_structured_output_retries') {
logger.error('❌ Failed to produce valid structured output after retries');
throw new Error('Could not produce valid spec output');
}
} else if ((msg as { type: string }).type === 'error') {
logger.error('❌ Received error message from stream:');
logger.error('Error message:', JSON.stringify(msg, null, 2));
} else if (msg.type === 'user') {
// Log user messages (tool results)
logger.info(`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`);
}
}
} catch (streamError) {
logger.error('❌ Error while iterating stream:');
logger.error('Stream error:', streamError);
throw streamError;
}
} }
logger.info(`Stream iteration complete. Total messages: ${messageCount}`); logger.info(`Stream iteration complete. Total messages: ${messageCount}`);

View File

@@ -7,6 +7,7 @@ import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js'; import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { getFeaturesDir } from '@automaker/platform'; import { getFeaturesDir } from '@automaker/platform';
import { extractJsonWithArray } from '../../lib/json-extractor.js';
const logger = createLogger('SpecRegeneration'); const logger = createLogger('SpecRegeneration');
@@ -22,23 +23,30 @@ export async function parseAndCreateFeatures(
logger.info('========== END CONTENT =========='); logger.info('========== END CONTENT ==========');
try { try {
// Extract JSON from response // Extract JSON from response using shared utility
logger.info('Extracting JSON from response...'); logger.info('Extracting JSON from response using extractJsonWithArray...');
logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`);
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/); interface FeaturesResponse {
if (!jsonMatch) { features: Array<{
logger.error('❌ No valid JSON found in response'); id: string;
category?: string;
title: string;
description: string;
priority?: number;
complexity?: string;
dependencies?: string[];
}>;
}
const parsed = extractJsonWithArray<FeaturesResponse>(content, 'features', { logger });
if (!parsed || !parsed.features) {
logger.error('❌ No valid JSON with "features" array found in response');
logger.error('Full content received:'); logger.error('Full content received:');
logger.error(content); logger.error(content);
throw new Error('No valid JSON found in response'); throw new Error('No valid JSON found in response');
} }
logger.info(`JSON match found (${jsonMatch[0].length} chars)`);
logger.info('========== MATCHED JSON ==========');
logger.info(jsonMatch[0]);
logger.info('========== END MATCHED JSON ==========');
const parsed = JSON.parse(jsonMatch[0]);
logger.info(`Parsed ${parsed.features?.length || 0} features`); logger.info(`Parsed ${parsed.features?.length || 0} features`);
logger.info('Parsed features:', JSON.stringify(parsed.features, null, 2)); logger.info('Parsed features:', JSON.stringify(parsed.features, null, 2));

View File

@@ -0,0 +1,247 @@
/**
* Auth routes - Login, logout, and status endpoints
*
* Security model:
* - Web mode: User enters API key (shown on server console) to get HTTP-only session cookie
* - Electron mode: Uses X-API-Key header (handled automatically via IPC)
*
* The session cookie is:
* - HTTP-only: JavaScript cannot read it (protects against XSS)
* - SameSite=Strict: Only sent for same-site requests (protects against CSRF)
*
* Mounted at /api/auth in the main server (BEFORE auth middleware).
*/
import { Router } from 'express';
import type { Request } from 'express';
import {
validateApiKey,
createSession,
invalidateSession,
getSessionCookieOptions,
getSessionCookieName,
isRequestAuthenticated,
createWsConnectionToken,
} from '../../lib/auth.js';
// Rate limiting configuration
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute window
const RATE_LIMIT_MAX_ATTEMPTS = 5; // Max 5 attempts per window
// Check if we're in test mode - disable rate limiting for E2E tests
const isTestMode = process.env.AUTOMAKER_MOCK_AGENT === 'true';
// In-memory rate limit tracking (resets on server restart)
const loginAttempts = new Map<string, { count: number; windowStart: number }>();
// Clean up old rate limit entries periodically (every 5 minutes)
setInterval(
() => {
const now = Date.now();
loginAttempts.forEach((data, ip) => {
if (now - data.windowStart > RATE_LIMIT_WINDOW_MS * 2) {
loginAttempts.delete(ip);
}
});
},
5 * 60 * 1000
);
/**
* Get client IP address from request
* Handles X-Forwarded-For header for reverse proxy setups
*/
function getClientIp(req: Request): string {
const forwarded = req.headers['x-forwarded-for'];
if (forwarded) {
// X-Forwarded-For can be a comma-separated list; take the first (original client)
const forwardedIp = Array.isArray(forwarded) ? forwarded[0] : forwarded.split(',')[0];
return forwardedIp.trim();
}
return req.ip || req.socket.remoteAddress || 'unknown';
}
/**
* Check if an IP is rate limited
* Returns { limited: boolean, retryAfter?: number }
*/
function checkRateLimit(ip: string): { limited: boolean; retryAfter?: number } {
const now = Date.now();
const attempt = loginAttempts.get(ip);
if (!attempt) {
return { limited: false };
}
// Check if window has expired
if (now - attempt.windowStart > RATE_LIMIT_WINDOW_MS) {
loginAttempts.delete(ip);
return { limited: false };
}
// Check if over limit
if (attempt.count >= RATE_LIMIT_MAX_ATTEMPTS) {
const retryAfter = Math.ceil((RATE_LIMIT_WINDOW_MS - (now - attempt.windowStart)) / 1000);
return { limited: true, retryAfter };
}
return { limited: false };
}
/**
* Record a login attempt for rate limiting
*/
function recordLoginAttempt(ip: string): void {
const now = Date.now();
const attempt = loginAttempts.get(ip);
if (!attempt || now - attempt.windowStart > RATE_LIMIT_WINDOW_MS) {
// Start new window
loginAttempts.set(ip, { count: 1, windowStart: now });
} else {
// Increment existing window
attempt.count++;
}
}
/**
* Create auth routes
*
* @returns Express Router with auth endpoints
*/
export function createAuthRoutes(): Router {
const router = Router();
/**
* GET /api/auth/status
*
* Returns whether the current request is authenticated.
* Used by the UI to determine if login is needed.
*/
router.get('/status', (req, res) => {
const authenticated = isRequestAuthenticated(req);
res.json({
success: true,
authenticated,
required: true,
});
});
/**
* POST /api/auth/login
*
* Validates the API key and sets a session cookie.
* Body: { apiKey: string }
*
* Rate limited to 5 attempts per minute per IP to prevent brute force attacks.
*/
router.post('/login', async (req, res) => {
const clientIp = getClientIp(req);
// Skip rate limiting in test mode to allow parallel E2E tests
if (!isTestMode) {
// Check rate limit before processing
const rateLimit = checkRateLimit(clientIp);
if (rateLimit.limited) {
res.status(429).json({
success: false,
error: 'Too many login attempts. Please try again later.',
retryAfter: rateLimit.retryAfter,
});
return;
}
}
const { apiKey } = req.body as { apiKey?: string };
if (!apiKey) {
res.status(400).json({
success: false,
error: 'API key is required.',
});
return;
}
// Record this attempt (only for actual API key validation attempts, skip in test mode)
if (!isTestMode) {
recordLoginAttempt(clientIp);
}
if (!validateApiKey(apiKey)) {
res.status(401).json({
success: false,
error: 'Invalid API key.',
});
return;
}
// Create session and set cookie
const sessionToken = await createSession();
const cookieOptions = getSessionCookieOptions();
const cookieName = getSessionCookieName();
res.cookie(cookieName, sessionToken, cookieOptions);
res.json({
success: true,
message: 'Logged in successfully.',
// Return token for explicit header-based auth (works around cross-origin cookie issues)
token: sessionToken,
});
});
/**
* GET /api/auth/token
*
* Generates a short-lived WebSocket connection token if the user has a valid session.
* This token is used for initial WebSocket handshake authentication and expires in 5 minutes.
* The token is NOT the session cookie value - it's a separate, short-lived token.
*/
router.get('/token', (req, res) => {
// Validate the session is still valid (via cookie, API key, or session token header)
if (!isRequestAuthenticated(req)) {
res.status(401).json({
success: false,
error: 'Authentication required.',
});
return;
}
// Generate a new short-lived WebSocket connection token
const wsToken = createWsConnectionToken();
res.json({
success: true,
token: wsToken,
expiresIn: 300, // 5 minutes in seconds
});
});
/**
* POST /api/auth/logout
*
* Clears the session cookie and invalidates the session.
*/
router.post('/logout', async (req, res) => {
const cookieName = getSessionCookieName();
const sessionToken = req.cookies?.[cookieName] as string | undefined;
if (sessionToken) {
await invalidateSession(sessionToken);
}
// Clear the cookie
res.clearCookie(cookieName, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
});
res.json({
success: true,
message: 'Logged out successfully.',
});
});
return router;
}

View File

@@ -31,7 +31,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
autoModeService autoModeService
.resumeFeature(projectPath, featureId, useWorktrees ?? false) .resumeFeature(projectPath, featureId, useWorktrees ?? false)
.catch((error) => { .catch((error) => {
logger.error(`[AutoMode] Resume feature ${featureId} error:`, error); logger.error(`Resume feature ${featureId} error:`, error);
}); });
res.json({ success: true }); res.json({ success: true });

View File

@@ -31,7 +31,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
autoModeService autoModeService
.executeFeature(projectPath, featureId, useWorktrees ?? false, false) .executeFeature(projectPath, featureId, useWorktrees ?? false, false)
.catch((error) => { .catch((error) => {
logger.error(`[AutoMode] Feature ${featureId} error:`, error); logger.error(`Feature ${featureId} error:`, error);
}) })
.finally(() => { .finally(() => {
// Release the starting slot when execution completes (success or error) // Release the starting slot when execution completes (success or error)

View File

@@ -1,14 +1,20 @@
/** /**
* Generate backlog plan using Claude AI * Generate backlog plan using Claude AI
*
* Model is configurable via phaseModels.backlogPlanningModel in settings
* (defaults to Sonnet). Can be overridden per-call via model parameter.
*/ */
import type { EventEmitter } from '../../lib/events.js'; import type { EventEmitter } from '../../lib/events.js';
import type { Feature, BacklogPlanResult, BacklogChange, DependencyUpdate } from '@automaker/types'; import type { Feature, BacklogPlanResult, BacklogChange, DependencyUpdate } from '@automaker/types';
import { DEFAULT_PHASE_MODELS, isCursorModel, type ThinkingLevel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { FeatureLoader } from '../../services/feature-loader.js'; import { FeatureLoader } from '../../services/feature-loader.js';
import { ProviderFactory } from '../../providers/provider-factory.js'; import { ProviderFactory } from '../../providers/provider-factory.js';
import { extractJsonWithArray } from '../../lib/json-extractor.js';
import { logger, setRunningState, getErrorMessage } from './common.js'; import { logger, setRunningState, getErrorMessage } from './common.js';
import type { SettingsService } from '../../services/settings-service.js'; import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js'; import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
const featureLoader = new FeatureLoader(); const featureLoader = new FeatureLoader();
@@ -39,24 +45,28 @@ function formatFeaturesForPrompt(features: Feature[]): string {
* Parse the AI response into a BacklogPlanResult * Parse the AI response into a BacklogPlanResult
*/ */
function parsePlanResponse(response: string): BacklogPlanResult { function parsePlanResponse(response: string): BacklogPlanResult {
try { // Use shared JSON extraction utility for robust parsing
// Try to extract JSON from the response // extractJsonWithArray validates that 'changes' exists AND is an array
const jsonMatch = response.match(/```json\n?([\s\S]*?)\n?```/); const parsed = extractJsonWithArray<BacklogPlanResult>(response, 'changes', {
if (jsonMatch) { logger,
return JSON.parse(jsonMatch[1]); });
}
// Try to parse the whole response as JSON if (parsed) {
return JSON.parse(response); return parsed;
} catch {
// If parsing fails, return an empty result
logger.warn('[BacklogPlan] Failed to parse AI response as JSON');
return {
changes: [],
summary: 'Failed to parse AI response',
dependencyUpdates: [],
};
} }
// If parsing fails, log details and return an empty result
logger.warn('[BacklogPlan] Failed to parse AI response as JSON');
logger.warn('[BacklogPlan] Response text length:', response.length);
logger.warn('[BacklogPlan] Response preview:', response.slice(0, 500));
if (response.length === 0) {
logger.error('[BacklogPlan] Response text is EMPTY! No content was extracted from stream.');
}
return {
changes: [],
summary: 'Failed to parse AI response',
dependencyUpdates: [],
};
} }
/** /**
@@ -79,80 +89,36 @@ export async function generateBacklogPlan(
content: `Loaded ${features.length} features from backlog`, content: `Loaded ${features.length} features from backlog`,
}); });
// Load prompts from settings
const prompts = await getPromptCustomization(settingsService, '[BacklogPlan]');
// Build the system prompt // Build the system prompt
const systemPrompt = `You are an AI assistant helping to modify a software project's feature backlog. const systemPrompt = prompts.backlogPlan.systemPrompt;
You will be given the current list of features and a user request to modify the backlog.
IMPORTANT CONTEXT (automatically injected): // Build the user prompt from template
- Remember to update the dependency graph if deleting existing features const currentFeatures = formatFeaturesForPrompt(features);
- Remember to define dependencies on new features hooked into relevant existing ones const userPrompt = prompts.backlogPlan.userPromptTemplate
- Maintain dependency graph integrity (no orphaned dependencies) .replace('{{currentFeatures}}', currentFeatures)
- When deleting a feature, identify which other features depend on it .replace('{{userRequest}}', prompt);
Your task is to analyze the request and produce a structured JSON plan with:
1. Features to ADD (include title, description, category, and dependencies)
2. Features to UPDATE (specify featureId and the updates)
3. Features to DELETE (specify featureId)
4. A summary of the changes
5. Any dependency updates needed (removed dependencies due to deletions, new dependencies for new features)
Respond with ONLY a JSON object in this exact format:
\`\`\`json
{
"changes": [
{
"type": "add",
"feature": {
"title": "Feature title",
"description": "Feature description",
"category": "Category name",
"dependencies": ["existing-feature-id"],
"priority": 1
},
"reason": "Why this feature should be added"
},
{
"type": "update",
"featureId": "existing-feature-id",
"feature": {
"title": "Updated title"
},
"reason": "Why this feature should be updated"
},
{
"type": "delete",
"featureId": "feature-id-to-delete",
"reason": "Why this feature should be deleted"
}
],
"summary": "Brief overview of all proposed changes",
"dependencyUpdates": [
{
"featureId": "feature-that-depended-on-deleted",
"removedDependencies": ["deleted-feature-id"],
"addedDependencies": []
}
]
}
\`\`\``;
// Build the user prompt
const userPrompt = `Current Features in Backlog:
${formatFeaturesForPrompt(features)}
---
User Request: ${prompt}
Please analyze the current backlog and the user's request, then provide a JSON plan for the modifications.`;
events.emit('backlog-plan:event', { events.emit('backlog-plan:event', {
type: 'backlog_plan_progress', type: 'backlog_plan_progress',
content: 'Generating plan with AI...', content: 'Generating plan with AI...',
}); });
// Get the model to use // Get the model to use from settings or provided override
const effectiveModel = model || 'sonnet'; let effectiveModel = model;
let thinkingLevel: ThinkingLevel | undefined;
if (!effectiveModel) {
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.backlogPlanningModel || DEFAULT_PHASE_MODELS.backlogPlanningModel;
const resolved = resolvePhaseModel(phaseModelEntry);
effectiveModel = resolved.model;
thinkingLevel = resolved.thinkingLevel;
}
logger.info('[BacklogPlan] Using model:', effectiveModel);
const provider = ProviderFactory.getProviderForModel(effectiveModel); const provider = ProviderFactory.getProviderForModel(effectiveModel);
// Get autoLoadClaudeMd setting // Get autoLoadClaudeMd setting
@@ -162,16 +128,38 @@ Please analyze the current backlog and the user's request, then provide a JSON p
'[BacklogPlan]' '[BacklogPlan]'
); );
// For Cursor models, we need to combine prompts with explicit instructions
// because Cursor doesn't support systemPrompt separation like Claude SDK
let finalPrompt = userPrompt;
let finalSystemPrompt: string | undefined = systemPrompt;
if (isCursorModel(effectiveModel)) {
logger.info('[BacklogPlan] Using Cursor model - adding explicit no-file-write instructions');
finalPrompt = `${systemPrompt}
CRITICAL INSTRUCTIONS:
1. DO NOT write any files. Return the JSON in your response only.
2. DO NOT use Write, Edit, or any file modification tools.
3. Respond with ONLY a JSON object - no explanations, no markdown, just raw JSON.
4. Your entire response should be valid JSON starting with { and ending with }.
5. No text before or after the JSON object.
${userPrompt}`;
finalSystemPrompt = undefined; // System prompt is now embedded in the user prompt
}
// Execute the query // Execute the query
const stream = provider.executeQuery({ const stream = provider.executeQuery({
prompt: userPrompt, prompt: finalPrompt,
model: effectiveModel, model: effectiveModel,
cwd: projectPath, cwd: projectPath,
systemPrompt, systemPrompt: finalSystemPrompt,
maxTurns: 1, maxTurns: 1,
allowedTools: [], // No tools needed for this allowedTools: [], // No tools needed for this
abortController, abortController,
settingSources: autoLoadClaudeMd ? ['user', 'project'] : undefined, settingSources: autoLoadClaudeMd ? ['user', 'project'] : undefined,
readOnly: true, // Plan generation only generates text, doesn't write files
thinkingLevel, // Pass thinking level for extended thinking
}); });
let responseText = ''; let responseText = '';
@@ -189,6 +177,16 @@ Please analyze the current backlog and the user's request, then provide a JSON p
} }
} }
} }
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
// Use result if it's a final accumulated message (from Cursor provider)
logger.info('[BacklogPlan] Received result from Cursor, length:', msg.result.length);
logger.info('[BacklogPlan] Previous responseText length:', responseText.length);
if (msg.result.length > responseText.length) {
logger.info('[BacklogPlan] Using Cursor result (longer than accumulated text)');
responseText = msg.result;
} else {
logger.info('[BacklogPlan] Keeping accumulated text (longer than Cursor result)');
}
} }
} }

View File

@@ -1,5 +1,8 @@
import { Router, Request, Response } from 'express'; import { Router, Request, Response } from 'express';
import { ClaudeUsageService } from '../../services/claude-usage-service.js'; import { ClaudeUsageService } from '../../services/claude-usage-service.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('Claude');
export function createClaudeRoutes(service: ClaudeUsageService): Router { export function createClaudeRoutes(service: ClaudeUsageService): Router {
const router = Router(); const router = Router();
@@ -33,7 +36,7 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
message: 'The Claude CLI took too long to respond', message: 'The Claude CLI took too long to respond',
}); });
} else { } else {
console.error('Error fetching usage:', error); logger.error('Error fetching usage:', error);
res.status(500).json({ error: message }); res.status(500).json({ error: message });
} }
} }

View File

@@ -1,8 +1,9 @@
/** /**
* POST /context/describe-file endpoint - Generate description for a text file * POST /context/describe-file endpoint - Generate description for a text file
* *
* Uses Claude Haiku to analyze a text file and generate a concise description * Uses AI to analyze a text file and generate a concise description
* suitable for context file metadata. * suitable for context file metadata. Model is configurable via
* phaseModels.fileDescriptionModel in settings (defaults to Haiku).
* *
* SECURITY: This endpoint validates file paths against ALLOWED_ROOT_DIRECTORY * SECURITY: This endpoint validates file paths against ALLOWED_ROOT_DIRECTORY
* and reads file content directly (not via Claude's Read tool) to prevent * and reads file content directly (not via Claude's Read tool) to prevent
@@ -12,9 +13,11 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from '@automaker/types'; import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { PathNotAllowedError } from '@automaker/platform'; import { PathNotAllowedError } from '@automaker/platform';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js'; import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import * as secureFs from '../../../lib/secure-fs.js'; import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path'; import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
@@ -94,7 +97,7 @@ export function createDescribeFileHandler(
return; return;
} }
logger.info(`[DescribeFile] Starting description generation for: ${filePath}`); logger.info(`Starting description generation for: ${filePath}`);
// Resolve the path for logging and cwd derivation // Resolve the path for logging and cwd derivation
const resolvedPath = secureFs.resolvePath(filePath); const resolvedPath = secureFs.resolvePath(filePath);
@@ -109,7 +112,7 @@ export function createDescribeFileHandler(
} catch (readError) { } catch (readError) {
// Path not allowed - return 403 Forbidden // Path not allowed - return 403 Forbidden
if (readError instanceof PathNotAllowedError) { if (readError instanceof PathNotAllowedError) {
logger.warn(`[DescribeFile] Path not allowed: ${filePath}`); logger.warn(`Path not allowed: ${filePath}`);
const response: DescribeFileErrorResponse = { const response: DescribeFileErrorResponse = {
success: false, success: false,
error: 'File path is not within the allowed directory', error: 'File path is not within the allowed directory',
@@ -125,7 +128,7 @@ export function createDescribeFileHandler(
'code' in readError && 'code' in readError &&
readError.code === 'ENOENT' readError.code === 'ENOENT'
) { ) {
logger.warn(`[DescribeFile] File not found: ${resolvedPath}`); logger.warn(`File not found: ${resolvedPath}`);
const response: DescribeFileErrorResponse = { const response: DescribeFileErrorResponse = {
success: false, success: false,
error: `File not found: ${filePath}`, error: `File not found: ${filePath}`,
@@ -135,7 +138,7 @@ export function createDescribeFileHandler(
} }
const errorMessage = readError instanceof Error ? readError.message : 'Unknown error'; const errorMessage = readError instanceof Error ? readError.message : 'Unknown error';
logger.error(`[DescribeFile] Failed to read file: ${errorMessage}`); logger.error(`Failed to read file: ${errorMessage}`);
const response: DescribeFileErrorResponse = { const response: DescribeFileErrorResponse = {
success: false, success: false,
error: `Failed to read file: ${errorMessage}`, error: `Failed to read file: ${errorMessage}`,
@@ -177,30 +180,76 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
'[DescribeFile]' '[DescribeFile]'
); );
// Use centralized SDK options with proper cwd validation // Get model from phase settings
// No tools needed since we're passing file content directly const settings = await settingsService?.getGlobalSettings();
const sdkOptions = createCustomOptions({ logger.info(`Raw phaseModels from settings:`, JSON.stringify(settings?.phaseModels, null, 2));
cwd, const phaseModelEntry =
model: CLAUDE_MODEL_MAP.haiku, settings?.phaseModels?.fileDescriptionModel || DEFAULT_PHASE_MODELS.fileDescriptionModel;
maxTurns: 1, logger.info(`fileDescriptionModel entry:`, JSON.stringify(phaseModelEntry));
allowedTools: [], const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
});
const promptGenerator = (async function* () { logger.info(`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`);
yield {
type: 'user' as const,
session_id: '',
message: { role: 'user' as const, content: promptContent },
parent_tool_use_id: null,
};
})();
const stream = query({ prompt: promptGenerator, options: sdkOptions }); let description: string;
// Extract the description from the response // Route to appropriate provider based on model type
const description = await extractTextFromStream(stream); if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
logger.info(`Using Cursor provider for model: ${model}`);
const provider = ProviderFactory.getProviderForModel(model);
// Build a simple text prompt for Cursor (no multi-part content blocks)
const cursorPrompt = `${instructionText}\n\n--- FILE CONTENT ---\n${contentToAnalyze}`;
let responseText = '';
for await (const msg of provider.executeQuery({
prompt: cursorPrompt,
model,
cwd,
maxTurns: 1,
allowedTools: [],
readOnly: true, // File description only reads, doesn't write
})) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
}
}
description = responseText;
} else {
// Use Claude SDK for Claude models
logger.info(`Using Claude SDK for model: ${model}`);
// Use centralized SDK options with proper cwd validation
// No tools needed since we're passing file content directly
const sdkOptions = createCustomOptions({
cwd,
model,
maxTurns: 1,
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
thinkingLevel, // Pass thinking level for extended thinking
});
const promptGenerator = (async function* () {
yield {
type: 'user' as const,
session_id: '',
message: { role: 'user' as const, content: promptContent },
parent_tool_use_id: null,
};
})();
const stream = query({ prompt: promptGenerator, options: sdkOptions });
// Extract the description from the response
description = await extractTextFromStream(stream);
}
if (!description || description.trim().length === 0) { if (!description || description.trim().length === 0) {
logger.warn('Received empty response from Claude'); logger.warn('Received empty response from Claude');

View File

@@ -1,8 +1,9 @@
/** /**
* POST /context/describe-image endpoint - Generate description for an image * POST /context/describe-image endpoint - Generate description for an image
* *
* Uses Claude Haiku to analyze an image and generate a concise description * Uses AI to analyze an image and generate a concise description
* suitable for context file metadata. * suitable for context file metadata. Model is configurable via
* phaseModels.imageDescriptionModel in settings (defaults to Haiku).
* *
* IMPORTANT: * IMPORTANT:
* The agent runner (chat/auto-mode) sends images as multi-part content blocks (base64 image blocks), * The agent runner (chat/auto-mode) sends images as multi-part content blocks (base64 image blocks),
@@ -13,9 +14,11 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger, readImageAsBase64 } from '@automaker/utils'; import { createLogger, readImageAsBase64 } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from '@automaker/types'; import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js'; import { createCustomOptions } from '../../../lib/sdk-options.js';
import * as fs from 'fs'; import { ProviderFactory } from '../../../providers/provider-factory.js';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path'; import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
@@ -57,13 +60,13 @@ function filterSafeHeaders(headers: Record<string, unknown>): Record<string, unk
*/ */
function findActualFilePath(requestedPath: string): string | null { function findActualFilePath(requestedPath: string): string | null {
// First, try the exact path // First, try the exact path
if (fs.existsSync(requestedPath)) { if (secureFs.existsSync(requestedPath)) {
return requestedPath; return requestedPath;
} }
// Try with Unicode normalization // Try with Unicode normalization
const normalizedPath = requestedPath.normalize('NFC'); const normalizedPath = requestedPath.normalize('NFC');
if (fs.existsSync(normalizedPath)) { if (secureFs.existsSync(normalizedPath)) {
return normalizedPath; return normalizedPath;
} }
@@ -72,12 +75,12 @@ function findActualFilePath(requestedPath: string): string | null {
const dir = path.dirname(requestedPath); const dir = path.dirname(requestedPath);
const baseName = path.basename(requestedPath); const baseName = path.basename(requestedPath);
if (!fs.existsSync(dir)) { if (!secureFs.existsSync(dir)) {
return null; return null;
} }
try { try {
const files = fs.readdirSync(dir); const files = secureFs.readdirSync(dir);
// Normalize the requested basename for comparison // Normalize the requested basename for comparison
// Replace various space-like characters with regular space for comparison // Replace various space-like characters with regular space for comparison
@@ -281,9 +284,9 @@ export function createDescribeImageHandler(
} }
// Log path + stats (this is often where issues start: missing file, perms, size) // Log path + stats (this is often where issues start: missing file, perms, size)
let stat: fs.Stats | null = null; let stat: ReturnType<typeof secureFs.statSync> | null = null;
try { try {
stat = fs.statSync(actualPath); stat = secureFs.statSync(actualPath);
logger.info( logger.info(
`[${requestId}] fileStats size=${stat.size} bytes mtime=${stat.mtime.toISOString()}` `[${requestId}] fileStats size=${stat.size} bytes mtime=${stat.mtime.toISOString()}`
); );
@@ -337,40 +340,89 @@ export function createDescribeImageHandler(
'[DescribeImage]' '[DescribeImage]'
); );
// Use the same centralized option builder used across the server (validates cwd) // Get model from phase settings
const sdkOptions = createCustomOptions({ const settings = await settingsService?.getGlobalSettings();
cwd, const phaseModelEntry =
model: CLAUDE_MODEL_MAP.haiku, settings?.phaseModels?.imageDescriptionModel || DEFAULT_PHASE_MODELS.imageDescriptionModel;
maxTurns: 1, const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
});
logger.info( logger.info(`[${requestId}] Using model: ${model}`);
`[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify(
sdkOptions.allowedTools
)} sandbox=${JSON.stringify(sdkOptions.sandbox)}`
);
const promptGenerator = (async function* () { let description: string;
yield {
type: 'user' as const,
session_id: '',
message: { role: 'user' as const, content: promptContent },
parent_tool_use_id: null,
};
})();
logger.info(`[${requestId}] Calling query()...`); // Route to appropriate provider based on model type
const queryStart = Date.now(); if (isCursorModel(model)) {
const stream = query({ prompt: promptGenerator, options: sdkOptions }); // Use Cursor provider for Cursor models
logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`); // Note: Cursor may have limited support for image content blocks
logger.info(`[${requestId}] Using Cursor provider for model: ${model}`);
// Extract the description from the response const provider = ProviderFactory.getProviderForModel(model);
const extractStart = Date.now();
const description = await extractTextFromStream(stream, requestId); // Build prompt with image reference for Cursor
logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`); // Note: Cursor CLI may not support base64 image blocks directly,
// so we include the image path as context
const cursorPrompt = `${instructionText}\n\nImage file: ${actualPath}\nMIME type: ${imageData.mimeType}`;
let responseText = '';
const queryStart = Date.now();
for await (const msg of provider.executeQuery({
prompt: cursorPrompt,
model,
cwd,
maxTurns: 1,
allowedTools: ['Read'], // Allow Read tool so Cursor can read the image if needed
readOnly: true, // Image description only reads, doesn't write
})) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
}
}
logger.info(`[${requestId}] Cursor query completed in ${Date.now() - queryStart}ms`);
description = responseText;
} else {
// Use Claude SDK for Claude models (supports image content blocks)
logger.info(`[${requestId}] Using Claude SDK for model: ${model}`);
// Use the same centralized option builder used across the server (validates cwd)
const sdkOptions = createCustomOptions({
cwd,
model,
maxTurns: 1,
allowedTools: [],
autoLoadClaudeMd,
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
thinkingLevel, // Pass thinking level for extended thinking
});
logger.info(
`[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify(
sdkOptions.allowedTools
)} sandbox=${JSON.stringify(sdkOptions.sandbox)}`
);
const promptGenerator = (async function* () {
yield {
type: 'user' as const,
session_id: '',
message: { role: 'user' as const, content: promptContent },
parent_tool_use_id: null,
};
})();
logger.info(`[${requestId}] Calling query()...`);
const queryStart = Date.now();
const stream = query({ prompt: promptGenerator, options: sdkOptions });
logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`);
// Extract the description from the response
const extractStart = Date.now();
description = await extractTextFromStream(stream, requestId);
logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`);
}
if (!description || description.trim().length === 0) { if (!description || description.trim().length === 0) {
logger.warn(`[${requestId}] Received empty response from Claude`); logger.warn(`[${requestId}] Received empty response from Claude`);

View File

@@ -6,17 +6,19 @@
*/ */
import { Router } from 'express'; import { Router } from 'express';
import type { SettingsService } from '../../services/settings-service.js';
import { createEnhanceHandler } from './routes/enhance.js'; import { createEnhanceHandler } from './routes/enhance.js';
/** /**
* Create the enhance-prompt router * Create the enhance-prompt router
* *
* @param settingsService - Settings service for loading custom prompts
* @returns Express router with enhance-prompt endpoints * @returns Express router with enhance-prompt endpoints
*/ */
export function createEnhancePromptRoutes(): Router { export function createEnhancePromptRoutes(settingsService?: SettingsService): Router {
const router = Router(); const router = Router();
router.post('/', createEnhanceHandler()); router.post('/', createEnhanceHandler(settingsService));
return router; return router;
} }

View File

@@ -1,7 +1,7 @@
/** /**
* POST /enhance-prompt endpoint - Enhance user input text * POST /enhance-prompt endpoint - Enhance user input text
* *
* Uses Claude AI to enhance text based on the specified enhancement mode. * Uses Claude AI or Cursor to enhance text based on the specified enhancement mode.
* Supports modes: improve, technical, simplify, acceptance * Supports modes: improve, technical, simplify, acceptance
*/ */
@@ -9,9 +9,16 @@ import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils'; import { createLogger } from '@automaker/utils';
import { resolveModelString } from '@automaker/model-resolver'; import { resolveModelString } from '@automaker/model-resolver';
import { CLAUDE_MODEL_MAP } from '@automaker/types';
import { import {
getSystemPrompt, CLAUDE_MODEL_MAP,
isCursorModel,
ThinkingLevel,
getThinkingTokenBudget,
} from '@automaker/types';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import type { SettingsService } from '../../../services/settings-service.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
import {
buildUserPrompt, buildUserPrompt,
isValidEnhancementMode, isValidEnhancementMode,
type EnhancementMode, type EnhancementMode,
@@ -29,6 +36,8 @@ interface EnhanceRequestBody {
enhancementMode: string; enhancementMode: string;
/** Optional model override */ /** Optional model override */
model?: string; model?: string;
/** Optional thinking level for Claude models (ignored for Cursor models) */
thinkingLevel?: ThinkingLevel;
} }
/** /**
@@ -80,15 +89,54 @@ async function extractTextFromStream(
return responseText; return responseText;
} }
/**
* Execute enhancement using Cursor provider
*
* @param prompt - The enhancement prompt
* @param model - The Cursor model to use
* @returns The enhanced text
*/
async function executeWithCursor(prompt: string, model: string): Promise<string> {
const provider = ProviderFactory.getProviderForModel(model);
let responseText = '';
for await (const msg of provider.executeQuery({
prompt,
model,
cwd: process.cwd(), // Enhancement doesn't need a specific working directory
readOnly: true, // Prompt enhancement only generates text, doesn't write files
})) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
// Use result if it's a final accumulated message
if (msg.result.length > responseText.length) {
responseText = msg.result;
}
}
}
return responseText;
}
/** /**
* Create the enhance request handler * Create the enhance request handler
* *
* @param settingsService - Optional settings service for loading custom prompts
* @returns Express request handler for text enhancement * @returns Express request handler for text enhancement
*/ */
export function createEnhanceHandler(): (req: Request, res: Response) => Promise<void> { export function createEnhanceHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => { return async (req: Request, res: Response): Promise<void> => {
try { try {
const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody; const { originalText, enhancementMode, model, thinkingLevel } =
req.body as EnhanceRequestBody;
// Validate required fields // Validate required fields
if (!originalText || typeof originalText !== 'string') { if (!originalText || typeof originalText !== 'string') {
@@ -128,8 +176,19 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise
logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`); logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`);
// Get the system prompt for this mode // Load enhancement prompts from settings (merges custom + defaults)
const systemPrompt = getSystemPrompt(validMode); const prompts = await getPromptCustomization(settingsService, '[EnhancePrompt]');
// Get the system prompt for this mode from merged prompts
const systemPromptMap: Record<EnhancementMode, string> = {
improve: prompts.enhancement.improveSystemPrompt,
technical: prompts.enhancement.technicalSystemPrompt,
simplify: prompts.enhancement.simplifySystemPrompt,
acceptance: prompts.enhancement.acceptanceSystemPrompt,
};
const systemPrompt = systemPromptMap[validMode];
logger.debug(`Using ${validMode} system prompt (length: ${systemPrompt.length} chars)`);
// Build the user prompt with few-shot examples // Build the user prompt with few-shot examples
// This helps the model understand this is text transformation, not a coding task // This helps the model understand this is text transformation, not a coding task
@@ -140,24 +199,43 @@ export function createEnhanceHandler(): (req: Request, res: Response) => Promise
logger.debug(`Using model: ${resolvedModel}`); logger.debug(`Using model: ${resolvedModel}`);
// Call Claude SDK with minimal configuration for text transformation let enhancedText: string;
// Key: no tools, just text completion
const stream = query({ // Route to appropriate provider based on model
prompt: userPrompt, if (isCursorModel(resolvedModel)) {
options: { // Use Cursor provider for Cursor models
logger.info(`Using Cursor provider for model: ${resolvedModel}`);
// Cursor doesn't have a separate system prompt concept, so combine them
const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`;
enhancedText = await executeWithCursor(combinedPrompt, resolvedModel);
} else {
// Use Claude SDK for Claude models
logger.info(`Using Claude provider for model: ${resolvedModel}`);
// Convert thinkingLevel to maxThinkingTokens for SDK
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
const queryOptions: Parameters<typeof query>[0]['options'] = {
model: resolvedModel, model: resolvedModel,
systemPrompt, systemPrompt,
maxTurns: 1, maxTurns: 1,
allowedTools: [], allowedTools: [],
permissionMode: 'acceptEdits', permissionMode: 'acceptEdits',
}, };
}); if (maxThinkingTokens) {
queryOptions.maxThinkingTokens = maxThinkingTokens;
}
// Extract the enhanced text from the response const stream = query({
const enhancedText = await extractTextFromStream(stream); prompt: userPrompt,
options: queryOptions,
});
enhancedText = await extractTextFromStream(stream);
}
if (!enhancedText || enhancedText.trim().length === 0) { if (!enhancedText || enhancedText.trim().length === 0) {
logger.warn('Received empty response from Claude'); logger.warn('Received empty response from AI');
const response: EnhanceErrorResponse = { const response: EnhanceErrorResponse = {
success: false, success: false,
error: 'Failed to generate enhanced text - empty response', error: 'Failed to generate enhanced text - empty response',

View File

@@ -10,7 +10,7 @@ import { createGetHandler } from './routes/get.js';
import { createCreateHandler } from './routes/create.js'; import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from './routes/update.js'; import { createUpdateHandler } from './routes/update.js';
import { createDeleteHandler } from './routes/delete.js'; import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler } from './routes/agent-output.js'; import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from './routes/generate-title.js'; import { createGenerateTitleHandler } from './routes/generate-title.js';
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router { export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
@@ -22,6 +22,7 @@ export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader)); router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader)); router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post('/agent-output', createAgentOutputHandler(featureLoader)); router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post('/raw-output', createRawOutputHandler(featureLoader));
router.post('/generate-title', createGenerateTitleHandler()); router.post('/generate-title', createGenerateTitleHandler());
return router; return router;

View File

@@ -1,5 +1,6 @@
/** /**
* POST /agent-output endpoint - Get agent output for a feature * POST /agent-output endpoint - Get agent output for a feature
* POST /raw-output endpoint - Get raw JSONL output for debugging
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
@@ -30,3 +31,31 @@ export function createAgentOutputHandler(featureLoader: FeatureLoader) {
} }
}; };
} }
/**
* Handler for getting raw JSONL output for debugging
*/
export function createRawOutputHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res.status(400).json({
success: false,
error: 'projectPath and featureId are required',
});
return;
}
const content = await featureLoader.getRawOutput(projectPath, featureId);
res.json({ success: true, content });
} catch (error) {
logError(error, 'Get raw output failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -96,7 +96,7 @@ export function createGenerateTitleHandler(): (req: Request, res: Response) => P
systemPrompt: SYSTEM_PROMPT, systemPrompt: SYSTEM_PROMPT,
maxTurns: 1, maxTurns: 1,
allowedTools: [], allowedTools: [],
permissionMode: 'acceptEdits', permissionMode: 'default',
}, },
}); });

View File

@@ -6,7 +6,7 @@ import type { Request, Response } from 'express';
import * as secureFs from '../../../lib/secure-fs.js'; import * as secureFs from '../../../lib/secure-fs.js';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { getAllowedRootDirectory, PathNotAllowedError } from '@automaker/platform'; import { getAllowedRootDirectory, PathNotAllowedError, isPathAllowed } from '@automaker/platform';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError } from '../common.js';
export function createBrowseHandler() { export function createBrowseHandler() {
@@ -40,9 +40,16 @@ export function createBrowseHandler() {
return drives; return drives;
}; };
// Get parent directory // Get parent directory - only if it's within the allowed root
const parentPath = path.dirname(targetPath); const parentPath = path.dirname(targetPath);
const hasParent = parentPath !== targetPath;
// Determine if parent navigation should be allowed:
// 1. Must have a different parent (not at filesystem root)
// 2. If ALLOWED_ROOT_DIRECTORY is set, parent must be within it
const hasParent = parentPath !== targetPath && isPathAllowed(parentPath);
// Security: Don't expose parent path outside allowed root
const safeParentPath = hasParent ? parentPath : null;
// Get available drives // Get available drives
const drives = await detectDrives(); const drives = await detectDrives();
@@ -70,7 +77,7 @@ export function createBrowseHandler() {
res.json({ res.json({
success: true, success: true,
currentPath: targetPath, currentPath: targetPath,
parentPath: hasParent ? parentPath : null, parentPath: safeParentPath,
directories, directories,
drives, drives,
}); });
@@ -84,7 +91,7 @@ export function createBrowseHandler() {
res.json({ res.json({
success: true, success: true,
currentPath: targetPath, currentPath: targetPath,
parentPath: hasParent ? parentPath : null, parentPath: safeParentPath,
directories: [], directories: [],
drives, drives,
warning: warning:

View File

@@ -5,7 +5,7 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import * as secureFs from '../../../lib/secure-fs.js'; import * as secureFs from '../../../lib/secure-fs.js';
import path from 'path'; import path from 'path';
import { isPathAllowed } from '@automaker/platform'; import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError } from '../common.js';
export function createValidatePathHandler() { export function createValidatePathHandler() {
@@ -20,6 +20,20 @@ export function createValidatePathHandler() {
const resolvedPath = path.resolve(filePath); const resolvedPath = path.resolve(filePath);
// Validate path against ALLOWED_ROOT_DIRECTORY before checking if it exists
if (!isPathAllowed(resolvedPath)) {
const allowedRoot = getAllowedRootDirectory();
const errorMessage = allowedRoot
? `Path not allowed: ${filePath}. Must be within ALLOWED_ROOT_DIRECTORY: ${allowedRoot}`
: `Path not allowed: ${filePath}`;
res.status(403).json({
success: false,
error: errorMessage,
isAllowed: false,
});
return;
}
// Check if path exists // Check if path exists
try { try {
const stats = await secureFs.stat(resolvedPath); const stats = await secureFs.stat(resolvedPath);
@@ -32,7 +46,7 @@ export function createValidatePathHandler() {
res.json({ res.json({
success: true, success: true,
path: resolvedPath, path: resolvedPath,
isAllowed: isPathAllowed(resolvedPath), isAllowed: true,
}); });
} catch { } catch {
res.status(400).json({ success: false, error: 'Path does not exist' }); res.status(400).json({ success: false, error: 'Path does not exist' });

View File

@@ -8,6 +8,7 @@ import { validatePathParams } from '../../middleware/validate-paths.js';
import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js'; import { createCheckGitHubRemoteHandler } from './routes/check-github-remote.js';
import { createListIssuesHandler } from './routes/list-issues.js'; import { createListIssuesHandler } from './routes/list-issues.js';
import { createListPRsHandler } from './routes/list-prs.js'; import { createListPRsHandler } from './routes/list-prs.js';
import { createListCommentsHandler } from './routes/list-comments.js';
import { createValidateIssueHandler } from './routes/validate-issue.js'; import { createValidateIssueHandler } from './routes/validate-issue.js';
import { import {
createValidationStatusHandler, createValidationStatusHandler,
@@ -27,6 +28,7 @@ export function createGitHubRoutes(
router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler()); router.post('/check-remote', validatePathParams('projectPath'), createCheckGitHubRemoteHandler());
router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler()); router.post('/issues', validatePathParams('projectPath'), createListIssuesHandler());
router.post('/prs', validatePathParams('projectPath'), createListPRsHandler()); router.post('/prs', validatePathParams('projectPath'), createListPRsHandler());
router.post('/issue-comments', validatePathParams('projectPath'), createListCommentsHandler());
router.post( router.post(
'/validate-issue', '/validate-issue',
validatePathParams('projectPath'), validatePathParams('projectPath'),

View File

@@ -4,6 +4,9 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { createLogger } from '@automaker/utils';
const logger = createLogger('GitHub');
export const execAsync = promisify(exec); export const execAsync = promisify(exec);
@@ -31,5 +34,5 @@ export function getErrorMessage(error: unknown): string {
} }
export function logError(error: unknown, context: string): void { export function logError(error: unknown, context: string): void {
console.error(`[GitHub] ${context}:`, error); logger.error(`${context}:`, error);
} }

View File

@@ -0,0 +1,212 @@
/**
* POST /issue-comments endpoint - Fetch comments for a GitHub issue
*/
import { spawn } from 'child_process';
import type { Request, Response } from 'express';
import type { GitHubComment, IssueCommentsResult } from '@automaker/types';
import { execEnv, getErrorMessage, logError } from './common.js';
import { checkGitHubRemote } from './check-github-remote.js';
interface ListCommentsRequest {
projectPath: string;
issueNumber: number;
cursor?: string;
}
interface GraphQLComment {
id: string;
author: {
login: string;
avatarUrl?: string;
} | null;
body: string;
createdAt: string;
updatedAt: string;
}
interface GraphQLResponse {
data?: {
repository?: {
issue?: {
comments: {
totalCount: number;
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
nodes: GraphQLComment[];
};
};
};
};
errors?: Array<{ message: string }>;
}
/** Timeout for GitHub API requests in milliseconds */
const GITHUB_API_TIMEOUT_MS = 30000;
/**
* Validate cursor format (GraphQL cursors are typically base64 strings)
*/
function isValidCursor(cursor: string): boolean {
return /^[A-Za-z0-9+/=]+$/.test(cursor);
}
/**
* Fetch comments for a specific issue using GitHub GraphQL API
*/
async function fetchIssueComments(
projectPath: string,
owner: string,
repo: string,
issueNumber: number,
cursor?: string
): Promise<IssueCommentsResult> {
// Validate cursor format to prevent potential injection
if (cursor && !isValidCursor(cursor)) {
throw new Error('Invalid cursor format');
}
// Use GraphQL variables instead of string interpolation for safety
const query = `
query GetIssueComments($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
comments(first: 50, after: $cursor) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
id
author {
login
avatarUrl
}
body
createdAt
updatedAt
}
}
}
}
}`;
const variables = {
owner,
repo,
issueNumber,
cursor: cursor || null,
};
const requestBody = JSON.stringify({ query, variables });
const response = await new Promise<GraphQLResponse>((resolve, reject) => {
const gh = spawn('gh', ['api', 'graphql', '--input', '-'], {
cwd: projectPath,
env: execEnv,
});
// Add timeout to prevent hanging indefinitely
const timeoutId = setTimeout(() => {
gh.kill();
reject(new Error('GitHub API request timed out'));
}, GITHUB_API_TIMEOUT_MS);
let stdout = '';
let stderr = '';
gh.stdout.on('data', (data: Buffer) => (stdout += data.toString()));
gh.stderr.on('data', (data: Buffer) => (stderr += data.toString()));
gh.on('close', (code) => {
clearTimeout(timeoutId);
if (code !== 0) {
return reject(new Error(`gh process exited with code ${code}: ${stderr}`));
}
try {
resolve(JSON.parse(stdout));
} catch (e) {
reject(e);
}
});
gh.stdin.write(requestBody);
gh.stdin.end();
});
if (response.errors && response.errors.length > 0) {
throw new Error(response.errors[0].message);
}
const commentsData = response.data?.repository?.issue?.comments;
if (!commentsData) {
throw new Error('Issue not found or no comments data available');
}
const comments: GitHubComment[] = commentsData.nodes.map((node) => ({
id: node.id,
author: {
login: node.author?.login || 'ghost',
avatarUrl: node.author?.avatarUrl,
},
body: node.body,
createdAt: node.createdAt,
updatedAt: node.updatedAt,
}));
return {
comments,
totalCount: commentsData.totalCount,
hasNextPage: commentsData.pageInfo.hasNextPage,
endCursor: commentsData.pageInfo.endCursor || undefined,
};
}
export function createListCommentsHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, issueNumber, cursor } = req.body as ListCommentsRequest;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!issueNumber || typeof issueNumber !== 'number') {
res
.status(400)
.json({ success: false, error: 'issueNumber is required and must be a number' });
return;
}
// First check if this is a GitHub repo and get owner/repo
const remoteStatus = await checkGitHubRemote(projectPath);
if (!remoteStatus.hasGitHubRemote || !remoteStatus.owner || !remoteStatus.repo) {
res.status(400).json({
success: false,
error: 'Project does not have a GitHub remote',
});
return;
}
const result = await fetchIssueComments(
projectPath,
remoteStatus.owner,
remoteStatus.repo,
issueNumber,
cursor
);
res.json({
success: true,
...result,
});
} catch (error) {
logError(error, `Fetch comments for issue failed`);
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -6,6 +6,9 @@ import { spawn } from 'child_process';
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { execAsync, execEnv, getErrorMessage, logError } from './common.js'; import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
import { checkGitHubRemote } from './check-github-remote.js'; import { checkGitHubRemote } from './check-github-remote.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('ListIssues');
export interface GitHubLabel { export interface GitHubLabel {
name: string; name: string;
@@ -179,7 +182,7 @@ async function fetchLinkedPRs(
} }
} catch (error) { } catch (error) {
// If GraphQL fails, continue without linked PRs // If GraphQL fails, continue without linked PRs
console.warn( logger.warn(
'Failed to fetch linked PRs via GraphQL:', 'Failed to fetch linked PRs via GraphQL:',
error instanceof Error ? error.message : error error instanceof Error ? error.message : error
); );

View File

@@ -1,20 +1,35 @@
/** /**
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK (async) * POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK or Cursor (async)
* *
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification. * Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
* Runs asynchronously and emits events for progress and completion. * Runs asynchronously and emits events for progress and completion.
* Supports both Claude models and Cursor models.
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk'; import { query } from '@anthropic-ai/claude-agent-sdk';
import type { EventEmitter } from '../../../lib/events.js'; import type { EventEmitter } from '../../../lib/events.js';
import type { IssueValidationResult, IssueValidationEvent, AgentModel } from '@automaker/types'; import type {
IssueValidationResult,
IssueValidationEvent,
ModelAlias,
CursorModelId,
GitHubComment,
LinkedPRInfo,
ThinkingLevel,
} from '@automaker/types';
import { isCursorModel, DEFAULT_PHASE_MODELS } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createSuggestionsOptions } from '../../../lib/sdk-options.js'; import { createSuggestionsOptions } from '../../../lib/sdk-options.js';
import { extractJson } from '../../../lib/json-extractor.js';
import { writeValidation } from '../../../lib/validation-storage.js'; import { writeValidation } from '../../../lib/validation-storage.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { import {
issueValidationSchema, issueValidationSchema,
ISSUE_VALIDATION_SYSTEM_PROMPT, ISSUE_VALIDATION_SYSTEM_PROMPT,
buildValidationPrompt, buildValidationPrompt,
ValidationComment,
ValidationLinkedPR,
} from './validation-schema.js'; } from './validation-schema.js';
import { import {
trySetValidationRunning, trySetValidationRunning,
@@ -26,8 +41,8 @@ import {
import type { SettingsService } from '../../../services/settings-service.js'; import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js'; import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
/** Valid model values for validation */ /** Valid Claude model values for validation */
const VALID_MODELS: readonly AgentModel[] = ['opus', 'sonnet', 'haiku'] as const; const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
/** /**
* Request body for issue validation * Request body for issue validation
@@ -38,8 +53,14 @@ interface ValidateIssueRequestBody {
issueTitle: string; issueTitle: string;
issueBody: string; issueBody: string;
issueLabels?: string[]; issueLabels?: string[];
/** Model to use for validation (opus, sonnet, haiku) */ /** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
model?: AgentModel; model?: ModelAlias | CursorModelId;
/** Thinking level for Claude models (ignored for Cursor models) */
thinkingLevel?: ThinkingLevel;
/** Comments to include in validation analysis */
comments?: GitHubComment[];
/** Linked pull requests for this issue */
linkedPRs?: LinkedPRInfo[];
} }
/** /**
@@ -47,6 +68,7 @@ interface ValidateIssueRequestBody {
* *
* Emits events for start, progress, complete, and error. * Emits events for start, progress, complete, and error.
* Stores result on completion. * Stores result on completion.
* Supports both Claude models (with structured output) and Cursor models (with JSON parsing).
*/ */
async function runValidation( async function runValidation(
projectPath: string, projectPath: string,
@@ -54,10 +76,13 @@ async function runValidation(
issueTitle: string, issueTitle: string,
issueBody: string, issueBody: string,
issueLabels: string[] | undefined, issueLabels: string[] | undefined,
model: AgentModel, model: ModelAlias | CursorModelId,
events: EventEmitter, events: EventEmitter,
abortController: AbortController, abortController: AbortController,
settingsService?: SettingsService settingsService?: SettingsService,
comments?: ValidationComment[],
linkedPRs?: ValidationLinkedPR[],
thinkingLevel?: ThinkingLevel
): Promise<void> { ): Promise<void> {
// Emit start event // Emit start event
const startEvent: IssueValidationEvent = { const startEvent: IssueValidationEvent = {
@@ -76,68 +101,146 @@ async function runValidation(
}, VALIDATION_TIMEOUT_MS); }, VALIDATION_TIMEOUT_MS);
try { try {
// Build the prompt // Build the prompt (include comments and linked PRs if provided)
const prompt = buildValidationPrompt(issueNumber, issueTitle, issueBody, issueLabels); const prompt = buildValidationPrompt(
issueNumber,
// Load autoLoadClaudeMd setting issueTitle,
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting( issueBody,
projectPath, issueLabels,
settingsService, comments,
'[ValidateIssue]' linkedPRs
); );
// Create SDK options with structured output and abort controller
const options = createSuggestionsOptions({
cwd: projectPath,
model,
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
abortController,
autoLoadClaudeMd,
outputFormat: {
type: 'json_schema',
schema: issueValidationSchema as Record<string, unknown>,
},
});
// Execute the query
const stream = query({ prompt, options });
let validationResult: IssueValidationResult | null = null; let validationResult: IssueValidationResult | null = null;
let responseText = ''; let responseText = '';
for await (const msg of stream) { // Route to appropriate provider based on model
// Collect assistant text for debugging and emit progress if (isCursorModel(model)) {
if (msg.type === 'assistant' && msg.message?.content) { // Use Cursor provider for Cursor models
for (const block of msg.message.content) { logger.info(`Using Cursor provider for validation with model: ${model}`);
if (block.type === 'text') {
responseText += block.text;
// Emit progress event const provider = ProviderFactory.getProviderForModel(model);
const progressEvent: IssueValidationEvent = {
type: 'issue_validation_progress', // For Cursor, include the system prompt and schema in the user prompt
issueNumber, const cursorPrompt = `${ISSUE_VALIDATION_SYSTEM_PROMPT}
content: block.text,
projectPath, CRITICAL INSTRUCTIONS:
}; 1. DO NOT write any files. Return the JSON in your response only.
events.emit('issue-validation:event', progressEvent); 2. Respond with ONLY a JSON object - no explanations, no markdown, just raw JSON.
3. The JSON must match this exact schema:
${JSON.stringify(issueValidationSchema, null, 2)}
Your entire response should be valid JSON starting with { and ending with }. No text before or after.
${prompt}`;
for await (const msg of provider.executeQuery({
prompt: cursorPrompt,
model,
cwd: projectPath,
readOnly: true, // Issue validation only reads code, doesn't write
})) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
responseText += block.text;
// Emit progress event
const progressEvent: IssueValidationEvent = {
type: 'issue_validation_progress',
issueNumber,
content: block.text,
projectPath,
};
events.emit('issue-validation:event', progressEvent);
}
}
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
// Use result if it's a final accumulated message
if (msg.result.length > responseText.length) {
responseText = msg.result;
} }
} }
} }
// Extract structured output on success // Parse JSON from the response text using shared utility
if (msg.type === 'result' && msg.subtype === 'success') { if (responseText) {
const resultMsg = msg as { structured_output?: IssueValidationResult }; validationResult = extractJson<IssueValidationResult>(responseText, { logger });
if (resultMsg.structured_output) { }
validationResult = resultMsg.structured_output; } else {
logger.debug('Received structured output:', validationResult); // Use Claude SDK for Claude models
} logger.info(`Using Claude provider for validation with model: ${model}`);
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[ValidateIssue]'
);
// Use thinkingLevel from request if provided, otherwise fall back to settings
let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel;
if (!effectiveThinkingLevel) {
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel;
const resolved = resolvePhaseModel(phaseModelEntry);
effectiveThinkingLevel = resolved.thinkingLevel;
} }
// Handle errors // Create SDK options with structured output and abort controller
if (msg.type === 'result') { const options = createSuggestionsOptions({
const resultMsg = msg as { subtype?: string }; cwd: projectPath,
if (resultMsg.subtype === 'error_max_structured_output_retries') { model: model as ModelAlias,
logger.error('Failed to produce valid structured output after retries'); systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
throw new Error('Could not produce valid validation output'); abortController,
autoLoadClaudeMd,
thinkingLevel: effectiveThinkingLevel,
outputFormat: {
type: 'json_schema',
schema: issueValidationSchema as Record<string, unknown>,
},
});
// Execute the query
const stream = query({ prompt, options });
for await (const msg of stream) {
// Collect assistant text for debugging and emit progress
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text') {
responseText += block.text;
// Emit progress event
const progressEvent: IssueValidationEvent = {
type: 'issue_validation_progress',
issueNumber,
content: block.text,
projectPath,
};
events.emit('issue-validation:event', progressEvent);
}
}
}
// Extract structured output on success
if (msg.type === 'result' && msg.subtype === 'success') {
const resultMsg = msg as { structured_output?: IssueValidationResult };
if (resultMsg.structured_output) {
validationResult = resultMsg.structured_output;
logger.debug('Received structured output:', validationResult);
}
}
// Handle errors
if (msg.type === 'result') {
const resultMsg = msg as { subtype?: string };
if (resultMsg.subtype === 'error_max_structured_output_retries') {
logger.error('Failed to produce valid structured output after retries');
throw new Error('Could not produce valid validation output');
}
} }
} }
} }
@@ -145,11 +248,10 @@ async function runValidation(
// Clear timeout // Clear timeout
clearTimeout(timeoutId); clearTimeout(timeoutId);
// Require structured output // Require validation result
if (!validationResult) { if (!validationResult) {
logger.error('No structured output received from Claude SDK'); logger.error('No validation result received from AI provider');
logger.debug('Raw response text:', responseText); throw new Error('Validation failed: no valid result received');
throw new Error('Validation failed: no structured output received');
} }
logger.info(`Issue #${issueNumber} validation complete: ${validationResult.verdict}`); logger.info(`Issue #${issueNumber} validation complete: ${validationResult.verdict}`);
@@ -214,8 +316,31 @@ export function createValidateIssueHandler(
issueBody, issueBody,
issueLabels, issueLabels,
model = 'opus', model = 'opus',
thinkingLevel,
comments: rawComments,
linkedPRs: rawLinkedPRs,
} = req.body as ValidateIssueRequestBody; } = req.body as ValidateIssueRequestBody;
// Transform GitHubComment[] to ValidationComment[] if provided
const validationComments: ValidationComment[] | undefined = rawComments?.map((c) => ({
author: c.author?.login || 'ghost',
createdAt: c.createdAt,
body: c.body,
}));
// Transform LinkedPRInfo[] to ValidationLinkedPR[] if provided
const validationLinkedPRs: ValidationLinkedPR[] | undefined = rawLinkedPRs?.map((pr) => ({
number: pr.number,
title: pr.title,
state: pr.state,
}));
logger.info(
`[ValidateIssue] Received validation request for issue #${issueNumber}` +
(rawComments?.length ? ` with ${rawComments.length} comments` : ' (no comments)') +
(rawLinkedPRs?.length ? ` and ${rawLinkedPRs.length} linked PRs` : '')
);
// Validate required fields // Validate required fields
if (!projectPath) { if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' }); res.status(400).json({ success: false, error: 'projectPath is required' });
@@ -239,11 +364,14 @@ export function createValidateIssueHandler(
return; return;
} }
// Validate model parameter at runtime // Validate model parameter at runtime - accept Claude models or Cursor models
if (!VALID_MODELS.includes(model)) { const isValidClaudeModel = VALID_CLAUDE_MODELS.includes(model as ModelAlias);
const isValidCursorModel = isCursorModel(model);
if (!isValidClaudeModel && !isValidCursorModel) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
error: `Invalid model. Must be one of: ${VALID_MODELS.join(', ')}`, error: `Invalid model. Must be one of: ${VALID_CLAUDE_MODELS.join(', ')}, or a Cursor model ID`,
}); });
return; return;
} }
@@ -271,11 +399,13 @@ export function createValidateIssueHandler(
model, model,
events, events,
abortController, abortController,
settingsService settingsService,
validationComments,
validationLinkedPRs,
thinkingLevel
) )
.catch((error) => { .catch(() => {
// Error is already handled inside runValidation (event emitted) // Error is already handled inside runValidation (event emitted)
logger.debug('Validation error caught in background handler:', error);
}) })
.finally(() => { .finally(() => {
clearValidationStatus(projectPath, issueNumber); clearValidationStatus(projectPath, issueNumber);

View File

@@ -49,6 +49,34 @@ export const issueValidationSchema = {
enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'], enum: ['trivial', 'simple', 'moderate', 'complex', 'very_complex'],
description: 'Estimated effort to address the issue', description: 'Estimated effort to address the issue',
}, },
prAnalysis: {
type: 'object',
properties: {
hasOpenPR: {
type: 'boolean',
description: 'Whether there is an open PR linked to this issue',
},
prFixesIssue: {
type: 'boolean',
description: 'Whether the PR appears to fix the issue based on the diff',
},
prNumber: {
type: 'number',
description: 'The PR number that was analyzed',
},
prSummary: {
type: 'string',
description: 'Brief summary of what the PR changes',
},
recommendation: {
type: 'string',
enum: ['wait_for_merge', 'pr_needs_work', 'no_pr'],
description:
'Recommendation: wait for PR to merge, PR needs more work, or no relevant PR',
},
},
description: 'Analysis of linked pull requests if any exist',
},
}, },
required: ['verdict', 'confidence', 'reasoning'], required: ['verdict', 'confidence', 'reasoning'],
additionalProperties: false, additionalProperties: false,
@@ -67,7 +95,8 @@ Your task is to analyze a GitHub issue and determine if it's valid by scanning t
1. **Read the issue carefully** - Understand what is being reported or requested 1. **Read the issue carefully** - Understand what is being reported or requested
2. **Search the codebase** - Use Glob to find relevant files by pattern, Grep to search for keywords 2. **Search the codebase** - Use Glob to find relevant files by pattern, Grep to search for keywords
3. **Examine the code** - Use Read to look at the actual implementation in relevant files 3. **Examine the code** - Use Read to look at the actual implementation in relevant files
4. **Form your verdict** - Based on your analysis, determine if the issue is valid 4. **Check linked PRs** - If there are linked pull requests, use \`gh pr diff <PR_NUMBER>\` to review the changes
5. **Form your verdict** - Based on your analysis, determine if the issue is valid
## Verdicts ## Verdicts
@@ -88,12 +117,32 @@ Your task is to analyze a GitHub issue and determine if it's valid by scanning t
- Is the implementation location clear? - Is the implementation location clear?
- Is the request technically feasible given the codebase structure? - Is the request technically feasible given the codebase structure?
## Analyzing Linked Pull Requests
When an issue has linked PRs (especially open ones), you MUST analyze them:
1. **Run \`gh pr diff <PR_NUMBER>\`** to see what changes the PR makes
2. **Run \`gh pr view <PR_NUMBER>\`** to see PR description and status
3. **Evaluate if the PR fixes the issue** - Does the diff address the reported problem?
4. **Provide a recommendation**:
- \`wait_for_merge\`: The PR appears to fix the issue correctly. No additional work needed - just wait for it to be merged.
- \`pr_needs_work\`: The PR attempts to fix the issue but is incomplete or has problems.
- \`no_pr\`: No relevant PR exists for this issue.
5. **Include prAnalysis in your response** with:
- hasOpenPR: true/false
- prFixesIssue: true/false (based on diff analysis)
- prNumber: the PR number you analyzed
- prSummary: brief description of what the PR changes
- recommendation: one of the above values
## Response Guidelines ## Response Guidelines
- **Always include relatedFiles** when you find relevant code - **Always include relatedFiles** when you find relevant code
- **Set bugConfirmed to true** only if you can definitively confirm a bug exists in the code - **Set bugConfirmed to true** only if you can definitively confirm a bug exists in the code
- **Provide a suggestedFix** when you have a clear idea of how to address the issue - **Provide a suggestedFix** when you have a clear idea of how to address the issue
- **Use missingInfo** when the verdict is needs_clarification to list what's needed - **Use missingInfo** when the verdict is needs_clarification to list what's needed
- **Include prAnalysis** when there are linked PRs - this is critical for avoiding duplicate work
- **Set estimatedComplexity** to help prioritize: - **Set estimatedComplexity** to help prioritize:
- trivial: Simple text changes, one-line fixes - trivial: Simple text changes, one-line fixes
- simple: Small changes to one file - simple: Small changes to one file
@@ -103,6 +152,24 @@ Your task is to analyze a GitHub issue and determine if it's valid by scanning t
Be thorough in your analysis but focus on files that are directly relevant to the issue.`; Be thorough in your analysis but focus on files that are directly relevant to the issue.`;
/**
* Comment data structure for validation prompt
*/
export interface ValidationComment {
author: string;
createdAt: string;
body: string;
}
/**
* Linked PR data structure for validation prompt
*/
export interface ValidationLinkedPR {
number: number;
title: string;
state: string;
}
/** /**
* Build the user prompt for issue validation. * Build the user prompt for issue validation.
* *
@@ -113,26 +180,60 @@ Be thorough in your analysis but focus on files that are directly relevant to th
* @param issueTitle - The issue title * @param issueTitle - The issue title
* @param issueBody - The issue body/description * @param issueBody - The issue body/description
* @param issueLabels - Optional array of label names * @param issueLabels - Optional array of label names
* @param comments - Optional array of comments to include in analysis
* @param linkedPRs - Optional array of linked pull requests
* @returns Formatted prompt string for the validation request * @returns Formatted prompt string for the validation request
*/ */
export function buildValidationPrompt( export function buildValidationPrompt(
issueNumber: number, issueNumber: number,
issueTitle: string, issueTitle: string,
issueBody: string, issueBody: string,
issueLabels?: string[] issueLabels?: string[],
comments?: ValidationComment[],
linkedPRs?: ValidationLinkedPR[]
): string { ): string {
const labelsSection = issueLabels?.length ? `\n\n**Labels:** ${issueLabels.join(', ')}` : ''; const labelsSection = issueLabels?.length ? `\n\n**Labels:** ${issueLabels.join(', ')}` : '';
let linkedPRsSection = '';
if (linkedPRs && linkedPRs.length > 0) {
const prsText = linkedPRs
.map((pr) => `- PR #${pr.number} (${pr.state}): ${pr.title}`)
.join('\n');
linkedPRsSection = `\n\n### Linked Pull Requests\n\n${prsText}`;
}
let commentsSection = '';
if (comments && comments.length > 0) {
// Limit to most recent 10 comments to control prompt size
const recentComments = comments.slice(-10);
const commentsText = recentComments
.map(
(c) => `**${c.author}** (${new Date(c.createdAt).toISOString().slice(0, 10)}):\n${c.body}`
)
.join('\n\n---\n\n');
commentsSection = `\n\n### Comments (${comments.length} total${comments.length > 10 ? ', showing last 10' : ''})\n\n${commentsText}`;
}
const hasWorkInProgress =
linkedPRs && linkedPRs.some((pr) => pr.state === 'open' || pr.state === 'OPEN');
const workInProgressNote = hasWorkInProgress
? '\n\n**Note:** This issue has an open pull request linked. Consider that someone may already be working on a fix.'
: '';
return `Please validate the following GitHub issue by analyzing the codebase: return `Please validate the following GitHub issue by analyzing the codebase:
## Issue #${issueNumber}: ${issueTitle} ## Issue #${issueNumber}: ${issueTitle}
${labelsSection} ${labelsSection}
${linkedPRsSection}
### Description ### Description
${issueBody || '(No description provided)'} ${issueBody || '(No description provided)'}
${commentsSection}
${workInProgressNote}
--- ---
Scan the codebase to verify this issue. Look for the files, components, or functionality mentioned. Determine if this issue is valid, invalid, or needs clarification.`; Scan the codebase to verify this issue. Look for the files, components, or functionality mentioned. Determine if this issue is valid, invalid, or needs clarification.${comments && comments.length > 0 ? ' Consider the context provided in the comments as well.' : ''}${hasWorkInProgress ? ' Also note in your analysis if there is already work in progress on this issue.' : ''}`;
} }

View File

@@ -1,16 +1,30 @@
/** /**
* Health check routes * Health check routes
*
* NOTE: Only the basic health check (/) and environment check are unauthenticated.
* The /detailed endpoint requires authentication.
*/ */
import { Router } from 'express'; import { Router } from 'express';
import { createIndexHandler } from './routes/index.js'; import { createIndexHandler } from './routes/index.js';
import { createDetailedHandler } from './routes/detailed.js'; import { createEnvironmentHandler } from './routes/environment.js';
/**
* Create unauthenticated health routes (basic check only)
* Used by load balancers and container orchestration
*/
export function createHealthRoutes(): Router { export function createHealthRoutes(): Router {
const router = Router(); const router = Router();
// Basic health check - no sensitive info
router.get('/', createIndexHandler()); router.get('/', createIndexHandler());
router.get('/detailed', createDetailedHandler());
// Environment info including containerization status
// This is unauthenticated so the UI can check on startup
router.get('/environment', createEnvironmentHandler());
return router; return router;
} }
// Re-export detailed handler for use in authenticated routes
export { createDetailedHandler } from './routes/detailed.js';

View File

@@ -4,13 +4,14 @@
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { getAuthStatus } from '../../../lib/auth.js'; import { getAuthStatus } from '../../../lib/auth.js';
import { getVersion } from '../../../lib/version.js';
export function createDetailedHandler() { export function createDetailedHandler() {
return (_req: Request, res: Response): void => { return (_req: Request, res: Response): void => {
res.json({ res.json({
status: 'ok', status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '0.1.0', version: getVersion(),
uptime: process.uptime(), uptime: process.uptime(),
memory: process.memoryUsage(), memory: process.memoryUsage(),
dataDir: process.env.DATA_DIR || './data', dataDir: process.env.DATA_DIR || './data',

View File

@@ -0,0 +1,20 @@
/**
* GET /environment endpoint - Environment information including containerization status
*
* This endpoint is unauthenticated so the UI can check it on startup
* before login to determine if sandbox risk warnings should be shown.
*/
import type { Request, Response } from 'express';
export interface EnvironmentResponse {
isContainerized: boolean;
}
export function createEnvironmentHandler() {
return (_req: Request, res: Response): void => {
res.json({
isContainerized: process.env.IS_CONTAINERIZED === 'true',
} satisfies EnvironmentResponse);
};
}

View File

@@ -3,13 +3,14 @@
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { getVersion } from '../../../lib/version.js';
export function createIndexHandler() { export function createIndexHandler() {
return (_req: Request, res: Response): void => { return (_req: Request, res: Response): void => {
res.json({ res.json({
status: 'ok', status: 'ok',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
version: process.env.npm_package_version || '0.1.0', version: getVersion(),
}); });
}; };
} }

View File

@@ -0,0 +1,12 @@
/**
* Common utilities for ideation routes
*/
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger('Ideation');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
export const logError = createLogError(logger);

View File

@@ -0,0 +1,109 @@
/**
* Ideation routes - HTTP API for brainstorming and idea management
*/
import { Router } from 'express';
import type { EventEmitter } from '../../lib/events.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import type { IdeationService } from '../../services/ideation-service.js';
import type { FeatureLoader } from '../../services/feature-loader.js';
// Route handlers
import { createSessionStartHandler } from './routes/session-start.js';
import { createSessionMessageHandler } from './routes/session-message.js';
import { createSessionStopHandler } from './routes/session-stop.js';
import { createSessionGetHandler } from './routes/session-get.js';
import { createIdeasListHandler } from './routes/ideas-list.js';
import { createIdeasCreateHandler } from './routes/ideas-create.js';
import { createIdeasGetHandler } from './routes/ideas-get.js';
import { createIdeasUpdateHandler } from './routes/ideas-update.js';
import { createIdeasDeleteHandler } from './routes/ideas-delete.js';
import { createAnalyzeHandler, createGetAnalysisHandler } from './routes/analyze.js';
import { createConvertHandler } from './routes/convert.js';
import { createAddSuggestionHandler } from './routes/add-suggestion.js';
import { createPromptsHandler, createPromptsByCategoryHandler } from './routes/prompts.js';
import { createSuggestionsGenerateHandler } from './routes/suggestions-generate.js';
export function createIdeationRoutes(
events: EventEmitter,
ideationService: IdeationService,
featureLoader: FeatureLoader
): Router {
const router = Router();
// Session management
router.post(
'/session/start',
validatePathParams('projectPath'),
createSessionStartHandler(ideationService)
);
router.post('/session/message', createSessionMessageHandler(ideationService));
router.post('/session/stop', createSessionStopHandler(events, ideationService));
router.post(
'/session/get',
validatePathParams('projectPath'),
createSessionGetHandler(ideationService)
);
// Ideas CRUD
router.post(
'/ideas/list',
validatePathParams('projectPath'),
createIdeasListHandler(ideationService)
);
router.post(
'/ideas/create',
validatePathParams('projectPath'),
createIdeasCreateHandler(events, ideationService)
);
router.post(
'/ideas/get',
validatePathParams('projectPath'),
createIdeasGetHandler(ideationService)
);
router.post(
'/ideas/update',
validatePathParams('projectPath'),
createIdeasUpdateHandler(events, ideationService)
);
router.post(
'/ideas/delete',
validatePathParams('projectPath'),
createIdeasDeleteHandler(events, ideationService)
);
// Project analysis
router.post('/analyze', validatePathParams('projectPath'), createAnalyzeHandler(ideationService));
router.post(
'/analysis',
validatePathParams('projectPath'),
createGetAnalysisHandler(ideationService)
);
// Convert to feature
router.post(
'/convert',
validatePathParams('projectPath'),
createConvertHandler(events, ideationService, featureLoader)
);
// Add suggestion to board as a feature
router.post(
'/add-suggestion',
validatePathParams('projectPath'),
createAddSuggestionHandler(ideationService, featureLoader)
);
// Guided prompts (no validation needed - static data)
router.get('/prompts', createPromptsHandler(ideationService));
router.get('/prompts/:category', createPromptsByCategoryHandler(ideationService));
// Generate suggestions (structured output)
router.post(
'/suggestions/generate',
validatePathParams('projectPath'),
createSuggestionsGenerateHandler(ideationService)
);
return router;
}

View File

@@ -0,0 +1,70 @@
/**
* POST /add-suggestion - Add an analysis suggestion to the board as a feature
*
* This endpoint converts an AnalysisSuggestion to a Feature using the
* IdeationService's mapIdeaCategoryToFeatureCategory for consistent category mapping.
* This ensures a single source of truth for the conversion logic.
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { FeatureLoader } from '../../../services/feature-loader.js';
import type { AnalysisSuggestion } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createAddSuggestionHandler(
ideationService: IdeationService,
featureLoader: FeatureLoader
) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, suggestion } = req.body as {
projectPath: string;
suggestion: AnalysisSuggestion;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!suggestion) {
res.status(400).json({ success: false, error: 'suggestion is required' });
return;
}
if (!suggestion.title) {
res.status(400).json({ success: false, error: 'suggestion.title is required' });
return;
}
if (!suggestion.category) {
res.status(400).json({ success: false, error: 'suggestion.category is required' });
return;
}
// Build description with rationale if provided
const description = suggestion.rationale
? `${suggestion.description}\n\n**Rationale:** ${suggestion.rationale}`
: suggestion.description;
// Use the service's category mapping for consistency
const featureCategory = ideationService.mapSuggestionCategoryToFeatureCategory(
suggestion.category
);
// Create the feature
const feature = await featureLoader.create(projectPath, {
title: suggestion.title,
description,
category: featureCategory,
status: 'backlog',
});
res.json({ success: true, featureId: feature.id });
} catch (error) {
logError(error, 'Add suggestion to board failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,49 @@
/**
* POST /analyze - Analyze project and generate suggestions
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createAnalyzeHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
// Start analysis - results come via WebSocket events
ideationService.analyzeProject(projectPath).catch((error) => {
logError(error, 'Analyze project failed (async)');
});
res.json({ success: true, message: 'Analysis started' });
} catch (error) {
logError(error, 'Analyze project failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}
export function createGetAnalysisHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const result = await ideationService.getCachedAnalysis(projectPath);
res.json({ success: true, result });
} catch (error) {
logError(error, 'Get analysis failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,77 @@
/**
* POST /convert - Convert an idea to a feature
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { FeatureLoader } from '../../../services/feature-loader.js';
import type { ConvertToFeatureOptions } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createConvertHandler(
events: EventEmitter,
ideationService: IdeationService,
featureLoader: FeatureLoader
) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, ideaId, keepIdea, column, dependencies, tags } = req.body as {
projectPath: string;
ideaId: string;
} & ConvertToFeatureOptions;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!ideaId) {
res.status(400).json({ success: false, error: 'ideaId is required' });
return;
}
// Convert idea to feature structure
const featureData = await ideationService.convertToFeature(projectPath, ideaId);
// Apply any options from the request
if (column) {
featureData.status = column;
}
if (dependencies && dependencies.length > 0) {
featureData.dependencies = dependencies;
}
if (tags && tags.length > 0) {
featureData.tags = tags;
}
// Create the feature using FeatureLoader
const feature = await featureLoader.create(projectPath, featureData);
// Delete the idea unless keepIdea is explicitly true
if (!keepIdea) {
await ideationService.deleteIdea(projectPath, ideaId);
// Emit idea deleted event
events.emit('ideation:idea-deleted', {
projectPath,
ideaId,
});
}
// Emit idea converted event to notify frontend
events.emit('ideation:idea-converted', {
projectPath,
ideaId,
featureId: feature.id,
keepIdea: !!keepIdea,
});
// Return featureId as expected by the frontend API interface
res.json({ success: true, featureId: feature.id });
} catch (error) {
logError(error, 'Convert to feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,51 @@
/**
* POST /ideas/create - Create a new idea
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { CreateIdeaInput } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasCreateHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, idea } = req.body as {
projectPath: string;
idea: CreateIdeaInput;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!idea) {
res.status(400).json({ success: false, error: 'idea is required' });
return;
}
if (!idea.title || !idea.description || !idea.category) {
res.status(400).json({
success: false,
error: 'idea must have title, description, and category',
});
return;
}
const created = await ideationService.createIdea(projectPath, idea);
// Emit idea created event for frontend notification
events.emit('ideation:idea-created', {
projectPath,
idea: created,
});
res.json({ success: true, idea: created });
} catch (error) {
logError(error, 'Create idea failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,42 @@
/**
* POST /ideas/delete - Delete an idea
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasDeleteHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, ideaId } = req.body as {
projectPath: string;
ideaId: string;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!ideaId) {
res.status(400).json({ success: false, error: 'ideaId is required' });
return;
}
await ideationService.deleteIdea(projectPath, ideaId);
// Emit idea deleted event for frontend notification
events.emit('ideation:idea-deleted', {
projectPath,
ideaId,
});
res.json({ success: true });
} catch (error) {
logError(error, 'Delete idea failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,39 @@
/**
* POST /ideas/get - Get a single idea
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasGetHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, ideaId } = req.body as {
projectPath: string;
ideaId: string;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!ideaId) {
res.status(400).json({ success: false, error: 'ideaId is required' });
return;
}
const idea = await ideationService.getIdea(projectPath, ideaId);
if (!idea) {
res.status(404).json({ success: false, error: 'Idea not found' });
return;
}
res.json({ success: true, idea });
} catch (error) {
logError(error, 'Get idea failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,26 @@
/**
* POST /ideas/list - List all ideas for a project
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasListHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const ideas = await ideationService.getIdeas(projectPath);
res.json({ success: true, ideas });
} catch (error) {
logError(error, 'List ideas failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,54 @@
/**
* POST /ideas/update - Update an idea
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { UpdateIdeaInput } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createIdeasUpdateHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, ideaId, updates } = req.body as {
projectPath: string;
ideaId: string;
updates: UpdateIdeaInput;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!ideaId) {
res.status(400).json({ success: false, error: 'ideaId is required' });
return;
}
if (!updates) {
res.status(400).json({ success: false, error: 'updates is required' });
return;
}
const idea = await ideationService.updateIdea(projectPath, ideaId, updates);
if (!idea) {
res.status(404).json({ success: false, error: 'Idea not found' });
return;
}
// Emit idea updated event for frontend notification
events.emit('ideation:idea-updated', {
projectPath,
ideaId,
idea,
});
res.json({ success: true, idea });
} catch (error) {
logError(error, 'Update idea failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,42 @@
/**
* GET /prompts - Get all guided prompts
* GET /prompts/:category - Get prompts for a specific category
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { IdeaCategory } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createPromptsHandler(ideationService: IdeationService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const prompts = ideationService.getAllPrompts();
const categories = ideationService.getPromptCategories();
res.json({ success: true, prompts, categories });
} catch (error) {
logError(error, 'Get prompts failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}
export function createPromptsByCategoryHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { category } = req.params as { category: string };
const validCategories = ideationService.getPromptCategories().map((c) => c.id);
if (!validCategories.includes(category as IdeaCategory)) {
res.status(400).json({ success: false, error: 'Invalid category' });
return;
}
const prompts = ideationService.getPromptsByCategory(category as IdeaCategory);
res.json({ success: true, prompts });
} catch (error) {
logError(error, 'Get prompts by category failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,45 @@
/**
* POST /session/get - Get an ideation session with messages
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createSessionGetHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, sessionId } = req.body as {
projectPath: string;
sessionId: string;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!sessionId) {
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const session = await ideationService.getSession(projectPath, sessionId);
if (!session) {
res.status(404).json({ success: false, error: 'Session not found' });
return;
}
const isRunning = ideationService.isSessionRunning(sessionId);
res.json({
success: true,
session: { ...session, isRunning },
messages: session.messages,
});
} catch (error) {
logError(error, 'Get session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,40 @@
/**
* POST /session/message - Send a message in an ideation session
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { SendMessageOptions } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createSessionMessageHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { sessionId, message, options } = req.body as {
sessionId: string;
message: string;
options?: SendMessageOptions;
};
if (!sessionId) {
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
if (!message) {
res.status(400).json({ success: false, error: 'message is required' });
return;
}
// This is async but we don't await - responses come via WebSocket
ideationService.sendMessage(sessionId, message, options).catch((error) => {
logError(error, 'Send message failed (async)');
});
res.json({ success: true });
} catch (error) {
logError(error, 'Send message failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,30 @@
/**
* POST /session/start - Start a new ideation session
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import type { StartSessionOptions } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createSessionStartHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, options } = req.body as {
projectPath: string;
options?: StartSessionOptions;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const session = await ideationService.startSession(projectPath, options);
res.json({ success: true, session });
} catch (error) {
logError(error, 'Start session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,39 @@
/**
* POST /session/stop - Stop an ideation session
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import type { IdeationService } from '../../../services/ideation-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createSessionStopHandler(events: EventEmitter, ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { sessionId, projectPath } = req.body as {
sessionId: string;
projectPath?: string;
};
if (!sessionId) {
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
await ideationService.stopSession(sessionId);
// Emit session stopped event for frontend notification
// Note: The service also emits 'ideation:session-ended' internally,
// but we emit here as well for route-level consistency with other routes
events.emit('ideation:session-ended', {
sessionId,
projectPath,
});
res.json({ success: true });
} catch (error) {
logError(error, 'Stop session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,56 @@
/**
* Generate suggestions route - Returns structured AI suggestions for a prompt
*/
import type { Request, Response } from 'express';
import type { IdeationService } from '../../../services/ideation-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('ideation:suggestions-generate');
export function createSuggestionsGenerateHandler(ideationService: IdeationService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, promptId, category, count } = req.body;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!promptId) {
res.status(400).json({ success: false, error: 'promptId is required' });
return;
}
if (!category) {
res.status(400).json({ success: false, error: 'category is required' });
return;
}
// Default to 10 suggestions, allow 1-20
const suggestionCount = Math.min(Math.max(count || 10, 1), 20);
logger.info(`Generating ${suggestionCount} suggestions for prompt: ${promptId}`);
const suggestions = await ideationService.generateSuggestions(
projectPath,
promptId,
category,
suggestionCount
);
res.json({
success: true,
suggestions,
});
} catch (error) {
logError(error, 'Failed to generate suggestions');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,24 @@
/**
* Common utilities for MCP routes
*/
import { createLogger } from '@automaker/utils';
const logger = createLogger('MCP');
/**
* Extract error message from unknown error
*/
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
/**
* Log error with prefix
*/
export function logError(error: unknown, message: string): void {
logger.error(`${message}:`, error);
}

View File

@@ -0,0 +1,36 @@
/**
* MCP routes - HTTP API for testing MCP servers
*
* Provides endpoints for:
* - Testing MCP server connections
* - Listing available tools from MCP servers
*
* Mounted at /api/mcp in the main server.
*/
import { Router } from 'express';
import type { MCPTestService } from '../../services/mcp-test-service.js';
import { createTestServerHandler } from './routes/test-server.js';
import { createListToolsHandler } from './routes/list-tools.js';
/**
* Create MCP router with all endpoints
*
* Endpoints:
* - POST /test - Test MCP server connection
* - POST /tools - List tools from MCP server
*
* @param mcpTestService - Instance of MCPTestService for testing connections
* @returns Express Router configured with all MCP endpoints
*/
export function createMCPRoutes(mcpTestService: MCPTestService): Router {
const router = Router();
// Test MCP server connection
router.post('/test', createTestServerHandler(mcpTestService));
// List tools from MCP server
router.post('/tools', createListToolsHandler(mcpTestService));
return router;
}

View File

@@ -0,0 +1,57 @@
/**
* POST /api/mcp/tools - List tools for an MCP server
*
* Lists available tools for an MCP server.
* Similar to test but focused on tool discovery.
*
* SECURITY: Only accepts serverId to look up saved configs. Does NOT accept
* arbitrary serverConfig to prevent drive-by command execution attacks.
* Users must explicitly save a server config through the UI before testing.
*
* Request body:
* { serverId: string } - Get tools by server ID from settings
*
* Response: { success: boolean, tools?: MCPToolInfo[], error?: string }
*/
import type { Request, Response } from 'express';
import type { MCPTestService } from '../../../services/mcp-test-service.js';
import { getErrorMessage, logError } from '../common.js';
interface ListToolsRequest {
serverId: string;
}
/**
* Create handler factory for POST /api/mcp/tools
*/
export function createListToolsHandler(mcpTestService: MCPTestService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const body = req.body as ListToolsRequest;
if (!body.serverId || typeof body.serverId !== 'string') {
res.status(400).json({
success: false,
error: 'serverId is required',
});
return;
}
const result = await mcpTestService.testServerById(body.serverId);
// Return only tool-related information
res.json({
success: result.success,
tools: result.tools,
error: result.error,
});
} catch (error) {
logError(error, 'List tools failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,50 @@
/**
* POST /api/mcp/test - Test MCP server connection and list tools
*
* Tests connection to an MCP server and returns available tools.
*
* SECURITY: Only accepts serverId to look up saved configs. Does NOT accept
* arbitrary serverConfig to prevent drive-by command execution attacks.
* Users must explicitly save a server config through the UI before testing.
*
* Request body:
* { serverId: string } - Test server by ID from settings
*
* Response: { success: boolean, tools?: MCPToolInfo[], error?: string, connectionTime?: number }
*/
import type { Request, Response } from 'express';
import type { MCPTestService } from '../../../services/mcp-test-service.js';
import { getErrorMessage, logError } from '../common.js';
interface TestServerRequest {
serverId: string;
}
/**
* Create handler factory for POST /api/mcp/test
*/
export function createTestServerHandler(mcpTestService: MCPTestService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const body = req.body as TestServerRequest;
if (!body.serverId || typeof body.serverId !== 'string') {
res.status(400).json({
success: false,
error: 'serverId is required',
});
return;
}
const result = await mcpTestService.testServerById(body.serverId);
res.json(result);
} catch (error) {
logError(error, 'Test server failed');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -1,61 +1,16 @@
/** /**
* GET /available endpoint - Get available models * GET /available endpoint - Get available models from all providers
*/ */
import type { Request, Response } from 'express'; import type { Request, Response } from 'express';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { getErrorMessage, logError } from '../common.js'; import { getErrorMessage, logError } from '../common.js';
interface ModelDefinition {
id: string;
name: string;
provider: string;
contextWindow: number;
maxOutputTokens: number;
supportsVision: boolean;
supportsTools: boolean;
}
export function createAvailableHandler() { export function createAvailableHandler() {
return async (_req: Request, res: Response): Promise<void> => { return async (_req: Request, res: Response): Promise<void> => {
try { try {
const models: ModelDefinition[] = [ // Get all models from all registered providers (Claude + Cursor)
{ const models = ProviderFactory.getAllAvailableModels();
id: 'claude-opus-4-5-20251101',
name: 'Claude Opus 4.5',
provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
id: 'claude-sonnet-4-20250514',
name: 'Claude Sonnet 4',
provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
id: 'claude-3-5-sonnet-20241022',
name: 'Claude 3.5 Sonnet',
provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
},
{
id: 'claude-3-5-haiku-20241022',
name: 'Claude 3.5 Haiku',
provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
},
];
res.json({ success: true, models }); res.json({ success: true, models });
} catch (error) { } catch (error) {

View File

@@ -17,6 +17,13 @@ export function createProvidersHandler() {
available: statuses.claude?.installed || false, available: statuses.claude?.installed || false,
hasApiKey: !!process.env.ANTHROPIC_API_KEY, hasApiKey: !!process.env.ANTHROPIC_API_KEY,
}, },
cursor: {
available: statuses.cursor?.installed || false,
version: statuses.cursor?.version,
path: statuses.cursor?.path,
method: statuses.cursor?.method,
authenticated: statuses.cursor?.authenticated,
},
}; };
res.json({ success: true, providers }); res.json({ success: true, providers });

View File

@@ -0,0 +1,21 @@
/**
* Common utilities for pipeline routes
*
* Provides logger and error handling utilities shared across all pipeline endpoints.
*/
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
/** Logger instance for pipeline-related operations */
export const logger = createLogger('Pipeline');
/**
* Extract user-friendly error message from error objects
*/
export { getErrorMessageShared as getErrorMessage };
/**
* Log error with automatic logger binding
*/
export const logError = createLogError(logger);

View File

@@ -0,0 +1,77 @@
/**
* Pipeline routes - HTTP API for pipeline configuration management
*
* Provides endpoints for:
* - Getting pipeline configuration
* - Saving pipeline configuration
* - Adding, updating, deleting, and reordering pipeline steps
*
* All endpoints use handler factories that receive the PipelineService instance.
* Mounted at /api/pipeline in the main server.
*/
import { Router } from 'express';
import type { PipelineService } from '../../services/pipeline-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createGetConfigHandler } from './routes/get-config.js';
import { createSaveConfigHandler } from './routes/save-config.js';
import { createAddStepHandler } from './routes/add-step.js';
import { createUpdateStepHandler } from './routes/update-step.js';
import { createDeleteStepHandler } from './routes/delete-step.js';
import { createReorderStepsHandler } from './routes/reorder-steps.js';
/**
* Create pipeline router with all endpoints
*
* Endpoints:
* - POST /config - Get pipeline configuration
* - POST /config/save - Save entire pipeline configuration
* - POST /steps/add - Add a new pipeline step
* - POST /steps/update - Update an existing pipeline step
* - POST /steps/delete - Delete a pipeline step
* - POST /steps/reorder - Reorder pipeline steps
*
* @param pipelineService - Instance of PipelineService for file I/O
* @returns Express Router configured with all pipeline endpoints
*/
export function createPipelineRoutes(pipelineService: PipelineService): Router {
const router = Router();
// Get pipeline configuration
router.post(
'/config',
validatePathParams('projectPath'),
createGetConfigHandler(pipelineService)
);
// Save entire pipeline configuration
router.post(
'/config/save',
validatePathParams('projectPath'),
createSaveConfigHandler(pipelineService)
);
// Pipeline step operations
router.post(
'/steps/add',
validatePathParams('projectPath'),
createAddStepHandler(pipelineService)
);
router.post(
'/steps/update',
validatePathParams('projectPath'),
createUpdateStepHandler(pipelineService)
);
router.post(
'/steps/delete',
validatePathParams('projectPath'),
createDeleteStepHandler(pipelineService)
);
router.post(
'/steps/reorder',
validatePathParams('projectPath'),
createReorderStepsHandler(pipelineService)
);
return router;
}

View File

@@ -0,0 +1,54 @@
/**
* POST /api/pipeline/steps/add - Add a new pipeline step
*
* Adds a new step to the pipeline configuration.
*
* Request body: { projectPath: string, step: { name, order, instructions, colorClass } }
* Response: { success: true, step: PipelineStep }
*/
import type { Request, Response } from 'express';
import type { PipelineService } from '../../../services/pipeline-service.js';
import type { PipelineStep } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createAddStepHandler(pipelineService: PipelineService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, step } = req.body as {
projectPath: string;
step: Omit<PipelineStep, 'id' | 'createdAt' | 'updatedAt'>;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!step) {
res.status(400).json({ success: false, error: 'step is required' });
return;
}
if (!step.name) {
res.status(400).json({ success: false, error: 'step.name is required' });
return;
}
if (step.instructions === undefined) {
res.status(400).json({ success: false, error: 'step.instructions is required' });
return;
}
const newStep = await pipelineService.addStep(projectPath, step);
res.json({
success: true,
step: newStep,
});
} catch (error) {
logError(error, 'Add pipeline step failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,42 @@
/**
* POST /api/pipeline/steps/delete - Delete a pipeline step
*
* Removes a step from the pipeline configuration.
*
* Request body: { projectPath: string, stepId: string }
* Response: { success: true }
*/
import type { Request, Response } from 'express';
import type { PipelineService } from '../../../services/pipeline-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createDeleteStepHandler(pipelineService: PipelineService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, stepId } = req.body as {
projectPath: string;
stepId: string;
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!stepId) {
res.status(400).json({ success: false, error: 'stepId is required' });
return;
}
await pipelineService.deleteStep(projectPath, stepId);
res.json({
success: true,
});
} catch (error) {
logError(error, 'Delete pipeline step failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,35 @@
/**
* POST /api/pipeline/config - Get pipeline configuration
*
* Returns the pipeline configuration for a project.
*
* Request body: { projectPath: string }
* Response: { success: true, config: PipelineConfig }
*/
import type { Request, Response } from 'express';
import type { PipelineService } from '../../../services/pipeline-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createGetConfigHandler(pipelineService: PipelineService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const config = await pipelineService.getPipelineConfig(projectPath);
res.json({
success: true,
config,
});
} catch (error) {
logError(error, 'Get pipeline config failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,42 @@
/**
* POST /api/pipeline/steps/reorder - Reorder pipeline steps
*
* Reorders the steps in the pipeline configuration.
*
* Request body: { projectPath: string, stepIds: string[] }
* Response: { success: true }
*/
import type { Request, Response } from 'express';
import type { PipelineService } from '../../../services/pipeline-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createReorderStepsHandler(pipelineService: PipelineService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, stepIds } = req.body as {
projectPath: string;
stepIds: string[];
};
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!stepIds || !Array.isArray(stepIds)) {
res.status(400).json({ success: false, error: 'stepIds array is required' });
return;
}
await pipelineService.reorderSteps(projectPath, stepIds);
res.json({
success: true,
});
} catch (error) {
logError(error, 'Reorder pipeline steps failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

Some files were not shown because too many files have changed in this diff Show More