Compare commits

...

484 Commits

Author SHA1 Message Date
Shirone
5b620011ad feat: add CodeRabbit integration for AI-powered code reviews
This commit introduces the CodeRabbit service and its associated routes, enabling users to trigger, manage, and check the status of code reviews through a new API. Key features include:

- New routes for triggering code reviews, checking status, and stopping reviews.
- Integration with the CodeRabbit CLI for authentication and status checks.
- UI components for displaying code review results and settings management.
- Unit tests for the new code review functionality to ensure reliability.

This enhancement aims to streamline the code review process and leverage AI capabilities for improved code quality.
2026-01-24 21:10:33 +01:00
Shirone
327aef89a2 Merge pull request #562 from AutoMaker-Org/feature/v0.12.0rc-1768688900786-5ea1
refactor: standardize PR state representation across the application
2026-01-18 10:45:59 +00:00
Shirone
44e665f1bf fix: adress pr comments 2026-01-18 00:22:27 +01:00
Shirone
5b1e0105f4 refactor: standardize PR state representation across the application
Updated the PR state handling to use a consistent uppercase format ('OPEN', 'MERGED', 'CLOSED') throughout the codebase. This includes changes to the worktree metadata interface, PR creation logic, and related tests to ensure uniformity and prevent potential mismatches in state representation.

Additionally, modified the GitHub PR fetching logic to retrieve all PR states, allowing for better detection of state changes.

This refactor enhances clarity and consistency in how PR states are managed and displayed.
2026-01-17 23:58:19 +01:00
webdevcody
832d10e133 refactor: replace Loader2 with Spinner component across the application
This update standardizes the loading indicators by replacing all instances of Loader2 with the new Spinner component. The Spinner component provides a consistent look and feel for loading states throughout the UI, enhancing the user experience.

Changes include:
- Updated loading indicators in various components such as popovers, modals, and views.
- Ensured that the Spinner component is used with appropriate sizes for different contexts.

No functional changes were made; this is purely a visual and structural improvement.
2026-01-17 17:58:16 -05:00
DhanushSantosh
044c3d50d1 fix: mark dmg-license as optional dependency for cross-platform builds
dmg-license is a macOS-only package used for building DMG installers.
Moving it from devDependencies to optionalDependencies allows npm ci
to succeed on Linux and Windows without failing on platform checks.

macOS developers will still get the package when available.
Linux/Windows developers can now run npm ci without errors.

Fixes: npm ci failing on Linux with "EBADPLATFORM" error

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-18 01:28:14 +05:30
Shirone
a1de0a78a0 Merge pull request #545 from stefandevo/fix/sandbox-warning-persistence
fix: sandbox warning persistence and add env var option
2026-01-17 18:57:19 +00:00
Shirone
fef9639e01 Merge pull request #539 from stefandevo/fix/light-mode-agent-output
fix: respect theme in agent output modal and log viewer
2026-01-17 18:35:32 +00:00
Stefan de Vogelaere
aef479218d fix: use DEFAULT_FONT_VALUE for initial terminal font
The initial terminalState.fontFamily was set to a raw font string
that didn't match any option in TERMINAL_FONT_OPTIONS, causing the
dropdown to appear empty. Changed to use DEFAULT_FONT_VALUE sentinel.
2026-01-17 19:32:42 +01:00
Stefan de Vogelaere
ded5ecf4e9 refactor: reduce code duplication in font settings and sync logic
Address CodeRabbit review feedback:
- Create getEffectiveFont helper to deduplicate getEffectiveFontSans/Mono
- Extract getSettingsFieldValue and hasSettingsFieldChanged helpers
- Create reusable FontSelector component for font selection UI
- Refactor project-theme-section and appearance-section to use FontSelector
2026-01-17 19:30:00 +01:00
Stefan de Vogelaere
a01f299597 fix: resolve type errors after merging upstream v0.12.0rc
- Fix ThemeMode type casting in __root.tsx
- Use specRegeneration.create() instead of non-existent generateAppSpec
- Add missing keyboard shortcut entries for projectSettings and notifications
- Fix lucide-react type casts with intermediate unknown cast
- Remove unused pipelineConfig prop from ListRow component
- Align SettingsProject interface with Project type
- Fix defaultDeleteBranchWithWorktree property name
2026-01-17 19:20:49 +01:00
Stefan de Vogelaere
21c9e88a86 Merge remote-tracking branch 'upstream/v0.12.0rc' into fix/light-mode-agent-output 2026-01-17 19:10:49 +01:00
Shirone
af17f6e36f Merge pull request #535 from stefandevo/v0.12.0rc
feat: add font customization and 8 new themes
2026-01-17 18:06:04 +00:00
Stefan de Vogelaere
e69a2ad722 docs: add AUTOMAKER_SKIP_SANDBOX_WARNING env var documentation
Document the new environment variable in README.md and .env.example
2026-01-17 18:33:08 +01:00
DhanushSantosh
0480f6ccd6 fix: handle dynamic model IDs with slashes in the model name
isOpencodeModel was rejecting valid dynamic model IDs like
'openrouter/qwen/qwen3-14b:free' because it was splitting on all slashes
and expecting exactly 2 parts. This caused valid OpenCode models to be
treated as unknown, falling back to Claude.

Now correctly splits on the FIRST slash only, allowing model names
like 'qwen/qwen3-14b:free' to be recognized as valid.

Fixes: User selects openrouter/qwen/qwen3-14b:free → server falls back to Claude

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-17 21:13:47 +05:30
DhanushSantosh
24042d20c2 fix: filter dynamic OpenCode models by enabled status in model selector
The phase model selector was showing ALL discovered dynamic models regardless
of whether they were enabled in settings. Now it filters dynamic models by
enabledDynamicModelIds, matching the behavior of Cursor models and making
the enable/disable setting meaningful.

Users can now:
- Disable models in settings they don't want to use
- See only enabled dynamic models in the model selector dropdown
- Have the "Select all" checkbox properly control which models appear

This ensures consistency: enabling/disabling models in settings affects
which models are available for feature execution.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-17 21:00:22 +05:30
DhanushSantosh
9c3b3a4104 fix: make dynamic models select-all checkbox respect search filters
The "Select all" checkbox for dynamic models was using the unfiltered models list,
causing the checkbox state to not reflect what users see when searching. Now it
correctly operates on the filtered models list so:

- Checkbox state matches the visible filtered models
- "Select all" only toggles models the user can see
- Indeterminate state shows if some filtered models are selected

This ensures the checkbox has a meaningful purpose when filtering/searching models.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-17 20:32:10 +05:30
Stefan de Vogelaere
17e2cdfc85 fix: sandbox warning persistence and add env var option
Fix race condition where sandbox warning appeared on every refresh
even after checking "Do not show again". The issue was that the
sandbox check effect ran before settings were hydrated from the
server, so skipSandboxWarning was always false (the default).

Changes:
- Add settingsLoaded to sandbox check dependencies to ensure the
  user's preference is loaded before checking
- Add AUTOMAKER_SKIP_SANDBOX_WARNING env var option to skip the
  warning entirely (useful for dev/CI environments)
2026-01-17 15:33:51 +01:00
DhanushSantosh
466c34afd4 ci: improve release workflow artifact uploads
- Use explicit file patterns to exclude builder config/debug files (builder-*.yml, *.yaml)
- Include blockmap files for efficient delta updates in auto-update scenarios
- Ensure only production-ready artifacts are uploaded to GitHub releases

This prevents accidental inclusion of builder configuration files in the release assets.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-17 19:18:15 +05:30
Shirone
b9567f5904 Merge pull request #542 from stefandevo/fix/api-key-info-on-dev-restart
docs: add hint about AUTOMAKER_API_KEY env var to API key banner
2026-01-17 13:09:36 +00:00
Shirone
c2cf8ae892 Merge pull request #540 from stefandevo/fix/gh-not-in-git-folder
fix: stop repeated GitHub PR fetch warnings for non-GitHub repos
2026-01-17 13:08:28 +00:00
Stefan de Vogelaere
3aa3c10ea4 docs: add hint about AUTOMAKER_API_KEY env var to API key banner
When the dev server restarts, developers need to re-enter the API key
in the browser. While the key is persisted to ./data/.api-key, this
file may be missing in clean dev scenarios.

This adds a helpful tip to the API key banner informing developers
they can set AUTOMAKER_API_KEY environment variable for a persistent
API key during development, avoiding the need to re-enter it after
server restarts.
2026-01-17 13:53:34 +01:00
Stefan de Vogelaere
5cd4183a7b fix: use fresh timestamp when setting cache entry
Use Date.now() after checkGitHubRemote() completes instead of the
pre-captured timestamp to ensure accurate 5-minute TTL.
2026-01-17 12:36:33 +01:00
Stefan de Vogelaere
2d9e38ad99 fix: stop repeated GitHub PR fetch warnings for non-GitHub repos
When opening a git repository without a GitHub remote, the server logs
were spammed with warnings every 5 seconds during worktree polling:

  WARN [Worktree] Failed to fetch GitHub PRs: Command failed: gh pr list
  ... no git remotes found

This happened because fetchGitHubPRs() ran `gh pr list` without first
checking if the project has a GitHub remote configured.

Changes:
- Add per-project cache for GitHub remote status with 5-minute TTL
- Check cache before attempting to fetch PRs, skip silently if no remote
- Add forceRefreshGitHub parameter to clear cache on manual refresh
- Pass forceRefreshGitHub when user clicks the refresh worktrees button

This allows users to add a GitHub remote and immediately detect it by
clicking the refresh button, while preventing log spam during normal
polling for projects without GitHub remotes.
2026-01-17 12:32:42 +01:00
Shirone
93d73f6d26 Merge pull request #529 from AutoMaker-Org/feature/v0.12.0rc-1768603410796-o2fn
fix: UUID generation fail in docker env
2026-01-17 11:19:01 +00:00
Stefan de Vogelaere
5209395a74 fix: respect theme in agent output modal and log viewer
The Agent Output modal and LogViewer component had hardcoded dark zinc
colors that didn't adapt to light mode themes. Replaced all hardcoded
colors with semantic Tailwind classes (bg-popover, text-foreground,
text-muted-foreground, bg-muted, border-border) that automatically
respect the active theme.
2026-01-17 11:44:33 +01:00
DhanushSantosh
ef6b9ac2d2 fix: add --force flag to npm ci for platform-specific dependencies
npm ci without --force rejects platform-specific packages like dmg-license
which is macOS-only. The --force flag tells npm to proceed even when
platform constraints are violated.

This allows Linux containers to skip dmg-license and continue with the
install, matching the behavior we want for Docker development.
2026-01-17 15:53:31 +05:30
DhanushSantosh
92afbeb6bd fix: run npm install as root to avoid permission issues
The named Docker volume for node_modules is created with root ownership,
causing EACCES errors when npm tries to write as the automaker user.

Solution:
- Run npm ci as root (installation phase)
- Use --legacy-peer-deps to properly handle optional dependencies
- Fix permissions after install
- Run server process as automaker user for security

This eliminates permission denied errors during npm install in dev containers.
2026-01-17 15:36:50 +05:30
DhanushSantosh
bbdc11ce47 fix: improve docker-compose npm install permissions and use npm ci
Fixes permission denied errors when installing dependencies in Docker containers:

Changes:
- Remove stale node_modules directories before installing (fresh start)
- Use 'npm ci --force' instead of 'npm install --force' for deterministic installs
- Add chmod to ensure writable permissions on node_modules
- Properly fix directory ownership and permissions before install

This prevents EACCES errors when multiple processes try to write to node_modules
and handles lingering permission issues from previous failed container runs.
2026-01-17 15:30:21 +05:30
DhanushSantosh
545bf2045d fix: add --force flag to npm install in docker-compose files
Allow npm to install platform-specific devDependencies (like dmg-license
which is macOS-only) by skipping platform checks in Linux Docker containers.
This matches the behavior already used in CI workflows.

Fixes Docker container startup failure:
- docker-compose.dev.yml (full stack development)
- docker-compose.dev-server.yml (server-only with local Electron)

The --force flag allows npm to proceed with installation even when some
optional/platform-specific dependencies can't be installed on the current
platform.
2026-01-17 15:00:48 +05:30
DhanushSantosh
a0471098fa fix: use specific data-testid selectors in project switcher assertions
Replace generic getByRole('button', { name: /.../ }) selectors with specific
getByTestId('project-switcher-project-') to avoid strict mode
violations where the selector resolves to multiple elements (project switcher
button and sidebar button).

Fixes failing E2E tests:
- feature-manual-review-flow.spec.ts
- new-project-creation.spec.ts
- open-existing-project.spec.ts
2026-01-17 14:53:06 +05:30
Stefan de Vogelaere
3320b40d15 feat: align terminal font settings with appearance fonts
- Terminal font dropdown now uses mono fonts from UI font options
- Unified font list between appearance section and terminal settings
- Terminal font persisted to GlobalSettings for import/export support
- Aligned global terminal settings popover with per-terminal popover:
  - Same settings in same order (Font Size, Run on New Terminal, Font Family, Scrollback, Line Height, Screen Reader)
  - Consistent styling (Radix Select instead of native select)
- Added terminal padding (12px vertical, 16px horizontal) for readability
2026-01-17 10:18:11 +01:00
DhanushSantosh
bac5e1c220 Merge upstream/v0.12.0rc into feature/fedora-rpm-support
Resolved conflict in backlog-plan/common.ts:
- Kept local (stricter) validation: Array.isArray(parsed?.result?.changes)
- This ensures type safety for the changes array
2026-01-17 14:44:37 +05:30
DhanushSantosh
33fa138d21 feat: add docker group support with sg docker command
Improve Docker access handling by detecting and using 'sg docker' command
when the user is in the docker group but hasn't logged out yet. This allows
running docker commands without requiring a full session restart after
`usermod -aG docker $USER`.

Changes:
- Detect docker group access and fall back to sg docker -c when needed
- Export DOCKER_CMD variable for use throughout the script
- Update all docker compose and docker ps commands to use DOCKER_CMD
- Improve error messages to guide users on fixing docker access issues
2026-01-17 14:40:08 +05:30
DhanushSantosh
bc09a22e1f fix: extract app version from apps/ui/package.json instead of monorepo root
The start-automaker.sh script now correctly sources the app version (0.12.0)
from apps/ui/package.json instead of the monorepo version (1.0.0) from the
root package.json. This ensures the launcher displays the correct Automaker
application version.
2026-01-17 14:19:12 +05:30
Stefan de Vogelaere
b771b51842 fix: address code review feedback
- Fix git+ssh URL to git+https for @electron/node-gyp (build compatibility)
- Remove duplicate @fontsource packages from root package.json
- Refactor font state initialization to reduce code duplication
2026-01-17 09:15:35 +01:00
Stefan de Vogelaere
1a7bf27ead feat: add new themes, Zed fonts, and sort theme/font lists
New themes added:
- Dark: Ayu Dark, Ayu Mirage, Ember, Matcha
- Light: Ayu Light, One Light, Bluloco, Feather

Other changes:
- Bundle Zed Sans and Zed Mono fonts from zed-industries/zed-fonts
- Sort font options alphabetically (default first)
- Sort theme options alphabetically (Dark/Light first)
- Improve Ayu Dark text contrast for better readability
- Fix Matcha theme to have green undertone instead of blue
2026-01-17 09:15:35 +01:00
Stefan de Vogelaere
f3b00d0f78 feat: add global font settings with per-project override
- Add fontFamilySans and fontFamilyMono to GlobalSettings type
- Add global font state and actions to app store
- Update getEffectiveFontSans/Mono to fall back to global settings
- Add font selectors to global Settings → Appearance
- Add "Use Global Font" checkboxes in Project Settings → Theme
- Add fonts to settings sync and migration
- Include fonts in import/export JSON
2026-01-17 09:15:34 +01:00
Stefan de Vogelaere
c747baaee2 fix: use sentinel value for default font selection
Radix UI Select doesn't allow empty string values, so use 'default'
as a sentinel value instead.
2026-01-17 09:15:34 +01:00
Stefan de Vogelaere
1322722db2 feat: add per-project font override settings
Add font selectors that allow per-project font customization for both
sans and mono fonts, independent of theme selection. Uses system fonts.

- Add fontFamilySans and fontFamilyMono to ProjectSettings and Project types
- Create ui-font-options.ts config with system font options
- Add store actions: setProjectFontSans, setProjectFontMono, getEffectiveFontSans, getEffectiveFontMono
- Apply font CSS variables in root component
- Add font selector UI in project-theme-section (Project Settings → Theme)
2026-01-17 09:15:34 +01:00
webdevcody
aa35eb3d3a feat: implement spec synchronization feature for improved project management
- Added a new `/sync` endpoint to synchronize the project specification with the current codebase and feature state.
- Introduced `syncSpec` function to handle the synchronization logic, updating technology stack, implemented features, and roadmap phases.
- Enhanced the running state management to track synchronization tasks alongside existing generation tasks.
- Updated UI components to support synchronization actions, including loading indicators and status updates.
- Improved logging and error handling for better visibility during sync operations.

These changes enhance project management capabilities by ensuring that the specification remains up-to-date with the latest code and feature developments.
2026-01-17 01:45:45 -05:00
webdevcody
616e2ef75f feat: add HOSTNAME and VITE_HOSTNAME support for improved server URL configuration
- Introduced `HOSTNAME` environment variable for user-facing URLs, defaulting to localhost.
- Updated server and client code to utilize `HOSTNAME` for constructing URLs instead of hardcoded localhost.
- Enhanced documentation in CLAUDE.md to reflect new configuration options.
- Added `VITE_HOSTNAME` for frontend API URLs, ensuring consistent hostname usage across the application.

These changes improve flexibility in server configuration and enhance the user experience by providing accurate URLs.
2026-01-16 22:40:36 -05:00
webdevcody
d98cae124f feat: enhance sidebar functionality for mobile and compact views
- Introduced a floating toggle button for mobile to show/hide the sidebar when collapsed.
- Updated sidebar behavior to completely hide on mobile when the new mobileSidebarHidden state is true.
- Added logic to conditionally render sidebar components based on screen size using the new useIsCompact hook.
- Enhanced SidebarHeader to include close and expand buttons for mobile views.
- Refactored CollapseToggleButton to hide in compact mode.
- Implemented HeaderActionsPanel for mobile actions in various views, improving accessibility and usability on smaller screens.

These changes improve the user experience on mobile devices by providing better navigation options and visibility controls.
2026-01-16 22:27:19 -05:00
Web Dev Cody
26aaef002d Merge pull request #537 from AutoMaker-Org/claude/issue-536-20260117-0132
feat: add configurable host binding for server and Vite dev server
2026-01-16 21:22:34 -05:00
claude[bot]
09bb59d090 feat: add configurable host binding for server and Vite dev server
- Add HOST environment variable (default: 0.0.0.0) to allow binding to specific network interfaces
- Update server to listen on configurable host instead of hardcoded localhost
- Update Vite dev server to respect HOST environment variable
- Enhanced server startup banner to display listening address
- Updated .env.example and CLAUDE.md documentation

Fixes #536

Co-authored-by: Web Dev Cody <webdevcody@users.noreply.github.com>
2026-01-17 01:34:06 +00:00
Shirone
2f38ffe2d5 Merge pull request #532 from AutoMaker-Org/feature/v0.12.0rc-1768605251997-8ufb
fix: feature.json corruption on crash lose
2026-01-17 00:00:18 +00:00
Shirone
12fa9d858d Merge pull request #533 from AutoMaker-Org/feature/v0.12.0rc-1768605477061-fhv5
fix: Codex freezes
2026-01-16 23:59:16 +00:00
Shirone
c4e1a58e0d refactor: update timeout constants in CLI and Codex providers
- Removed redundant definition of CLI base timeout in `cli-provider.ts` and added a detailed comment explaining its purpose.
- Updated `codex-provider.ts` to use the imported `DEFAULT_TIMEOUT_MS` directly instead of an alias.
- Enhanced unit tests to ensure fallback behavior for invalid reasoning effort values in timeout calculations.
2026-01-17 00:52:57 +01:00
Shirone
8661f33c6d feat: implement atomic file writing and recovery utilities
- Introduced atomic write functionality for JSON files to ensure data integrity during writes.
- Added recovery mechanisms to read JSON files with fallback options for corrupted or missing files.
- Enhanced existing services to utilize atomic write and recovery features for improved reliability.
- Updated tests to cover new atomic writing and recovery scenarios, ensuring robust error handling and data consistency.
2026-01-17 00:50:51 +01:00
Shirone
5c24ca2220 feat: implement dynamic timeout calculation for reasoning efforts in CLI and Codex providers
- Added `calculateReasoningTimeout` function to dynamically adjust timeouts based on reasoning effort levels.
- Updated CLI and Codex providers to utilize the new timeout calculation, addressing potential timeouts for high reasoning efforts.
- Enhanced unit tests to validate timeout behavior for various reasoning efforts, ensuring correct timeout values are applied.
2026-01-17 00:50:06 +01:00
webdevcody
14559354dd refactor: update sidebar navigation sections for clarity
- Added Notifications and Project Settings as standalone sections in the sidebar without labels for visual separation.
- Removed the previous 'Other' label to enhance the organization of navigation items.
2026-01-16 18:49:35 -05:00
webdevcody
3bf9dbd43a Merge branch 'v0.12.0rc' of github.com:AutoMaker-Org/automaker into v0.12.0rc 2026-01-16 18:39:31 -05:00
webdevcody
bd3999416b feat: implement notifications and event history features
- Added Notification Service to manage project-level notifications, including creation, listing, marking as read, and dismissing notifications.
- Introduced Event History Service to store and manage historical events, allowing for listing, retrieval, deletion, and replaying of events.
- Integrated notifications into the server and UI, providing real-time updates for feature statuses and operations.
- Enhanced sidebar and project switcher components to display unread notifications count.
- Created dedicated views for managing notifications and event history, improving user experience and accessibility.

These changes enhance the application's ability to inform users about important events and statuses, improving overall usability and responsiveness.
2026-01-16 18:37:11 -05:00
Shirone
cc9f7d48c8 fix: enhance authentication error handling in Claude usage service tests
- Updated test to send a specific authentication error pattern to the data callback.
- Triggered the exit handler to validate the handling of authentication errors.
- Improved error message expectations for better clarity during test failures.
2026-01-16 23:58:48 +01:00
Shirone
6bb0461be7 Merge pull request #527 from AutoMaker-Org/feature/v0.12.0rc-1768598412391-lnp7
feat: implement XML extraction utilities and enhance feature handling
2026-01-16 22:52:55 +00:00
Shirone
16ef026b38 refactor: Centralize UUID generation with fallback support 2026-01-16 23:49:36 +01:00
Shirone
50ed405c4a fix: adress pr comments 2026-01-16 23:41:23 +01:00
Web Dev Cody
5407e1a9ff Merge pull request #525 from stefandevo/feature/project-settings
feat: Separate Project Settings from Global Settings
2026-01-16 17:31:19 -05:00
Stefan de Vogelaere
5436b18f70 refactor: move Project Settings below Tools section in sidebar
- Remove Project Settings from Project section
- Add Project Settings as standalone section below Tools/GitHub
- Use empty label for visual separation without header
- Add horizontal separator line above sections without labels
- Rename to "Project Settings" for clarity
- Keep "Global Settings" at bottom of sidebar
2026-01-16 23:27:53 +01:00
Stefan de Vogelaere
8b7700364d refactor: move project settings to Project section, rename global settings
- Move "Settings" from Tools section to Project section in sidebar
- Rename bottom settings link from "Settings" to "Global Settings"
- Update keyboard shortcut description accordingly
2026-01-16 23:17:50 +01:00
Shirone
3bdf3cbb5c fix: improve branch name generation logic in BoardView and useBoardActions
- Updated the logic for auto-generating branch names to consistently use the primary branch (main/master) and avoid nested feature paths.
- Removed references to currentWorktreeBranch in favor of getPrimaryWorktreeBranch for better clarity and maintainability.
- Enhanced comments to clarify the purpose of branch name generation.
2026-01-16 23:14:22 +01:00
webdevcody
45d9c9a5d8 fix: adjust menu dimensions and formatting in start-automaker.sh
- Increased MENU_BOX_WIDTH and MENU_INNER_WIDTH for better layout.
- Updated printf statements in show_menu() for consistent spacing and alignment of menu options.
- Enhanced exit option formatting for improved readability.
2026-01-16 17:10:20 -05:00
Stefan de Vogelaere
6a23e6ce78 fix: address PR review feedback
- Fix race conditions when rapidly switching projects
  - Added cancellation logic to prevent stale responses from updating state
  - Both project settings and init script loading now properly cancelled on unmount

- Improve error handling in custom icon upload
  - Added toast notifications for validation errors (file type, file size)
  - Added toast notifications for upload success/failure
  - Handle network errors gracefully with user feedback
  - Handle file reader errors
2026-01-16 23:03:21 +01:00
Stefan de Vogelaere
4e53215104 chore: reset package-lock.json to match base branch 2026-01-16 23:03:21 +01:00
Stefan de Vogelaere
2899b6d416 feat: separate project settings from global settings
This PR introduces a new dedicated Project Settings screen accessible from
the sidebar, clearly separating project-specific settings from global
application settings.

- Added new route `/project-settings` with dedicated view
- Sidebar navigation item "Settings" in Tools section (Shift+S shortcut)
- Sidebar-based navigation matching global Settings pattern
- Sections: Identity, Worktrees, Theme, Danger Zone

**Moved to Project Settings:**
- Project name and icon customization
- Project-specific theme override
- Worktree isolation enable/disable (per-project override)
- Init script indicator visibility and auto-dismiss
- Delete branch by default preference
- Initialization script editor
- Delete project (Danger Zone)

**Remains in Global Settings:**
- Global theme (default for all projects)
- Global worktree isolation (default for new projects)
- Feature Defaults, Model Defaults
- API Keys, AI Providers, MCP Servers
- Terminal, Keyboard Shortcuts, Audio
- Account, Security, Developer settings

Both Theme and Worktree Isolation now follow a consistent override pattern:
1. Global Settings defines the default value
2. New projects inherit the global value
3. Project Settings can override for that specific project
4. Changing global setting doesn't affect projects with overrides

- Fixed: Changing global theme was incorrectly overwriting project themes
- Fixed: Project worktree setting not persisting across sessions
- Project settings now properly load from server on component mount

- Shell syntax editor: improved background contrast (bg-background)
- Shell syntax editor: removed distracting active line highlight
- Project Settings header matches Context/Memory views pattern

- `apps/ui/src/routes/project-settings.tsx`
- `apps/ui/src/components/views/project-settings-view/` (9 files)

- Global settings simplified (removed project-specific options)
- Sidebar navigation updated with project settings link
- App store: added project-specific useWorktrees state/actions
- Types: added projectSettings keyboard shortcut
- HTTP client: added missing project settings response fields
2026-01-16 23:03:21 +01:00
Kacper
b263cc615e feat: implement XML extraction utilities and enhance feature handling
- Introduced a new xml-extractor module with functions for XML parsing, including escaping/unescaping XML characters, extracting sections and elements, and managing implemented features.
- Added functionality to add, remove, update, and check for implemented features in the app_spec.txt file.
- Enhanced the create and update feature handlers to check for duplicate titles and trigger synchronization with app_spec.txt on status changes.
- Updated tests to cover new XML extraction utilities and feature handling logic, ensuring robust functionality and reliability.
2026-01-16 22:55:10 +01:00
webdevcody
97b0028919 chore: update package versions to 0.12.0 and 0.12.0rc
- Updated the version in package.json for the main project to 0.12.0rc.
- Updated the version in apps/server/package.json and apps/ui/package.json to 0.12.0.
- Adjusted the version extraction logic in start-automaker.sh to reference the correct package.json path.
2026-01-16 16:48:43 -05:00
webdevcody
fd1727a443 Merge branch 'v0.12.0rc' of github.com:AutoMaker-Org/automaker into v0.12.0rc 2026-01-16 16:11:56 -05:00
webdevcody
597cb9bfae refactor: remove dev.mjs and integrate start-automaker.sh for development mode
- Deleted the dev.mjs script, consolidating development mode functionality into start-automaker.sh.
- Updated package.json to use start-automaker.sh for the "dev" script and added a "start" script for production mode.
- Enhanced start-automaker.sh with production build capabilities and improved argument parsing for better user experience.
- Removed launcher-utils.mjs as its functionality has been integrated into start-automaker.sh.
2026-01-16 16:11:53 -05:00
Kacper
c2430e5bd3 feat: enhance PTY handling for Windows in ClaudeUsageService and TerminalService
- Added detection for Electron environment to improve compatibility with Windows PTY processes.
- Implemented winpty fallback for ConPTY failures, ensuring robust terminal session creation in Electron and other contexts.
- Updated error handling to provide clearer messages for authentication and terminal access issues.
- Refined usage data detection logic to avoid false positives, improving the accuracy of usage reporting.

These changes aim to enhance the reliability and user experience of terminal interactions on Windows, particularly in Electron applications.
2026-01-16 21:53:53 +01:00
Shirone
68df8efd10 Merge pull request #522 from AutoMaker-Org/feature/v0.12.0rc-1768590871767-bl1c
feat: add filters to github issues view
2026-01-16 20:08:05 +00:00
Kacper
c0d64bc994 fix: adress pr comments 2026-01-16 21:05:58 +01:00
Kacper
6237f1a0fe feat: add filtering capabilities to GitHub issues view
- Implemented a comprehensive filtering system for GitHub issues, allowing users to filter by state, labels, assignees, and validation status.
- Introduced a new IssuesFilterControls component for managing filter options.
- Updated the GitHubIssuesView to utilize the new filtering logic, enhancing the user experience by providing clearer visibility into matching issues.
- Added hooks for filtering logic and state management, ensuring efficient updates and rendering of filtered issues.

These changes aim to improve the usability of the issues view by enabling users to easily navigate and manage their issues based on specific criteria.
2026-01-16 20:56:23 +01:00
Web Dev Cody
30c50d9b78 Merge pull request #513 from JZilla808/feature/tui-launcher
feat: add TUI launcher script for easy app startup
2026-01-16 14:51:36 -05:00
Web Dev Cody
03516ac09e Merge pull request #519 from WikiRik/WikiRik/audit-fix
chore: run npm audit fix
2026-01-16 14:43:27 -05:00
Shirone
5e5a136f1f Merge pull request #521 from AutoMaker-Org/feature/v0.12.0rc-1768591325146-pye6
fix: Signals not supported on windows.
2026-01-16 19:39:00 +00:00
Kacper
98c50d44a4 test: mock Unix platform for SIGTERM behavior in ClaudeUsageService tests
Added a mock for the Unix platform in the SIGTERM test case to ensure proper behavior during testing on non-Windows systems. This change enhances the reliability of the tests by simulating the expected environment for process termination.
2026-01-16 20:38:29 +01:00
Kacper
0e9369816f fix: unify PTY process termination handling across platforms
Refactored the process termination logic in both ClaudeUsageService and TerminalService to use a centralized method for killing PTY processes. This ensures consistent handling of process termination across Windows and Unix-like systems, improving reliability and maintainability of the code.
2026-01-16 20:34:12 +01:00
Kacper
be63a59e9c fix: improve process termination handling for Windows
Updated the process termination logic in ClaudeUsageService to handle Windows environments correctly. The code now checks the operating system and calls the appropriate kill method, ensuring consistent behavior across platforms.
2026-01-16 20:27:53 +01:00
Kacper
dbb84aba23 fix: ensure proper type handling for JSON parsing in loadBacklogPlan function
Updated the JSON parsing in the loadBacklogPlan function to explicitly cast the raw input as a string, improving type safety and preventing potential runtime errors when handling backlog plan data.
2026-01-16 20:09:01 +01:00
Shirone
9819d2e91c Merge pull request #514 from Seonfx/fix/510-spec-generation-json-fallback
fix: add JSON fallback for spec generation with custom API endpoints
2026-01-16 19:01:47 +00:00
Kacper
4c24ba5a8b feat: enhance TUI launcher with Docker/Electron process detection
- Add 4 launch options matching dev.mjs (Web, Electron, Docker Dev, Electron+Docker)
- Add arrow key navigation in menu with visual selection indicator
- Add cross-platform port conflict detection and resolution (Windows/Unix)
- Add Docker container detection with Stop/Restart/Attach/Cancel options
- Add Electron process detection when switching between modes
- Add centered, styled output for Docker build progress
- Add HUSKY=0 to docker-compose files to prevent permission errors
- Fix Windows/Git Bash compatibility (platform detection, netstat/taskkill)
- Fix bash arithmetic issue with set -e causing script to hang

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:58:32 +01:00
Rik Smale
e67cab1e07 chore: fix lockfile 2026-01-16 19:23:18 +01:00
Rik Smale
132b8f7529 chore: run npm audit fix 2026-01-16 19:18:16 +01:00
Seonfx
d651e9d8d6 fix: address PR review feedback for JSON fallback
- Simplify escapeXml() using 'str == null' check (type narrowing)
- Add validation for extracted JSON before passing to specToXml()
- Prevents runtime errors when JSON doesn't match SpecOutput schema

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 13:43:56 -04:00
webdevcody
92f14508aa chore: update environment variable documentation for Anthropic API key
- Changed comments in docker-compose files to clarify that the ANTHROPIC_API_KEY is optional.
- Updated README to reflect changes in authentication setup, emphasizing integration with Claude Code CLI and removing outdated API key instructions.
- Improved clarity on authentication methods and streamlined the setup process for users.
2026-01-16 11:23:45 -05:00
DhanushSantosh
842b059fac fix: remove invalid local keyword in main script body
The 'local' keyword can only be used inside functions. Line 423 had
'local timeout_count=0' in the main script body which caused a bash error.
Removed the unused variable declaration.

Fixes: bash error 'local: can only be used in a function'

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 20:44:17 +05:30
DhanushSantosh
49f9ecc168 feat: enhance TUI launcher with production-ready features and documentation
Major improvements to start-automaker.sh launcher script:

**Architecture & Code Quality:**
- Organized into logical sections with clear separators (8 sections)
- Extracted all magic numbers into named constants at top
- Added comprehensive comments throughout

**Functionality:**
- Dynamic version extraction from package.json (no manual updates)
- Pre-flight checks: validates Node.js, npm, tput installed
- Platform detection: warns on Windows/unsupported systems
- Terminal size validation: checks min 70x20, displays warning if too small
- Input timeout: 30-second auto-timeout for hands-free operation
- History tracking: remembers last selected mode in ~/.automaker_launcher_history

**User Experience:**
- Added --help flag with comprehensive usage documentation
- Added --version flag showing version, Node.js, Bash info
- Added --check-deps flag to verify project dependencies
- Added --no-colors flag for terminals without color support
- Added --no-history flag to disable history tracking
- Enhanced cleanup function: restores cursor + echo, better signal handling
- Better error messages with actionable remediation steps
- Improved exit experience: "Goodbye! See you soon." message

**Robustness:**
- Real initialization checks (validates node_modules, build artifacts)
- Spinner uses frame counting instead of infinite loop (max 1.6s)
- Proper signal trap handling (EXIT, INT, TERM)
- Error recovery: respects --no-colors in pre-flight checks

**File Management:**
- Renamed from "start automaker.sh" to "start-automaker.sh" for consistency
- Made script more portable with SCRIPT_DIR detection

**Documentation:**
- Added section to README.md: "Interactive TUI Launcher"
- Documented all launch modes and options with examples
- Added feature list, history file location, usage tips
- Updated table of contents with TUI launcher section

Fixes: #511 (CI test failures resolved)
Improvements: Better UX for new users, production-ready error handling

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 20:27:53 +05:30
DhanushSantosh
e02fd889c2 fix: add --force flag to npm install in format-check workflow
Ensures dmg-license can be installed on Linux CI runners even though it's
a darwin-only package. The --force flag allows npm to skip platform mismatches.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:51:23 +05:30
DhanushSantosh
52a821d3bb fix: add --force flag to npm install in CI to allow platform-specific devDependencies
dmg-license is a darwin-only package required for macOS DMG building. The CI runs on
Linux, so npm install fails when trying to install a platform-specific devDependency.

Using --force allows npm to skip platform mismatches instead of erroring out, allowing
the build to proceed on non-darwin platforms where the darwin-only dependency will simply
be skipped.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:43:09 +05:30
DhanushSantosh
becd79f1e3 fix: add missing dmg-license dependency to fix release builds
The release workflow was failing for all platforms because macOS DMG
builder requires dmg-license. This single dependency was preventing
AppImage, DEB, RPM, DMG, and EXE artifacts from being built and
uploaded to any release since v0.7.3.

Includes lockfile updates and conversion of git+ssh:// URLs to https://
to prevent SSH key requirement issues in CI/CD and across environments.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:38:10 +05:30
DhanushSantosh
883ad2a04b fix(backlog-plan): clear running details in generate-plan finally block
Ensure running details are cleared when generation completes or fails, preventing state leaks.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:37:21 +05:30
DhanushSantosh
bf93cdf0c4 fix(backlog-plan): clear running details when stopping generation
Add setRunningDetails(null) to stop handler to prevent state leaks when aborting operation.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:37:21 +05:30
DhanushSantosh
c0ea1c736a fix(backlog-plan): clear running details and handle plan cleanup safely
- Add setRunningDetails(null) in finally block of generate handler to prevent state leaks
- Move clearBacklogPlan before response in apply handler and wrap in try-catch to prevent errors after headers sent

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:37:21 +05:30
DhanushSantosh
8b448b9481 fix: address CodeRabbit security and validation issues in Fedora docs and backlog plan
Documentation improvements:
- Fix GitHub URL placeholder issues in install-fedora.md - GitHub /latest/download/ endpoint
  doesn't support version substitution, use explicit download URL pattern instead
- Improve security in network troubleshooting section:
  - Change ping target from claude.ai (marketing site) to api.anthropic.com (actual API)
  - Remove unsafe 'echo \$ANTHROPIC_API_KEY' command that exposes secrets in shell history
  - Use safe API key check with conditional output instead

Code improvements:
- apps/server/src/routes/backlog-plan/common.ts: Add Array.isArray() validation
  for stored plan shape before returning it. Ensures changes is actually an array,
  not just truthy, preventing downstream runtime errors.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:37:21 +05:30
DhanushSantosh
12f2b9f2b3 fix: remove invalid license property from RPM configuration
The 'license' property is not supported by electron-builder's RPM schema.
Valid RPM properties are: afterInstall, afterRemove, appArmorProfile,
artifactName, category, compression, depends, description, desktop,
executableArgs, fpm, icon, maintainer, mimeTypes, packageCategory,
packageName, publish, synopsis, vendor.

This fix allows electron-builder to proceed to the RPM build stage.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:37:21 +05:30
DhanushSantosh
017ff3ca0a fix: resolve TypeScript error in backlog plan loading
Fix type mismatch in loadBacklogPlan where secureFs.readFile with 'utf-8'
encoding returns union type string | Buffer, causing JSON.parse to fail type checking.
Cast raw to string to satisfy TypeScript strict mode.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 19:37:21 +05:30
Seonfx
bcec178bbe fix: add JSON fallback for spec generation with custom API endpoints
Fixes spec generation failure when using custom API endpoints (e.g., GLM proxy)
that don't support structured output. The AI returns JSON instead of XML, but
the fallback parser only looked for XML tags.

Changes:
- escapeXml: Handle undefined/null values gracefully (converts to empty string)
- generate-spec: Add JSON extraction fallback when XML tags aren't found
  - Reuses existing extractJson() utility (already used for Cursor models)
  - Converts extracted JSON to XML using specToXml()

Closes #510

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-16 08:37:53 -04:00
Jay Zhou
e3347c7b9c feat: add TUI launcher script for easy app startup
Add a beautiful terminal user interface (TUI) script that provides an
interactive menu for launching Automaker in different modes:

- [1] Web Browser mode (localhost:3007)
- [2] Desktop App (Electron)
- [3] Desktop + Debug (Electron with DevTools)
- [Q] Exit

Features:
- ASCII art logo with gradient colors
- Centered, responsive layout that adapts to terminal size
- Animated spinner during launch sequence
- Cross-shell compatibility (bash/zsh)
- Clean exit handling with cursor restoration

This provides a more user-friendly alternative to remembering
npm commands, especially for new users getting started with
the project.
2026-01-16 03:34:47 -08:00
DhanushSantosh
6529446281 feat: add Fedora/RHEL RPM package support with comprehensive documentation
Add native RPM package building for Fedora-based distributions:
- Extend electron-builder configuration to include RPM target
- Add rpm-build installation to GitHub Actions CI/CD workflow
- Update artifact upload patterns to include .rpm files
- Declare proper RPM dependencies (gtk3, libnotify, nss, etc.)
- Use xz compression for optimal package size

Documentation:
- Update README.md with Fedora/RHEL installation instructions
- Create comprehensive docs/install-fedora.md guide covering:
  - Installation methods (dnf/yum, direct URL)
  - System requirements and capabilities
  - Configuration and troubleshooting
  - SELinux handling and firewall rules
  - Performance tips and security considerations
  - Building from source
- Support for Fedora 39+, RHEL 9+, Rocky Linux, AlmaLinux

End-to-end support enables Fedora users to install Automaker via:
  sudo dnf install ./Automaker-<version>-x86_64.rpm

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-16 12:31:30 +05:30
webdevcody
379551c40e feat: add JSON import/export functionality in settings view
- Introduced a new ImportExportDialog component for managing settings import and export via JSON.
- Integrated JsonSyntaxEditor for editing JSON settings with syntax highlighting.
- Updated SettingsView to include the import/export dialog and associated state management.
- Enhanced SettingsHeader with an import/export button for easy access.

These changes aim to improve user experience by allowing seamless transfer of settings between installations.
2026-01-16 00:34:59 -05:00
webdevcody
7465017600 feat: implement server logging and event hook features
- Introduced server log level configuration and HTTP request logging settings, allowing users to control the verbosity of server logs and enable or disable request logging at runtime.
- Added an Event Hook Service to execute custom actions based on system events, supporting shell commands and HTTP webhooks.
- Enhanced the UI with new sections for managing server logging preferences and event hooks, including a dialog for creating and editing hooks.
- Updated global settings to include server log level and request logging options, ensuring persistence across sessions.

These changes aim to improve debugging capabilities and provide users with customizable event-driven actions within the application.
2026-01-16 00:21:49 -05:00
webdevcody
874c5a36de feat: enable auto loading of ClaudeMd by default
- Updated the default setting for autoLoadClaudeMd from false to true in the global settings. This change aims to enhance user experience by automatically loading ClaudeMd, streamlining the workflow for users.
2026-01-15 23:54:08 -05:00
webdevcody
03436103d1 feat: implement backlog plan management and UI enhancements
- Added functionality to save, clear, and load backlog plans within the application.
- Introduced a new API endpoint for clearing saved backlog plans.
- Enhanced the backlog plan dialog to allow users to review and apply changes to their features.
- Integrated dependency management features in the UI, allowing users to select parent and child dependencies for features.
- Improved the graph view with options to manage plans and visualize dependencies effectively.
- Updated the sidebar and settings to include provider visibility toggles for better user control over model selection.

These changes aim to enhance the user experience by providing robust backlog management capabilities and improving the overall UI for feature planning.
2026-01-15 22:21:46 -05:00
Shirone
cb544e0011 Merge pull request #505 from AutoMaker-Org/feature/v0.12.0rc-1768509532254-tt6z
fix: "Remove Project" button not working on right click of the project
2026-01-15 22:05:24 +00:00
Shirone
df23c9e6ab Merge pull request #507 from AutoMaker-Org/feat/improve-ideation-view
feat: improve ideation view
2026-01-15 22:03:50 +00:00
Shirone
52cc82fb3f feat: enhance ideation dashboard and prompt components
- Added a helper function to map priority levels to badge variants in the IdeationDashboard.
- Improved UI elements in SuggestionCard for better spacing and visual hierarchy.
- Updated PromptCategoryGrid and PromptList components with enhanced hover effects and layout adjustments for a more responsive design.
- Refined button styles and interactions for better user experience across components.

These changes aim to improve the overall usability and aesthetics of the ideation view.
2026-01-15 23:01:12 +01:00
Shirone
d9571bfb8d Merge pull request #506 from AutoMaker-Org/feature/v0.12.0rc-1768509904121-pjft
feat: add discard all functionality to ideation view
2026-01-15 21:39:54 +00:00
Shirone
07d800b589 feat: add discard all functionality to ideation view
- Introduced a new button in the IdeationHeader for discarding all ideas when in dashboard mode.
- Implemented state management for discard readiness and count in IdeationView.
- Added confirmation dialog for discarding ideas in IdeationDashboard.
- Enhanced bulk action readiness checks to include discard operations.

This update improves user experience by allowing bulk discarding of ideas with confirmation, ensuring actions are intentional.
2026-01-15 22:37:26 +01:00
Shirone
ec042de69c fix: streamline context menu behavior for project removal dialog
- Ensure the context menu closes consistently after the confirmation dialog, regardless of user action.
- Reset confirmation state upon dialog closure to prevent unintended interactions.
2026-01-15 22:20:30 +01:00
Shirone
585ae32c32 fix: Prevent race condition in project removal dialog cleanup 2026-01-15 22:15:16 +01:00
Shirone
a89ba04109 fix: project removal not being executed
- Prevent context menu from closing when a confirmation dialog is open.
- Add success toast notification upon project removal.
- Refactor event handlers to account for dialog state, improving user experience.
2026-01-15 22:06:35 +01:00
Shirone
05a3b95d75 Merge pull request #501 from AutoMaker-Org/feature/v0.11.0rc-1768426435282-1ogl
feat: centralize prompts and add customization UI for App Spec, Context, Suggestions, Tasks
2026-01-15 20:20:56 +00:00
Shirone
0e269ca15d fix: update outdated server unit tests
- auto-mode-service-planning.test.ts: Add taskExecutionPrompts argument
  to buildFeaturePrompt calls, update test for implementation instructions
- claude-usage-service.test.ts: Skip deprecated Mac tests (service now
  uses PTY for all platforms), rename Windows tests to PTY tests, update
  to use process.cwd() instead of home directory
- claude-provider.test.ts: Add missing model parameter to environment
  variable passthrough tests

All tests now pass (1093 passed, 23 skipped).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:16:46 +01:00
Shirone
fd03cb4afa refactor: split prompt customization into multiple files
Split prompt-customization-section.tsx into focused modules:
- types.ts (51 lines) - Type definitions
- tab-configs.ts (448 lines) - Configuration data for all tabs
- components.tsx (159 lines) - Reusable Banner, PromptField, PromptFieldList
- prompt-customization-section.tsx (176 lines) - Main component

Benefits:
- Main component reduced from ~810 to 176 lines
- Clear separation of concerns
- Easier to find and modify specific parts
- Configuration data isolated for easy updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:07:38 +01:00
Shirone
d6c5c93fe5 refactor: use data-driven configuration for prompt customization UI
- Replace repetitive JSX with TAB_CONFIGS array defining all tabs and fields
- Create reusable Banner component for info/warning banners
- Create PromptFieldList component for rendering fields from config
- Support nested sections (like Auto Mode's Template Prompts section)
- Reduce file from ~950 lines to ~810 lines (-15% code)

Benefits:
- Adding new prompt tabs/fields is now declarative (just add to config)
- Consistent structure enforced by TypeScript interfaces
- Much easier to maintain and extend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:03:51 +01:00
Shirone
1abf219230 refactor: create reusable PromptTabContent component and add {{count}} placeholder
- Create PromptTabContent reusable component in prompt-customization-section.tsx
- Update all tabs (Agent, Commit Message, Title Generation, Ideation, App Spec,
  Context Description, Suggestions, Task Execution) to use the new component
- Add {{count}} placeholder to DEFAULT_SUGGESTIONS_SYSTEM_PROMPT for dynamic
  suggestion count

Addresses PR review comments from Gemini.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 21:00:32 +01:00
Shirone
3a2ba6dbfe feat: connect Task Execution prompts to auto-mode-service
Update auto-mode-service.ts to use centralized Task Execution prompts
from settings, making all 9 task execution prompts customizable via UI:

- buildFeaturePrompt: uses implementationInstructions and
  playwrightVerificationInstructions from settings
- buildTaskPrompt: uses taskPromptTemplate with variable substitution
- buildPipelineStepPrompt: updated to pass prompts through
- executeFeatureWithContext: uses resumeFeatureTemplate
- resolvePlanApproval recovery: uses continuationAfterApprovalTemplate
- Multi-agent continuation: uses continuationAfterApprovalTemplate
- recordLearningsFromFeature: uses learningExtractionSystemPrompt
  and learningExtractionUserPromptTemplate

All 12 prompt categories are now fully customizable from the UI.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 20:54:26 +01:00
Shirone
8fa8ba0a16 fix: address PR comments and complete prompt centralization
- Fix inline type imports in defaults.ts (move to top-level imports)
- Update ideation-service.ts to use centralized prompts from settings
- Update generate-title.ts to use centralized prompts
- Update validate-issue.ts to use centralized prompts
- Clean up validation-schema.ts (prompts already centralized)
- Minor server index cleanup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 20:31:19 +01:00
Shirone
285f526e0c feat: centralize prompts and add customization UI for App Spec, Context, Suggestions, Tasks
- Add 4 new prompt type interfaces (AppSpecPrompts, ContextDescriptionPrompts,
  SuggestionsPrompts, TaskExecutionPrompts) with resolved types
- Add default prompts for all new categories to @automaker/prompts/defaults.ts
- Add merge functions for new prompt categories in merge.ts
- Update settings-helpers.ts getPromptCustomization() to return all 12 categories
- Update server routes (generate-spec, generate-features-from-spec, describe-file,
  describe-image, generate-suggestions) to use centralized prompts
- Add 4 new tabs in prompt customization UI (App Spec, Context, Suggestions, Tasks)
- Fix Ideation tab layout using grid-cols-4 for even distribution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 20:13:14 +01:00
webdevcody
bd68b497ac Merge branch 'v0.12.0rc' of github.com:AutoMaker-Org/automaker into v0.12.0rc 2026-01-15 13:14:19 -05:00
webdevcody
06b047cfcb feat: implement bulk feature verification and enhance selection mode
- Added functionality for bulk verifying features in the BoardView, allowing users to mark multiple features as verified at once.
- Introduced a selection target mechanism to differentiate between 'backlog' and 'waiting_approval' features during selection mode.
- Updated the KanbanCard and SelectionActionBar components to support the new selection target logic, improving user experience for bulk actions.
- Enhanced the UI to provide appropriate actions based on the current selection target, including verification options for waiting approval features.
2026-01-15 13:14:15 -05:00
DhanushSantosh
c585cee12f feat: add dynamic usage status icon and tab-aware updates to usage button
- Add provider icon (Anthropic/OpenAI) that displays based on active tab
- Icon color reflects usage status (green/orange/red)
- Progress bar and stale indicator update dynamically when switching tabs
- Shows Claude metrics when Claude tab is active, Codex metrics when Codex tab is active

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-15 21:05:35 +05:30
DhanushSantosh
241fd0b252 Merge remote-tracking branch 'upstream/v0.12.0rc' into patchcraft 2026-01-15 19:38:37 +05:30
DhanushSantosh
164acc1b4e chore: update package-lock.json 2026-01-15 19:38:18 +05:30
webdevcody
78e5ddb4a8 chore: release v0.11.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:34:37 -05:00
Web Dev Cody
43904cdb02 Merge pull request #475 from AutoMaker-Org/v0.11.0rc
V0.11.0rc
2026-01-14 20:31:11 -05:00
Shirone
7ea1383e10 Merge pull request #492 from AutoMaker-Org/feature/v0.11.0rc-1768413895104-31pa
feat: merge worktree to main in dropdown menu
2026-01-14 22:10:02 +00:00
Shirone
425e38811f Merge pull request #493 from AutoMaker-Org/feature/v0.11.0rc-1768413909856-a0al
fix: agent output modal ui/ux and task list with spec/full plan mode
2026-01-14 21:49:47 +00:00
Shirone
f6bda66ed4 feat: enhance agent info panel with real-time task status updates and fresh planSpec integration
- Added support for real-time task status updates using WebSocket events, allowing the Kanban card to reflect current task progress accurately.
- Introduced a new state for fresh planSpec data fetched from the API to ensure the agent info panel displays up-to-date task information.
- Updated the effectiveTodos calculation to prioritize fresh planSpec data and incorporate real-time status, improving task display accuracy.
- Enhanced the logic to listen for relevant WebSocket events and update task statuses accordingly, ensuring synchronization with the agent output modal.
2026-01-14 22:39:47 +01:00
Web Dev Cody
0df7e4a33d Merge pull request #490 from thesobercoder/fix/openrouter-models-kanban
fix: load OpenCode models on Kanban
2026-01-14 16:12:45 -05:00
Web Dev Cody
41ad717b8e Merge pull request #494 from mcbodge/feature/add-copilot-support
Fix OpenCode GitHub Copilot authentication detection
2026-01-14 16:11:38 -05:00
Manuel Grillo
fec5f88d91 feat: add GitHub Copilot support for OAuth token validation 2026-01-14 21:44:33 +01:00
Shirone
724858d215 fix: adjust more 2026-01-14 21:12:48 +01:00
Shirone
2b93afbd43 Changes from feature/v0.11.0rc-1768413909856-a0al 2026-01-14 21:03:54 +01:00
Shirone
ca0f3ecedf fix: adjust task progress panel height and improve effective todos handling in agent info panel
- Reduced the maximum height of the task progress panel from 300px to 200px for better UI consistency.
- Introduced a new `effectiveTodos` calculation in the agent info panel to correctly display tasks from `planSpec` when available, ensuring accurate task counts and statuses.
- Updated references to use `effectiveTodos` instead of the original `agentInfo.todos` for task display logic in the agent info panel.
- Adjusted the height of various modal components to align with the new task progress panel height.
2026-01-14 21:02:24 +01:00
Shirone
ee0d0c6c59 fix: merge worktree handler now uses correct branch name and path
The merge handler previously hardcoded branch names as `feature/${featureId}`
and worktree paths as `.worktrees/${featureId}`, which failed for auto-generated
branches (e.g., `feature/v0.11.0rc-1768413895104-31pa`) and custom worktrees.

Changes:
- Server handler now accepts branchName and worktreePath directly from the UI
- Added branch existence validation before attempting merge
- Updated merge dialog with 2-step confirmation (type "merge" to confirm)
- Removed feature branch naming restriction - any branch can now be merged
- Updated API types and client to pass correct parameters

Closes #408

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:49:17 +01:00
Soham Dasgupta
ac38e85f3c Merge branch 'v0.11.0rc' into fix/openrouter-models-kanban 2026-01-15 00:01:00 +05:30
Shirone
ca3286a374 Merge pull request #489 from AutoMaker-Org/feature/v0.11.0rc-1768410827235-36uf
feat: enhance pr dialog base branch selection
2026-01-14 17:39:27 +00:00
Shirone
0898578c11 fix: Include remote branches in PR base selection even when local branch exists
The branch listing logic now correctly shows remote branches (e.g., "origin/main") even if a local branch with the same base name exists, since users need remote branches as PR base targets. Also extracted duplicate state reset logic in create-pr-dialog into a reusable function.
2026-01-14 18:36:14 +01:00
Shirone
07593f8704 feat: enhance list-branches endpoint to support fetching remote branches
- Updated the list-branches endpoint to accept an optional parameter for including remote branches.
- Implemented logic to fetch and deduplicate remote branches alongside local branches.
- Modified the CreatePRDialog component to utilize the updated API for branch selection, allowing users to select from both local and remote branches.
2026-01-14 18:25:31 +01:00
Shirone
3f8a8db7a5 Merge pull request #486 from AutoMaker-Org/fix/claude-usage-parsing
fix: Claude usage parsing for CLI v2.x and trust prompt handling
2026-01-14 16:56:26 +00:00
Shirone
13eead3855 fix: use process.cwd() consistently across all platforms
Address PR review comment - use process.cwd() for Windows too instead of
USERPROFILE/homedir fallback for consistency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:53:13 +01:00
Shirone
cb910feae9 fix: Claude usage parsing for CLI v2.x and trust prompt handling
- Use node-pty on all platforms instead of expect on macOS for more reliable PTY handling
- Use process.cwd() as working directory (project dir is likely already trusted)
- Add detection for new trust prompt text variants ("Ready to code here", "permission to work")
- Add specific error handling for trust prompt pending state
- Show helpful UI message when trust prompt needs manual approval

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:48:44 +01:00
Shirone
c75f9a29cb Merge pull request #485 from AutoMaker-Org/feature/v0.11.0rc-1768406316676-kza4
feat: add feature request github template
2026-01-14 16:18:44 +00:00
Shirone
3c5e453b01 fix: adress pr comments 2026-01-14 17:13:41 +01:00
Shirone
63e0ffac42 Merge pull request #484 from AutoMaker-Org/feature/v0.11.0rc-1768405788678-28bn
fix: adress pr comments task not respecting worktrees
2026-01-14 16:10:15 +00:00
Shirone
d0155f28c8 feat: add feature request github template 2026-01-14 17:07:01 +01:00
Shirone
27ca08d98a fix: Set workMode to custom for PR and conflict flows 2026-01-14 17:02:52 +01:00
Shirone
df99950475 Merge pull request #481 from AutoMaker-Org/feature/v0.11.0rc-1768383713091-hnir
feat(ui): Add project theme selection to context menu
2026-01-14 15:28:18 +00:00
Web Dev Cody
6a85073d94 Merge pull request #339 from ramarivera/feat/custom-anthropic-endpoint
feat: support ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN for custom endpoints
2026-01-14 10:09:56 -05:00
Shirone
7b73ff34f1 fix: adress pr comments 2026-01-14 15:43:42 +01:00
Shirone
8419b12f3f feat(ui): Add project theme selection to context menu with clean code refactoring
Implement per-project theme override capability in the Discord-like layout:
- Add theme submenu to project context menu with live preview
- Reuse existing theme constants and useThemePreview hook from sidebar
- Extract reusable ThemeButton and ThemeColumn components (DRY principle)
- Replace magic z-index values with named constants

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 15:33:51 +01:00
Soham Dasgupta
f1a5bcd17a fix: load OpenRouter models on kanban board without visiting settings
Problem:
- OpenRouter dynamic models only appeared after visiting settings page
- PhaseModelSelector (used in Add/Edit Feature dialogs) only fetched Codex models
- dynamicOpencodeModels remained empty until OpencodeSettingsTab mounted

Solution:
- Add fetchOpencodeModels() action to app-store mirroring fetchCodexModels pattern
- Add state tracking: opencodeModelsLoading, opencodeModelsError, timestamps
- Call fetchOpencodeModels() in PhaseModelSelector useEffect on mount
- Use same caching strategy: 5min success cache, 30sec failure cooldown

Files changed:
- apps/ui/src/store/app-store.ts
  - Add OpenCode model loading state properties
  - Add fetchOpencodeModels action with error handling & caching
- apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx
  - Add opencodeModelsLoading, fetchOpencodeModels to store hook
  - Add useEffect to fetch OpenCode models on mount

Result:
- OpenRouter models now appear in Add/Edit Feature dialogs immediately
- No need to visit settings page first
- Consistent with Codex model loading behavior
2026-01-14 19:46:43 +05:30
Web Dev Cody
28d8a4cc9e Merge pull request #473 from thesobercoder/fix/npm-cache-permissions
fix: ensure npm cache directory has correct permissions
2026-01-14 09:16:30 -05:00
Web Dev Cody
7108cdd2ca Merge pull request #480 from thesobercoder/fix/cli-provider-system-prompt
fix: embed systemPrompt into prompt for CLI-based providers
2026-01-14 09:16:01 -05:00
Soham Dasgupta
e7bfb19203 fix: embed systemPrompt into prompt for CLI-based providers
CLI-based providers (OpenCode, etc.) only accept a single prompt via
stdin/args and don't support separate system/user message channels like
Claude SDK. When systemPrompt is passed to these providers, it was
silently dropped, causing:

- BacklogPlan JSON parsing failures with OpenCode/GPT-5.2 (missing
  "output ONLY JSON" formatting instruction)
- Loss of critical formatting/schema instructions for structured outputs

This fix adds embedSystemPromptIntoPrompt() method to CliProvider base
class that:
- Prepends systemPrompt to the user prompt before CLI execution
- Handles both string and array prompts (vision support)
- Handles both string systemPrompt and SystemPromptPreset objects
- Uses standard \n\n---\n\n separator (consistent with codebase)
- Sets systemPrompt to undefined to prevent double-injection

Benefits OpencodeProvider immediately (uses base executeQuery).
CursorProvider still uses manual workarounds (overrides executeQuery).

Fixes the immediate BacklogPlan + OpenCode bug while maintaining
backward compatibility with existing Cursor workarounds.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 19:20:47 +05:30
Shirone
beac823472 Merge pull request #479 from AutoMaker-Org/fix/followup-run
fix: follow up prompts
2026-01-14 08:43:39 +00:00
Shirone
c7fac3d9e6 refactor: Replace worktreePath param with useWorktrees flag 2026-01-14 09:29:38 +01:00
Shirone
3689eb969d Merge pull request #478 from thesobercoder/chore/playwright-docker-prereqs
chore(docker): add Playwright Chromium prerequisites
2026-01-14 07:30:24 +00:00
Soham Dasgupta
5e330b7691 chore(docker): add Playwright Chromium deps
- Add missing system libraries required by Playwright/Chromium in server and dev images\n- Document optional Playwright browser cache volume in docker-compose.override.yml.example
2026-01-14 12:47:46 +05:30
webdevcody
5ec5fe82e6 refactor: Enhance project management features and UI components
- Updated create-pr.ts to improve commit error handling and logging.
- Enhanced project-switcher.tsx with new folder opening functionality and state management for project setup.
- Expanded icon-picker.tsx to include a comprehensive list of icons organized by category.
- Replaced dialog components with popover components for auto mode and plan settings, improving UI responsiveness.
- Refactored board-view components to streamline feature management and enhance user experience.
- Removed outdated dialog components and replaced them with popover alternatives for better accessibility.

These changes aim to improve the overall usability and functionality of the project management interface.
2026-01-13 22:35:45 -05:00
Shirone
ee13bf9a8f Merge pull request #476 from DenyCZ/fix/windows-npx
fix: Resolve windows npx spawn errors
2026-01-14 00:00:32 +00:00
Shirone
219af28afc Merge pull request #477 from AutoMaker-Org/fix/dynamic-branch-references
fix: use dynamic branch references instead of hardcoded origin/main
2026-01-13 23:59:49 +00:00
Shirone
b64025b134 fix: adress pr comments 2026-01-14 00:57:30 +01:00
Shirone
51e4e8489a fix: use dynamic branch references instead of hardcoded origin/main
- Fix handleResolveConflicts to use origin/${worktree.branch} instead of
  hardcoded origin/main for pull and resolve conflicts
- Add defaultBaseBranch prop to CreatePRDialog to use selected branch
- Fix branchCardCounts to use primary worktree branch as default
- Enable PR status and Address PR Comments for main branch tab
- Add automatic PR detection from GitHub for branches without stored metadata

This allows users working on release branches (like v0.11.0rc) to properly
pull from their branch's remote and see PR status for any branch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 00:48:09 +01:00
DenyCZ
bb70d04b88 fix: Resolve windows npx spawn errors 2026-01-14 00:31:04 +01:00
Shirone
32f6c6d6eb Merge pull request #474 from AutoMaker-Org/feat/dev-server-log-panel
feat: add dev server log panel with real-time streaming
2026-01-13 21:04:58 +00:00
Shirone
b6688e630e Merge branch 'v0.11.0rc' into feat/dev-server-log-panel
Resolved conflict in worktree-panel.tsx by combining imports:
- DevServerLogsPanel from this branch
- WorktreeMobileDropdown, WorktreeActionsDropdown, BranchSwitchDropdown from v0.11.0rc

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 22:01:44 +01:00
Shirone
073f6d5793 feat: add dev server log panel with real-time streaming
Add the ability to view dev server logs in a dedicated panel with:
- Real-time log streaming via WebSocket events
- ANSI color support using xterm.js
- Scrollback buffer (50KB) for log history on reconnect
- Output throttling to prevent UI flooding
- "View Logs" option in worktree dropdown menu

Server changes:
- Add scrollback buffer and event emission to DevServerService
- Add GET /api/worktree/dev-server-logs endpoint
- Add dev-server:started, dev-server:output, dev-server:stopped events

UI changes:
- Add reusable XtermLogViewer component
- Add DevServerLogsPanel dialog component
- Add useDevServerLogs hook for WebSocket subscription

Closes #462

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 21:56:35 +01:00
Shirone
9153b06f09 Merge pull request #449 from AutoMaker-Org/feat/mobile-improvements-contributor
feat: Mobile responsiveness improvements from community contributor
2026-01-13 20:48:47 +00:00
Shirone
6cb2af8757 test: update claude-usage-service tests for improved error handling and timeout management
- Modified command arguments in tests to include '--add-dir' for better context.
- Updated error messages for authentication and timeout scenarios to provide clearer guidance.
- Adjusted timer values in tests to align with implementation delays, ensuring accurate simulation of usage data retrieval.
2026-01-13 21:37:16 +01:00
Shirone
ca3b013a7b Merge v0.11.0rc into feat/mobile-improvements-contributor
Resolves merge conflicts by keeping both features:
- enableAiCommitMessages (from our branch)
- defaultFeatureModel (from v0.11.0rc)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 21:31:44 +01:00
Web Dev Cody
abde1ba40a Merge pull request #472 from AutoMaker-Org/claude/issue-469-20260113-1744
feat: Add Discord-like project switcher sidebar with icon support
2026-01-13 14:59:32 -05:00
webdevcody
b04659fb56 Merge branch 'v0.11.0rc' into claude/issue-469-20260113-1744 2026-01-13 14:59:14 -05:00
webdevcody
74ee30d5db refactor: improve code readability and maintainability in SDK options and icon picker
- Reformatted the fullAccess and chat tool presets in sdk-options.ts for better readability.
- Simplified the return statement in icon-picker.tsx for cleaner code.
- Removed the board-background-persistence.spec.ts test file as it is no longer needed.
2026-01-13 14:56:44 -05:00
webdevcody
a300466ca9 feat: enhance project management with custom icon support and UI improvements
- Introduced custom icon functionality for projects, allowing users to upload and manage their own icons.
- Updated Project and ProjectRef types to include customIconPath.
- Enhanced the ProjectSwitcher component to display custom icons alongside preset icons.
- Added EditProjectDialog for inline editing of project details, including icon uploads.
- Improved AppearanceSection to support custom icon uploads and display.
- Updated sidebar and project switcher UI for better user experience and accessibility.

Implements #469
2026-01-13 14:39:19 -05:00
DhanushSantosh
9311f2e62a Merge remote-tracking branch 'upstream/v0.11.0rc' into patchcraft 2026-01-14 00:55:46 +05:30
DhanushSantosh
67245158ea chore: ignore .codex directory 2026-01-14 00:55:22 +05:30
DhanushSantosh
520d9a945c chore: add workflow files to git tracking 2026-01-14 00:54:46 +05:30
DhanushSantosh
fa3ead0e8d feat(auto-mode): skip memory extraction when Claude not configured and add reasoning effort support
- Skip learning extraction when ANTHROPIC_API_KEY is not available
- Add reasoningEffort parameter to simpleQuery for Codex model configuration
- Add stdinData support to spawnProcess for CLI stdin input
- Update UI API types for model override with reasoning support
2026-01-14 00:50:33 +05:30
DhanushSantosh
253ab94646 feat(github): add Codex/OpenCode model support for issue validation
- Support Codex and OpenCode models in issue validation
- Add reasoningEffort parameter for Codex model configuration
- Update validation logic to use structured output for Claude/Codex
- Update UI hooks and types for multi-provider model selection
2026-01-14 00:50:02 +05:30
DhanushSantosh
fbb3f697e1 feat(settings): add OpenAI/Google API key support and unified ModelId type
- Add OpenAI API key storage to store-api-key handler
- Include Google/OpenAI key status in credentials API responses
- Add unified ModelId type for Claude, Codex, Cursor, OpenCode, and dynamic providers
- Update PhaseModelEntry to support all provider model types
2026-01-14 00:49:35 +05:30
Soham Dasgupta
1a1517dffb fix: ensure npm cache directory has correct permissions
Fix EACCES permission error when running npx commands (e.g., MCP servers)
inside the Docker container.

Error that was occurring:
  npm error code EACCES
  npm error syscall mkdir
  npm error path /home/automaker/.npm/_cacache/index-v5/1f/fc
  npm error errno EACCES
  npm error Your cache folder contains root-owned files, due to a bug in
  npm error previous versions of npm which has since been addressed.

The fix ensures the /home/automaker/.npm directory exists and has correct
ownership before switching to the automaker user in the entrypoint script.
2026-01-14 00:49:28 +05:30
DhanushSantosh
690cf1f281 fix(codex-provider): use SDK mode when API key is present to avoid OAuth failures
When an OpenAI API key is stored in settings or environment, use SDK mode
instead of CLI mode. This bypasses the MCP transport layer which was
failing with 'TokenRefreshFailed' errors due to OAuth token issues.

The SDK uses the API key directly via @openai/codex-sdk, avoiding the
OAuth token refresh mechanism that was causing mid-execution failures.
2026-01-14 00:45:01 +05:30
Shirone
6f55da46ac Merge pull request #471 from AutoMaker-Org/fix/dev-server-url
fix: use browser hostname for dev server URLs instead of localhost
2026-01-13 18:46:17 +00:00
webdevcody
57453966ac Merge branch 'v0.11.0rc' of github.com:AutoMaker-Org/automaker into v0.11.0rc 2026-01-13 13:45:35 -05:00
webdevcody
298acc9f89 style: add overflow-y-auto class to dialog content containers
- Updated the BacklogPlanDialog, AddEditServerDialog, CreateSpecDialog, and RegenerateSpecDialog components to include the overflow-y-auto class for improved scrolling behavior in dialog content.
2026-01-13 13:45:29 -05:00
Shirone
f4390bc82f security: add noopener,noreferrer to window.open calls
Add 'noopener,noreferrer' parameter to all window.open() calls with
target='_blank' to prevent tabnabbing attacks. This prevents the newly
opened page from accessing window.opener, protecting against potential
security vulnerabilities.

Affected files:
- use-dev-servers.ts: Dev server URL links
- worktree-actions-dropdown.tsx: PR URL links
- create-pr-dialog.tsx: PR creation and browser fallback links

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 19:43:20 +01:00
Shirone
62af2031f6 feat: enhance dev server URL handling and improve accessibility
- Added URL and URLSearchParams as readonly globals in ESLint configuration.
- Updated WorktreeActionsDropdown and WorktreeTab components to include aria-labels for better accessibility.
- Implemented error handling for dev server URL opening, ensuring only valid HTTP/HTTPS protocols are used and providing user feedback for errors.

These changes improve user experience and accessibility when interacting with the dev server functionality.
2026-01-13 19:33:09 +01:00
claude[bot]
0ddd672e0e feat: Add Discord-like project switcher sidebar with icon support
- Add project icon field to ProjectRef and Project types
- Create vertical project switcher sidebar component
  - Project icons with hover tooltips
  - Active project highlighting
  - Plus button to create new projects
  - Right-click context menu for edit/delete
- Add IconPicker component with 35+ Lucide icons
- Add EditProjectDialog for inline project editing
- Update settings appearance section with project details editor
- Add setProjectIcon and setProjectName actions to app store
- Integrate ProjectSwitcher in root layout (shows on app pages only)

Implements #469

Co-authored-by: Web Dev Cody <webdevcody@users.noreply.github.com>
2026-01-13 17:53:15 +00:00
Shirone
7ef525effa fix: clarify comments on branch name handling in BoardView
Updated comments in BoardView to better explain the behavior of the 'current' work mode. The changes specify that an empty string clears the branch assignment, allowing work to proceed on the main/current branch. This enhances code readability and understanding of branch management logic.
2026-01-13 18:51:20 +01:00
Shirone
2303dcd133 Merge pull request #468 from AutoMaker-Org/feat/add-branch-name-to-mass-edit
feat: add branch/worktree support to mass edit dialog
2026-01-13 17:43:25 +00:00
Shirone
cc4f39a6ab chore: fix formatting issues for CI
Fix Prettier formatting in two files:
- apps/server/src/lib/sdk-options.ts: Split long arrays to one item per line
- docs/docker-isolation.md: Align markdown table columns

Resolves CI format check failures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 18:38:09 +01:00
Shirone
d4076ad0ce refactor: address CodeRabbit PR feedback
Improvements based on CodeRabbit review comments:

1. Use getPrimaryWorktreeBranch for consistent branch detection
   - Replace hardcoded 'main' fallback with getPrimaryWorktreeBranch()
   - Ensures auto-generated branch names respect the repo's actual primary branch
   - Handles repos using 'master' or other primary branch names

2. Extract worktree auto-selection logic to helper function
   - Create addAndSelectWorktree helper to eliminate code duplication
   - Use helper in both onWorktreeAutoSelect and handleBulkUpdate
   - Reduces maintenance burden and ensures consistent behavior

These changes improve code consistency and maintainability without affecting functionality.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 18:37:26 +01:00
Shirone
3bd8626d48 feat: add branch/worktree support to mass edit dialog
Implement worktree creation and branch assignment in the mass edit dialog to match the functionality of the add-feature and edit-feature dialogs.

Changes:
- Add WorkModeSelector to mass-edit-dialog.tsx with three modes:
  - 'Current Branch': Work on current branch (no worktree)
  - 'Auto Worktree': Auto-generate branch name and create worktree
  - 'Custom Branch': Use specified branch name and create worktree
- Update handleBulkUpdate in board-view.tsx to:
  - Accept workMode parameter
  - Create worktrees for 'auto' and 'custom' modes
  - Auto-select created worktrees in the board header
  - Handle branch name generation for 'auto' mode
- Add necessary props to MassEditDialog (branchSuggestions, branchCardCounts, currentBranch)

Users can now bulk-assign features to a branch and automatically create/select worktrees, enabling efficient project setup with many features.

Fixes #459

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 18:27:22 +01:00
webdevcody
ff5915dd20 refactor: update terminology in board view components
- Renamed "Worktrees" to "Worktree Bar" in the BoardHeader component for clarity.
- Updated comments and labels in AddFeatureDialog, PlanSettingsDialog, and WorktreeSettingsDialog to reflect the new terminology and improve user understanding of worktree mode functionality.
2026-01-13 12:19:24 -05:00
webdevcody
8500f71565 Merge branches 'v0.11.0rc' and 'v0.11.0rc' of github.com:AutoMaker-Org/automaker into v0.11.0rc 2026-01-13 12:05:57 -05:00
Shirone
81bab1d8ab Merge pull request #465 from thesobercoder/fix/opencode-cache-volume
fix: add OpenCode cache volume for version file persistence
2026-01-13 15:22:52 +00:00
Soham Dasgupta
24a6633322 fix: add OpenCode cache volume for version file persistence
OpenCode stores a version file in ~/.cache/opencode/ which was causing
EACCES permission errors. This adds:

- Volume mount for ~/.cache/opencode
- Entrypoint script to set correct ownership/permissions on the cache directory
2026-01-13 20:45:33 +05:30
DhanushSantosh
f073f6ecc3 Merge remote-tracking branch 'upstream/v0.11.0rc' into patchcraft 2026-01-13 20:28:49 +05:30
Web Dev Cody
2870ddb223 Merge pull request #460 from thesobercoder/feature/opencode-docker-support
feat: add OpenCode CLI support in Docker
2026-01-13 09:35:28 -05:00
Web Dev Cody
1578d02e70 Merge branch 'v0.11.0rc' into feature/opencode-docker-support 2026-01-13 09:35:18 -05:00
webdevcody
bb710ada1a feat: enhance settings view and feature defaults management
- Introduced default feature model settings in the settings view, allowing users to specify the default AI model for new feature cards.
- Updated navigation to include a direct link to model defaults in the settings menu.
- Enhanced the Add Feature dialog to utilize the default feature model from the app store.
- Implemented synchronization of the default feature model in settings migration and sync hooks.
- Improved UI components to reflect changes in default settings, ensuring a cohesive user experience.
2026-01-13 20:04:36 +05:30
Soham Dasgupta
33ae860059 feat: update Docker volumes for OpenCode CLI data and user configuration 2026-01-13 20:01:22 +05:30
webdevcody
3de6d58af3 Merge branch 'v0.11.0rc' of github.com:AutoMaker-Org/automaker into v0.11.0rc 2026-01-13 09:30:20 -05:00
webdevcody
c8e66a866e feat: enhance settings view and feature defaults management
- Introduced default feature model settings in the settings view, allowing users to specify the default AI model for new feature cards.
- Updated navigation to include a direct link to model defaults in the settings menu.
- Enhanced the Add Feature dialog to utilize the default feature model from the app store.
- Implemented synchronization of the default feature model in settings migration and sync hooks.
- Improved UI components to reflect changes in default settings, ensuring a cohesive user experience.
2026-01-13 09:30:15 -05:00
DhanushSantosh
c25efdc0d8 Revert "Wire provider model enablement into selectors"
This reverts commit 8f1740c0f5.
2026-01-13 19:54:20 +05:30
DhanushSantosh
bde82492ae Revert "Sync branch state"
This reverts commit 6704293cb1.
2026-01-13 19:54:05 +05:30
Soham Dasgupta
67f18021c3 feat: add OpenCode CLI config path to Docker example 2026-01-13 19:50:54 +05:30
DhanushSantosh
6704293cb1 Sync branch state 2026-01-13 16:58:20 +05:30
DhanushSantosh
8f1740c0f5 Wire provider model enablement into selectors 2026-01-13 16:53:17 +05:30
Soham Dasgupta
62019d5916 feat: add OpenCode CLI support in Docker
- Install OpenCode CLI in Dockerfile alongside Claude and Cursor
- Add automaker-opencode-config volume for persisting auth
- Add OpenCode directory setup in docker-entrypoint.sh
- Update docker-isolation.md with OpenCode documentation
- Add OpenCode bind mount example to docker-compose.override.yml.example
2026-01-13 14:14:56 +05:30
DhanushSantosh
e66283b1d6 Merge remote-tracking branch 'upstream/main' into patchcraft 2026-01-13 13:45:17 +05:30
Web Dev Cody
a0d6d76626 Merge pull request #448 from comzine/fix/docker-uid-gid-configurable
fix: make Docker container UID/GID configurable
2026-01-13 00:00:47 -05:00
Web Dev Cody
c2f5c07038 Merge pull request #344 from casiusss/fix/pipeline-resume-edge-cases
fix: handle pipeline resume edge cases and improve robustness
2026-01-12 23:57:27 -05:00
webdevcody
419abf88dd Merge branch 'v0.11.0rc' into fix/pipeline-resume-edge-cases 2026-01-12 23:49:33 -05:00
Web Dev Cody
b7596617ed Merge pull request #455 from stefandevo/fix/codex-infinite-loop
fix(codex): prevent infinite loop when fetching models on settings screen
2026-01-12 23:46:02 -05:00
Web Dev Cody
26da99e834 Merge pull request #454 from AutoMaker-Org/abstract-anthropic-sdk
feat: implement simple query service and enhance provider abstraction
2026-01-12 23:40:42 -05:00
webdevcody
2b33a0d322 refactor: integrate simple query service into auto mode
- Replaced dynamic import of the query function with a call to the new Simple Query Service for improved clarity and maintainability.
- Streamlined the response handling by directly utilizing the result from the simple query, enhancing code readability.
- Updated the prompt and options structure to align with the new service's requirements, ensuring consistent behavior in learning extraction.
2026-01-12 21:39:39 -05:00
webdevcody
c796adbae8 test: update project view tests for dashboard integration
- Modified tests to navigate directly to the dashboard instead of the welcome view, ensuring a smoother project selection process.
- Updated project name verification to check against the sidebar button instead of multiple elements.
- Added logic to expand the sidebar if collapsed, improving visibility for project names during tests.
- Adjusted test assertions to reflect changes in the UI structure, including the introduction of the dashboard view.
2026-01-12 21:23:33 -05:00
Stefan de Vogelaere
18d82b1bb1 refactor: improve time constant readability
- Rename FAILURE_COOLDOWN to FAILURE_COOLDOWN_MS with explicit calculation
- Add SUCCESS_CACHE_MS constant to replace magic number 300000
- Use multiplication (30 * 1000, 5 * 60 * 1000) to make units explicit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 00:53:39 +01:00
webdevcody
0c68fcc8c8 chore: update node-gyp dependency URL in package-lock.json
- Changed the resolved URL for the @electron/node-gyp module from SSH to HTTPS for improved accessibility and compatibility.
2026-01-12 18:51:03 -05:00
Stefan de Vogelaere
e4458b8222 fix(codex): prevent infinite loop when fetching models on settings screen
When Codex is not connected/authenticated, the /api/codex/models endpoint
returns 503. The fetchCodexModels function had no cooldown after failures,
causing infinite retries when navigating to the Settings screen.

Added codexModelsLastFailedAt state to track failed fetch attempts and
skip retries for 30 seconds after a failure. This prevents the infinite
loop while still allowing periodic retry attempts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 00:47:51 +01:00
webdevcody
eb8ebe3ce0 feat: implement simple query service and enhance provider abstraction
- Introduced a new Simple Query Service to streamline basic AI queries, allowing for structured JSON outputs.
- Updated existing routes to utilize the new service, replacing direct SDK calls with a unified interface for querying.
- Enhanced provider handling in various routes, including generate-spec, generate-features-from-spec, and validate-issue, to support both Claude and Cursor models seamlessly.
- Added structured output support for improved response handling and error management across the application.
2026-01-12 17:33:54 -05:00
Web Dev Cody
0dc70addb6 Merge pull request #424 from comzine/fix/add-todowrite-to-allowed-tools
fix: add TodoWrite to allowed tools in SDK presets
2026-01-12 16:30:56 -05:00
webdevcody
f3f5d05349 chore: release v0.10.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 16:16:44 -05:00
Web Dev Cody
0c4b833b07 Merge pull request #405 from AutoMaker-Org/v0.10.0rc
V0.10.0rc
2026-01-12 16:11:56 -05:00
Shirone
029c5ca855 fix: adress pr comments 2026-01-12 22:03:14 +01:00
Shirone
1f270edbe1 refactor(list-row): remove priority display logic and related components
- Eliminated the getPriorityDisplay function and the PriorityBadge component from the ListRow implementation.
- Removed the pipelineConfig prop from ListRowProps interface.
- Cleaned up the code to streamline the ListRow component, focusing on essential features.
2026-01-12 21:46:04 +01:00
Shirone
47c188d8f9 fix: adress pr comments
- Added validation to check if the specified worktree path exists before generating commit messages.
- Implemented a check to ensure the worktree path is a valid git repository by verifying the presence of the .git directory.
- Improved error handling by returning appropriate responses for invalid paths and non-git repositories.
2026-01-12 21:41:55 +01:00
Shirone
cca4638b71 fix: adjust pr commnets 2026-01-12 21:21:24 +01:00
Shirone
19c12b7813 refactor(settings): remove deprecated notification settings from GlobalSettings interface
- Eliminated unused properties related to notification commands and ntfy.sh integration from the GlobalSettings interface.
- Updated default global settings to reflect the removal of these properties.
2026-01-12 20:58:58 +01:00
Shirone
0261ec2892 feat(prompts): implement customizable commit message prompts
- Added a new section in the UI for customizing commit message prompts.
- Integrated a system prompt for AI-generated commit messages, allowing users to define their own instructions.
- Updated the backend to merge custom prompts with default settings for commit message generation.
- Enhanced the commit message generation logic to utilize the effective system prompt based on user settings.
2026-01-12 20:55:01 +01:00
Shirone
5e4f5f86cd feat(worktree): add AI commit message generation feature
- Implemented a new endpoint to generate commit messages based on git diffs.
- Updated worktree routes to include the AI commit message generation functionality.
- Enhanced the UI to support automatic generation of commit messages when the commit dialog opens, based on user settings.
- Added settings for enabling/disabling AI-generated commit messages and configuring the model used for generation.
2026-01-12 20:55:01 +01:00
DhanushSantosh
fbab1d323f test: align app-spec and enhancement mode tests 2026-01-13 00:11:11 +05:30
eclipxe
8b19266c9a feat: Add secondary inline actions for waiting_approval status 2026-01-12 19:39:12 +01:00
anonymous
1b9d194dd1 feat: Improve mobile scrolling experience in autocomplete and dropdown components 2026-01-12 19:39:12 +01:00
anonymous
74c793b6c6 Add branch switch to mobile worktree panel 2026-01-12 19:39:12 +01:00
anonymous
d1222268c3 feat: Improve Claude CLI usage detection, mobile usage view, and add provider auth initialization 2026-01-12 19:39:12 +01:00
anonymous
df7a0f8687 feat: Make input controls and settings responsive for mobile devices 2026-01-12 19:37:36 +01:00
anonymous
c7def000df feat: Handle backlog feature editing on row click in board view 2026-01-12 19:37:35 +01:00
anonymous
e2394244f6 feat: Implement responsive mobile header layout with menu consolidation 2026-01-12 19:37:35 +01:00
anonymous
007830ec74 feat: Add responsive session manager with mobile backdrop overlay 2026-01-12 19:33:33 +01:00
anonymous
f721eb7152 List View Features 2026-01-12 19:33:33 +01:00
anonymous
e56db2362c feat: Add AI-generated commit messages
Integrate Claude Haiku to automatically generate commit messages when
committing worktree changes. Shows a sparkle animation while generating
and auto-populates the commit message field.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 19:32:20 +01:00
anonymous
d2c7a9e05d Fix model selector on mobile 2026-01-12 19:32:20 +01:00
anonymous
acce06b304 Fix sidebar lables not showign up 2026-01-12 19:32:20 +01:00
anonymous
4ab54270db fix: enable sidebar expand and project switching on mobile
- Sidebar now uses overlay pattern on mobile (fixed position when open)
- Added backdrop overlay that dismisses sidebar on tap
- Made collapse toggle button visible on all screen sizes
- Made project options menu visible on all screen sizes

Previously the sidebar was forced to collapsed width (w-16) on mobile
even when sidebarOpen was true, and the toggle/options buttons were
hidden with `hidden lg:flex`.
2026-01-12 19:31:38 +01:00
Shirone
f50520c93f feat(delete): enhance branch deletion handling and validation
- Introduced a flag to track if a branch was successfully deleted, improving response clarity.
- Updated the response structure to include the new branchDeleted flag.
- Enhanced projectPath validation in init-script to ensure it is a non-empty string before processing.
2026-01-12 19:21:37 +01:00
Dhanush Santosh
cebf57ffd3 Merge pull request #426 from stefandevo/opencode-dynamic-providers
feat: add dynamic model discovery and routing for OpenCode provider
2026-01-12 23:51:06 +05:30
DhanushSantosh
6020219fda fix(opencode): address review feedback 2026-01-12 23:44:21 +05:30
DhanushSantosh
8094941385 feat(opencode): persist dynamic model selection 2026-01-12 23:44:21 +05:30
DhanushSantosh
9ce3cfee7d feat(opencode): drop bedrock defaults 2026-01-12 23:44:05 +05:30
DhanushSantosh
6184440441 fix(ui): tie dynamic models to connected providers 2026-01-12 23:42:38 +05:30
DhanushSantosh
0cff4cf510 feat(ui): add OpenRouter icon 2026-01-12 23:42:28 +05:30
DhanushSantosh
b152f119c5 fix(ui): refresh OpenCode models on new providers 2026-01-12 23:42:27 +05:30
DhanushSantosh
9f936c6968 fix(opencode): parse api-key provider models 2026-01-12 23:42:12 +05:30
Stefan de Vogelaere
b8531cf7e8 fix: add OpenCode settings to migration for persistence
Add enabledOpencodeModels and opencodeDefaultModel to the settings
migration to ensure they are properly persisted like Cursor settings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:41:40 +05:30
Stefan de Vogelaere
edcc4e789b fix: address CodeRabbitAI review feedback
- Replace busy-wait loop in refreshModels with Promise-based approach
- Remove duplicate error logging in opencode-models.ts handlers
- Fix multi-slash parsing in provider-icon.tsx (only handle exactly one slash)
- Use dynamic icon resolution for selected OpenCode model in trigger
- Fix misleading comment about merge precedence (static takes precedence)
- Add enabledOpencodeModels and opencodeDefaultModel to settings sync
- Add clarifying comments about session-only dynamic model settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:41:26 +05:30
Stefan de Vogelaere
20cc401238 fix: update enhancement test to include ux-reviewer mode
Test expected 4 enhancement modes but there are now 5 after adding
the ux-reviewer mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:41:14 +05:30
Stefan de Vogelaere
70204a2d36 fix: address code review feedback from gemini-code-assist
- Convert execFileSync to async execFile in fetchModelsFromCli and
  fetchAuthenticatedProviders to avoid blocking the event loop
- Remove unused opencode-dynamic-providers.tsx component
- Use regex for more robust model ID validation in parseModelsOutput

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:40:57 +05:30
Stefan de Vogelaere
e38325c27f fix: improve dynamic model icons and fix React reference
- Add icon detection for dynamic OpenCode provider models (provider/model format)
- Support zai-coding-plan, github-copilot, google, xai, and other providers
- Detect model type from name (glm, claude, gpt, gemini, grok, etc.)
- Fix React.useMemo → useMemo to resolve "React is not defined" error

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:40:28 +05:30
Stefan de Vogelaere
5e4b422315 fix: improve OpenCode error handling and message extraction
- Update error event interface to handle nested error objects with
  name/data/message structure from OpenCode CLI
- Extract meaningful error messages from provider errors in normalizeEvent
- Add error type handling in executeWithProvider to throw errors with
  actual provider messages instead of returning empty response

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:40:13 +05:30
Stefan de Vogelaere
6c5206daf4 feat: add dynamic model discovery and routing for OpenCode provider
- Update isOpencodeModel() to detect dynamic models with provider/model format
  (e.g., github-copilot/gpt-4o, google/gemini-2.5-pro, zai-coding-plan/glm-4.7)
- Update resolveModelString() to recognize and pass through OpenCode models
- Update enhance route to route OpenCode models to OpenCode provider
- Fix OpenCode CLI command format: use --format json (not stream-json)
- Remove unsupported -q and - flags from CLI arguments
- Update normalizeEvent() to handle actual OpenCode JSON event format
- Add dynamic model configuration UI with provider grouping
- Cache providers and models in app store for snappier navigation
- Show authenticated providers in OpenCode CLI status card

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:39:38 +05:30
Shirone
ed65f70315 Merge pull request #409 from AutoMaker-Org/feat/worktrees-init-script
feat: worktrees init script
2026-01-12 17:52:31 +00:00
Shirone
f41a42010c fix: address pr comments 2026-01-12 18:41:56 +01:00
Tobias Weber
aa8caeaeb0 fix: make Docker container UID/GID configurable
Add UID and GID build arguments to Dockerfiles to allow matching the
container user to the host user. This fixes file permission issues when
mounting host directories as volumes.

Default remains 1001 for backward compatibility. To match host user:
  UID=$(id -u) GID=$(id -g) docker-compose build

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 16:14:56 +01:00
Kacper
a0669d4262 feat(board-view): enhance feature and plan dialogs with worktree branch settings
- Added WorktreeSettingsDialog and PlanSettingsDialog components to manage worktree branch settings.
- Integrated new settings into BoardHeader for toggling worktree branch usage in feature creation.
- Updated AddFeatureDialog to utilize selected worktree branch for custom mode.
- Introduced new state management in app-store for handling worktree branch preferences.

These changes improve user control over feature creation workflows by allowing branch selection based on the current worktree context.
2026-01-11 23:05:32 +01:00
Shirone
a4a792c6b1 Merge pull request #416 from AutoMaker-Org/feat/emtpy-columns-enhancments
feat: add empty state card component and integrate AI suggestion func…
2026-01-11 21:37:17 +00:00
Shirone
6842e4c7f7 refactor: simplify EmptyStateCard and update empty state configurations
- Removed unused properties and state management from the EmptyStateCard component for cleaner code.
- Updated the EMPTY_STATE_CONFIGS to remove exampleCard entries, streamlining the empty state configuration.
- Enhanced the primary action handling in the EmptyStateCard for improved functionality.
2026-01-11 22:35:25 +01:00
webdevcody
6638c35945 refactor(sidebar): enhance sidebar responsiveness and improve layout
- Updated sidebar component to include a mobile overlay backdrop when open.
- Adjusted visibility of logo and footer elements based on sidebar state.
- Improved layout and spacing for various components within the sidebar for better usability on different screen sizes.
- Refined styles for buttons and project selectors to enhance visual consistency and responsiveness.
2026-01-11 16:02:25 -05:00
Kacper
53f5c2b2bb feat(backlog): add branchName support to apply handler and UI components
- Updated apply handler to accept an optional branchName from the request body.
- Modified BoardView and BacklogPlanDialog components to pass currentBranch to the apply API.
- Enhanced ElectronAPI and HttpApiClient to include branchName in the apply method.

This change allows users to specify a branch when applying backlog plans, improving flexibility in feature management.
2026-01-11 20:52:07 +01:00
Kacper
6e13cdd516 Merge branch: resolve conflict in worktree-actions-dropdown.tsx 2026-01-11 20:08:19 +01:00
Kacper
a48c67d271 refactor: update EmptyStateCard component for improved layout and functionality
- Removed unused props and adjusted styles for a more compact and centered design.
- Enhanced the display of the icon, title, and description for better visibility.
- Updated keyboard shortcut hint and AI suggestion action for improved user interaction.
- Refined dismiss/minimize controls to appear on hover, enhancing the user experience.
2026-01-11 19:59:01 +01:00
Shirone
43fc3de2e1 Merge pull request #423 from stefandevo/main
feat: add default IDE setting and multi-editor support with icons
2026-01-11 18:36:12 +00:00
Kacper
80081b60bf fix(platform): remove logger import to avoid circular dependency
Replace createLogger with console.warn to prevent circular import
between @automaker/platform and @automaker/utils.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:34:29 +01:00
Kacper
cbca9b68e6 fix: correct Kiro CLI command typo (kido -> kiro)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:25:26 +01:00
Shirone
b9b3695497 feat(platform): add VS Code Insiders and Kiro editor support
Added support for two new editors:
- VS Code Insiders (code-insiders command)
- Kiro (kido command) - VS Code fork

Changes:
- Added editor definitions to SUPPORTED_EDITORS list
- Added VSCodeInsidersIcon (reuses VS Code icon)
- Added KiroIcon with custom SVG logo
- Updated getEditorIcon() to handle both new commands
- Fixed logger initialization to be lazy-loaded, preventing circular
  dependency error with isBrowser variable during module initialization

Both editors were tested and successfully open directories on macOS.
2026-01-11 19:14:44 +01:00
Shirone
1b9acb1395 fix(platform): verify full Xcode installation for xed command
The xed command requires full Xcode.app, not just Command Line Tools.
This fix adds validation to ensure Xcode is properly configured before
offering it as an editor option.

Changes:
- Added isXcodeFullyInstalled() to check xcode-select points to Xcode.app
- Added helpful warning when Xcode is installed but xcode-select points to CLT
- Users see clear instructions on how to fix the configuration

Fixes issue where xed would fail with "tool 'xed' requires Xcode" error
when only Command Line Tools are configured via xcode-select.
2026-01-11 19:04:39 +01:00
DhanushSantosh
01cf81a105 fix(platform): detect Antigravity CLI aliases 2026-01-11 23:22:13 +05:30
Tobias Weber
6381ecaa37 fix: add TodoWrite to allowed tools in SDK presets
The TodoWrite tool was missing from the fullAccess and chat tool
presets, causing the Claude Agent SDK to crash with exit code 1
when the agent attempted to use it for task tracking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:33:11 +01:00
Kacper
6d267ce0fa feat(platform): add cross-platform editor utilities and refresh functionality
- Add libs/platform/src/editor.ts with cross-platform editor detection and launching
  - Handles Windows .cmd batch scripts (cursor.cmd, code.cmd, etc.)
  - Supports macOS app bundles in /Applications and ~/Applications
  - Includes caching with 5-minute TTL for performance
- Refactor open-in-editor.ts to use @automaker/platform utilities
- Add POST /api/worktree/refresh-editors endpoint to clear cache
- Add refresh button to Settings > Account for IDE selection
- Update useAvailableEditors hook with refresh() and isRefreshing

Fixes Windows issue where "Open in Editor" was falling back to Explorer
because execFile cannot run .cmd scripts without shell:true.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:08:09 +01:00
Kacper
8b0b565282 Merge remote-tracking branch 'origin/v0.10.0rc' into stefandevo/main 2026-01-11 17:34:19 +01:00
DhanushSantosh
a046d1232e Merge remote-tracking branch 'upstream/v0.10.0rc' into feature/codex-cli 2026-01-11 21:59:04 +05:30
DhanushSantosh
d724e782dd fix(ui): restore startup project context 2026-01-11 21:58:36 +05:30
Shirone
a266d85ecd Merge pull request #421 from AutoMaker-Org/refactor/extract-enhance-with-ai-shared-components
refactor: extract Enhance with AI into shared components
2026-01-11 16:22:18 +00:00
Kacper
a4a111fad0 feat: add pre-enhancement description tracking for feature updates
- Introduced a new parameter `preEnhancementDescription` to capture the original description before enhancements.
- Updated the `update` method in `FeatureLoader` to handle the new parameter and maintain a history of original descriptions.
- Enhanced UI components to support tracking and restoring pre-enhancement descriptions across various dialogs.
- Improved history management in `AddFeatureDialog`, `EditFeatureDialog`, and `FollowUpDialog` to include original text for better user experience.

This change enhances the ability to revert to previous descriptions, improving the overall functionality of the feature enhancement process.
2026-01-11 17:19:39 +01:00
Stefan de Vogelaere
2a98de85a8 fix: improve cache management and editor fallback handling
Cache management improvements:
- Remove separate cachedEditor variable; derive default from cachedEditors
- Update isCacheValid() to check cachedEditors existence
- detectDefaultEditor() now always goes through detectAllEditors()
  to ensure cache TTL is respected consistently

Editor fallback improvements:
- Log warning when requested editorCommand is not found in available editors
- Include list of available editor commands in warning message
- Make fallback to default editor explicit rather than silent

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:08:10 +01:00
Stefan de Vogelaere
fb3a8499f3 fix: address CodeRabbitAI security and UX review comments
Security improvements in open-in-editor.ts:
- Use execFile with argument arrays instead of shell interpolation
  in commandExists() to prevent command injection
- Replace shell `test -d` commands with Node.js fs/promises access()
  in findMacApp() for safer file system checks
- Add cache TTL (5 minutes) for editor detection to prevent stale data

UX improvements in worktree-actions-dropdown.tsx:
- Add error handling for clipboard copy operation
- Show success toast when path is copied
- Show error toast if clipboard write fails

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:55:25 +01:00
Stefan de Vogelaere
33dd9ae347 fix: address nitpick feedback from PR #423
## Security Fix (Command Injection)
- Use `execFile` with argument arrays instead of string interpolation
- Add `safeOpenInEditor` helper that properly handles `open -a` commands
- Validate that worktreePath is an absolute path before execution
- Prevents shell metacharacter injection attacks

## Shared Type Definition
- Move `EditorInfo` interface to `@automaker/types` package
- Server and UI now import from shared package to prevent drift
- Re-export from use-available-editors.ts for convenience

## Remove Unused Code
- Remove unused `defaultEditorName` prop from WorktreeActionsDropdown
- Remove prop from WorktreeTab component interface
- Remove useDefaultEditor hook usage from WorktreePanel
- Export new hooks from hooks/index.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:37:05 +01:00
Stefan de Vogelaere
ac87594b5d fix: address code review feedback from PR #423
Addresses feedback from gemini-code-assist and coderabbitai reviewers:

## Duplicate Code (High Priority)
- Extract `getEffectiveDefaultEditor` logic into shared `useEffectiveDefaultEditor` hook
- Both account-section.tsx and worktree-actions-dropdown.tsx now use the shared hook

## Performance (Medium Priority)
- Refactor `detectAllEditors` to use `Promise.all` for parallel editor detection
- Replace sequential `await tryAddEditor()` calls with parallel `findEditor()` checks

## Code Quality (Medium Priority)
- Remove verbose IIFE pattern for editor icon rendering
- Pre-compute icon components before JSX return statement

## Bug Fixes
- Use `os.homedir()` instead of `~` fallback which doesn't expand in shell
- Normalize Select value to 'auto' when saved editor command not found in editors
- Add defensive check for empty editors array in useEffectiveDefaultEditor
- Improve mock openInEditor to correctly map all editor commands to display names

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:28:31 +01:00
Stefan de Vogelaere
32656a9662 feat: add default IDE setting and multi-editor support with icons
Add comprehensive editor detection and selection system that allows users
to configure their preferred IDE for opening branches and worktrees.

## Server-side Changes

- Add `/api/worktree/available-editors` endpoint to detect installed editors
- Support detection via CLI commands (cursor, code, zed, subl, etc.)
- Support detection via macOS app bundles in /Applications and ~/Applications
- Detect editors: Cursor, VS Code, Zed, Sublime Text, Windsurf, Trae,
  Rider, WebStorm, Xcode, Android Studio, Antigravity, and file managers

## UI Changes

### Editor Icons
- Add new `editor-icons.tsx` with SVG icons for all supported editors
- Icons: Cursor, VS Code, Zed, Sublime Text, Windsurf, Trae, Rider,
  WebStorm, Xcode, Android Studio, Antigravity, Finder
- `getEditorIcon()` helper maps editor commands to appropriate icons

### Default IDE Setting
- Add "Default IDE" selector in Settings > Account section
- Options: Auto-detect (Cursor > VS Code > first available) or explicit choice
- Setting persists via `defaultEditorCommand` in global settings

### Worktree Dropdown Improvements
- Implement split-button UX for "Open In" action
- Click main area: opens directly in default IDE (single click)
- Click chevron: shows submenu with other editors + Copy Path
- Each editor shows with its branded icon

## Type & Store Changes

- Add `defaultEditorCommand: string | null` to GlobalSettings
- Add to app-store with `setDefaultEditorCommand` action
- Add to SETTINGS_FIELDS_TO_SYNC for persistence
- Add `useAvailableEditors` hook for fetching detected editors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 16:17:05 +01:00
DhanushSantosh
785a4d2c3b fix: restore auth and auto-open last project 2026-01-11 20:43:55 +05:30
Shirone
41a6c7f712 fix: address second round of PR review feedback
- Add fallback for unknown enhancement modes in history button to prevent "Enhanced (undefined)" UI bug
- Move DescriptionHistoryEntry interface to top level in add-feature-dialog
- Import and use EnhancementMode type in edit-feature-dialog to eliminate hardcoded types
- Make FollowUpHistoryEntry extend BaseHistoryEntry for consistency

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 15:27:17 +01:00
Shirone
7e5d915b60 fix: address PR review feedback from Gemini Code Assist
Address three issues identified in code review:

1. Fix missing thinkingLevel parameter (Critical)
   - Added thinkingLevel parameter to enhance API call
   - Updated electron.ts type definition to match http-api-client
   - Fixes functional regression in Claude model enhancement

2. Refactor dropdown menu to use constants dynamically
   - Changed hardcoded DropdownMenuItem components to dynamic generation
   - Now iterates over ENHANCEMENT_MODE_LABELS object
   - Ensures automatic sync when new modes are added
   - Eliminates manual UI updates for new enhancement modes

3. Optimize array reversal performance
   - Added useMemo hook to memoize reversed history array
   - Prevents creating new array on every render
   - Improves performance with lengthy histories

All TypeScript errors resolved. Build verified.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 15:17:46 +01:00
Shirone
8321c06e16 refactor: extract Enhance with AI into shared components
Extract all "Enhance with AI" functionality into reusable shared components
following DRY principles and clean code guidelines.

Changes:
- Create shared/enhancement/ folder for related functionality
- Extract EnhanceWithAI component (AI enhancement with model override)
- Extract EnhancementHistoryButton component (version history UI)
- Extract enhancement constants and types
- Refactor add-feature-dialog.tsx to use shared components
- Refactor edit-feature-dialog.tsx to use shared components
- Refactor follow-up-dialog.tsx to use shared components
- Add history tracking to add-feature-dialog for consistency

Benefits:
- Eliminated ~527 lines of duplicated code
- Single source of truth for enhancement logic
- Consistent UX across all dialogs
- Easier maintenance and extensibility
- Better code organization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 15:10:54 +01:00
Shirone
f60c18d31a Update apps/ui/src/components/views/board-view/constants.ts
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-11 12:08:33 +01:00
Shirone
e171b6a049 feat: add empty state card component and integrate AI suggestion functionality
- Introduced the EmptyStateCard component to display contextual guidance when columns are empty.
- Enhanced the KanbanBoard and BoardView components to utilize the new EmptyStateCard for improved user experience.
- Added AI suggestion functionality to the empty state configuration, allowing users to generate ideas directly from the backlog column.
- Updated constants to define empty state configurations for various column types.
2026-01-11 12:03:52 +01:00
Shirone
6e4b611662 refactor: optimize bulk delete handler and UI feedback
- Refactored the bulk delete handler to utilize Promise.all for concurrent deletion of features, improving performance and error handling.
- Updated the BoardView component to handle deletion results more effectively, providing user feedback for both successful and failed deletions.
- Enhanced local state management to avoid redundant API calls during feature deletion.
2026-01-11 11:17:28 +01:00
DhanushSantosh
7522e58fee Merge remote-tracking branch 'upstream/v0.10.0rc' into feature/codex-cli 2026-01-11 14:51:06 +05:30
Shirone
317c21ffc0 Merge pull request #413 from AutoMaker-Org/feat/bulk-delete-features-in-backlog
feat: bulk delete in backlog mass select
2026-01-11 09:19:15 +00:00
Shirone
9c5fe44617 feat: add bulk delete functionality for features
- Introduced a new endpoint `/bulk-delete` to allow deletion of multiple features at once.
- Implemented `createBulkDeleteHandler` to process bulk delete requests and handle success/failure responses.
- Updated the UI to include a bulk delete option in the BoardView component, with confirmation dialog for user actions.
- Enhanced the HTTP API client to support bulk delete requests.
- Improved the selection action bar to trigger bulk delete functionality and provide user feedback.
2026-01-11 10:17:35 +01:00
DhanushSantosh
7f79d9692c feat: Add official icons for MiniMax, GLM (Z.ai), and BigPickle models
- Add official MiniMax logo SVG from LobeHub icons library
- Add official Z.ai logo SVG for GLM models from LobeHub icons library
- Add BigPickle icon with custom green color (#4ADE80)
- Fix icon detection logic to properly handle amazon-bedrock/ and opencode/ prefixes
- Update phase-model-selector and opencode-model-configuration to use
  getProviderIconForModel() for consistent icon display across the app

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:44:32 +05:30
Shirone
2d4ffc7514 feat: add accept all functionality in ideation view
- Introduced a new "Accept All" button in the IdeationHeader component, allowing users to accept all filtered suggestions at once.
- Implemented state management for accept all readiness and count in the IdeationView component.
- Enhanced the IdeationDashboard to notify the parent component about the readiness of the accept all feature.
- Added logic to handle the acceptance of all suggestions, including success and failure notifications.
- Updated UI components to reflect the new functionality and improve user experience.
2026-01-11 10:01:01 +01:00
webdevcody
5f3db1f25e feat: enhance spec regeneration management by project
- Refactored spec regeneration status tracking to support multiple projects using a Map for running states and abort controllers.
- Updated `getSpecRegenerationStatus` to accept a project path, allowing retrieval of status specific to a project.
- Modified `setRunningState` to manage running states and abort controllers per project.
- Adjusted related route handlers to utilize project-specific status checks and updates.
- Introduced a new Graph View page and integrated it into the routing structure.
- Enhanced UI components to reflect the current project’s spec generation state.
2026-01-11 01:37:26 -05:00
webdevcody
7115460804 feat: add resume interrupted features endpoint and handler
- Introduced a new endpoint `/resume-interrupted` to handle resuming features that were interrupted during server restarts.
- Implemented the `createResumeInterruptedHandler` to check for and resume interrupted features based on the project path.
- Enhanced the `AutoModeService` to track and manage the execution state of features, ensuring they can be resumed correctly.
- Updated relevant types and prompts to include the new 'ux-reviewer' enhancement mode for better user experience handling.
- Added new templates for UX review and other enhancement modes to improve task descriptions from a user experience perspective.
2026-01-11 01:37:13 -05:00
webdevcody
0db8808b2a Merge branch 'memory-ui' into v0.10.0rc 2026-01-10 20:13:07 -05:00
webdevcody
cf3ed1dd8f Merge branch 'v0.10.0rc' of github.com:AutoMaker-Org/automaker into v0.10.0rc 2026-01-10 20:08:02 -05:00
webdevcody
da682e3993 feat: add memory management feature with UI components
- Introduced a new MemoryView component for viewing and editing AI memory files.
- Updated navigation hooks and keyboard shortcuts to include memory functionality.
- Added memory file creation, deletion, and renaming capabilities.
- Enhanced the sidebar navigation to support memory as a new section.
- Implemented loading and saving of memory files with a markdown editor.
- Integrated dialogs for creating, deleting, and renaming memory files.
2026-01-10 20:07:50 -05:00
Shirone
4a59e901e6 chore: format 2026-01-11 01:15:27 +01:00
Shirone
8ed2fa07a0 security: Fix critical vulnerabilities in worktree init script feature
Fix multiple command injection and security vulnerabilities in the worktree
initialization script system:

**Critical Fixes:**
- Add branch name validation to prevent command injection in create/delete endpoints
- Replace string interpolation with array-based command execution using spawnProcess
- Implement safe environment variable allowlist to prevent credential exposure
- Add script content validation with 1MB size limit and dangerous pattern detection

**Code Quality:**
- Centralize execGitCommand helper in common.ts using @automaker/platform's spawnProcess
- Remove duplicate isGitRepo implementation, standardize imports to @automaker/git-utils
- Follow DRY principle by reusing existing platform utilities
- Add comprehensive JSDoc documentation with security examples

This addresses 6 critical/high severity vulnerabilities identified in security audit:
1. Command injection via unsanitized branch names (delete.ts)
2. Command injection via unsanitized branch names (create.ts)
3. Missing branch validation in init script execution
4. Environment variable exposure (ANTHROPIC_API_KEY and other secrets)
5. Path injection via command substitution
6. Arbitrary script execution without content limits

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 01:14:07 +01:00
Kacper
385e7f5c1e fix: address pr comments 2026-01-11 00:01:23 +01:00
Kacper
861fff1aae fix: broken lock file 2026-01-10 23:48:33 +01:00
Kacper
09527b3b67 feat: Add auto-dismiss functionality for Init Script Indicator
This commit introduces an auto-dismiss feature for the Init Script Indicator, enhancing user experience by automatically hiding the indicator 5 seconds after the script completes. Key changes include:

1. **State Management**: Added `autoDismissInitScriptIndicatorByProject` to manage the auto-dismiss setting per project.
2. **UI Components**: Updated the WorktreesSection to include a toggle for enabling or disabling the auto-dismiss feature, allowing users to customize their experience.
3. **Indicator Logic**: Implemented logic in the SingleIndicator component to handle auto-dismiss based on the new setting.

These enhancements provide users with more control over the visibility of the Init Script Indicator, streamlining project management workflows.
2026-01-10 23:43:52 +01:00
Kacper
d98ff16c8f feat: Enhance CreateWorktreeDialog with user-friendly error handling
This commit introduces a new error parsing function to provide clearer, user-friendly error messages in the CreateWorktreeDialog component. Key changes include:

1. **Error Parsing**: Added `parseWorktreeError` function to interpret various git-related error messages and return structured titles and descriptions for better user feedback.
2. **State Management**: Updated the error state to store structured error objects instead of strings, allowing for more detailed error display.
3. **UI Updates**: Enhanced the error display in the dialog to show both title and description, improving clarity for users encountering issues during worktree creation.

These improvements enhance the user experience by providing more informative error messages, helping users troubleshoot issues effectively.
2026-01-10 23:37:39 +01:00
Kacper
e902e8ea4c feat: Introduce default delete branch option for worktrees
This commit adds a new feature allowing users to set a default value for the "delete branch" checkbox when deleting a worktree. Key changes include:

1. **State Management**: Introduced `defaultDeleteBranchByProject` to manage the default delete branch setting per project.
2. **UI Components**: Updated the WorktreesSection to include a toggle for the default delete branch option, enhancing user control during worktree deletion.
3. **Dialog Updates**: Modified the DeleteWorktreeDialog to respect the default delete branch setting, improving the user experience by streamlining the deletion process.

These enhancements provide users with more flexibility and control over worktree management, improving overall project workflows.
2026-01-10 23:18:39 +01:00
Kacper
aeb5bd829f feat: Add Init Script Indicator visibility feature for worktrees
This commit introduces a new feature allowing users to toggle the visibility of the Init Script Indicator for each project. Key changes include:

1. **State Management**: Added `showInitScriptIndicatorByProject` to manage the visibility state per project.
2. **UI Components**: Implemented a checkbox in the WorktreesSection to enable or disable the Init Script Indicator, enhancing user control over the UI.
3. **BoardView Updates**: Modified the BoardView to conditionally render the Init Script Indicator based on the new visibility state.

These enhancements improve the user experience by providing customizable visibility options for the Init Script Indicator, streamlining project management workflows.
2026-01-10 23:03:29 +01:00
DhanushSantosh
a92457b871 fix: Handle Claude CLI unavailability gracefully in CI
- Add try-catch around pty.spawn() to prevent crashes when PTY unavailable
- Add unhandledRejection/uncaughtException handlers for graceful degradation
- Add checkBackendHealth/waitForBackendHealth utilities for tests
- Add data/.api-key and data/credentials.json to .gitignore
2026-01-11 03:22:43 +05:30
Kacper
c24e6207d0 feat: Enhance ShellSyntaxEditor and WorktreesSection with new features
This commit introduces several improvements to the ShellSyntaxEditor and WorktreesSection components:

1. **ShellSyntaxEditor**: Added a `maxHeight` prop to allow for customizable maximum height, enhancing layout flexibility.
2. **WorktreesSection**:
   - Introduced state management for original script content and existence checks for scripts.
   - Implemented save, reset, and delete functionalities for initialization scripts, providing users with better control over their scripts.
   - Added action buttons for saving, resetting, and deleting scripts, along with loading indicators for improved user feedback.
   - Enhanced UI to indicate unsaved changes, improving user awareness of script modifications.

These changes improve the user experience by providing more robust script management capabilities and a more responsive UI.
2026-01-10 22:46:06 +01:00
Kacper
6c412cd367 feat: Add run init script functionality for worktrees
This commit introduces the ability to run initialization scripts for worktrees, enhancing the setup process. Key changes include:

1. **New API Endpoint**: Added a POST endpoint to run the init script for a specified worktree.
2. **Worktree Routes**: Updated worktree routes to include the new run init script handler.
3. **Init Script Service**: Enhanced the Init Script Service to support running scripts asynchronously and handling errors.
4. **UI Updates**: Added UI components to check for the existence of init scripts and trigger their execution, providing user feedback through toast notifications.
5. **Event Handling**: Implemented event handling for init script execution status, allowing real-time updates in the UI.

This feature streamlines the workflow for users by automating the execution of setup scripts, improving overall project management.
2026-01-10 22:36:50 +01:00
DhanushSantosh
89a960629a fix: Improve E2E test workflow for better backend debugging
Enhanced backend server startup in CI:
- Track server PID and process status
- Save logs to backend.log for debugging
- Better error detection with process monitoring
- Added cleanup step to kill server process
- Print backend logs on test failure

Improves reliability of E2E tests by providing better diagnostics when backend fails to start
2026-01-11 02:58:56 +05:30
Kacper
05d96a7d6e feat: Implement worktree initialization script functionality
This commit introduces a new feature for managing worktree initialization scripts, allowing users to configure and execute scripts upon worktree creation. Key changes include:

1. **New API Endpoints**: Added endpoints for getting, setting, and deleting init scripts.
2. **Worktree Routes**: Updated worktree routes to include init script handling.
3. **Init Script Service**: Created a service to execute the init scripts asynchronously, with support for cross-platform compatibility.
4. **UI Components**: Added UI components for displaying and editing init scripts, including a dedicated section in the settings view.
5. **Event Handling**: Implemented event handling for init script execution status, providing real-time feedback in the UI.

This enhancement improves the user experience by allowing automated setup processes for new worktrees, streamlining project workflows.
2026-01-10 22:19:34 +01:00
DhanushSantosh
41144ff1fa Merge recovered upstream commits including worktree enhancement
This brings back commits that were accidentally overwritten during a force push:
- fa8ae149 feat: enhance worktree listing by scanning external directories
- Plus any other changes from upstream/v0.10.0rc at that time

The merge ensures all changes are preserved while keeping the history intact.
2026-01-11 02:27:21 +05:30
DhanushSantosh
360cddcb91 Merge upstream commits including worktree enhancement
This recovers the commits that were accidentally overwritten during force push.

Included:
- fa8ae149 feat: enhance worktree listing by scanning external directories
- Any other commits from upstream/v0.10.0rc at that point
2026-01-11 02:27:08 +05:30
DhanushSantosh
427832e72e fix: Display correct provider icons for all OpenCode/Bedrock models
The issue was that ALL OpenCode models were showing the OpenCode icon, regardless
of their actual underlying provider. This fix ensures each model shows its
authentic brand icon.

Changes:

1. **model-constants.ts** - Fixed provider field assignment
   - Changed provider from hardcoded 'opencode' to actual config.provider
   - Now correctly maps: opencode/big-pickle, amazon-bedrock/anthropic.*, etc.

2. **phase-model-selector.tsx** - Added provider-specific icon logic
   - Added imports for DeepSeekIcon, NovaIcon, QwenIcon, MistralIcon, MetaIcon
   - Added ProviderIcon selector based on model.provider field
   - Each model type now displays its correct provider icon

3. **provider-icon.tsx** - Updated icon detection and mapping
   - Enhanced getUnderlyingModelIcon() to detect specific Bedrock providers:
     * amazon-bedrock/anthropic.* → anthropic icon
     * amazon-bedrock/deepseek.* → deepseek icon
     * amazon-bedrock/nova.* → nova icon
     * amazon-bedrock/meta.* or llama → meta icon
     * amazon-bedrock/mistral.* → mistral icon
     * amazon-bedrock/qwen.* → qwen icon
     * opencode/* → opencode icon
   - Added meta and mistral to PROVIDER_ICON_KEYS
   - Added placeholder definitions for meta/mistral in PROVIDER_ICON_DEFINITIONS
   - Updated iconMap to include all provider icons
   - Set OpenCode icon to official brand color (#6366F1 indigo)

Result: All model selectors and kanban cards now show correct brand icons
for each OpenCode model (DeepSeek whale, Amazon Nova sparkle, Qwen star, etc.)
2026-01-11 02:18:55 +05:30
webdevcody
27c60658f7 Merge branch 'v0.10.0rc' of github.com:AutoMaker-Org/automaker into v0.10.0rc 2026-01-10 15:41:50 -05:00
webdevcody
fa8ae149d3 feat: enhance worktree listing by scanning external directories
- Implemented a new function to scan the .worktrees directory for worktrees that may exist outside of git's management, allowing for better detection of externally created or corrupted worktrees.
- Updated the /list endpoint to include discovered worktrees in the response, improving the accuracy of the worktree listing.
- Added logging for discovered worktrees to aid in debugging and tracking.
- Cleaned up and organized imports in the list.ts file for better maintainability.
2026-01-10 15:41:35 -05:00
DhanushSantosh
0c19beb11c fix: Set OpenCode icon to official brand color (#6366F1 indigo)
The OpenCode icon now uses the official indigo brand color (#6366F1)
from opencode.ai instead of white, making it visible in both light
and dark themes.
2026-01-11 01:54:17 +05:30
DhanushSantosh
e34e4a59e9 fix: Resolve TypeScript error assigning part.result to string field
Fix TS2322 error where finishEvent.part?.result (typed as {}) was being
assigned to result.result (typed as string).

Solution: Safely handle arbitrary result payloads by:
1. Reading raw value as unknown from Record<string, unknown>
2. Checking if it's a string, otherwise JSON.stringify()

This ensures type safety while supporting both string and object results
from the OpenCode CLI.
2026-01-11 01:35:32 +05:30
DhanushSantosh
7cc092cd59 test: Fix remaining OpenCode provider test failures
Fix all 8 remaining test failures:

1. Update executeQuery integration tests to use new OpenCode event format:
   - text events use type='text' with part.text
   - tool_call events use type='tool_call' with part containing call_id, name, args
   - tool_result events use type='tool_result' with part
   - step_finish events use type='step_finish' with part
   - Use sessionID field instead of session_id

2. Fix step_finish event handling:
   - Include result field in successful completion response
   - Check for reason === 'error' to detect failed steps
   - Provide default error message when error field is missing

3. Update model test expectations:
   - Model 'opencode/big-pickle' stays as-is (not stripped to 'big-pickle')
   - PROVIDER_PREFIXES only strips 'opencode-' prefix, not 'opencode/'

All 84 tests now pass successfully!
2026-01-11 01:23:42 +05:30
DhanushSantosh
51cd7156d2 test: Update OpenCode provider tests to use new event format
Update normalizeEvent tests to match new OpenCode API:
- text events use type='text' with part.text instead of text-delta
- tool_call events use type='tool_call' with part containing call_id, name, args
- tool_result events use type='tool_result' with part
- tool_error events use type='tool_error' with part
- step_finish events use type='step_finish' with part

Update buildCliArgs tests:
- Remove expectations for -q flag (no longer used)
- Remove expectations for -c flag (cwd set at subprocess level)
- Remove expectations for - final arg (prompt via stdin)
- Update format to 'json' instead of 'stream-json'

Remaining 8 test failures are in integration tests that use executeQuery
and require more extensive mock data updates.
2026-01-11 01:07:55 +05:30
DhanushSantosh
1dc843d2d0 Merge upstream/v0.10.0rc into feature/codex-cli
Sync with latest upstream changes:
- feat: enhance feature dialogs with planning mode tooltips
- refactor: remove kanbanCardDetailLevel from settings and UI components
- refactor: streamline feature addition in BoardView and KanbanBoard
- feat: implement dashboard view and enhance sidebar navigation
2026-01-11 00:59:36 +05:30
DhanushSantosh
4040bef4b8 feat: Add OpenCode provider integration with official brand icons
This commit integrates OpenCode as a new AI provider and updates all provider
icons with their official brand colors for better visual recognition.

**OpenCode Provider Integration:**
- Add OpencodeProvider class with CLI-based execution
- Support for OpenCode native models (opencode/) and Bedrock models
- Proper event normalization for OpenCode streaming format
- Correct CLI arguments: --format json (not stream-json)
- Event structure: type, part.text, sessionID fields

**Provider Icons:**
- Add official OpenCode icon (white square frame from opencode.ai)
- Add DeepSeek icon (blue whale #4D6BFE)
- Add Qwen icon (purple gradient #6336E7 → #6F69F7)
- Add Amazon Nova icon (AWS orange #FF9900)
- Add Mistral icon (rainbow gradient gold→red)
- Add Meta icon (blue #1877F2)
- Update existing icons with brand colors:
  * Claude: #d97757 (terra cotta)
  * OpenAI/Codex: #74aa9c (teal-green)
  * Cursor: #5E9EFF (bright blue)

**Settings UI Updates:**
- Update settings navigation to show OpenCode icon
- Update model configuration to use provider-specific icons
- Differentiate between OpenCode free models and Bedrock-hosted models
- All AI models now display their official brand logos

**Model Resolution:**
- Add isOpencodeModel() function to detect OpenCode models
- Support patterns: opencode/, opencode-*, amazon-bedrock/*
- Update getProviderFromModel to recognize opencode provider

Note: Some unit tests in opencode-provider.test.ts need updating to match
the new event structure and CLI argument format.
2026-01-11 00:56:25 +05:30
Kacper
e64a850f57 feat: enhance feature dialogs with planning mode tooltips
- Integrated Tooltip components into AddFeatureDialog, EditFeatureDialog, and MassEditDialog to provide user guidance on planning mode availability.
- Updated the rendering logic for planning mode selection to conditionally display tooltips when planning modes are not supported.
- Improved user experience by clarifying the conditions under which planning modes can be utilized.
2026-01-10 20:08:17 +01:00
webdevcody
555523df38 refactor: remove kanbanCardDetailLevel from settings and UI components
- Eliminated kanbanCardDetailLevel from the SettingsService, app state, and various UI components including BoardView and BoardControls.
- Updated related hooks and API client to reflect the removal of kanbanCardDetailLevel.
- Cleaned up imports and props associated with kanbanCardDetailLevel across the codebase for improved clarity and maintainability.
2026-01-10 13:39:45 -05:00
webdevcody
dd882139f3 refactor: streamline feature addition in BoardView and KanbanBoard
- Removed the add feature shortcut from BoardHeader and integrated the add feature functionality directly into the KanbanBoard and BoardView components.
- Added a floating action button for adding features in the KanbanBoard's backlog column.
- Updated KanbanColumn to support a footer action for enhanced UI consistency.
- Cleaned up unused imports and props related to feature addition.
2026-01-10 13:19:21 -05:00
webdevcody
a67b8c6109 feat: implement dashboard view and enhance sidebar navigation
- Added a new DashboardView component for improved project management.
- Updated sidebar navigation to redirect to the dashboard instead of the home page.
- Removed ProjectActions from the sidebar for a cleaner interface.
- Enhanced BoardView to conditionally render the WorktreePanel based on visibility settings.
- Introduced worktree panel visibility management per project in the app store.
- Updated project settings to include worktree panel visibility and favorite status.
- Adjusted navigation logic to ensure users are directed to the appropriate view based on project state.
2026-01-10 13:08:59 -05:00
webdevcody
134208dab6 Merge branch 'v0.10.0rc' of github.com:AutoMaker-Org/automaker into v0.10.0rc 2026-01-10 12:27:33 -05:00
webdevcody
887343d232 Merge branch 'main' into v0.10.0rc 2026-01-10 12:27:29 -05:00
Web Dev Cody
299b838400 Merge pull request #404 from AutoMaker-Org/fix/security-vulnerability
chore: update @modelcontextprotocol/sdk in server dep
2026-01-10 12:25:29 -05:00
Kacper
c5d0a8be7d chore: update @modelcontextprotocol/sdk in server dep 2026-01-10 17:18:08 +01:00
Shirone
fe433a84c9 Merge pull request #402 from AutoMaker-Org/fix/security-vulnerability-in-dep
fix: security vulnerability in server
2026-01-10 15:52:00 +00:00
Kacper
543aa7a27b chore: update @modelcontextprotocol/sdk in server dep 2026-01-10 16:42:26 +01:00
Shirone
36ddf0513b Merge pull request #400 from AutoMaker-Org/feat/codex-usage
feat: improve codex plan and usage detection
2026-01-10 15:29:33 +00:00
Shirone
c99883e634 fix: address PR review comments
- Add error logging to CodexProvider auth check instead of silent failure
- Fix cachedAt timestamp to return actual cache time instead of request time
- Replace misleading hardcoded rate limit values (100) with sentinel value (-1)
- Fix unused parameter warning in codex routes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:26:12 +01:00
Shirone
604f98b08f chore: ignore .codex/config.toml to prevent API key exposure
Move .codex/config.toml to .gitignore to prevent accidental commits of
API keys. The file will remain local to each user's setup.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 16:18:19 +01:00
Shirone
c5009a0333 refactor: remove Codex credits handling from services and UI components
- Eliminated CodexCreditsSnapshot interface and related logic from CodexUsageService and UI components.
- Updated CodexUsageSection to display only plan type, removing credits information for a cleaner interface.
- Streamlined Codex usage formatting functions by removing unused credit formatting logic.

These changes simplify the Codex usage management by focusing on plan types, enhancing clarity and maintainability.
2026-01-10 16:18:19 +01:00
Shirone
99b05d35a2 feat: update Codex services and UI components for enhanced model management
- Bumped version numbers for @automaker/server and @automaker/ui to 0.9.0 in package-lock.json.
- Introduced CodexAppServerService and CodexModelCacheService to manage communication with the Codex CLI's app-server and cache model data.
- Updated CodexUsageService to utilize app-server for fetching usage data.
- Enhanced Codex routes to support fetching available models and integrated model caching.
- Improved UI components to dynamically load and display Codex models, including error handling and loading states.
- Added new API methods for fetching Codex models and integrated them into the app store for state management.

These changes improve the overall functionality and user experience of the Codex integration, ensuring efficient model management and data retrieval.
2026-01-10 16:18:08 +01:00
Web Dev Cody
a3ecc6fe02 Merge pull request #394 from AutoMaker-Org/remove-profiles
refactor: remove AI profile functionality and related components
2026-01-09 19:25:15 -05:00
webdevcody
fc20dd5ad4 refactor: remove AI profile functionality and related components
- Deleted the AI profile management feature, including all associated views, hooks, and types.
- Updated settings and navigation components to remove references to AI profiles.
- Adjusted local storage and settings synchronization logic to reflect the removal of AI profiles.
- Cleaned up tests and utility functions that were dependent on the AI profile feature.

These changes streamline the application by eliminating unused functionality, improving maintainability and reducing complexity.
2026-01-09 19:21:30 -05:00
webdevcody
3f2707404c chore: release v0.9.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 18:40:15 -05:00
Web Dev Cody
fdd3a28be7 Merge pull request #385 from AutoMaker-Org/v0.9.0rc
V0.9.0rc
2026-01-09 18:37:32 -05:00
Kacper
eb94e4de72 feat: enhance CodexUsageService to fetch usage data from app-server JSON-RPC API
- Implemented a new method to retrieve usage data from the Codex app-server, providing real-time data and improving reliability.
- Updated the fetchUsageData method to prioritize app-server data over fallback methods.
- Added detailed logging for better traceability and debugging.
- Removed unused methods related to OpenAI API usage and Codex CLI requests, streamlining the service.

These changes enhance the functionality and robustness of the CodexUsageService, ensuring accurate usage statistics retrieval.
2026-01-10 00:11:42 +01:00
webdevcody
21d275c984 increase panel width 2026-01-09 17:10:49 -05:00
webdevcody
cadb19d7ed feat: reorganize global settings navigation into groups
- Refactored the global navigation structure to group settings items into distinct categories for improved organization and usability.
- Updated the settings navigation component to render these groups dynamically, enhancing the user experience.
- Changed the default initial view in the settings hook to 'model-defaults' for better alignment with the new navigation structure.

These changes streamline navigation and make it easier for users to find relevant settings.
2026-01-09 17:09:08 -05:00
webdevcody
a01241fabe Merge branch 'v0.9.0rc' of github.com:AutoMaker-Org/automaker into v0.9.0rc 2026-01-09 16:40:17 -05:00
webdevcody
89a0877bcc feat: enhance settings navigation with collapsible sub-items and local storage state
- Implemented a collapsible dropdown for navigation items in the settings view, allowing users to expand or collapse sub-items.
- Added local storage functionality to remember the open/closed state of the dropdown across sessions.
- Updated the styling and interaction logic for improved user experience and accessibility.

These changes improve the organization of navigation items and enhance user interaction within the settings view.
2026-01-09 16:40:07 -05:00
Shirone
950a97d72b Merge pull request #392 from AutoMaker-Org/fix/codex-usage-plan-detection
fix: correct Codex plan type detection from JWT auth
2026-01-09 22:34:12 +01:00
Kacper
639c1de2e8 merge: sync with latest v0.9.0rc changes 2026-01-09 22:27:06 +01:00
Kacper
254e4f630c fix: address PR review comments for type safety
- Add typeof checks for fallback claim values to prevent runtime errors
- Make openaiAuth parsing more robust with proper type validation
- Add isNaN check for date parsing to handle invalid dates
- Refactor fetchFromAuthFile to reuse getPlanTypeFromAuthFile (DRY)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:25:06 +01:00
Kacper
5d0fb08651 fix: correct Codex plan type detection from JWT auth
- Fix hardcoded 'plus' planType that was returned as default
- Read plan type from correct JWT path: https://api.openai.com/auth.chatgpt_plan_type
- Add subscription expiry check - override to 'free' if expired
- Use getCodexAuthPath() from @automaker/platform instead of manual path
- Remove unused imports (os, fs, path) and class properties
- Clean up code and add minimal essential logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:18:41 +01:00
webdevcody
7ea64b32f3 feat: implement work mode selection in feature dialogs
- Added WorkModeSelector component to allow users to choose between 'current', 'auto', and 'custom' work modes for feature management.
- Updated AddFeatureDialog and EditFeatureDialog to utilize the new work mode functionality, replacing the previous branch selector logic.
- Enhanced useBoardActions hook to handle branch name generation based on the selected work mode.
- Adjusted settings to default to using worktrees, improving the overall feature creation experience.

These changes streamline the feature management process by providing clearer options for branch handling and worktree isolation.
2026-01-09 16:15:09 -05:00
webdevcody
93807c22c1 feat: add VITE_APP_MODE environment variable support
- Introduced VITE_APP_MODE variable in multiple files to manage application modes.
- Updated dev.mjs and docker-compose.dev.yml to set different modes for development.
- Enhanced type definitions in vite-env.d.ts to include VITE_APP_MODE options.
- Modified AutomakerLogo component to display version suffix based on the current app mode.
- Improved OS detection logic in use-os-detection.ts to utilize Electron's platform information.
- Updated ElectronAPI interface to expose platform information.

These changes provide better control over application behavior based on the mode, enhancing the development experience.
2026-01-09 15:30:49 -05:00
SuperComboGamer
b2cf17b53b feat: add project-scoped agent memory system (#351)
* memory

* feat: add smart memory selection with task context

- Add taskContext parameter to loadContextFiles for intelligent file selection
- Memory files are scored based on tag matching with task keywords
- Category name matching (e.g., "terminals" matches terminals.md) with 4x weight
- Usage statistics influence scoring (files that helped before rank higher)
- Limit to top 5 files + always include gotchas.md
- Auto-mode passes feature title/description as context
- Chat sessions pass user message as context

This prevents loading 40+ memory files and killing context limits.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: enhance auto-mode service and context loader

- Improved context loading by adding task context for better memory selection.
- Updated JSON parsing logic to handle various formats and ensure robust error handling.
- Introduced file locking mechanisms to prevent race conditions during memory file updates.
- Enhanced metadata handling in memory files, including validation and sanitization.
- Refactored scoring logic for context files to improve selection accuracy based on task relevance.

These changes optimize memory file management and enhance the overall performance of the auto-mode service.

* refactor: enhance learning extraction and formatting in auto-mode service

- Improved the learning extraction process by refining the user prompt to focus on meaningful insights and structured JSON output.
- Updated the LearningEntry interface to include additional context fields for better documentation of decisions and patterns.
- Enhanced the formatLearning function to adopt an Architecture Decision Record (ADR) style, providing richer context for recorded learnings.
- Added detailed logging for better traceability during the learning extraction and appending processes.

These changes aim to improve the quality and clarity of learnings captured during the auto-mode service's operation.

* feat: integrate stripProviderPrefix utility for model ID handling

- Added stripProviderPrefix utility to various routes to ensure providers receive bare model IDs.
- Updated model references in executeQuery calls across multiple files, enhancing consistency in model ID handling.
- Introduced memoryExtractionModel in settings for improved learning extraction tasks.

These changes streamline the model ID processing and enhance the overall functionality of the provider interactions.

* feat: enhance error handling and server offline management in board actions

- Improved error handling in the handleRunFeature and handleStartImplementation functions to throw errors for better caller management.
- Integrated connection error detection and server offline handling, redirecting users to the login page when the server is unreachable.
- Updated follow-up feature logic to include rollback mechanisms and improved user feedback for error scenarios.

These changes enhance the robustness of the board actions by ensuring proper error management and user experience during server connectivity issues.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: webdevcody <webdevcody@gmail.com>
2026-01-09 15:11:59 -05:00
DhanushSantosh
7e768b6290 fix: restore complete E2E workflow structure
- Restore missing workflow metadata (name, on, jobs)
- Fix YAML structure that got corrupted during edits
- Ensure E2E tests will run on PRs and pushes to main/master
2026-01-09 22:44:57 +05:30
DhanushSantosh
7bdf5e4261 fix: resolve CI E2E test failures
- Fix disposed response object in Playwright route handler
- Add git user config to prevent 'empty ident' errors
- Increase server startup timeout and improve debugging
- Fix YAML indentation in E2E workflow

Resolves:
- 'Response has been disposed' error in open-existing-project test
- Git identity configuration issues in CI
- Backend server startup timing issues
2026-01-09 22:40:33 +05:30
DhanushSantosh
f6fed612df merge: resolve conflicts with upstream OpenCode support
- Combined CLI disconnection markers with OpenCode support
- Added OpenCode auth/deauth routes and API methods
- Resolved merge conflicts between feature branch and upstream v0.9.0rc
2026-01-09 22:25:46 +05:30
DhanushSantosh
3b3e282df7 merge: sync with upstream v0.9.0rc branch 2026-01-09 22:10:51 +05:30
Web Dev Cody
7f69f652fb Merge pull request #390 from AutoMaker-Org/opencode-support
Opencode support
2026-01-09 11:11:07 -05:00
DhanushSantosh
1452232409 feat: fix CLI authentication detection to prevent unnecessary browser prompts
- Fix Claude, Codex, and Cursor auth handlers to check if CLI is already authenticated
- Use same detection logic as each provider's internal checkAuth/codexAuthIndicators()
- For Codex: Check for API keys and auth files before requiring manual login
- For Cursor: Check for env var and credentials files before requiring manual auth
- For Claude: Check for cached auth tokens, settings, and credentials files
- If CLI is already authenticated: Just reconnect by removing disconnected marker
- If CLI needs auth: Tell user to manually run login command
- This prevents timeout errors when login commands can't run in non-interactive mode
2026-01-09 21:34:14 +05:30
webdevcody
4f0f56a7ba feat: enhance provider setup and authentication flow
- Refactored ProvidersSetupStep component to improve the UI and streamline provider status checks for Claude, Cursor, Codex, and OpenCode.
- Introduced auto-verification for CLI authentication and improved error handling for authentication states.
- Added loading indicators for provider status checks and enhanced user feedback for installation and authentication processes.
- Updated setup store to manage verification states and ensure accurate representation of provider statuses.

These changes enhance the user experience by providing clearer feedback and a more efficient setup process for AI providers.
2026-01-09 11:03:01 -05:00
webdevcody
a695d0db7b feat: enhance OpenCode provider tests and UI setup
- Updated unit tests for OpenCode provider to include new authentication indicators.
- Refactored ProvidersSetupStep component by removing unnecessary UI elements for better clarity.
- Improved board background persistence tests by utilizing a setup function for initializing app state.
- Enhanced settings synchronization tests to ensure proper handling of login and app state.

These changes improve the testing framework and user interface for OpenCode integration, ensuring a smoother setup and authentication process.
2026-01-09 10:08:38 -05:00
webdevcody
87c3d766c9 Merge branch 'v0.9.0rc' into opencode-support 2026-01-09 09:40:16 -05:00
webdevcody
89248001e4 feat: implement OpenCode authentication and provider setup
- Added OpenCode authentication status check to the OpencodeProvider class.
- Introduced OpenCodeAuthStatus interface to manage authentication states.
- Updated detectInstallation method to include authentication status in the response.
- Created ProvidersSetupStep component to consolidate provider setup UI, including Claude, Cursor, Codex, and OpenCode.
- Refactored setup view to streamline navigation and improve user experience.
- Enhanced OpenCode CLI integration with updated installation paths and authentication checks.

This commit enhances the setup process by allowing users to configure and authenticate multiple AI providers, improving overall functionality and user experience.
2026-01-09 09:39:46 -05:00
webdevcody
41b4869068 feat: enhance feature dialogs with OpenCode model support
- Added OpenCode model selection to AddFeatureDialog and EditFeatureDialog.
- Introduced ProfileTypeahead component for improved profile selection.
- Updated model constants to include OpenCode models and integrated them into the PhaseModelSelector.
- Enhanced planning mode options with new UI elements for OpenCode.
- Refactored existing components to streamline model handling and improve user experience.

This commit expands the functionality of the feature dialogs, allowing users to select and manage OpenCode models effectively.
2026-01-09 09:02:30 -05:00
webdevcody
be88a07329 feat: add OpenCode CLI support with status endpoint
- Implemented OpenCode CLI installation and authentication status check.
- Added new route for OpenCode status in setup routes.
- Updated HttpApiClient to include method for fetching OpenCode status.
- Enhanced system paths to include OpenCode's default installation directories.

This commit introduces functionality to check the installation and authentication status of the OpenCode CLI, improving integration with the overall system.
2026-01-08 23:15:35 -05:00
Kacper
f6738ff26c fix: update getDefaultDocumentsPath to use window.electronAPI for Electron mode
- Removed dependency on getElectronAPI and directly accessed window.electronAPI for retrieving the documents path in Electron mode.
- Added handling for web mode to return null when the documents path cannot be accessed.
- Included logging for the resolved documents path to aid in debugging.
2026-01-09 00:04:28 +01:00
Shirone
ae22f781d8 Merge pull request #370 from AutoMaker-Org/feat/subagents-skills
feat: add skills and subagents configuration support
2026-01-08 23:33:52 +01:00
Kacper
e649c4ced5 refactor: reduce code duplication in agent-discovery.ts
Addresses PR feedback to reduce duplicated code in scanAgentsDirectory
by introducing an FsAdapter interface that abstracts the differences
between systemPaths (user directory) and secureFs (project directory).

Changes:
- Extract parseAgentContent helper for parsing agent file content
- Add FsAdapter interface with exists, readdir, and readFile methods
- Create createSystemPathAdapter for user-level paths
- Create createSecureFsAdapter for project-level paths
- Refactor scanAgentsDirectory to use a single loop with the adapter

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 23:02:41 +01:00
Kacper
50da1b401c Merge branch 'v0.9.0rc' into feat/subagents-skills
Resolved conflict in agent-service.ts by keeping both:
- agents parameter for custom subagents (from our branch)
- thinkingLevel and reasoningEffort parameters (from v0.9.0rc)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 22:57:09 +01:00
DhanushSantosh
33d02d1df8 Merge branch 'v0.9.0rc' of https://github.com/AutoMaker-Org/automaker into feature/codex-cli 2026-01-09 03:14:06 +05:30
Shirone
f3041190fa Merge pull request #383 from AutoMaker-Org/fix/electron-initial-auth
fix: add API key header to verifySession for Electron auth
2026-01-08 22:39:19 +01:00
webdevcody
55c2530d5a Merge branch 'v0.9.0rc' into opencode-support 2026-01-08 15:40:06 -05:00
webdevcody
5fbc7dd13e opencode support 2026-01-08 15:30:20 -05:00
DhanushSantosh
b2e5ff1460 fix: centralize model prefix handling to prevent provider errors
Moves prefix stripping from individual providers to AgentService/IdeationService
and adds validation to ensure providers receive bare model IDs. This prevents
bugs like the Codex CLI receiving "codex-gpt-5.1-codex-max" instead of the
expected "gpt-5.1-codex-max".

- Add validateBareModelId() helper with fail-fast validation
- Add originalModel field to ExecuteOptions for logging
- Update all providers to validate model has no prefix
- Centralize prefix stripping in service layer
- Remove redundant prefix stripping from individual providers

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-09 01:40:29 +05:30
Shirone
0f9232ea33 fix: add API key header to verifySession for Electron auth
The verifySession() function was not including the X-API-Key header
when making requests to /api/settings/status, causing Electron mode
to fail authentication on app startup despite having a valid API key.

This resulted in users seeing "You've been logged out" screen
immediately after launching the Electron app.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 21:00:22 +01:00
DhanushSantosh
a815be6a20 fix: update model resolver for codex- prefix and fix thinking/reasoning separation
- Add codex- prefix support in model resolver
- Fix modelSupportsThinking() to properly detect provider types
- Update CODEX_MODEL_PREFIXES to include codex- prefix
2026-01-09 00:45:31 +05:30
DhanushSantosh
7b4667eba9 fix: add provider prefixes to CLI models for clear separation
- Add 'codex-' prefix to all Codex CLI model IDs
- Add 'cursor-' prefix to Cursor CLI GPT model IDs
- Update provider-utils.ts to use prefix-based matching
- Update UI components to use prefixed model IDs
- Fix model routing to prevent Cursor picking up Codex models
2026-01-09 00:03:49 +05:30
DhanushSantosh
08ccf2632a fix: expand Codex model routing to cover ALL Codex models
- Update isCursorModel to exclude ALL Codex models from Cursor routing
- Check against CODEX_MODEL_CONFIG_MAP for comprehensive exclusion
- Includes: gpt-5.2-codex, gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5.2, gpt-5.1
- Also excludes overlapping models like gpt-5.2 and gpt-5.1 that exist in both maps
- Update test to expect CodexProvider for gpt-5.2 (correct behavior)

This ensures ALL Codex CLI models route to Codex provider, not Cursor.
Previously only gpt-5.1-codex-* and gpt-5.2-codex-* were excluded.
2026-01-08 23:17:35 +05:30
DhanushSantosh
7583598a05 fix: differentiate Codex CLI models from Cursor CLI models
- Fix isCursorModel to exclude Codex-specific models (gpt-5.1-codex-*, gpt-5.2-codex-*)
- These models should route to Codex provider, not Cursor provider
- Add CODEX_YOLO_FLAG constant for --dangerously-bypass-approvals-and-sandbox
- Always use YOLO flag in codex-provider for full permissions
- Simplify codex CLI args to minimal set with YOLO flag
- Update tests to reflect new behavior with YOLO flag

This fixes the bug where selecting a Codex model (e.g., gpt-5.1-codex-max)
was incorrectly spawning cursor-agent instead of codex exec.

The root cause was:
1. Cursor provider had higher priority (10) than Codex (5)
2. isCursorModel() returned true for Codex models in CURSOR_MODEL_MAP
3. Models like gpt-5.1-codex-max routed to Cursor instead of Codex

The fix:
1. isCursorModel now excludes Codex-specific model IDs
2. Codex always uses --dangerously-bypass-approvals-and-sandbox flag
2026-01-08 23:03:03 +05:30
DhanushSantosh
7e68691e92 fix: remove sandbox and approval policy dropdowns from Codex settings UI
- Remove SANDBOX_OPTIONS and APPROVAL_OPTIONS from codex-settings.tsx
- Remove Select components for sandbox mode and approval policy
- Remove codexSandboxMode and codexApprovalPolicy from CodexSettingsProps interface
- Update codex-settings-tab.tsx to pass only simplified props
- Codex now always runs with full permissions (--dangerously-bypass-approvals-and-sandbox)

The UI no longer shows sandbox or approval settings since Codex always uses full permissions.
2026-01-08 22:27:18 +05:30
DhanushSantosh
4b2034b834 fix: Codex CLI always runs with full permissions (--dangerously-bypass-approvals-and-sandbox)
- Always use CODEX_YOLO_FLAG (--dangerously-bypass-approvals-and-sandbox) for Codex
- Remove all conditional logic - no sandbox/approval config, no config overrides
- Simplify codex-provider.ts to always run Codex in full-permissions mode
- Codex always gets: full access, no approvals, web search enabled, images enabled
- Update services to apply full-permission settings automatically for Codex models
- Remove sandbox and approval controls from UI settings page
- Update tests to reflect new behavior (some pre-existing tests disabled/updated)

Note: 3 pre-existing tests disabled/skipped due to old behavior expectations (require separate PR to update)
2026-01-08 21:56:32 +05:30
DhanushSantosh
4dcf54146c feat: add reasoning effort support for Codex models
- Add ReasoningEffortSelector component for UI selection
- Integrate reasoning effort in feature creation/editing dialogs
- Add reasoning effort support to phase model selector
- Update agent service and board actions to handle reasoning effort
- Add reasoning effort fields to feature and settings types
- Update model selector and agent info panel with reasoning effort display
- Enhance agent context parser for reasoning effort processing

Reasoning effort allows fine-tuned control over Codex model reasoning
capabilities, providing options from 'none' to 'xhigh' for different
task complexity requirements.
2026-01-08 20:43:36 +05:30
DhanushSantosh
8a9715adef fix: AI profile display issues for Codex models
- Fix Codex profiles showing as 'Claude Sonnet' in UI
- Add proper Codex model display in profile cards and selectors
- Add useEffect to sync profile form data when editing profiles
- Update provider badges to show 'Codex' with OpenAI icon
- Enhance profile selection validation across feature dialogs
- Add getCodexModelLabel support to display functions

The issue was that profile display functions only handled Claude and Cursor
providers, causing Codex profiles to fallback to 'sonnet' display.
2026-01-08 17:13:45 +05:30
DhanushSantosh
d253d494ba feat: enhance Codex usage tracking with multiple data sources
- Try OpenAI API if API key is available
- Parse rate limit info from Codex CLI responses
- Extract plan type from Codex auth file
- Provide helpful configuration message when usage unavailable
2026-01-08 15:13:05 +05:30
DhanushSantosh
d1f7794afa Merge branch 'main' of https://github.com/AutoMaker-Org/automaker into feature/codex-cli 2026-01-08 14:38:23 +05:30
DhanushSantosh
4d80a93710 refactor: update Codex models to latest OpenAI models
- Replace gpt-5-codex, gpt-5-codex-mini, codex-1, codex-mini-latest, gpt-5
- Add gpt-5.1-codex-max, gpt-5.1-codex-mini, gpt-5.2, gpt-5.1
- Update thinking support for all Codex models
2026-01-08 14:22:35 +05:30
webdevcody
d70faf3b28 Merge branch 'v0.9.0rc' into feat/subagents-skills 2026-01-08 00:33:30 -05:00
Web Dev Cody
271749a5a4 Merge pull request #375 from yumesha/main
fix background image and create test incase background image failed again
2026-01-08 00:26:31 -05:00
Web Dev Cody
d1bd131cab Merge pull request #378 from AutoMaker-Org/remove-sandbox-as-it-is-broken
completly remove sandbox related code as the downstream libraries do …
2026-01-08 00:25:30 -05:00
webdevcody
96fe90ca65 chore: remove worktree integration E2E test file
- Deleted the worktree integration test file to streamline the test suite and remove obsolete tests. This change helps maintain focus on relevant test cases and improves overall test management.
2026-01-08 00:23:00 -05:00
webdevcody
fd5f7b873a fix: improve worktree branch handling in list route
- Updated the logic in the createListHandler to ensure that the branch name is correctly assigned, especially for the main worktree when it may be missing.
- Added checks to handle cases where the worktree directory might not exist, ensuring that removed worktrees are accurately tracked.
- Enhanced the final worktree entry handling to account for scenarios where the output does not end with a blank line, improving robustness.
2026-01-08 00:13:12 -05:00
webdevcody
959467de90 feat: add UI test command and clean up integration test
- Introduced a new npm script "test:ui" for running UI tests in the apps/ui workspace.
- Removed unnecessary login screen handling from the worktree integration test to streamline the test flow.
2026-01-08 00:07:23 -05:00
webdevcody
69434fe356 feat: enhance login view with retry mechanism for server checks
- Added useRef to manage AbortController for retry requests in the LoginView component.
- Implemented logic to abort any ongoing retry requests before initiating a new server check, improving error handling and user experience during login attempts.
2026-01-07 23:18:52 -05:00
webdevcody
dc264bd164 feat: update E2E fixture settings and improve test repository initialization
- Changed the E2E settings to enable the use of worktrees for better test isolation.
- Modified the test repository initialization to explicitly set the initial branch to 'main', ensuring compatibility across different git versions and avoiding CI environment discrepancies.
2026-01-07 23:10:02 -05:00
webdevcody
8992f667c7 refactor: clean up settings service and improve E2E fixture descriptions
- Removed the redundant call to ignore empty array overwrite for 'enabledCodexModels' in the SettingsService.
- Reformatted the description of the 'Heavy Task' profile in the E2E fixture setup script for better readability.
2026-01-07 23:04:27 -05:00
webdevcody
eb627ef323 feat: enhance E2E test setup and error handling
- Updated Playwright configuration to explicitly unset ALLOWED_ROOT_DIRECTORY for unrestricted testing paths.
- Improved E2E fixture setup script to reset server settings to a known state, ensuring test isolation.
- Enhanced error handling in ContextView and WelcomeView components to reset state and provide user feedback on failures.
- Updated tests to ensure proper navigation and visibility checks during logout processes, improving reliability.
2026-01-07 23:01:57 -05:00
webdevcody
d8cdb0bf7a feat: enhance global settings update with data loss prevention
- Added safeguards to prevent overwriting non-empty arrays with empty arrays during global settings updates, specifically for the 'projects' field.
- Implemented logging for updates to assist in diagnosing accidental wipes of critical settings.
- Updated tests to verify that projects are preserved during logout transitions and that theme changes are ignored if a project wipe is attempted.
- Enhanced the settings synchronization logic to ensure safe handling during authentication state changes.
2026-01-07 21:38:46 -05:00
webdevcody
8c68c24716 feat: implement Codex CLI authentication check and integrate with provider
- Added a new utility for checking Codex CLI authentication status using the 'codex login status' command.
- Integrated the authentication check into the CodexProvider's installation detection and authentication methods.
- Updated Codex CLI status display in the UI to reflect authentication status and method.
- Enhanced error handling and logging for better debugging during authentication checks.
- Refactored related components to ensure consistent handling of authentication across the application.
2026-01-07 21:06:39 -05:00
antdev
9b302583c4 fix background image part 2 2026-01-08 09:23:57 +08:00
webdevcody
47c2d795e0 chore: update e2e test results upload configuration
- Renamed the upload step to clarify that it includes screenshots, traces, and videos.
- Changed the condition for uploading test results to always run, ensuring artifacts are uploaded regardless of test outcome.
- Added a new option to ignore if no files are found during the upload process.
2026-01-07 20:00:52 -05:00
yumesha
d608d8c2d4 Merge branch 'AutoMaker-Org:main' into main 2026-01-08 08:52:59 +08:00
webdevcody
f737b1f30a merge in v0.9.0 2026-01-07 18:22:32 -05:00
webdevcody
8b36fce7d7 refactor: improve test stability and clarity in various test cases
- Updated the 'Add Context Image' test to simplify file verification by relying on UI visibility instead of disk checks.
- Enhanced the 'Feature Manual Review Flow' test with better project setup and API interception to ensure consistent test conditions.
- Improved the 'AI Profiles' test by replacing arbitrary timeouts with dynamic checks for profile count.
- Refined the 'Project Creation' and 'Open Existing Project' tests to ensure proper project visibility and settings management during tests.
- Added mechanisms to prevent settings hydration from restoring previous project states, ensuring tests run in isolation.
- Removed unused test image from fixtures to clean up the repository.
2026-01-07 18:07:27 -05:00
DhanushSantosh
30a2a1c921 feat: add unified usage popover with Claude and Codex tabs
- Created combined UsagePopover component with tab switching between providers
- Added Codex usage API endpoint and service (returns not available message)
- Updated BoardHeader to show single usage button for both providers
- Enhanced type definitions for Codex usage with primary/secondary rate limits
- Wired up Codex usage API in HTTP client

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-08 03:37:37 +05:30
webdevcody
763f9832c3 feat: enhance test setup with splash screen handling and sandbox warnings
- Added `skipSandboxWarning` option to project setup functions to streamline testing.
- Implemented logic to disable the splash screen during tests by setting `automaker-splash-shown` in sessionStorage.
- Introduced a new package.json for a test project and added a test image to the fixtures for improved testing capabilities.
2026-01-07 16:31:48 -05:00
webdevcody
11b1bbc143 feat: implement splash screen handling in navigation and interactions
- Added a new function `waitForSplashScreenToDisappear` to manage splash screen visibility, ensuring it does not block user interactions.
- Integrated splash screen checks in various navigation functions and interaction methods to enhance user experience by waiting for the splash screen to disappear before proceeding.
- Updated test setup to disable the splash screen during tests for consistent testing behavior.
2026-01-07 16:10:17 -05:00
webdevcody
7176d3e513 fix: enhance sandbox compatibility checks in sdk-options and improve login view effect handling
- Added additional cloud storage path patterns for macOS and Linux to the checkSandboxCompatibility function, ensuring better compatibility with sandbox environments.
- Revised the login view to simplify the initial server/session check logic, removing unnecessary ref guard and improving responsiveness during component unmounting.
2026-01-07 15:54:17 -05:00
DhanushSantosh
9c3ba34b51 Merge remote-tracking branch 'upstream/v0.9.0rc' into feature/codex-cli 2026-01-08 01:57:27 +05:30
DhanushSantosh
ff3af937da fix: update event type in CodexProvider from threadCompleted to turnCompleted
- Changed the event type from 'thread.completed' to 'turn.completed' in the CODEX_EVENT_TYPES constant and its usage within the CodexProvider class.
- This update aligns the event handling with the intended functionality, ensuring correct event processing.
2026-01-08 01:54:02 +05:30
webdevcody
b9fcb916a6 fix: add missing checkSandboxCompatibility function to sdk-options
The codex-provider.ts imports this function but it was missing from
sdk-options.ts. This adds the implementation that checks if sandbox
mode is compatible with the working directory (disables sandbox for
cloud storage paths).

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 15:13:52 -05:00
webdevcody
cfa1f114fd Merge branch 'v0.9.0rc' into remove-sandbox-as-it-is-broken 2026-01-07 15:01:31 -05:00
Web Dev Cody
761929ea8e Merge pull request #367 from DhanushSantosh/feature/codex-cli
Codex CLI Implementation
2026-01-07 14:44:48 -05:00
webdevcody
4d36e66deb refactor: update session cookie options and improve login view authentication flow
- Revised SameSite attribute for session cookies to clarify its behavior in documentation.
- Streamlined cookie clearing logic in the authentication route by utilizing `getSessionCookieOptions()`.
- Enhanced the login view to support aborting server checks, improving responsiveness during component unmounting.
- Ensured proper handling of server check retries with abort signal integration for better user experience.
2026-01-07 14:33:55 -05:00
webdevcody
e58e389658 feat: implement settings migration from localStorage to server
- Added logic to perform settings migration, merging localStorage data with server settings if necessary.
- Introduced `localStorageMigrated` flag to prevent re-migration on subsequent app loads.
- Updated `useSettingsMigration` hook to handle migration and hydration of settings.
- Ensured localStorage values are preserved post-migration for user flexibility.
- Enhanced documentation within the migration logic for clarity.
2026-01-07 14:29:32 -05:00
DhanushSantosh
821827f850 refactor: simplify config value formatting in CodexProvider
- Removed unnecessary JSON.stringify conversion for string values in formatConfigValue function, streamlining the value formatting process.
- This change enhances code clarity and reduces complexity in the configuration handling of the CodexProvider.
2026-01-08 00:27:11 +05:30
DhanushSantosh
9d8464cceb feat: enhance CodexProvider argument handling and configuration
- Added approval policy and web search features to the CodexProvider's argument construction, improving flexibility in command execution.
- Updated unit tests to validate the new configuration handling for approval and search features, ensuring accurate argument parsing.

These changes enhance the functionality of the CodexProvider, allowing for more dynamic command configurations and improving test coverage.
2026-01-08 00:16:57 +05:30
DhanushSantosh
48a4fa5c6c refactor: streamline argument handling in CodexProvider
- Reorganized argument construction in CodexProvider to separate pre-execution arguments from global flags, improving clarity and maintainability.
- Updated unit tests to reflect changes in argument order, ensuring correct validation of approval and search indices.

These changes enhance the structure of the CodexProvider's command execution process and improve test reliability.
2026-01-08 00:04:52 +05:30
webdevcody
70c04b5a3f feat: update session cookie options and enhance authentication flow
- Changed SameSite attribute for session cookies from 'strict' to 'lax' to allow cross-origin fetches, improving compatibility with various client requests.
- Updated cookie clearing logic in the authentication route to use `res.cookie()` for better reliability in cross-origin environments.
- Refactored the login view to implement a state machine for managing authentication phases, enhancing clarity and maintainability.
- Introduced a new logged-out view to inform users of session expiration and provide options to log in or retry.
- Added account and security sections to the settings view, allowing users to manage their account and security preferences more effectively.
2026-01-07 12:55:23 -05:00
DhanushSantosh
24ea10e818 feat: enhance Codex authentication and API key management
- Introduced a new method to check Codex authentication status, allowing for better handling of API keys and OAuth tokens.
- Updated API key management to include OpenAI, enabling users to manage their keys more effectively.
- Enhanced the CodexProvider to support session ID tracking and deduplication of text blocks in assistant messages.
- Improved error handling and logging in authentication routes, providing clearer feedback to users.

These changes improve the overall user experience and security of the Codex integration, ensuring smoother authentication processes and better management of API keys.
2026-01-07 22:49:30 +05:30
webdevcody
927451013c feat: add sandbox risk confirmation and rejection screens
- Introduced `SandboxRiskDialog` to prompt users about risks when running outside a containerized environment.
- Added `SandboxRejectionScreen` for users who deny the sandbox risk confirmation, providing options to reload or restart the app.
- Updated settings view and danger zone section to manage sandbox warning preferences.
- Implemented a new API endpoint to check if the application is running in a containerized environment.
- Enhanced state management to handle sandbox warning settings across the application.
2026-01-07 10:41:43 -05:00
webdevcody
0d206fe75f feat: enhance login view with session verification and loading state
- Implemented session verification on component mount using exponential backoff to handle server live reload scenarios.
- Added loading state to the login view while checking for an existing session, improving user experience.
- Removed unused setup wizard navigation from the API keys section for cleaner code.
2026-01-07 10:18:06 -05:00
webdevcody
11accac5ae feat: implement API-first settings management and description history tracking
- Migrated settings persistence from localStorage to an API-first approach, ensuring consistency between Electron and web modes.
- Introduced `useSettingsSync` hook for automatic synchronization of settings to the server with debouncing.
- Enhanced feature update logic to track description changes with a history, allowing for better management of feature descriptions.
- Updated various components and services to utilize the new settings structure and description history functionality.
- Removed persist middleware from Zustand store, streamlining state management and improving performance.
2026-01-07 10:05:54 -05:00
DhanushSantosh
2250367ddc chore: update npm audit level in CI workflow
- Changed the npm audit command in the security audit workflow to check for critical vulnerabilities instead of moderate ones.
- This adjustment enhances the security posture of the application by ensuring that critical issues are identified and addressed promptly.
2026-01-07 20:24:49 +05:30
DhanushSantosh
fe305bbc81 feat: add vision support validation for image processing
- Introduced a new method in ProviderFactory to check if a model supports vision/image input.
- Updated AgentService and AutoModeService to validate vision support before processing images, throwing an error if the model does not support it.
- Enhanced error messages to guide users on switching models or removing images if necessary.

These changes improve the robustness of image processing by ensuring compatibility with the selected models.
2026-01-07 20:12:39 +05:30
DhanushSantosh
92195340c6 feat: enhance authentication handling and API key validation
- Added optional API keys for OpenAI and Cursor to the .env.example file.
- Implemented API key validation in CursorProvider to ensure valid keys are used.
- Introduced rate limiting in Claude and Codex authentication routes to prevent abuse.
- Created secure environment handling for authentication without modifying process.env.
- Improved error handling and logging for authentication processes, enhancing user feedback.

These changes improve the security and reliability of the authentication mechanisms across the application.
2026-01-07 19:26:42 +05:30
webdevcody
1316ead8c8 completly remove sandbox related code as the downstream libraries do not work with it on various os 2026-01-07 08:54:14 -05:00
DhanushSantosh
03b33106e0 fix: replace git+ssh URLs with https in package-lock.json
- Configure git to use HTTPS for GitHub URLs globally
- Run npm run fix:lockfile to rewrite package-lock.json
- Resolves lint-lockfile failure in CI/CD environments
2026-01-07 19:09:26 +05:30
DhanushSantosh
251f0fd88e chore: update CI configuration and enhance test stability
- Added deterministic API key and environment variables in e2e-tests.yml to ensure consistent test behavior.
- Refactored CodexProvider tests to improve type safety and mock handling, ensuring reliable test execution.
- Updated provider-factory tests to mock installation detection for CodexProvider, enhancing test isolation.
- Adjusted Playwright configuration to conditionally use external backend, improving flexibility in test environments.
- Enhanced kill-test-servers script to handle external server scenarios, ensuring proper cleanup of test processes.

These changes improve the reliability and maintainability of the testing framework, leading to a more stable development experience.
2026-01-07 19:09:26 +05:30
DhanushSantosh
96f154d440 refactor: enhance type safety and error handling in navigation and project creation
- Updated navigation functions to cast route paths correctly, improving type safety.
- Added error handling for the templates API in project creation hooks to ensure robustness.
- Refactored task progress panel to improve type handling for feature data.
- Introduced type checks and default values in various components to enhance overall stability.

These changes improve the reliability and maintainability of the application, ensuring better user experience and code quality.
2026-01-07 19:09:25 +05:30
DhanushSantosh
27c6d5a3bb refactor: improve error handling and CLI integration
- Updated CodexProvider to read prompts from stdin to prevent shell escaping issues.
- Enhanced AgentService to handle streamed error messages from providers, ensuring a consistent user experience.
- Modified UI components to display error messages clearly, including visual indicators for errors in chat bubbles.
- Updated CLI status handling to support both Claude and Codex APIs, improving compatibility and user feedback.

These changes enhance the robustness of the application and improve the user experience during error scenarios.
2026-01-07 19:09:25 +05:30
DhanushSantosh
a57dcc170d feature/codex-cli 2026-01-07 19:09:24 +05:30
Shirone
5c601ff200 feat: implement subagents configuration management
- Added a new function to retrieve subagents configuration from settings, allowing users to enable/disable subagents and select sources for loading them.
- Updated the AgentService to incorporate subagents configuration, dynamically adding tools based on the settings.
- Enhanced the UI components to manage subagents, including a settings section for enabling/disabling and selecting sources.
- Introduced a new hook for managing subagents settings state and interactions.

These changes improve the flexibility and usability of subagents within the application, enhancing user experience and configuration options.
2026-01-07 10:32:42 +01:00
webdevcody
4d4025ca06 fix: improve WebSocket connection handling in Electron mode
- Updated the logic for establishing WebSocket connections in Electron mode to handle cases where the API key is unavailable.
- Added fallback to wsToken/cookie authentication for real-time event updates, enhancing reliability in external server scenarios.
- Improved logging for better debugging of WebSocket connection issues.
2026-01-06 22:29:08 -05:00
Web Dev Cody
77f253c7bd Merge pull request #376 from AutoMaker-Org/electron-with-docker-api
feat: add support for external server mode with Docker integration
2026-01-06 21:12:49 -05:00
Shirone
fe13d47b24 refactor: improve agent file model validation and settings source deduplication
- Enhanced model parsing in agent discovery to validate against allowed values and log warnings for invalid models.
- Refactored settingSources construction in AgentService to utilize Set for automatic deduplication, simplifying the merging of user and project settings with skills sources.
- Updated tests to reflect changes in allowedTools for improved functionality.

These changes enhance the robustness of agent configuration and streamline settings management.
2026-01-07 00:05:33 +01:00
Shirone
33acf502ed refactor: enhance skills and subagents settings management
- Updated useSkillsSettings and useSubagents hooks to improve state management and error handling.
- Added new settings API methods for skills configuration and agent discovery.
- Refactored app-store to include enableSkills and skillsSources state management.
- Enhanced settings migration to sync skills configuration with the server.

These changes streamline the management of skills and subagents, ensuring better integration and user experience.
2026-01-06 23:43:31 +01:00
webdevcody
66557b2093 feat: add support for external server mode with Docker integration
- Introduced a new `docker-compose.dev-server.yml` for running the backend API in a container, enabling local Electron to connect to it.
- Updated `dev.mjs` to include a new option for launching the Docker server container.
- Enhanced the UI application to support external server mode, allowing session-based authentication and adjusting routing logic accordingly.
- Added utility functions to check and cache the external server mode status for improved performance.
- Updated various components to handle authentication and routing based on the server mode.
2026-01-06 17:26:25 -05:00
Web Dev Cody
c713cef484 Merge pull request #360 from AutoMaker-Org/feat/mass-edit-backlog-features
feat: add mass edit feature for backlog kanban cards
2026-01-06 16:17:14 -05:00
webdevcody
5f7cbd3435 refactor: improve logging format in launchDockerContainers function
- Updated the logging format in the launchDockerContainers function to enhance readability by breaking long lines into multiple lines. This change improves the clarity of log messages when starting Docker containers.
2026-01-06 16:14:10 -05:00
webdevcody
a4290b5863 feat: enhance development environment with Docker support and UI improvements
- Introduced a new `docker-compose.dev.yml` for development mode, enabling live reload and improved container management.
- Updated `dev.mjs` to utilize `launchDockerDevContainers` for starting development containers with live reload capabilities.
- Refactored `printModeMenu` to differentiate between development and production Docker options.
- Enhanced the `BoardView` and `KanbanBoard` components by streamlining props and improving UI interactions.
- Removed the `start.mjs` script, consolidating production launch logic into `dev.mjs` for a more unified approach.
2026-01-06 16:11:29 -05:00
webdevcody
f9b0a38642 Merge branch 'main' into feat/mass-edit-backlog-features 2026-01-06 14:38:59 -05:00
antdev
46636cf385 fix background image and create test in case background image failed again 2026-01-07 02:04:22 +08:00
antdev
e24a6894a5 Merge remote-tracking branch 'refs/remotes/origin/main'
# Conflicts:
#	apps/ui/src/routes/__root.tsx
2026-01-07 01:48:34 +08:00
antdev
cf9289e21a fix background image and create test incase background image failed again 2026-01-07 01:21:46 +08:00
webdevcody
fe7bc954ba chore: add OpenSSH client to Dockerfile for enhanced SSH capabilities
- Updated the Dockerfile to include the OpenSSH client, improving the container's ability to handle SSH connections and operations.
2026-01-06 00:36:45 -05:00
Shirone
236989bf6e feat: add skills and subagents configuration support
- Updated .gitignore to include skills directory.
- Introduced agent discovery functionality to scan for AGENT.md files in user and project directories.
- Added new API endpoint for discovering filesystem agents.
- Implemented UI components for managing skills and viewing custom subagents.
- Enhanced settings helpers to retrieve skills configuration and custom subagents.
- Updated agent service to incorporate skills and subagents in task delegation.

These changes enhance the capabilities of the system by allowing users to define and manage skills and custom subagents effectively.
2026-01-06 04:31:57 +01:00
Web Dev Cody
8a6a83bf52 Merge pull request #366 from AutoMaker-Org/cursor-docker-oauth
feat: add Cursor CLI installation attempts documentation and enhance …
2026-01-05 21:53:31 -05:00
webdevcody
84b582ffa7 refactor: streamline Docker container management and enhance utility functions
- Removed redundant Docker image rebuilding logic from `dev.mjs` and `start.mjs`, centralizing it in the new `launchDockerContainers` function within `launcher-utils.mjs`.
- Introduced `sanitizeProjectName` and `shouldRebuildDockerImages` functions to improve project name handling and Docker image management.
- Updated the Docker launch process to provide clearer logging and ensure proper handling of environment variables, enhancing the overall development experience.
2026-01-05 21:50:12 -05:00
webdevcody
bd5176165d refactor: remove duplicate logger initialization in useCliStatus hook
- Eliminated redundant logger declaration within the useCliStatus hook to improve code clarity and prevent potential performance issues.
- This change enhances the maintainability of the code by ensuring the logger is created only once outside the hook.
2026-01-05 21:38:18 -05:00
webdevcody
49f32c4d59 Merge branch 'main' of github.com:AutoMaker-Org/automaker into cursor-docker-oauth 2026-01-05 21:29:20 -05:00
webdevcody
0af5bc86f4 Merge branch 'cursor-docker-oauth' of github.com:AutoMaker-Org/automaker into cursor-docker-oauth 2026-01-05 21:29:01 -05:00
webdevcody
bc5a36c5f4 feat: enhance project name sanitization and improve Docker image naming
- Added a `sanitizeProjectName` function to ensure project names are safe for shell commands and Docker image names by converting them to lowercase and removing non-alphanumeric characters.
- Updated `dev.mjs` and `start.mjs` to utilize the new sanitization function when determining Docker image names, enhancing security and consistency.
- Refactored the Docker entrypoint script to ensure proper permissions for the Cursor CLI config directory, improving setup reliability.
- Clarified documentation regarding the storage location of OAuth tokens for the Cursor CLI on Linux.

These changes improve the robustness of the Docker setup and enhance the overall development workflow.
2026-01-05 21:28:42 -05:00
Web Dev Cody
2934d73db2 Merge pull request #368 from AutoMaker-Org/fix/small-bugs
fix: small bugs
2026-01-05 20:23:42 -05:00
Shirone
a4968f7235 fix: show success toast only during project creation flow
- Updated the useSpecRegeneration hook to conditionally display the success toast message only when the user is in the active project creation flow, preventing unnecessary notifications during regular spec regeneration.
2026-01-06 02:04:08 +01:00
Shirone
b8e0c18c53 fix: theme switch bug
- when user had set up theme on the project lvl i and went trought the setup wizard again and changed theme its was not updating because its was only updating global theme and app was reverting back to show current project theme
2026-01-06 02:00:41 +01:00
Shirone
d0b3e0d9bb refactor: move logger initialization outside of useCliStatus function
- Moved the logger initialization to the top of the file for better readability and to avoid re-initialization on each function call.
- This change enhances the performance and clarity of the code in the useCliStatus hook.
- fix infinite loop calling caused by rerender because of logger
2026-01-06 01:53:08 +01:00
Kacper
2a0719e00c refactor: move logger initialization outside of useCliStatus hook
- Moved the logger creation outside the hook to prevent infinite re-renders.
- Updated dependencies in the checkStatus function to remove logger from the dependency array.

These changes enhance performance and maintainability of the useCliStatus hook.
2026-01-06 00:58:31 +01:00
webdevcody
af394183e6 feat: add Cursor CLI installation attempts documentation and enhance Docker setup
- Introduced a new markdown file summarizing various attempts to install the Cursor CLI in Docker, detailing approaches, results, and key learnings.
- Updated Dockerfile to ensure proper installation of Cursor CLI for the non-root user, including necessary PATH adjustments for interactive shells.
- Enhanced entrypoint script to manage OAuth tokens for both Claude and Cursor CLIs, ensuring correct permissions and directory setups.
- Added scripts for extracting OAuth tokens from macOS Keychain and Linux JSON files for seamless integration with Docker.
- Updated docker-compose files to support persistent storage for CLI configurations and authentication tokens.

These changes improve the development workflow and provide clear guidance on CLI installation and authentication processes.
2026-01-05 18:13:14 -05:00
Shirone
1117afc37a refactor: update mass edit dialog and introduce new select components
- Removed advanced options toggle and related state from the mass edit dialog for a cleaner UI.
- Replaced ProfileQuickSelect with ProfileSelect for better profile management.
- Introduced new PlanningModeSelect and PrioritySelect components for streamlined selection of planning modes and priorities.
- Updated imports in shared index to include new select components.
- Enhanced the mass edit dialog to utilize the new components, improving user experience during bulk edits.
2026-01-04 23:24:24 +01:00
Kacper
06c02de1cb feat: add mass edit feature for backlog kanban cards
Add ability to select multiple backlog features and edit their configuration
in bulk. Selection is limited to backlog column features in the current
branch/worktree only.

Changes:
- Add selection mode toggle in board controls
- Add checkbox selection on kanban cards (backlog only)
- Disable drag and drop during selection mode
- Hide action buttons during selection mode
- Add floating selection action bar with Edit/Clear/Select All
- Add mass edit dialog with all configuration options in single scroll view
- Add server endpoint for bulk feature updates
2026-01-04 22:25:19 +01:00
Stephan Rieche
0fa5fdd478 docs: add comprehensive JSDoc docstrings for pipeline resume methods
Add detailed JSDoc documentation to meet 80% docstring coverage requirement:

- PipelineStatusInfo interface: Document all properties with types and descriptions
- resumePipelineFeature(): Document edge case handling and parameters
- resumeFromPipelineStep(): Document complete pipeline resume workflow
- detectPipelineStatus(): Document pipeline status detection scenarios

Each docstring includes:
- Clear method purpose and behavior
- All parameters with types and descriptions
- Return value documentation
- Error conditions and exceptions
- @private tags for internal methods

This improves code maintainability and helps developers understand the
complex pipeline resume logic.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 15:00:25 +01:00
Stephan Rieche
472342c246 chore: run prettier to fix formatting
Auto-format all files to fix format-check CI failure.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 13:56:39 +01:00
Stephan Rieche
71e03c2a13 fix: restore Verify button fallback with honest labeling
Re-add the onVerify fallback for in_progress/pipeline features without context,
but fix the misleading UX issue where the button said 'Resume' but executed
verification (tests/build).

Changes:
- Restore onVerify fallback as 3rd option after skipTests Verify and Resume
- Change button label from 'Resume' to 'Verify' (honest!)
- Change icon from PlayCircle to CheckCircle2 (matches action)
- Keep same green styling for consistency

This makes sense because if a feature is in_progress but has no context,
it likely completed execution but the context was deleted. User should be
able to verify it (run tests/build) rather than having no action available.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 13:55:05 +01:00
Stephan Rieche
c3403c033c Merge upstream/main into fix/pipeline-resume-edge-cases
Resolved conflict in card-actions.tsx by:
- Keeping pipeline_status check from our branch (supports pipeline_step_* statuses)
- Adopting simplified Resume button logic from main (removed hasContext check and onVerify fallback)

The Resume button now appears for features with:
- status === 'in_progress'
- status.startsWith('pipeline_')

This combines our pipeline support fix with main's simplified button rendering logic.
2026-01-02 13:45:12 +01:00
Stephan Rieche
2a87d55519 fix: show Resume button for features stuck in pipeline steps
Enable the Resume button to appear for features with pipeline status
(e.g., 'pipeline_step_xyz') in addition to 'in_progress' status.

Previously, features that crashed during pipeline execution would show
a 'testing' status badge but no Resume button, making it impossible to
resume them from the UI.

Changes:
- Update card-actions.tsx condition to include pipeline_ status check
- Resume button now shows for both in_progress and pipeline_step_* statuses
- Maintains all existing behavior for other feature states

This fixes the UX issue where users could see a feature was stuck in a
pipeline step but had no way to resume it.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 13:22:11 +01:00
Stephan Rieche
2d309f6833 perf: eliminate redundant file read in detectPipelineStatus
Remove unnecessary call to pipelineService.getStep() which was causing
a redundant file read of pipeline.json. The config is already loaded at
line 2807, so we can find the step directly from the in-memory config.

Changes:
- Sort config.steps first
- Find stepIndex using findIndex()
- Get step directly from sortedSteps[stepIndex] instead of calling getStep()
- Simplify null check to only check !step instead of stepIndex === -1 || !step

This optimization reduces file I/O operations and improves performance when
resuming pipeline features.

Co-authored-by: gemini-code-assist bot

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 13:04:01 +01:00
Stephan Rieche
7a2a3ef500 refactor: create PipelineStatusInfo interface to eliminate type duplication
Define a dedicated PipelineStatusInfo interface and use it consistently in both
resumePipelineFeature() parameter and detectPipelineStatus() return type.

This eliminates duplicate inline type definitions and improves maintainability
by ensuring both locations always stay in sync. Any future changes to the
pipeline status structure only need to be made in one place.

Changes:
- Add PipelineStatusInfo interface definition
- Replace inline type in resumePipelineFeature() with PipelineStatusInfo
- Replace inline return type in detectPipelineStatus() with PipelineStatusInfo

Co-authored-by: gemini-code-assist bot

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 13:02:43 +01:00
Stephan Rieche
3ff9658723 refactor: remove unnecessary runningFeatures.delete() calls
Remove confusing and unnecessary delete calls from resumeFeature() and
resumePipelineFeature() methods. These were leftovers from a previous
implementation where temporary entries were added to runningFeatures.

The resumeFeature() method already ensures the feature is not running
at the start (via has() check that throws if already running), so these
delete calls serve no purpose and only add confusion about state management.

Removed delete calls from:
- resumeFeature() non-pipeline flow (line 748)
- resumePipelineFeature() no-context case (line 798)
- resumePipelineFeature() step-not-found case (line 822)

Co-authored-by: gemini-code-assist bot

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 12:44:19 +01:00
Stephan Rieche
c587947de6 refactor: optimize feature loading in pipeline resume
Reduce redundant file reads by loading the feature object once and passing
it down the call chain instead of reloading it multiple times.

Changes:
- Pass feature object to resumePipelineFeature() instead of featureId
- Pass feature object to resumeFromPipelineStep() instead of featureId
- Remove redundant loadFeature() calls from these methods
- Add FeatureStatusWithPipeline import for type safety

This improves performance by eliminating unnecessary file I/O operations
and makes the data flow clearer.

Co-authored-by: gemini-code-assist bot

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 12:42:04 +01:00
Stephan Rieche
a9403651d4 fix: handle pipeline resume edge cases and improve robustness
This commit fixes several edge cases when resuming features stuck in pipeline steps:

- Detect if feature is stuck in a pipeline step during resume
- Handle case where context file is missing (restart from beginning)
- Handle case where pipeline step no longer exists in config
- Add dedicated resumePipelineFeature() method for pipeline-specific resume logic
- Add detectPipelineStatus() to extract and validate pipeline step information
- Add resumeFromPipelineStep() to resume from a specific pipeline step index
- Update board view to check context availability for features with pipeline status

Edge cases handled:
1. No context file → restart entire pipeline from beginning
2. Step no longer exists in config → complete feature without pipeline
3. Valid step exists → resume from the crashed step

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 00:58:32 +01:00
Ramiro Rivera
d2f64f10ff test: add tests for ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN passthrough
- apps/server/tests/unit/providers/claude-provider.test.ts

Verify custom endpoint environment variables are passed to the SDK.
2026-01-01 13:43:12 +01:00
Ramiro Rivera
9fe5b485f8 feat: support ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN for custom endpoints
- apps/server/src/providers/claude-provider.ts

Add ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN to the environment variable
allowlist, enabling use of LLM gateways (LiteLLM, Helicone) and Anthropic-
compatible providers (GLM 4.7, Minimax M2.1, etc.).

Closes #338
2026-01-01 13:35:10 +01:00
650 changed files with 80340 additions and 14864 deletions

1
.claude/.gitignore vendored
View File

@@ -1 +1,2 @@
hans/
skills/

View File

@@ -0,0 +1,108 @@
name: Feature Request
description: Suggest a new feature or enhancement for Automaker
title: '[Feature]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a feature! Please fill out the form below to help us understand your request.
- type: dropdown
id: feature-area
attributes:
label: Feature Area
description: Which area of Automaker does this feature relate to?
options:
- UI/UX (User Interface)
- Agent/AI
- Kanban Board
- Git/Worktree Management
- Project Management
- Settings/Configuration
- Documentation
- Performance
- Other
default: 0
validations:
required: true
- type: dropdown
id: priority
attributes:
label: Priority
description: How important is this feature to your workflow?
options:
- Nice to have
- Would improve my workflow
- Critical for my use case
default: 0
validations:
required: true
- type: textarea
id: problem-statement
attributes:
label: Problem Statement
description: Is your feature request related to a problem? Please describe the problem you're trying to solve.
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when...
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like to see implemented.
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives-considered
attributes:
label: Alternatives Considered
description: Describe any alternative solutions or workarounds you've considered.
placeholder: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: use-cases
attributes:
label: Use Cases
description: Describe specific scenarios where this feature would be useful.
placeholder: |
1. When working on...
2. As a user who needs to...
3. In situations where...
validations:
required: false
- type: textarea
id: mockups
attributes:
label: Mockups/Screenshots
description: If applicable, add mockups, wireframes, or screenshots to help illustrate your feature request.
placeholder: Drag and drop images here or paste image URLs
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context, references, or examples about the feature request here.
placeholder: Any additional information that might be helpful...
validations:
required: false
- type: checkboxes
id: terms
attributes:
label: Checklist
options:
- label: I have searched existing issues to ensure this feature hasn't been requested already
required: true
- label: I have provided a clear description of the problem and proposed solution
required: true

View File

@@ -41,7 +41,8 @@ runs:
# Use npm install instead of npm ci to correctly resolve platform-specific
# optional dependencies (e.g., @tailwindcss/oxide, lightningcss binaries)
# Skip scripts to avoid electron-builder install-app-deps which uses too much memory
run: npm install --ignore-scripts
# Use --force to allow platform-specific dev dependencies like dmg-license on non-darwin platforms
run: npm install --ignore-scripts --force
- name: Install Linux native bindings
shell: bash

View File

@@ -31,24 +31,99 @@ jobs:
- name: Build server
run: npm run build --workspace=apps/server
- name: Set up Git user
run: |
git config --global user.name "GitHub CI"
git config --global user.email "ci@example.com"
- name: Start backend server
run: npm run start --workspace=apps/server &
run: |
echo "Starting backend server..."
# Start server in background and save PID
npm run start --workspace=apps/server > backend.log 2>&1 &
SERVER_PID=$!
echo "Server started with PID: $SERVER_PID"
echo "SERVER_PID=$SERVER_PID" >> $GITHUB_ENV
env:
PORT: 3008
NODE_ENV: test
# Use a deterministic API key so Playwright can log in reliably
AUTOMAKER_API_KEY: test-api-key-for-e2e-tests
# Reduce log noise in CI
AUTOMAKER_HIDE_API_KEY: 'true'
# Avoid real API calls during CI
AUTOMAKER_MOCK_AGENT: 'true'
# Simulate containerized environment to skip sandbox confirmation dialogs
IS_CONTAINERIZED: 'true'
- name: Wait for backend server
run: |
echo "Waiting for backend server to be ready..."
for i in {1..30}; do
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
# Check if server process is running
if [ -z "$SERVER_PID" ]; then
echo "ERROR: Server PID not found in environment"
cat backend.log 2>/dev/null || echo "No backend log found"
exit 1
fi
# Check if process is actually running
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "ERROR: Server process $SERVER_PID is not running!"
echo "=== Backend logs ==="
cat backend.log
echo ""
echo "=== Recent system logs ==="
dmesg 2>/dev/null | tail -20 || echo "No dmesg available"
exit 1
fi
# Wait for health endpoint
for i in {1..60}; do
if curl -s -f http://localhost:3008/api/health > /dev/null 2>&1; then
echo "Backend server is ready!"
echo "=== Backend logs ==="
cat backend.log
echo ""
echo "Health check response:"
curl -s http://localhost:3008/api/health | jq . 2>/dev/null || echo "Health check: $(curl -s http://localhost:3008/api/health 2>/dev/null || echo 'No response')"
exit 0
fi
echo "Waiting... ($i/30)"
# Check if server process is still running
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "ERROR: Server process died during wait!"
echo "=== Backend logs ==="
cat backend.log
exit 1
fi
echo "Waiting... ($i/60)"
sleep 1
done
echo "Backend server failed to start!"
echo "ERROR: Backend server failed to start within 60 seconds!"
echo "=== Backend logs ==="
cat backend.log
echo ""
echo "=== Process status ==="
ps aux | grep -E "(node|tsx)" | grep -v grep || echo "No node processes found"
echo ""
echo "=== Port status ==="
netstat -tlnp 2>/dev/null | grep :3008 || echo "Port 3008 not listening"
lsof -i :3008 2>/dev/null || echo "lsof not available or port not in use"
echo ""
echo "=== Health endpoint test ==="
curl -v http://localhost:3008/api/health 2>&1 || echo "Health endpoint failed"
# Kill the server process if it's still hanging
if kill -0 $SERVER_PID 2>/dev/null; then
echo ""
echo "Killing stuck server process..."
kill -9 $SERVER_PID 2>/dev/null || true
fi
exit 1
- name: Run E2E tests
@@ -59,6 +134,20 @@ jobs:
CI: true
VITE_SERVER_URL: http://localhost:3008
VITE_SKIP_SETUP: 'true'
# Keep UI-side login/defaults consistent
AUTOMAKER_API_KEY: test-api-key-for-e2e-tests
- name: Print backend logs on failure
if: failure()
run: |
echo "=== E2E Tests Failed - Backend Logs ==="
cat backend.log 2>/dev/null || echo "No backend log found"
echo ""
echo "=== Process status at failure ==="
ps aux | grep -E "(node|tsx)" | grep -v grep || echo "No node processes found"
echo ""
echo "=== Port status ==="
netstat -tlnp 2>/dev/null | grep :3008 || echo "Port 3008 not listening"
- name: Upload Playwright report
uses: actions/upload-artifact@v4
@@ -68,10 +157,22 @@ jobs:
path: apps/ui/playwright-report/
retention-days: 7
- name: Upload test results
- name: Upload test results (screenshots, traces, videos)
uses: actions/upload-artifact@v4
if: failure()
if: always()
with:
name: test-results
path: apps/ui/test-results/
path: |
apps/ui/test-results/
retention-days: 7
if-no-files-found: ignore
- name: Cleanup - Kill backend server
if: always()
run: |
if [ -n "$SERVER_PID" ]; then
echo "Cleaning up backend server (PID: $SERVER_PID)..."
kill $SERVER_PID 2>/dev/null || true
kill -9 $SERVER_PID 2>/dev/null || true
echo "Backend server cleanup complete"
fi

View File

@@ -25,7 +25,7 @@ jobs:
cache-dependency-path: package-lock.json
- name: Install dependencies
run: npm install --ignore-scripts
run: npm install --ignore-scripts --force
- name: Check formatting
run: npm run format:check

View File

@@ -35,6 +35,11 @@ jobs:
with:
check-lockfile: 'true'
- name: Install RPM build tools (Linux)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: sudo apt-get update && sudo apt-get install -y rpm
- name: Build Electron app (macOS)
if: matrix.os == 'macos-latest'
shell: bash
@@ -73,7 +78,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: linux-builds
path: apps/ui/release/*.{AppImage,deb}
path: apps/ui/release/*.{AppImage,deb,rpm}
retention-days: 30
upload:
@@ -104,8 +109,8 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/macos-builds/*
artifacts/windows-builds/*
artifacts/linux-builds/*
artifacts/macos-builds/*.{dmg,zip,blockmap}
artifacts/windows-builds/*.{exe,blockmap}
artifacts/linux-builds/*.{AppImage,deb,rpm,blockmap}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -26,5 +26,5 @@ jobs:
check-lockfile: 'true'
- name: Run npm audit
run: npm audit --audit-level=moderate
run: npm audit --audit-level=critical
continue-on-error: false

10
.gitignore vendored
View File

@@ -73,6 +73,9 @@ blob-report/
!.env.example
!.env.local.example
# Codex config (contains API keys)
.codex/config.toml
# TypeScript
*.tsbuildinfo
@@ -85,3 +88,10 @@ docker-compose.override.yml
pnpm-lock.yaml
yarn.lock
# Fork-specific workflow files (should never be committed)
# API key files
data/.api-key
data/credentials.json
data/
.codex/

View File

@@ -31,7 +31,12 @@ 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"
if [ -n "$WINDIR" ]; then
export PATH="$PATH:/c/Program Files/nodejs:/c/Program Files (x86)/nodejs"
export PATH="$PATH:$APPDATA/npm:$LOCALAPPDATA/Programs/nodejs"
else
export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin"
fi
# Run lint-staged - works with or without nvm
# Prefer npx, fallback to npm exec, both work with system-installed Node.js

View File

@@ -166,7 +166,10 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali
## Environment Variables
- `ANTHROPIC_API_KEY` - Anthropic API key (or use Claude Code CLI auth)
- `HOST` - Host to bind server to (default: 0.0.0.0)
- `HOSTNAME` - Hostname for user-facing URLs (default: localhost)
- `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
- `VITE_HOSTNAME` - Hostname for frontend API URLs (default: localhost)

View File

@@ -24,6 +24,7 @@ For complete details on contribution terms and rights assignment, please review
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
- [Pull Request Process](#pull-request-process)
- [Branching Strategy (RC Branches)](#branching-strategy-rc-branches)
- [Branch Naming Convention](#branch-naming-convention)
- [Commit Message Format](#commit-message-format)
- [Submitting a Pull Request](#submitting-a-pull-request)
@@ -186,6 +187,59 @@ automaker/
This section covers everything you need to know about contributing changes through pull requests, from creating your branch to getting your code merged.
### Branching Strategy (RC Branches)
Automaker uses **Release Candidate (RC) branches** for all development work. Understanding this workflow is essential before contributing.
**How it works:**
1. **All development happens on RC branches** - We maintain version-specific RC branches (e.g., `v0.10.0rc`, `v0.11.0rc`) where all active development occurs
2. **RC branches are eventually merged to main** - Once an RC branch is stable and ready for release, it gets merged into `main`
3. **Main branch is for releases only** - The `main` branch contains only released, stable code
**Before creating a PR:**
1. **Check for the latest RC branch** - Before starting work, check the repository for the current RC branch:
```bash
git fetch upstream
git branch -r | grep rc
```
2. **Base your work on the RC branch** - Create your feature branch from the latest RC branch, not from `main`:
```bash
# Find the latest RC branch (e.g., v0.11.0rc)
git checkout upstream/v0.11.0rc
git checkout -b feature/your-feature-name
```
3. **Target the RC branch in your PR** - When opening your pull request, set the base branch to the current RC branch, not `main`
**Example workflow:**
```bash
# 1. Fetch latest changes
git fetch upstream
# 2. Check for RC branches
git branch -r | grep rc
# Output: upstream/v0.11.0rc
# 3. Create your branch from the RC
git checkout -b feature/add-dark-mode upstream/v0.11.0rc
# 4. Make your changes and commit
git commit -m "feat: Add dark mode support"
# 5. Push to your fork
git push origin feature/add-dark-mode
# 6. Open PR targeting the RC branch (v0.11.0rc), NOT main
```
**Important:** PRs opened directly against `main` will be asked to retarget to the current RC branch.
### Branch Naming Convention
We use a consistent branch naming pattern to keep our repository organized:
@@ -275,14 +329,14 @@ Follow these steps to submit your contribution:
#### 1. Prepare Your Changes
Ensure you've synced with the latest upstream changes:
Ensure you've synced with the latest upstream changes from the RC branch:
```bash
# Fetch latest changes from upstream
git fetch upstream
# Rebase your branch on main (if needed)
git rebase upstream/main
# Rebase your branch on the current RC branch (if needed)
git rebase upstream/v0.11.0rc # Use the current RC branch name
```
#### 2. Run Pre-submission Checks
@@ -314,18 +368,19 @@ git push origin feature/your-feature-name
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`
3. **Important:** Set the base repository to `AutoMaker-Org/automaker` and the base branch to the **current RC branch** (e.g., `v0.11.0rc`), not `main`
4. Fill out the PR template completely
#### PR Requirements Checklist
Your PR should include:
- [ ] **Targets the current RC branch** (not `main`) - see [Branching Strategy](#branching-strategy-rc-branches)
- [ ] **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
- [ ] **No merge conflicts** with the RC branch
- [ ] **Tests included** for new functionality
- [ ] **Documentation updated** if adding/changing public APIs

253
DEVELOPMENT_WORKFLOW.md Normal file
View File

@@ -0,0 +1,253 @@
# Development Workflow
This document defines the standard workflow for keeping a branch in sync with the upstream
release candidate (RC) and for shipping feature work. It is paired with `check-sync.sh`.
## Quick Decision Rule
1. Ask the user to select a workflow:
- **Sync Workflow** → you are maintaining the current RC branch with fixes/improvements
and will push the same fixes to both origin and upstream RC when you have local
commits to publish.
- **PR Workflow** → you are starting new feature work on a new branch; upstream updates
happen via PR only.
2. After the user selects, run:
```bash
./check-sync.sh
```
3. Use the status output to confirm alignment. If it reports **diverged**, default to
merging `upstream/<TARGET_RC>` into the current branch and preserving local commits.
For Sync Workflow, when the working tree is clean and you are behind upstream RC,
proceed with the fetch + merge without asking for additional confirmation.
## Target RC Resolution
The target RC is resolved dynamically so the workflow stays current as the RC changes.
Resolution order:
1. Latest `upstream/v*rc` branch (auto-detected)
2. `upstream/HEAD` (fallback)
3. If neither is available, you must pass `--rc <branch>`
Override for a single run:
```bash
./check-sync.sh --rc <rc-branch>
```
## Pre-Flight Checklist
1. Confirm a clean working tree:
```bash
git status
```
2. Confirm the current branch:
```bash
git branch --show-current
```
3. Ensure remotes exist (origin + upstream):
```bash
git remote -v
```
## Sync Workflow (Upstream Sync)
Use this flow when you are updating the current branch with fixes or improvements and
intend to keep origin and upstream RC in lockstep.
1. **Check sync status**
```bash
./check-sync.sh
```
2. **Update from upstream RC before editing (no pulls)**
- **Behind upstream RC** → fetch and merge RC into your branch:
```bash
git fetch upstream
git merge upstream/<TARGET_RC> --no-edit
```
When the working tree is clean and the user selected Sync Workflow, proceed without
an extra confirmation prompt.
- **Diverged** → stop and resolve manually.
3. **Resolve conflicts if needed**
- Handle conflicts intelligently: preserve upstream behavior and your local intent.
4. **Make changes and commit (if you are delivering fixes)**
```bash
git add -A
git commit -m "type: description"
```
5. **Build to verify**
```bash
npm run build:packages
npm run build
```
6. **Push after a successful merge to keep remotes aligned**
- If you only merged upstream RC changes, push **origin only** to sync your fork:
```bash
git push origin <branch>
```
- If you have local fixes to publish, push **origin + upstream**:
```bash
git push origin <branch>
git push upstream <branch>:<TARGET_RC>
```
- Always ask the user which push to perform.
- Origin (origin-only sync):
```bash
git push origin <branch>
```
- Upstream RC (publish the same fixes when you have local commits):
```bash
git push upstream <branch>:<TARGET_RC>
```
7. **Re-check sync**
```bash
./check-sync.sh
```
## PR Workflow (Feature Work)
Use this flow only for new feature work on a new branch. Do not push to upstream RC.
1. **Create or switch to a feature branch**
```bash
git checkout -b <branch>
```
2. **Make changes and commit**
```bash
git add -A
git commit -m "type: description"
```
3. **Merge upstream RC before shipping**
```bash
git merge upstream/<TARGET_RC> --no-edit
```
4. **Build and/or test**
```bash
npm run build:packages
npm run build
```
5. **Push to origin**
```bash
git push -u origin <branch>
```
6. **Create or update the PR**
- Use `gh pr create` or the GitHub UI.
7. **Review and follow-up**
- Apply feedback, commit changes, and push again.
- Re-run `./check-sync.sh` if additional upstream sync is needed.
## Conflict Resolution Checklist
1. Identify which changes are from upstream vs. local.
2. Preserve both behaviors where possible; avoid dropping either side.
3. Prefer minimal, safe integrations over refactors.
4. Re-run build commands after resolving conflicts.
5. Re-run `./check-sync.sh` to confirm status.
## Build/Test Matrix
- **Sync Workflow**: `npm run build:packages` and `npm run build`.
- **PR Workflow**: `npm run build:packages` and `npm run build` (plus relevant tests).
## Post-Sync Verification
1. `git status` should be clean.
2. `./check-sync.sh` should show expected alignment.
3. Verify recent commits with:
```bash
git log --oneline -5
```
## check-sync.sh Usage
- Uses dynamic Target RC resolution (see above).
- Override target RC:
```bash
./check-sync.sh --rc <rc-branch>
```
- Optional preview limit:
```bash
./check-sync.sh --preview 10
```
- The script prints sync status for both origin and upstream and previews recent commits
when you are behind.
## Stop Conditions
Stop and ask for guidance if any of the following are true:
- The working tree is dirty and you are about to merge or push.
- `./check-sync.sh` reports **diverged** during PR Workflow, or a merge cannot be completed.
- The script cannot resolve a target RC and requests `--rc`.
- A build fails after sync or conflict resolution.
## AI Agent Guardrails
- Always run `./check-sync.sh` before merges or pushes.
- Always ask for explicit user approval before any push command.
- Do not ask for additional confirmation before a Sync Workflow fetch + merge when the
working tree is clean and the user has already selected the Sync Workflow.
- Choose Sync vs PR workflow based on intent (RC maintenance vs new feature work), not
on the script's workflow hint.
- Only use force push when the user explicitly requests a history rewrite.
- Ask for explicit approval before dependency installs, branch deletion, or destructive operations.
- When resolving merge conflicts, preserve both upstream changes and local intent where possible.
- Do not create or switch to new branches unless the user explicitly requests it.
## AI Agent Decision Guidance
Agents should provide concrete, task-specific suggestions instead of repeatedly asking
open-ended questions. Use the user's stated goal and the `./check-sync.sh` status to
propose a default path plus one or two alternatives, and only ask for confirmation when
an action requires explicit approval.
Default behavior:
- If the intent is RC maintenance, recommend the Sync Workflow and proceed with
safe preparation steps (status checks, previews). If the branch is behind upstream RC,
fetch and merge without additional confirmation when the working tree is clean, then
push to origin to keep the fork aligned. Push upstream only when there are local fixes
to publish.
- If the intent is new feature work, recommend the PR Workflow and proceed with safe
preparation steps (status checks, identifying scope). Ask for approval before merges,
pushes, or dependency installs.
- If `./check-sync.sh` reports **diverged** during Sync Workflow, merge
`upstream/<TARGET_RC>` into the current branch and preserve local commits.
- If `./check-sync.sh` reports **diverged** during PR Workflow, stop and ask for guidance
with a short explanation of the divergence and the minimal options to resolve it.
If the user's intent is RC maintenance, prefer the Sync Workflow regardless of the
script hint. When the intent is new feature work, use the PR Workflow and avoid upstream
RC pushes.
Suggestion format (keep it short):
- **Recommended**: one sentence with the default path and why it fits the task.
- **Alternatives**: one or two options with the tradeoff or prerequisite.
- **Approval points**: mention any upcoming actions that need explicit approval (exclude sync
workflow pushes and merges).
## Failure Modes and How to Avoid Them
Sync Workflow:
- Wrong RC target: verify the auto-detected RC in `./check-sync.sh` output before merging.
- Diverged from upstream RC: stop and resolve manually before any merge or push.
- Dirty working tree: commit or stash before syncing to avoid accidental merges.
- Missing remotes: ensure both `origin` and `upstream` are configured before syncing.
- Build breaks after sync: run `npm run build:packages` and `npm run build` before pushing.
PR Workflow:
- Branch not synced to current RC: re-run `./check-sync.sh` and merge RC before shipping.
- Pushing the wrong branch: confirm `git branch --show-current` before pushing.
- Unreviewed changes: always commit and push to origin before opening or updating a PR.
- Skipped tests/builds: run the build commands before declaring the PR ready.
## Notes
- Avoid merging with uncommitted changes; commit or stash first.
- Prefer merge over rebase for PR branches; rebases rewrite history and often require a force push,
which should only be done with an explicit user request.
- Use clear, conventional commit messages and split unrelated changes into separate commits.

View File

@@ -8,10 +8,12 @@
# =============================================================================
# BASE STAGE - Common setup for all builds (DRY: defined once, used by all)
# =============================================================================
FROM node:22-alpine AS base
FROM node:22-slim AS base
# Install build dependencies for native modules (node-pty)
RUN apk add --no-cache python3 make g++
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
@@ -51,32 +53,84 @@ RUN npm run build:packages && npm run build --workspace=apps/server
# =============================================================================
# SERVER PRODUCTION STAGE
# =============================================================================
FROM node:22-alpine AS server
FROM node:22-slim 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 \
# Build argument for tracking which commit this image was built from
ARG GIT_COMMIT_SHA=unknown
LABEL automaker.git.commit.sha="${GIT_COMMIT_SHA}"
# Build arguments for user ID matching (allows matching host user for mounted volumes)
# Override at build time: docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) ...
ARG UID=1001
ARG GID=1001
# Install git, curl, bash (for terminal), gosu (for user switching), and GitHub CLI (pinned version, multi-arch)
# Also install Playwright/Chromium system dependencies (aligns with playwright install-deps on Debian/Ubuntu)
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl bash gosu ca-certificates openssh-client \
# Playwright/Chromium dependencies
libglib2.0-0 libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 \
libxfixes3 libxrandr2 libgbm1 libasound2 libpango-1.0-0 libcairo2 \
libx11-6 libx11-xcb1 libxcb1 libxext6 libxrender1 libxss1 libxtst6 \
libxshmfence1 libgtk-3-0 libexpat1 libfontconfig1 fonts-liberation \
xdg-utils libpangocairo-1.0-0 libpangoft2-1.0-0 libu2f-udev libvulkan1 \
&& 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}
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} \
&& rm -rf /var/lib/apt/lists/*
# Install Claude CLI globally
# Install Claude CLI globally (available to all users via npm global bin)
RUN npm install -g @anthropic-ai/claude-code
WORKDIR /app
# Create non-root user with home directory BEFORE installing Cursor CLI
# Uses UID/GID build args to match host user for mounted volume permissions
# Use -o flag to allow non-unique IDs (GID 1000 may already exist as 'node' group)
RUN groupadd -o -g ${GID} automaker && \
useradd -o -u ${UID} -g automaker -m -d /home/automaker -s /bin/bash automaker && \
mkdir -p /home/automaker/.local/bin && \
mkdir -p /home/automaker/.cursor && \
chown -R automaker:automaker /home/automaker && \
chmod 700 /home/automaker/.cursor
# 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
# Install Cursor CLI as the automaker user
# Set HOME explicitly and install to /home/automaker/.local/bin/
USER automaker
ENV HOME=/home/automaker
RUN curl https://cursor.com/install -fsS | bash && \
echo "=== Checking Cursor CLI installation ===" && \
ls -la /home/automaker/.local/bin/ && \
echo "=== PATH is: $PATH ===" && \
(which cursor-agent && cursor-agent --version) || echo "cursor-agent installed (may need auth setup)"
# Install OpenCode CLI (for multi-provider AI model access)
RUN curl -fsSL https://opencode.ai/install | bash && \
echo "=== Checking OpenCode CLI installation ===" && \
ls -la /home/automaker/.local/bin/ && \
(which opencode && opencode --version) || echo "opencode installed (may need auth setup)"
USER root
# Add PATH to profile so it's available in all interactive shells (for login shells)
RUN mkdir -p /etc/profile.d && \
echo 'export PATH="/home/automaker/.local/bin:$PATH"' > /etc/profile.d/cursor-cli.sh && \
chmod +x /etc/profile.d/cursor-cli.sh
# Add to automaker's .bashrc for bash interactive shells
RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /home/automaker/.bashrc && \
chown automaker:automaker /home/automaker/.bashrc
# Also add to root's .bashrc since docker exec defaults to root
RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /root/.bashrc
WORKDIR /app
# Copy root package.json (needed for workspace resolution)
COPY --from=server-builder /app/package*.json ./
@@ -111,6 +165,8 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENV PORT=3008
ENV DATA_DIR=/data
ENV HOME=/home/automaker
# Add user's local bin to PATH for cursor-agent
ENV PATH="/home/automaker/.local/bin:${PATH}"
# Expose port
EXPOSE 3008
@@ -153,6 +209,10 @@ RUN npm run build:packages && npm run build --workspace=apps/ui
# =============================================================================
FROM nginx:alpine AS ui
# Build argument for tracking which commit this image was built from
ARG GIT_COMMIT_SHA=unknown
LABEL automaker.git.commit.sha="${GIT_COMMIT_SHA}"
# Copy built files
COPY --from=ui-builder /app/apps/ui/dist /usr/share/nginx/html

94
Dockerfile.dev Normal file
View File

@@ -0,0 +1,94 @@
# Automaker Development Dockerfile
# For development with live reload via volume mounting
# Source code is NOT copied - it's mounted as a volume
#
# Usage:
# docker compose -f docker-compose.dev.yml up
FROM node:22-slim
# Install build dependencies for native modules (node-pty) and runtime tools
# Also install Playwright/Chromium system dependencies (aligns with playwright install-deps on Debian/Ubuntu)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 make g++ \
git curl bash gosu ca-certificates openssh-client \
# Playwright/Chromium dependencies
libglib2.0-0 libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 libxdamage1 \
libxfixes3 libxrandr2 libgbm1 libasound2 libpango-1.0-0 libcairo2 \
libx11-6 libx11-xcb1 libxcb1 libxext6 libxrender1 libxss1 libxtst6 \
libxshmfence1 libgtk-3-0 libexpat1 libfontconfig1 fonts-liberation \
xdg-utils libpangocairo-1.0-0 libpangoft2-1.0-0 libu2f-udev libvulkan1 \
&& 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} \
&& rm -rf /var/lib/apt/lists/*
# Install Claude CLI globally
RUN npm install -g @anthropic-ai/claude-code
# Build arguments for user ID matching (allows matching host user for mounted volumes)
# Override at build time: docker-compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g)
ARG UID=1001
ARG GID=1001
# Create non-root user with configurable UID/GID
# Use -o flag to allow non-unique IDs (GID 1000 may already exist as 'node' group)
RUN groupadd -o -g ${GID} automaker && \
useradd -o -u ${UID} -g automaker -m -d /home/automaker -s /bin/bash automaker && \
mkdir -p /home/automaker/.local/bin && \
mkdir -p /home/automaker/.cursor && \
chown -R automaker:automaker /home/automaker && \
chmod 700 /home/automaker/.cursor
# Install Cursor CLI as automaker user
USER automaker
ENV HOME=/home/automaker
RUN curl https://cursor.com/install -fsS | bash || true
USER root
# Add PATH to profile for Cursor CLI
RUN mkdir -p /etc/profile.d && \
echo 'export PATH="/home/automaker/.local/bin:$PATH"' > /etc/profile.d/cursor-cli.sh && \
chmod +x /etc/profile.d/cursor-cli.sh
# Add to user bashrc files
RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /home/automaker/.bashrc && \
chown automaker:automaker /home/automaker/.bashrc
RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /root/.bashrc
WORKDIR /app
# Create directories with proper permissions
RUN mkdir -p /data /projects && chown automaker:automaker /data /projects
# Configure git for mounted volumes
RUN git config --system --add safe.directory '*' && \
git config --system credential.helper '!gh auth git-credential'
# Copy entrypoint script
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Environment variables
ENV PORT=3008
ENV DATA_DIR=/data
ENV HOME=/home/automaker
ENV PATH="/home/automaker/.local/bin:${PATH}"
# Expose both dev ports
EXPOSE 3007 3008
# Use entrypoint for permission handling
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
# Default command - will be overridden by docker-compose
CMD ["npm", "run", "dev:web"]

174
README.md
View File

@@ -28,6 +28,7 @@
- [Quick Start](#quick-start)
- [How to Run](#how-to-run)
- [Development Mode](#development-mode)
- [Interactive TUI Launcher](#interactive-tui-launcher-recommended-for-new-users)
- [Building for Production](#building-for-production)
- [Testing](#testing)
- [Linting](#linting)
@@ -101,11 +102,9 @@ In the Discord, you can:
### Prerequisites
- **Node.js 18+** (tested with Node.js 22)
- **Node.js 22+** (required: >=22.0.0 <23.0.0)
- **npm** (comes with Node.js)
- **Authentication** (choose one):
- **[Claude Code CLI](https://code.claude.com/docs/en/overview)** (recommended) - Install and authenticate, credentials used automatically
- **Anthropic API Key** - Direct API key for Claude Agent SDK ([get one here](https://console.anthropic.com/))
- **[Claude Code CLI](https://code.claude.com/docs/en/overview)** - Install and authenticate with your Anthropic subscription. Automaker integrates with your authenticated Claude Code CLI to access Claude models.
### Quick Start
@@ -117,40 +116,16 @@ cd automaker
# 2. Install dependencies
npm install
# 3. Build shared packages (Now can be skipped npm install / run dev does it automaticly)
npm run build:packages
# 4. Start Automaker (production mode)
npm run start
# 3. Start Automaker
npm run dev
# Choose between:
# 1. Web Application (browser at localhost:3007)
# 2. Desktop Application (Electron - recommended)
```
**Note:** The `npm run start` command will:
**Authentication:** Automaker integrates with your authenticated Claude Code CLI. Make sure you have [installed and authenticated](https://code.claude.com/docs/en/quickstart) the Claude Code CLI before running Automaker. Your CLI credentials will be detected automatically.
- Check for dependencies and install if needed
- Build the application if needed
- Kill any processes on ports 3007/3008
- 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.
**For Development:** `npm run dev` starts the development server with Vite live reload and hot module replacement for fast refresh and instant updates as you make changes.
## How to Run
@@ -187,6 +162,40 @@ npm run dev:electron:wsl:gpu
npm run dev:web
```
### Interactive TUI Launcher (Recommended for New Users)
For a user-friendly interactive menu, use the built-in TUI launcher script:
```bash
# Show interactive menu with all launch options
./start-automaker.sh
# Or launch directly without menu
./start-automaker.sh web # Web browser
./start-automaker.sh electron # Desktop app
./start-automaker.sh electron-debug # Desktop + DevTools
# Additional options
./start-automaker.sh --help # Show all available options
./start-automaker.sh --version # Show version information
./start-automaker.sh --check-deps # Verify project dependencies
./start-automaker.sh --no-colors # Disable colored output
./start-automaker.sh --no-history # Don't remember last choice
```
**Features:**
- 🎨 Beautiful terminal UI with gradient colors and ASCII art
- Interactive menu (press 1-3 to select, Q to exit)
- 💾 Remembers your last choice
- Pre-flight checks (validates Node.js, npm, dependencies)
- 📏 Responsive layout (adapts to terminal size)
- 30-second timeout for hands-free selection
- 🌐 Cross-shell compatible (bash/zsh)
**History File:**
Your last selected mode is saved in `~/.automaker_launcher_history` for quick re-runs.
### Building for Production
#### Web Application
@@ -194,9 +203,6 @@ npm run dev:web
```bash
# Build for web deployment (uses Vite)
npm run build
# Run production build
npm run start
```
#### Desktop Application
@@ -208,11 +214,30 @@ npm run build:electron
# Platform-specific builds
npm run build:electron:mac # macOS (DMG + ZIP, x64 + arm64)
npm run build:electron:win # Windows (NSIS installer, x64)
npm run build:electron:linux # Linux (AppImage + DEB, x64)
npm run build:electron:linux # Linux (AppImage + DEB + RPM, x64)
# Output directory: apps/ui/release/
```
**Linux Distribution Packages:**
- **AppImage**: Universal format, works on any Linux distribution
- **DEB**: Ubuntu, Debian, Linux Mint, Pop!\_OS
- **RPM**: Fedora, RHEL, Rocky Linux, AlmaLinux, openSUSE
**Installing on Fedora/RHEL:**
```bash
# Download the RPM package
wget https://github.com/AutoMaker-Org/automaker/releases/latest/download/Automaker-<version>-x86_64.rpm
# Install with dnf (Fedora)
sudo dnf install ./Automaker-<version>-x86_64.rpm
# Or with yum (RHEL/CentOS)
sudo yum localinstall ./Automaker-<version>-x86_64.rpm
```
#### Docker Deployment
Docker provides the most secure way to run Automaker by isolating it from your host filesystem.
@@ -231,16 +256,9 @@ docker-compose logs -f
docker-compose down
```
##### Configuration
##### Authentication
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.
Automaker integrates with your authenticated Claude Code CLI. To use CLI authentication in Docker, mount your Claude CLI config directory (see [Claude CLI Authentication](#claude-cli-authentication) below).
##### Working with Projects (Host Directory Access)
@@ -254,9 +272,9 @@ services:
- /path/to/your/project:/projects/your-project
```
##### Claude CLI Authentication (Optional)
##### Claude CLI Authentication
To use Claude Code CLI authentication instead of an API key, mount your Claude CLI config directory:
Mount your Claude CLI config directory to use your authenticated CLI credentials:
```yaml
services:
@@ -354,10 +372,6 @@ npm run lint
### Environment Configuration
#### Authentication (if not using Claude Code CLI)
- `ANTHROPIC_API_KEY` - Your Anthropic API key for Claude Agent SDK (not needed if using Claude Code CLI)
#### Optional - Server
- `PORT` - Server port (default: 3008)
@@ -368,49 +382,22 @@ npm run lint
- `AUTOMAKER_API_KEY` - Optional API authentication for the server
- `ALLOWED_ROOT_DIRECTORY` - Restrict file operations to specific directory
- `CORS_ORIGIN` - CORS policy (default: \*)
- `CORS_ORIGIN` - CORS allowed origins (comma-separated list; defaults to localhost only)
#### Optional - Development
- `VITE_SKIP_ELECTRON` - Skip Electron in dev mode
- `OPEN_DEVTOOLS` - Auto-open DevTools in Electron
- `AUTOMAKER_SKIP_SANDBOX_WARNING` - Skip sandbox warning dialog (useful for dev/CI)
### Authentication Setup
#### Option 1: Claude Code CLI (Recommended)
Automaker integrates with your authenticated Claude Code CLI and uses your Anthropic subscription.
Install and authenticate the Claude Code CLI following the [official quickstart guide](https://code.claude.com/docs/en/quickstart).
Once authenticated, Automaker will automatically detect and use your CLI credentials. No additional configuration needed!
#### Option 2: Direct API Key
If you prefer not to use the CLI, you can provide an Anthropic API key directly using one of these methods:
##### 2a. Shell Configuration
Add to your `~/.bashrc` or `~/.zshrc`:
```bash
export ANTHROPIC_API_KEY="sk-ant-..."
```
Then restart your terminal or run `source ~/.bashrc` (or `source ~/.zshrc`).
##### 2b. .env File
Create a `.env` file in the project root (gitignored):
```bash
ANTHROPIC_API_KEY=sk-ant-...
PORT=3008
DATA_DIR=./data
```
##### 2c. In-App Storage
The application can store your API key securely in the settings UI. The key is persisted in the `DATA_DIR` directory.
## Features
### Core Workflow
@@ -519,20 +506,24 @@ Automaker provides several specialized views accessible via the sidebar or keybo
| **Agent** | `A` | Interactive chat sessions with AI agents for exploratory work and questions |
| **Spec** | `D` | Project specification editor with AI-powered generation and feature suggestions |
| **Context** | `C` | Manage context files (markdown, images) that AI agents automatically reference |
| **Profiles** | `M` | Create and manage AI agent profiles with custom prompts and configurations |
| **Settings** | `S` | Configure themes, shortcuts, defaults, authentication, and more |
| **Terminal** | `T` | Integrated terminal with tabs, splits, and persistent sessions |
| **GitHub Issues** | - | Import and validate GitHub issues, convert to tasks |
| **Graph** | `H` | Visualize feature dependencies with interactive graph visualization |
| **Ideation** | `I` | Brainstorm and generate ideas with AI assistance |
| **Memory** | `Y` | View and manage agent memory and conversation history |
| **GitHub Issues** | `G` | Import and validate GitHub issues, convert to tasks |
| **GitHub PRs** | `R` | View and manage GitHub pull requests |
| **Running Agents** | - | View all active agents across projects with status and progress |
### Keyboard Navigation
All shortcuts are customizable in Settings. Default shortcuts:
- **Navigation:** `K` (Board), `A` (Agent), `D` (Spec), `C` (Context), `S` (Settings), `M` (Profiles), `T` (Terminal)
- **Navigation:** `K` (Board), `A` (Agent), `D` (Spec), `C` (Context), `S` (Settings), `T` (Terminal), `H` (Graph), `I` (Ideation), `Y` (Memory), `G` (GitHub Issues), `R` (GitHub PRs)
- **UI:** `` ` `` (Toggle sidebar)
- **Actions:** `N` (New item in current view), `G` (Start next features), `O` (Open project), `P` (Project picker)
- **Actions:** `N` (New item in current view), `O` (Open project), `P` (Project picker)
- **Projects:** `Q`/`E` (Cycle previous/next project)
- **Terminal:** `Alt+D` (Split right), `Alt+S` (Split down), `Alt+W` (Close), `Alt+T` (New tab)
## Architecture
@@ -597,10 +588,16 @@ Stored in `{projectPath}/.automaker/`:
│ ├── agent-output.md # AI agent output log
│ └── images/ # Attached images
├── context/ # Context files for AI agents
├── worktrees/ # Git worktree metadata
├── validations/ # GitHub issue validation results
├── ideation/ # Brainstorming and analysis data
│ └── analysis.json # Project structure analysis
├── board/ # Board-related data
├── images/ # Project-level images
├── settings.json # Project-specific settings
├── spec.md # Project specification
├── analysis.json # Project structure analysis
└── feature-suggestions.json # AI-generated suggestions
├── app_spec.txt # Project specification (XML format)
├── active-branches.json # Active git branches tracking
└── execution-state.json # Auto-mode execution state
```
#### Global Data
@@ -638,7 +635,6 @@ data/
- [Contributing Guide](./CONTRIBUTING.md) - How to contribute to Automaker
- [Project Documentation](./docs/) - Architecture guides, patterns, and developer docs
- [Docker Isolation Guide](./docs/docker-isolation.md) - Security-focused Docker deployment
- [Shared Packages Guide](./docs/llm-shared-packages.md) - Using monorepo packages
### Community

View File

@@ -8,6 +8,20 @@
# Your Anthropic API key for Claude models
ANTHROPIC_API_KEY=sk-ant-...
# ============================================
# OPTIONAL - Additional API Keys
# ============================================
# OpenAI API key for Codex/GPT models
OPENAI_API_KEY=sk-...
# Cursor API key for Cursor models
CURSOR_API_KEY=...
# OAuth credentials for CLI authentication (extracted automatically)
CLAUDE_OAUTH_CREDENTIALS=
CURSOR_AUTH_TOKEN=
# ============================================
# OPTIONAL - Security
# ============================================
@@ -30,6 +44,11 @@ CORS_ORIGIN=http://localhost:3007
# OPTIONAL - Server
# ============================================
# Host to bind the server to (default: 0.0.0.0)
# Use 0.0.0.0 to listen on all interfaces (recommended for Docker/remote access)
# Use 127.0.0.1 or localhost to restrict to local connections only
HOST=0.0.0.0
# Port to run the server on
PORT=3008
@@ -49,6 +68,14 @@ TERMINAL_PASSWORD=
ENABLE_REQUEST_LOGGING=false
# ============================================
# OPTIONAL - UI Behavior
# ============================================
# Skip the sandbox warning dialog on startup (default: false)
# Set to "true" to disable the warning entirely (useful for dev/CI environments)
AUTOMAKER_SKIP_SANDBOX_WARNING=false
# ============================================
# OPTIONAL - Debugging
# ============================================

View File

@@ -1,6 +1,6 @@
{
"name": "@automaker/server",
"version": "0.8.0",
"version": "0.12.0",
"description": "Backend server for Automaker - provides API for both web and Electron modes",
"author": "AutoMaker Team",
"license": "SEE LICENSE IN LICENSE",
@@ -32,7 +32,8 @@
"@automaker/prompts": "1.0.0",
"@automaker/types": "1.0.0",
"@automaker/utils": "1.0.0",
"@modelcontextprotocol/sdk": "1.25.1",
"@modelcontextprotocol/sdk": "1.25.2",
"@openai/codex-sdk": "^0.77.0",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"dotenv": "17.2.3",

View File

@@ -17,9 +17,19 @@ import dotenv from 'dotenv';
import { createEventEmitter, type EventEmitter } from './lib/events.js';
import { initAllowedPaths } from '@automaker/platform';
import { createLogger } from '@automaker/utils';
import { createLogger, setLogLevel, LogLevel } from '@automaker/utils';
const logger = createLogger('Server');
/**
* Map server log level string to LogLevel enum
*/
const LOG_LEVEL_MAP: Record<string, LogLevel> = {
error: LogLevel.ERROR,
warn: LogLevel.WARN,
info: LogLevel.INFO,
debug: LogLevel.DEBUG,
};
import { authMiddleware, validateWsConnectionToken, checkRawAuthentication } from './lib/auth.js';
import { requireJsonContentType } from './middleware/require-json-content-type.js';
import { createAuthRoutes } from './routes/auth/index.js';
@@ -53,6 +63,10 @@ import { SettingsService } from './services/settings-service.js';
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
import { createClaudeRoutes } from './routes/claude/index.js';
import { ClaudeUsageService } from './services/claude-usage-service.js';
import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.js';
import { CodexAppServerService } from './services/codex-app-server-service.js';
import { CodexModelCacheService } from './services/codex-model-cache-service.js';
import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
@@ -63,13 +77,40 @@ 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';
import { getDevServerService } from './services/dev-server-service.js';
import { eventHookService } from './services/event-hook-service.js';
import { createNotificationsRoutes } from './routes/notifications/index.js';
import { getNotificationService } from './services/notification-service.js';
import { createEventHistoryRoutes } from './routes/event-history/index.js';
import { getEventHistoryService } from './services/event-history-service.js';
import { createCodeReviewRoutes } from './routes/code-review/index.js';
import { CodeReviewService } from './services/code-review-service.js';
// Load environment variables
dotenv.config();
const PORT = parseInt(process.env.PORT || '3008', 10);
const HOST = process.env.HOST || '0.0.0.0';
const HOSTNAME = process.env.HOSTNAME || 'localhost';
const DATA_DIR = process.env.DATA_DIR || './data';
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
// Runtime-configurable request logging flag (can be changed via settings)
let requestLoggingEnabled = ENABLE_REQUEST_LOGGING_DEFAULT;
/**
* Enable or disable HTTP request logging at runtime
*/
export function setRequestLoggingEnabled(enabled: boolean): void {
requestLoggingEnabled = enabled;
}
/**
* Get current request logging state
*/
export function isRequestLoggingEnabled(): boolean {
return requestLoggingEnabled;
}
// Check for required environment variables
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
@@ -98,22 +139,21 @@ initAllowedPaths();
const app = express();
// Middleware
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
if (ENABLE_REQUEST_LOGGING) {
morgan.token('status-colored', (_req, res) => {
const status = res.statusCode;
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 >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
return `\x1b[32m${status}\x1b[0m`; // Green for success
});
// Custom colored logger showing only endpoint and status code (dynamically configurable)
morgan.token('status-colored', (_req, res) => {
const status = res.statusCode;
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 >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
return `\x1b[32m${status}\x1b[0m`; // Green for success
});
app.use(
morgan(':method :url :status-colored', {
skip: (req) => req.url === '/api/health', // Skip health check logs
})
);
}
app.use(
morgan(':method :url :status-colored', {
// Skip when request logging is disabled or for health check endpoints
skip: (req) => !requestLoggingEnabled || req.url === '/api/health',
})
);
// CORS configuration
// When using credentials (cookies), origin cannot be '*'
// We dynamically allow the requesting origin for local development
@@ -166,13 +206,51 @@ const agentService = new AgentService(DATA_DIR, events, settingsService);
const featureLoader = new FeatureLoader();
const autoModeService = new AutoModeService(events, settingsService);
const claudeUsageService = new ClaudeUsageService();
const codexAppServerService = new CodexAppServerService();
const codexModelCacheService = new CodexModelCacheService(DATA_DIR, codexAppServerService);
const codexUsageService = new CodexUsageService(codexAppServerService);
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);
const codeReviewService = new CodeReviewService(events, settingsService);
// Initialize DevServerService with event emitter for real-time log streaming
const devServerService = getDevServerService();
devServerService.setEventEmitter(events);
// Initialize Notification Service with event emitter for real-time updates
const notificationService = getNotificationService();
notificationService.setEventEmitter(events);
// Initialize Event History Service
const eventHistoryService = getEventHistoryService();
// Initialize Event Hook Service for custom event triggers (with history storage)
eventHookService.initialize(events, settingsService, eventHistoryService);
// Initialize services
(async () => {
// Apply logging settings from saved settings
try {
const settings = await settingsService.getGlobalSettings();
if (settings.serverLogLevel && LOG_LEVEL_MAP[settings.serverLogLevel] !== undefined) {
setLogLevel(LOG_LEVEL_MAP[settings.serverLogLevel]);
logger.info(`Server log level set to: ${settings.serverLogLevel}`);
}
// Apply request logging setting (default true if not set)
const enableRequestLog = settings.enableRequestLogging ?? true;
setRequestLoggingEnabled(enableRequestLog);
logger.info(`HTTP request logging: ${enableRequestLog ? 'enabled' : 'disabled'}`);
} catch (err) {
logger.warn('Failed to load logging settings, using defaults');
}
await agentService.initialize();
logger.info('Agent service initialized');
// Bootstrap Codex model cache in background (don't block server startup)
void codexModelCacheService.getModels().catch((err) => {
logger.error('Failed to bootstrap Codex model cache:', err);
});
})();
// Run stale validation cleanup every hour to prevent memory leaks from crashed validations
@@ -188,9 +266,10 @@ setInterval(() => {
// This helps prevent CSRF and content-type confusion attacks
app.use('/api', requireJsonContentType);
// Mount API routes - health and auth are unauthenticated
// Mount API routes - health, auth, and setup are unauthenticated
app.use('/api/health', createHealthRoutes());
app.use('/api/auth', createAuthRoutes());
app.use('/api/setup', createSetupRoutes());
// Apply authentication to all other routes
app.use('/api', authMiddleware);
@@ -201,12 +280,11 @@ app.get('/api/health/detailed', createDetailedHandler());
app.use('/api/fs', createFsRoutes(events));
app.use('/api/agent', createAgentRoutes(agentService, events));
app.use('/api/sessions', createSessionsRoutes(agentService));
app.use('/api/features', createFeaturesRoutes(featureLoader));
app.use('/api/features', createFeaturesRoutes(featureLoader, settingsService, events));
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
app.use('/api/enhance-prompt', createEnhancePromptRoutes(settingsService));
app.use('/api/worktree', createWorktreeRoutes());
app.use('/api/worktree', createWorktreeRoutes(events, settingsService));
app.use('/api/git', createGitRoutes());
app.use('/api/setup', createSetupRoutes());
app.use('/api/suggestions', createSuggestionsRoutes(events, settingsService));
app.use('/api/models', createModelsRoutes());
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events, settingsService));
@@ -216,12 +294,16 @@ app.use('/api/templates', createTemplatesRoutes());
app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/codex', createCodexRoutes(codexUsageService, codexModelCacheService));
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(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));
app.use('/api/notifications', createNotificationsRoutes(notificationService));
app.use('/api/event-history', createEventHistoryRoutes(eventHistoryService, settingsService));
app.use('/api/code-review', createCodeReviewRoutes(codeReviewService));
// Create HTTP server
const server = createServer(app);
@@ -533,8 +615,8 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
});
// Start server with error handling for port conflicts
const startServer = (port: number) => {
server.listen(port, () => {
const startServer = (port: number, host: string) => {
server.listen(port, host, () => {
const terminalStatus = isTerminalEnabled()
? isTerminalPasswordRequired()
? 'enabled (password protected)'
@@ -545,10 +627,11 @@ const startServer = (port: number) => {
╔═══════════════════════════════════════════════════════╗
║ Automaker Backend Server ║
╠═══════════════════════════════════════════════════════╣
HTTP API: http://localhost:${portStr}
WebSocket: ws://localhost:${portStr}/api/events
Terminal: ws://localhost:${portStr}/api/terminal/ws
Health: http://localhost:${portStr}/api/health
Listening: ${host}:${port}${' '.repeat(Math.max(0, 34 - host.length - port.toString().length))}
HTTP API: http://${HOSTNAME}:${portStr}
WebSocket: ws://${HOSTNAME}:${portStr}/api/events
Terminal: ws://${HOSTNAME}:${portStr}/api/terminal/ws
║ Health: http://${HOSTNAME}:${portStr}/api/health ║
║ Terminal: ${terminalStatus.padEnd(37)}
╚═══════════════════════════════════════════════════════╝
`);
@@ -582,7 +665,27 @@ const startServer = (port: number) => {
});
};
startServer(PORT);
startServer(PORT, HOST);
// Global error handlers to prevent crashes from uncaught errors
process.on('unhandledRejection', (reason: unknown, _promise: Promise<unknown>) => {
logger.error('Unhandled Promise Rejection:', {
reason: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
});
// Don't exit - log the error and continue running
// This prevents the server from crashing due to unhandled rejections
});
process.on('uncaughtException', (error: Error) => {
logger.error('Uncaught Exception:', {
message: error.message,
stack: error.stack,
});
// Exit on uncaught exceptions to prevent undefined behavior
// The process is in an unknown state after an uncaught exception
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', () => {

View File

@@ -0,0 +1,257 @@
/**
* Agent Discovery - Scans filesystem for AGENT.md files
*
* Discovers agents from:
* - ~/.claude/agents/ (user-level, global)
* - .claude/agents/ (project-level)
*
* Similar to Skills, but for custom subagents defined in AGENT.md files.
*/
import path from 'path';
import os from 'os';
import { createLogger } from '@automaker/utils';
import { secureFs, systemPaths } from '@automaker/platform';
import type { AgentDefinition } from '@automaker/types';
const logger = createLogger('AgentDiscovery');
export interface FilesystemAgent {
name: string; // Directory name (e.g., 'code-reviewer')
definition: AgentDefinition;
source: 'user' | 'project';
filePath: string; // Full path to AGENT.md
}
/**
* Parse agent content string into AgentDefinition
* Format:
* ---
* name: agent-name # Optional
* description: When to use this agent
* tools: tool1, tool2, tool3 # Optional (comma or space separated list)
* model: sonnet # Optional: sonnet, opus, haiku
* ---
* System prompt content here...
*/
function parseAgentContent(content: string, filePath: string): AgentDefinition | null {
// Extract frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
logger.warn(`Invalid agent file format (missing frontmatter): ${filePath}`);
return null;
}
const [, frontmatter, prompt] = frontmatterMatch;
// Parse description (required)
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim();
if (!description) {
logger.warn(`Missing description in agent file: ${filePath}`);
return null;
}
// Parse tools (optional) - supports both comma-separated and space-separated
const toolsMatch = frontmatter.match(/tools:\s*(.+)/);
const tools = toolsMatch
? toolsMatch[1]
.split(/[,\s]+/) // Split by comma or whitespace
.map((t) => t.trim())
.filter((t) => t && t !== '')
: undefined;
// Parse model (optional) - validate against allowed values
const modelMatch = frontmatter.match(/model:\s*(\w+)/);
const modelValue = modelMatch?.[1]?.trim();
const validModels = ['sonnet', 'opus', 'haiku', 'inherit'] as const;
const model =
modelValue && validModels.includes(modelValue as (typeof validModels)[number])
? (modelValue as 'sonnet' | 'opus' | 'haiku' | 'inherit')
: undefined;
if (modelValue && !model) {
logger.warn(
`Invalid model "${modelValue}" in agent file: ${filePath}. Expected one of: ${validModels.join(', ')}`
);
}
return {
description,
prompt: prompt.trim(),
tools,
model,
};
}
/**
* Directory entry with type information
*/
interface DirEntry {
name: string;
isFile: boolean;
isDirectory: boolean;
}
/**
* Filesystem adapter interface for abstracting systemPaths vs secureFs
*/
interface FsAdapter {
exists: (filePath: string) => Promise<boolean>;
readdir: (dirPath: string) => Promise<DirEntry[]>;
readFile: (filePath: string) => Promise<string>;
}
/**
* Create a filesystem adapter for system paths (user directory)
*/
function createSystemPathAdapter(): FsAdapter {
return {
exists: (filePath) => Promise.resolve(systemPaths.systemPathExists(filePath)),
readdir: async (dirPath) => {
const entryNames = await systemPaths.systemPathReaddir(dirPath);
const entries: DirEntry[] = [];
for (const name of entryNames) {
const stat = await systemPaths.systemPathStat(path.join(dirPath, name));
entries.push({
name,
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
});
}
return entries;
},
readFile: (filePath) => systemPaths.systemPathReadFile(filePath, 'utf-8') as Promise<string>,
};
}
/**
* Create a filesystem adapter for project paths (secureFs)
*/
function createSecureFsAdapter(): FsAdapter {
return {
exists: (filePath) =>
secureFs
.access(filePath)
.then(() => true)
.catch(() => false),
readdir: async (dirPath) => {
const entries = await secureFs.readdir(dirPath, { withFileTypes: true });
return entries.map((entry) => ({
name: entry.name,
isFile: entry.isFile(),
isDirectory: entry.isDirectory(),
}));
},
readFile: (filePath) => secureFs.readFile(filePath, 'utf-8') as Promise<string>,
};
}
/**
* Parse agent file using the provided filesystem adapter
*/
async function parseAgentFileWithAdapter(
filePath: string,
fsAdapter: FsAdapter
): Promise<AgentDefinition | null> {
try {
const content = await fsAdapter.readFile(filePath);
return parseAgentContent(content, filePath);
} catch (error) {
logger.error(`Failed to parse agent file: ${filePath}`, error);
return null;
}
}
/**
* Scan a directory for agent .md files
* Agents can be in two formats:
* 1. Flat: agent-name.md (file directly in agents/)
* 2. Subdirectory: agent-name/AGENT.md (folder + file, similar to Skills)
*/
async function scanAgentsDirectory(
baseDir: string,
source: 'user' | 'project'
): Promise<FilesystemAgent[]> {
const agents: FilesystemAgent[] = [];
const fsAdapter = source === 'user' ? createSystemPathAdapter() : createSecureFsAdapter();
try {
// Check if directory exists
const exists = await fsAdapter.exists(baseDir);
if (!exists) {
logger.debug(`Directory does not exist: ${baseDir}`);
return agents;
}
// Read all entries in the directory
const entries = await fsAdapter.readdir(baseDir);
for (const entry of entries) {
// Check for flat .md file format (agent-name.md)
if (entry.isFile && entry.name.endsWith('.md')) {
const agentName = entry.name.slice(0, -3); // Remove .md extension
const agentFilePath = path.join(baseDir, entry.name);
const definition = await parseAgentFileWithAdapter(agentFilePath, fsAdapter);
if (definition) {
agents.push({
name: agentName,
definition,
source,
filePath: agentFilePath,
});
logger.debug(`Discovered ${source} agent (flat): ${agentName}`);
}
}
// Check for subdirectory format (agent-name/AGENT.md)
else if (entry.isDirectory) {
const agentFilePath = path.join(baseDir, entry.name, 'AGENT.md');
const agentFileExists = await fsAdapter.exists(agentFilePath);
if (agentFileExists) {
const definition = await parseAgentFileWithAdapter(agentFilePath, fsAdapter);
if (definition) {
agents.push({
name: entry.name,
definition,
source,
filePath: agentFilePath,
});
logger.debug(`Discovered ${source} agent (subdirectory): ${entry.name}`);
}
}
}
}
} catch (error) {
logger.error(`Failed to scan agents directory: ${baseDir}`, error);
}
return agents;
}
/**
* Discover all filesystem-based agents from user and project sources
*/
export async function discoverFilesystemAgents(
projectPath?: string,
sources: Array<'user' | 'project'> = ['user', 'project']
): Promise<FilesystemAgent[]> {
const agents: FilesystemAgent[] = [];
// Discover user-level agents from ~/.claude/agents/
if (sources.includes('user')) {
const userAgentsDir = path.join(os.homedir(), '.claude', 'agents');
const userAgents = await scanAgentsDirectory(userAgentsDir, 'user');
agents.push(...userAgents);
logger.info(`Discovered ${userAgents.length} user-level agents from ${userAgentsDir}`);
}
// Discover project-level agents from .claude/agents/
if (sources.includes('project') && projectPath) {
const projectAgentsDir = path.join(projectPath, '.claude', 'agents');
const projectAgents = await scanAgentsDirectory(projectAgentsDir, 'project');
agents.push(...projectAgents);
logger.info(`Discovered ${projectAgents.length} project-level agents from ${projectAgentsDir}`);
}
return agents;
}

View File

@@ -11,8 +11,12 @@ export { specOutputSchema } from '@automaker/types';
/**
* Escape special XML characters
* Handles undefined/null values by converting them to empty strings
*/
function escapeXml(str: string): string {
export function escapeXml(str: string | undefined | null): string {
if (str == null) {
return '';
}
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')

View File

@@ -0,0 +1,263 @@
/**
* Secure authentication utilities that avoid environment variable race conditions
*/
import { spawn } from 'child_process';
import { createLogger } from '@automaker/utils';
const logger = createLogger('AuthUtils');
export interface SecureAuthEnv {
[key: string]: string | undefined;
}
export interface AuthValidationResult {
isValid: boolean;
error?: string;
normalizedKey?: string;
}
/**
* Validates API key format without modifying process.env
*/
export function validateApiKey(
key: string,
provider: 'anthropic' | 'openai' | 'cursor'
): AuthValidationResult {
if (!key || typeof key !== 'string' || key.trim().length === 0) {
return { isValid: false, error: 'API key is required' };
}
const trimmedKey = key.trim();
switch (provider) {
case 'anthropic':
if (!trimmedKey.startsWith('sk-ant-')) {
return {
isValid: false,
error: 'Invalid Anthropic API key format. Should start with "sk-ant-"',
};
}
if (trimmedKey.length < 20) {
return { isValid: false, error: 'Anthropic API key too short' };
}
break;
case 'openai':
if (!trimmedKey.startsWith('sk-')) {
return { isValid: false, error: 'Invalid OpenAI API key format. Should start with "sk-"' };
}
if (trimmedKey.length < 20) {
return { isValid: false, error: 'OpenAI API key too short' };
}
break;
case 'cursor':
// Cursor API keys might have different format
if (trimmedKey.length < 10) {
return { isValid: false, error: 'Cursor API key too short' };
}
break;
}
return { isValid: true, normalizedKey: trimmedKey };
}
/**
* Creates a secure environment object for authentication testing
* without modifying the global process.env
*/
export function createSecureAuthEnv(
authMethod: 'cli' | 'api_key',
apiKey?: string,
provider: 'anthropic' | 'openai' | 'cursor' = 'anthropic'
): SecureAuthEnv {
const env: SecureAuthEnv = { ...process.env };
if (authMethod === 'cli') {
// For CLI auth, remove the API key to force CLI authentication
const envKey = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
delete env[envKey];
} else if (authMethod === 'api_key' && apiKey) {
// For API key auth, validate and set the provided key
const validation = validateApiKey(apiKey, provider);
if (!validation.isValid) {
throw new Error(validation.error);
}
const envKey = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
env[envKey] = validation.normalizedKey;
}
return env;
}
/**
* Creates a temporary environment override for the current process
* WARNING: This should only be used in isolated contexts and immediately cleaned up
*/
export function createTempEnvOverride(authEnv: SecureAuthEnv): () => void {
const originalEnv = { ...process.env };
// Apply the auth environment
Object.assign(process.env, authEnv);
// Return cleanup function
return () => {
// Restore original environment
Object.keys(process.env).forEach((key) => {
if (!(key in originalEnv)) {
delete process.env[key];
}
});
Object.assign(process.env, originalEnv);
};
}
/**
* Spawns a process with secure environment isolation
*/
export function spawnSecureAuth(
command: string,
args: string[],
authEnv: SecureAuthEnv,
options: {
cwd?: string;
timeout?: number;
} = {}
): Promise<{ stdout: string; stderr: string; exitCode: number | null }> {
return new Promise((resolve, reject) => {
const { cwd = process.cwd(), timeout = 30000 } = options;
logger.debug(`Spawning secure auth process: ${command} ${args.join(' ')}`);
const child = spawn(command, args, {
cwd,
env: authEnv,
stdio: 'pipe',
shell: false,
});
let stdout = '';
let stderr = '';
let isResolved = false;
const timeoutId = setTimeout(() => {
if (!isResolved) {
child.kill('SIGTERM');
isResolved = true;
reject(new Error('Authentication process timed out'));
}
}, timeout);
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
clearTimeout(timeoutId);
if (!isResolved) {
isResolved = true;
resolve({ stdout, stderr, exitCode: code });
}
});
child.on('error', (error) => {
clearTimeout(timeoutId);
if (!isResolved) {
isResolved = true;
reject(error);
}
});
});
}
/**
* Safely extracts environment variable without race conditions
*/
export function safeGetEnv(key: string): string | undefined {
return process.env[key];
}
/**
* Checks if an environment variable would be modified without actually modifying it
*/
export function wouldModifyEnv(key: string, newValue: string): boolean {
const currentValue = safeGetEnv(key);
return currentValue !== newValue;
}
/**
* Secure auth session management
*/
export class AuthSessionManager {
private static activeSessions = new Map<string, SecureAuthEnv>();
static createSession(
sessionId: string,
authMethod: 'cli' | 'api_key',
apiKey?: string,
provider: 'anthropic' | 'openai' | 'cursor' = 'anthropic'
): SecureAuthEnv {
const env = createSecureAuthEnv(authMethod, apiKey, provider);
this.activeSessions.set(sessionId, env);
return env;
}
static getSession(sessionId: string): SecureAuthEnv | undefined {
return this.activeSessions.get(sessionId);
}
static destroySession(sessionId: string): void {
this.activeSessions.delete(sessionId);
}
static cleanup(): void {
this.activeSessions.clear();
}
}
/**
* Rate limiting for auth attempts to prevent abuse
*/
export class AuthRateLimiter {
private attempts = new Map<string, { count: number; lastAttempt: number }>();
constructor(
private maxAttempts = 5,
private windowMs = 60000
) {}
canAttempt(identifier: string): boolean {
const now = Date.now();
const record = this.attempts.get(identifier);
if (!record || now - record.lastAttempt > this.windowMs) {
this.attempts.set(identifier, { count: 1, lastAttempt: now });
return true;
}
if (record.count >= this.maxAttempts) {
return false;
}
record.count++;
record.lastAttempt = now;
return true;
}
getRemainingAttempts(identifier: string): number {
const record = this.attempts.get(identifier);
if (!record) return this.maxAttempts;
return Math.max(0, this.maxAttempts - record.count);
}
getResetTime(identifier: string): Date | null {
const record = this.attempts.get(identifier);
if (!record) return null;
return new Date(record.lastAttempt + this.windowMs);
}
}

View File

@@ -142,6 +142,8 @@ if (process.env.AUTOMAKER_HIDE_API_KEY !== 'true') {
${API_KEY}
║ ║
║ In Electron mode, authentication is handled automatically. ║
║ ║
║ 💡 Tip: Set AUTOMAKER_API_KEY env var to use a fixed key for dev ║
╚═══════════════════════════════════════════════════════════════════════╝
`);
} else {
@@ -262,7 +264,7 @@ export function getSessionCookieOptions(): {
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)
sameSite: 'lax', // Sent for same-site requests and top-level navigations, but not cross-origin fetch/XHR
maxAge: SESSION_MAX_AGE_MS,
path: '/',
};

View File

@@ -0,0 +1,518 @@
/**
* Unified CLI Detection Framework
*
* Provides consistent CLI detection and management across all providers
*/
import { spawn, execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { createLogger } from '@automaker/utils';
const logger = createLogger('CliDetection');
export interface CliInfo {
name: string;
command: string;
version?: string;
path?: string;
installed: boolean;
authenticated: boolean;
authMethod: 'cli' | 'api_key' | 'none';
platform?: string;
architectures?: string[];
}
export interface CliDetectionOptions {
timeout?: number;
includeWsl?: boolean;
wslDistribution?: string;
}
export interface CliDetectionResult {
cli: CliInfo;
detected: boolean;
issues: string[];
}
export interface UnifiedCliDetection {
claude?: CliDetectionResult;
codex?: CliDetectionResult;
cursor?: CliDetectionResult;
coderabbit?: CliDetectionResult;
}
/**
* CLI Configuration for different providers
*/
const CLI_CONFIGS = {
claude: {
name: 'Claude CLI',
commands: ['claude'],
versionArgs: ['--version'],
installCommands: {
darwin: 'brew install anthropics/claude/claude',
linux: 'curl -fsSL https://claude.ai/install.sh | sh',
win32: 'iwr https://claude.ai/install.ps1 -UseBasicParsing | iex',
},
},
codex: {
name: 'Codex CLI',
commands: ['codex', 'openai'],
versionArgs: ['--version'],
installCommands: {
darwin: 'npm install -g @openai/codex-cli',
linux: 'npm install -g @openai/codex-cli',
win32: 'npm install -g @openai/codex-cli',
},
},
cursor: {
name: 'Cursor CLI',
commands: ['cursor-agent', 'cursor'],
versionArgs: ['--version'],
installCommands: {
darwin: 'brew install cursor/cursor/cursor-agent',
linux: 'curl -fsSL https://cursor.sh/install.sh | sh',
win32: 'iwr https://cursor.sh/install.ps1 -UseBasicParsing | iex',
},
},
coderabbit: {
name: 'CodeRabbit CLI',
commands: ['coderabbit', 'cr'],
versionArgs: ['--version'],
installCommands: {
darwin: 'npm install -g coderabbit',
linux: 'npm install -g coderabbit',
win32: 'npm install -g coderabbit',
},
},
} as const;
/**
* Detect if a CLI is installed and available
*/
export async function detectCli(
provider: keyof typeof CLI_CONFIGS,
options: CliDetectionOptions = {}
): Promise<CliDetectionResult> {
const config = CLI_CONFIGS[provider];
const { timeout = 5000, includeWsl = false, wslDistribution } = options;
const issues: string[] = [];
const cliInfo: CliInfo = {
name: config.name,
command: '',
installed: false,
authenticated: false,
authMethod: 'none',
};
try {
// Find the command in PATH
const command = await findCommand([...config.commands]);
if (command) {
cliInfo.command = command;
}
if (!cliInfo.command) {
issues.push(`${config.name} not found in PATH`);
return { cli: cliInfo, detected: false, issues };
}
cliInfo.path = cliInfo.command;
cliInfo.installed = true;
// Get version
try {
cliInfo.version = await getCliVersion(cliInfo.command, [...config.versionArgs], timeout);
} catch (error) {
issues.push(`Failed to get ${config.name} version: ${error}`);
}
// Check authentication
cliInfo.authMethod = await checkCliAuth(provider, cliInfo.command);
cliInfo.authenticated = cliInfo.authMethod !== 'none';
return { cli: cliInfo, detected: true, issues };
} catch (error) {
issues.push(`Error detecting ${config.name}: ${error}`);
return { cli: cliInfo, detected: false, issues };
}
}
/**
* Detect all CLIs in the system
*/
export async function detectAllCLis(
options: CliDetectionOptions = {}
): Promise<UnifiedCliDetection> {
const results: UnifiedCliDetection = {};
// Detect all providers in parallel
const providers = Object.keys(CLI_CONFIGS) as Array<keyof typeof CLI_CONFIGS>;
const detectionPromises = providers.map(async (provider) => {
const result = await detectCli(provider, options);
return { provider, result };
});
const detections = await Promise.all(detectionPromises);
for (const { provider, result } of detections) {
results[provider] = result;
}
return results;
}
/**
* Find the first available command from a list of alternatives
*/
export async function findCommand(commands: string[]): Promise<string | null> {
for (const command of commands) {
try {
const whichCommand = process.platform === 'win32' ? 'where' : 'which';
const result = execSync(`${whichCommand} ${command}`, {
encoding: 'utf8',
timeout: 2000,
}).trim();
if (result) {
return result.split('\n')[0]; // Take first result on Windows
}
} catch {
// Command not found, try next
}
}
return null;
}
/**
* Get CLI version
*/
export async function getCliVersion(
command: string,
args: string[],
timeout: number = 5000
): Promise<string> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: 'pipe',
timeout,
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0 && stdout) {
resolve(stdout.trim());
} else if (stderr) {
reject(stderr.trim());
} else {
reject(`Command exited with code ${code}`);
}
});
child.on('error', reject);
});
}
/**
* Check authentication status for a CLI
*/
export async function checkCliAuth(
provider: keyof typeof CLI_CONFIGS,
command: string
): Promise<'cli' | 'api_key' | 'none'> {
try {
switch (provider) {
case 'claude':
return await checkClaudeAuth(command);
case 'codex':
return await checkCodexAuth(command);
case 'cursor':
return await checkCursorAuth(command);
case 'coderabbit':
return await checkCodeRabbitAuth(command);
default:
return 'none';
}
} catch {
return 'none';
}
}
/**
* Check Claude CLI authentication
*/
async function checkClaudeAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
try {
// Check for environment variable
if (process.env.ANTHROPIC_API_KEY) {
return 'api_key';
}
// Try running a simple command to check CLI auth
const result = await getCliVersion(command, ['--version'], 3000);
if (result) {
return 'cli'; // If version works, assume CLI is authenticated
}
} catch {
// Version command might work even without auth, so we need a better check
}
// Try a more specific auth check
return new Promise((resolve) => {
const child = spawn(command, ['whoami'], {
stdio: 'pipe',
timeout: 3000,
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0 && stdout && !stderr.includes('not authenticated')) {
resolve('cli');
} else {
resolve('none');
}
});
child.on('error', () => {
resolve('none');
});
});
}
/**
* Check Codex CLI authentication
*/
async function checkCodexAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
// Check for environment variable
if (process.env.OPENAI_API_KEY) {
return 'api_key';
}
try {
// Try a simple auth check
const result = await getCliVersion(command, ['--version'], 3000);
if (result) {
return 'cli';
}
} catch {
// Version check failed
}
return 'none';
}
/**
* Check Cursor CLI authentication
*/
async function checkCursorAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
// Check for environment variable
if (process.env.CURSOR_API_KEY) {
return 'api_key';
}
// Check for credentials files
const credentialPaths = [
path.join(os.homedir(), '.cursor', 'credentials.json'),
path.join(os.homedir(), '.config', 'cursor', 'credentials.json'),
path.join(os.homedir(), '.cursor', 'auth.json'),
path.join(os.homedir(), '.config', 'cursor', 'auth.json'),
];
for (const credPath of credentialPaths) {
try {
if (fs.existsSync(credPath)) {
const content = fs.readFileSync(credPath, 'utf8');
const creds = JSON.parse(content);
if (creds.accessToken || creds.token || creds.apiKey) {
return 'cli';
}
}
} catch {
// Invalid credentials file
}
}
// Try a simple command
try {
const result = await getCliVersion(command, ['--version'], 3000);
if (result) {
return 'cli';
}
} catch {
// Version check failed
}
return 'none';
}
/**
* Check CodeRabbit CLI authentication
*
* Expected output when authenticated:
* ```
* CodeRabbit CLI Status
* ✅ Authentication: Logged in
* User Information:
* 👤 Name: ...
* ```
*/
async function checkCodeRabbitAuth(command: string): Promise<'cli' | 'api_key' | 'none'> {
// Check for environment variable
if (process.env.CODERABBIT_API_KEY) {
return 'api_key';
}
// Try running auth status command
return new Promise((resolve) => {
const child = spawn(command, ['auth', 'status'], {
stdio: 'pipe',
timeout: 10000, // Increased timeout for slower systems
});
let stdout = '';
let stderr = '';
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
const output = stdout + stderr;
// Check for positive authentication indicators in output
const isAuthenticated =
code === 0 &&
(output.includes('Logged in') || output.includes('logged in')) &&
!output.toLowerCase().includes('not logged in') &&
!output.toLowerCase().includes('not authenticated');
if (isAuthenticated) {
resolve('cli');
} else {
resolve('none');
}
});
child.on('error', () => {
resolve('none');
});
});
}
/**
* Get installation instructions for a provider
*/
export function getInstallInstructions(
provider: keyof typeof CLI_CONFIGS,
platform: NodeJS.Platform = process.platform
): string {
const config = CLI_CONFIGS[provider];
const command = config.installCommands[platform as keyof typeof config.installCommands];
if (!command) {
return `No installation instructions available for ${provider} on ${platform}`;
}
return command;
}
/**
* Get platform-specific CLI paths and versions
*/
export function getPlatformCliPaths(provider: keyof typeof CLI_CONFIGS): string[] {
const config = CLI_CONFIGS[provider];
const platform = process.platform;
switch (platform) {
case 'darwin':
return [
`/usr/local/bin/${config.commands[0]}`,
`/opt/homebrew/bin/${config.commands[0]}`,
path.join(os.homedir(), '.local', 'bin', config.commands[0]),
];
case 'linux':
return [
`/usr/bin/${config.commands[0]}`,
`/usr/local/bin/${config.commands[0]}`,
path.join(os.homedir(), '.local', 'bin', config.commands[0]),
path.join(os.homedir(), '.npm', 'global', 'bin', config.commands[0]),
];
case 'win32':
return [
path.join(
os.homedir(),
'AppData',
'Local',
'Programs',
config.commands[0],
`${config.commands[0]}.exe`
),
path.join(process.env.ProgramFiles || '', config.commands[0], `${config.commands[0]}.exe`),
path.join(
process.env.ProgramFiles || '',
config.commands[0],
'bin',
`${config.commands[0]}.exe`
),
];
default:
return [];
}
}
/**
* Validate CLI installation
*/
export function validateCliInstallation(cliInfo: CliInfo): {
valid: boolean;
issues: string[];
} {
const issues: string[] = [];
if (!cliInfo.installed) {
issues.push('CLI is not installed');
}
if (cliInfo.installed && !cliInfo.version) {
issues.push('Could not determine CLI version');
}
if (cliInfo.installed && cliInfo.authMethod === 'none') {
issues.push('CLI is not authenticated');
}
return {
valid: issues.length === 0,
issues,
};
}

View File

@@ -0,0 +1,68 @@
/**
* Shared utility for checking Codex CLI authentication status
*
* Uses 'codex login status' command to verify authentication.
* Never assumes authenticated - only returns true if CLI confirms.
*/
import { spawnProcess } from '@automaker/platform';
import { findCodexCliPath } from '@automaker/platform';
import { createLogger } from '@automaker/utils';
const logger = createLogger('CodexAuth');
const CODEX_COMMAND = 'codex';
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
export interface CodexAuthCheckResult {
authenticated: boolean;
method: 'api_key_env' | 'cli_authenticated' | 'none';
}
/**
* Check Codex authentication status using 'codex login status' command
*
* @param cliPath Optional CLI path. If not provided, will attempt to find it.
* @returns Authentication status and method
*/
export async function checkCodexAuthentication(
cliPath?: string | null
): Promise<CodexAuthCheckResult> {
const resolvedCliPath = cliPath || (await findCodexCliPath());
const hasApiKey = !!process.env[OPENAI_API_KEY_ENV];
// If CLI is not installed, cannot be authenticated
if (!resolvedCliPath) {
logger.info('CLI not found');
return { authenticated: false, method: 'none' };
}
try {
const result = await spawnProcess({
command: resolvedCliPath || CODEX_COMMAND,
args: ['login', 'status'],
cwd: process.cwd(),
env: {
...process.env,
TERM: 'dumb', // Avoid interactive output
},
});
// Check both stdout and stderr for "logged in" - Codex CLI outputs to stderr
const combinedOutput = (result.stdout + result.stderr).toLowerCase();
const isLoggedIn = combinedOutput.includes('logged in');
if (result.exitCode === 0 && isLoggedIn) {
// Determine auth method based on what we know
const method = hasApiKey ? 'api_key_env' : 'cli_authenticated';
logger.info(`✓ Authenticated (${method})`);
return { authenticated: true, method };
}
logger.info('Not authenticated');
return { authenticated: false, method: 'none' };
} catch (error) {
logger.error('Failed to check authentication:', error);
return { authenticated: false, method: 'none' };
}
}

View File

@@ -0,0 +1,414 @@
/**
* Unified Error Handling System for CLI Providers
*
* Provides consistent error classification, user-friendly messages, and debugging support
* across all AI providers (Claude, Codex, Cursor)
*/
import { createLogger } from '@automaker/utils';
const logger = createLogger('ErrorHandler');
export enum ErrorType {
AUTHENTICATION = 'authentication',
BILLING = 'billing',
RATE_LIMIT = 'rate_limit',
NETWORK = 'network',
TIMEOUT = 'timeout',
VALIDATION = 'validation',
PERMISSION = 'permission',
CLI_NOT_FOUND = 'cli_not_found',
CLI_NOT_INSTALLED = 'cli_not_installed',
MODEL_NOT_SUPPORTED = 'model_not_supported',
INVALID_REQUEST = 'invalid_request',
SERVER_ERROR = 'server_error',
UNKNOWN = 'unknown',
}
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical',
}
export interface ErrorClassification {
type: ErrorType;
severity: ErrorSeverity;
userMessage: string;
technicalMessage: string;
suggestedAction?: string;
retryable: boolean;
provider?: string;
context?: Record<string, any>;
}
export interface ErrorPattern {
type: ErrorType;
severity: ErrorSeverity;
patterns: RegExp[];
userMessage: string;
suggestedAction?: string;
retryable: boolean;
}
/**
* Error patterns for different types of errors
*/
const ERROR_PATTERNS: ErrorPattern[] = [
// Authentication errors
{
type: ErrorType.AUTHENTICATION,
severity: ErrorSeverity.HIGH,
patterns: [
/unauthorized/i,
/authentication.*fail/i,
/invalid_api_key/i,
/invalid api key/i,
/not authenticated/i,
/please.*log/i,
/token.*revoked/i,
/oauth.*error/i,
/credentials.*invalid/i,
],
userMessage: 'Authentication failed. Please check your API key or login credentials.',
suggestedAction:
"Verify your API key is correct and hasn't expired, or run the CLI login command.",
retryable: false,
},
// Billing errors
{
type: ErrorType.BILLING,
severity: ErrorSeverity.HIGH,
patterns: [
/credit.*balance.*low/i,
/insufficient.*credit/i,
/billing.*issue/i,
/payment.*required/i,
/usage.*exceeded/i,
/quota.*exceeded/i,
/add.*credit/i,
],
userMessage: 'Account has insufficient credits or billing issues.',
suggestedAction: 'Please add credits to your account or check your billing settings.',
retryable: false,
},
// Rate limit errors
{
type: ErrorType.RATE_LIMIT,
severity: ErrorSeverity.MEDIUM,
patterns: [
/rate.*limit/i,
/too.*many.*request/i,
/limit.*reached/i,
/try.*later/i,
/429/i,
/reset.*time/i,
/upgrade.*plan/i,
],
userMessage: 'Rate limit reached. Please wait before trying again.',
suggestedAction: 'Wait a few minutes before retrying, or consider upgrading your plan.',
retryable: true,
},
// Network errors
{
type: ErrorType.NETWORK,
severity: ErrorSeverity.MEDIUM,
patterns: [/network/i, /connection/i, /dns/i, /timeout/i, /econnrefused/i, /enotfound/i],
userMessage: 'Network connection issue.',
suggestedAction: 'Check your internet connection and try again.',
retryable: true,
},
// Timeout errors
{
type: ErrorType.TIMEOUT,
severity: ErrorSeverity.MEDIUM,
patterns: [/timeout/i, /aborted/i, /time.*out/i],
userMessage: 'Operation timed out.',
suggestedAction: 'Try again with a simpler request or check your connection.',
retryable: true,
},
// Permission errors
{
type: ErrorType.PERMISSION,
severity: ErrorSeverity.HIGH,
patterns: [/permission.*denied/i, /access.*denied/i, /forbidden/i, /403/i, /not.*authorized/i],
userMessage: 'Permission denied.',
suggestedAction: 'Check if you have the required permissions for this operation.',
retryable: false,
},
// CLI not found
{
type: ErrorType.CLI_NOT_FOUND,
severity: ErrorSeverity.HIGH,
patterns: [/command not found/i, /not recognized/i, /not.*installed/i, /ENOENT/i],
userMessage: 'CLI tool not found.',
suggestedAction: "Please install the required CLI tool and ensure it's in your PATH.",
retryable: false,
},
// Model not supported
{
type: ErrorType.MODEL_NOT_SUPPORTED,
severity: ErrorSeverity.HIGH,
patterns: [/model.*not.*support/i, /unknown.*model/i, /invalid.*model/i],
userMessage: 'Model not supported.',
suggestedAction: 'Check available models and use a supported one.',
retryable: false,
},
// Server errors
{
type: ErrorType.SERVER_ERROR,
severity: ErrorSeverity.HIGH,
patterns: [/internal.*server/i, /server.*error/i, /500/i, /502/i, /503/i, /504/i],
userMessage: 'Server error occurred.',
suggestedAction: 'Try again in a few minutes or contact support if the issue persists.',
retryable: true,
},
];
/**
* Classify an error into a specific type with user-friendly message
*/
export function classifyError(
error: unknown,
provider?: string,
context?: Record<string, any>
): ErrorClassification {
const errorText = getErrorText(error);
// Try to match against known patterns
for (const pattern of ERROR_PATTERNS) {
for (const regex of pattern.patterns) {
if (regex.test(errorText)) {
return {
type: pattern.type,
severity: pattern.severity,
userMessage: pattern.userMessage,
technicalMessage: errorText,
suggestedAction: pattern.suggestedAction,
retryable: pattern.retryable,
provider,
context,
};
}
}
}
// Unknown error
return {
type: ErrorType.UNKNOWN,
severity: ErrorSeverity.MEDIUM,
userMessage: 'An unexpected error occurred.',
technicalMessage: errorText,
suggestedAction: 'Please try again or contact support if the issue persists.',
retryable: true,
provider,
context,
};
}
/**
* Get a user-friendly error message
*/
export function getUserFriendlyErrorMessage(error: unknown, provider?: string): string {
const classification = classifyError(error, provider);
let message = classification.userMessage;
if (classification.suggestedAction) {
message += ` ${classification.suggestedAction}`;
}
// Add provider-specific context if available
if (provider) {
message = `[${provider.toUpperCase()}] ${message}`;
}
return message;
}
/**
* Check if an error is retryable
*/
export function isRetryableError(error: unknown): boolean {
const classification = classifyError(error);
return classification.retryable;
}
/**
* Check if an error is authentication-related
*/
export function isAuthenticationError(error: unknown): boolean {
const classification = classifyError(error);
return classification.type === ErrorType.AUTHENTICATION;
}
/**
* Check if an error is billing-related
*/
export function isBillingError(error: unknown): boolean {
const classification = classifyError(error);
return classification.type === ErrorType.BILLING;
}
/**
* Check if an error is rate limit related
*/
export function isRateLimitError(error: unknown): boolean {
const classification = classifyError(error);
return classification.type === ErrorType.RATE_LIMIT;
}
/**
* Get error text from various error types
*/
function getErrorText(error: unknown): string {
if (typeof error === 'string') {
return error;
}
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'object' && error !== null) {
// Handle structured error objects
const errorObj = error as any;
if (errorObj.message) {
return errorObj.message;
}
if (errorObj.error?.message) {
return errorObj.error.message;
}
if (errorObj.error) {
return typeof errorObj.error === 'string' ? errorObj.error : JSON.stringify(errorObj.error);
}
return JSON.stringify(error);
}
return String(error);
}
/**
* Create a standardized error response
*/
export function createErrorResponse(
error: unknown,
provider?: string,
context?: Record<string, any>
): {
success: false;
error: string;
errorType: ErrorType;
severity: ErrorSeverity;
retryable: boolean;
suggestedAction?: string;
} {
const classification = classifyError(error, provider, context);
return {
success: false,
error: classification.userMessage,
errorType: classification.type,
severity: classification.severity,
retryable: classification.retryable,
suggestedAction: classification.suggestedAction,
};
}
/**
* Log error with full context
*/
export function logError(
error: unknown,
provider?: string,
operation?: string,
additionalContext?: Record<string, any>
): void {
const classification = classifyError(error, provider, {
operation,
...additionalContext,
});
logger.error(`Error in ${provider || 'unknown'}${operation ? ` during ${operation}` : ''}`, {
type: classification.type,
severity: classification.severity,
message: classification.userMessage,
technicalMessage: classification.technicalMessage,
retryable: classification.retryable,
suggestedAction: classification.suggestedAction,
context: classification.context,
});
}
/**
* Provider-specific error handlers
*/
export const ProviderErrorHandler = {
claude: {
classify: (error: unknown) => classifyError(error, 'claude'),
getUserMessage: (error: unknown) => getUserFriendlyErrorMessage(error, 'claude'),
isAuth: (error: unknown) => isAuthenticationError(error),
isBilling: (error: unknown) => isBillingError(error),
isRateLimit: (error: unknown) => isRateLimitError(error),
},
codex: {
classify: (error: unknown) => classifyError(error, 'codex'),
getUserMessage: (error: unknown) => getUserFriendlyErrorMessage(error, 'codex'),
isAuth: (error: unknown) => isAuthenticationError(error),
isBilling: (error: unknown) => isBillingError(error),
isRateLimit: (error: unknown) => isRateLimitError(error),
},
cursor: {
classify: (error: unknown) => classifyError(error, 'cursor'),
getUserMessage: (error: unknown) => getUserFriendlyErrorMessage(error, 'cursor'),
isAuth: (error: unknown) => isAuthenticationError(error),
isBilling: (error: unknown) => isBillingError(error),
isRateLimit: (error: unknown) => isRateLimitError(error),
},
};
/**
* Create a retry handler for retryable errors
*/
export function createRetryHandler(maxRetries: number = 3, baseDelay: number = 1000) {
return async function <T>(
operation: () => Promise<T>,
shouldRetry: (error: unknown) => boolean = isRetryableError
): Promise<T> {
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries || !shouldRetry(error)) {
throw error;
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
logger.debug(`Retrying operation in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
};
}

View File

@@ -0,0 +1,173 @@
/**
* Permission enforcement utilities for Cursor provider
*/
import type { CursorCliConfigFile } from '@automaker/types';
import { createLogger } from '@automaker/utils';
const logger = createLogger('PermissionEnforcer');
export interface PermissionCheckResult {
allowed: boolean;
reason?: string;
}
/**
* Check if a tool call is allowed based on permissions
*/
export function checkToolCallPermission(
toolCall: any,
permissions: CursorCliConfigFile | null
): PermissionCheckResult {
if (!permissions || !permissions.permissions) {
// If no permissions are configured, allow everything (backward compatibility)
return { allowed: true };
}
const { allow = [], deny = [] } = permissions.permissions;
// Check shell tool calls
if (toolCall.shellToolCall?.args?.command) {
const command = toolCall.shellToolCall.args.command;
const toolName = `Shell(${extractCommandName(command)})`;
// Check deny list first (deny takes precedence)
for (const denyRule of deny) {
if (matchesRule(toolName, denyRule)) {
return {
allowed: false,
reason: `Operation blocked by permission rule: ${denyRule}`,
};
}
}
// Then check allow list
for (const allowRule of allow) {
if (matchesRule(toolName, allowRule)) {
return { allowed: true };
}
}
return {
allowed: false,
reason: `Operation not in allow list: ${toolName}`,
};
}
// Check read tool calls
if (toolCall.readToolCall?.args?.path) {
const path = toolCall.readToolCall.args.path;
const toolName = `Read(${path})`;
// Check deny list first
for (const denyRule of deny) {
if (matchesRule(toolName, denyRule)) {
return {
allowed: false,
reason: `Read operation blocked by permission rule: ${denyRule}`,
};
}
}
// Then check allow list
for (const allowRule of allow) {
if (matchesRule(toolName, allowRule)) {
return { allowed: true };
}
}
return {
allowed: false,
reason: `Read operation not in allow list: ${toolName}`,
};
}
// Check write tool calls
if (toolCall.writeToolCall?.args?.path) {
const path = toolCall.writeToolCall.args.path;
const toolName = `Write(${path})`;
// Check deny list first
for (const denyRule of deny) {
if (matchesRule(toolName, denyRule)) {
return {
allowed: false,
reason: `Write operation blocked by permission rule: ${denyRule}`,
};
}
}
// Then check allow list
for (const allowRule of allow) {
if (matchesRule(toolName, allowRule)) {
return { allowed: true };
}
}
return {
allowed: false,
reason: `Write operation not in allow list: ${toolName}`,
};
}
// For other tool types, allow by default for now
return { allowed: true };
}
/**
* Extract the base command name from a shell command
*/
function extractCommandName(command: string): string {
// Remove leading spaces and get the first word
const trimmed = command.trim();
const firstWord = trimmed.split(/\s+/)[0];
return firstWord || 'unknown';
}
/**
* Check if a tool name matches a permission rule
*/
function matchesRule(toolName: string, rule: string): boolean {
// Exact match
if (toolName === rule) {
return true;
}
// Wildcard patterns
if (rule.includes('*')) {
const regex = new RegExp(rule.replace(/\*/g, '.*'));
return regex.test(toolName);
}
// Prefix match for shell commands (e.g., "Shell(git)" matches "Shell(git status)")
if (rule.startsWith('Shell(') && toolName.startsWith('Shell(')) {
const ruleCommand = rule.slice(6, -1); // Remove "Shell(" and ")"
const toolCommand = extractCommandName(toolName.slice(6, -1)); // Remove "Shell(" and ")"
return toolCommand.startsWith(ruleCommand);
}
return false;
}
/**
* Log permission violations
*/
export function logPermissionViolation(toolCall: any, reason: string, sessionId?: string): void {
const sessionIdStr = sessionId ? ` [${sessionId}]` : '';
if (toolCall.shellToolCall?.args?.command) {
logger.warn(
`Permission violation${sessionIdStr}: Shell command blocked - ${toolCall.shellToolCall.args.command} (${reason})`
);
} else if (toolCall.readToolCall?.args?.path) {
logger.warn(
`Permission violation${sessionIdStr}: Read operation blocked - ${toolCall.readToolCall.args.path} (${reason})`
);
} else if (toolCall.writeToolCall?.args?.path) {
logger.warn(
`Permission violation${sessionIdStr}: Write operation blocked - ${toolCall.writeToolCall.args.path} (${reason})`
);
} else {
logger.warn(`Permission violation${sessionIdStr}: Tool call blocked (${reason})`, { toolCall });
}
}

View File

@@ -16,7 +16,6 @@
*/
import type { Options } from '@anthropic-ai/claude-agent-sdk';
import os from 'os';
import path from 'path';
import { resolveModelString } from '@automaker/model-resolver';
import { createLogger } from '@automaker/utils';
@@ -31,6 +30,68 @@ import {
} from '@automaker/types';
import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform';
/**
* Result of sandbox compatibility check
*/
export interface SandboxCompatibilityResult {
/** Whether sandbox mode can be enabled for this path */
enabled: boolean;
/** Optional message explaining why sandbox is disabled */
message?: string;
}
/**
* Check if a working directory is compatible with sandbox mode.
* Some paths (like cloud storage mounts) may not work with sandboxed execution.
*
* @param cwd - The working directory to check
* @param sandboxRequested - Whether sandbox mode was requested by settings
* @returns Object indicating if sandbox can be enabled and why not if disabled
*/
export function checkSandboxCompatibility(
cwd: string,
sandboxRequested: boolean
): SandboxCompatibilityResult {
if (!sandboxRequested) {
return { enabled: false };
}
const resolvedCwd = path.resolve(cwd);
// Check for cloud storage paths that may not be compatible with sandbox
const cloudStoragePatterns = [
// macOS mounted volumes
/^\/Volumes\/GoogleDrive/i,
/^\/Volumes\/Dropbox/i,
/^\/Volumes\/OneDrive/i,
/^\/Volumes\/iCloud/i,
// macOS home directory
/^\/Users\/[^/]+\/Google Drive/i,
/^\/Users\/[^/]+\/Dropbox/i,
/^\/Users\/[^/]+\/OneDrive/i,
/^\/Users\/[^/]+\/Library\/Mobile Documents/i, // iCloud
// Linux home directory
/^\/home\/[^/]+\/Google Drive/i,
/^\/home\/[^/]+\/Dropbox/i,
/^\/home\/[^/]+\/OneDrive/i,
// Windows
/^C:\\Users\\[^\\]+\\Google Drive/i,
/^C:\\Users\\[^\\]+\\Dropbox/i,
/^C:\\Users\\[^\\]+\\OneDrive/i,
];
for (const pattern of cloudStoragePatterns) {
if (pattern.test(resolvedCwd)) {
return {
enabled: false,
message: `Sandbox disabled: Cloud storage path detected (${resolvedCwd}). Sandbox mode may not work correctly with cloud-synced directories.`,
};
}
}
return { enabled: true };
}
/**
* Validate that a working directory is allowed by ALLOWED_ROOT_DIRECTORY.
* This is the centralized security check for ALL AI model invocations.
@@ -57,139 +118,6 @@ 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
*/
@@ -201,10 +129,30 @@ export const TOOL_PRESETS = {
specGeneration: ['Read', 'Glob', 'Grep'] as const,
/** Full tool access for feature implementation */
fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'] as const,
fullAccess: [
'Read',
'Write',
'Edit',
'Glob',
'Grep',
'Bash',
'WebSearch',
'WebFetch',
'TodoWrite',
] as const,
/** Tools for chat/interactive mode */
chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'] as const,
chat: [
'Read',
'Write',
'Edit',
'Glob',
'Grep',
'Bash',
'WebSearch',
'WebFetch',
'TodoWrite',
] as const,
} as const;
/**
@@ -272,55 +220,31 @@ export function getModelForUseCase(
/**
* Base options that apply to all SDK calls
* AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
*/
function getBaseOptions(): Partial<Options> {
return {
permissionMode: 'acceptEdits',
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
};
}
/**
* MCP permission options result
* MCP 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>;
interface McpOptions {
/** 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
* @returns Object with MCP server 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;
function buildMcpOptions(config: CreateSdkOptionsConfig): McpOptions {
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 } : {},
};
@@ -422,18 +346,9 @@ export interface CreateSdkOptionsConfig {
/** Enable auto-loading of CLAUDE.md files via SDK's settingSources */
autoLoadClaudeMd?: boolean;
/** Enable sandbox mode for bash command isolation */
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;
}
@@ -554,7 +469,6 @@ export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Option
* - Full tool access for code modification
* - Standard turns for interactive sessions
* - Model priority: explicit model > session model > chat default
* - Sandbox mode controlled by enableSandboxMode setting (auto-disabled for cloud storage)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
@@ -573,24 +487,12 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
// 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 {
...getBaseOptions(),
model: getModelForUseCase('chat', effectiveModel),
maxTurns: MAX_TURNS.standard,
cwd: config.cwd,
// Only restrict tools if no MCP servers configured or unrestricted is disabled
...(mcpOptions.shouldRestrictTools && { allowedTools: [...TOOL_PRESETS.chat] }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...(sandboxCheck.enabled && {
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
}),
allowedTools: [...TOOL_PRESETS.chat],
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
@@ -605,7 +507,6 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
* - Full tool access for code modification and implementation
* - Extended turns for thorough feature implementation
* - Uses default model (can be overridden)
* - Sandbox mode controlled by enableSandboxMode setting (auto-disabled for cloud storage)
* - When autoLoadClaudeMd is true, uses preset mode and settingSources for CLAUDE.md loading
*/
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
@@ -621,24 +522,12 @@ export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
// 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 {
...getBaseOptions(),
model: getModelForUseCase('auto', config.model),
maxTurns: MAX_TURNS.maximum,
cwd: config.cwd,
// Only restrict tools if no MCP servers configured or unrestricted is disabled
...(mcpOptions.shouldRestrictTools && { allowedTools: [...TOOL_PRESETS.fullAccess] }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
...(sandboxCheck.enabled && {
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
},
}),
allowedTools: [...TOOL_PRESETS.fullAccess],
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),
@@ -656,7 +545,6 @@ export function createCustomOptions(
config: CreateSdkOptionsConfig & {
maxTurns?: number;
allowedTools?: readonly string[];
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean };
}
): Options {
// Validate working directory before creating options
@@ -671,22 +559,17 @@ export function createCustomOptions(
// Build thinking options
const thinkingOptions = buildThinkingOptions(config.thinkingLevel);
// For custom options: use explicit allowedTools if provided, otherwise use preset based on MCP settings
// For custom options: use explicit allowedTools if provided, otherwise default to readOnly
const effectiveAllowedTools = config.allowedTools
? [...config.allowedTools]
: mcpOptions.shouldRestrictTools
? [...TOOL_PRESETS.readOnly]
: undefined;
: [...TOOL_PRESETS.readOnly];
return {
...getBaseOptions(),
model: getModelForUseCase('default', config.model),
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
cwd: config.cwd,
...(effectiveAllowedTools && { allowedTools: effectiveAllowedTools }),
...(config.sandbox && { sandbox: config.sandbox }),
// Apply MCP bypass options if configured
...mcpOptions.bypassOptions,
allowedTools: effectiveAllowedTools,
...claudeMdOptions,
...thinkingOptions,
...(config.abortController && { abortController: config.abortController }),

View File

@@ -11,6 +11,14 @@ import {
mergeAgentPrompts,
mergeBacklogPlanPrompts,
mergeEnhancementPrompts,
mergeCommitMessagePrompts,
mergeTitleGenerationPrompts,
mergeIssueValidationPrompts,
mergeIdeationPrompts,
mergeAppSpecPrompts,
mergeContextDescriptionPrompts,
mergeSuggestionsPrompts,
mergeTaskExecutionPrompts,
} from '@automaker/prompts';
const logger = createLogger('SettingsHelper');
@@ -55,34 +63,6 @@ export async function getAutoLoadClaudeMdSetting(
}
}
/**
* Get the enableSandboxMode setting from global settings.
* Returns false if settings service is not available.
*
* @param settingsService - Optional settings service instance
* @param logPrefix - Prefix for log messages (e.g., '[AgentService]')
* @returns Promise resolving to the enableSandboxMode setting value
*/
export async function getEnableSandboxModeSetting(
settingsService?: SettingsService | null,
logPrefix = '[SettingsHelper]'
): Promise<boolean> {
if (!settingsService) {
logger.info(`${logPrefix} SettingsService not available, sandbox mode disabled`);
return false;
}
try {
const globalSettings = await settingsService.getGlobalSettings();
const result = globalSettings.enableSandboxMode ?? false;
logger.info(`${logPrefix} enableSandboxMode from global settings: ${result}`);
return result;
} catch (error) {
logger.error(`${logPrefix} Failed to load enableSandboxMode setting:`, error);
throw error;
}
}
/**
* Filters out CLAUDE.md from context files when autoLoadClaudeMd is enabled
* and rebuilds the formatted prompt without it.
@@ -246,6 +226,14 @@ export async function getPromptCustomization(
agent: ReturnType<typeof mergeAgentPrompts>;
backlogPlan: ReturnType<typeof mergeBacklogPlanPrompts>;
enhancement: ReturnType<typeof mergeEnhancementPrompts>;
commitMessage: ReturnType<typeof mergeCommitMessagePrompts>;
titleGeneration: ReturnType<typeof mergeTitleGenerationPrompts>;
issueValidation: ReturnType<typeof mergeIssueValidationPrompts>;
ideation: ReturnType<typeof mergeIdeationPrompts>;
appSpec: ReturnType<typeof mergeAppSpecPrompts>;
contextDescription: ReturnType<typeof mergeContextDescriptionPrompts>;
suggestions: ReturnType<typeof mergeSuggestionsPrompts>;
taskExecution: ReturnType<typeof mergeTaskExecutionPrompts>;
}> {
let customization: PromptCustomization = {};
@@ -267,5 +255,93 @@ export async function getPromptCustomization(
agent: mergeAgentPrompts(customization.agent),
backlogPlan: mergeBacklogPlanPrompts(customization.backlogPlan),
enhancement: mergeEnhancementPrompts(customization.enhancement),
commitMessage: mergeCommitMessagePrompts(customization.commitMessage),
titleGeneration: mergeTitleGenerationPrompts(customization.titleGeneration),
issueValidation: mergeIssueValidationPrompts(customization.issueValidation),
ideation: mergeIdeationPrompts(customization.ideation),
appSpec: mergeAppSpecPrompts(customization.appSpec),
contextDescription: mergeContextDescriptionPrompts(customization.contextDescription),
suggestions: mergeSuggestionsPrompts(customization.suggestions),
taskExecution: mergeTaskExecutionPrompts(customization.taskExecution),
};
}
/**
* Get Skills configuration from settings.
* Returns configuration for enabling skills and which sources to load from.
*
* @param settingsService - Settings service instance
* @returns Skills configuration with enabled state, sources, and tool inclusion flag
*/
export async function getSkillsConfiguration(settingsService: SettingsService): Promise<{
enabled: boolean;
sources: Array<'user' | 'project'>;
shouldIncludeInTools: boolean;
}> {
const settings = await settingsService.getGlobalSettings();
const enabled = settings.enableSkills ?? true; // Default enabled
const sources = settings.skillsSources ?? ['user', 'project']; // Default both sources
return {
enabled,
sources,
shouldIncludeInTools: enabled && sources.length > 0,
};
}
/**
* Get Subagents configuration from settings.
* Returns configuration for enabling subagents and which sources to load from.
*
* @param settingsService - Settings service instance
* @returns Subagents configuration with enabled state, sources, and tool inclusion flag
*/
export async function getSubagentsConfiguration(settingsService: SettingsService): Promise<{
enabled: boolean;
sources: Array<'user' | 'project'>;
shouldIncludeInTools: boolean;
}> {
const settings = await settingsService.getGlobalSettings();
const enabled = settings.enableSubagents ?? true; // Default enabled
const sources = settings.subagentsSources ?? ['user', 'project']; // Default both sources
return {
enabled,
sources,
shouldIncludeInTools: enabled && sources.length > 0,
};
}
/**
* Get custom subagents from settings, merging global and project-level definitions.
* Project-level subagents take precedence over global ones with the same name.
*
* @param settingsService - Settings service instance
* @param projectPath - Path to the project for loading project-specific subagents
* @returns Record of agent names to definitions, or undefined if none configured
*/
export async function getCustomSubagents(
settingsService: SettingsService,
projectPath?: string
): Promise<Record<string, import('@automaker/types').AgentDefinition> | undefined> {
// Get global subagents
const globalSettings = await settingsService.getGlobalSettings();
const globalSubagents = globalSettings.customSubagents || {};
// If no project path, return only global subagents
if (!projectPath) {
return Object.keys(globalSubagents).length > 0 ? globalSubagents : undefined;
}
// Get project-specific subagents
const projectSettings = await settingsService.getProjectSettings(projectPath);
const projectSubagents = projectSettings.customSubagents || {};
// Merge: project-level takes precedence
const merged = {
...globalSubagents,
...projectSubagents,
};
return Object.keys(merged).length > 0 ? merged : undefined;
}

View File

@@ -5,22 +5,24 @@
import * as secureFs from './secure-fs.js';
import * as path from 'path';
import type { PRState, WorktreePRInfo } from '@automaker/types';
// Re-export types for backwards compatibility
export type { PRState, WorktreePRInfo };
/** Maximum length for sanitized branch names in filesystem paths */
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
export interface WorktreePRInfo {
number: number;
url: string;
title: string;
state: string;
createdAt: string;
}
export interface WorktreeMetadata {
branch: string;
createdAt: string;
pr?: WorktreePRInfo;
/** Whether the init script has been executed for this worktree */
initScriptRan?: boolean;
/** Status of the init script execution */
initScriptStatus?: 'running' | 'success' | 'failed';
/** Error message if init script failed */
initScriptError?: string;
}
/**

View File

@@ -0,0 +1,611 @@
/**
* XML Extraction Utilities
*
* Robust XML parsing utilities for extracting and updating sections
* from app_spec.txt XML content. Uses regex-based parsing which is
* sufficient for our controlled XML structure.
*
* Note: If more complex XML parsing is needed in the future, consider
* using a library like 'fast-xml-parser' or 'xml2js'.
*/
import { createLogger } from '@automaker/utils';
import type { SpecOutput } from '@automaker/types';
const logger = createLogger('XmlExtractor');
/**
* Represents an implemented feature extracted from XML
*/
export interface ImplementedFeature {
name: string;
description: string;
file_locations?: string[];
}
/**
* Logger interface for optional custom logging
*/
export interface XmlExtractorLogger {
debug: (message: string, ...args: unknown[]) => void;
warn?: (message: string, ...args: unknown[]) => void;
}
/**
* Options for XML extraction operations
*/
export interface ExtractXmlOptions {
/** Custom logger (defaults to internal logger) */
logger?: XmlExtractorLogger;
}
/**
* Escape special XML characters
* Handles undefined/null values by converting them to empty strings
*/
export function escapeXml(str: string | undefined | null): string {
if (str == null) {
return '';
}
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Unescape XML entities back to regular characters
*/
export function unescapeXml(str: string): string {
return str
.replace(/&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&');
}
/**
* Extract the content of a specific XML section
*
* @param xmlContent - The full XML content
* @param tagName - The tag name to extract (e.g., 'implemented_features')
* @param options - Optional extraction options
* @returns The content between the tags, or null if not found
*/
export function extractXmlSection(
xmlContent: string,
tagName: string,
options: ExtractXmlOptions = {}
): string | null {
const log = options.logger || logger;
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'i');
const match = xmlContent.match(regex);
if (match) {
log.debug(`Extracted <${tagName}> section`);
return match[1];
}
log.debug(`Section <${tagName}> not found`);
return null;
}
/**
* Extract all values from repeated XML elements
*
* @param xmlContent - The XML content to search
* @param tagName - The tag name to extract values from
* @param options - Optional extraction options
* @returns Array of extracted values (unescaped)
*/
export function extractXmlElements(
xmlContent: string,
tagName: string,
options: ExtractXmlOptions = {}
): string[] {
const log = options.logger || logger;
const values: string[] = [];
const regex = new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`, 'g');
const matches = xmlContent.matchAll(regex);
for (const match of matches) {
values.push(unescapeXml(match[1].trim()));
}
log.debug(`Extracted ${values.length} <${tagName}> elements`);
return values;
}
/**
* Extract implemented features from app_spec.txt XML content
*
* @param specContent - The full XML content of app_spec.txt
* @param options - Optional extraction options
* @returns Array of implemented features with name, description, and optional file_locations
*/
export function extractImplementedFeatures(
specContent: string,
options: ExtractXmlOptions = {}
): ImplementedFeature[] {
const log = options.logger || logger;
const features: ImplementedFeature[] = [];
// Match <implemented_features>...</implemented_features> section
const implementedSection = extractXmlSection(specContent, 'implemented_features', options);
if (!implementedSection) {
log.debug('No implemented_features section found');
return features;
}
// Extract individual feature blocks
const featureRegex = /<feature>([\s\S]*?)<\/feature>/g;
const featureMatches = implementedSection.matchAll(featureRegex);
for (const featureMatch of featureMatches) {
const featureContent = featureMatch[1];
// Extract name
const nameMatch = featureContent.match(/<name>([\s\S]*?)<\/name>/);
const name = nameMatch ? unescapeXml(nameMatch[1].trim()) : '';
// Extract description
const descMatch = featureContent.match(/<description>([\s\S]*?)<\/description>/);
const description = descMatch ? unescapeXml(descMatch[1].trim()) : '';
// Extract file_locations if present
const locationsSection = extractXmlSection(featureContent, 'file_locations', options);
const file_locations = locationsSection
? extractXmlElements(locationsSection, 'location', options)
: undefined;
if (name) {
features.push({
name,
description,
...(file_locations && file_locations.length > 0 ? { file_locations } : {}),
});
}
}
log.debug(`Extracted ${features.length} implemented features`);
return features;
}
/**
* Extract only the feature names from implemented_features section
*
* @param specContent - The full XML content of app_spec.txt
* @param options - Optional extraction options
* @returns Array of feature names
*/
export function extractImplementedFeatureNames(
specContent: string,
options: ExtractXmlOptions = {}
): string[] {
const features = extractImplementedFeatures(specContent, options);
return features.map((f) => f.name);
}
/**
* Generate XML for a single implemented feature
*
* @param feature - The feature to convert to XML
* @param indent - The base indentation level (default: 2 spaces)
* @returns XML string for the feature
*/
export function featureToXml(feature: ImplementedFeature, indent: string = ' '): string {
const i2 = indent.repeat(2);
const i3 = indent.repeat(3);
const i4 = indent.repeat(4);
let xml = `${i2}<feature>
${i3}<name>${escapeXml(feature.name)}</name>
${i3}<description>${escapeXml(feature.description)}</description>`;
if (feature.file_locations && feature.file_locations.length > 0) {
xml += `
${i3}<file_locations>
${feature.file_locations.map((loc) => `${i4}<location>${escapeXml(loc)}</location>`).join('\n')}
${i3}</file_locations>`;
}
xml += `
${i2}</feature>`;
return xml;
}
/**
* Generate XML for an array of implemented features
*
* @param features - Array of features to convert to XML
* @param indent - The base indentation level (default: 2 spaces)
* @returns XML string for the implemented_features section content
*/
export function featuresToXml(features: ImplementedFeature[], indent: string = ' '): string {
return features.map((f) => featureToXml(f, indent)).join('\n');
}
/**
* Update the implemented_features section in XML content
*
* @param specContent - The full XML content
* @param newFeatures - The new features to set
* @param options - Optional extraction options
* @returns Updated XML content with the new implemented_features section
*/
export function updateImplementedFeaturesSection(
specContent: string,
newFeatures: ImplementedFeature[],
options: ExtractXmlOptions = {}
): string {
const log = options.logger || logger;
const indent = ' ';
// Generate new section content
const newSectionContent = featuresToXml(newFeatures, indent);
// Build the new section
const newSection = `<implemented_features>
${newSectionContent}
${indent}</implemented_features>`;
// Check if section exists
const sectionRegex = /<implemented_features>[\s\S]*?<\/implemented_features>/;
if (sectionRegex.test(specContent)) {
log.debug('Replacing existing implemented_features section');
return specContent.replace(sectionRegex, newSection);
}
// If section doesn't exist, try to insert after core_capabilities
const coreCapabilitiesEnd = '</core_capabilities>';
const insertIndex = specContent.indexOf(coreCapabilitiesEnd);
if (insertIndex !== -1) {
const insertPosition = insertIndex + coreCapabilitiesEnd.length;
log.debug('Inserting implemented_features after core_capabilities');
return (
specContent.slice(0, insertPosition) +
'\n\n' +
indent +
newSection +
specContent.slice(insertPosition)
);
}
// As a fallback, insert before </project_specification>
const projectSpecEnd = '</project_specification>';
const fallbackIndex = specContent.indexOf(projectSpecEnd);
if (fallbackIndex !== -1) {
log.debug('Inserting implemented_features before </project_specification>');
return (
specContent.slice(0, fallbackIndex) +
indent +
newSection +
'\n' +
specContent.slice(fallbackIndex)
);
}
log.warn?.('Could not find appropriate insertion point for implemented_features');
log.debug('Could not find appropriate insertion point for implemented_features');
return specContent;
}
/**
* Add a new feature to the implemented_features section
*
* @param specContent - The full XML content
* @param newFeature - The feature to add
* @param options - Optional extraction options
* @returns Updated XML content with the new feature added
*/
export function addImplementedFeature(
specContent: string,
newFeature: ImplementedFeature,
options: ExtractXmlOptions = {}
): string {
const log = options.logger || logger;
// Extract existing features
const existingFeatures = extractImplementedFeatures(specContent, options);
// Check for duplicates by name
const isDuplicate = existingFeatures.some(
(f) => f.name.toLowerCase() === newFeature.name.toLowerCase()
);
if (isDuplicate) {
log.debug(`Feature "${newFeature.name}" already exists, skipping`);
return specContent;
}
// Add the new feature
const updatedFeatures = [...existingFeatures, newFeature];
log.debug(`Adding feature "${newFeature.name}"`);
return updateImplementedFeaturesSection(specContent, updatedFeatures, options);
}
/**
* Remove a feature from the implemented_features section by name
*
* @param specContent - The full XML content
* @param featureName - The name of the feature to remove
* @param options - Optional extraction options
* @returns Updated XML content with the feature removed
*/
export function removeImplementedFeature(
specContent: string,
featureName: string,
options: ExtractXmlOptions = {}
): string {
const log = options.logger || logger;
// Extract existing features
const existingFeatures = extractImplementedFeatures(specContent, options);
// Filter out the feature to remove
const updatedFeatures = existingFeatures.filter(
(f) => f.name.toLowerCase() !== featureName.toLowerCase()
);
if (updatedFeatures.length === existingFeatures.length) {
log.debug(`Feature "${featureName}" not found, no changes made`);
return specContent;
}
log.debug(`Removing feature "${featureName}"`);
return updateImplementedFeaturesSection(specContent, updatedFeatures, options);
}
/**
* Update an existing feature in the implemented_features section
*
* @param specContent - The full XML content
* @param featureName - The name of the feature to update
* @param updates - Partial updates to apply to the feature
* @param options - Optional extraction options
* @returns Updated XML content with the feature modified
*/
export function updateImplementedFeature(
specContent: string,
featureName: string,
updates: Partial<ImplementedFeature>,
options: ExtractXmlOptions = {}
): string {
const log = options.logger || logger;
// Extract existing features
const existingFeatures = extractImplementedFeatures(specContent, options);
// Find and update the feature
let found = false;
const updatedFeatures = existingFeatures.map((f) => {
if (f.name.toLowerCase() === featureName.toLowerCase()) {
found = true;
return {
...f,
...updates,
// Preserve the original name if not explicitly updated
name: updates.name ?? f.name,
};
}
return f;
});
if (!found) {
log.debug(`Feature "${featureName}" not found, no changes made`);
return specContent;
}
log.debug(`Updating feature "${featureName}"`);
return updateImplementedFeaturesSection(specContent, updatedFeatures, options);
}
/**
* Check if a feature exists in the implemented_features section
*
* @param specContent - The full XML content
* @param featureName - The name of the feature to check
* @param options - Optional extraction options
* @returns True if the feature exists
*/
export function hasImplementedFeature(
specContent: string,
featureName: string,
options: ExtractXmlOptions = {}
): boolean {
const features = extractImplementedFeatures(specContent, options);
return features.some((f) => f.name.toLowerCase() === featureName.toLowerCase());
}
/**
* Convert extracted features to SpecOutput.implemented_features format
*
* @param features - Array of extracted features
* @returns Features in SpecOutput format
*/
export function toSpecOutputFeatures(
features: ImplementedFeature[]
): SpecOutput['implemented_features'] {
return features.map((f) => ({
name: f.name,
description: f.description,
...(f.file_locations && f.file_locations.length > 0
? { file_locations: f.file_locations }
: {}),
}));
}
/**
* Convert SpecOutput.implemented_features to ImplementedFeature format
*
* @param specFeatures - Features from SpecOutput
* @returns Features in ImplementedFeature format
*/
export function fromSpecOutputFeatures(
specFeatures: SpecOutput['implemented_features']
): ImplementedFeature[] {
return specFeatures.map((f) => ({
name: f.name,
description: f.description,
...(f.file_locations && f.file_locations.length > 0
? { file_locations: f.file_locations }
: {}),
}));
}
/**
* Represents a roadmap phase extracted from XML
*/
export interface RoadmapPhase {
name: string;
status: string;
description?: string;
}
/**
* Extract the technology stack from app_spec.txt XML content
*
* @param specContent - The full XML content
* @param options - Optional extraction options
* @returns Array of technology names
*/
export function extractTechnologyStack(
specContent: string,
options: ExtractXmlOptions = {}
): string[] {
const log = options.logger || logger;
const techSection = extractXmlSection(specContent, 'technology_stack', options);
if (!techSection) {
log.debug('No technology_stack section found');
return [];
}
const technologies = extractXmlElements(techSection, 'technology', options);
log.debug(`Extracted ${technologies.length} technologies`);
return technologies;
}
/**
* Update the technology_stack section in XML content
*
* @param specContent - The full XML content
* @param technologies - The new technology list
* @param options - Optional extraction options
* @returns Updated XML content
*/
export function updateTechnologyStack(
specContent: string,
technologies: string[],
options: ExtractXmlOptions = {}
): string {
const log = options.logger || logger;
const indent = ' ';
const i2 = indent.repeat(2);
// Generate new section content
const techXml = technologies
.map((t) => `${i2}<technology>${escapeXml(t)}</technology>`)
.join('\n');
const newSection = `<technology_stack>\n${techXml}\n${indent}</technology_stack>`;
// Check if section exists
const sectionRegex = /<technology_stack>[\s\S]*?<\/technology_stack>/;
if (sectionRegex.test(specContent)) {
log.debug('Replacing existing technology_stack section');
return specContent.replace(sectionRegex, newSection);
}
log.debug('No technology_stack section found to update');
return specContent;
}
/**
* Extract roadmap phases from app_spec.txt XML content
*
* @param specContent - The full XML content
* @param options - Optional extraction options
* @returns Array of roadmap phases
*/
export function extractRoadmapPhases(
specContent: string,
options: ExtractXmlOptions = {}
): RoadmapPhase[] {
const log = options.logger || logger;
const phases: RoadmapPhase[] = [];
const roadmapSection = extractXmlSection(specContent, 'implementation_roadmap', options);
if (!roadmapSection) {
log.debug('No implementation_roadmap section found');
return phases;
}
// Extract individual phase blocks
const phaseRegex = /<phase>([\s\S]*?)<\/phase>/g;
const phaseMatches = roadmapSection.matchAll(phaseRegex);
for (const phaseMatch of phaseMatches) {
const phaseContent = phaseMatch[1];
const nameMatch = phaseContent.match(/<name>([\s\S]*?)<\/name>/);
const name = nameMatch ? unescapeXml(nameMatch[1].trim()) : '';
const statusMatch = phaseContent.match(/<status>([\s\S]*?)<\/status>/);
const status = statusMatch ? unescapeXml(statusMatch[1].trim()) : 'pending';
const descMatch = phaseContent.match(/<description>([\s\S]*?)<\/description>/);
const description = descMatch ? unescapeXml(descMatch[1].trim()) : undefined;
if (name) {
phases.push({ name, status, description });
}
}
log.debug(`Extracted ${phases.length} roadmap phases`);
return phases;
}
/**
* Update a roadmap phase status in XML content
*
* @param specContent - The full XML content
* @param phaseName - The name of the phase to update
* @param newStatus - The new status value
* @param options - Optional extraction options
* @returns Updated XML content
*/
export function updateRoadmapPhaseStatus(
specContent: string,
phaseName: string,
newStatus: string,
options: ExtractXmlOptions = {}
): string {
const log = options.logger || logger;
// Find the phase and update its status
// Match the phase block containing the specific name
const phaseRegex = new RegExp(
`(<phase>\\s*<name>\\s*${escapeXml(phaseName)}\\s*<\\/name>\\s*<status>)[\\s\\S]*?(<\\/status>)`,
'i'
);
if (phaseRegex.test(specContent)) {
log.debug(`Updating phase "${phaseName}" status to "${newStatus}"`);
return specContent.replace(phaseRegex, `$1${escapeXml(newStatus)}$2`);
}
log.debug(`Phase "${phaseName}" not found`);
return specContent;
}

View File

@@ -8,12 +8,28 @@ import type { Request, Response, NextFunction } from 'express';
import { validatePath, PathNotAllowedError } from '@automaker/platform';
/**
* Creates a middleware that validates specified path parameters in req.body
* Helper to get parameter value from request (checks body first, then query)
*/
function getParamValue(req: Request, paramName: string): unknown {
// Check body first (for POST/PUT/PATCH requests)
if (req.body && req.body[paramName] !== undefined) {
return req.body[paramName];
}
// Fall back to query params (for GET requests)
if (req.query && req.query[paramName] !== undefined) {
return req.query[paramName];
}
return undefined;
}
/**
* Creates a middleware that validates specified path parameters in req.body or req.query
* @param paramNames - Names of parameters to validate (e.g., 'projectPath', 'worktreePath')
* @example
* router.post('/create', validatePathParams('projectPath'), handler);
* router.post('/delete', validatePathParams('projectPath', 'worktreePath'), handler);
* router.post('/send', validatePathParams('workingDirectory?', 'imagePaths[]'), handler);
* router.get('/logs', validatePathParams('worktreePath'), handler); // Works with query params too
*
* Special syntax:
* - 'paramName?' - Optional parameter (only validated if present)
@@ -26,8 +42,8 @@ export function validatePathParams(...paramNames: string[]) {
// Handle optional parameters (paramName?)
if (paramName.endsWith('?')) {
const actualName = paramName.slice(0, -1);
const value = req.body[actualName];
if (value) {
const value = getParamValue(req, actualName);
if (value && typeof value === 'string') {
validatePath(value);
}
continue;
@@ -36,18 +52,20 @@ export function validatePathParams(...paramNames: string[]) {
// Handle array parameters (paramName[])
if (paramName.endsWith('[]')) {
const actualName = paramName.slice(0, -2);
const values = req.body[actualName];
const values = getParamValue(req, actualName);
if (Array.isArray(values) && values.length > 0) {
for (const value of values) {
validatePath(value);
if (typeof value === 'string') {
validatePath(value);
}
}
}
continue;
}
// Handle regular parameters
const value = req.body[paramName];
if (value) {
const value = getParamValue(req, paramName);
if (value && typeof value === 'string') {
validatePath(value);
}
}

View File

@@ -10,7 +10,7 @@ import { BaseProvider } from './base-provider.js';
import { classifyError, getUserFriendlyErrorMessage, createLogger } from '@automaker/utils';
const logger = createLogger('ClaudeProvider');
import { getThinkingTokenBudget } from '@automaker/types';
import { getThinkingTokenBudget, validateBareModelId } from '@automaker/types';
import type {
ExecuteOptions,
ProviderMessage,
@@ -22,6 +22,8 @@ import type {
// Only these vars are passed - nothing else from process.env leaks through.
const ALLOWED_ENV_VARS = [
'ANTHROPIC_API_KEY',
'ANTHROPIC_BASE_URL',
'ANTHROPIC_AUTH_TOKEN',
'PATH',
'HOME',
'SHELL',
@@ -53,6 +55,10 @@ export class ClaudeProvider extends BaseProvider {
* Execute a query using Claude Agent SDK
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
// Validate that model doesn't have a provider prefix
// AgentService should strip prefixes before passing to providers
validateBareModelId(options.model, 'ClaudeProvider');
const {
prompt,
model,
@@ -70,14 +76,6 @@ export class ClaudeProvider extends BaseProvider {
const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel);
// 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'];
// AUTONOMOUS MODE: Always bypass permissions and allow unrestricted tools
// Only restrict tools when no MCP servers are configured
const shouldRestrictTools = !hasMcpServers;
const sdkOptions: Options = {
model,
systemPrompt,
@@ -85,10 +83,9 @@ export class ClaudeProvider extends BaseProvider {
cwd,
// Pass only explicitly allowed environment variables to SDK
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
// Pass through allowedTools if provided by caller (decided by sdk-options.ts)
...(allowedTools && { allowedTools }),
// AUTONOMOUS MODE: Always bypass permissions for fully autonomous operation
permissionMode: 'bypassPermissions',
allowDangerouslySkipPermissions: true,
abortController,
@@ -98,12 +95,14 @@ export class ClaudeProvider extends BaseProvider {
: {}),
// Forward settingSources for CLAUDE.md file loading
...(options.settingSources && { settingSources: options.settingSources }),
// Forward sandbox configuration
...(options.sandbox && { sandbox: options.sandbox }),
// Forward MCP servers configuration
...(options.mcpServers && { mcpServers: options.mcpServers }),
// Extended thinking configuration
...(maxThinkingTokens && { maxThinkingTokens }),
// Subagents configuration for specialized task delegation
...(options.agents && { agents: options.agents }),
// Pass through outputFormat for structured JSON outputs
...(options.outputFormat && { outputFormat: options.outputFormat }),
};
// Build prompt payload

View File

@@ -26,22 +26,23 @@
* ```
*/
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,
findCliInWsl,
isWslAvailable,
spawnJSONLProcess,
windowsToWslPath,
type SubprocessOptions,
type WslCliResult,
} from '@automaker/platform';
import { calculateReasoningTimeout } from '@automaker/types';
import { createLogger, isAbortError } from '@automaker/utils';
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { BaseProvider } from './base-provider.js';
import type { ExecuteOptions, ProviderConfig, ProviderMessage } from './types.js';
/**
* Spawn strategy for CLI tools on Windows
@@ -107,6 +108,15 @@ export interface CliDetectionResult {
// Create logger for CLI operations
const cliLogger = createLogger('CliProvider');
/**
* Base timeout for CLI operations in milliseconds.
* CLI tools have longer startup and processing times compared to direct API calls,
* so we use a higher base timeout (120s) than the default provider timeout (30s).
* This is multiplied by reasoning effort multipliers when applicable.
* @see calculateReasoningTimeout from @automaker/types
*/
const CLI_BASE_TIMEOUT_MS = 120000;
/**
* Abstract base class for CLI-based providers
*
@@ -450,6 +460,10 @@ export abstract class CliProvider extends BaseProvider {
}
}
// Calculate dynamic timeout based on reasoning effort.
// This addresses GitHub issue #530 where reasoning models with 'xhigh' effort would timeout.
const timeout = calculateReasoningTimeout(options.reasoningEffort, CLI_BASE_TIMEOUT_MS);
// WSL strategy
if (this.useWsl && this.wslCliPath) {
const wslCwd = windowsToWslPath(cwd);
@@ -473,7 +487,7 @@ export abstract class CliProvider extends BaseProvider {
cwd, // Windows cwd for spawn
env: filteredEnv,
abortController: options.abortController,
timeout: 120000, // CLI operations may take longer
timeout,
};
}
@@ -488,7 +502,7 @@ export abstract class CliProvider extends BaseProvider {
cwd,
env: filteredEnv,
abortController: options.abortController,
timeout: 120000,
timeout,
};
}
@@ -501,7 +515,7 @@ export abstract class CliProvider extends BaseProvider {
cwd,
env: filteredEnv,
abortController: options.abortController,
timeout: 120000,
timeout,
};
}
@@ -522,8 +536,13 @@ export abstract class CliProvider extends BaseProvider {
throw new Error(`${this.getCliName()} CLI not found. ${this.getInstallInstructions()}`);
}
const cliArgs = this.buildCliArgs(options);
const subprocessOptions = this.buildSubprocessOptions(options, cliArgs);
// Many CLI-based providers do not support a separate "system" message.
// If a systemPrompt is provided, embed it into the prompt so downstream models
// still receive critical formatting/schema instructions (e.g., JSON-only outputs).
const effectiveOptions = this.embedSystemPromptIntoPrompt(options);
const cliArgs = this.buildCliArgs(effectiveOptions);
const subprocessOptions = this.buildSubprocessOptions(effectiveOptions, cliArgs);
try {
for await (const rawEvent of spawnJSONLProcess(subprocessOptions)) {
@@ -555,4 +574,52 @@ export abstract class CliProvider extends BaseProvider {
throw error;
}
}
/**
* Embed system prompt text into the user prompt for CLI providers.
*
* Most CLI providers we integrate with only accept a single prompt via stdin/args.
* When upstream code supplies `options.systemPrompt`, we prepend it to the prompt
* content and clear `systemPrompt` to avoid any accidental double-injection by
* subclasses.
*/
protected embedSystemPromptIntoPrompt(options: ExecuteOptions): ExecuteOptions {
if (!options.systemPrompt) {
return options;
}
// Only string system prompts can be reliably embedded for CLI providers.
// Presets are provider-specific (e.g., Claude SDK) and cannot be represented
// universally. If a preset is provided, we only embed its optional `append`.
const systemText =
typeof options.systemPrompt === 'string'
? options.systemPrompt
: options.systemPrompt.append
? options.systemPrompt.append
: '';
if (!systemText) {
return { ...options, systemPrompt: undefined };
}
// Preserve original prompt structure.
if (typeof options.prompt === 'string') {
return {
...options,
prompt: `${systemText}\n\n---\n\n${options.prompt}`,
systemPrompt: undefined,
};
}
if (Array.isArray(options.prompt)) {
return {
...options,
prompt: [{ type: 'text', text: systemText }, ...options.prompt],
systemPrompt: undefined,
};
}
// Should be unreachable due to ExecuteOptions typing, but keep safe.
return { ...options, systemPrompt: undefined };
}
}

View File

@@ -0,0 +1,85 @@
/**
* Codex Config Manager - Writes MCP server configuration for Codex CLI
*/
import path from 'path';
import type { McpServerConfig } from '@automaker/types';
import * as secureFs from '../lib/secure-fs.js';
const CODEX_CONFIG_DIR = '.codex';
const CODEX_CONFIG_FILENAME = 'config.toml';
const CODEX_MCP_SECTION = 'mcp_servers';
function formatTomlString(value: string): string {
return JSON.stringify(value);
}
function formatTomlArray(values: string[]): string {
const formatted = values.map((value) => formatTomlString(value)).join(', ');
return `[${formatted}]`;
}
function formatTomlInlineTable(values: Record<string, string>): string {
const entries = Object.entries(values).map(
([key, value]) => `${key} = ${formatTomlString(value)}`
);
return `{ ${entries.join(', ')} }`;
}
function formatTomlKey(key: string): string {
return `"${key.replace(/"/g, '\\"')}"`;
}
function buildServerBlock(name: string, server: McpServerConfig): string[] {
const lines: string[] = [];
const section = `${CODEX_MCP_SECTION}.${formatTomlKey(name)}`;
lines.push(`[${section}]`);
if (server.type) {
lines.push(`type = ${formatTomlString(server.type)}`);
}
if ('command' in server && server.command) {
lines.push(`command = ${formatTomlString(server.command)}`);
}
if ('args' in server && server.args && server.args.length > 0) {
lines.push(`args = ${formatTomlArray(server.args)}`);
}
if ('env' in server && server.env && Object.keys(server.env).length > 0) {
lines.push(`env = ${formatTomlInlineTable(server.env)}`);
}
if ('url' in server && server.url) {
lines.push(`url = ${formatTomlString(server.url)}`);
}
if ('headers' in server && server.headers && Object.keys(server.headers).length > 0) {
lines.push(`headers = ${formatTomlInlineTable(server.headers)}`);
}
return lines;
}
export class CodexConfigManager {
async configureMcpServers(
cwd: string,
mcpServers: Record<string, McpServerConfig>
): Promise<void> {
const configDir = path.join(cwd, CODEX_CONFIG_DIR);
const configPath = path.join(configDir, CODEX_CONFIG_FILENAME);
await secureFs.mkdir(configDir, { recursive: true });
const blocks: string[] = [];
for (const [name, server] of Object.entries(mcpServers)) {
blocks.push(...buildServerBlock(name, server), '');
}
const content = blocks.join('\n').trim();
if (content) {
await secureFs.writeFile(configPath, content + '\n', 'utf-8');
}
}
}

View File

@@ -0,0 +1,111 @@
/**
* Codex Model Definitions
*
* Official Codex CLI models as documented at https://developers.openai.com/codex/models/
*/
import { CODEX_MODEL_MAP } from '@automaker/types';
import type { ModelDefinition } from './types.js';
const CONTEXT_WINDOW_256K = 256000;
const CONTEXT_WINDOW_128K = 128000;
const MAX_OUTPUT_32K = 32000;
const MAX_OUTPUT_16K = 16000;
/**
* All available Codex models with their specifications
* Based on https://developers.openai.com/codex/models/
*/
export const CODEX_MODELS: ModelDefinition[] = [
// ========== Recommended Codex Models ==========
{
id: CODEX_MODEL_MAP.gpt52Codex,
name: 'GPT-5.2-Codex',
modelString: CODEX_MODEL_MAP.gpt52Codex,
provider: 'openai',
description:
'Most advanced agentic coding model for complex software engineering (default for ChatGPT users).',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'premium' as const,
default: true,
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt51CodexMax,
name: 'GPT-5.1-Codex-Max',
modelString: CODEX_MODEL_MAP.gpt51CodexMax,
provider: 'openai',
description: 'Optimized for long-horizon, agentic coding tasks in Codex.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'premium' as const,
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt51CodexMini,
name: 'GPT-5.1-Codex-Mini',
modelString: CODEX_MODEL_MAP.gpt51CodexMini,
provider: 'openai',
description: 'Smaller, more cost-effective version for faster workflows.',
contextWindow: CONTEXT_WINDOW_128K,
maxOutputTokens: MAX_OUTPUT_16K,
supportsVision: true,
supportsTools: true,
tier: 'basic' as const,
hasReasoning: false,
},
// ========== General-Purpose GPT Models ==========
{
id: CODEX_MODEL_MAP.gpt52,
name: 'GPT-5.2',
modelString: CODEX_MODEL_MAP.gpt52,
provider: 'openai',
description: 'Best general agentic model for tasks across industries and domains.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'standard' as const,
hasReasoning: true,
},
{
id: CODEX_MODEL_MAP.gpt51,
name: 'GPT-5.1',
modelString: CODEX_MODEL_MAP.gpt51,
provider: 'openai',
description: 'Great for coding and agentic tasks across domains.',
contextWindow: CONTEXT_WINDOW_256K,
maxOutputTokens: MAX_OUTPUT_32K,
supportsVision: true,
supportsTools: true,
tier: 'standard' as const,
hasReasoning: true,
},
];
/**
* Get model definition by ID
*/
export function getCodexModelById(modelId: string): ModelDefinition | undefined {
return CODEX_MODELS.find((m) => m.id === modelId || m.modelString === modelId);
}
/**
* Get all models that support reasoning
*/
export function getReasoningModels(): ModelDefinition[] {
return CODEX_MODELS.filter((m) => m.hasReasoning);
}
/**
* Get models by tier
*/
export function getModelsByTier(tier: 'premium' | 'standard' | 'basic'): ModelDefinition[] {
return CODEX_MODELS.filter((m) => m.tier === tier);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,173 @@
/**
* Codex SDK client - Executes Codex queries via official @openai/codex-sdk
*
* Used for programmatic control of Codex from within the application.
* Provides cleaner integration than spawning CLI processes.
*/
import { Codex } from '@openai/codex-sdk';
import { formatHistoryAsText, classifyError, getUserFriendlyErrorMessage } from '@automaker/utils';
import { supportsReasoningEffort } from '@automaker/types';
import type { ExecuteOptions, ProviderMessage } from './types.js';
const OPENAI_API_KEY_ENV = 'OPENAI_API_KEY';
const SDK_HISTORY_HEADER = 'Current request:\n';
const DEFAULT_RESPONSE_TEXT = '';
const SDK_ERROR_DETAILS_LABEL = 'Details:';
type PromptBlock = {
type: string;
text?: string;
source?: {
type?: string;
media_type?: string;
data?: string;
};
};
function resolveApiKey(): string {
const apiKey = process.env[OPENAI_API_KEY_ENV];
if (!apiKey) {
throw new Error('OPENAI_API_KEY is not set.');
}
return apiKey;
}
function normalizePromptBlocks(prompt: ExecuteOptions['prompt']): PromptBlock[] {
if (Array.isArray(prompt)) {
return prompt as PromptBlock[];
}
return [{ type: 'text', text: prompt }];
}
function buildPromptText(options: ExecuteOptions, systemPrompt: string | null): string {
const historyText =
options.conversationHistory && options.conversationHistory.length > 0
? formatHistoryAsText(options.conversationHistory)
: '';
const promptBlocks = normalizePromptBlocks(options.prompt);
const promptTexts: string[] = [];
for (const block of promptBlocks) {
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
promptTexts.push(block.text);
}
}
const promptContent = promptTexts.join('\n\n');
if (!promptContent.trim()) {
throw new Error('Codex SDK prompt is empty.');
}
const parts: string[] = [];
if (systemPrompt) {
parts.push(`System: ${systemPrompt}`);
}
if (historyText) {
parts.push(historyText);
}
parts.push(`${SDK_HISTORY_HEADER}${promptContent}`);
return parts.join('\n\n');
}
function buildSdkErrorMessage(rawMessage: string, userMessage: string): string {
if (!rawMessage) {
return userMessage;
}
if (!userMessage || rawMessage === userMessage) {
return rawMessage;
}
return `${userMessage}\n\n${SDK_ERROR_DETAILS_LABEL} ${rawMessage}`;
}
/**
* Execute a query using the official Codex SDK
*
* The SDK provides a cleaner interface than spawning CLI processes:
* - Handles authentication automatically
* - Provides TypeScript types
* - Supports thread management and resumption
* - Better error handling
*/
export async function* executeCodexSdkQuery(
options: ExecuteOptions,
systemPrompt: string | null
): AsyncGenerator<ProviderMessage> {
try {
const apiKey = resolveApiKey();
const codex = new Codex({ apiKey });
// Resume existing thread or start new one
let thread;
if (options.sdkSessionId) {
try {
thread = codex.resumeThread(options.sdkSessionId);
} catch {
// If resume fails, start a new thread
thread = codex.startThread();
}
} else {
thread = codex.startThread();
}
const promptText = buildPromptText(options, systemPrompt);
// Build run options with reasoning effort if supported
const runOptions: {
signal?: AbortSignal;
reasoning?: { effort: string };
} = {
signal: options.abortController?.signal,
};
// Add reasoning effort if model supports it and reasoningEffort is specified
if (
options.reasoningEffort &&
supportsReasoningEffort(options.model) &&
options.reasoningEffort !== 'none'
) {
runOptions.reasoning = { effort: options.reasoningEffort };
}
// Run the query
const result = await thread.run(promptText, runOptions);
// Extract response text (from finalResponse property)
const outputText = result.finalResponse ?? DEFAULT_RESPONSE_TEXT;
// Get thread ID (may be null if not populated yet)
const threadId = thread.id ?? undefined;
// Yield assistant message
yield {
type: 'assistant',
session_id: threadId,
message: {
role: 'assistant',
content: [{ type: 'text', text: outputText }],
},
};
// Yield result
yield {
type: 'result',
subtype: 'success',
session_id: threadId,
result: outputText,
};
} catch (error) {
const errorInfo = classifyError(error);
const userMessage = getUserFriendlyErrorMessage(error);
const combinedMessage = buildSdkErrorMessage(errorInfo.message, userMessage);
console.error('[CodexSDK] executeQuery() error during execution:', {
type: errorInfo.type,
message: errorInfo.message,
isRateLimit: errorInfo.isRateLimit,
retryAfter: errorInfo.retryAfter,
stack: error instanceof Error ? error.stack : undefined,
});
yield { type: 'error', error: combinedMessage };
}
}

View File

@@ -0,0 +1,436 @@
export type CodexToolResolution = {
name: string;
input: Record<string, unknown>;
};
export type CodexTodoItem = {
content: string;
status: 'pending' | 'in_progress' | 'completed';
activeForm?: string;
};
const TOOL_NAME_BASH = 'Bash';
const TOOL_NAME_READ = 'Read';
const TOOL_NAME_EDIT = 'Edit';
const TOOL_NAME_WRITE = 'Write';
const TOOL_NAME_GREP = 'Grep';
const TOOL_NAME_GLOB = 'Glob';
const TOOL_NAME_TODO = 'TodoWrite';
const TOOL_NAME_DELETE = 'Delete';
const TOOL_NAME_LS = 'Ls';
const INPUT_KEY_COMMAND = 'command';
const INPUT_KEY_FILE_PATH = 'file_path';
const INPUT_KEY_PATTERN = 'pattern';
const SHELL_WRAPPER_PATTERNS = [
/^\/bin\/bash\s+-lc\s+["']([\s\S]+)["']$/,
/^bash\s+-lc\s+["']([\s\S]+)["']$/,
/^\/bin\/sh\s+-lc\s+["']([\s\S]+)["']$/,
/^sh\s+-lc\s+["']([\s\S]+)["']$/,
/^cmd\.exe\s+\/c\s+["']?([\s\S]+)["']?$/i,
/^powershell(?:\.exe)?\s+-Command\s+["']?([\s\S]+)["']?$/i,
/^pwsh(?:\.exe)?\s+-Command\s+["']?([\s\S]+)["']?$/i,
] as const;
const COMMAND_SEPARATOR_PATTERN = /\s*(?:&&|\|\||;)\s*/;
const SEGMENT_SKIP_PREFIXES = ['cd ', 'export ', 'set ', 'pushd '] as const;
const WRAPPER_COMMANDS = new Set(['sudo', 'env', 'command']);
const READ_COMMANDS = new Set(['cat', 'sed', 'head', 'tail', 'less', 'more', 'bat', 'stat', 'wc']);
const SEARCH_COMMANDS = new Set(['rg', 'grep', 'ag', 'ack']);
const GLOB_COMMANDS = new Set(['ls', 'find', 'fd', 'tree']);
const DELETE_COMMANDS = new Set(['rm', 'del', 'erase', 'remove', 'unlink']);
const LIST_COMMANDS = new Set(['ls', 'dir', 'll', 'la']);
const WRITE_COMMANDS = new Set(['tee', 'touch', 'mkdir']);
const APPLY_PATCH_COMMAND = 'apply_patch';
const APPLY_PATCH_PATTERN = /\bapply_patch\b/;
const REDIRECTION_TARGET_PATTERN = /(?:>>|>)\s*([^\s]+)/;
const SED_IN_PLACE_FLAGS = new Set(['-i', '--in-place']);
const PERL_IN_PLACE_FLAG = /-.*i/;
const SEARCH_PATTERN_FLAGS = new Set(['-e', '--regexp']);
const SEARCH_VALUE_FLAGS = new Set([
'-g',
'--glob',
'--iglob',
'--type',
'--type-add',
'--type-clear',
'--encoding',
]);
const SEARCH_FILE_LIST_FLAGS = new Set(['--files']);
const TODO_LINE_PATTERN = /^[-*]\s*(?:\[(?<status>[ x~])\]\s*)?(?<content>.+)$/;
const TODO_STATUS_COMPLETED = 'completed';
const TODO_STATUS_IN_PROGRESS = 'in_progress';
const TODO_STATUS_PENDING = 'pending';
const PATCH_FILE_MARKERS = [
'*** Update File: ',
'*** Add File: ',
'*** Delete File: ',
'*** Move to: ',
] as const;
function stripShellWrapper(command: string): string {
const trimmed = command.trim();
for (const pattern of SHELL_WRAPPER_PATTERNS) {
const match = trimmed.match(pattern);
if (match && match[1]) {
return unescapeCommand(match[1].trim());
}
}
return trimmed;
}
function unescapeCommand(command: string): string {
return command.replace(/\\(["'])/g, '$1');
}
function extractPrimarySegment(command: string): string {
const segments = command
.split(COMMAND_SEPARATOR_PATTERN)
.map((segment) => segment.trim())
.filter(Boolean);
for (const segment of segments) {
const shouldSkip = SEGMENT_SKIP_PREFIXES.some((prefix) => segment.startsWith(prefix));
if (!shouldSkip) {
return segment;
}
}
return command.trim();
}
function tokenizeCommand(command: string): string[] {
const tokens: string[] = [];
let current = '';
let inSingleQuote = false;
let inDoubleQuote = false;
let isEscaped = false;
for (const char of command) {
if (isEscaped) {
current += char;
isEscaped = false;
continue;
}
if (char === '\\') {
isEscaped = true;
continue;
}
if (char === "'" && !inDoubleQuote) {
inSingleQuote = !inSingleQuote;
continue;
}
if (char === '"' && !inSingleQuote) {
inDoubleQuote = !inDoubleQuote;
continue;
}
if (!inSingleQuote && !inDoubleQuote && /\s/.test(char)) {
if (current) {
tokens.push(current);
current = '';
}
continue;
}
current += char;
}
if (current) {
tokens.push(current);
}
return tokens;
}
function stripWrapperTokens(tokens: string[]): string[] {
let index = 0;
while (index < tokens.length && WRAPPER_COMMANDS.has(tokens[index].toLowerCase())) {
index += 1;
}
return tokens.slice(index);
}
function extractFilePathFromTokens(tokens: string[]): string | null {
const candidates = tokens.slice(1).filter((token) => token && !token.startsWith('-'));
if (candidates.length === 0) return null;
return candidates[candidates.length - 1];
}
function extractSearchPattern(tokens: string[]): string | null {
const remaining = tokens.slice(1);
for (let index = 0; index < remaining.length; index += 1) {
const token = remaining[index];
if (token === '--') {
return remaining[index + 1] ?? null;
}
if (SEARCH_PATTERN_FLAGS.has(token)) {
return remaining[index + 1] ?? null;
}
if (SEARCH_VALUE_FLAGS.has(token)) {
index += 1;
continue;
}
if (token.startsWith('-')) {
continue;
}
return token;
}
return null;
}
function extractTeeTarget(tokens: string[]): string | null {
const teeIndex = tokens.findIndex((token) => token === 'tee');
if (teeIndex < 0) return null;
const candidate = tokens[teeIndex + 1];
return candidate && !candidate.startsWith('-') ? candidate : null;
}
function extractRedirectionTarget(command: string): string | null {
const match = command.match(REDIRECTION_TARGET_PATTERN);
return match?.[1] ?? null;
}
function extractFilePathFromDeleteTokens(tokens: string[]): string | null {
// rm file.txt or rm /path/to/file.txt
// Skip flags and get the first non-flag argument
for (let i = 1; i < tokens.length; i++) {
const token = tokens[i];
if (token && !token.startsWith('-')) {
return token;
}
}
return null;
}
function hasSedInPlaceFlag(tokens: string[]): boolean {
return tokens.some((token) => SED_IN_PLACE_FLAGS.has(token) || token.startsWith('-i'));
}
function hasPerlInPlaceFlag(tokens: string[]): boolean {
return tokens.some((token) => PERL_IN_PLACE_FLAG.test(token));
}
function extractPatchFilePath(command: string): string | null {
for (const marker of PATCH_FILE_MARKERS) {
const index = command.indexOf(marker);
if (index < 0) continue;
const start = index + marker.length;
const end = command.indexOf('\n', start);
const rawPath = (end === -1 ? command.slice(start) : command.slice(start, end)).trim();
if (rawPath) return rawPath;
}
return null;
}
function buildInputWithFilePath(filePath: string | null): Record<string, unknown> {
return filePath ? { [INPUT_KEY_FILE_PATH]: filePath } : {};
}
function buildInputWithPattern(pattern: string | null): Record<string, unknown> {
return pattern ? { [INPUT_KEY_PATTERN]: pattern } : {};
}
export function resolveCodexToolCall(command: string): CodexToolResolution {
const normalized = stripShellWrapper(command);
const primarySegment = extractPrimarySegment(normalized);
const tokens = stripWrapperTokens(tokenizeCommand(primarySegment));
const commandToken = tokens[0]?.toLowerCase() ?? '';
const redirectionTarget = extractRedirectionTarget(primarySegment);
if (redirectionTarget) {
return {
name: TOOL_NAME_WRITE,
input: buildInputWithFilePath(redirectionTarget),
};
}
if (commandToken === APPLY_PATCH_COMMAND || APPLY_PATCH_PATTERN.test(primarySegment)) {
return {
name: TOOL_NAME_EDIT,
input: buildInputWithFilePath(extractPatchFilePath(primarySegment)),
};
}
if (commandToken === 'sed' && hasSedInPlaceFlag(tokens)) {
return {
name: TOOL_NAME_EDIT,
input: buildInputWithFilePath(extractFilePathFromTokens(tokens)),
};
}
if (commandToken === 'perl' && hasPerlInPlaceFlag(tokens)) {
return {
name: TOOL_NAME_EDIT,
input: buildInputWithFilePath(extractFilePathFromTokens(tokens)),
};
}
if (WRITE_COMMANDS.has(commandToken)) {
const filePath =
commandToken === 'tee' ? extractTeeTarget(tokens) : extractFilePathFromTokens(tokens);
return {
name: TOOL_NAME_WRITE,
input: buildInputWithFilePath(filePath),
};
}
if (SEARCH_COMMANDS.has(commandToken)) {
if (tokens.some((token) => SEARCH_FILE_LIST_FLAGS.has(token))) {
return {
name: TOOL_NAME_GLOB,
input: buildInputWithPattern(extractFilePathFromTokens(tokens)),
};
}
return {
name: TOOL_NAME_GREP,
input: buildInputWithPattern(extractSearchPattern(tokens)),
};
}
// Handle Delete commands (rm, del, erase, remove, unlink)
if (DELETE_COMMANDS.has(commandToken)) {
// Skip if -r or -rf flags (recursive delete should go to Bash)
if (
tokens.some((token) => token === '-r' || token === '-rf' || token === '-f' || token === '-rf')
) {
return {
name: TOOL_NAME_BASH,
input: { [INPUT_KEY_COMMAND]: normalized },
};
}
// Simple file deletion - extract the file path
const filePath = extractFilePathFromDeleteTokens(tokens);
if (filePath) {
return {
name: TOOL_NAME_DELETE,
input: { path: filePath },
};
}
// Fall back to bash if we can't determine the file path
return {
name: TOOL_NAME_BASH,
input: { [INPUT_KEY_COMMAND]: normalized },
};
}
// Handle simple Ls commands (just listing, not find/glob)
if (LIST_COMMANDS.has(commandToken)) {
const filePath = extractFilePathFromTokens(tokens);
return {
name: TOOL_NAME_LS,
input: { path: filePath || '.' },
};
}
if (GLOB_COMMANDS.has(commandToken)) {
return {
name: TOOL_NAME_GLOB,
input: buildInputWithPattern(extractFilePathFromTokens(tokens)),
};
}
if (READ_COMMANDS.has(commandToken)) {
return {
name: TOOL_NAME_READ,
input: buildInputWithFilePath(extractFilePathFromTokens(tokens)),
};
}
return {
name: TOOL_NAME_BASH,
input: { [INPUT_KEY_COMMAND]: normalized },
};
}
function parseTodoLines(lines: string[]): CodexTodoItem[] {
const todos: CodexTodoItem[] = [];
for (const line of lines) {
const match = line.match(TODO_LINE_PATTERN);
if (!match?.groups?.content) continue;
const statusToken = match.groups.status;
const status =
statusToken === 'x'
? TODO_STATUS_COMPLETED
: statusToken === '~'
? TODO_STATUS_IN_PROGRESS
: TODO_STATUS_PENDING;
todos.push({ content: match.groups.content.trim(), status });
}
return todos;
}
function extractTodoFromArray(value: unknown[]): CodexTodoItem[] {
return value
.map((entry) => {
if (typeof entry === 'string') {
return { content: entry, status: TODO_STATUS_PENDING };
}
if (entry && typeof entry === 'object') {
const record = entry as Record<string, unknown>;
const content =
typeof record.content === 'string'
? record.content
: typeof record.text === 'string'
? record.text
: typeof record.title === 'string'
? record.title
: null;
if (!content) return null;
const status =
record.status === TODO_STATUS_COMPLETED ||
record.status === TODO_STATUS_IN_PROGRESS ||
record.status === TODO_STATUS_PENDING
? (record.status as CodexTodoItem['status'])
: TODO_STATUS_PENDING;
const activeForm = typeof record.activeForm === 'string' ? record.activeForm : undefined;
return { content, status, activeForm };
}
return null;
})
.filter((item): item is CodexTodoItem => Boolean(item));
}
export function extractCodexTodoItems(item: Record<string, unknown>): CodexTodoItem[] | null {
const todosValue = item.todos;
if (Array.isArray(todosValue)) {
const todos = extractTodoFromArray(todosValue);
return todos.length > 0 ? todos : null;
}
const itemsValue = item.items;
if (Array.isArray(itemsValue)) {
const todos = extractTodoFromArray(itemsValue);
return todos.length > 0 ? todos : null;
}
const textValue =
typeof item.text === 'string'
? item.text
: typeof item.content === 'string'
? item.content
: null;
if (!textValue) return null;
const lines = textValue
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
const todos = parseTodoLines(lines);
return todos.length > 0 ? todos : null;
}
export function getCodexTodoToolName(): string {
return TOOL_NAME_TODO;
}

View File

@@ -28,7 +28,9 @@ import type {
ModelDefinition,
ContentBlock,
} from './types.js';
import { stripProviderPrefix } from '@automaker/types';
import { validateBareModelId } from '@automaker/types';
import { validateApiKey } from '../lib/auth-utils.js';
import { getEffectivePermissions } from '../services/cursor-config-service.js';
import {
type CursorStreamEvent,
type CursorSystemEvent,
@@ -315,18 +317,25 @@ export class CursorProvider extends CliProvider {
}
buildCliArgs(options: ExecuteOptions): string[] {
// Extract model (strip 'cursor-' prefix if present)
const model = stripProviderPrefix(options.model || 'auto');
// Model is already bare (no prefix) - validated by executeQuery
const model = 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[] = [
const cliArgs: string[] = [];
// If using Cursor IDE (cliPath is 'cursor' not 'cursor-agent'), add 'agent' subcommand
if (this.cliPath && !this.cliPath.includes('cursor-agent')) {
cliArgs.push('agent');
}
cliArgs.push(
'-p', // Print mode (non-interactive)
'--output-format',
'stream-json',
'--stream-partial-output', // Real-time streaming
];
'--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
@@ -472,7 +481,9 @@ export class CursorProvider extends CliProvider {
// ==========================================================================
/**
* Override CLI detection to add Cursor-specific versions directory check
* Override CLI detection to add Cursor-specific checks:
* 1. Versions directory for cursor-agent installations
* 2. Cursor IDE with 'cursor agent' subcommand support
*/
protected detectCli(): CliDetectionResult {
// First try standard detection (PATH, common paths, WSL)
@@ -507,6 +518,39 @@ export class CursorProvider extends CliProvider {
}
}
// If cursor-agent not found, try to find 'cursor' IDE and use 'cursor agent' subcommand
// The Cursor IDE includes the agent as a subcommand: cursor agent
if (process.platform !== 'win32') {
const cursorPaths = [
'/usr/bin/cursor',
'/usr/local/bin/cursor',
path.join(os.homedir(), '.local/bin/cursor'),
'/opt/cursor/cursor',
];
for (const cursorPath of cursorPaths) {
if (fs.existsSync(cursorPath)) {
// Verify cursor agent subcommand works
try {
execSync(`"${cursorPath}" agent --version`, {
encoding: 'utf8',
timeout: 5000,
stdio: 'pipe',
});
logger.debug(`Using cursor agent via Cursor IDE: ${cursorPath}`);
// Return cursor path but we'll use 'cursor agent' subcommand
return {
cliPath: cursorPath,
useWsl: false,
strategy: 'native',
};
} catch {
// cursor agent subcommand doesn't work, try next path
}
}
}
}
return result;
}
@@ -605,6 +649,10 @@ export class CursorProvider extends CliProvider {
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
this.ensureCliDetected();
// Validate that model doesn't have a provider prefix
// AgentService should strip prefixes before passing to providers
validateBareModelId(options.model, 'CursorProvider');
if (!this.cliPath) {
throw this.createError(
CursorErrorCode.NOT_INSTALLED,
@@ -642,6 +690,9 @@ export class CursorProvider extends CliProvider {
logger.debug(`CursorProvider.executeQuery called with model: "${options.model}"`);
// Get effective permissions for this project
const effectivePermissions = await getEffectivePermissions(options.cwd || process.cwd());
// Debug: log raw events when AUTOMAKER_DEBUG_RAW_OUTPUT is enabled
const debugRawEvents =
process.env.AUTOMAKER_DEBUG_RAW_OUTPUT === 'true' ||
@@ -838,9 +889,16 @@ export class CursorProvider extends CliProvider {
});
return result;
}
const result = execSync(`"${this.cliPath}" --version`, {
// If using Cursor IDE, use 'cursor agent --version'
const versionCmd = this.cliPath.includes('cursor-agent')
? `"${this.cliPath}" --version`
: `"${this.cliPath}" agent --version`;
const result = execSync(versionCmd, {
encoding: 'utf8',
timeout: 5000,
stdio: 'pipe',
}).trim();
return result;
} catch {
@@ -857,8 +915,13 @@ export class CursorProvider extends CliProvider {
return { authenticated: false, method: 'none' };
}
// Check for API key in environment
// Check for API key in environment with validation
if (process.env.CURSOR_API_KEY) {
const validation = validateApiKey(process.env.CURSOR_API_KEY, 'cursor');
if (!validation.isValid) {
logger.warn('Cursor API key validation failed:', validation.error);
return { authenticated: false, method: 'api_key', error: validation.error };
}
return { authenticated: true, method: 'api_key' };
}

View File

@@ -25,5 +25,16 @@ export { ClaudeProvider } from './claude-provider.js';
export { CursorProvider, CursorErrorCode, CursorError } from './cursor-provider.js';
export { CursorConfigManager } from './cursor-config-manager.js';
// OpenCode provider
export { OpencodeProvider } from './opencode-provider.js';
// Provider factory
export { ProviderFactory } from './provider-factory.js';
// Simple query service - unified interface for basic AI queries
export { simpleQuery, streamingQuery } from './simple-query-service.js';
export type {
SimpleQueryOptions,
SimpleQueryResult,
StreamingQueryOptions,
} from './simple-query-service.js';

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,27 @@
import { BaseProvider } from './base-provider.js';
import type { InstallationStatus, ModelDefinition } from './types.js';
import { isCursorModel, type ModelProvider } from '@automaker/types';
import { isCursorModel, isCodexModel, isOpencodeModel, type ModelProvider } from '@automaker/types';
import * as fs from 'fs';
import * as path from 'path';
const DISCONNECTED_MARKERS: Record<string, string> = {
claude: '.claude-disconnected',
codex: '.codex-disconnected',
cursor: '.cursor-disconnected',
opencode: '.opencode-disconnected',
};
/**
* Check if a provider CLI is disconnected from the app
*/
export function isProviderDisconnected(providerName: string): boolean {
const markerFile = DISCONNECTED_MARKERS[providerName.toLowerCase()];
if (!markerFile) return false;
const markerPath = path.join(process.cwd(), '.automaker', markerFile);
return fs.existsSync(markerPath);
}
/**
* Provider registration entry
@@ -75,10 +95,26 @@ export class ProviderFactory {
* Get the appropriate provider for a given model ID
*
* @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto")
* @param options Optional settings
* @param options.throwOnDisconnected Throw error if provider is disconnected (default: true)
* @returns Provider instance for the model
* @throws Error if provider is disconnected and throwOnDisconnected is true
*/
static getProviderForModel(modelId: string): BaseProvider {
const providerName = this.getProviderNameForModel(modelId);
static getProviderForModel(
modelId: string,
options: { throwOnDisconnected?: boolean } = {}
): BaseProvider {
const { throwOnDisconnected = true } = options;
const providerName = this.getProviderForModelName(modelId);
// Check if provider is disconnected
if (throwOnDisconnected && isProviderDisconnected(providerName)) {
throw new Error(
`${providerName.charAt(0).toUpperCase() + providerName.slice(1)} CLI is disconnected from the app. ` +
`Please go to Settings > Providers and click "Sign In" to reconnect.`
);
}
const provider = this.getProviderByName(providerName);
if (!provider) {
@@ -93,6 +129,35 @@ export class ProviderFactory {
return provider;
}
/**
* Get the provider name for a given model ID (without creating provider instance)
*/
static getProviderForModelName(modelId: string): string {
const lowerModel = modelId.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;
}
}
// Fallback: Check for explicit prefixes
for (const [name] of registrations) {
if (lowerModel.startsWith(`${name}-`)) {
return name;
}
}
// Default to claude (first registered provider or claude)
return 'claude';
}
/**
* Get all available providers
*/
@@ -156,6 +221,41 @@ export class ProviderFactory {
static getRegisteredProviderNames(): string[] {
return Array.from(providerRegistry.keys());
}
/**
* Check if a specific model supports vision/image input
*
* @param modelId Model identifier
* @returns Whether the model supports vision (defaults to true if model not found)
*/
static modelSupportsVision(modelId: string): boolean {
const provider = this.getProviderForModel(modelId);
const models = provider.getAvailableModels();
// Find the model in the available models list
for (const model of models) {
if (
model.id === modelId ||
model.modelString === modelId ||
model.id.endsWith(`-${modelId}`) ||
model.modelString.endsWith(`-${modelId}`) ||
model.modelString === modelId.replace(/^(claude|cursor|codex)-/, '') ||
model.modelString === modelId.replace(/-(claude|cursor|codex)$/, '')
) {
return model.supportsVision ?? true;
}
}
// Also try exact match with model string from provider's model map
for (const model of models) {
if (model.modelString === modelId || model.id === modelId) {
return model.supportsVision ?? true;
}
}
// Default to true (Claude SDK supports vision by default)
return true;
}
}
// =============================================================================
@@ -165,6 +265,8 @@ export class ProviderFactory {
// Import providers for registration side-effects
import { ClaudeProvider } from './claude-provider.js';
import { CursorProvider } from './cursor-provider.js';
import { CodexProvider } from './codex-provider.js';
import { OpencodeProvider } from './opencode-provider.js';
// Register Claude provider
registerProvider('claude', {
@@ -184,3 +286,18 @@ registerProvider('cursor', {
canHandleModel: (model: string) => isCursorModel(model),
priority: 10, // Higher priority - check Cursor models first
});
// Register Codex provider
registerProvider('codex', {
factory: () => new CodexProvider(),
aliases: ['openai'],
canHandleModel: (model: string) => isCodexModel(model),
priority: 5, // Medium priority - check after Cursor but before Claude
});
// Register OpenCode provider
registerProvider('opencode', {
factory: () => new OpencodeProvider(),
canHandleModel: (model: string) => isOpencodeModel(model),
priority: 3, // Between codex (5) and claude (0)
});

View File

@@ -0,0 +1,254 @@
/**
* Simple Query Service - Simplified interface for basic AI queries
*
* Use this for routes that need simple text responses without
* complex event handling. This service abstracts away the provider
* selection and streaming details, providing a clean interface
* for common query patterns.
*
* Benefits:
* - No direct SDK imports needed in route files
* - Consistent provider routing based on model
* - Automatic text extraction from streaming responses
* - Structured output support for JSON schema responses
* - Eliminates duplicate extractTextFromStream() functions
*/
import { ProviderFactory } from './provider-factory.js';
import type {
ProviderMessage,
ContentBlock,
ThinkingLevel,
ReasoningEffort,
} from '@automaker/types';
import { stripProviderPrefix } from '@automaker/types';
/**
* Options for simple query execution
*/
export interface SimpleQueryOptions {
/** The prompt to send to the AI (can be text or multi-part content) */
prompt: string | Array<{ type: string; text?: string; source?: object }>;
/** Model to use (with or without provider prefix) */
model?: string;
/** Working directory for the query */
cwd: string;
/** System prompt (combined with user prompt for some providers) */
systemPrompt?: string;
/** Maximum turns for agentic operations (default: 1) */
maxTurns?: number;
/** Tools to allow (default: [] for simple queries) */
allowedTools?: string[];
/** Abort controller for cancellation */
abortController?: AbortController;
/** Structured output format for JSON responses */
outputFormat?: {
type: 'json_schema';
schema: Record<string, unknown>;
};
/** Thinking level for Claude models */
thinkingLevel?: ThinkingLevel;
/** Reasoning effort for Codex/OpenAI models */
reasoningEffort?: ReasoningEffort;
/** If true, runs in read-only mode (no file writes) */
readOnly?: boolean;
/** Setting sources for CLAUDE.md loading */
settingSources?: Array<'user' | 'project' | 'local'>;
}
/**
* Result from a simple query
*/
export interface SimpleQueryResult {
/** The accumulated text response */
text: string;
/** Structured output if outputFormat was specified and provider supports it */
structured_output?: Record<string, unknown>;
}
/**
* Options for streaming query execution
*/
export interface StreamingQueryOptions extends SimpleQueryOptions {
/** Callback for each text chunk received */
onText?: (text: string) => void;
/** Callback for tool use events */
onToolUse?: (tool: string, input: unknown) => void;
/** Callback for thinking blocks (if available) */
onThinking?: (thinking: string) => void;
}
/**
* Default model to use when none specified
*/
const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
/**
* Execute a simple query and return the text result
*
* Use this for simple, non-streaming queries where you just need
* the final text response. For more complex use cases with progress
* callbacks, use streamingQuery() instead.
*
* @example
* ```typescript
* const result = await simpleQuery({
* prompt: 'Generate a title for: user authentication',
* cwd: process.cwd(),
* systemPrompt: 'You are a title generator...',
* maxTurns: 1,
* allowedTools: [],
* });
* console.log(result.text); // "Add user authentication"
* ```
*/
export async function simpleQuery(options: SimpleQueryOptions): Promise<SimpleQueryResult> {
const model = options.model || DEFAULT_MODEL;
const provider = ProviderFactory.getProviderForModel(model);
const bareModel = stripProviderPrefix(model);
let responseText = '';
let structuredOutput: Record<string, unknown> | undefined;
// Build provider options
const providerOptions = {
prompt: options.prompt,
model: bareModel,
originalModel: model,
cwd: options.cwd,
systemPrompt: options.systemPrompt,
maxTurns: options.maxTurns ?? 1,
allowedTools: options.allowedTools ?? [],
abortController: options.abortController,
outputFormat: options.outputFormat,
thinkingLevel: options.thinkingLevel,
reasoningEffort: options.reasoningEffort,
readOnly: options.readOnly,
settingSources: options.settingSources,
};
for await (const msg of provider.executeQuery(providerOptions)) {
// Handle error messages
if (msg.type === 'error') {
const errorMessage = msg.error || 'Provider returned an error';
throw new Error(errorMessage);
}
// Extract text from assistant messages
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
}
// Handle result messages
if (msg.type === 'result') {
if (msg.subtype === 'success') {
// Use result text if longer than accumulated text
if (msg.result && msg.result.length > responseText.length) {
responseText = msg.result;
}
// Capture structured output if present
if (msg.structured_output) {
structuredOutput = msg.structured_output;
}
} else if (msg.subtype === 'error_max_turns') {
// Max turns reached - return what we have
break;
} else if (msg.subtype === 'error_max_structured_output_retries') {
throw new Error('Could not produce valid structured output after retries');
}
}
}
return { text: responseText, structured_output: structuredOutput };
}
/**
* Execute a streaming query with event callbacks
*
* Use this for queries where you need real-time progress updates,
* such as when displaying streaming output to a user.
*
* @example
* ```typescript
* const result = await streamingQuery({
* prompt: 'Analyze this project and suggest improvements',
* cwd: '/path/to/project',
* maxTurns: 250,
* allowedTools: ['Read', 'Glob', 'Grep'],
* onText: (text) => emitProgress(text),
* onToolUse: (tool, input) => emitToolUse(tool, input),
* });
* ```
*/
export async function streamingQuery(options: StreamingQueryOptions): Promise<SimpleQueryResult> {
const model = options.model || DEFAULT_MODEL;
const provider = ProviderFactory.getProviderForModel(model);
const bareModel = stripProviderPrefix(model);
let responseText = '';
let structuredOutput: Record<string, unknown> | undefined;
// Build provider options
const providerOptions = {
prompt: options.prompt,
model: bareModel,
originalModel: model,
cwd: options.cwd,
systemPrompt: options.systemPrompt,
maxTurns: options.maxTurns ?? 250,
allowedTools: options.allowedTools ?? ['Read', 'Glob', 'Grep'],
abortController: options.abortController,
outputFormat: options.outputFormat,
thinkingLevel: options.thinkingLevel,
reasoningEffort: options.reasoningEffort,
readOnly: options.readOnly,
settingSources: options.settingSources,
};
for await (const msg of provider.executeQuery(providerOptions)) {
// Handle error messages
if (msg.type === 'error') {
const errorMessage = msg.error || 'Provider returned an error';
throw new Error(errorMessage);
}
// Extract content from assistant messages
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.text) {
responseText += block.text;
options.onText?.(block.text);
} else if (block.type === 'tool_use' && block.name) {
options.onToolUse?.(block.name, block.input);
} else if (block.type === 'thinking' && block.thinking) {
options.onThinking?.(block.thinking);
}
}
}
// Handle result messages
if (msg.type === 'result') {
if (msg.subtype === 'success') {
// Use result text if longer than accumulated text
if (msg.result && msg.result.length > responseText.length) {
responseText = msg.result;
}
// Capture structured output if present
if (msg.structured_output) {
structuredOutput = msg.structured_output;
}
} else if (msg.subtype === 'error_max_turns') {
// Max turns reached - return what we have
break;
} else if (msg.subtype === 'error_max_structured_output_retries') {
throw new Error('Could not produce valid structured output after retries');
}
}
}
return { text: responseText, structured_output: structuredOutput };
}

View File

@@ -6,26 +6,103 @@ import { createLogger } from '@automaker/utils';
const logger = createLogger('SpecRegeneration');
// Shared state for tracking generation status - private
let isRunning = false;
let currentAbortController: AbortController | null = null;
// Types for running generation
export type GenerationType = 'spec_regeneration' | 'feature_generation' | 'sync';
interface RunningGeneration {
isRunning: boolean;
type: GenerationType;
startedAt: string;
}
// Shared state for tracking generation status - scoped by project path
const runningProjects = new Map<string, RunningGeneration>();
const abortControllers = new Map<string, AbortController>();
/**
* Get the current running state
* Get the running state for a specific project
*/
export function getSpecRegenerationStatus(): {
export function getSpecRegenerationStatus(projectPath?: string): {
isRunning: boolean;
currentAbortController: AbortController | null;
projectPath?: string;
type?: GenerationType;
startedAt?: string;
} {
return { isRunning, currentAbortController };
if (projectPath) {
const generation = runningProjects.get(projectPath);
return {
isRunning: generation?.isRunning || false,
currentAbortController: abortControllers.get(projectPath) || null,
projectPath,
type: generation?.type,
startedAt: generation?.startedAt,
};
}
// Fallback: check if any project is running (for backward compatibility)
const isAnyRunning = Array.from(runningProjects.values()).some((g) => g.isRunning);
return { isRunning: isAnyRunning, currentAbortController: null };
}
/**
* Set the running state and abort controller
* Get the project path that is currently running (if any)
*/
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
isRunning = running;
currentAbortController = controller;
export function getRunningProjectPath(): string | null {
for (const [path, running] of runningProjects.entries()) {
if (running) return path;
}
return null;
}
/**
* Set the running state and abort controller for a specific project
*/
export function setRunningState(
projectPath: string,
running: boolean,
controller: AbortController | null = null,
type: GenerationType = 'spec_regeneration'
): void {
if (running) {
runningProjects.set(projectPath, {
isRunning: true,
type,
startedAt: new Date().toISOString(),
});
if (controller) {
abortControllers.set(projectPath, controller);
}
} else {
runningProjects.delete(projectPath);
abortControllers.delete(projectPath);
}
}
/**
* Get all running spec/feature generations for the running agents view
*/
export function getAllRunningGenerations(): Array<{
projectPath: string;
type: GenerationType;
startedAt: string;
}> {
const results: Array<{
projectPath: string;
type: GenerationType;
startedAt: string;
}> = [];
for (const [projectPath, generation] of runningProjects.entries()) {
if (generation.isRunning) {
results.push({
projectPath,
type: generation.type,
startedAt: generation.startedAt,
});
}
}
return results;
}
/**

View File

@@ -5,19 +5,17 @@
* (defaults to Sonnet for balanced speed and quality).
*/
import { query } from '@anthropic-ai/claude-agent-sdk';
import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { logAuthStatus } from './common.js';
import { streamingQuery } from '../../providers/simple-query-service.js';
import { parseAndCreateFeatures } from './parse-and-create-features.js';
import { getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
import { FeatureLoader } from '../../services/feature-loader.js';
const logger = createLogger('SpecRegeneration');
@@ -56,38 +54,48 @@ export async function generateFeaturesFromSpec(
return;
}
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[FeatureGeneration]');
// Load existing features to prevent duplicates
const featureLoader = new FeatureLoader();
const existingFeatures = await featureLoader.getAll(projectPath);
logger.info(`Found ${existingFeatures.length} existing features to exclude from generation`);
// Build existing features context for the prompt
let existingFeaturesContext = '';
if (existingFeatures.length > 0) {
const featuresList = existingFeatures
.map(
(f) =>
`- "${f.title}" (ID: ${f.id}): ${f.description?.substring(0, 100) || 'No description'}`
)
.join('\n');
existingFeaturesContext = `
## EXISTING FEATURES (DO NOT REGENERATE THESE)
The following ${existingFeatures.length} features already exist in the project. You MUST NOT generate features that duplicate or overlap with these:
${featuresList}
CRITICAL INSTRUCTIONS:
- DO NOT generate any features with the same or similar titles as the existing features listed above
- DO NOT generate features that cover the same functionality as existing features
- ONLY generate NEW features that are not yet in the system
- If a feature from the roadmap already exists, skip it entirely
- Generate unique feature IDs that do not conflict with existing IDs: ${existingFeatures.map((f) => f.id).join(', ')}
`;
}
const prompt = `Based on this project specification:
${spec}
${existingFeaturesContext}
${prompts.appSpec.generateFeaturesFromSpecPrompt}
Generate a prioritized list of implementable features. For each feature provide:
1. **id**: A unique lowercase-hyphenated identifier
2. **category**: Functional category (e.g., "Core", "UI", "API", "Authentication", "Database")
3. **title**: Short descriptive title
4. **description**: What this feature does (2-3 sentences)
5. **priority**: 1 (high), 2 (medium), or 3 (low)
6. **complexity**: "simple", "moderate", or "complex"
7. **dependencies**: Array of feature IDs this depends on (can be empty)
Format as JSON:
{
"features": [
{
"id": "feature-id",
"category": "Feature Category",
"title": "Feature Title",
"description": "What it does",
"priority": 1,
"complexity": "moderate",
"dependencies": []
}
]
}
Generate ${featureCount} features that build on each other logically.
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
Generate ${featureCount} NEW features that build on each other logically. Remember: ONLY generate features that DO NOT already exist.`;
logger.info('========== PROMPT BEING SENT ==========');
logger.info(`Prompt length: ${prompt.length} chars`);
@@ -115,119 +123,30 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
logger.info('Using model:', model);
let responseText = '';
let messageCount = 0;
// Use streamingQuery with event callbacks
const result = await streamingQuery({
prompt,
model,
cwd: projectPath,
maxTurns: 250,
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
thinkingLevel,
readOnly: true, // Feature generation only reads code, doesn't write
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
onText: (text) => {
logger.debug(`Feature text block received (${text.length} chars)`);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: text,
projectPath: projectPath,
});
},
});
// Route to appropriate provider based on model type
if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
logger.info('[FeatureGeneration] Using Cursor provider');
const responseText = result.text;
const provider = ProviderFactory.getProviderForModel(model);
// 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++;
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === 'text' && block.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.subtype === 'success' && msg.result) {
// Use result if it's a final accumulated message
if (msg.result.length > responseText.length) {
responseText = msg.result;
}
}
}
} else {
// Use Claude SDK for Claude models
logger.info('[FeatureGeneration] Using Claude SDK');
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.`);
logger.info(`Feature response length: ${responseText.length} chars`);
logger.info('========== FULL RESPONSE TEXT ==========');
logger.info(responseText);

View File

@@ -5,27 +5,18 @@
* (defaults to Opus for high-quality specification generation).
*/
import { query } from '@anthropic-ai/claude-agent-sdk';
import path from 'path';
import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import {
specOutputSchema,
specToXml,
getStructuredSpecPromptInstruction,
type SpecOutput,
} from '../../lib/app-spec-format.js';
import { specOutputSchema, specToXml, type SpecOutput } from '../../lib/app-spec-format.js';
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 { extractJson } from '../../lib/json-extractor.js';
import { ProviderFactory } from '../../providers/provider-factory.js';
import { logAuthStatus } from './common.js';
import { streamingQuery } from '../../providers/simple-query-service.js';
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
const logger = createLogger('SpecRegeneration');
@@ -47,6 +38,9 @@ export async function generateSpec(
logger.info('analyzeProject:', analyzeProject);
logger.info('maxFeatures:', maxFeatures);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[SpecRegeneration]');
// Build the prompt based on whether we should analyze the project
let analysisInstructions = '';
let techStackDefaults = '';
@@ -70,9 +64,7 @@ export async function generateSpec(
Use these technologies as the foundation for the specification.`;
}
const prompt = `You are helping to define a software project specification.
IMPORTANT: Never ask for clarification or additional information. Use the information provided and make reasonable assumptions to create the best possible specification. If details are missing, infer them based on common patterns and best practices.
const prompt = `${prompts.appSpec.generateSpecSystemPrompt}
Project Overview:
${projectOverview}
@@ -81,7 +73,7 @@ ${techStackDefaults}
${analysisInstructions}
${getStructuredSpecPromptInstruction()}`;
${prompts.appSpec.structuredSpecInstructions}`;
logger.info('========== PROMPT BEING SENT ==========');
logger.info(`Prompt length: ${prompt.length} chars`);
@@ -109,19 +101,15 @@ ${getStructuredSpecPromptInstruction()}`;
logger.info('Using model:', model);
let responseText = '';
let messageCount = 0;
let structuredOutput: SpecOutput | null = null;
// Route to appropriate provider based on model type
if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
logger.info('[SpecGeneration] Using Cursor provider');
// Determine if we should use structured output (Claude supports it, Cursor doesn't)
const useStructuredOutput = !isCursorModel(model);
const provider = ProviderFactory.getProviderForModel(model);
// 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}
// Build the final prompt - for Cursor, include JSON schema instructions
let finalPrompt = prompt;
if (!useStructuredOutput) {
finalPrompt = `${prompt}
CRITICAL INSTRUCTIONS:
1. DO NOT write any files. DO NOT create any files like "project_specification.json".
@@ -131,153 +119,57 @@ CRITICAL INSTRUCTIONS:
${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++;
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(
`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.subtype === 'success' && msg.result) {
// Use result if it's a final accumulated message
if (msg.result.length > responseText.length) {
responseText = msg.result;
}
}
}
// Parse JSON from the response text using shared utility
if (responseText) {
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}`);
// Use streamingQuery with event callbacks
const result = await streamingQuery({
prompt: finalPrompt,
model,
cwd: projectPath,
maxTurns: 250,
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
thinkingLevel,
readOnly: true, // Spec generation only reads code, we write the spec ourselves
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
outputFormat: useStructuredOutput
? {
type: 'json_schema',
schema: specOutputSchema,
}
: undefined,
onText: (text) => {
responseText += text;
logger.info(
`Text block received (${text.length} chars), total now: ${responseText.length} chars`
);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: text,
projectPath: projectPath,
});
},
onToolUse: (tool, input) => {
logger.info('Tool use:', tool);
events.emit('spec-regeneration:event', {
type: 'spec_tool',
tool,
input,
});
},
});
// Get structured output if available
if (result.structured_output) {
structuredOutput = result.structured_output as unknown as SpecOutput;
logger.info('✅ Received structured output');
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
} else if (!useStructuredOutput && responseText) {
// For non-Claude providers, parse JSON from response text
structuredOutput = extractJson<SpecOutput>(responseText, { logger });
}
logger.info(`Stream iteration complete.`);
logger.info(`Response text length: ${responseText.length} chars`);
// Determine XML content to save
@@ -309,19 +201,33 @@ Your entire response should be valid JSON starting with { and ending with }. No
xmlContent = responseText.substring(xmlStart, xmlEnd + '</project_specification>'.length);
logger.info(`Extracted XML content: ${xmlContent.length} chars (from position ${xmlStart})`);
} else {
// No valid XML structure found in the response text
// This happens when structured output was expected but not received, and the agent
// output conversational text instead of XML (e.g., "The project directory appears to be empty...")
// We should NOT save this conversational text as it's not a valid spec
logger.error('❌ Response does not contain valid <project_specification> XML structure');
logger.error(
'This typically happens when structured output failed and the agent produced conversational text instead of XML'
);
throw new Error(
'Failed to generate spec: No valid XML structure found in response. ' +
'The response contained conversational text but no <project_specification> tags. ' +
'Please try again.'
);
// No XML found, try JSON extraction
logger.warn('⚠️ No XML tags found, attempting JSON extraction...');
const extractedJson = extractJson<SpecOutput>(responseText, { logger });
if (
extractedJson &&
typeof extractedJson.project_name === 'string' &&
typeof extractedJson.overview === 'string' &&
Array.isArray(extractedJson.technology_stack) &&
Array.isArray(extractedJson.core_capabilities) &&
Array.isArray(extractedJson.implemented_features)
) {
logger.info('✅ Successfully extracted JSON from response text');
xmlContent = specToXml(extractedJson);
logger.info(`✅ Converted extracted JSON to XML: ${xmlContent.length} chars`);
} else {
// Neither XML nor valid JSON found
logger.error('❌ Response does not contain valid XML or JSON structure');
logger.error(
'This typically happens when structured output failed and the agent produced conversational text instead of structured output'
);
throw new Error(
'Failed to generate spec: No valid XML or JSON structure found in response. ' +
'The response contained conversational text but no <project_specification> tags or valid JSON. ' +
'Please try again.'
);
}
}
}

View File

@@ -7,6 +7,7 @@ import type { EventEmitter } from '../../lib/events.js';
import { createCreateHandler } from './routes/create.js';
import { createGenerateHandler } from './routes/generate.js';
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
import { createSyncHandler } from './routes/sync.js';
import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from './routes/status.js';
import type { SettingsService } from '../../services/settings-service.js';
@@ -20,6 +21,7 @@ export function createSpecRegenerationRoutes(
router.post('/create', createCreateHandler(events));
router.post('/generate', createGenerateHandler(events, settingsService));
router.post('/generate-features', createGenerateFeaturesHandler(events, settingsService));
router.post('/sync', createSyncHandler(events, settingsService));
router.post('/stop', createStopHandler());
router.get('/status', createStatusHandler());

View File

@@ -5,9 +5,10 @@
import path from 'path';
import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { createLogger, atomicWriteJson, DEFAULT_BACKUP_COUNT } from '@automaker/utils';
import { getFeaturesDir } from '@automaker/platform';
import { extractJsonWithArray } from '../../lib/json-extractor.js';
import { getNotificationService } from '../../services/notification-service.js';
const logger = createLogger('SpecRegeneration');
@@ -73,10 +74,10 @@ export async function parseAndCreateFeatures(
updatedAt: new Date().toISOString(),
};
await secureFs.writeFile(
path.join(featureDir, 'feature.json'),
JSON.stringify(featureData, null, 2)
);
// Use atomic write with backup support for crash protection
await atomicWriteJson(path.join(featureDir, 'feature.json'), featureData, {
backupCount: DEFAULT_BACKUP_COUNT,
});
createdFeatures.push({ id: feature.id, title: feature.title });
}
@@ -88,6 +89,15 @@ export async function parseAndCreateFeatures(
message: `Spec regeneration complete! Created ${createdFeatures.length} features.`,
projectPath: projectPath,
});
// Create notification for spec generation completion
const notificationService = getNotificationService();
await notificationService.createNotification({
type: 'spec_regeneration_complete',
title: 'Spec Generation Complete',
message: `Created ${createdFeatures.length} features from the project specification.`,
projectPath: projectPath,
});
} catch (error) {
logger.error('❌ parseAndCreateFeatures() failed:');
logger.error('Error:', error);

View File

@@ -47,17 +47,17 @@ export function createCreateHandler(events: EventEmitter) {
return;
}
const { isRunning } = getSpecRegenerationStatus();
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Spec generation already running' });
logger.warn('Generation already running for project:', projectPath);
res.json({ success: false, error: 'Spec generation already running for this project' });
return;
}
logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
setRunningState(projectPath, true, abortController);
logger.info('Starting background generation task...');
// Start generation in background
@@ -80,7 +80,7 @@ export function createCreateHandler(events: EventEmitter) {
})
.finally(() => {
logger.info('Generation task finished (success or error)');
setRunningState(false, null);
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (generation running in background)');

View File

@@ -40,17 +40,17 @@ export function createGenerateFeaturesHandler(
return;
}
const { isRunning } = getSpecRegenerationStatus();
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Generation already running' });
logger.warn('Generation already running for project:', projectPath);
res.json({ success: false, error: 'Generation already running for this project' });
return;
}
logAuthStatus('Before starting feature generation');
const abortController = new AbortController();
setRunningState(true, abortController);
setRunningState(projectPath, true, abortController, 'feature_generation');
logger.info('Starting background feature generation task...');
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures, settingsService)
@@ -63,7 +63,7 @@ export function createGenerateFeaturesHandler(
})
.finally(() => {
logger.info('Feature generation task finished (success or error)');
setRunningState(false, null);
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (generation running in background)');

View File

@@ -48,17 +48,17 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
return;
}
const { isRunning } = getSpecRegenerationStatus();
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Spec generation already running' });
logger.warn('Generation already running for project:', projectPath);
res.json({ success: false, error: 'Spec generation already running for this project' });
return;
}
logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
setRunningState(projectPath, true, abortController);
logger.info('Starting background generation task...');
generateSpec(
@@ -81,7 +81,7 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
})
.finally(() => {
logger.info('Generation task finished (success or error)');
setRunningState(false, null);
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (generation running in background)');

View File

@@ -6,10 +6,11 @@ import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {
return async (req: Request, res: Response): Promise<void> => {
try {
const { isRunning } = getSpecRegenerationStatus();
res.json({ success: true, isRunning });
const projectPath = req.query.projectPath as string | undefined;
const { isRunning } = getSpecRegenerationStatus(projectPath);
res.json({ success: true, isRunning, projectPath });
} catch (error) {
res.status(500).json({ success: false, error: getErrorMessage(error) });
}

View File

@@ -6,13 +6,16 @@ import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => {
return async (req: Request, res: Response): Promise<void> => {
try {
const { currentAbortController } = getSpecRegenerationStatus();
const { projectPath } = req.body as { projectPath?: string };
const { currentAbortController } = getSpecRegenerationStatus(projectPath);
if (currentAbortController) {
currentAbortController.abort();
}
setRunningState(false, null);
if (projectPath) {
setRunningState(projectPath, false, null);
}
res.json({ success: true });
} catch (error) {
res.status(500).json({ success: false, error: getErrorMessage(error) });

View File

@@ -0,0 +1,76 @@
/**
* POST /sync endpoint - Sync spec with codebase and features
*/
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from '../common.js';
import { syncSpec } from '../sync-spec.js';
import type { SettingsService } from '../../../services/settings-service.js';
const logger = createLogger('SpecSync');
export function createSyncHandler(events: EventEmitter, settingsService?: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
logger.info('========== /sync endpoint called ==========');
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
const { projectPath } = req.body as {
projectPath: string;
};
logger.debug('projectPath:', projectPath);
if (!projectPath) {
logger.error('Missing projectPath parameter');
res.status(400).json({ success: false, error: 'projectPath required' });
return;
}
const { isRunning } = getSpecRegenerationStatus(projectPath);
if (isRunning) {
logger.warn('Generation/sync already running for project:', projectPath);
res.json({ success: false, error: 'Operation already running for this project' });
return;
}
logAuthStatus('Before starting spec sync');
const abortController = new AbortController();
setRunningState(projectPath, true, abortController, 'sync');
logger.info('Starting background spec sync task...');
syncSpec(projectPath, events, abortController, settingsService)
.then((result) => {
logger.info('Spec sync completed successfully');
logger.info('Result:', JSON.stringify(result, null, 2));
})
.catch((error) => {
logError(error, 'Spec sync failed with error');
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_error',
error: getErrorMessage(error),
projectPath,
});
})
.finally(() => {
logger.info('Spec sync task finished (success or error)');
setRunningState(projectPath, false, null);
});
logger.info('Returning success response (sync running in background)');
res.json({ success: true });
} catch (error) {
logError(error, 'Sync route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,307 @@
/**
* Sync spec with current codebase and feature state
*
* Updates the spec file based on:
* - Completed Automaker features
* - Code analysis for tech stack and implementations
* - Roadmap phase status updates
*/
import * as secureFs from '../../lib/secure-fs.js';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { streamingQuery } from '../../providers/simple-query-service.js';
import { getAppSpecPath } from '@automaker/platform';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../lib/settings-helpers.js';
import { FeatureLoader } from '../../services/feature-loader.js';
import {
extractImplementedFeatures,
extractTechnologyStack,
extractRoadmapPhases,
updateImplementedFeaturesSection,
updateTechnologyStack,
updateRoadmapPhaseStatus,
type ImplementedFeature,
type RoadmapPhase,
} from '../../lib/xml-extractor.js';
import { getNotificationService } from '../../services/notification-service.js';
const logger = createLogger('SpecSync');
/**
* Result of a sync operation
*/
export interface SyncResult {
techStackUpdates: {
added: string[];
removed: string[];
};
implementedFeaturesUpdates: {
addedFromFeatures: string[];
removed: string[];
};
roadmapUpdates: Array<{ phaseName: string; newStatus: string }>;
summary: string;
}
/**
* Sync the spec with current codebase and feature state
*/
export async function syncSpec(
projectPath: string,
events: EventEmitter,
abortController: AbortController,
settingsService?: SettingsService
): Promise<SyncResult> {
logger.info('========== syncSpec() started ==========');
logger.info('projectPath:', projectPath);
const result: SyncResult = {
techStackUpdates: { added: [], removed: [] },
implementedFeaturesUpdates: { addedFromFeatures: [], removed: [] },
roadmapUpdates: [],
summary: '',
};
// Read existing spec
const specPath = getAppSpecPath(projectPath);
let specContent: string;
try {
specContent = (await secureFs.readFile(specPath, 'utf-8')) as string;
logger.info(`Spec loaded successfully (${specContent.length} chars)`);
} catch (readError) {
logger.error('Failed to read spec file:', readError);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_error',
error: 'No project spec found. Create or regenerate spec first.',
projectPath,
});
throw new Error('No project spec found');
}
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: '[Phase: sync] Starting spec sync...\n',
projectPath,
});
// Extract current state from spec
const currentImplementedFeatures = extractImplementedFeatures(specContent);
const currentTechStack = extractTechnologyStack(specContent);
const currentRoadmapPhases = extractRoadmapPhases(specContent);
logger.info(`Current spec has ${currentImplementedFeatures.length} implemented features`);
logger.info(`Current spec has ${currentTechStack.length} technologies`);
logger.info(`Current spec has ${currentRoadmapPhases.length} roadmap phases`);
// Load completed Automaker features
const featureLoader = new FeatureLoader();
const allFeatures = await featureLoader.getAll(projectPath);
const completedFeatures = allFeatures.filter(
(f) => f.status === 'completed' || f.status === 'verified'
);
logger.info(`Found ${completedFeatures.length} completed/verified features in Automaker`);
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: `Found ${completedFeatures.length} completed features to sync...\n`,
projectPath,
});
// Build new implemented features list from completed Automaker features
const newImplementedFeatures: ImplementedFeature[] = [];
const existingNames = new Set(currentImplementedFeatures.map((f) => f.name.toLowerCase()));
for (const feature of completedFeatures) {
const name = feature.title || `Feature: ${feature.id}`;
if (!existingNames.has(name.toLowerCase())) {
newImplementedFeatures.push({
name,
description: feature.description || '',
});
result.implementedFeaturesUpdates.addedFromFeatures.push(name);
}
}
// Merge: keep existing + add new from completed features
const mergedFeatures = [...currentImplementedFeatures, ...newImplementedFeatures];
// Update spec with merged features
if (result.implementedFeaturesUpdates.addedFromFeatures.length > 0) {
specContent = updateImplementedFeaturesSection(specContent, mergedFeatures);
logger.info(
`Added ${result.implementedFeaturesUpdates.addedFromFeatures.length} features to spec`
);
}
// Analyze codebase for tech stack updates using AI
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: 'Analyzing codebase for technology updates...\n',
projectPath,
});
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[SpecSync]'
);
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
const { model, thinkingLevel } = resolvePhaseModel(phaseModelEntry);
// Use AI to analyze tech stack
const techAnalysisPrompt = `Analyze this project and return ONLY a JSON object with the current technology stack.
Current known technologies: ${currentTechStack.join(', ')}
Look at package.json, config files, and source code to identify:
- Frameworks (React, Vue, Express, etc.)
- Languages (TypeScript, JavaScript, Python, etc.)
- Build tools (Vite, Webpack, etc.)
- Databases (PostgreSQL, MongoDB, etc.)
- Key libraries and tools
Return ONLY this JSON format, no other text:
{
"technologies": ["Technology 1", "Technology 2", ...]
}`;
try {
const techResult = await streamingQuery({
prompt: techAnalysisPrompt,
model,
cwd: projectPath,
maxTurns: 10,
allowedTools: ['Read', 'Glob', 'Grep'],
abortController,
thinkingLevel,
readOnly: true,
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
onText: (text) => {
logger.debug(`Tech analysis text: ${text.substring(0, 100)}`);
},
});
// Parse tech stack from response
const jsonMatch = techResult.text.match(/\{[\s\S]*"technologies"[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
if (Array.isArray(parsed.technologies)) {
const newTechStack = parsed.technologies as string[];
// Calculate differences
const currentSet = new Set(currentTechStack.map((t) => t.toLowerCase()));
const newSet = new Set(newTechStack.map((t) => t.toLowerCase()));
for (const tech of newTechStack) {
if (!currentSet.has(tech.toLowerCase())) {
result.techStackUpdates.added.push(tech);
}
}
for (const tech of currentTechStack) {
if (!newSet.has(tech.toLowerCase())) {
result.techStackUpdates.removed.push(tech);
}
}
// Update spec with new tech stack if there are changes
if (
result.techStackUpdates.added.length > 0 ||
result.techStackUpdates.removed.length > 0
) {
specContent = updateTechnologyStack(specContent, newTechStack);
logger.info(
`Updated tech stack: +${result.techStackUpdates.added.length}, -${result.techStackUpdates.removed.length}`
);
}
}
}
} catch (error) {
logger.warn('Failed to analyze tech stack:', error);
// Continue with other sync operations
}
// Update roadmap phase statuses based on completed features
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_progress',
content: 'Checking roadmap phase statuses...\n',
projectPath,
});
// For each phase, check if all its features are completed
// This is a heuristic - we check if the phase name appears in any feature titles/descriptions
for (const phase of currentRoadmapPhases) {
if (phase.status === 'completed') continue; // Already completed
// Check if this phase should be marked as completed
// A phase is considered complete if we have completed features that mention it
const phaseNameLower = phase.name.toLowerCase();
const relatedCompletedFeatures = completedFeatures.filter(
(f) =>
f.title?.toLowerCase().includes(phaseNameLower) ||
f.description?.toLowerCase().includes(phaseNameLower) ||
f.category?.toLowerCase().includes(phaseNameLower)
);
// If we have related completed features and the phase is still pending/in_progress,
// update it to in_progress or completed based on feature count
if (relatedCompletedFeatures.length > 0 && phase.status !== 'completed') {
const newStatus = 'in_progress';
specContent = updateRoadmapPhaseStatus(specContent, phase.name, newStatus);
result.roadmapUpdates.push({ phaseName: phase.name, newStatus });
logger.info(`Updated phase "${phase.name}" to ${newStatus}`);
}
}
// Save updated spec
await secureFs.writeFile(specPath, specContent, 'utf-8');
logger.info('Spec saved successfully');
// Build summary
const summaryParts: string[] = [];
if (result.implementedFeaturesUpdates.addedFromFeatures.length > 0) {
summaryParts.push(
`Added ${result.implementedFeaturesUpdates.addedFromFeatures.length} implemented features`
);
}
if (result.techStackUpdates.added.length > 0) {
summaryParts.push(`Added ${result.techStackUpdates.added.length} technologies`);
}
if (result.techStackUpdates.removed.length > 0) {
summaryParts.push(`Removed ${result.techStackUpdates.removed.length} technologies`);
}
if (result.roadmapUpdates.length > 0) {
summaryParts.push(`Updated ${result.roadmapUpdates.length} roadmap phases`);
}
result.summary = summaryParts.length > 0 ? summaryParts.join(', ') : 'Spec is already up to date';
// Create notification
const notificationService = getNotificationService();
await notificationService.createNotification({
type: 'spec_regeneration_complete',
title: 'Spec Sync Complete',
message: result.summary,
projectPath,
});
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_complete',
message: `Spec sync complete! ${result.summary}`,
projectPath,
});
logger.info('========== syncSpec() completed ==========');
logger.info('Summary:', result.summary);
return result;
}

View File

@@ -229,12 +229,13 @@ export function createAuthRoutes(): Router {
await invalidateSession(sessionToken);
}
// Clear the cookie
res.clearCookie(cookieName, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
path: '/',
// Clear the cookie by setting it to empty with immediate expiration
// Using res.cookie() with maxAge: 0 is more reliable than clearCookie()
// in cross-origin development environments
res.cookie(cookieName, '', {
...getSessionCookieOptions(),
maxAge: 0,
expires: new Date(0),
});
res.json({

View File

@@ -17,6 +17,7 @@ import { createAnalyzeProjectHandler } from './routes/analyze-project.js';
import { createFollowUpFeatureHandler } from './routes/follow-up-feature.js';
import { createCommitFeatureHandler } from './routes/commit-feature.js';
import { createApprovePlanHandler } from './routes/approve-plan.js';
import { createResumeInterruptedHandler } from './routes/resume-interrupted.js';
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router();
@@ -63,6 +64,11 @@ export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
validatePathParams('projectPath'),
createApprovePlanHandler(autoModeService)
);
router.post(
'/resume-interrupted',
validatePathParams('projectPath'),
createResumeInterruptedHandler(autoModeService)
);
return router;
}

View File

@@ -31,7 +31,9 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
autoModeService
.followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? true)
// Default to false to match run-feature/resume-feature behavior.
// Worktrees should only be used when explicitly enabled by the user.
.followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? false)
.catch((error) => {
logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
})

View File

@@ -0,0 +1,42 @@
/**
* Resume Interrupted Features Handler
*
* Checks for features that were interrupted (in pipeline steps or in_progress)
* when the server was restarted and resumes them.
*/
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
const logger = createLogger('ResumeInterrupted');
interface ResumeInterruptedRequest {
projectPath: string;
}
export function createResumeInterruptedHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
const { projectPath } = req.body as ResumeInterruptedRequest;
if (!projectPath) {
res.status(400).json({ error: 'Project path is required' });
return;
}
logger.info(`Checking for interrupted features in ${projectPath}`);
try {
await autoModeService.resumeInterruptedFeatures(projectPath);
res.json({
success: true,
message: 'Resume check completed',
});
} catch (error) {
logger.error('Error resuming interrupted features:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Unknown error',
});
}
};
}

View File

@@ -3,12 +3,31 @@
*/
import { createLogger } from '@automaker/utils';
import { ensureAutomakerDir, getAutomakerDir } from '@automaker/platform';
import * as secureFs from '../../lib/secure-fs.js';
import path from 'path';
import type { BacklogPlanResult } from '@automaker/types';
const logger = createLogger('BacklogPlan');
// State for tracking running generation
let isRunning = false;
let currentAbortController: AbortController | null = null;
let runningDetails: {
projectPath: string;
prompt: string;
model?: string;
startedAt: string;
} | null = null;
const BACKLOG_PLAN_FILENAME = 'backlog-plan.json';
export interface StoredBacklogPlan {
savedAt: string;
prompt: string;
model?: string;
result: BacklogPlanResult;
}
export function getBacklogPlanStatus(): { isRunning: boolean } {
return { isRunning };
@@ -16,11 +35,67 @@ export function getBacklogPlanStatus(): { isRunning: boolean } {
export function setRunningState(running: boolean, abortController?: AbortController | null): void {
isRunning = running;
if (!running) {
runningDetails = null;
}
if (abortController !== undefined) {
currentAbortController = abortController;
}
}
export function setRunningDetails(
details: {
projectPath: string;
prompt: string;
model?: string;
startedAt: string;
} | null
): void {
runningDetails = details;
}
export function getRunningDetails(): {
projectPath: string;
prompt: string;
model?: string;
startedAt: string;
} | null {
return runningDetails;
}
function getBacklogPlanPath(projectPath: string): string {
return path.join(getAutomakerDir(projectPath), BACKLOG_PLAN_FILENAME);
}
export async function saveBacklogPlan(projectPath: string, plan: StoredBacklogPlan): Promise<void> {
await ensureAutomakerDir(projectPath);
const filePath = getBacklogPlanPath(projectPath);
await secureFs.writeFile(filePath, JSON.stringify(plan, null, 2), 'utf-8');
}
export async function loadBacklogPlan(projectPath: string): Promise<StoredBacklogPlan | null> {
try {
const filePath = getBacklogPlanPath(projectPath);
const raw = await secureFs.readFile(filePath, 'utf-8');
const parsed = JSON.parse(raw as string) as StoredBacklogPlan;
if (!Array.isArray(parsed?.result?.changes)) {
return null;
}
return parsed;
} catch {
return null;
}
}
export async function clearBacklogPlan(projectPath: string): Promise<void> {
try {
const filePath = getBacklogPlanPath(projectPath);
await secureFs.unlink(filePath);
} catch {
// ignore missing file
}
}
export function getAbortController(): AbortController | null {
return currentAbortController;
}

View File

@@ -7,12 +7,23 @@
import type { EventEmitter } from '../../lib/events.js';
import type { Feature, BacklogPlanResult, BacklogChange, DependencyUpdate } from '@automaker/types';
import { DEFAULT_PHASE_MODELS, isCursorModel, type ThinkingLevel } from '@automaker/types';
import {
DEFAULT_PHASE_MODELS,
isCursorModel,
stripProviderPrefix,
type ThinkingLevel,
} from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { FeatureLoader } from '../../services/feature-loader.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,
setRunningDetails,
getErrorMessage,
saveBacklogPlan,
} from './common.js';
import type { SettingsService } from '../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting, getPromptCustomization } from '../../lib/settings-helpers.js';
@@ -120,6 +131,8 @@ export async function generateBacklogPlan(
logger.info('[BacklogPlan] Using model:', effectiveModel);
const provider = ProviderFactory.getProviderForModel(effectiveModel);
// Strip provider prefix - providers expect bare model IDs
const bareModel = stripProviderPrefix(effectiveModel);
// Get autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
@@ -151,7 +164,7 @@ ${userPrompt}`;
// Execute the query
const stream = provider.executeQuery({
prompt: finalPrompt,
model: effectiveModel,
model: bareModel,
cwd: projectPath,
systemPrompt: finalSystemPrompt,
maxTurns: 1,
@@ -193,6 +206,13 @@ ${userPrompt}`;
// Parse the response
const result = parsePlanResponse(responseText);
await saveBacklogPlan(projectPath, {
savedAt: new Date().toISOString(),
prompt,
model: effectiveModel,
result,
});
events.emit('backlog-plan:event', {
type: 'backlog_plan_complete',
result,
@@ -211,5 +231,6 @@ ${userPrompt}`;
throw error;
} finally {
setRunningState(false, null);
setRunningDetails(null);
}
}

View File

@@ -9,6 +9,7 @@ import { createGenerateHandler } from './routes/generate.js';
import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from './routes/status.js';
import { createApplyHandler } from './routes/apply.js';
import { createClearHandler } from './routes/clear.js';
import type { SettingsService } from '../../services/settings-service.js';
export function createBacklogPlanRoutes(
@@ -23,8 +24,9 @@ export function createBacklogPlanRoutes(
createGenerateHandler(events, settingsService)
);
router.post('/stop', createStopHandler());
router.get('/status', createStatusHandler());
router.get('/status', validatePathParams('projectPath'), createStatusHandler());
router.post('/apply', validatePathParams('projectPath'), createApplyHandler());
router.post('/clear', validatePathParams('projectPath'), createClearHandler());
return router;
}

View File

@@ -5,18 +5,29 @@
import type { Request, Response } from 'express';
import type { BacklogPlanResult, BacklogChange, Feature } from '@automaker/types';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError, logger } from '../common.js';
import { clearBacklogPlan, getErrorMessage, logError, logger } from '../common.js';
const featureLoader = new FeatureLoader();
export function createApplyHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, plan } = req.body as {
const {
projectPath,
plan,
branchName: rawBranchName,
} = req.body as {
projectPath: string;
plan: BacklogPlanResult;
branchName?: string;
};
// Validate branchName: must be undefined or a non-empty trimmed string
const branchName =
typeof rawBranchName === 'string' && rawBranchName.trim().length > 0
? rawBranchName.trim()
: undefined;
if (!projectPath) {
res.status(400).json({ success: false, error: 'projectPath required' });
return;
@@ -82,6 +93,7 @@ export function createApplyHandler() {
dependencies: change.feature.dependencies,
priority: change.feature.priority,
status: 'backlog',
branchName,
});
appliedChanges.push(`added:${newFeature.id}`);
@@ -135,6 +147,17 @@ export function createApplyHandler() {
}
}
// Clear the plan before responding
try {
await clearBacklogPlan(projectPath);
} catch (error) {
logger.warn(
`[BacklogPlan] Failed to clear backlog plan after apply:`,
getErrorMessage(error)
);
// Don't throw - operation succeeded, just cleanup failed
}
res.json({
success: true,
appliedChanges,

View File

@@ -0,0 +1,25 @@
/**
* POST /clear endpoint - Clear saved backlog plan
*/
import type { Request, Response } from 'express';
import { clearBacklogPlan, getErrorMessage, logError } from '../common.js';
export function createClearHandler() {
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 required' });
return;
}
await clearBacklogPlan(projectPath);
res.json({ success: true });
} catch (error) {
logError(error, 'Clear backlog plan failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -4,7 +4,13 @@
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import { getBacklogPlanStatus, setRunningState, getErrorMessage, logError } from '../common.js';
import {
getBacklogPlanStatus,
setRunningState,
setRunningDetails,
getErrorMessage,
logError,
} from '../common.js';
import { generateBacklogPlan } from '../generate-plan.js';
import type { SettingsService } from '../../../services/settings-service.js';
@@ -37,6 +43,12 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
}
setRunningState(true);
setRunningDetails({
projectPath,
prompt,
model,
startedAt: new Date().toISOString(),
});
const abortController = new AbortController();
setRunningState(true, abortController);
@@ -51,6 +63,7 @@ export function createGenerateHandler(events: EventEmitter, settingsService?: Se
})
.finally(() => {
setRunningState(false, null);
setRunningDetails(null);
});
res.json({ success: true });

View File

@@ -3,13 +3,15 @@
*/
import type { Request, Response } from 'express';
import { getBacklogPlanStatus, getErrorMessage, logError } from '../common.js';
import { getBacklogPlanStatus, loadBacklogPlan, getErrorMessage, logError } from '../common.js';
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {
return async (req: Request, res: Response): Promise<void> => {
try {
const status = getBacklogPlanStatus();
res.json({ success: true, ...status });
const projectPath = typeof req.query.projectPath === 'string' ? req.query.projectPath : '';
const savedPlan = projectPath ? await loadBacklogPlan(projectPath) : null;
res.json({ success: true, ...status, savedPlan });
} catch (error) {
logError(error, 'Get backlog plan status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });

View File

@@ -3,7 +3,13 @@
*/
import type { Request, Response } from 'express';
import { getAbortController, setRunningState, getErrorMessage, logError } from '../common.js';
import {
getAbortController,
setRunningState,
setRunningDetails,
getErrorMessage,
logError,
} from '../common.js';
export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -12,6 +18,7 @@ export function createStopHandler() {
if (abortController) {
abortController.abort();
setRunningState(false, null);
setRunningDetails(null);
}
res.json({ success: true });
} catch (error) {

View File

@@ -13,7 +13,10 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
// Check if Claude CLI is available first
const isAvailable = await service.isAvailable();
if (!isAvailable) {
res.status(503).json({
// IMPORTANT: This endpoint is behind Automaker session auth already.
// Use a 200 + error payload for Claude CLI issues so the UI doesn't
// interpret it as an invalid Automaker session (401/403 triggers logout).
res.status(200).json({
error: 'Claude CLI not found',
message: "Please install Claude Code CLI and run 'claude login' to authenticate",
});
@@ -26,12 +29,20 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
const message = error instanceof Error ? error.message : 'Unknown error';
if (message.includes('Authentication required') || message.includes('token_expired')) {
res.status(401).json({
// Do NOT use 401/403 here: that status code is reserved for Automaker session auth.
res.status(200).json({
error: 'Authentication required',
message: "Please run 'claude login' to authenticate",
});
} else if (message.includes('TRUST_PROMPT_PENDING')) {
// Trust prompt appeared but couldn't be auto-approved
res.status(200).json({
error: 'Trust prompt pending',
message:
'Claude CLI needs folder permission. Please run "claude" in your terminal and approve access.',
});
} else if (message.includes('timed out')) {
res.status(504).json({
res.status(200).json({
error: 'Command timed out',
message: 'The Claude CLI took too long to respond',
});

View File

@@ -0,0 +1,78 @@
/**
* Common utilities for code-review routes
*/
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger('CodeReview');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };
export const logError = createLogError(logger);
/**
* Review state interface
*/
interface ReviewState {
isRunning: boolean;
abortController: AbortController | null;
projectPath: string | null;
}
/**
* Shared state for code review operations
* Using an object to avoid mutable `let` exports which can cause issues in ES modules
*/
const reviewState: ReviewState = {
isRunning: false,
abortController: null,
projectPath: null,
};
/**
* Check if a review is currently running
*/
export function isRunning(): boolean {
return reviewState.isRunning;
}
/**
* Get the current abort controller (for stopping reviews)
*/
export function getAbortController(): AbortController | null {
return reviewState.abortController;
}
/**
* Get the current project path being reviewed
*/
export function getCurrentProjectPath(): string | null {
return reviewState.projectPath;
}
/**
* Set the running state for code review operations
*/
export function setRunningState(
running: boolean,
controller: AbortController | null = null,
projectPath: string | null = null
): void {
reviewState.isRunning = running;
reviewState.abortController = controller;
reviewState.projectPath = projectPath;
}
/**
* Get the current review status
*/
export function getReviewStatus(): {
isRunning: boolean;
projectPath: string | null;
} {
return {
isRunning: reviewState.isRunning,
projectPath: reviewState.projectPath,
};
}

View File

@@ -0,0 +1,40 @@
/**
* Code Review routes - HTTP API for triggering and managing code reviews
*
* Provides endpoints for:
* - Triggering code reviews on projects
* - Checking review status
* - Stopping in-progress reviews
*
* Uses the CodeReviewService for actual review execution with AI providers.
*/
import { Router } from 'express';
import type { CodeReviewService } from '../../services/code-review-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createTriggerHandler } from './routes/trigger.js';
import { createStatusHandler } from './routes/status.js';
import { createStopHandler } from './routes/stop.js';
import { createProvidersHandler } from './routes/providers.js';
export function createCodeReviewRoutes(codeReviewService: CodeReviewService): Router {
const router = Router();
// POST /trigger - Start a new code review
router.post(
'/trigger',
validatePathParams('projectPath'),
createTriggerHandler(codeReviewService)
);
// GET /status - Get current review status
router.get('/status', createStatusHandler());
// POST /stop - Stop current review
router.post('/stop', createStopHandler());
// GET /providers - Get available providers and their status
router.get('/providers', createProvidersHandler(codeReviewService));
return router;
}

View File

@@ -0,0 +1,38 @@
/**
* GET /providers endpoint - Get available code review providers
*
* Returns the status of all available AI providers that can be used for code reviews.
*/
import type { Request, Response } from 'express';
import type { CodeReviewService } from '../../../services/code-review-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('CodeReview');
export function createProvidersHandler(codeReviewService: CodeReviewService) {
return async (req: Request, res: Response): Promise<void> => {
logger.debug('========== /providers endpoint called ==========');
try {
// Check if refresh is requested
const forceRefresh = req.query.refresh === 'true';
const providers = await codeReviewService.getProviderStatus(forceRefresh);
const bestProvider = await codeReviewService.getBestProvider();
res.json({
success: true,
providers,
recommended: bestProvider,
});
} catch (error) {
logError(error, 'Providers handler exception');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,32 @@
/**
* GET /status endpoint - Get current code review status
*
* Returns whether a code review is currently running and which project.
*/
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import { getReviewStatus, getErrorMessage, logError } from '../common.js';
const logger = createLogger('CodeReview');
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {
logger.debug('========== /status endpoint called ==========');
try {
const status = getReviewStatus();
res.json({
success: true,
...status,
});
} catch (error) {
logError(error, 'Status handler exception');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,54 @@
/**
* POST /stop endpoint - Stop the current code review
*
* Aborts any running code review operation.
*/
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import {
isRunning,
getAbortController,
setRunningState,
getErrorMessage,
logError,
} from '../common.js';
const logger = createLogger('CodeReview');
export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => {
logger.info('========== /stop endpoint called ==========');
try {
if (!isRunning()) {
res.json({
success: true,
message: 'No code review is currently running',
});
return;
}
// Abort the current operation
const abortController = getAbortController();
if (abortController) {
abortController.abort();
logger.info('Code review aborted');
}
// Reset state
setRunningState(false, null, null);
res.json({
success: true,
message: 'Code review stopped',
});
} catch (error) {
logError(error, 'Stop handler exception');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,188 @@
/**
* POST /trigger endpoint - Trigger a code review
*
* Starts an asynchronous code review on the specified project.
* Progress updates are streamed via WebSocket events.
*/
import type { Request, Response } from 'express';
import type { CodeReviewService } from '../../../services/code-review-service.js';
import type { CodeReviewCategory, ThinkingLevel, ModelId } from '@automaker/types';
import { createLogger } from '@automaker/utils';
import { isRunning, setRunningState, getErrorMessage, logError } from '../common.js';
const logger = createLogger('CodeReview');
/**
* Maximum number of files allowed per review request
*/
const MAX_FILES_PER_REQUEST = 100;
/**
* Maximum length for baseRef parameter
*/
const MAX_BASE_REF_LENGTH = 256;
/**
* Valid categories for code review
*/
const VALID_CATEGORIES: CodeReviewCategory[] = [
'tech_stack',
'security',
'code_quality',
'implementation',
'architecture',
'performance',
'testing',
'documentation',
];
/**
* Valid thinking levels
*/
const VALID_THINKING_LEVELS: ThinkingLevel[] = ['low', 'medium', 'high'];
interface TriggerRequestBody {
projectPath: string;
files?: string[];
baseRef?: string;
categories?: CodeReviewCategory[];
autoFix?: boolean;
model?: ModelId;
thinkingLevel?: ThinkingLevel;
}
/**
* Validate and sanitize the request body
*/
function validateRequestBody(body: TriggerRequestBody): { valid: boolean; error?: string } {
const { files, baseRef, categories, autoFix, thinkingLevel } = body;
// Validate files array
if (files !== undefined) {
if (!Array.isArray(files)) {
return { valid: false, error: 'files must be an array' };
}
if (files.length > MAX_FILES_PER_REQUEST) {
return { valid: false, error: `Maximum ${MAX_FILES_PER_REQUEST} files allowed per request` };
}
for (const file of files) {
if (typeof file !== 'string') {
return { valid: false, error: 'Each file must be a string' };
}
if (file.length > 500) {
return { valid: false, error: 'File path too long' };
}
}
}
// Validate baseRef
if (baseRef !== undefined) {
if (typeof baseRef !== 'string') {
return { valid: false, error: 'baseRef must be a string' };
}
if (baseRef.length > MAX_BASE_REF_LENGTH) {
return { valid: false, error: 'baseRef is too long' };
}
}
// Validate categories
if (categories !== undefined) {
if (!Array.isArray(categories)) {
return { valid: false, error: 'categories must be an array' };
}
for (const category of categories) {
if (!VALID_CATEGORIES.includes(category)) {
return { valid: false, error: `Invalid category: ${category}` };
}
}
}
// Validate autoFix
if (autoFix !== undefined && typeof autoFix !== 'boolean') {
return { valid: false, error: 'autoFix must be a boolean' };
}
// Validate thinkingLevel
if (thinkingLevel !== undefined) {
if (!VALID_THINKING_LEVELS.includes(thinkingLevel)) {
return { valid: false, error: `Invalid thinkingLevel: ${thinkingLevel}` };
}
}
return { valid: true };
}
export function createTriggerHandler(codeReviewService: CodeReviewService) {
return async (req: Request, res: Response): Promise<void> => {
logger.info('========== /trigger endpoint called ==========');
try {
const body = req.body as TriggerRequestBody;
const { projectPath, files, baseRef, categories, autoFix, model, thinkingLevel } = body;
// Validate required parameters
if (!projectPath) {
res.status(400).json({
success: false,
error: 'projectPath is required',
});
return;
}
// SECURITY: Validate all input parameters
const validation = validateRequestBody(body);
if (!validation.valid) {
res.status(400).json({
success: false,
error: validation.error,
});
return;
}
// Check if a review is already running
if (isRunning()) {
res.status(409).json({
success: false,
error: 'A code review is already in progress',
});
return;
}
// Set up abort controller for cancellation
const abortController = new AbortController();
setRunningState(true, abortController, projectPath);
// Start the review in the background
codeReviewService
.executeReview({
projectPath,
files,
baseRef,
categories,
autoFix,
model,
thinkingLevel,
abortController,
})
.catch((error) => {
logError(error, 'Code review failed');
})
.finally(() => {
setRunningState(false, null, null);
});
// Return immediate response
res.json({
success: true,
message: 'Code review started',
});
} catch (error) {
logError(error, 'Trigger handler exception');
res.status(500).json({
success: false,
error: getErrorMessage(error),
});
}
};
}

View File

@@ -0,0 +1,90 @@
import { Router, Request, Response } from 'express';
import { CodexUsageService } from '../../services/codex-usage-service.js';
import { CodexModelCacheService } from '../../services/codex-model-cache-service.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('Codex');
export function createCodexRoutes(
usageService: CodexUsageService,
modelCacheService: CodexModelCacheService
): Router {
const router = Router();
// Get current usage (attempts to fetch from Codex CLI)
router.get('/usage', async (_req: Request, res: Response) => {
try {
// Check if Codex CLI is available first
const isAvailable = await usageService.isAvailable();
if (!isAvailable) {
// IMPORTANT: This endpoint is behind Automaker session auth already.
// Use a 200 + error payload for Codex CLI issues so the UI doesn't
// interpret it as an invalid Automaker session (401/403 triggers logout).
res.status(200).json({
error: 'Codex CLI not found',
message: "Please install Codex CLI and run 'codex login' to authenticate",
});
return;
}
const usage = await usageService.fetchUsageData();
res.json(usage);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
if (message.includes('not authenticated') || message.includes('login')) {
// Do NOT use 401/403 here: that status code is reserved for Automaker session auth.
res.status(200).json({
error: 'Authentication required',
message: "Please run 'codex login' to authenticate",
});
} else if (message.includes('not available') || message.includes('does not provide')) {
// This is the expected case - Codex doesn't provide usage stats
res.status(200).json({
error: 'Usage statistics not available',
message: message,
});
} else if (message.includes('timed out')) {
res.status(200).json({
error: 'Command timed out',
message: 'The Codex CLI took too long to respond',
});
} else {
logger.error('Error fetching usage:', error);
res.status(500).json({ error: message });
}
}
});
// Get available Codex models (cached)
router.get('/models', async (req: Request, res: Response) => {
try {
const forceRefresh = req.query.refresh === 'true';
const { models, cachedAt } = await modelCacheService.getModelsWithMetadata(forceRefresh);
if (models.length === 0) {
res.status(503).json({
success: false,
error: 'Codex CLI not available or not authenticated',
message: "Please install Codex CLI and run 'codex login' to authenticate",
});
return;
}
res.json({
success: true,
models,
cachedAt,
});
} catch (error) {
logger.error('Error fetching models:', error);
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({
success: false,
error: message,
});
}
});
return router;
}

View File

@@ -11,17 +11,18 @@
*/
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
import { PathNotAllowedError } from '@automaker/platform';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
} from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeFile');
@@ -49,31 +50,6 @@ interface DescribeFileErrorResponse {
error: string;
}
/**
* Extract text content from Claude SDK response messages
*/
async function extractTextFromStream(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stream: AsyncIterable<any>
): Promise<string> {
let responseText = '';
for await (const msg of stream) {
if (msg.type === 'assistant' && msg.message?.content) {
const blocks = msg.message.content as Array<{ type: string; text?: string }>;
for (const block of blocks) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
} else if (msg.type === 'result' && msg.subtype === 'success') {
responseText = msg.result || responseText;
}
}
return responseText;
}
/**
* Create the describe-file request handler
*
@@ -157,18 +133,17 @@ export function createDescribeFileHandler(
// Get the filename for context
const fileName = path.basename(resolvedPath);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[DescribeFile]');
// Build prompt with file content passed as structured data
// The file content is included directly, not via tool invocation
const instructionText = `Analyze the following file and provide a 1-2 sentence description suitable for use as context in an AI coding assistant. Focus on what the file contains, its purpose, and why an AI agent might want to use this context in the future (e.g., "API documentation for the authentication endpoints", "Configuration file for database connections", "Coding style guidelines for the project").
const prompt = `${prompts.contextDescription.describeFilePrompt}
Respond with ONLY the description text, no additional formatting, preamble, or explanation.
File: ${fileName}${truncated ? ' (truncated)' : ''}
File: ${fileName}${truncated ? ' (truncated)' : ''}`;
const promptContent = [
{ type: 'text' as const, text: instructionText },
{ type: 'text' as const, text: `\n\n--- FILE CONTENT ---\n${contentToAnalyze}` },
];
--- FILE CONTENT ---
${contentToAnalyze}`;
// Use the file's directory as the working directory
const cwd = path.dirname(resolvedPath);
@@ -190,66 +165,19 @@ File: ${fileName}${truncated ? ' (truncated)' : ''}`;
logger.info(`Resolved model: ${model}, thinkingLevel: ${thinkingLevel}`);
let description: string;
// Use simpleQuery - provider abstraction handles routing to correct provider
const result = await simpleQuery({
prompt,
model,
cwd,
maxTurns: 1,
allowedTools: [],
thinkingLevel,
readOnly: true, // File description only reads, doesn't write
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
});
// Route to appropriate provider based on model type
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);
}
const description = result.text;
if (!description || description.trim().length === 0) {
logger.warn('Received empty response from Claude');

View File

@@ -12,16 +12,17 @@
*/
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger, readImageAsBase64 } from '@automaker/utils';
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
import { resolvePhaseModel } from '@automaker/model-resolver';
import { createCustomOptions } from '../../../lib/sdk-options.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import * as secureFs from '../../../lib/secure-fs.js';
import * as path from 'path';
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
import {
getAutoLoadClaudeMdSetting,
getPromptCustomization,
} from '../../../lib/settings-helpers.js';
const logger = createLogger('DescribeImage');
@@ -178,57 +179,10 @@ function mapDescribeImageError(rawMessage: string | undefined): {
return baseResponse;
}
/**
* Extract text content from Claude SDK response messages and log high-signal stream events.
*/
async function extractTextFromStream(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
stream: AsyncIterable<any>,
requestId: string
): Promise<string> {
let responseText = '';
let messageCount = 0;
logger.info(`[${requestId}] [Stream] Begin reading SDK stream...`);
for await (const msg of stream) {
messageCount++;
const msgType = msg?.type;
const msgSubtype = msg?.subtype;
// Keep this concise but informative. Full error object is logged in catch blocks.
logger.info(
`[${requestId}] [Stream] #${messageCount} type=${String(msgType)} subtype=${String(msgSubtype ?? '')}`
);
if (msgType === 'assistant' && msg.message?.content) {
const blocks = msg.message.content as Array<{ type: string; text?: string }>;
logger.info(`[${requestId}] [Stream] assistant blocks=${blocks.length}`);
for (const block of blocks) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
}
if (msgType === 'result' && msgSubtype === 'success') {
if (typeof msg.result === 'string' && msg.result.length > 0) {
responseText = msg.result;
}
}
}
logger.info(
`[${requestId}] [Stream] End of stream. messages=${messageCount} textLength=${responseText.length}`
);
return responseText;
}
/**
* Create the describe-image request handler
*
* Uses Claude SDK query with multi-part content blocks to include the image (base64),
* Uses the provider abstraction with multi-part content blocks to include the image (base64),
* matching the agent runner behavior.
*
* @param settingsService - Optional settings service for loading autoLoadClaudeMd setting
@@ -309,27 +263,6 @@ export function createDescribeImageHandler(
`[${requestId}] image meta filename=${imageData.filename} mime=${imageData.mimeType} base64Len=${base64Length} estBytes=${estimatedBytes}`
);
// Build multi-part prompt with image block (no Read tool required)
const instructionText =
`Describe this image in 1-2 sentences suitable for use as context in an AI coding assistant. ` +
`Focus on what the image shows and its purpose (e.g., "UI mockup showing login form with email/password fields", ` +
`"Architecture diagram of microservices", "Screenshot of error message in terminal").\n\n` +
`Respond with ONLY the description text, no additional formatting, preamble, or explanation.`;
const promptContent = [
{ type: 'text' as const, text: instructionText },
{
type: 'image' as const,
source: {
type: 'base64' as const,
media_type: imageData.mimeType,
data: imageData.base64,
},
},
];
logger.info(`[${requestId}] Built multi-part prompt blocks=${promptContent.length}`);
const cwd = path.dirname(actualPath);
logger.info(`[${requestId}] Using cwd=${cwd}`);
@@ -348,84 +281,58 @@ export function createDescribeImageHandler(
logger.info(`[${requestId}] Using model: ${model}`);
let description: string;
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[DescribeImage]');
// Build the instruction text from centralized prompts
const instructionText = prompts.contextDescription.describeImagePrompt;
// Build prompt based on provider capability
// Some providers (like Cursor) may not support image content blocks
let prompt: string | Array<{ type: string; text?: string; source?: object }>;
// Route to appropriate provider based on model type
if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
// Note: Cursor may have limited support for image content blocks
logger.info(`[${requestId}] Using Cursor provider for model: ${model}`);
const provider = ProviderFactory.getProviderForModel(model);
// Build prompt with image reference for Cursor
// 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;
// Cursor may not support base64 image blocks directly
// Use text prompt with image path reference
logger.info(`[${requestId}] Using text prompt for Cursor model`);
prompt = `${instructionText}\n\nImage file: ${actualPath}\nMIME type: ${imageData.mimeType}`;
} 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}`);
// Claude and other vision-capable models support multi-part prompts with images
logger.info(`[${requestId}] Using multi-part prompt with image block`);
prompt = [
{ type: 'text', text: instructionText },
{
type: 'image',
source: {
type: 'base64',
media_type: imageData.mimeType,
data: imageData.base64,
},
},
];
}
logger.info(`[${requestId}] Calling simpleQuery...`);
const queryStart = Date.now();
// Use simpleQuery - provider abstraction handles routing
const result = await simpleQuery({
prompt,
model,
cwd,
maxTurns: 1,
allowedTools: isCursorModel(model) ? ['Read'] : [], // Allow Read for Cursor to read image if needed
thinkingLevel,
readOnly: true, // Image description only reads, doesn't write
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
});
logger.info(`[${requestId}] simpleQuery completed in ${Date.now() - queryStart}ms`);
const description = result.text;
if (!description || description.trim().length === 0) {
logger.warn(`[${requestId}] Received empty response from Claude`);
logger.warn(`[${requestId}] Received empty response from AI`);
const response: DescribeImageErrorResponse = {
success: false,
error: 'Failed to generate description - empty response',

View File

@@ -1,21 +1,16 @@
/**
* POST /enhance-prompt endpoint - Enhance user input text
*
* Uses Claude AI or Cursor to enhance text based on the specified enhancement mode.
* Supports modes: improve, technical, simplify, acceptance
* Uses the provider abstraction to enhance text based on the specified
* enhancement mode. Works with any configured provider (Claude, Cursor, etc.).
* Supports modes: improve, technical, simplify, acceptance, ux-reviewer
*/
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { resolveModelString } from '@automaker/model-resolver';
import {
CLAUDE_MODEL_MAP,
isCursorModel,
ThinkingLevel,
getThinkingTokenBudget,
} from '@automaker/types';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { CLAUDE_MODEL_MAP, type ThinkingLevel } from '@automaker/types';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import type { SettingsService } from '../../../services/settings-service.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
import {
@@ -36,7 +31,7 @@ interface EnhanceRequestBody {
enhancementMode: string;
/** Optional model override */
model?: string;
/** Optional thinking level for Claude models (ignored for Cursor models) */
/** Optional thinking level for Claude models */
thinkingLevel?: ThinkingLevel;
}
@@ -56,74 +51,6 @@ interface EnhanceErrorResponse {
error: string;
}
/**
* Extract text content from Claude SDK response messages
*
* @param stream - The async iterable from the query function
* @returns The extracted text content
*/
async function extractTextFromStream(
stream: AsyncIterable<{
type: string;
subtype?: string;
result?: string;
message?: {
content?: Array<{ type: string; text?: string }>;
};
}>
): Promise<string> {
let responseText = '';
for await (const msg of stream) {
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') {
responseText = msg.result || 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
*
@@ -185,13 +112,13 @@ export function createEnhanceHandler(
technical: prompts.enhancement.technicalSystemPrompt,
simplify: prompts.enhancement.simplifySystemPrompt,
acceptance: prompts.enhancement.acceptanceSystemPrompt,
'ux-reviewer': prompts.enhancement.uxReviewerSystemPrompt,
};
const systemPrompt = systemPromptMap[validMode];
logger.debug(`Using ${validMode} system prompt (length: ${systemPrompt.length} chars)`);
// Build the user prompt with few-shot examples
// This helps the model understand this is text transformation, not a coding task
const userPrompt = buildUserPrompt(validMode, trimmedText, true);
// Resolve the model - use the passed model, default to sonnet for quality
@@ -199,40 +126,20 @@ export function createEnhanceHandler(
logger.debug(`Using model: ${resolvedModel}`);
let enhancedText: string;
// Use simpleQuery - provider abstraction handles routing to correct provider
// The system prompt is combined with user prompt since some providers
// don't have a separate system prompt concept
const result = await simpleQuery({
prompt: `${systemPrompt}\n\n${userPrompt}`,
model: resolvedModel,
cwd: process.cwd(), // Enhancement doesn't need a specific working directory
maxTurns: 1,
allowedTools: [],
thinkingLevel,
readOnly: true, // Prompt enhancement only generates text, doesn't write files
});
// Route to appropriate provider based on model
if (isCursorModel(resolvedModel)) {
// 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,
systemPrompt,
maxTurns: 1,
allowedTools: [],
permissionMode: 'acceptEdits',
};
if (maxThinkingTokens) {
queryOptions.maxThinkingTokens = maxThinkingTokens;
}
const stream = query({
prompt: userPrompt,
options: queryOptions,
});
enhancedText = await extractTextFromStream(stream);
}
const enhancedText = result.text;
if (!enhancedText || enhancedText.trim().length === 0) {
logger.warn('Received empty response from AI');

View File

@@ -0,0 +1,19 @@
/**
* Common utilities for event history routes
*/
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
/** Logger instance for event history operations */
export const logger = createLogger('EventHistory');
/**
* 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,68 @@
/**
* Event History routes - HTTP API for event history management
*
* Provides endpoints for:
* - Listing events with filtering
* - Getting individual event details
* - Deleting events
* - Clearing all events
* - Replaying events to test hooks
*
* Mounted at /api/event-history in the main server.
*/
import { Router } from 'express';
import type { EventHistoryService } from '../../services/event-history-service.js';
import type { SettingsService } from '../../services/settings-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createListHandler } from './routes/list.js';
import { createGetHandler } from './routes/get.js';
import { createDeleteHandler } from './routes/delete.js';
import { createClearHandler } from './routes/clear.js';
import { createReplayHandler } from './routes/replay.js';
/**
* Create event history router with all endpoints
*
* Endpoints:
* - POST /list - List events with optional filtering
* - POST /get - Get a single event by ID
* - POST /delete - Delete an event by ID
* - POST /clear - Clear all events for a project
* - POST /replay - Replay an event to trigger hooks
*
* @param eventHistoryService - Instance of EventHistoryService
* @param settingsService - Instance of SettingsService (for replay)
* @returns Express Router configured with all event history endpoints
*/
export function createEventHistoryRoutes(
eventHistoryService: EventHistoryService,
settingsService: SettingsService
): Router {
const router = Router();
// List events with filtering
router.post('/list', validatePathParams('projectPath'), createListHandler(eventHistoryService));
// Get single event
router.post('/get', validatePathParams('projectPath'), createGetHandler(eventHistoryService));
// Delete event
router.post(
'/delete',
validatePathParams('projectPath'),
createDeleteHandler(eventHistoryService)
);
// Clear all events
router.post('/clear', validatePathParams('projectPath'), createClearHandler(eventHistoryService));
// Replay event
router.post(
'/replay',
validatePathParams('projectPath'),
createReplayHandler(eventHistoryService, settingsService)
);
return router;
}

View File

@@ -0,0 +1,33 @@
/**
* POST /api/event-history/clear - Clear all events for a project
*
* Request body: { projectPath: string }
* Response: { success: true, cleared: number }
*/
import type { Request, Response } from 'express';
import type { EventHistoryService } from '../../../services/event-history-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createClearHandler(eventHistoryService: EventHistoryService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const cleared = await eventHistoryService.clearEvents(projectPath);
res.json({
success: true,
cleared,
});
} catch (error) {
logError(error, 'Clear events failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,43 @@
/**
* POST /api/event-history/delete - Delete an event by ID
*
* Request body: { projectPath: string, eventId: string }
* Response: { success: true } or { success: false, error: string }
*/
import type { Request, Response } from 'express';
import type { EventHistoryService } from '../../../services/event-history-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(eventHistoryService: EventHistoryService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, eventId } = req.body as {
projectPath: string;
eventId: string;
};
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!eventId || typeof eventId !== 'string') {
res.status(400).json({ success: false, error: 'eventId is required' });
return;
}
const deleted = await eventHistoryService.deleteEvent(projectPath, eventId);
if (!deleted) {
res.status(404).json({ success: false, error: 'Event not found' });
return;
}
res.json({ success: true });
} catch (error) {
logError(error, 'Delete event failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,46 @@
/**
* POST /api/event-history/get - Get a single event by ID
*
* Request body: { projectPath: string, eventId: string }
* Response: { success: true, event: StoredEvent } or { success: false, error: string }
*/
import type { Request, Response } from 'express';
import type { EventHistoryService } from '../../../services/event-history-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createGetHandler(eventHistoryService: EventHistoryService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, eventId } = req.body as {
projectPath: string;
eventId: string;
};
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!eventId || typeof eventId !== 'string') {
res.status(400).json({ success: false, error: 'eventId is required' });
return;
}
const event = await eventHistoryService.getEvent(projectPath, eventId);
if (!event) {
res.status(404).json({ success: false, error: 'Event not found' });
return;
}
res.json({
success: true,
event,
});
} catch (error) {
logError(error, 'Get event failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,53 @@
/**
* POST /api/event-history/list - List events for a project
*
* Request body: {
* projectPath: string,
* filter?: {
* trigger?: EventHookTrigger,
* featureId?: string,
* since?: string,
* until?: string,
* limit?: number,
* offset?: number
* }
* }
* Response: { success: true, events: StoredEventSummary[], total: number }
*/
import type { Request, Response } from 'express';
import type { EventHistoryService } from '../../../services/event-history-service.js';
import type { EventHistoryFilter } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createListHandler(eventHistoryService: EventHistoryService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, filter } = req.body as {
projectPath: string;
filter?: EventHistoryFilter;
};
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const events = await eventHistoryService.getEvents(projectPath, filter);
const total = await eventHistoryService.getEventCount(projectPath, {
...filter,
limit: undefined,
offset: undefined,
});
res.json({
success: true,
events,
total,
});
} catch (error) {
logError(error, 'List events failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,234 @@
/**
* POST /api/event-history/replay - Replay an event to trigger hooks
*
* Request body: {
* projectPath: string,
* eventId: string,
* hookIds?: string[] // Optional: specific hooks to run (if not provided, runs all enabled matching hooks)
* }
* Response: { success: true, result: EventReplayResult }
*/
import type { Request, Response } from 'express';
import type { EventHistoryService } from '../../../services/event-history-service.js';
import type { SettingsService } from '../../../services/settings-service.js';
import type { EventReplayResult, EventReplayHookResult, EventHook } from '@automaker/types';
import { exec } from 'child_process';
import { promisify } from 'util';
import { getErrorMessage, logError, logger } from '../common.js';
const execAsync = promisify(exec);
/** Default timeout for shell commands (30 seconds) */
const DEFAULT_SHELL_TIMEOUT = 30000;
/** Default timeout for HTTP requests (10 seconds) */
const DEFAULT_HTTP_TIMEOUT = 10000;
interface HookContext {
featureId?: string;
featureName?: string;
projectPath?: string;
projectName?: string;
error?: string;
errorType?: string;
timestamp: string;
eventType: string;
}
/**
* Substitute {{variable}} placeholders in a string
*/
function substituteVariables(template: string, context: HookContext): string {
return template.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
const value = context[variable as keyof HookContext];
if (value === undefined || value === null) {
return '';
}
return String(value);
});
}
/**
* Execute a single hook and return the result
*/
async function executeHook(hook: EventHook, context: HookContext): Promise<EventReplayHookResult> {
const hookName = hook.name || hook.id;
const startTime = Date.now();
try {
if (hook.action.type === 'shell') {
const command = substituteVariables(hook.action.command, context);
const timeout = hook.action.timeout || DEFAULT_SHELL_TIMEOUT;
logger.info(`Replaying shell hook "${hookName}": ${command}`);
await execAsync(command, {
timeout,
maxBuffer: 1024 * 1024,
});
return {
hookId: hook.id,
hookName: hook.name,
success: true,
durationMs: Date.now() - startTime,
};
} else if (hook.action.type === 'http') {
const url = substituteVariables(hook.action.url, context);
const method = hook.action.method || 'POST';
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (hook.action.headers) {
for (const [key, value] of Object.entries(hook.action.headers)) {
headers[key] = substituteVariables(value, context);
}
}
let body: string | undefined;
if (hook.action.body) {
body = substituteVariables(hook.action.body, context);
} else if (method !== 'GET') {
body = JSON.stringify({
eventType: context.eventType,
timestamp: context.timestamp,
featureId: context.featureId,
projectPath: context.projectPath,
projectName: context.projectName,
error: context.error,
});
}
logger.info(`Replaying HTTP hook "${hookName}": ${method} ${url}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_HTTP_TIMEOUT);
const response = await fetch(url, {
method,
headers,
body: method !== 'GET' ? body : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
return {
hookId: hook.id,
hookName: hook.name,
success: false,
error: `HTTP ${response.status}: ${response.statusText}`,
durationMs: Date.now() - startTime,
};
}
return {
hookId: hook.id,
hookName: hook.name,
success: true,
durationMs: Date.now() - startTime,
};
}
return {
hookId: hook.id,
hookName: hook.name,
success: false,
error: 'Unknown hook action type',
durationMs: Date.now() - startTime,
};
} catch (error) {
const errorMessage =
error instanceof Error
? error.name === 'AbortError'
? 'Request timed out'
: error.message
: String(error);
return {
hookId: hook.id,
hookName: hook.name,
success: false,
error: errorMessage,
durationMs: Date.now() - startTime,
};
}
}
export function createReplayHandler(
eventHistoryService: EventHistoryService,
settingsService: SettingsService
) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, eventId, hookIds } = req.body as {
projectPath: string;
eventId: string;
hookIds?: string[];
};
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
if (!eventId || typeof eventId !== 'string') {
res.status(400).json({ success: false, error: 'eventId is required' });
return;
}
// Get the event
const event = await eventHistoryService.getEvent(projectPath, eventId);
if (!event) {
res.status(404).json({ success: false, error: 'Event not found' });
return;
}
// Get hooks from settings
const settings = await settingsService.getGlobalSettings();
let hooks = settings.eventHooks || [];
// Filter to matching trigger and enabled hooks
hooks = hooks.filter((h) => h.enabled && h.trigger === event.trigger);
// If specific hook IDs requested, filter to those
if (hookIds && hookIds.length > 0) {
hooks = hooks.filter((h) => hookIds.includes(h.id));
}
// Build context for variable substitution
const context: HookContext = {
featureId: event.featureId,
featureName: event.featureName,
projectPath: event.projectPath,
projectName: event.projectName,
error: event.error,
errorType: event.errorType,
timestamp: event.timestamp,
eventType: event.trigger,
};
// Execute all hooks in parallel
const hookResults = await Promise.all(hooks.map((hook) => executeHook(hook, context)));
const result: EventReplayResult = {
eventId,
hooksTriggered: hooks.length,
hookResults,
};
logger.info(`Replayed event ${eventId}: ${hooks.length} hooks triggered`);
res.json({
success: true,
result,
});
} catch (error) {
logError(error, 'Replay event failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -4,26 +4,48 @@
import { Router } from 'express';
import { FeatureLoader } from '../../services/feature-loader.js';
import type { SettingsService } from '../../services/settings-service.js';
import type { EventEmitter } from '../../lib/events.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createListHandler } from './routes/list.js';
import { createGetHandler } from './routes/get.js';
import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from './routes/update.js';
import { createBulkUpdateHandler } from './routes/bulk-update.js';
import { createBulkDeleteHandler } from './routes/bulk-delete.js';
import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler, createRawOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from './routes/generate-title.js';
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
export function createFeaturesRoutes(
featureLoader: FeatureLoader,
settingsService?: SettingsService,
events?: EventEmitter
): Router {
const router = Router();
router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader));
router.post('/create', validatePathParams('projectPath'), createCreateHandler(featureLoader));
router.post(
'/create',
validatePathParams('projectPath'),
createCreateHandler(featureLoader, events)
);
router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
router.post(
'/bulk-update',
validatePathParams('projectPath'),
createBulkUpdateHandler(featureLoader)
);
router.post(
'/bulk-delete',
validatePathParams('projectPath'),
createBulkDeleteHandler(featureLoader)
);
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post('/raw-output', createRawOutputHandler(featureLoader));
router.post('/generate-title', createGenerateTitleHandler());
router.post('/generate-title', createGenerateTitleHandler(settingsService));
return router;
}

View File

@@ -0,0 +1,69 @@
/**
* POST /bulk-delete endpoint - Delete multiple features at once
*/
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from '../common.js';
interface BulkDeleteRequest {
projectPath: string;
featureIds: string[];
}
interface BulkDeleteResult {
featureId: string;
success: boolean;
error?: string;
}
export function createBulkDeleteHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureIds } = req.body as BulkDeleteRequest;
if (!projectPath || !featureIds || !Array.isArray(featureIds) || featureIds.length === 0) {
res.status(400).json({
success: false,
error: 'projectPath and featureIds (non-empty array) are required',
});
return;
}
// Process in parallel batches of 20 for efficiency
const BATCH_SIZE = 20;
const results: BulkDeleteResult[] = [];
for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
const batch = featureIds.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.all(
batch.map(async (featureId) => {
const success = await featureLoader.delete(projectPath, featureId);
if (success) {
return { featureId, success: true };
}
return {
featureId,
success: false,
error: 'Deletion failed. Check server logs for details.',
};
})
);
results.push(...batchResults);
}
const successCount = results.reduce((count, r) => count + (r.success ? 1 : 0), 0);
const failureCount = results.length - successCount;
res.json({
success: failureCount === 0,
deletedCount: successCount,
failedCount: failureCount,
results,
});
} catch (error) {
logError(error, 'Bulk delete features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,94 @@
/**
* POST /bulk-update endpoint - Update multiple features at once
*/
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
interface BulkUpdateRequest {
projectPath: string;
featureIds: string[];
updates: Partial<Feature>;
}
interface BulkUpdateResult {
featureId: string;
success: boolean;
error?: string;
}
export function createBulkUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureIds, updates } = req.body as BulkUpdateRequest;
if (!projectPath || !featureIds || !Array.isArray(featureIds) || featureIds.length === 0) {
res.status(400).json({
success: false,
error: 'projectPath and featureIds (non-empty array) are required',
});
return;
}
if (!updates || Object.keys(updates).length === 0) {
res.status(400).json({
success: false,
error: 'updates object with at least one field is required',
});
return;
}
const results: BulkUpdateResult[] = [];
const updatedFeatures: Feature[] = [];
// Process in parallel batches of 20 for efficiency
const BATCH_SIZE = 20;
for (let i = 0; i < featureIds.length; i += BATCH_SIZE) {
const batch = featureIds.slice(i, i + BATCH_SIZE);
const batchResults = await Promise.all(
batch.map(async (featureId) => {
try {
const updated = await featureLoader.update(projectPath, featureId, updates);
return { featureId, success: true as const, feature: updated };
} catch (error) {
return {
featureId,
success: false as const,
error: getErrorMessage(error),
};
}
})
);
for (const result of batchResults) {
if (result.success) {
results.push({ featureId: result.featureId, success: true });
updatedFeatures.push(result.feature);
} else {
results.push({
featureId: result.featureId,
success: false,
error: result.error,
});
}
}
}
const successCount = results.filter((r) => r.success).length;
const failureCount = results.filter((r) => !r.success).length;
res.json({
success: failureCount === 0,
updatedCount: successCount,
failedCount: failureCount,
results,
features: updatedFeatures,
});
} catch (error) {
logError(error, 'Bulk update features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -4,10 +4,11 @@
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import type { EventEmitter } from '../../../lib/events.js';
import type { Feature } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(featureLoader: FeatureLoader) {
export function createCreateHandler(featureLoader: FeatureLoader, events?: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, feature } = req.body as {
@@ -23,7 +24,30 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
return;
}
// Check for duplicate title if title is provided
if (feature.title && feature.title.trim()) {
const duplicate = await featureLoader.findDuplicateTitle(projectPath, feature.title);
if (duplicate) {
res.status(409).json({
success: false,
error: `A feature with title "${feature.title}" already exists`,
duplicateFeatureId: duplicate.id,
});
return;
}
}
const created = await featureLoader.create(projectPath, feature);
// Emit feature_created event for hooks
if (events) {
events.emit('feature:created', {
featureId: created.id,
featureName: created.name,
projectPath,
});
}
res.json({ success: true, feature: created });
} catch (error) {
logError(error, 'Create feature failed');

View File

@@ -1,13 +1,16 @@
/**
* POST /features/generate-title endpoint - Generate a concise title from description
*
* Uses Claude Haiku to generate a short, descriptive title from feature description.
* Uses the provider abstraction to generate a short, descriptive title
* from a feature description. Works with any configured provider (Claude, Cursor, etc.).
*/
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
import { simpleQuery } from '../../../providers/simple-query-service.js';
import type { SettingsService } from '../../../services/settings-service.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
const logger = createLogger('GenerateTitle');
@@ -25,43 +28,9 @@ interface GenerateTitleErrorResponse {
error: string;
}
const SYSTEM_PROMPT = `You are a title generator. Your task is to create a concise, descriptive title (5-10 words max) for a software feature based on its description.
Rules:
- Output ONLY the title, nothing else
- Keep it short and action-oriented (e.g., "Add dark mode toggle", "Fix login validation")
- Start with a verb when possible (Add, Fix, Update, Implement, Create, etc.)
- No quotes, periods, or extra formatting
- Capture the essence of the feature in a scannable way`;
async function extractTextFromStream(
stream: AsyncIterable<{
type: string;
subtype?: string;
result?: string;
message?: {
content?: Array<{ type: string; text?: string }>;
};
}>
): Promise<string> {
let responseText = '';
for await (const msg of stream) {
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') {
responseText = msg.result || responseText;
}
}
return responseText;
}
export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
export function createGenerateTitleHandler(
settingsService?: SettingsService
): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
try {
const { description } = req.body as GenerateTitleRequestBody;
@@ -87,23 +56,25 @@ export function createGenerateTitleHandler(): (req: Request, res: Response) => P
logger.info(`Generating title for description: ${trimmedDescription.substring(0, 50)}...`);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[GenerateTitle]');
const systemPrompt = prompts.titleGeneration.systemPrompt;
const userPrompt = `Generate a concise title for this feature:\n\n${trimmedDescription}`;
const stream = query({
prompt: userPrompt,
options: {
model: CLAUDE_MODEL_MAP.haiku,
systemPrompt: SYSTEM_PROMPT,
maxTurns: 1,
allowedTools: [],
permissionMode: 'default',
},
// Use simpleQuery - provider abstraction handles all the streaming/extraction
const result = await simpleQuery({
prompt: `${systemPrompt}\n\n${userPrompt}`,
model: CLAUDE_MODEL_MAP.haiku,
cwd: process.cwd(),
maxTurns: 1,
allowedTools: [],
});
const title = await extractTextFromStream(stream);
const title = result.text;
if (!title || title.trim().length === 0) {
logger.warn('Received empty response from Claude');
logger.warn('Received empty response from AI');
const response: GenerateTitleErrorResponse = {
success: false,
error: 'Failed to generate title - empty response',

View File

@@ -4,16 +4,32 @@
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature } from '@automaker/types';
import type { Feature, FeatureStatus } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('features/update');
// Statuses that should trigger syncing to app_spec.txt
const SYNC_TRIGGER_STATUSES: FeatureStatus[] = ['verified', 'completed'];
export function createUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, updates } = req.body as {
const {
projectPath,
featureId,
updates,
descriptionHistorySource,
enhancementMode,
preEnhancementDescription,
} = req.body as {
projectPath: string;
featureId: string;
updates: Partial<Feature>;
descriptionHistorySource?: 'enhance' | 'edit';
enhancementMode?: 'improve' | 'technical' | 'simplify' | 'acceptance' | 'ux-reviewer';
preEnhancementDescription?: string;
};
if (!projectPath || !featureId || !updates) {
@@ -24,7 +40,52 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
return;
}
const updated = await featureLoader.update(projectPath, featureId, updates);
// Check for duplicate title if title is being updated
if (updates.title && updates.title.trim()) {
const duplicate = await featureLoader.findDuplicateTitle(
projectPath,
updates.title,
featureId // Exclude the current feature from duplicate check
);
if (duplicate) {
res.status(409).json({
success: false,
error: `A feature with title "${updates.title}" already exists`,
duplicateFeatureId: duplicate.id,
});
return;
}
}
// Get the current feature to detect status changes
const currentFeature = await featureLoader.get(projectPath, featureId);
const previousStatus = currentFeature?.status as FeatureStatus | undefined;
const newStatus = updates.status as FeatureStatus | undefined;
const updated = await featureLoader.update(
projectPath,
featureId,
updates,
descriptionHistorySource,
enhancementMode,
preEnhancementDescription
);
// Trigger sync to app_spec.txt when status changes to verified or completed
if (newStatus && SYNC_TRIGGER_STATUSES.includes(newStatus) && previousStatus !== newStatus) {
try {
const synced = await featureLoader.syncFeatureToAppSpec(projectPath, updated);
if (synced) {
logger.info(
`Synced feature "${updated.title || updated.id}" to app_spec.txt on status change to ${newStatus}`
);
}
} catch (syncError) {
// Log the sync error but don't fail the update operation
logger.error(`Failed to sync feature to app_spec.txt:`, syncError);
}
}
res.json({ success: true, feature: updated });
} catch (error) {
logError(error, 'Update feature failed');

View File

@@ -5,6 +5,43 @@
import type { Request, Response } from 'express';
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
const GIT_REMOTE_ORIGIN_COMMAND = 'git remote get-url origin';
const GH_REPO_VIEW_COMMAND = 'gh repo view --json name,owner';
const GITHUB_REPO_URL_PREFIX = 'https://github.com/';
const GITHUB_HTTPS_REMOTE_REGEX = /https:\/\/github\.com\/([^/]+)\/([^/.]+)/;
const GITHUB_SSH_REMOTE_REGEX = /git@github\.com:([^/]+)\/([^/.]+)/;
interface GhRepoViewResponse {
name?: string;
owner?: {
login?: string;
};
}
async function resolveRepoFromGh(projectPath: string): Promise<{
owner: string;
repo: string;
} | null> {
try {
const { stdout } = await execAsync(GH_REPO_VIEW_COMMAND, {
cwd: projectPath,
env: execEnv,
});
const data = JSON.parse(stdout) as GhRepoViewResponse;
const owner = typeof data.owner?.login === 'string' ? data.owner.login : null;
const repo = typeof data.name === 'string' ? data.name : null;
if (!owner || !repo) {
return null;
}
return { owner, repo };
} catch {
return null;
}
}
export interface GitHubRemoteStatus {
hasGitHubRemote: boolean;
remoteUrl: string | null;
@@ -21,19 +58,38 @@ export async function checkGitHubRemote(projectPath: string): Promise<GitHubRemo
};
try {
// Get the remote URL (origin by default)
const { stdout } = await execAsync('git remote get-url origin', {
cwd: projectPath,
env: execEnv,
});
let remoteUrl = '';
try {
// Get the remote URL (origin by default)
const { stdout } = await execAsync(GIT_REMOTE_ORIGIN_COMMAND, {
cwd: projectPath,
env: execEnv,
});
remoteUrl = stdout.trim();
status.remoteUrl = remoteUrl || null;
} catch {
// Ignore missing origin remote
}
const remoteUrl = stdout.trim();
status.remoteUrl = remoteUrl;
const ghRepo = await resolveRepoFromGh(projectPath);
if (ghRepo) {
status.hasGitHubRemote = true;
status.owner = ghRepo.owner;
status.repo = ghRepo.repo;
if (!status.remoteUrl) {
status.remoteUrl = `${GITHUB_REPO_URL_PREFIX}${ghRepo.owner}/${ghRepo.repo}`;
}
return status;
}
// Check if it's a GitHub URL
// Formats: https://github.com/owner/repo.git, git@github.com:owner/repo.git
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)/);
const sshMatch = remoteUrl.match(/git@github\.com:([^/]+)\/([^/.]+)/);
if (!remoteUrl) {
return status;
}
const httpsMatch = remoteUrl.match(GITHUB_HTTPS_REMOTE_REGEX);
const sshMatch = remoteUrl.match(GITHUB_SSH_REMOTE_REGEX);
const match = httpsMatch || sshMatch;
if (match) {

View File

@@ -25,19 +25,24 @@ interface GraphQLComment {
updatedAt: string;
}
interface GraphQLCommentConnection {
totalCount: number;
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
nodes: GraphQLComment[];
}
interface GraphQLIssueOrPullRequest {
__typename: 'Issue' | 'PullRequest';
comments: GraphQLCommentConnection;
}
interface GraphQLResponse {
data?: {
repository?: {
issue?: {
comments: {
totalCount: number;
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
nodes: GraphQLComment[];
};
};
issueOrPullRequest?: GraphQLIssueOrPullRequest | null;
};
};
errors?: Array<{ message: string }>;
@@ -45,6 +50,7 @@ interface GraphQLResponse {
/** Timeout for GitHub API requests in milliseconds */
const GITHUB_API_TIMEOUT_MS = 30000;
const COMMENTS_PAGE_SIZE = 50;
/**
* Validate cursor format (GraphQL cursors are typically base64 strings)
@@ -54,7 +60,7 @@ function isValidCursor(cursor: string): boolean {
}
/**
* Fetch comments for a specific issue using GitHub GraphQL API
* Fetch comments for a specific issue or pull request using GitHub GraphQL API
*/
async function fetchIssueComments(
projectPath: string,
@@ -70,24 +76,52 @@ async function fetchIssueComments(
// Use GraphQL variables instead of string interpolation for safety
const query = `
query GetIssueComments($owner: String!, $repo: String!, $issueNumber: Int!, $cursor: String) {
query GetIssueComments(
$owner: String!
$repo: String!
$issueNumber: Int!
$cursor: String
$pageSize: Int!
) {
repository(owner: $owner, name: $repo) {
issue(number: $issueNumber) {
comments(first: 50, after: $cursor) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
id
author {
login
avatarUrl
issueOrPullRequest(number: $issueNumber) {
__typename
... on Issue {
comments(first: $pageSize, after: $cursor) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
id
author {
login
avatarUrl
}
body
createdAt
updatedAt
}
}
}
... on PullRequest {
comments(first: $pageSize, after: $cursor) {
totalCount
pageInfo {
hasNextPage
endCursor
}
nodes {
id
author {
login
avatarUrl
}
body
createdAt
updatedAt
}
body
createdAt
updatedAt
}
}
}
@@ -99,6 +133,7 @@ async function fetchIssueComments(
repo,
issueNumber,
cursor: cursor || null,
pageSize: COMMENTS_PAGE_SIZE,
};
const requestBody = JSON.stringify({ query, variables });
@@ -140,10 +175,10 @@ async function fetchIssueComments(
throw new Error(response.errors[0].message);
}
const commentsData = response.data?.repository?.issue?.comments;
const commentsData = response.data?.repository?.issueOrPullRequest?.comments;
if (!commentsData) {
throw new Error('Issue not found or no comments data available');
throw new Error('Issue or pull request not found or no comments data available');
}
const comments: GitHubComment[] = commentsData.nodes.map((node) => ({

View File

@@ -9,6 +9,17 @@ import { checkGitHubRemote } from './check-github-remote.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger('ListIssues');
const OPEN_ISSUES_LIMIT = 100;
const CLOSED_ISSUES_LIMIT = 50;
const ISSUE_LIST_FIELDS = 'number,title,state,author,createdAt,labels,url,body,assignees';
const ISSUE_STATE_OPEN = 'open';
const ISSUE_STATE_CLOSED = 'closed';
const GH_ISSUE_LIST_COMMAND = 'gh issue list';
const GH_STATE_FLAG = '--state';
const GH_JSON_FLAG = '--json';
const GH_LIMIT_FLAG = '--limit';
const LINKED_PRS_BATCH_SIZE = 20;
const LINKED_PRS_TIMELINE_ITEMS = 10;
export interface GitHubLabel {
name: string;
@@ -69,34 +80,68 @@ async function fetchLinkedPRs(
// Build GraphQL query for batch fetching linked PRs
// We fetch up to 20 issues at a time to avoid query limits
const batchSize = 20;
for (let i = 0; i < issueNumbers.length; i += batchSize) {
const batch = issueNumbers.slice(i, i + batchSize);
for (let i = 0; i < issueNumbers.length; i += LINKED_PRS_BATCH_SIZE) {
const batch = issueNumbers.slice(i, i + LINKED_PRS_BATCH_SIZE);
const issueQueries = batch
.map(
(num, idx) => `
issue${idx}: issue(number: ${num}) {
number
timelineItems(first: 10, itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]) {
nodes {
... on CrossReferencedEvent {
source {
... on PullRequest {
number
title
state
url
issue${idx}: issueOrPullRequest(number: ${num}) {
... on Issue {
number
timelineItems(
first: ${LINKED_PRS_TIMELINE_ITEMS}
itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]
) {
nodes {
... on CrossReferencedEvent {
source {
... on PullRequest {
number
title
state
url
}
}
}
... on ConnectedEvent {
subject {
... on PullRequest {
number
title
state
url
}
}
}
}
... on ConnectedEvent {
subject {
... on PullRequest {
number
title
state
url
}
}
... on PullRequest {
number
timelineItems(
first: ${LINKED_PRS_TIMELINE_ITEMS}
itemTypes: [CROSS_REFERENCED_EVENT, CONNECTED_EVENT]
) {
nodes {
... on CrossReferencedEvent {
source {
... on PullRequest {
number
title
state
url
}
}
}
... on ConnectedEvent {
subject {
... on PullRequest {
number
title
state
url
}
}
}
}
@@ -213,16 +258,35 @@ export function createListIssuesHandler() {
}
// Fetch open and closed issues in parallel (now including assignees)
const repoQualifier =
remoteStatus.owner && remoteStatus.repo ? `${remoteStatus.owner}/${remoteStatus.repo}` : '';
const repoFlag = repoQualifier ? `-R ${repoQualifier}` : '';
const [openResult, closedResult] = await Promise.all([
execAsync(
'gh issue list --state open --json number,title,state,author,createdAt,labels,url,body,assignees --limit 100',
[
GH_ISSUE_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${ISSUE_STATE_OPEN}`,
`${GH_JSON_FLAG} ${ISSUE_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${OPEN_ISSUES_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{
cwd: projectPath,
env: execEnv,
}
),
execAsync(
'gh issue list --state closed --json number,title,state,author,createdAt,labels,url,body,assignees --limit 50',
[
GH_ISSUE_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${ISSUE_STATE_CLOSED}`,
`${GH_JSON_FLAG} ${ISSUE_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${CLOSED_ISSUES_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{
cwd: projectPath,
env: execEnv,

View File

@@ -6,6 +6,17 @@ import type { Request, Response } from 'express';
import { execAsync, execEnv, getErrorMessage, logError } from './common.js';
import { checkGitHubRemote } from './check-github-remote.js';
const OPEN_PRS_LIMIT = 100;
const MERGED_PRS_LIMIT = 50;
const PR_LIST_FIELDS =
'number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body';
const PR_STATE_OPEN = 'open';
const PR_STATE_MERGED = 'merged';
const GH_PR_LIST_COMMAND = 'gh pr list';
const GH_STATE_FLAG = '--state';
const GH_JSON_FLAG = '--json';
const GH_LIMIT_FLAG = '--limit';
export interface GitHubLabel {
name: string;
color: string;
@@ -57,16 +68,36 @@ export function createListPRsHandler() {
return;
}
const repoQualifier =
remoteStatus.owner && remoteStatus.repo ? `${remoteStatus.owner}/${remoteStatus.repo}` : '';
const repoFlag = repoQualifier ? `-R ${repoQualifier}` : '';
const [openResult, mergedResult] = await Promise.all([
execAsync(
'gh pr list --state open --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 100',
[
GH_PR_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${PR_STATE_OPEN}`,
`${GH_JSON_FLAG} ${PR_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${OPEN_PRS_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{
cwd: projectPath,
env: execEnv,
}
),
execAsync(
'gh pr list --state merged --json number,title,state,author,createdAt,labels,url,isDraft,headRefName,reviewDecision,mergeable,body --limit 50',
[
GH_PR_LIST_COMMAND,
repoFlag,
`${GH_STATE_FLAG} ${PR_STATE_MERGED}`,
`${GH_JSON_FLAG} ${PR_LIST_FIELDS}`,
`${GH_LIMIT_FLAG} ${MERGED_PRS_LIMIT}`,
]
.filter(Boolean)
.join(' '),
{
cwd: projectPath,
env: execEnv,

View File

@@ -1,36 +1,40 @@
/**
* POST /validate-issue endpoint - Validate a GitHub issue using Claude SDK or Cursor (async)
* POST /validate-issue endpoint - Validate a GitHub issue using provider abstraction (async)
*
* Scans the codebase to determine if an issue is valid, invalid, or needs clarification.
* Runs asynchronously and emits events for progress and completion.
* Supports both Claude models and Cursor models.
* Supports Claude, Codex, Cursor, and OpenCode models.
*/
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import type { EventEmitter } from '../../../lib/events.js';
import type {
IssueValidationResult,
IssueValidationEvent,
ModelAlias,
CursorModelId,
ModelId,
GitHubComment,
LinkedPRInfo,
ThinkingLevel,
ReasoningEffort,
} from '@automaker/types';
import {
DEFAULT_PHASE_MODELS,
isClaudeModel,
isCodexModel,
isCursorModel,
isOpencodeModel,
} 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 { extractJson } from '../../../lib/json-extractor.js';
import { writeValidation } from '../../../lib/validation-storage.js';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { streamingQuery } from '../../../providers/simple-query-service.js';
import {
issueValidationSchema,
ISSUE_VALIDATION_SYSTEM_PROMPT,
buildValidationPrompt,
ValidationComment,
ValidationLinkedPR,
} from './validation-schema.js';
import { getPromptCustomization } from '../../../lib/settings-helpers.js';
import {
trySetValidationRunning,
clearValidationStatus,
@@ -41,9 +45,6 @@ import {
import type { SettingsService } from '../../../services/settings-service.js';
import { getAutoLoadClaudeMdSetting } from '../../../lib/settings-helpers.js';
/** Valid Claude model values for validation */
const VALID_CLAUDE_MODELS: readonly ModelAlias[] = ['opus', 'sonnet', 'haiku'] as const;
/**
* Request body for issue validation
*/
@@ -53,10 +54,12 @@ interface ValidateIssueRequestBody {
issueTitle: string;
issueBody: string;
issueLabels?: string[];
/** Model to use for validation (opus, sonnet, haiku, or cursor model IDs) */
model?: ModelAlias | CursorModelId;
/** Thinking level for Claude models (ignored for Cursor models) */
/** Model to use for validation (Claude alias or provider model ID) */
model?: ModelId;
/** Thinking level for Claude models (ignored for non-Claude models) */
thinkingLevel?: ThinkingLevel;
/** Reasoning effort for Codex models (ignored for non-Codex models) */
reasoningEffort?: ReasoningEffort;
/** Comments to include in validation analysis */
comments?: GitHubComment[];
/** Linked pull requests for this issue */
@@ -68,7 +71,7 @@ interface ValidateIssueRequestBody {
*
* Emits events for start, progress, complete, and error.
* Stores result on completion.
* Supports both Claude models (with structured output) and Cursor models (with JSON parsing).
* Supports Claude/Codex models (structured output) and Cursor/OpenCode models (JSON parsing).
*/
async function runValidation(
projectPath: string,
@@ -76,13 +79,14 @@ async function runValidation(
issueTitle: string,
issueBody: string,
issueLabels: string[] | undefined,
model: ModelAlias | CursorModelId,
model: ModelId,
events: EventEmitter,
abortController: AbortController,
settingsService?: SettingsService,
comments?: ValidationComment[],
linkedPRs?: ValidationLinkedPR[],
thinkingLevel?: ThinkingLevel
thinkingLevel?: ThinkingLevel,
reasoningEffort?: ReasoningEffort
): Promise<void> {
// Emit start event
const startEvent: IssueValidationEvent = {
@@ -102,7 +106,7 @@ async function runValidation(
try {
// Build the prompt (include comments and linked PRs if provided)
const prompt = buildValidationPrompt(
const basePrompt = buildValidationPrompt(
issueNumber,
issueTitle,
issueBody,
@@ -111,18 +115,19 @@ async function runValidation(
linkedPRs
);
let validationResult: IssueValidationResult | null = null;
let responseText = '';
// Route to appropriate provider based on model
if (isCursorModel(model)) {
// Use Cursor provider for Cursor models
logger.info(`Using Cursor provider for validation with model: ${model}`);
// Get customized prompts from settings
const prompts = await getPromptCustomization(settingsService, '[ValidateIssue]');
const issueValidationSystemPrompt = prompts.issueValidation.systemPrompt;
const provider = ProviderFactory.getProviderForModel(model);
// Determine if we should use structured output (Claude/Codex support it, Cursor/OpenCode don't)
const useStructuredOutput = isClaudeModel(model) || isCodexModel(model);
// For Cursor, include the system prompt and schema in the user prompt
const cursorPrompt = `${ISSUE_VALIDATION_SYSTEM_PROMPT}
// Build the final prompt - for Cursor, include system prompt and JSON schema instructions
let finalPrompt = basePrompt;
if (!useStructuredOutput) {
finalPrompt = `${issueValidationSystemPrompt}
CRITICAL INSTRUCTIONS:
1. DO NOT write any files. Return the JSON in your response only.
@@ -133,121 +138,78 @@ ${JSON.stringify(issueValidationSchema, null, 2)}
Your entire response should be valid JSON starting with { and ending with }. No text before or after.
${prompt}`;
${basePrompt}`;
}
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;
// Load autoLoadClaudeMd setting
const autoLoadClaudeMd = await getAutoLoadClaudeMdSetting(
projectPath,
settingsService,
'[ValidateIssue]'
);
// 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;
}
}
}
// Parse JSON from the response text using shared utility
if (responseText) {
validationResult = extractJson<IssueValidationResult>(responseText, { logger });
}
} else {
// 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;
// Use request overrides if provided, otherwise fall back to settings
let effectiveThinkingLevel: ThinkingLevel | undefined = thinkingLevel;
let effectiveReasoningEffort: ReasoningEffort | undefined = reasoningEffort;
if (!effectiveThinkingLevel || !effectiveReasoningEffort) {
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel;
const resolved = resolvePhaseModel(phaseModelEntry);
if (!effectiveThinkingLevel) {
const settings = await settingsService?.getGlobalSettings();
const phaseModelEntry =
settings?.phaseModels?.validationModel || DEFAULT_PHASE_MODELS.validationModel;
const resolved = resolvePhaseModel(phaseModelEntry);
effectiveThinkingLevel = resolved.thinkingLevel;
}
// Create SDK options with structured output and abort controller
const options = createSuggestionsOptions({
cwd: projectPath,
model: model as ModelAlias,
systemPrompt: ISSUE_VALIDATION_SYSTEM_PROMPT,
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');
}
}
if (!effectiveReasoningEffort && typeof phaseModelEntry !== 'string') {
effectiveReasoningEffort = phaseModelEntry.reasoningEffort;
}
}
logger.info(`Using model: ${model}`);
// Use streamingQuery with event callbacks
const result = await streamingQuery({
prompt: finalPrompt,
model: model as string,
cwd: projectPath,
systemPrompt: useStructuredOutput ? issueValidationSystemPrompt : undefined,
abortController,
thinkingLevel: effectiveThinkingLevel,
reasoningEffort: effectiveReasoningEffort,
readOnly: true, // Issue validation only reads code, doesn't write
settingSources: autoLoadClaudeMd ? ['user', 'project', 'local'] : undefined,
outputFormat: useStructuredOutput
? {
type: 'json_schema',
schema: issueValidationSchema as Record<string, unknown>,
}
: undefined,
onText: (text) => {
responseText += text;
// Emit progress event
const progressEvent: IssueValidationEvent = {
type: 'issue_validation_progress',
issueNumber,
content: text,
projectPath,
};
events.emit('issue-validation:event', progressEvent);
},
});
// Clear timeout
clearTimeout(timeoutId);
// Get validation result from structured output or parse from text
let validationResult: IssueValidationResult | null = null;
if (result.structured_output) {
validationResult = result.structured_output as unknown as IssueValidationResult;
logger.debug('Received structured output:', validationResult);
} else if (responseText) {
// Parse JSON from response text
validationResult = extractJson<IssueValidationResult>(responseText, { logger });
}
// Require validation result
if (!validationResult) {
logger.error('No validation result received from AI provider');
@@ -297,7 +259,7 @@ ${prompt}`;
/**
* Creates the handler for validating GitHub issues against the codebase.
*
* Uses Claude SDK with:
* Uses the provider abstraction with:
* - Read-only tools (Read, Glob, Grep) for codebase analysis
* - JSON schema structured output for reliable parsing
* - System prompt guiding the validation process
@@ -317,6 +279,7 @@ export function createValidateIssueHandler(
issueLabels,
model = 'opus',
thinkingLevel,
reasoningEffort,
comments: rawComments,
linkedPRs: rawLinkedPRs,
} = req.body as ValidateIssueRequestBody;
@@ -364,14 +327,17 @@ export function createValidateIssueHandler(
return;
}
// Validate model parameter at runtime - accept Claude models or Cursor models
const isValidClaudeModel = VALID_CLAUDE_MODELS.includes(model as ModelAlias);
const isValidCursorModel = isCursorModel(model);
// Validate model parameter at runtime - accept any supported provider model
const isValidModel =
isClaudeModel(model) ||
isCursorModel(model) ||
isCodexModel(model) ||
isOpencodeModel(model);
if (!isValidClaudeModel && !isValidCursorModel) {
if (!isValidModel) {
res.status(400).json({
success: false,
error: `Invalid model. Must be one of: ${VALID_CLAUDE_MODELS.join(', ')}, or a Cursor model ID`,
error: 'Invalid model. Must be a Claude, Cursor, Codex, or OpenCode model ID (or alias).',
});
return;
}
@@ -402,7 +368,8 @@ export function createValidateIssueHandler(
settingsService,
validationComments,
validationLinkedPRs,
thinkingLevel
thinkingLevel,
reasoningEffort
)
.catch(() => {
// Error is already handled inside runValidation (event emitted)

View File

@@ -1,8 +1,11 @@
/**
* Issue Validation Schema and System Prompt
* Issue Validation Schema and Prompt Building
*
* Defines the JSON schema for Claude's structured output and
* the system prompt that guides the validation process.
* helper functions for building validation prompts.
*
* Note: The system prompt is now centralized in @automaker/prompts
* and accessed via getPromptCustomization() in validate-issue.ts
*/
/**
@@ -82,76 +85,6 @@ export const issueValidationSchema = {
additionalProperties: false,
} as const;
/**
* System prompt that guides Claude in validating GitHub issues.
* Instructs the model to use read-only tools to analyze the codebase.
*/
export const ISSUE_VALIDATION_SYSTEM_PROMPT = `You are an expert code analyst validating GitHub issues against a codebase.
Your task is to analyze a GitHub issue and determine if it's valid by scanning the codebase.
## Validation Process
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
3. **Examine the code** - Use Read to look at the actual implementation in relevant files
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
- **valid**: The issue describes a real problem that exists in the codebase, or a clear feature request that can be implemented. The referenced files/components exist and the issue is actionable.
- **invalid**: The issue describes behavior that doesn't exist, references non-existent files or components, is based on a misunderstanding of the code, or the described "bug" is actually expected behavior.
- **needs_clarification**: The issue lacks sufficient detail to verify. Specify what additional information is needed in the missingInfo field.
## For Bug Reports, Check:
- Do the referenced files/components exist?
- Does the code match what the issue describes?
- Is the described behavior actually a bug or expected?
- Can you locate the code that would cause the reported issue?
## For Feature Requests, Check:
- Does the feature already exist?
- Is the implementation location clear?
- 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
- **Always include relatedFiles** when you find relevant 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
- **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:
- trivial: Simple text changes, one-line fixes
- simple: Small changes to one file
- moderate: Changes to multiple files or moderate logic changes
- complex: Significant refactoring or new feature implementation
- very_complex: Major architectural changes or cross-cutting concerns
Be thorough in your analysis but focus on files that are directly relevant to the issue.`;
/**
* Comment data structure for validation prompt
*/

View File

@@ -9,12 +9,14 @@ import type { Request, Response } from 'express';
export interface EnvironmentResponse {
isContainerized: boolean;
skipSandboxWarning?: boolean;
}
export function createEnvironmentHandler() {
return (_req: Request, res: Response): void => {
res.json({
isContainerized: process.env.IS_CONTAINERIZED === 'true',
skipSandboxWarning: process.env.AUTOMAKER_SKIP_SANDBOX_WARNING === 'true',
} satisfies EnvironmentResponse);
};
}

View File

@@ -0,0 +1,21 @@
/**
* Common utilities for notification routes
*
* Provides logger and error handling utilities shared across all notification endpoints.
*/
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
/** Logger instance for notification-related operations */
export const logger = createLogger('Notifications');
/**
* 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,62 @@
/**
* Notifications routes - HTTP API for project-level notifications
*
* Provides endpoints for:
* - Listing notifications
* - Getting unread count
* - Marking notifications as read
* - Dismissing notifications
*
* All endpoints use handler factories that receive the NotificationService instance.
* Mounted at /api/notifications in the main server.
*/
import { Router } from 'express';
import type { NotificationService } from '../../services/notification-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createListHandler } from './routes/list.js';
import { createUnreadCountHandler } from './routes/unread-count.js';
import { createMarkReadHandler } from './routes/mark-read.js';
import { createDismissHandler } from './routes/dismiss.js';
/**
* Create notifications router with all endpoints
*
* Endpoints:
* - POST /list - List all notifications for a project
* - POST /unread-count - Get unread notification count
* - POST /mark-read - Mark notification(s) as read
* - POST /dismiss - Dismiss notification(s)
*
* @param notificationService - Instance of NotificationService
* @returns Express Router configured with all notification endpoints
*/
export function createNotificationsRoutes(notificationService: NotificationService): Router {
const router = Router();
// List notifications
router.post('/list', validatePathParams('projectPath'), createListHandler(notificationService));
// Get unread count
router.post(
'/unread-count',
validatePathParams('projectPath'),
createUnreadCountHandler(notificationService)
);
// Mark as read (single or all)
router.post(
'/mark-read',
validatePathParams('projectPath'),
createMarkReadHandler(notificationService)
);
// Dismiss (single or all)
router.post(
'/dismiss',
validatePathParams('projectPath'),
createDismissHandler(notificationService)
);
return router;
}

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