Merge PR #52: fix: save Claude OAuth token to active profile during GitHub setup flow
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
name: Validate Version
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
validate-version:
|
||||
name: Validate package.json version matches tag
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version from tag
|
||||
id: tag_version
|
||||
run: |
|
||||
# Extract version from tag (e.g., v2.5.5 -> 2.5.5)
|
||||
TAG_VERSION=${GITHUB_REF#refs/tags/v}
|
||||
echo "version=$TAG_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Tag version: $TAG_VERSION"
|
||||
|
||||
- name: Extract version from package.json
|
||||
id: package_version
|
||||
run: |
|
||||
# Read version from package.json
|
||||
PACKAGE_VERSION=$(node -p "require('./auto-claude-ui/package.json').version")
|
||||
echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
|
||||
echo "Package.json version: $PACKAGE_VERSION"
|
||||
|
||||
- name: Compare versions
|
||||
run: |
|
||||
TAG_VERSION="${{ steps.tag_version.outputs.version }}"
|
||||
PACKAGE_VERSION="${{ steps.package_version.outputs.version }}"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Version Validation"
|
||||
echo "=========================================="
|
||||
echo "Git tag version: v$TAG_VERSION"
|
||||
echo "package.json version: $PACKAGE_VERSION"
|
||||
echo "=========================================="
|
||||
|
||||
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
|
||||
echo ""
|
||||
echo "❌ ERROR: Version mismatch detected!"
|
||||
echo ""
|
||||
echo "The version in package.json ($PACKAGE_VERSION) does not match"
|
||||
echo "the git tag version ($TAG_VERSION)."
|
||||
echo ""
|
||||
echo "To fix this:"
|
||||
echo " 1. Delete this tag: git tag -d v$TAG_VERSION"
|
||||
echo " 2. Update package.json version to $TAG_VERSION"
|
||||
echo " 3. Commit the change"
|
||||
echo " 4. Recreate the tag: git tag -a v$TAG_VERSION -m 'Release v$TAG_VERSION'"
|
||||
echo ""
|
||||
echo "Or use the automated script:"
|
||||
echo " node scripts/bump-version.js $TAG_VERSION"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ SUCCESS: Versions match!"
|
||||
echo ""
|
||||
|
||||
- name: Version validation result
|
||||
if: success()
|
||||
run: |
|
||||
echo "::notice::Version validation passed - package.json version matches tag v${{ steps.tag_version.outputs.version }}"
|
||||
@@ -81,6 +81,21 @@ auto-claude/.venv/bin/pytest tests/ -m "not slow"
|
||||
python auto-claude/validate_spec.py --spec-dir auto-claude/specs/001-feature --checkpoint all
|
||||
```
|
||||
|
||||
### Releases
|
||||
```bash
|
||||
# Automated version bump and release (recommended)
|
||||
node scripts/bump-version.js patch # 2.5.5 -> 2.5.6
|
||||
node scripts/bump-version.js minor # 2.5.5 -> 2.6.0
|
||||
node scripts/bump-version.js major # 2.5.5 -> 3.0.0
|
||||
node scripts/bump-version.js 2.6.0 # Set specific version
|
||||
|
||||
# Then push to trigger GitHub release workflows
|
||||
git push origin main
|
||||
git push origin v2.6.0
|
||||
```
|
||||
|
||||
See [RELEASE.md](RELEASE.md) for detailed release process documentation.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Pipeline
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
# Release Process
|
||||
|
||||
This document describes how to create a new release of Auto Claude.
|
||||
|
||||
## Automated Release Process (Recommended)
|
||||
|
||||
We provide an automated script that handles version bumping, git commits, and tagging to ensure version consistency.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Clean git working directory (no uncommitted changes)
|
||||
- You're on the branch you want to release from (usually `main`)
|
||||
|
||||
### Steps
|
||||
|
||||
1. **Run the version bump script:**
|
||||
|
||||
```bash
|
||||
# Bump patch version (2.5.5 -> 2.5.6)
|
||||
node scripts/bump-version.js patch
|
||||
|
||||
# Bump minor version (2.5.5 -> 2.6.0)
|
||||
node scripts/bump-version.js minor
|
||||
|
||||
# Bump major version (2.5.5 -> 3.0.0)
|
||||
node scripts/bump-version.js major
|
||||
|
||||
# Set specific version
|
||||
node scripts/bump-version.js 2.6.0
|
||||
```
|
||||
|
||||
This script will:
|
||||
- ✅ Update `auto-claude-ui/package.json` with the new version
|
||||
- ✅ Create a git commit with the version change
|
||||
- ✅ Create a git tag (e.g., `v2.5.6`)
|
||||
- ⚠️ **NOT** push to remote (you control when to push)
|
||||
|
||||
2. **Review the changes:**
|
||||
|
||||
```bash
|
||||
git log -1 # View the commit
|
||||
git show v2.5.6 # View the tag
|
||||
```
|
||||
|
||||
3. **Push to GitHub:**
|
||||
|
||||
```bash
|
||||
# Push the commit
|
||||
git push origin main
|
||||
|
||||
# Push the tag
|
||||
git push origin v2.5.6
|
||||
```
|
||||
|
||||
4. **Create GitHub Release:**
|
||||
|
||||
- Go to [GitHub Releases](https://github.com/AndyMik90/Auto-Claude/releases)
|
||||
- Click "Draft a new release"
|
||||
- Select the tag you just pushed (e.g., `v2.5.6`)
|
||||
- Add release notes (describe what changed)
|
||||
- Click "Publish release"
|
||||
|
||||
5. **Automated builds will trigger:**
|
||||
|
||||
- ✅ Version validation workflow will verify version consistency
|
||||
- ✅ Tests will run (`test-on-tag.yml`)
|
||||
- ✅ Native module prebuilds will be created (`build-prebuilds.yml`)
|
||||
- ✅ Discord notification will be sent (`discord-release.yml`)
|
||||
|
||||
## Manual Release Process (Not Recommended)
|
||||
|
||||
If you need to create a release manually, follow these steps **carefully** to avoid version mismatches:
|
||||
|
||||
1. **Update `auto-claude-ui/package.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.5.6"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Commit the change:**
|
||||
|
||||
```bash
|
||||
git add auto-claude-ui/package.json
|
||||
git commit -m "chore: bump version to 2.5.6"
|
||||
```
|
||||
|
||||
3. **Create and push tag:**
|
||||
|
||||
```bash
|
||||
git tag -a v2.5.6 -m "Release v2.5.6"
|
||||
git push origin main
|
||||
git push origin v2.5.6
|
||||
```
|
||||
|
||||
4. **Create GitHub Release** (same as step 4 above)
|
||||
|
||||
## Version Validation
|
||||
|
||||
A GitHub Action automatically validates that the version in `package.json` matches the git tag.
|
||||
|
||||
If there's a mismatch, the workflow will **fail** with a clear error message:
|
||||
|
||||
```
|
||||
❌ ERROR: Version mismatch detected!
|
||||
|
||||
The version in package.json (2.5.0) does not match
|
||||
the git tag version (2.5.5).
|
||||
|
||||
To fix this:
|
||||
1. Delete this tag: git tag -d v2.5.5
|
||||
2. Update package.json version to 2.5.5
|
||||
3. Commit the change
|
||||
4. Recreate the tag: git tag -a v2.5.5 -m 'Release v2.5.5'
|
||||
```
|
||||
|
||||
This validation ensures we never ship a release where the updater shows the wrong version.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Version Mismatch Error
|
||||
|
||||
If you see a version mismatch error in GitHub Actions:
|
||||
|
||||
1. **Delete the incorrect tag:**
|
||||
```bash
|
||||
git tag -d v2.5.6 # Delete locally
|
||||
git push origin :refs/tags/v2.5.6 # Delete remotely
|
||||
```
|
||||
|
||||
2. **Use the automated script:**
|
||||
```bash
|
||||
node scripts/bump-version.js 2.5.6
|
||||
git push origin main
|
||||
git push origin v2.5.6
|
||||
```
|
||||
|
||||
### Git Working Directory Not Clean
|
||||
|
||||
If the version bump script fails with "Git working directory is not clean":
|
||||
|
||||
```bash
|
||||
# Commit or stash your changes first
|
||||
git status
|
||||
git add .
|
||||
git commit -m "your changes"
|
||||
|
||||
# Then run the version bump script
|
||||
node scripts/bump-version.js patch
|
||||
```
|
||||
|
||||
## Release Checklist
|
||||
|
||||
Use this checklist when creating a new release:
|
||||
|
||||
- [ ] All tests passing on main branch
|
||||
- [ ] CHANGELOG updated (if applicable)
|
||||
- [ ] Run `node scripts/bump-version.js <type>`
|
||||
- [ ] Review commit and tag
|
||||
- [ ] Push commit and tag to GitHub
|
||||
- [ ] Create GitHub Release with release notes
|
||||
- [ ] Verify version validation passed
|
||||
- [ ] Verify builds completed successfully
|
||||
- [ ] Test the updater shows correct version
|
||||
|
||||
## What Gets Released
|
||||
|
||||
When you create a release, the following are built and published:
|
||||
|
||||
1. **Native module prebuilds** - Windows node-pty binaries
|
||||
2. **Electron app packages** - Desktop installers (triggered manually or via electron-builder)
|
||||
3. **Discord notification** - Sent to the Auto Claude community
|
||||
|
||||
## Version Numbering
|
||||
|
||||
We follow [Semantic Versioning (SemVer)](https://semver.org/):
|
||||
|
||||
- **MAJOR** version (X.0.0) - Breaking changes
|
||||
- **MINOR** version (0.X.0) - New features (backward compatible)
|
||||
- **PATCH** version (0.0.X) - Bug fixes (backward compatible)
|
||||
|
||||
Examples:
|
||||
- `2.5.5 -> 2.5.6` - Bug fix
|
||||
- `2.5.6 -> 2.6.0` - New feature
|
||||
- `2.6.0 -> 3.0.0` - Breaking change
|
||||
@@ -20,7 +20,7 @@ export default defineConfig({
|
||||
index: resolve(__dirname, 'src/main/index.ts')
|
||||
},
|
||||
// Only node-pty needs to be external (native module rebuilt by electron-builder)
|
||||
external: ['node-pty']
|
||||
external: ['@lydell/node-pty']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "auto-claude-ui",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.5",
|
||||
"description": "Desktop UI for Auto Claude autonomous coding framework",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "Auto Claude Team",
|
||||
@@ -30,6 +30,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@lydell/node-pty": "^1.1.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
@@ -59,7 +60,6 @@
|
||||
"ioredis": "^5.8.2",
|
||||
"lucide-react": "^0.560.0",
|
||||
"motion": "^12.23.26",
|
||||
"node-pty": "^1.1.0-beta42",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -104,12 +104,13 @@
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"electron-builder-squirrel-windows": "^26.0.12",
|
||||
"dmg-builder": "^26.0.12"
|
||||
"dmg-builder": "^26.0.12",
|
||||
"node-pty": "npm:@lydell/node-pty@^1.1.0"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild",
|
||||
"node-pty"
|
||||
"electron-winstaller",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
@@ -132,8 +133,8 @@
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "node_modules/node-pty",
|
||||
"to": "node_modules/node-pty"
|
||||
"from": "node_modules/@lydell/node-pty",
|
||||
"to": "node_modules/@lydell/node-pty"
|
||||
},
|
||||
{
|
||||
"from": "resources/icon.ico",
|
||||
@@ -180,5 +181,6 @@
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
}
|
||||
},
|
||||
"packageManager": "[email protected]+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa"
|
||||
}
|
||||
|
||||
Generated
+645
-335
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import { ProcessType, ExecutionProgressData } from './types';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv, detectAuthFailure } from '../rate-limit-detector';
|
||||
import { projectStore } from '../project-store';
|
||||
import { getClaudeProfileManager } from '../claude-profile-manager';
|
||||
import { findPythonCommand, parsePythonCommand } from '../python-detector';
|
||||
|
||||
/**
|
||||
* Process spawning and lifecycle management
|
||||
@@ -17,7 +18,8 @@ export class AgentProcessManager {
|
||||
private state: AgentState;
|
||||
private events: AgentEvents;
|
||||
private emitter: EventEmitter;
|
||||
private pythonPath: string = 'python3';
|
||||
// Auto-detect Python command on initialization
|
||||
private pythonPath: string = findPythonCommand() || 'python';
|
||||
private autoBuildSourcePath: string = '';
|
||||
|
||||
constructor(state: AgentState, events: AgentEvents, emitter: EventEmitter) {
|
||||
@@ -161,7 +163,9 @@ export class AgentProcessManager {
|
||||
// Get active Claude profile environment (CLAUDE_CONFIG_DIR if not default)
|
||||
const profileEnv = getProfileEnv();
|
||||
|
||||
const childProcess = spawn(this.pythonPath, args, {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { AgentProcessManager } from './agent-process';
|
||||
import { IdeationConfig } from './types';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from '../rate-limit-detector';
|
||||
import { debugLog, debugError } from '../../shared/utils/debug-logger';
|
||||
import { parsePythonCommand } from '../python-detector';
|
||||
|
||||
/**
|
||||
* Queue management for ideation and roadmap generation
|
||||
@@ -206,7 +207,9 @@ export class AgentQueueManager {
|
||||
tokenPreview: hasToken ? oauthToken?.substring(0, 20) + '...' : 'none'
|
||||
});
|
||||
|
||||
const childProcess = spawn(pythonPath, args, {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
|
||||
cwd,
|
||||
env: finalEnv
|
||||
});
|
||||
@@ -441,7 +444,9 @@ export class AgentQueueManager {
|
||||
tokenPreview: hasToken ? oauthToken?.substring(0, 20) + '...' : 'none'
|
||||
});
|
||||
|
||||
const childProcess = spawn(pythonPath, args, {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
|
||||
cwd,
|
||||
env: finalEnv
|
||||
});
|
||||
|
||||
@@ -27,13 +27,15 @@ import {
|
||||
getCommits,
|
||||
getBranchDiffCommits
|
||||
} from './git-integration';
|
||||
import { findPythonCommand } from '../python-detector';
|
||||
|
||||
/**
|
||||
* Main changelog service - orchestrates all changelog operations
|
||||
* Delegates to specialized modules for specific concerns
|
||||
*/
|
||||
export class ChangelogService extends EventEmitter {
|
||||
private pythonPath: string = 'python3';
|
||||
// Auto-detect Python command on initialization
|
||||
private pythonPath: string = findPythonCommand() || 'python';
|
||||
private claudePath: string = 'claude';
|
||||
private autoBuildSourcePath: string = '';
|
||||
private cachedEnv: Record<string, string> | null = null;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { buildChangelogPrompt, buildGitPrompt, createGenerationScript } from './
|
||||
import { extractChangelog } from './parser';
|
||||
import { getCommits, getBranchDiffCommits } from './git-integration';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from '../rate-limit-detector';
|
||||
import { parsePythonCommand } from '../python-detector';
|
||||
|
||||
/**
|
||||
* Core changelog generation logic
|
||||
@@ -139,7 +140,9 @@ export class ChangelogGenerator extends EventEmitter {
|
||||
// Build environment with explicit critical variables
|
||||
const spawnEnv = this.buildSpawnEnvironment();
|
||||
|
||||
const childProcess = spawn(this.pythonPath, ['-c', script], {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, '-c', script], {
|
||||
cwd: this.autoBuildSourcePath,
|
||||
env: spawnEnv
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import type { GitCommit } from '../../shared/types';
|
||||
import { getProfileEnv } from '../rate-limit-detector';
|
||||
import { parsePythonCommand } from '../python-detector';
|
||||
|
||||
interface VersionSuggestion {
|
||||
version: string;
|
||||
@@ -52,7 +53,9 @@ export class VersionSuggester {
|
||||
const spawnEnv = this.buildSpawnEnvironment();
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
const childProcess = spawn(this.pythonPath, ['-c', script], {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, '-c', script], {
|
||||
cwd: this.autoBuildSourcePath,
|
||||
env: spawnEnv
|
||||
});
|
||||
|
||||
@@ -2,13 +2,15 @@ import path from 'path';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { app } from 'electron';
|
||||
import { getProfileEnv } from '../rate-limit-detector';
|
||||
import { findPythonCommand } from '../python-detector';
|
||||
|
||||
/**
|
||||
* Configuration manager for insights service
|
||||
* Handles path detection and environment variable loading
|
||||
*/
|
||||
export class InsightsConfig {
|
||||
private pythonPath: string = 'python3';
|
||||
// Auto-detect Python command on initialization
|
||||
private pythonPath: string = findPythonCommand() || 'python';
|
||||
private autoBuildSourcePath: string = '';
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,8 +8,8 @@ import type { IPCResult, FileNode } from '../../shared/types';
|
||||
const IGNORED_DIRS = new Set([
|
||||
'node_modules', '.git', '__pycache__', 'dist', 'build',
|
||||
'.next', '.nuxt', 'coverage', '.cache', '.venv', 'venv',
|
||||
'.idea', '.vscode', 'out', '.turbo', '.auto-claude',
|
||||
'.worktrees', 'vendor', 'target', '.gradle', '.maven'
|
||||
'out', '.turbo', '.worktrees',
|
||||
'vendor', 'target', '.gradle', '.maven'
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -29,8 +29,11 @@ export function registerFileHandlers(): void {
|
||||
// Filter and map entries
|
||||
const nodes: FileNode[] = [];
|
||||
for (const entry of entries) {
|
||||
// Skip hidden files (except .env which is often useful)
|
||||
if (entry.name.startsWith('.') && entry.name !== '.env') continue;
|
||||
// Skip hidden files (not directories) except useful ones like .env, .gitignore
|
||||
if (!entry.isDirectory() && entry.name.startsWith('.') &&
|
||||
!['.env', '.gitignore', '.env.example', '.env.local'].includes(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
// Skip ignored directories
|
||||
if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ipcMain } from 'electron';
|
||||
import { IPC_CHANNELS } from '../../../shared/constants';
|
||||
import type { IPCResult, GitHubIssue } from '../../../shared/types';
|
||||
import { projectStore } from '../../project-store';
|
||||
import { getGitHubConfig, githubFetch } from './utils';
|
||||
import { getGitHubConfig, githubFetch, normalizeRepoReference } from './utils';
|
||||
import type { GitHubAPIIssue, GitHubAPIComment } from './types';
|
||||
|
||||
/**
|
||||
@@ -57,16 +57,32 @@ export function registerGetIssues(): void {
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||
if (!normalizedRepo) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||
};
|
||||
}
|
||||
|
||||
const issues = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}/issues?state=${state}&per_page=100&sort=updated`
|
||||
) as GitHubAPIIssue[];
|
||||
`/repos/${normalizedRepo}/issues?state=${state}&per_page=100&sort=updated`
|
||||
);
|
||||
|
||||
// Ensure issues is an array
|
||||
if (!Array.isArray(issues)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unexpected response format from GitHub API'
|
||||
};
|
||||
}
|
||||
|
||||
// Filter out pull requests
|
||||
const issuesOnly = issues.filter(issue => !issue.pull_request);
|
||||
const issuesOnly = issues.filter((issue: GitHubAPIIssue) => !issue.pull_request);
|
||||
|
||||
const result: GitHubIssue[] = issuesOnly.map(issue =>
|
||||
transformIssue(issue, config.repo)
|
||||
const result: GitHubIssue[] = issuesOnly.map((issue: GitHubAPIIssue) =>
|
||||
transformIssue(issue, normalizedRepo)
|
||||
);
|
||||
|
||||
return { success: true, data: result };
|
||||
@@ -98,12 +114,20 @@ export function registerGetIssue(): void {
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||
if (!normalizedRepo) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||
};
|
||||
}
|
||||
|
||||
const issue = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}/issues/${issueNumber}`
|
||||
`/repos/${normalizedRepo}/issues/${issueNumber}`
|
||||
) as GitHubAPIIssue;
|
||||
|
||||
const result = transformIssue(issue, config.repo);
|
||||
const result = transformIssue(issue, normalizedRepo);
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (error) {
|
||||
@@ -134,9 +158,17 @@ export function registerGetIssueComments(): void {
|
||||
}
|
||||
|
||||
try {
|
||||
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||
if (!normalizedRepo) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||
};
|
||||
}
|
||||
|
||||
const comments = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}/issues/${issueNumber}/comments`
|
||||
`/repos/${normalizedRepo}/issues/${issueNumber}/comments`
|
||||
) as GitHubAPIComment[];
|
||||
|
||||
return { success: true, data: comments };
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ipcMain } from 'electron';
|
||||
import { IPC_CHANNELS } from '../../../shared/constants';
|
||||
import type { IPCResult, GitHubRepository, GitHubSyncStatus } from '../../../shared/types';
|
||||
import { projectStore } from '../../project-store';
|
||||
import { getGitHubConfig, githubFetch } from './utils';
|
||||
import { getGitHubConfig, githubFetch, normalizeRepoReference } from './utils';
|
||||
import type { GitHubAPIRepository } from './types';
|
||||
|
||||
/**
|
||||
@@ -33,16 +33,28 @@ export function registerCheckConnection(): void {
|
||||
}
|
||||
|
||||
try {
|
||||
// Normalize repo reference (handles full URLs, git URLs, etc.)
|
||||
const normalizedRepo = normalizeRepoReference(config.repo);
|
||||
if (!normalizedRepo) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
connected: false,
|
||||
error: 'Invalid repository format. Use owner/repo or GitHub URL.'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch repo info
|
||||
const repoData = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}`
|
||||
`/repos/${normalizedRepo}`
|
||||
) as { full_name: string; description?: string };
|
||||
|
||||
// Count open issues
|
||||
const issuesData = await githubFetch(
|
||||
config.token,
|
||||
`/repos/${config.repo}/issues?state=open&per_page=1`
|
||||
`/repos/${normalizedRepo}/issues?state=open&per_page=1`
|
||||
) as unknown[];
|
||||
|
||||
const openCount = Array.isArray(issuesData) ? issuesData.length : 0;
|
||||
@@ -71,7 +83,7 @@ export function registerCheckConnection(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of GitHub repositories
|
||||
* Get list of GitHub repositories (personal + organization)
|
||||
*/
|
||||
export function registerGetRepositories(): void {
|
||||
ipcMain.handle(
|
||||
@@ -88,9 +100,11 @@ export function registerGetRepositories(): void {
|
||||
}
|
||||
|
||||
try {
|
||||
// Fetch user's personal + organization repos
|
||||
// affiliation parameter includes: owner, collaborator, organization_member
|
||||
const repos = await githubFetch(
|
||||
config.token,
|
||||
'/user/repos?per_page=100&sort=updated'
|
||||
'/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member'
|
||||
) as GitHubAPIRepository[];
|
||||
|
||||
const result: GitHubRepository[] = repos.map(repo => ({
|
||||
|
||||
@@ -54,6 +54,32 @@ export function getGitHubConfig(project: Project): GitHubConfig | null {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a GitHub repository reference to owner/repo format
|
||||
* Handles:
|
||||
* - owner/repo (already normalized)
|
||||
* - https://github.com/owner/repo
|
||||
* - https://github.com/owner/repo.git
|
||||
* - [email protected]:owner/repo.git
|
||||
*/
|
||||
export function normalizeRepoReference(repo: string): string {
|
||||
if (!repo) return '';
|
||||
|
||||
// Remove trailing .git if present
|
||||
let normalized = repo.replace(/\.git$/, '');
|
||||
|
||||
// Handle full GitHub URLs
|
||||
if (normalized.startsWith('https://github.com/')) {
|
||||
normalized = normalized.replace('https://github.com/', '');
|
||||
} else if (normalized.startsWith('http://github.com/')) {
|
||||
normalized = normalized.replace('http://github.com/', '');
|
||||
} else if (normalized.startsWith('[email protected]:')) {
|
||||
normalized = normalized.replace('[email protected]:', '');
|
||||
}
|
||||
|
||||
return normalized.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the GitHub API
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IPC_CHANNELS, AUTO_BUILD_PATHS, getSpecsDir } from '../../../shared/con
|
||||
import type { IPCResult, TaskStartOptions, TaskStatus } from '../../../shared/types';
|
||||
import path from 'path';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { spawnSync } from 'child_process';
|
||||
import { AgentManager } from '../../agent';
|
||||
import { fileWatcher } from '../../file-watcher';
|
||||
import { findTaskAndProject } from './shared';
|
||||
@@ -200,6 +201,11 @@ export function registerTaskExecutionHandlers(
|
||||
task.specId
|
||||
);
|
||||
|
||||
// Check if worktree exists - QA needs to run in the worktree where the build happened
|
||||
const worktreePath = path.join(project.path, '.worktrees', task.specId);
|
||||
const worktreeSpecDir = path.join(worktreePath, specsBaseDir, task.specId);
|
||||
const hasWorktree = existsSync(worktreePath);
|
||||
|
||||
if (approved) {
|
||||
// Write approval to QA report
|
||||
const qaReportPath = path.join(specDir, AUTO_BUILD_PATHS.QA_REPORT);
|
||||
@@ -217,15 +223,60 @@ export function registerTaskExecutionHandlers(
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Write feedback for QA fixer
|
||||
const fixRequestPath = path.join(specDir, 'QA_FIX_REQUEST.md');
|
||||
// Reset and discard all changes from worktree merge in main
|
||||
// The worktree still has all changes, so nothing is lost
|
||||
if (hasWorktree) {
|
||||
// Step 1: Unstage all changes
|
||||
const resetResult = spawnSync('git', ['reset', 'HEAD'], {
|
||||
cwd: project.path,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
if (resetResult.status === 0) {
|
||||
console.log('[TASK_REVIEW] Unstaged changes in main');
|
||||
}
|
||||
|
||||
// Step 2: Discard all working tree changes (restore to pre-merge state)
|
||||
const checkoutResult = spawnSync('git', ['checkout', '--', '.'], {
|
||||
cwd: project.path,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
if (checkoutResult.status === 0) {
|
||||
console.log('[TASK_REVIEW] Discarded working tree changes in main');
|
||||
}
|
||||
|
||||
// Step 3: Clean untracked files that came from the merge
|
||||
const cleanResult = spawnSync('git', ['clean', '-fd'], {
|
||||
cwd: project.path,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe'
|
||||
});
|
||||
if (cleanResult.status === 0) {
|
||||
console.log('[TASK_REVIEW] Cleaned untracked files in main');
|
||||
}
|
||||
|
||||
console.log('[TASK_REVIEW] Main branch restored to pre-merge state');
|
||||
}
|
||||
|
||||
// Write feedback for QA fixer - write to WORKTREE spec dir if it exists
|
||||
// The QA process runs in the worktree where the build and implementation_plan.json are
|
||||
const targetSpecDir = hasWorktree ? worktreeSpecDir : specDir;
|
||||
const fixRequestPath = path.join(targetSpecDir, 'QA_FIX_REQUEST.md');
|
||||
|
||||
console.warn('[TASK_REVIEW] Writing QA fix request to:', fixRequestPath);
|
||||
console.warn('[TASK_REVIEW] hasWorktree:', hasWorktree, 'worktreePath:', worktreePath);
|
||||
|
||||
writeFileSync(
|
||||
fixRequestPath,
|
||||
`# QA Fix Request\n\nStatus: REJECTED\n\n## Feedback\n\n${feedback || 'No feedback provided'}\n\nCreated at: ${new Date().toISOString()}\n`
|
||||
);
|
||||
|
||||
// Restart QA process with dev mode
|
||||
agentManager.startQAProcess(taskId, project.path, task.specId);
|
||||
// Restart QA process - use worktree path if it exists, otherwise main project
|
||||
// The QA process needs to run where the implementation_plan.json with completed subtasks is
|
||||
const qaProjectPath = hasWorktree ? worktreePath : project.path;
|
||||
console.warn('[TASK_REVIEW] Starting QA process with projectPath:', qaProjectPath);
|
||||
agentManager.startQAProcess(taskId, qaProjectPath, task.specId);
|
||||
|
||||
const mainWindow = getMainWindow();
|
||||
if (mainWindow) {
|
||||
|
||||
@@ -3,12 +3,13 @@ import { IPC_CHANNELS, AUTO_BUILD_PATHS } from '../../../shared/constants';
|
||||
import type { IPCResult, WorktreeStatus, WorktreeDiff, WorktreeDiffFile, WorktreeMergeResult, WorktreeDiscardResult, WorktreeListResult, WorktreeListItem } from '../../../shared/types';
|
||||
import path from 'path';
|
||||
import { existsSync, readdirSync, statSync } from 'fs';
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { execSync, spawn, spawnSync } from 'child_process';
|
||||
import { projectStore } from '../../project-store';
|
||||
import { PythonEnvManager } from '../../python-env-manager';
|
||||
import { getEffectiveSourcePath } from '../../auto-claude-updater';
|
||||
import { getProfileEnv } from '../../rate-limit-detector';
|
||||
import { findTaskAndProject } from './shared';
|
||||
import { findPythonCommand, parsePythonCommand } from '../../python-detector';
|
||||
|
||||
/**
|
||||
* Register worktree management handlers
|
||||
@@ -272,6 +273,31 @@ export function registerWorktreeHandlers(
|
||||
const worktreePath = path.join(project.path, '.worktrees', task.specId);
|
||||
debug('Worktree path:', worktreePath, 'exists:', existsSync(worktreePath));
|
||||
|
||||
// Check if changes are already staged (for stage-only mode)
|
||||
if (options?.noCommit) {
|
||||
const stagedResult = spawnSync('git', ['diff', '--staged', '--name-only'], {
|
||||
cwd: project.path,
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
if (stagedResult.status === 0 && stagedResult.stdout?.trim()) {
|
||||
const stagedFiles = stagedResult.stdout.trim().split('\n');
|
||||
debug('Changes already staged:', stagedFiles.length, 'files');
|
||||
// Return success - changes are already staged
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
success: true,
|
||||
merged: false,
|
||||
message: `Changes already staged (${stagedFiles.length} files). Review with git diff --staged.`,
|
||||
staged: true,
|
||||
alreadyStaged: true,
|
||||
projectPath: project.path
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get git status before merge
|
||||
try {
|
||||
const gitStatusBefore = execSync('git status --short', { cwd: project.path, encoding: 'utf-8' });
|
||||
@@ -294,7 +320,7 @@ export function registerWorktreeHandlers(
|
||||
args.push('--no-commit');
|
||||
}
|
||||
|
||||
const pythonPath = pythonEnvManager.getPythonPath() || 'python3';
|
||||
const pythonPath = pythonEnvManager.getPythonPath() || findPythonCommand() || 'python';
|
||||
debug('Running command:', pythonPath, args.join(' '));
|
||||
debug('Working directory:', sourcePath);
|
||||
|
||||
@@ -310,7 +336,9 @@ export function registerWorktreeHandlers(
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
let resolved = false;
|
||||
|
||||
const mergeProcess = spawn(pythonPath, args, {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonPath);
|
||||
const mergeProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
|
||||
cwd: sourcePath,
|
||||
env: {
|
||||
...process.env,
|
||||
@@ -622,14 +650,16 @@ export function registerWorktreeHandlers(
|
||||
'--merge-preview'
|
||||
];
|
||||
|
||||
const pythonPath = pythonEnvManager.getPythonPath() || 'python3';
|
||||
const pythonPath = pythonEnvManager.getPythonPath() || findPythonCommand() || 'python';
|
||||
console.warn('[IPC] Running merge preview:', pythonPath, args.join(' '));
|
||||
|
||||
// Get profile environment for consistency
|
||||
const previewProfileEnv = getProfileEnv();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const previewProcess = spawn(pythonPath, args, {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonPath);
|
||||
const previewProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
|
||||
cwd: sourcePath,
|
||||
env: { ...process.env, ...previewProfileEnv, PYTHONUNBUFFERED: '1', PYTHONIOENCODING: 'utf-8', PYTHONUTF8: '1', DEBUG: 'true' }
|
||||
});
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* Detect and return the best available Python command.
|
||||
* Tries multiple candidates and returns the first one that works with Python 3.
|
||||
*
|
||||
* @returns The Python command to use, or null if none found
|
||||
*/
|
||||
export function findPythonCommand(): string | null {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// On Windows, try py launcher first (most reliable), then python, then python3
|
||||
// On Unix, try python3 first, then python
|
||||
const candidates = isWindows
|
||||
? ['py -3', 'python', 'python3', 'py']
|
||||
: ['python3', 'python'];
|
||||
|
||||
for (const cmd of candidates) {
|
||||
try {
|
||||
const version = execSync(`${cmd} --version`, {
|
||||
stdio: 'pipe',
|
||||
timeout: 5000,
|
||||
windowsHide: true
|
||||
}).toString();
|
||||
|
||||
if (version.includes('Python 3')) {
|
||||
return cmd;
|
||||
}
|
||||
} catch {
|
||||
// Command not found or errored, try next
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to platform-specific default
|
||||
return isWindows ? 'python' : 'python3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default Python command for the current platform.
|
||||
* This is a synchronous fallback that doesn't test if Python actually exists.
|
||||
*
|
||||
* @returns The default Python command for this platform
|
||||
*/
|
||||
export function getDefaultPythonCommand(): string {
|
||||
return process.platform === 'win32' ? 'python' : 'python3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a Python command string into command and base arguments.
|
||||
* Handles space-separated commands like "py -3".
|
||||
*
|
||||
* @param pythonPath - The Python command string (e.g., "python3", "py -3")
|
||||
* @returns Tuple of [command, baseArgs] ready for use with spawn()
|
||||
*/
|
||||
export function parsePythonCommand(pythonPath: string): [string, string[]] {
|
||||
const parts = pythonPath.split(' ');
|
||||
const command = parts[0];
|
||||
const baseArgs = parts.slice(1);
|
||||
return [command, baseArgs];
|
||||
}
|
||||
@@ -37,16 +37,11 @@ export class PythonEnvManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Get the path to pip in the venv
|
||||
* Returns null - we use python -m pip instead for better compatibility
|
||||
* @deprecated Use getVenvPythonPath() with -m pip instead
|
||||
*/
|
||||
private getVenvPipPath(): string | null {
|
||||
if (!this.autoBuildSourcePath) return null;
|
||||
|
||||
const venvPip =
|
||||
process.platform === 'win32'
|
||||
? path.join(this.autoBuildSourcePath, '.venv', 'Scripts', 'pip.exe')
|
||||
: path.join(this.autoBuildSourcePath, '.venv', 'bin', 'pip');
|
||||
|
||||
return venvPip;
|
||||
return null; // Not used - we use python -m pip
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,16 +176,54 @@ export class PythonEnvManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Install dependencies from requirements.txt
|
||||
* Bootstrap pip in the venv using ensurepip
|
||||
*/
|
||||
private async bootstrapPip(): Promise<boolean> {
|
||||
const venvPython = this.getVenvPythonPath();
|
||||
if (!venvPython || !existsSync(venvPython)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.warn('[PythonEnvManager] Bootstrapping pip...');
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(venvPython, ['-m', 'ensurepip'], {
|
||||
cwd: this.autoBuildSourcePath!,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
proc.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.warn('[PythonEnvManager] Pip bootstrapped successfully');
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error('[PythonEnvManager] Failed to bootstrap pip:', stderr);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
console.error('[PythonEnvManager] Error bootstrapping pip:', err);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install dependencies from requirements.txt using python -m pip
|
||||
*/
|
||||
private async installDeps(): Promise<boolean> {
|
||||
if (!this.autoBuildSourcePath) return false;
|
||||
|
||||
const venvPip = this.getVenvPipPath();
|
||||
const venvPython = this.getVenvPythonPath();
|
||||
const requirementsPath = path.join(this.autoBuildSourcePath, 'requirements.txt');
|
||||
|
||||
if (!venvPip || !existsSync(venvPip)) {
|
||||
this.emit('error', 'Pip not found in virtual environment');
|
||||
if (!venvPython || !existsSync(venvPython)) {
|
||||
this.emit('error', 'Python not found in virtual environment');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -199,11 +232,15 @@ export class PythonEnvManager extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bootstrap pip first if needed
|
||||
await this.bootstrapPip();
|
||||
|
||||
this.emit('status', 'Installing Python dependencies (this may take a minute)...');
|
||||
console.warn('[PythonEnvManager] Installing dependencies from:', requirementsPath);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(venvPip, ['install', '-r', requirementsPath], {
|
||||
// Use python -m pip for better compatibility across Python versions
|
||||
const proc = spawn(venvPython, ['-m', 'pip', 'install', '-r', requirementsPath], {
|
||||
cwd: this.autoBuildSourcePath!,
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { spawn } from 'child_process';
|
||||
import { app } from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from './rate-limit-detector';
|
||||
import { findPythonCommand, parsePythonCommand } from './python-detector';
|
||||
|
||||
/**
|
||||
* Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set
|
||||
@@ -20,7 +21,8 @@ function debug(...args: unknown[]): void {
|
||||
* Service for generating terminal names from commands using Claude AI
|
||||
*/
|
||||
export class TerminalNameGenerator extends EventEmitter {
|
||||
private pythonPath: string = 'python3';
|
||||
// Auto-detect Python command on initialization
|
||||
private pythonPath: string = findPythonCommand() || 'python';
|
||||
private autoBuildSourcePath: string = '';
|
||||
|
||||
constructor() {
|
||||
@@ -130,7 +132,9 @@ export class TerminalNameGenerator extends EventEmitter {
|
||||
const profileEnv = getProfileEnv();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const childProcess = spawn(this.pythonPath, ['-c', script], {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, '-c', script], {
|
||||
cwd: autoBuildSource,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -97,6 +97,7 @@ export function handleOAuthToken(
|
||||
const profileIdMatch = terminal.id.match(/claude-login-(profile-\d+|default)-/);
|
||||
|
||||
if (profileIdMatch) {
|
||||
// Save to specific profile (profile login terminal)
|
||||
const profileId = profileIdMatch[1];
|
||||
const profileManager = getClaudeProfileManager();
|
||||
const success = profileManager.setProfileToken(profileId, token, email || undefined);
|
||||
@@ -118,16 +119,56 @@ export function handleOAuthToken(
|
||||
console.error('[ClaudeIntegration] Failed to save OAuth token to profile:', profileId);
|
||||
}
|
||||
} else {
|
||||
console.warn('[ClaudeIntegration] OAuth token detected but not in a profile login terminal');
|
||||
const win = getWindow();
|
||||
if (win) {
|
||||
win.webContents.send(IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
|
||||
terminalId: terminal.id,
|
||||
email,
|
||||
success: false,
|
||||
message: 'Token detected but no profile associated with this terminal',
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OAuthTokenEvent);
|
||||
// No profile-specific terminal, save to active profile (GitHub OAuth flow, etc.)
|
||||
console.warn('[ClaudeIntegration] OAuth token detected in non-profile terminal, saving to active profile');
|
||||
const profileManager = getClaudeProfileManager();
|
||||
const activeProfile = profileManager.getActiveProfile();
|
||||
|
||||
// Defensive null check for active profile
|
||||
if (!activeProfile) {
|
||||
console.error('[ClaudeIntegration] Failed to save OAuth token: no active profile found');
|
||||
const win = getWindow();
|
||||
if (win) {
|
||||
win.webContents.send(IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
|
||||
terminalId: terminal.id,
|
||||
profileId: undefined,
|
||||
email,
|
||||
success: false,
|
||||
message: 'No active profile found',
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OAuthTokenEvent);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const success = profileManager.setProfileToken(activeProfile.id, token, email || undefined);
|
||||
|
||||
if (success) {
|
||||
console.warn('[ClaudeIntegration] OAuth token auto-saved to active profile:', activeProfile.name);
|
||||
|
||||
const win = getWindow();
|
||||
if (win) {
|
||||
win.webContents.send(IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
|
||||
terminalId: terminal.id,
|
||||
profileId: activeProfile.id,
|
||||
email,
|
||||
success: true,
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OAuthTokenEvent);
|
||||
}
|
||||
} else {
|
||||
console.error('[ClaudeIntegration] Failed to save OAuth token to active profile:', activeProfile.name);
|
||||
const win = getWindow();
|
||||
if (win) {
|
||||
win.webContents.send(IPC_CHANNELS.TERMINAL_OAUTH_TOKEN, {
|
||||
terminalId: terminal.id,
|
||||
profileId: activeProfile?.id,
|
||||
email,
|
||||
success: false,
|
||||
message: 'Failed to save token to active profile',
|
||||
detectedAt: new Date().toISOString()
|
||||
} as OAuthTokenEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
import * as net from 'net';
|
||||
import * as fs from 'fs';
|
||||
import * as pty from 'node-pty';
|
||||
import * as pty from '@lydell/node-pty';
|
||||
|
||||
const SOCKET_PATH =
|
||||
process.platform === 'win32'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Handles low-level PTY process creation and lifecycle
|
||||
*/
|
||||
|
||||
import * as pty from 'node-pty';
|
||||
import * as pty from '@lydell/node-pty';
|
||||
import * as os from 'os';
|
||||
import type { TerminalProcess, WindowGetter } from './types';
|
||||
import { IPC_CHANNELS } from '../../shared/constants';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as pty from 'node-pty';
|
||||
import type * as pty from '@lydell/node-pty';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import { spawn } from 'child_process';
|
||||
import { app } from 'electron';
|
||||
import { EventEmitter } from 'events';
|
||||
import { detectRateLimit, createSDKRateLimitInfo, getProfileEnv } from './rate-limit-detector';
|
||||
import { findPythonCommand, parsePythonCommand } from './python-detector';
|
||||
|
||||
/**
|
||||
* Debug logging - only logs when AUTO_CLAUDE_DEBUG env var is set
|
||||
@@ -20,7 +21,8 @@ function debug(...args: unknown[]): void {
|
||||
* Service for generating task titles from descriptions using Claude AI
|
||||
*/
|
||||
export class TitleGenerator extends EventEmitter {
|
||||
private pythonPath: string = 'python3';
|
||||
// Auto-detect Python command on initialization
|
||||
private pythonPath: string = findPythonCommand() || 'python';
|
||||
private autoBuildSourcePath: string = '';
|
||||
|
||||
constructor() {
|
||||
@@ -129,7 +131,9 @@ export class TitleGenerator extends EventEmitter {
|
||||
const profileEnv = getProfileEnv();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const childProcess = spawn(this.pythonPath, ['-c', script], {
|
||||
// Parse Python command to handle space-separated commands like "py -3"
|
||||
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath);
|
||||
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, '-c', script], {
|
||||
cwd: autoBuildSource,
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -69,6 +69,8 @@ export function App() {
|
||||
const [showInitDialog, setShowInitDialog] = useState(false);
|
||||
const [pendingProject, setPendingProject] = useState<Project | null>(null);
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
const [initSuccess, setInitSuccess] = useState(false);
|
||||
const [initError, setInitError] = useState<string | null>(null);
|
||||
const [skippedInitProjectId, setSkippedInitProjectId] = useState<string | null>(null);
|
||||
|
||||
// GitHub setup state (shown after Auto Claude init)
|
||||
@@ -141,6 +143,8 @@ export function App() {
|
||||
if (selectedProject && !selectedProject.autoBuildPath && skippedInitProjectId !== selectedProject.id) {
|
||||
// Project exists but isn't initialized - show init dialog
|
||||
setPendingProject(selectedProject);
|
||||
setInitError(null); // Clear any previous errors
|
||||
setInitSuccess(false); // Reset success flag
|
||||
setShowInitDialog(true);
|
||||
}
|
||||
}, [selectedProject, skippedInitProjectId, isInitializing]);
|
||||
@@ -232,6 +236,8 @@ export function App() {
|
||||
if (project && !project.autoBuildPath) {
|
||||
// Project doesn't have Auto Claude initialized, show init dialog
|
||||
setPendingProject(project);
|
||||
setInitError(null); // Clear any previous errors
|
||||
setInitSuccess(false); // Reset success flag
|
||||
setShowInitDialog(true);
|
||||
}
|
||||
}
|
||||
@@ -244,24 +250,45 @@ export function App() {
|
||||
if (!pendingProject) return;
|
||||
|
||||
const projectId = pendingProject.id;
|
||||
console.log('[InitDialog] Starting initialization for project:', projectId);
|
||||
setIsInitializing(true);
|
||||
setInitSuccess(false);
|
||||
setInitError(null); // Clear any previous errors
|
||||
try {
|
||||
const result = await initializeProject(projectId);
|
||||
console.log('[InitDialog] Initialization result:', result);
|
||||
|
||||
if (result?.success) {
|
||||
console.log('[InitDialog] Initialization successful, closing dialog');
|
||||
// Get the updated project from store
|
||||
const updatedProject = useProjectStore.getState().projects.find(p => p.id === projectId);
|
||||
console.log('[InitDialog] Updated project:', updatedProject);
|
||||
|
||||
// Clear init dialog state
|
||||
setPendingProject(null);
|
||||
// Mark as successful to prevent onOpenChange from treating this as a skip
|
||||
setInitSuccess(true);
|
||||
setIsInitializing(false);
|
||||
|
||||
// Now close the dialog
|
||||
setShowInitDialog(false);
|
||||
setPendingProject(null);
|
||||
|
||||
// Show GitHub setup modal
|
||||
if (updatedProject) {
|
||||
setGitHubSetupProject(updatedProject);
|
||||
setShowGitHubSetup(true);
|
||||
}
|
||||
} else {
|
||||
// Initialization failed - show error but keep dialog open
|
||||
console.log('[InitDialog] Initialization failed, showing error');
|
||||
const errorMessage = result?.error || 'Failed to initialize Auto Claude. Please try again.';
|
||||
setInitError(errorMessage);
|
||||
setIsInitializing(false);
|
||||
}
|
||||
} finally {
|
||||
} catch (error) {
|
||||
// Unexpected error occurred
|
||||
console.error('[InitDialog] Unexpected error during initialization:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred';
|
||||
setInitError(errorMessage);
|
||||
setIsInitializing(false);
|
||||
}
|
||||
};
|
||||
@@ -274,10 +301,16 @@ export function App() {
|
||||
if (!gitHubSetupProject) return;
|
||||
|
||||
try {
|
||||
// NOTE: settings.githubToken is a GitHub access token (from gh CLI),
|
||||
// NOT a Claude Code OAuth token. They are different things:
|
||||
// - GitHub token: for GitHub API access (repo operations)
|
||||
// - Claude token: for Claude AI access (run.py, roadmap, etc.)
|
||||
// The user needs to separately authenticate with Claude using 'claude setup-token'
|
||||
|
||||
// Update project env config with GitHub settings
|
||||
await window.electronAPI.updateProjectEnv(gitHubSetupProject.id, {
|
||||
githubEnabled: true,
|
||||
githubToken: settings.githubToken,
|
||||
githubToken: settings.githubToken, // GitHub token for repo access
|
||||
githubRepo: settings.githubRepo
|
||||
});
|
||||
|
||||
@@ -302,11 +335,14 @@ export function App() {
|
||||
};
|
||||
|
||||
const handleSkipInit = () => {
|
||||
console.log('[InitDialog] User skipped initialization');
|
||||
if (pendingProject) {
|
||||
setSkippedInitProjectId(pendingProject.id);
|
||||
}
|
||||
setShowInitDialog(false);
|
||||
setPendingProject(null);
|
||||
setInitError(null); // Clear any error when skipping
|
||||
setInitSuccess(false); // Reset success flag
|
||||
};
|
||||
|
||||
const handleGoToTask = (taskId: string) => {
|
||||
@@ -471,9 +507,10 @@ export function App() {
|
||||
|
||||
{/* Initialize Auto Claude Dialog */}
|
||||
<Dialog open={showInitDialog} onOpenChange={(open) => {
|
||||
console.log('[InitDialog] onOpenChange called', { open, pendingProject: !!pendingProject, isInitializing, initSuccess });
|
||||
// Only trigger skip if user manually closed the dialog
|
||||
// Don't trigger if pendingProject is null (successful init) or if initializing
|
||||
if (!open && pendingProject && !isInitializing) {
|
||||
// Don't trigger if: successful init, no pending project, or currently initializing
|
||||
if (!open && pendingProject && !isInitializing && !initSuccess) {
|
||||
handleSkipInit();
|
||||
}
|
||||
}}>
|
||||
@@ -509,6 +546,19 @@ export function App() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{initError && (
|
||||
<div className="mt-4 rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="h-4 w-4 text-destructive mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="font-medium text-destructive">Initialization Failed</p>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{initError}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleSkipInit} disabled={isInitializing}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Github,
|
||||
GitBranch,
|
||||
Key,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
SelectValue
|
||||
} from './ui/select';
|
||||
import { GitHubOAuthFlow } from './project-settings/GitHubOAuthFlow';
|
||||
import { ClaudeOAuthFlow } from './project-settings/ClaudeOAuthFlow';
|
||||
import type { Project, ProjectSettings } from '../../shared/types';
|
||||
|
||||
interface GitHubSetupModalProps {
|
||||
@@ -36,15 +38,16 @@ interface GitHubSetupModalProps {
|
||||
onSkip?: () => void;
|
||||
}
|
||||
|
||||
type SetupStep = 'auth' | 'repo' | 'branch' | 'complete';
|
||||
type SetupStep = 'github-auth' | 'claude-auth' | 'repo' | 'branch' | 'complete';
|
||||
|
||||
/**
|
||||
* GitHub Setup Modal - Required setup flow after Auto Claude initialization
|
||||
* Setup Modal - Required setup flow after Auto Claude initialization
|
||||
*
|
||||
* Flow:
|
||||
* 1. Authenticate with GitHub (via gh CLI OAuth)
|
||||
* 2. Detect/confirm repository
|
||||
* 3. Select base branch for tasks (with recommended default)
|
||||
* 1. Authenticate with GitHub (via gh CLI OAuth) - for repo operations
|
||||
* 2. Authenticate with Claude (via claude CLI OAuth) - for AI features
|
||||
* 3. Detect/confirm repository
|
||||
* 4. Select base branch for tasks (with recommended default)
|
||||
*/
|
||||
export function GitHubSetupModal({
|
||||
open,
|
||||
@@ -53,7 +56,7 @@ export function GitHubSetupModal({
|
||||
onComplete,
|
||||
onSkip
|
||||
}: GitHubSetupModalProps) {
|
||||
const [step, setStep] = useState<SetupStep>('auth');
|
||||
const [step, setStep] = useState<SetupStep>('github-auth');
|
||||
const [githubToken, setGithubToken] = useState<string | null>(null);
|
||||
const [githubRepo, setGithubRepo] = useState<string | null>(null);
|
||||
const [detectedRepo, setDetectedRepo] = useState<string | null>(null);
|
||||
@@ -67,7 +70,7 @@ export function GitHubSetupModal({
|
||||
// Reset state when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setStep('auth');
|
||||
setStep('github-auth');
|
||||
setGithubToken(null);
|
||||
setGithubRepo(null);
|
||||
setDetectedRepo(null);
|
||||
@@ -140,9 +143,16 @@ export function GitHubSetupModal({
|
||||
return branchList[0] || null;
|
||||
};
|
||||
|
||||
// Handle OAuth success
|
||||
const handleAuthSuccess = async (token: string) => {
|
||||
// Handle GitHub OAuth success
|
||||
const handleGitHubAuthSuccess = async (token: string) => {
|
||||
setGithubToken(token);
|
||||
// Move to Claude auth step
|
||||
setStep('claude-auth');
|
||||
};
|
||||
|
||||
// Handle Claude OAuth success
|
||||
const handleClaudeAuthSuccess = async () => {
|
||||
// Claude token is already saved to active profile by the OAuth flow
|
||||
// Move to repo detection
|
||||
await detectRepository();
|
||||
};
|
||||
@@ -161,7 +171,7 @@ export function GitHubSetupModal({
|
||||
// Render step content
|
||||
const renderStepContent = () => {
|
||||
switch (step) {
|
||||
case 'auth':
|
||||
case 'github-auth':
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
@@ -176,7 +186,29 @@ export function GitHubSetupModal({
|
||||
|
||||
<div className="py-4">
|
||||
<GitHubOAuthFlow
|
||||
onSuccess={handleAuthSuccess}
|
||||
onSuccess={handleGitHubAuthSuccess}
|
||||
onCancel={onSkip}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
case 'claude-auth':
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Key className="h-5 w-5" />
|
||||
Connect to Claude AI
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Auto Claude uses Claude AI for intelligent features like Roadmap generation, Task automation, and Ideation.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
<ClaudeOAuthFlow
|
||||
onSuccess={handleClaudeAuthSuccess}
|
||||
onCancel={onSkip}
|
||||
/>
|
||||
</div>
|
||||
@@ -372,20 +404,27 @@ export function GitHubSetupModal({
|
||||
|
||||
// Progress indicator
|
||||
const renderProgress = () => {
|
||||
const steps: { key: SetupStep; label: string }[] = [
|
||||
{ key: 'auth', label: 'Connect' },
|
||||
{ key: 'branch', label: 'Configure' },
|
||||
const steps: { label: string }[] = [
|
||||
{ label: 'Authenticate' },
|
||||
{ label: 'Configure' },
|
||||
];
|
||||
|
||||
// Don't show progress on complete step
|
||||
if (step === 'complete') return null;
|
||||
|
||||
const currentIndex = step === 'auth' ? 0 : step === 'repo' ? 0 : 1;
|
||||
// Map steps to progress indices
|
||||
// Auth steps (github-auth, claude-auth, repo) = 0
|
||||
// Config steps (branch) = 1
|
||||
const currentIndex =
|
||||
step === 'github-auth' ? 0 :
|
||||
step === 'claude-auth' ? 0 :
|
||||
step === 'repo' ? 0 :
|
||||
1;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-2 mb-4">
|
||||
{steps.map((s, index) => (
|
||||
<div key={s.key} className="flex items-center">
|
||||
<div key={index} className="flex items-center">
|
||||
<div
|
||||
className={`flex items-center justify-center w-6 h-6 rounded-full text-xs font-medium ${
|
||||
index < currentIndex
|
||||
|
||||
@@ -238,6 +238,7 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings
|
||||
onUpdateConfig={updateEnvConfig}
|
||||
gitHubConnectionStatus={gitHubConnectionStatus}
|
||||
isCheckingGitHub={isCheckingGitHub}
|
||||
projectName={project.name}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -18,12 +18,15 @@ import { Input } from '../ui/input';
|
||||
import { Label } from '../ui/label';
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
import { Switch } from '../ui/switch';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger
|
||||
} from '../ui/tooltip';
|
||||
import { useSettingsStore } from '../../stores/settings-store';
|
||||
import type { GraphitiProviderType } from '../../../shared/types';
|
||||
import type { AppSettings } from '../../../shared/types/settings';
|
||||
|
||||
interface GraphitiStepProps {
|
||||
onNext: () => void;
|
||||
@@ -34,12 +37,52 @@ interface GraphitiStepProps {
|
||||
interface GraphitiConfig {
|
||||
enabled: boolean;
|
||||
falkorDbUri: string;
|
||||
openAiApiKey: string;
|
||||
llmProvider: GraphitiProviderType;
|
||||
apiKey: string;
|
||||
ollamaBaseUrl: string; // For Ollama provider (no API key needed)
|
||||
}
|
||||
|
||||
// Provider display info
|
||||
const PROVIDER_INFO: Record<GraphitiProviderType, {
|
||||
name: string;
|
||||
placeholder: string;
|
||||
link: string;
|
||||
requiresApiKey: boolean;
|
||||
description?: string;
|
||||
}> = {
|
||||
openai: { name: 'OpenAI', placeholder: 'sk-...', link: 'https://platform.openai.com/api-keys', requiresApiKey: true },
|
||||
anthropic: { name: 'Anthropic', placeholder: 'sk-ant-...', link: 'https://console.anthropic.com/settings/keys', requiresApiKey: true },
|
||||
google: { name: 'Google (Gemini)', placeholder: 'AIza...', link: 'https://aistudio.google.com/apikey', requiresApiKey: true },
|
||||
groq: { name: 'Groq', placeholder: 'gsk_...', link: 'https://console.groq.com/keys', requiresApiKey: true },
|
||||
ollama: {
|
||||
name: 'Ollama',
|
||||
placeholder: 'http://localhost:11434',
|
||||
link: 'https://ollama.ai',
|
||||
requiresApiKey: false,
|
||||
description: 'Local LLM - no API key required'
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to get the saved API key for a provider from settings
|
||||
function getApiKeyForProvider(provider: GraphitiProviderType, settings: AppSettings): string {
|
||||
switch (provider) {
|
||||
case 'openai': return settings.globalOpenAIApiKey || '';
|
||||
case 'anthropic': return settings.globalAnthropicApiKey || '';
|
||||
case 'google': return settings.globalGoogleApiKey || '';
|
||||
case 'groq': return settings.globalGroqApiKey || '';
|
||||
case 'ollama': return ''; // Ollama doesn't need an API key
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to get the saved Ollama base URL from settings
|
||||
function getOllamaBaseUrl(settings: AppSettings): string {
|
||||
return settings.ollamaBaseUrl || 'http://localhost:11434';
|
||||
}
|
||||
|
||||
interface ValidationStatus {
|
||||
falkordb: { tested: boolean; success: boolean; message: string } | null;
|
||||
openai: { tested: boolean; success: boolean; message: string } | null;
|
||||
llm: { tested: boolean; success: boolean; message: string } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,10 +92,14 @@ interface ValidationStatus {
|
||||
*/
|
||||
export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
const { settings, updateSettings } = useSettingsStore();
|
||||
// Load saved provider preference, defaulting to 'openai'
|
||||
const savedProvider = settings.graphitiLlmProvider || 'openai';
|
||||
const [config, setConfig] = useState<GraphitiConfig>({
|
||||
enabled: false,
|
||||
falkorDbUri: 'bolt://localhost:6379', // Standard FalkorDB port, will be auto-detected from Docker
|
||||
openAiApiKey: settings.globalOpenAIApiKey || ''
|
||||
llmProvider: savedProvider,
|
||||
apiKey: getApiKeyForProvider(savedProvider, settings),
|
||||
ollamaBaseUrl: getOllamaBaseUrl(settings)
|
||||
});
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -63,7 +110,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
const [validationStatus, setValidationStatus] = useState<ValidationStatus>({
|
||||
falkordb: null,
|
||||
openai: null
|
||||
llm: null
|
||||
});
|
||||
|
||||
// Check Docker/Infrastructure availability on mount
|
||||
@@ -99,23 +146,51 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
setError(null);
|
||||
setSuccess(false);
|
||||
// Reset validation status when toggling
|
||||
setValidationStatus({ falkordb: null, openai: null });
|
||||
setValidationStatus({ falkordb: null, llm: null });
|
||||
};
|
||||
|
||||
const handleProviderChange = (provider: GraphitiProviderType) => {
|
||||
// Load saved API key or base URL for the selected provider
|
||||
const savedKey = getApiKeyForProvider(provider, settings);
|
||||
const savedOllamaUrl = getOllamaBaseUrl(settings);
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
llmProvider: provider,
|
||||
apiKey: savedKey,
|
||||
ollamaBaseUrl: savedOllamaUrl
|
||||
}));
|
||||
setValidationStatus(prev => ({ ...prev, llm: null }));
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!config.openAiApiKey.trim()) {
|
||||
setError('Please enter an OpenAI API key to test the connection');
|
||||
const providerName = PROVIDER_INFO[config.llmProvider].name;
|
||||
const providerInfo = PROVIDER_INFO[config.llmProvider];
|
||||
|
||||
// Validate input based on provider type
|
||||
if (providerInfo.requiresApiKey && !config.apiKey.trim()) {
|
||||
setError(`Please enter a ${providerName} API key to test the connection`);
|
||||
return;
|
||||
}
|
||||
if (config.llmProvider === 'ollama' && !config.ollamaBaseUrl.trim()) {
|
||||
setError('Please enter the Ollama server URL to test the connection');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
setError(null);
|
||||
setValidationStatus({ falkordb: null, openai: null });
|
||||
setValidationStatus({ falkordb: null, llm: null });
|
||||
|
||||
try {
|
||||
// For now, we still use the OpenAI test endpoint, but pass the provider info
|
||||
// TODO: Add provider-specific validation endpoints
|
||||
// For Ollama, pass the base URL instead of API key
|
||||
const testCredential = config.llmProvider === 'ollama'
|
||||
? config.ollamaBaseUrl.trim()
|
||||
: config.apiKey.trim();
|
||||
const result = await window.electronAPI.testGraphitiConnection(
|
||||
config.falkorDbUri,
|
||||
config.openAiApiKey.trim()
|
||||
testCredential
|
||||
);
|
||||
|
||||
if (result?.success && result?.data) {
|
||||
@@ -125,7 +200,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
success: result.data.falkordb.success,
|
||||
message: result.data.falkordb.message
|
||||
},
|
||||
openai: {
|
||||
llm: {
|
||||
tested: true,
|
||||
success: result.data.openai.success,
|
||||
message: result.data.openai.message
|
||||
@@ -138,7 +213,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
errors.push(`FalkorDB: ${result.data.falkordb.message}`);
|
||||
}
|
||||
if (!result.data.openai.success) {
|
||||
errors.push(`OpenAI: ${result.data.openai.message}`);
|
||||
errors.push(`${providerName}: ${result.data.openai.message}`);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
setError(errors.join('\n'));
|
||||
@@ -161,8 +236,16 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!config.openAiApiKey.trim()) {
|
||||
setError('OpenAI API key is required for Graphiti embeddings');
|
||||
const providerName = PROVIDER_INFO[config.llmProvider].name;
|
||||
const providerInfo = PROVIDER_INFO[config.llmProvider];
|
||||
|
||||
// Validate input based on provider type
|
||||
if (providerInfo.requiresApiKey && !config.apiKey.trim()) {
|
||||
setError(`${providerName} API key is required for Graphiti`);
|
||||
return;
|
||||
}
|
||||
if (config.llmProvider === 'ollama' && !config.ollamaBaseUrl.trim()) {
|
||||
setError('Ollama server URL is required for Graphiti');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,14 +253,41 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Save OpenAI API key to global settings
|
||||
const result = await window.electronAPI.saveSettings({
|
||||
globalOpenAIApiKey: config.openAiApiKey.trim()
|
||||
});
|
||||
// Build settings update based on selected provider
|
||||
const settingsUpdate: Record<string, string> = {
|
||||
graphitiLlmProvider: config.llmProvider,
|
||||
};
|
||||
|
||||
// Save the API key or base URL for the selected provider
|
||||
if (config.llmProvider === 'openai') {
|
||||
settingsUpdate.globalOpenAIApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'anthropic') {
|
||||
settingsUpdate.globalAnthropicApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'google') {
|
||||
settingsUpdate.globalGoogleApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'groq') {
|
||||
settingsUpdate.globalGroqApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'ollama') {
|
||||
settingsUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim();
|
||||
}
|
||||
|
||||
const result = await window.electronAPI.saveSettings(settingsUpdate);
|
||||
|
||||
if (result?.success) {
|
||||
// Update local settings store
|
||||
updateSettings({ globalOpenAIApiKey: config.openAiApiKey.trim() });
|
||||
// Update local settings store for all providers
|
||||
const storeUpdate: Record<string, string> = {};
|
||||
if (config.llmProvider === 'openai') {
|
||||
storeUpdate.globalOpenAIApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'anthropic') {
|
||||
storeUpdate.globalAnthropicApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'google') {
|
||||
storeUpdate.globalGoogleApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'groq') {
|
||||
storeUpdate.globalGroqApiKey = config.apiKey.trim();
|
||||
} else if (config.llmProvider === 'ollama') {
|
||||
storeUpdate.ollamaBaseUrl = config.ollamaBaseUrl.trim();
|
||||
}
|
||||
updateSettings(storeUpdate);
|
||||
// Proceed to next step immediately after successful save
|
||||
onNext();
|
||||
} else {
|
||||
@@ -344,7 +454,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
Enable Graphiti Memory
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
Requires FalkorDB (Docker) and OpenAI API key
|
||||
Requires FalkorDB (Docker) and an LLM provider (API key or local Ollama)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -360,6 +470,30 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
{/* Configuration fields (shown when enabled) */}
|
||||
{config.enabled && (
|
||||
<div className="space-y-4 animate-in slide-in-from-top-2 duration-200">
|
||||
{/* LLM Provider Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">LLM Provider</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select the AI provider for graph operations
|
||||
</p>
|
||||
<Select
|
||||
value={config.llmProvider}
|
||||
onValueChange={(value) => handleProviderChange(value as GraphitiProviderType)}
|
||||
disabled={isSaving || isValidating}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI (GPT)</SelectItem>
|
||||
<SelectItem value="anthropic">Anthropic (Claude)</SelectItem>
|
||||
<SelectItem value="google">Google (Gemini)</SelectItem>
|
||||
<SelectItem value="groq">Groq (Llama)</SelectItem>
|
||||
<SelectItem value="ollama">Ollama (Local)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* FalkorDB URI */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -399,76 +533,124 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* OpenAI API Key */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="openai-key" className="text-sm font-medium text-foreground">
|
||||
OpenAI API Key
|
||||
</Label>
|
||||
{validationStatus.openai && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{validationStatus.openai.success ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-success" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
)}
|
||||
<span className={`text-xs ${validationStatus.openai.success ? 'text-success' : 'text-destructive'}`}>
|
||||
{validationStatus.openai.success ? 'Valid' : 'Invalid'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
{/* Dynamic credential field based on provider */}
|
||||
{config.llmProvider === 'ollama' ? (
|
||||
/* Ollama Base URL field */
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="ollama-url" className="text-sm font-medium text-foreground">
|
||||
Ollama Server URL
|
||||
</Label>
|
||||
{validationStatus.llm && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{validationStatus.llm.success ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-success" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
)}
|
||||
<span className={`text-xs ${validationStatus.llm.success ? 'text-success' : 'text-destructive'}`}>
|
||||
{validationStatus.llm.success ? 'Connected' : 'Failed'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
id="openai-key"
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
value={config.openAiApiKey}
|
||||
id="ollama-url"
|
||||
type="text"
|
||||
value={config.ollamaBaseUrl}
|
||||
onChange={(e) => {
|
||||
setConfig(prev => ({ ...prev, openAiApiKey: e.target.value }));
|
||||
setValidationStatus(prev => ({ ...prev, openai: null }));
|
||||
setConfig(prev => ({ ...prev, ollamaBaseUrl: e.target.value }));
|
||||
setValidationStatus(prev => ({ ...prev, llm: null }));
|
||||
}}
|
||||
placeholder="sk-..."
|
||||
className="pr-10 font-mono text-sm"
|
||||
placeholder="http://localhost:11434"
|
||||
className="font-mono text-sm"
|
||||
disabled={isSaving || isValidating}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{showApiKey ? 'Hide API key' : 'Show API key'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No API key required. Make sure{' '}
|
||||
<a
|
||||
href="https://ollama.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
Ollama
|
||||
</a>
|
||||
{' '}is running locally.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Required for generating embeddings. Get your key from{' '}
|
||||
<a
|
||||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
OpenAI
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* API Key field for other providers */
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="api-key" className="text-sm font-medium text-foreground">
|
||||
{PROVIDER_INFO[config.llmProvider].name} API Key
|
||||
</Label>
|
||||
{validationStatus.llm && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{validationStatus.llm.success ? (
|
||||
<CheckCircle2 className="h-4 w-4 text-success" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
)}
|
||||
<span className={`text-xs ${validationStatus.llm.success ? 'text-success' : 'text-destructive'}`}>
|
||||
{validationStatus.llm.success ? 'Valid' : 'Invalid'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="api-key"
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
value={config.apiKey}
|
||||
onChange={(e) => {
|
||||
setConfig(prev => ({ ...prev, apiKey: e.target.value }));
|
||||
setValidationStatus(prev => ({ ...prev, llm: null }));
|
||||
}}
|
||||
placeholder={PROVIDER_INFO[config.llmProvider].placeholder}
|
||||
className="pr-10 font-mono text-sm"
|
||||
disabled={isSaving || isValidating}
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{showApiKey ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{showApiKey ? 'Hide API key' : 'Show API key'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Required for graph operations. Get your key from{' '}
|
||||
<a
|
||||
href={PROVIDER_INFO[config.llmProvider].link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:text-primary/80"
|
||||
>
|
||||
{PROVIDER_INFO[config.llmProvider].name}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test Connection Button */}
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleTestConnection}
|
||||
disabled={!config.openAiApiKey.trim() || isValidating || isSaving}
|
||||
disabled={(config.llmProvider === 'ollama' ? !config.ollamaBaseUrl.trim() : !config.apiKey.trim()) || isValidating || isSaving}
|
||||
className="w-full"
|
||||
>
|
||||
{isValidating ? (
|
||||
@@ -483,11 +665,21 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{validationStatus.falkordb?.success && validationStatus.openai?.success && (
|
||||
{validationStatus.falkordb?.success && validationStatus.llm?.success && (
|
||||
<p className="text-xs text-success text-center mt-2">
|
||||
All connections validated successfully!
|
||||
</p>
|
||||
)}
|
||||
{config.llmProvider !== 'openai' && config.llmProvider !== 'ollama' && (
|
||||
<p className="text-xs text-muted-foreground text-center mt-2">
|
||||
Note: API key validation currently only fully supports OpenAI. Your {PROVIDER_INFO[config.llmProvider].name} key will be saved and used at runtime.
|
||||
</p>
|
||||
)}
|
||||
{config.llmProvider === 'ollama' && (
|
||||
<p className="text-xs text-muted-foreground text-center mt-2">
|
||||
Note: Ollama connection will be tested by checking if the server is reachable.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -515,7 +707,7 @@ export function GraphitiStep({ onNext, onBack, onSkip }: GraphitiStepProps) {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={isCheckingDocker || (config.enabled && !config.openAiApiKey.trim() && !success) || isSaving || isValidating}
|
||||
disabled={isCheckingDocker || (config.enabled && !success && (config.llmProvider === 'ollama' ? !config.ollamaBaseUrl.trim() : !config.apiKey.trim())) || isSaving || isValidating}
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Key,
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Info,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { Card, CardContent } from '../ui/card';
|
||||
|
||||
interface ClaudeOAuthFlowProps {
|
||||
onSuccess: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claude OAuth flow component for setup wizard
|
||||
* Guides users through authenticating with Claude using claude setup-token
|
||||
*/
|
||||
export function ClaudeOAuthFlow({ onSuccess, onCancel }: ClaudeOAuthFlowProps) {
|
||||
const [status, setStatus] = useState<'ready' | 'authenticating' | 'success' | 'error'>('ready');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [email, setEmail] = useState<string | undefined>();
|
||||
|
||||
// Track if we've already started auth to prevent double-execution
|
||||
const hasStartedRef = useRef(false);
|
||||
const listenerSetupRef = useRef(false);
|
||||
|
||||
// Listen for OAuth token detection
|
||||
useEffect(() => {
|
||||
if (listenerSetupRef.current) return;
|
||||
listenerSetupRef.current = true;
|
||||
|
||||
const unsubscribe = window.electronAPI.onTerminalOAuthToken((info) => {
|
||||
console.warn('[ClaudeOAuth] Token event received:', {
|
||||
success: info.success,
|
||||
hasEmail: !!info.email,
|
||||
profileId: info.profileId
|
||||
});
|
||||
|
||||
if (info.success) {
|
||||
setEmail(info.email);
|
||||
setStatus('success');
|
||||
// Auto-advance after a short delay to show success message
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
}, 1500);
|
||||
} else {
|
||||
setError(info.message || 'Failed to save OAuth token');
|
||||
setStatus('error');
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (unsubscribe) {
|
||||
unsubscribe();
|
||||
}
|
||||
};
|
||||
}, [onSuccess]);
|
||||
|
||||
const handleStartAuth = async () => {
|
||||
if (hasStartedRef.current) {
|
||||
console.warn('[ClaudeOAuth] Auth already started, ignoring duplicate call');
|
||||
return;
|
||||
}
|
||||
hasStartedRef.current = true;
|
||||
|
||||
console.warn('[ClaudeOAuth] Starting Claude authentication');
|
||||
setStatus('authenticating');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get the active profile ID
|
||||
const profilesResult = await window.electronAPI.getClaudeProfiles();
|
||||
|
||||
if (!profilesResult.success || !profilesResult.data) {
|
||||
throw new Error('Failed to get Claude profiles');
|
||||
}
|
||||
|
||||
const activeProfileId = profilesResult.data.activeProfileId;
|
||||
console.warn('[ClaudeOAuth] Initializing profile:', activeProfileId);
|
||||
|
||||
// Initialize the profile - this opens a terminal and runs 'claude setup-token'
|
||||
const result = await window.electronAPI.initializeClaudeProfile(activeProfileId);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to start authentication');
|
||||
}
|
||||
|
||||
console.warn('[ClaudeOAuth] Authentication started, waiting for token...');
|
||||
// Status will be updated by the event listener when token is detected
|
||||
} catch (err) {
|
||||
console.error('[ClaudeOAuth] Authentication failed:', err);
|
||||
setError(err instanceof Error ? err.message : 'Authentication failed');
|
||||
setStatus('error');
|
||||
hasStartedRef.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRetry = () => {
|
||||
hasStartedRef.current = false;
|
||||
setStatus('ready');
|
||||
setError(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Ready to authenticate */}
|
||||
{status === 'ready' && (
|
||||
<div className="space-y-4">
|
||||
<Card className="border border-info/30 bg-info/10">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<Key className="h-6 w-6 text-info shrink-0 mt-0.5" />
|
||||
<div className="flex-1 space-y-3">
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
Authenticate with Claude
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Auto Claude requires Claude AI authentication for AI-powered features like
|
||||
Roadmap generation, Task automation, and Ideation.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This will open a browser window to authenticate with your Claude account.
|
||||
Your credentials are stored securely and are valid for 1 year.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Button onClick={handleStartAuth} size="lg" className="gap-2">
|
||||
<Key className="h-5 w-5" />
|
||||
Authenticate with Claude
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authenticating */}
|
||||
{status === 'authenticating' && (
|
||||
<Card className="border border-info/30 bg-info/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-info shrink-0" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-foreground">
|
||||
Authenticating...
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
A terminal window has opened. Please complete the authentication in your browser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-background/50 p-3 space-y-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5" />
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p className="font-medium">What's happening:</p>
|
||||
<ol className="list-decimal list-inside space-y-1 ml-2">
|
||||
<li>A terminal opened and ran <code className="px-1 bg-muted rounded">claude setup-token</code></li>
|
||||
<li>Your browser should open to authenticate with Claude</li>
|
||||
<li>Complete the OAuth flow in your browser</li>
|
||||
<li>The terminal will display your token (starts with sk-ant-oat01-...)</li>
|
||||
<li>Auto Claude will automatically detect and save it</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Success */}
|
||||
{status === 'success' && (
|
||||
<Card className="border border-success/30 bg-success/10">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<CheckCircle2 className="h-6 w-6 text-success shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-success">
|
||||
Successfully Authenticated!
|
||||
</h3>
|
||||
<p className="text-sm text-success/80 mt-1">
|
||||
{email ? `Connected as ${email}` : 'Your Claude credentials have been saved'}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mt-3 text-xs text-success/70">
|
||||
<Sparkles className="h-3 w-3" />
|
||||
<span>You can now use all Auto Claude AI features</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{status === 'error' && error && (
|
||||
<div className="space-y-4">
|
||||
<Card className="border border-destructive/30 bg-destructive/10">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-destructive shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-destructive">
|
||||
Authentication Failed
|
||||
</h3>
|
||||
<p className="text-sm text-destructive/80 mt-1">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center gap-3">
|
||||
<Button onClick={handleRetry} variant="outline">
|
||||
Retry
|
||||
</Button>
|
||||
{onCancel && (
|
||||
<Button onClick={onCancel} variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cancel button for ready/authenticating states */}
|
||||
{(status === 'ready' || status === 'authenticating') && onCancel && (
|
||||
<div className="flex justify-center pt-2">
|
||||
<Button onClick={onCancel} variant="ghost" size="sm">
|
||||
Skip for now
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+19
-1
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Github, RefreshCw, KeyRound } from 'lucide-react';
|
||||
import { Github, RefreshCw, KeyRound, Info } from 'lucide-react';
|
||||
import { CollapsibleSection } from './CollapsibleSection';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
import { PasswordInput } from './PasswordInput';
|
||||
@@ -19,6 +19,7 @@ interface GitHubIntegrationSectionProps {
|
||||
onUpdateConfig: (updates: Partial<ProjectEnvConfig>) => void;
|
||||
gitHubConnectionStatus: GitHubSyncStatus | null;
|
||||
isCheckingGitHub: boolean;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export function GitHubIntegrationSection({
|
||||
@@ -28,6 +29,7 @@ export function GitHubIntegrationSection({
|
||||
onUpdateConfig,
|
||||
gitHubConnectionStatus,
|
||||
isCheckingGitHub,
|
||||
projectName,
|
||||
}: GitHubIntegrationSectionProps) {
|
||||
const [showOAuthFlow, setShowOAuthFlow] = useState(false);
|
||||
|
||||
@@ -48,6 +50,22 @@ export function GitHubIntegrationSection({
|
||||
onToggle={onToggle}
|
||||
badge={badge}
|
||||
>
|
||||
{/* Project-Specific Configuration Notice */}
|
||||
{projectName && (
|
||||
<div className="rounded-lg border border-info/30 bg-info/5 p-3 mb-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<Info className="h-4 w-4 text-info mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-foreground">Project-Specific Configuration</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
This GitHub repository is configured only for <span className="font-semibold text-foreground">{projectName}</span>.
|
||||
Each project can have its own GitHub repository.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="font-normal text-foreground">Enable GitHub Issues</Label>
|
||||
|
||||
@@ -205,17 +205,26 @@ export async function initializeProject(
|
||||
const store = useProjectStore.getState();
|
||||
|
||||
try {
|
||||
console.log('[ProjectStore] initializeProject called for:', projectId);
|
||||
const result = await window.electronAPI.initializeProject(projectId);
|
||||
console.log('[ProjectStore] IPC result:', result);
|
||||
|
||||
if (result.success && result.data) {
|
||||
console.log('[ProjectStore] IPC succeeded, result.data:', result.data);
|
||||
// Update the project's autoBuildPath in local state
|
||||
if (result.data.success) {
|
||||
console.log('[ProjectStore] Updating project autoBuildPath to .auto-claude');
|
||||
store.updateProject(projectId, { autoBuildPath: '.auto-claude' });
|
||||
} else {
|
||||
console.log('[ProjectStore] result.data.success is false, not updating project');
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
console.log('[ProjectStore] IPC failed or no data, setting error');
|
||||
store.setError(result.error || 'Failed to initialize project');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('[ProjectStore] Exception during initializeProject:', error);
|
||||
store.setError(error instanceof Error ? error.message : 'Unknown error');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -186,8 +186,8 @@ export interface GraphitiConnectionTestResult {
|
||||
}
|
||||
|
||||
// Graphiti Provider Types (Memory System V2)
|
||||
export type GraphitiProviderType = 'openai' | 'anthropic' | 'google' | 'groq';
|
||||
export type GraphitiEmbeddingProvider = 'openai' | 'voyage' | 'google' | 'huggingface';
|
||||
export type GraphitiProviderType = 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama';
|
||||
export type GraphitiEmbeddingProvider = 'openai' | 'voyage' | 'google' | 'huggingface' | 'ollama';
|
||||
|
||||
export interface GraphitiProviderConfig {
|
||||
// LLM Provider
|
||||
@@ -205,6 +205,12 @@ export interface GraphitiProviderConfig {
|
||||
groqApiKey?: string;
|
||||
voyageApiKey?: string;
|
||||
|
||||
// Ollama-specific config (local LLM, no API key required)
|
||||
ollamaBaseUrl?: string; // Default: http://localhost:11434
|
||||
ollamaLlmModel?: string;
|
||||
ollamaEmbeddingModel?: string;
|
||||
ollamaEmbeddingDim?: number;
|
||||
|
||||
// FalkorDB connection (required for all providers)
|
||||
falkorDbHost?: string;
|
||||
falkorDbPort?: number;
|
||||
|
||||
@@ -54,6 +54,12 @@ export interface AppSettings {
|
||||
// Global API keys (used as defaults for all projects)
|
||||
globalClaudeOAuthToken?: string;
|
||||
globalOpenAIApiKey?: string;
|
||||
globalAnthropicApiKey?: string;
|
||||
globalGoogleApiKey?: string;
|
||||
globalGroqApiKey?: string;
|
||||
// Graphiti LLM provider settings
|
||||
graphitiLlmProvider?: 'openai' | 'anthropic' | 'google' | 'groq' | 'ollama';
|
||||
ollamaBaseUrl?: string;
|
||||
// Onboarding wizard completion state
|
||||
onboardingCompleted?: boolean;
|
||||
// Selected agent profile for preset model/thinking configurations
|
||||
|
||||
@@ -351,8 +351,10 @@ export interface MergeStats {
|
||||
export interface WorktreeMergeResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
merged?: boolean;
|
||||
conflictFiles?: string[];
|
||||
staged?: boolean;
|
||||
alreadyStaged?: boolean;
|
||||
projectPath?: string;
|
||||
// New conflict info from smart merge
|
||||
conflicts?: MergeConflict[];
|
||||
|
||||
@@ -91,7 +91,12 @@ def handle_qa_command(
|
||||
if not validate_environment(spec_dir):
|
||||
sys.exit(1)
|
||||
|
||||
if not should_run_qa(spec_dir):
|
||||
# Check if there's pending human feedback that needs to be processed
|
||||
# Human feedback takes priority over "already approved" status
|
||||
fix_request_file = spec_dir / "QA_FIX_REQUEST.md"
|
||||
has_human_feedback = fix_request_file.exists()
|
||||
|
||||
if not should_run_qa(spec_dir) and not has_human_feedback:
|
||||
if is_qa_approved(spec_dir):
|
||||
print("\n✅ Build already approved by QA.")
|
||||
else:
|
||||
@@ -100,6 +105,9 @@ def handle_qa_command(
|
||||
print("Complete all subtasks before running QA validation.")
|
||||
return
|
||||
|
||||
if has_human_feedback:
|
||||
print("\n📝 Human feedback detected - processing fix request...")
|
||||
|
||||
try:
|
||||
approved = asyncio.run(
|
||||
run_qa_validation_loop(
|
||||
|
||||
@@ -144,24 +144,31 @@ try:
|
||||
except ImportError:
|
||||
|
||||
def debug(*args, **kwargs):
|
||||
"""Fallback debug function when debug module is not available."""
|
||||
pass
|
||||
|
||||
def debug_detailed(*args, **kwargs):
|
||||
"""Fallback debug_detailed function when debug module is not available."""
|
||||
pass
|
||||
|
||||
def debug_verbose(*args, **kwargs):
|
||||
"""Fallback debug_verbose function when debug module is not available."""
|
||||
pass
|
||||
|
||||
def debug_success(*args, **kwargs):
|
||||
"""Fallback debug_success function when debug module is not available."""
|
||||
pass
|
||||
|
||||
def debug_error(*args, **kwargs):
|
||||
"""Fallback debug_error function when debug module is not available."""
|
||||
pass
|
||||
|
||||
def debug_section(*args, **kwargs):
|
||||
"""Fallback debug_section function when debug module is not available."""
|
||||
pass
|
||||
|
||||
def is_debug_enabled():
|
||||
"""Fallback is_debug_enabled function when debug module is not available."""
|
||||
return False
|
||||
|
||||
|
||||
|
||||
+50
-2
@@ -108,12 +108,60 @@ async def run_qa_validation_loop(
|
||||
print(f" Progress: {completed}/{total} subtasks completed")
|
||||
return False
|
||||
|
||||
# Check if already approved
|
||||
if is_qa_approved(spec_dir):
|
||||
# Check if there's pending human feedback that needs to be processed
|
||||
fix_request_file = spec_dir / "QA_FIX_REQUEST.md"
|
||||
has_human_feedback = fix_request_file.exists()
|
||||
|
||||
# Check if already approved - but if there's human feedback, we need to process it first
|
||||
if is_qa_approved(spec_dir) and not has_human_feedback:
|
||||
debug_success("qa_loop", "Build already approved by QA")
|
||||
print("\n✅ Build already approved by QA.")
|
||||
return True
|
||||
|
||||
# If there's human feedback, we need to run the fixer first before re-validating
|
||||
if has_human_feedback:
|
||||
debug(
|
||||
"qa_loop",
|
||||
"Human feedback detected - will run fixer first",
|
||||
fix_request_file=str(fix_request_file),
|
||||
)
|
||||
print("\n📝 Human feedback detected. Running QA Fixer first...")
|
||||
|
||||
# Get model for fixer
|
||||
qa_model = get_phase_model(spec_dir, "qa", model)
|
||||
fixer_thinking_budget = get_thinking_budget("medium")
|
||||
|
||||
fix_client = create_client(
|
||||
project_dir,
|
||||
spec_dir,
|
||||
qa_model,
|
||||
agent_type="qa_fixer",
|
||||
max_thinking_tokens=fixer_thinking_budget,
|
||||
)
|
||||
|
||||
async with fix_client:
|
||||
fix_status, fix_response = await run_qa_fixer_session(
|
||||
fix_client,
|
||||
spec_dir,
|
||||
0,
|
||||
False, # iteration 0 for human feedback
|
||||
)
|
||||
|
||||
if fix_status == "error":
|
||||
debug_error("qa_loop", f"Fixer error: {fix_response[:200]}")
|
||||
print(f"\n❌ Fixer encountered error: {fix_response}")
|
||||
return False
|
||||
|
||||
debug_success("qa_loop", "Human feedback fixes applied")
|
||||
print("\n✅ Fixes applied based on human feedback. Running QA validation...")
|
||||
|
||||
# Remove the fix request file after processing
|
||||
try:
|
||||
fix_request_file.unlink()
|
||||
debug("qa_loop", "Removed processed QA_FIX_REQUEST.md")
|
||||
except OSError:
|
||||
pass # Ignore if file removal fails
|
||||
|
||||
# Check for no-test projects
|
||||
if is_no_test_project(spec_dir, project_dir):
|
||||
print("\n⚠️ No test framework detected in project.")
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Version Bump Script
|
||||
*
|
||||
* Automatically bumps the version in package.json and creates a git tag.
|
||||
* This ensures version consistency between package.json and git tags.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/bump-version.js <major|minor|patch|x.y.z>
|
||||
*
|
||||
* Examples:
|
||||
* node scripts/bump-version.js patch # 2.5.5 -> 2.5.6
|
||||
* node scripts/bump-version.js minor # 2.5.5 -> 2.6.0
|
||||
* node scripts/bump-version.js major # 2.5.5 -> 3.0.0
|
||||
* node scripts/bump-version.js 2.6.0 # Set to specific version
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Colors for terminal output
|
||||
const colors = {
|
||||
reset: '\x1b[0m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
};
|
||||
|
||||
function log(message, color = colors.reset) {
|
||||
console.log(`${color}${message}${colors.reset}`);
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
log(`❌ Error: ${message}`, colors.red);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function success(message) {
|
||||
log(`✅ ${message}`, colors.green);
|
||||
}
|
||||
|
||||
function info(message) {
|
||||
log(`ℹ️ ${message}`, colors.cyan);
|
||||
}
|
||||
|
||||
function warning(message) {
|
||||
log(`⚠️ ${message}`, colors.yellow);
|
||||
}
|
||||
|
||||
// Parse semver version
|
||||
function parseVersion(version) {
|
||||
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!match) {
|
||||
error(`Invalid version format: ${version}. Expected format: x.y.z`);
|
||||
}
|
||||
return {
|
||||
major: parseInt(match[1]),
|
||||
minor: parseInt(match[2]),
|
||||
patch: parseInt(match[3]),
|
||||
};
|
||||
}
|
||||
|
||||
// Bump version based on type
|
||||
function bumpVersion(currentVersion, bumpType) {
|
||||
const version = parseVersion(currentVersion);
|
||||
|
||||
switch (bumpType) {
|
||||
case 'major':
|
||||
return `${version.major + 1}.0.0`;
|
||||
case 'minor':
|
||||
return `${version.major}.${version.minor + 1}.0`;
|
||||
case 'patch':
|
||||
return `${version.major}.${version.minor}.${version.patch + 1}`;
|
||||
default:
|
||||
// Assume it's a specific version
|
||||
parseVersion(bumpType); // Validate format
|
||||
return bumpType;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute shell command
|
||||
function exec(command, options = {}) {
|
||||
try {
|
||||
return execSync(command, { encoding: 'utf8', stdio: 'pipe', ...options }).trim();
|
||||
} catch (err) {
|
||||
error(`Command failed: ${command}\n${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if git working directory is clean
|
||||
function checkGitStatus() {
|
||||
const status = exec('git status --porcelain');
|
||||
if (status) {
|
||||
error('Git working directory is not clean. Please commit or stash changes first.');
|
||||
}
|
||||
}
|
||||
|
||||
// Update package.json version
|
||||
function updatePackageJson(newVersion) {
|
||||
const packagePath = path.join(__dirname, '..', 'auto-claude-ui', 'package.json');
|
||||
|
||||
if (!fs.existsSync(packagePath)) {
|
||||
error(`package.json not found at ${packagePath}`);
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
const oldVersion = packageJson.version;
|
||||
|
||||
packageJson.version = newVersion;
|
||||
|
||||
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||
|
||||
return { oldVersion, packagePath };
|
||||
}
|
||||
|
||||
// Main function
|
||||
function main() {
|
||||
const bumpType = process.argv[2];
|
||||
|
||||
if (!bumpType) {
|
||||
error('Please specify version bump type or version number.\n' +
|
||||
'Usage: node scripts/bump-version.js <major|minor|patch|x.y.z>');
|
||||
}
|
||||
|
||||
log('\n🚀 Auto Claude Version Bump\n', colors.cyan);
|
||||
|
||||
// 1. Check git status
|
||||
info('Checking git status...');
|
||||
checkGitStatus();
|
||||
success('Git working directory is clean');
|
||||
|
||||
// 2. Read current version
|
||||
const packagePath = path.join(__dirname, '..', 'auto-claude-ui', 'package.json');
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
const currentVersion = packageJson.version;
|
||||
info(`Current version: ${currentVersion}`);
|
||||
|
||||
// 3. Calculate new version
|
||||
const newVersion = bumpVersion(currentVersion, bumpType);
|
||||
info(`New version: ${newVersion}`);
|
||||
|
||||
if (currentVersion === newVersion) {
|
||||
error('New version is the same as current version');
|
||||
}
|
||||
|
||||
// 4. Update package.json
|
||||
info('Updating package.json...');
|
||||
updatePackageJson(newVersion);
|
||||
success('Updated package.json');
|
||||
|
||||
// 5. Create git commit
|
||||
info('Creating git commit...');
|
||||
exec('git add auto-claude-ui/package.json');
|
||||
exec(`git commit -m "chore: bump version to ${newVersion}"`);
|
||||
success(`Created commit: "chore: bump version to ${newVersion}"`);
|
||||
|
||||
// 6. Create git tag
|
||||
info('Creating git tag...');
|
||||
exec(`git tag -a v${newVersion} -m "Release v${newVersion}"`);
|
||||
success(`Created tag: v${newVersion}`);
|
||||
|
||||
// 7. Instructions
|
||||
log('\n📋 Next steps:', colors.yellow);
|
||||
log(` 1. Review the changes: git log -1`, colors.yellow);
|
||||
log(` 2. Push the commit: git push origin <branch-name>`, colors.yellow);
|
||||
log(` 3. Push the tag: git push origin v${newVersion}`, colors.yellow);
|
||||
log(` 4. Create a GitHub release from the tag\n`, colors.yellow);
|
||||
|
||||
warning('Note: The commit and tag have been created locally but NOT pushed.');
|
||||
warning('Please review and push manually when ready.');
|
||||
|
||||
log('\n✨ Version bump complete!\n', colors.green);
|
||||
}
|
||||
|
||||
// Run
|
||||
main();
|
||||
Reference in New Issue
Block a user