fix(python): use venv Python for all services to fix dotenv errors (#311)

* fix(python): use venv Python for all services to fix dotenv errors

Services were spawning Python processes using findPythonCommand() which
returns the bundled Python directly. However, dependencies like python-dotenv
are only installed in the venv created from the bundled Python.

Changes:
- Add getConfiguredPythonPath() helper that returns venv Python when ready
- Update all services to use venv Python instead of bundled Python directly:
  - memory-service.ts
  - memory-handlers.ts
  - agent-process.ts
  - changelog-service.ts
  - title-generator.ts
  - insights/config.ts
  - project-context-handlers.ts
  - worktree-handlers.ts
- Fix availability checks to use findPythonCommand() (can return null)
- Add python:verify script for bundling verification

The flow now works correctly:
1. App starts → findPythonCommand() finds bundled Python
2. pythonEnvManager creates venv using bundled Python
3. pip installs dependencies (dotenv, claude-agent-sdk, etc.)
4. All services use venv Python → has all dependencies

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

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

* fix lintin and test

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Alex
2025-12-26 17:14:57 +01:00
committed by GitHub
parent 1c14227324
commit 92c6f2786d
12 changed files with 244 additions and 87 deletions
+28 -26
View File
@@ -195,6 +195,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -579,6 +580,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -622,6 +624,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -661,6 +664,7 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -1067,7 +1071,6 @@
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": {
"cross-dirname": "^0.1.0",
"debug": "^4.3.4",
@@ -1089,7 +1092,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
@@ -4212,8 +4214,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4400,6 +4401,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4410,6 +4412,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4501,6 +4504,7 @@
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.1",
"@typescript-eslint/types": "8.50.1",
@@ -4913,6 +4917,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4973,6 +4978,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -5145,7 +5151,6 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -5540,6 +5545,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6210,8 +6216,7 @@
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
"optional": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@@ -6561,6 +6566,7 @@
"integrity": "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"app-builder-lib": "26.0.12",
"builder-util": "26.0.11",
@@ -6618,8 +6624,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.6.1",
@@ -6695,6 +6700,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^22.7.7",
@@ -6823,7 +6829,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@electron/asar": "^3.2.1",
"debug": "^4.1.1",
@@ -6844,7 +6849,6 @@
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"graceful-fs": "^4.1.2",
"jsonfile": "^4.0.0",
@@ -6860,7 +6864,6 @@
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
"dev": true,
"license": "MIT",
"peer": true,
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
@@ -6871,7 +6874,6 @@
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 4.0.0"
}
@@ -7241,6 +7243,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8486,6 +8489,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -9276,6 +9280,7 @@
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.28",
"@asamuzakjp/dom-selector": "^6.7.6",
@@ -10207,7 +10212,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -12398,6 +12402,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -12495,6 +12500,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -12531,7 +12537,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"commander": "^9.4.0"
},
@@ -12549,7 +12554,6 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": "^12.20.0 || >=14"
}
@@ -12570,7 +12574,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12586,7 +12589,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12599,8 +12601,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/proc-log": {
"version": "2.0.1",
@@ -12704,6 +12705,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12713,6 +12715,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -14032,7 +14035,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -14089,7 +14093,6 @@
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
@@ -14116,7 +14119,6 @@
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -14138,7 +14140,6 @@
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -14152,7 +14153,6 @@
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"minimist": "^1.2.6"
},
@@ -14167,7 +14167,6 @@
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"dev": true,
"license": "ISC",
"peer": true,
"dependencies": {
"glob": "^7.1.3"
},
@@ -14484,6 +14483,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14833,6 +14833,7 @@
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -15875,6 +15876,7 @@
"integrity": "sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+1
View File
@@ -30,6 +30,7 @@
"rebuild": "electron-rebuild",
"python:download": "node scripts/download-python.cjs",
"python:download:all": "node scripts/download-python.cjs --all",
"python:verify": "node scripts/verify-python-bundling.cjs",
"package": "npm run python:download && electron-vite build && electron-builder",
"package:mac": "npm run python:download && electron-vite build && electron-builder --mac",
"package:win": "npm run python:download && electron-vite build && electron-builder --win",
@@ -0,0 +1,102 @@
#!/usr/bin/env node
/**
* Verify Python bundling configuration is correct.
* Run this before packaging to ensure Python will be properly bundled.
*/
const fs = require('fs');
const path = require('path');
const { execSync, spawnSync } = require('child_process');
const FRONTEND_DIR = path.resolve(__dirname, '..');
const PYTHON_RUNTIME_DIR = path.join(FRONTEND_DIR, 'python-runtime');
console.log('=== Python Bundling Verification ===\n');
// Check 1: Python runtime downloaded?
console.log('1. Checking if Python runtime is downloaded...');
const platform = process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux';
const arch = process.arch;
const runtimePath = path.join(PYTHON_RUNTIME_DIR, `${platform}-${arch}`, 'python');
if (fs.existsSync(runtimePath)) {
const pythonExe = process.platform === 'win32'
? path.join(runtimePath, 'python.exe')
: path.join(runtimePath, 'bin', 'python3');
if (fs.existsSync(pythonExe)) {
console.log(` ✓ Found bundled Python at: ${pythonExe}`);
// Test version
try {
const version = execSync(`"${pythonExe}" --version`, { encoding: 'utf8' }).trim();
console.log(` ✓ Version: ${version}`);
} catch (e) {
console.log(` ✗ Failed to get version: ${e.message}`);
}
} else {
console.log(` ✗ Python executable not found at: ${pythonExe}`);
}
} else {
console.log(` ✗ Python runtime not downloaded. Run: npm run python:download`);
}
// Check 2: package.json extraResources configured?
console.log('\n2. Checking package.json extraResources configuration...');
const packageJson = require(path.join(FRONTEND_DIR, 'package.json'));
const extraResources = packageJson.build?.extraResources || [];
const pythonResource = extraResources.find(r =>
(typeof r === 'string' && r.includes('python')) ||
(typeof r === 'object' && r.from?.includes('python'))
);
if (pythonResource) {
console.log(' ✓ Python is configured in extraResources:');
console.log(` ${JSON.stringify(pythonResource)}`);
} else {
console.log(' ✗ Python not found in extraResources configuration');
}
// Check 3: Test venv creation simulation
console.log('\n3. Checking venv creation capability...');
try {
// Find system Python for testing
let pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
const result = spawnSync(pythonCmd, ['-m', 'venv', '--help'], { encoding: 'utf8' });
if (result.status === 0) {
console.log(` ✓ venv module is available`);
} else {
console.log(` ✗ venv module not available: ${result.stderr}`);
}
} catch (e) {
console.log(` ✗ Failed to check venv: ${e.message}`);
}
// Check 4: Verify requirements.txt exists
console.log('\n4. Checking requirements.txt...');
const backendDir = path.join(FRONTEND_DIR, '..', 'backend');
const requirementsPath = path.join(backendDir, 'requirements.txt');
if (fs.existsSync(requirementsPath)) {
const content = fs.readFileSync(requirementsPath, 'utf8');
const hasDotenv = content.includes('python-dotenv');
const hasSDK = content.includes('claude-agent-sdk');
console.log(` ✓ requirements.txt found`);
console.log(` ${hasDotenv ? '✓' : '✗'} python-dotenv: ${hasDotenv ? 'present' : 'MISSING!'}`);
console.log(` ${hasSDK ? '✓' : '✗'} claude-agent-sdk: ${hasSDK ? 'present' : 'MISSING!'}`);
} else {
console.log(` ✗ requirements.txt not found at: ${requirementsPath}`);
}
// Summary
console.log('\n=== Summary ===');
console.log('To fully test Python bundling:');
console.log('1. Run: npm run python:download');
console.log('2. Run: npm run package:win (or :mac/:linux)');
console.log('3. Launch the packaged app and check Dev Tools console for:');
console.log(' - "[Python] Found bundled Python at: ..."');
console.log(' - "[PythonEnvManager] Ready with Python path: ..."');
console.log('4. Try creating and running a task - should work without dotenv errors');
+16 -7
View File
@@ -9,7 +9,8 @@ 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';
import { parsePythonCommand } from '../python-detector';
import { getConfiguredPythonPath } from '../python-env-manager';
/**
* Process spawning and lifecycle management
@@ -18,8 +19,9 @@ export class AgentProcessManager {
private state: AgentState;
private events: AgentEvents;
private emitter: EventEmitter;
// Auto-detect Python command on initialization
private pythonPath: string = findPythonCommand() || 'python';
// Python path will be configured by pythonEnvManager after venv is ready
// Use null to indicate not yet configured - getPythonPath() will use fallback
private _pythonPath: string | null = null;
private autoBuildSourcePath: string = '';
constructor(state: AgentState, events: AgentEvents, emitter: EventEmitter) {
@@ -33,7 +35,7 @@ export class AgentProcessManager {
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
if (pythonPath) {
this.pythonPath = pythonPath;
this._pythonPath = pythonPath;
}
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
@@ -41,10 +43,17 @@ export class AgentProcessManager {
}
/**
* Get the configured Python path
* Get the configured Python path.
* Returns explicitly configured path, or falls back to getConfiguredPythonPath()
* which uses the venv Python if ready.
*/
getPythonPath(): string {
return this.pythonPath;
// If explicitly configured (by pythonEnvManager), use that
if (this._pythonPath) {
return this._pythonPath;
}
// Otherwise use the global configured path (venv if ready, else bundled/system)
return getConfiguredPythonPath();
}
/**
@@ -168,7 +177,7 @@ export class AgentProcessManager {
const profileEnv = getProfileEnv();
// Parse Python command to handle space-separated commands like "py -3"
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.pythonPath);
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(this.getPythonPath());
const childProcess = spawn(pythonCommand, [...pythonBaseArgs, ...args], {
cwd,
env: {
@@ -27,15 +27,15 @@ import {
getCommits,
getBranchDiffCommits
} from './git-integration';
import { findPythonCommand } from '../python-detector';
import { getConfiguredPythonPath } from '../python-env-manager';
/**
* Main changelog service - orchestrates all changelog operations
* Delegates to specialized modules for specific concerns
*/
export class ChangelogService extends EventEmitter {
// Auto-detect Python command on initialization
private pythonPath: string = findPythonCommand() || 'python';
// Python path will be configured by pythonEnvManager after venv is ready
private _pythonPath: string | null = null;
private claudePath: string = 'claude';
private autoBuildSourcePath: string = '';
private cachedEnv: Record<string, string> | null = null;
@@ -130,13 +130,25 @@ export class ChangelogService extends EventEmitter {
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
if (pythonPath) {
this.pythonPath = pythonPath;
this._pythonPath = pythonPath;
}
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
}
}
/**
* Get the configured Python path.
* Returns explicitly configured path, or falls back to getConfiguredPythonPath()
* which uses the venv Python if ready.
*/
private get pythonPath(): string {
if (this._pythonPath) {
return this._pythonPath;
}
return getConfiguredPythonPath();
}
/**
* Get the auto-claude source path (detects automatically if not configured)
*/
+14 -6
View File
@@ -2,15 +2,16 @@ import path from 'path';
import { existsSync, readFileSync } from 'fs';
import { app } from 'electron';
import { getProfileEnv } from '../rate-limit-detector';
import { findPythonCommand } from '../python-detector';
import { getConfiguredPythonPath } from '../python-env-manager';
/**
* Configuration manager for insights service
* Handles path detection and environment variable loading
*/
export class InsightsConfig {
// Auto-detect Python command on initialization
private pythonPath: string = findPythonCommand() || 'python';
// Python path will be configured by pythonEnvManager after venv is ready
// Use getter to always get current configured path
private _pythonPath: string | null = null;
private autoBuildSourcePath: string = '';
/**
@@ -18,7 +19,7 @@ export class InsightsConfig {
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
if (pythonPath) {
this.pythonPath = pythonPath;
this._pythonPath = pythonPath;
}
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
@@ -26,10 +27,17 @@ export class InsightsConfig {
}
/**
* Get configured Python path
* Get configured Python path.
* Returns explicitly configured path, or falls back to getConfiguredPythonPath()
* which uses the venv Python if ready.
*/
getPythonPath(): string {
return this.pythonPath;
// If explicitly configured (by pythonEnvManager), use that
if (this._pythonPath) {
return this._pythonPath;
}
// Otherwise use the global configured path (venv if ready, else bundled/system)
return getConfiguredPythonPath();
}
/**
@@ -1,4 +1,4 @@
import { ipcMain, app } from 'electron';
import { ipcMain } from 'electron';
import type { BrowserWindow } from 'electron';
import path from 'path';
import { existsSync, readFileSync } from 'fs';
@@ -21,7 +21,8 @@ import {
buildMemoryStatus
} from './memory-status-handlers';
import { loadFileBasedMemories } from './memory-data-handlers';
import { findPythonCommand, parsePythonCommand } from '../../python-detector';
import { parsePythonCommand } from '../../python-detector';
import { getConfiguredPythonPath } from '../../python-env-manager';
import { getAugmentedEnv } from '../../env-utils';
/**
@@ -159,20 +160,10 @@ export function registerProjectContextHandlers(
const analyzerPath = path.join(autoBuildSource, 'analyzer.py');
const indexOutputPath = path.join(project.path, AUTO_BUILD_PATHS.PROJECT_INDEX);
// Get Python command directly from settings file (not pythonEnvManager which creates NEW venv)
let pythonCmd = findPythonCommand() || 'python3';
try {
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
if (existsSync(settingsPath)) {
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
if (settings.pythonPath && existsSync(settings.pythonPath)) {
pythonCmd = settings.pythonPath;
console.log('[project-context] Using Python from settings:', pythonCmd);
}
}
} catch (err) {
console.warn('[project-context] Could not read Python from settings:', err);
}
// Get configured Python path (venv if ready, otherwise bundled/system)
// This ensures we use the venv Python which has dependencies installed
const pythonCmd = getConfiguredPythonPath();
console.log('[project-context] Using Python:', pythonCmd);
const [pythonCommand, pythonBaseArgs] = parsePythonCommand(pythonCmd);
@@ -23,7 +23,8 @@ import {
isKuzuAvailable,
} from '../memory-service';
import { validateOpenAIApiKey } from '../api-validation-service';
import { findPythonCommand, parsePythonCommand } from '../python-detector';
import { parsePythonCommand } from '../python-detector';
import { getConfiguredPythonPath } from '../python-env-manager';
/**
* Ollama Service Status
@@ -104,10 +105,9 @@ async function executeOllamaDetector(
command: string,
baseUrl?: string
): Promise<{ success: boolean; data?: unknown; error?: string }> {
const pythonCmd = findPythonCommand();
if (!pythonCmd) {
return { success: false, error: 'Python not found' };
}
// Use configured Python path (venv if ready, otherwise bundled/system)
// Note: ollama_model_detector.py doesn't require dotenv, but using venv is safer
const pythonCmd = getConfiguredPythonPath();
// Find the ollama_model_detector.py script
const possiblePaths = [
@@ -543,10 +543,8 @@ export function registerMemoryHandlers(): void {
baseUrl?: string
): Promise<IPCResult<OllamaPullResult>> => {
try {
const pythonCmd = findPythonCommand();
if (!pythonCmd) {
return { success: false, error: 'Python not found' };
}
// Use configured Python path (venv if ready, otherwise bundled/system)
const pythonCmd = getConfiguredPythonPath();
// Find the ollama_model_detector.py script
const possiblePaths = [
@@ -5,11 +5,11 @@ import path from 'path';
import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
import { execSync, spawn, spawnSync } from 'child_process';
import { projectStore } from '../../project-store';
import { PythonEnvManager } from '../../python-env-manager';
import { getConfiguredPythonPath, 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';
import { parsePythonCommand } from '../../python-detector';
/**
* Read the stored base branch from task_metadata.json
@@ -354,7 +354,8 @@ export function registerWorktreeHandlers(
debug('Using stored base branch:', taskBaseBranch);
}
const pythonPath = pythonEnvManager.getPythonPath() || findPythonCommand() || 'python';
// Use configured Python path (venv if ready, otherwise bundled/system)
const pythonPath = getConfiguredPythonPath();
debug('Running command:', pythonPath, args.join(' '));
debug('Working directory:', sourcePath);
@@ -711,7 +712,8 @@ export function registerWorktreeHandlers(
console.warn('[IPC] Using stored base branch for preview:', taskBaseBranch);
}
const pythonPath = pythonEnvManager.getPythonPath() || findPythonCommand() || 'python';
// Use configured Python path (venv if ready, otherwise bundled/system)
const pythonPath = getConfiguredPythonPath();
console.warn('[IPC] Running merge preview:', pythonPath, args.join(' '));
// Get profile environment for consistency
+5 -11
View File
@@ -9,10 +9,10 @@
import { spawn } from 'child_process';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import { app } from 'electron';
import { findPythonCommand, parsePythonCommand } from './python-detector';
import { getConfiguredPythonPath } from './python-env-manager';
import { getMemoriesDir } from './config-paths';
import type { MemoryEpisode } from '../shared/types';
@@ -120,10 +120,7 @@ async function executeQuery(
args: string[],
timeout: number = 10000
): Promise<QueryResult> {
const pythonCmd = findPythonCommand();
if (!pythonCmd) {
return { success: false, error: 'Python not found' };
}
const pythonCmd = getConfiguredPythonPath();
const scriptPath = getQueryScriptPath();
if (!scriptPath) {
@@ -186,10 +183,7 @@ async function executeSemanticQuery(
embedderConfig: EmbedderConfig,
timeout: number = 30000 // Longer timeout for embedding operations
): Promise<QueryResult> {
const pythonCmd = findPythonCommand();
if (!pythonCmd) {
return { success: false, error: 'Python not found' };
}
const pythonCmd = getConfiguredPythonPath();
const scriptPath = getQueryScriptPath();
if (!scriptPath) {
@@ -591,7 +585,7 @@ export async function closeMemoryService(): Promise<void> {
* Check if Python with LadybugDB is available
*/
export function isKuzuAvailable(): boolean {
// Check if Python is available
// Check if Python is available (findPythonCommand can return null)
const pythonCmd = findPythonCommand();
if (!pythonCmd) {
return false;
@@ -619,7 +613,7 @@ export function getMemoryServiceStatus(dbPath?: string): MemoryServiceStatus {
? fs.readdirSync(basePath).filter((name) => !name.startsWith('.'))
: [];
// Check if Python and script are available
// Check if Python and script are available (findPythonCommand can return null)
const pythonAvailable = findPythonCommand() !== null;
const scriptAvailable = getQueryScriptPath() !== null;
@@ -432,3 +432,28 @@ export class PythonEnvManager extends EventEmitter {
// Singleton instance
export const pythonEnvManager = new PythonEnvManager();
/**
* Get the configured venv Python path if ready, otherwise fall back to system Python.
* This should be used by ALL services that need to spawn Python processes.
*
* Priority:
* 1. If venv is ready -> return venv Python (has all dependencies installed)
* 2. Fall back to findPythonCommand() -> bundled or system Python
*
* Note: For scripts that require dependencies (dotenv, claude-agent-sdk, etc.),
* the venv Python MUST be used. Only use this fallback for scripts that
* don't have external dependencies (like ollama_model_detector.py).
*/
export function getConfiguredPythonPath(): string {
// If venv is ready, always prefer it (has dependencies installed)
if (pythonEnvManager.isEnvReady()) {
const venvPath = pythonEnvManager.getPythonPath();
if (venvPath) {
return venvPath;
}
}
// Fall back to system/bundled Python
return findPythonCommand() || 'python';
}
+17 -4
View File
@@ -4,7 +4,8 @@ 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';
import { parsePythonCommand } from './python-detector';
import { getConfiguredPythonPath } from './python-env-manager';
/**
* Debug logging - only logs when DEBUG=true or in development mode
@@ -21,8 +22,8 @@ function debug(...args: unknown[]): void {
* Service for generating task titles from descriptions using Claude AI
*/
export class TitleGenerator extends EventEmitter {
// Auto-detect Python command on initialization
private pythonPath: string = findPythonCommand() || 'python';
// Python path will be configured by pythonEnvManager after venv is ready
private _pythonPath: string | null = null;
private autoBuildSourcePath: string = '';
constructor() {
@@ -35,13 +36,25 @@ export class TitleGenerator extends EventEmitter {
*/
configure(pythonPath?: string, autoBuildSourcePath?: string): void {
if (pythonPath) {
this.pythonPath = pythonPath;
this._pythonPath = pythonPath;
}
if (autoBuildSourcePath) {
this.autoBuildSourcePath = autoBuildSourcePath;
}
}
/**
* Get the configured Python path.
* Returns explicitly configured path, or falls back to getConfiguredPythonPath()
* which uses the venv Python if ready.
*/
private get pythonPath(): string {
if (this._pythonPath) {
return this._pythonPath;
}
return getConfiguredPythonPath();
}
/**
* Get the auto-claude source path (detects automatically if not configured)
*/