chore: merge origin/develop into feat/bmad-adopter-integration
Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
+6
-3
@@ -290,11 +290,14 @@ if git diff --cached --name-only | grep -q "^apps/frontend/"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for vulnerabilities (only high severity)
|
||||
# Check for vulnerabilities (only critical severity)
|
||||
# Note: Using critical level because electron-builder has a known high-severity
|
||||
# tar vulnerability (CVE-2026-23745) that cannot be fixed until electron-builder
|
||||
# releases an update with [email protected] support. This is a build dependency, not runtime.
|
||||
echo "Checking for vulnerabilities..."
|
||||
npm audit --audit-level=high
|
||||
npm audit --audit-level=critical
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "High severity vulnerabilities found. Run 'npm audit fix' to resolve."
|
||||
echo "Critical severity vulnerabilities found. Run 'npm audit fix' to resolve."
|
||||
exit 1
|
||||
fi
|
||||
)
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
### Stable Release
|
||||
|
||||
<!-- STABLE_VERSION_BADGE -->
|
||||
[](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.7.5)
|
||||
[](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.7.4)
|
||||
<!-- STABLE_VERSION_BADGE_END -->
|
||||
|
||||
<!-- STABLE_DOWNLOADS -->
|
||||
| Platform | Download |
|
||||
|----------|----------|
|
||||
| **Windows** | [Auto-Claude-2.7.5-win32-x64.exe](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.5/Auto-Claude-2.7.5-win32-x64.exe) |
|
||||
| **macOS (Apple Silicon)** | [Auto-Claude-2.7.5-darwin-arm64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.5/Auto-Claude-2.7.5-darwin-arm64.dmg) |
|
||||
| **macOS (Intel)** | [Auto-Claude-2.7.5-darwin-x64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.5/Auto-Claude-2.7.5-darwin-x64.dmg) |
|
||||
| **Linux** | [Auto-Claude-2.7.5-linux-x86_64.AppImage](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.5/Auto-Claude-2.7.5-linux-x86_64.AppImage) |
|
||||
| **Linux (Debian)** | [Auto-Claude-2.7.5-linux-amd64.deb](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.5/Auto-Claude-2.7.5-linux-amd64.deb) |
|
||||
| **Windows** | [Auto-Claude-2.7.4-win32-x64.exe](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.4/Auto-Claude-2.7.4-win32-x64.exe) |
|
||||
| **macOS (Apple Silicon)** | [Auto-Claude-2.7.4-darwin-arm64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.4/Auto-Claude-2.7.4-darwin-arm64.dmg) |
|
||||
| **macOS (Intel)** | [Auto-Claude-2.7.4-darwin-x64.dmg](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.4/Auto-Claude-2.7.4-darwin-x64.dmg) |
|
||||
| **Linux** | [Auto-Claude-2.7.4-linux-x86_64.AppImage](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.4/Auto-Claude-2.7.4-linux-x86_64.AppImage) |
|
||||
| **Linux (Debian)** | [Auto-Claude-2.7.4-linux-amd64.deb](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.4/Auto-Claude-2.7.4-linux-amd64.deb) |
|
||||
| **Linux (Flatpak)** | [Auto-Claude-2.7.4-linux-x86_64.flatpak](https://github.com/AndyMik90/Auto-Claude/releases/download/v2.7.4/Auto-Claude-2.7.4-linux-x86_64.flatpak) |
|
||||
<!-- STABLE_DOWNLOADS_END -->
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ THINKING_BUDGET_MAP: dict[str, int | None] = {
|
||||
"low": 1024,
|
||||
"medium": 4096, # Moderate analysis
|
||||
"high": 16384, # Deep thinking for QA review
|
||||
"ultrathink": 64000, # Maximum reasoning depth (within Claude API's 64000 token limit)
|
||||
"ultrathink": 63999, # Maximum reasoning depth (API requires max_tokens >= budget + 1, so 63999 + 1 = 64000 limit)
|
||||
}
|
||||
|
||||
# Spec runner phase-specific thinking levels
|
||||
|
||||
@@ -25,6 +25,12 @@ export default defineConfig({
|
||||
exclude: [
|
||||
'uuid',
|
||||
'chokidar',
|
||||
'dotenv',
|
||||
'electron-log',
|
||||
'proper-lockfile',
|
||||
'semver',
|
||||
'zod',
|
||||
'@anthropic-ai/sdk',
|
||||
'kuzu',
|
||||
'electron-updater',
|
||||
'@electron-toolkit/utils',
|
||||
@@ -35,7 +41,9 @@ export default defineConfig({
|
||||
'@sentry/utils',
|
||||
'@opentelemetry/instrumentation',
|
||||
'debug',
|
||||
'ms'
|
||||
'ms',
|
||||
// Minimatch for glob pattern matching in worktree handlers
|
||||
'minimatch'
|
||||
]
|
||||
})],
|
||||
build: {
|
||||
|
||||
@@ -31,11 +31,11 @@
|
||||
"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 --publish never",
|
||||
"package:mac": "npm run python:download && electron-vite build && electron-builder --mac --publish never",
|
||||
"package:win": "npm run python:download && electron-vite build && electron-builder --win --publish never",
|
||||
"package:linux": "npm run python:download && electron-vite build && electron-builder --linux --publish never",
|
||||
"package:flatpak": "npm run python:download && electron-vite build && electron-builder --linux flatpak --publish never",
|
||||
"package": "node scripts/package-with-python.cjs",
|
||||
"package:mac": "node scripts/package-with-python.cjs --mac",
|
||||
"package:win": "node scripts/package-with-python.cjs --win",
|
||||
"package:linux": "node scripts/package-with-python.cjs --linux",
|
||||
"package:flatpak": "node scripts/package-with-python.cjs --linux flatpak",
|
||||
"start:packaged:mac": "open dist/mac-arm64/Auto-Claude.app || open dist/mac/Auto-Claude.app",
|
||||
"start:packaged:win": "start \"\" \"dist\\win-unpacked\\Auto-Claude.exe\"",
|
||||
"start:packaged:linux": "./dist/linux-unpacked/auto-claude",
|
||||
@@ -137,7 +137,6 @@
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.3",
|
||||
"electron-builder-squirrel-windows": "^26.0.12",
|
||||
"dmg-builder": "^26.0.12",
|
||||
"@electron/rebuild": "4.0.2"
|
||||
@@ -161,6 +160,9 @@
|
||||
"out/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"out/main/node_modules/@lydell/node-pty-*/**"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "resources/icon.ico",
|
||||
|
||||
@@ -21,6 +21,7 @@ const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const os = require('os');
|
||||
const nodeCrypto = require('crypto');
|
||||
const { toNodePlatform } = require('../src/shared/platform.cjs');
|
||||
|
||||
// Python version to bundle (must be 3.10+ for claude-agent-sdk, 3.12+ for full Graphiti support)
|
||||
const PYTHON_VERSION = '3.12.8';
|
||||
@@ -165,18 +166,6 @@ function toElectronBuilderPlatform(nodePlatform) {
|
||||
return map[nodePlatform] || nodePlatform;
|
||||
}
|
||||
|
||||
// Map electron-builder platform names to Node.js platform names (for internal use)
|
||||
function toNodePlatform(platform) {
|
||||
const map = {
|
||||
'mac': 'darwin',
|
||||
'win': 'win32',
|
||||
'darwin': 'darwin',
|
||||
'win32': 'win32',
|
||||
'linux': 'linux',
|
||||
};
|
||||
return map[platform] || platform;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download URL for a specific platform/arch combination.
|
||||
* python-build-standalone uses specific naming conventions.
|
||||
@@ -464,6 +453,71 @@ function formatBytes(bytes) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function hasPackage(sitePackagesDir, pkg) {
|
||||
const pkgPath = path.join(sitePackagesDir, pkg);
|
||||
const initPath = path.join(pkgPath, '__init__.py');
|
||||
const moduleFile = path.join(sitePackagesDir, pkg + '.py');
|
||||
return (fs.existsSync(pkgPath) && fs.existsSync(initPath)) || fs.existsSync(moduleFile);
|
||||
}
|
||||
|
||||
function hasPydanticCoreBinary(sitePackagesDir) {
|
||||
const pkgDir = path.join(sitePackagesDir, 'pydantic_core');
|
||||
if (!fs.existsSync(pkgDir)) return false;
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(pkgDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return entries.some((name) => {
|
||||
if (!name.startsWith('_pydantic_core')) return false;
|
||||
const lower = name.toLowerCase();
|
||||
return lower.endsWith('.so') || lower.endsWith('.pyd') || lower.endsWith('.dylib');
|
||||
});
|
||||
}
|
||||
|
||||
function getPinnedPydanticCoreVersion(sitePackagesDir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(sitePackagesDir);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const distInfo = entries.find((entry) => {
|
||||
return entry.startsWith('pydantic-')
|
||||
&& !entry.startsWith('pydantic_core-')
|
||||
&& entry.endsWith('.dist-info');
|
||||
});
|
||||
if (!distInfo) return null;
|
||||
|
||||
const metadataPath = path.join(sitePackagesDir, distInfo, 'METADATA');
|
||||
if (!fs.existsSync(metadataPath)) return null;
|
||||
|
||||
let metadata;
|
||||
try {
|
||||
metadata = fs.readFileSync(metadataPath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const line of metadata.split(/\r?\n/)) {
|
||||
if (!line.startsWith('Requires-Dist: pydantic-core')) continue;
|
||||
const match = line.match(/pydantic-core==([0-9A-Za-z.+-]+)/);
|
||||
if (match) return match[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isCriticalPackageMissing(sitePackagesDir, pkg) {
|
||||
if (pkg === 'pydantic_core') {
|
||||
return !hasPackage(sitePackagesDir, pkg) || !hasPydanticCoreBinary(sitePackagesDir);
|
||||
}
|
||||
return !hasPackage(sitePackagesDir, pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip unnecessary files from site-packages to reduce bundle size.
|
||||
* This removes tests, docs, cache files, and other non-essential content.
|
||||
@@ -797,6 +851,44 @@ function installPackages(pythonBin, requirementsPath, targetSitePackages) {
|
||||
// Strip unnecessary files
|
||||
stripSitePackages(targetSitePackages);
|
||||
|
||||
if (!hasPydanticCoreBinary(targetSitePackages)) {
|
||||
console.warn('[download-python] pydantic_core binary missing after strip; reinstalling pydantic-core...');
|
||||
const pinnedVersion = getPinnedPydanticCoreVersion(targetSitePackages);
|
||||
const coreSpec = pinnedVersion ? `pydantic-core==${pinnedVersion}` : 'pydantic-core';
|
||||
if (pinnedVersion) {
|
||||
console.log(`[download-python] Reinstalling pydantic-core ${pinnedVersion} to match pydantic metadata`);
|
||||
} else {
|
||||
console.warn('[download-python] Unable to determine pydantic-core pin; reinstalling latest');
|
||||
}
|
||||
const pipArgs = [
|
||||
'-m', 'pip', 'install',
|
||||
'--no-compile',
|
||||
'--only-binary', 'pydantic-core',
|
||||
'--no-deps',
|
||||
'--target', targetSitePackages,
|
||||
coreSpec,
|
||||
];
|
||||
const result = spawnSync(pythonBin, pipArgs, {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
PYTHONDONTWRITEBYTECODE: '1',
|
||||
PYTHONIOENCODING: 'utf-8',
|
||||
},
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(`Failed to reinstall pydantic-core: ${result.error.message}`);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`pydantic-core reinstall failed with exit code ${result.status}`);
|
||||
}
|
||||
|
||||
if (!hasPydanticCoreBinary(targetSitePackages)) {
|
||||
throw new Error('pydantic_core binary missing after reinstall');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove bin/Scripts directory (we don't need console scripts)
|
||||
const binDir = path.join(targetSitePackages, 'bin');
|
||||
const scriptsDir = path.join(targetSitePackages, 'Scripts');
|
||||
@@ -862,14 +954,7 @@ async function downloadPython(targetPlatform, targetArch, options = {}) {
|
||||
// while this script validates it during build to ensure it's bundled
|
||||
const criticalPackages = ['claude_agent_sdk', 'dotenv', 'pydantic_core']
|
||||
.concat(PLATFORM_CRITICAL_PACKAGES[info.nodePlatform] || []);
|
||||
const missingPackages = criticalPackages.filter(pkg => {
|
||||
const pkgPath = path.join(sitePackagesDir, pkg);
|
||||
const initPath = path.join(pkgPath, '__init__.py');
|
||||
// For single-file modules (like pywintypes.py), check for the file directly
|
||||
const moduleFile = path.join(sitePackagesDir, pkg + '.py');
|
||||
// Package is valid if directory+__init__.py exists OR single-file module exists
|
||||
return !(fs.existsSync(pkgPath) && fs.existsSync(initPath)) && !fs.existsSync(moduleFile);
|
||||
});
|
||||
const missingPackages = criticalPackages.filter(pkg => isCriticalPackageMissing(sitePackagesDir, pkg));
|
||||
|
||||
if (missingPackages.length > 0) {
|
||||
console.log(`[download-python] Critical packages missing or incomplete: ${missingPackages.join(', ')}`);
|
||||
@@ -969,14 +1054,7 @@ async function downloadPython(targetPlatform, targetArch, options = {}) {
|
||||
// while this script validates it during build to ensure it's bundled
|
||||
const criticalPackages = ['claude_agent_sdk', 'dotenv', 'pydantic_core']
|
||||
.concat(PLATFORM_CRITICAL_PACKAGES[info.nodePlatform] || []);
|
||||
const postInstallMissing = criticalPackages.filter(pkg => {
|
||||
const pkgPath = path.join(sitePackagesDir, pkg);
|
||||
const initPath = path.join(pkgPath, '__init__.py');
|
||||
// For single-file modules (like pywintypes.py), check for the file directly
|
||||
const moduleFile = path.join(sitePackagesDir, pkg + '.py');
|
||||
// Package is valid if directory+__init__.py exists OR single-file module exists
|
||||
return !(fs.existsSync(pkgPath) && fs.existsSync(initPath)) && !fs.existsSync(moduleFile);
|
||||
});
|
||||
const postInstallMissing = criticalPackages.filter(pkg => isCriticalPackageMissing(sitePackagesDir, pkg));
|
||||
|
||||
if (postInstallMissing.length > 0) {
|
||||
throw new Error(`Package installation failed - missing critical packages: ${postInstallMissing.join(', ')}`);
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Packaging script that downloads bundled Python, stages runtime modules,
|
||||
* and builds the Electron app for the requested platforms and architectures.
|
||||
*
|
||||
* Usage: node scripts/package-with-python.cjs [--mac|--win|--linux] [--x64|--arm64|--universal]
|
||||
*/
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const { isWindows, getCurrentPlatform, toNodePlatform } = require('../src/shared/platform.cjs');
|
||||
const { downloadPython } = require('./download-python.cjs');
|
||||
|
||||
/**
|
||||
* Shell metacharacters that could enable command injection when shell: true is used on Windows.
|
||||
* These characters have special meaning in cmd.exe and could be used to inject arbitrary commands.
|
||||
*
|
||||
* Includes:
|
||||
* - Standard operators: & | > < ^ %
|
||||
* - Command separators: ;
|
||||
* - Variable expansion: $ %
|
||||
* - Command grouping: ( ) [ ] { }
|
||||
* - Delayed expansion: !
|
||||
* - Command substitution: `
|
||||
* - Quotes: "
|
||||
* - Line breaks: \n \r
|
||||
*
|
||||
* Note: Single quote (') is not included as cmd.exe does not treat it as a shell metacharacter.
|
||||
*/
|
||||
const SHELL_METACHARACTERS = Object.freeze(['&', '|', '>', '<', '^', '%', ';', '$', '(', ')', '[', ']', '{', '}', '!', '`', '"', '\n', '\r']);
|
||||
|
||||
/**
|
||||
* Validate that arguments don't contain shell metacharacters on Windows.
|
||||
* When shell: true is used, cmd.exe interprets metacharacters which could lead to command injection.
|
||||
*
|
||||
* @param {string[]} commandArgs - Arguments to validate
|
||||
* @throws {Error} If any argument contains dangerous shell metacharacters on Windows
|
||||
* @throws {TypeError} If any argument is not a string
|
||||
*/
|
||||
function validateArgs(commandArgs) {
|
||||
if (!isWindows()) return; // Only validate on Windows where shell: true is used
|
||||
|
||||
for (const arg of commandArgs) {
|
||||
// Defensive check: skip non-string arguments to prevent TypeError
|
||||
if (typeof arg !== 'string') {
|
||||
throw new TypeError(
|
||||
`Security: Argument must be a string, got ${typeof arg}. ` +
|
||||
`This may indicate incorrect argument passing.`
|
||||
);
|
||||
}
|
||||
|
||||
for (const char of SHELL_METACHARACTERS) {
|
||||
if (arg.includes(char)) {
|
||||
throw new Error(
|
||||
`Security: Argument contains shell metacharacter '${char}' which could enable command injection. ` +
|
||||
`Argument: "${arg}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
const PLATFORM_FLAGS = new Map([
|
||||
['--mac', 'mac'],
|
||||
['--win', 'win'],
|
||||
['--windows', 'win'],
|
||||
['--linux', 'linux'],
|
||||
]);
|
||||
|
||||
const ARCH_FLAGS = new Map([
|
||||
['--x64', 'x64'],
|
||||
['--arm64', 'arm64'],
|
||||
['--universal', 'universal'],
|
||||
]);
|
||||
|
||||
function mapHostPlatform(platform) {
|
||||
const map = { darwin: 'mac', win32: 'win', linux: 'linux' };
|
||||
return map[platform] || platform;
|
||||
}
|
||||
|
||||
function resolvePlatforms() {
|
||||
const platforms = new Set();
|
||||
for (const arg of args) {
|
||||
const mapped = PLATFORM_FLAGS.get(arg);
|
||||
if (mapped) platforms.add(mapped);
|
||||
}
|
||||
|
||||
if (platforms.size === 0) {
|
||||
platforms.add(mapHostPlatform(getCurrentPlatform()));
|
||||
}
|
||||
|
||||
return [...platforms];
|
||||
}
|
||||
|
||||
function resolveArchs() {
|
||||
const archs = new Set();
|
||||
let wantsUniversal = false;
|
||||
|
||||
for (const arg of args) {
|
||||
const mapped = ARCH_FLAGS.get(arg);
|
||||
if (!mapped) continue;
|
||||
if (mapped === 'universal') {
|
||||
wantsUniversal = true;
|
||||
} else {
|
||||
archs.add(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
if (wantsUniversal) {
|
||||
archs.add('x64');
|
||||
archs.add('arm64');
|
||||
}
|
||||
|
||||
if (archs.size === 0) {
|
||||
archs.add(os.arch());
|
||||
}
|
||||
|
||||
for (const arch of archs) {
|
||||
if (!['x64', 'arm64'].includes(arch)) {
|
||||
throw new Error(
|
||||
`Host architecture '${arch}' is not supported for bundled Python. Please specify --x64 or --arm64 explicitly.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [...archs];
|
||||
}
|
||||
|
||||
function buildEnv(frontendDir) {
|
||||
const binDir = path.join(frontendDir, 'node_modules', '.bin');
|
||||
const rootBinDir = path.join(frontendDir, '..', '..', 'node_modules', '.bin');
|
||||
const pathParts = [binDir, rootBinDir];
|
||||
const pathValue = process.env.PATH
|
||||
? `${pathParts.join(path.delimiter)}${path.delimiter}${process.env.PATH}`
|
||||
: pathParts.join(path.delimiter);
|
||||
return { ...process.env, PATH: pathValue };
|
||||
}
|
||||
|
||||
function runCommand(command, commandArgs, cwd, env) {
|
||||
// Validate arguments to prevent command injection via shell metacharacters.
|
||||
// Note: validateArgs only validates on Windows because shell: true is only used on Windows.
|
||||
// On non-Windows platforms, .cmd files are not used and shell: false, so no injection risk.
|
||||
validateArgs(commandArgs);
|
||||
|
||||
const bin = isWindows() ? `${command}.cmd` : command;
|
||||
const result = spawnSync(bin, commandArgs, {
|
||||
cwd,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
shell: isWindows(),
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
const code = result.status ?? 1;
|
||||
throw new Error(`Command "${command}" failed with exit code ${code}.`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePackageDir(baseDir, pkgName) {
|
||||
return path.join(baseDir, 'node_modules', ...pkgName.split('/'));
|
||||
}
|
||||
|
||||
function copyPackage(fromDir, toDir) {
|
||||
if (!fs.existsSync(fromDir)) {
|
||||
throw new Error(`Required package not found: ${fromDir}`);
|
||||
}
|
||||
|
||||
if (fs.existsSync(toDir)) {
|
||||
fs.rmSync(toDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(toDir), { recursive: true });
|
||||
fs.cpSync(fromDir, toDir, { recursive: true, dereference: true });
|
||||
}
|
||||
|
||||
function readPackageJson(pkgDir) {
|
||||
const pkgPath = path.join(pkgDir, 'package.json');
|
||||
if (!fs.existsSync(pkgPath)) return null;
|
||||
return JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
}
|
||||
|
||||
function stageRuntimePackages(frontendDir, platform, arch) {
|
||||
const rootDir = path.join(frontendDir, '..', '..');
|
||||
const nodePlatform = toNodePlatform(platform);
|
||||
const packages = [
|
||||
'@lydell/node-pty',
|
||||
`@lydell/node-pty-${nodePlatform}-${arch}`,
|
||||
'minimatch',
|
||||
];
|
||||
const outMainDir = path.join(frontendDir, 'out', 'main');
|
||||
const outModulesDir = path.join(outMainDir, 'node_modules');
|
||||
|
||||
if (!fs.existsSync(outMainDir)) {
|
||||
throw new Error('Missing build output. Run electron-vite build before staging node-pty.');
|
||||
}
|
||||
|
||||
fs.mkdirSync(outModulesDir, { recursive: true });
|
||||
|
||||
const staged = new Set();
|
||||
|
||||
function stagePackage(pkgName) {
|
||||
if (staged.has(pkgName)) return;
|
||||
staged.add(pkgName);
|
||||
|
||||
const rootDirPath = resolvePackageDir(rootDir, pkgName);
|
||||
if (!fs.existsSync(rootDirPath)) {
|
||||
throw new Error(`Missing ${pkgName} in workspace. Run npm install before packaging.`);
|
||||
}
|
||||
|
||||
const localDir = path.join(outModulesDir, ...pkgName.split('/'));
|
||||
console.log(`[package] Staging ${pkgName} into build output...`);
|
||||
copyPackage(rootDirPath, localDir);
|
||||
|
||||
const pkgJson = readPackageJson(rootDirPath);
|
||||
if (!pkgJson) return;
|
||||
|
||||
const deps = pkgJson.dependencies || {};
|
||||
const optionalDeps = pkgJson.optionalDependencies || {};
|
||||
|
||||
for (const depName of Object.keys(deps)) {
|
||||
stagePackage(depName);
|
||||
}
|
||||
|
||||
for (const depName of Object.keys(optionalDeps)) {
|
||||
const optionalPath = resolvePackageDir(rootDir, depName);
|
||||
if (fs.existsSync(optionalPath)) {
|
||||
stagePackage(depName);
|
||||
} else {
|
||||
console.log(`[package] Skipping optional dependency not installed: ${depName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const pkgName of packages) {
|
||||
stagePackage(pkgName);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const frontendDir = path.join(__dirname, '..');
|
||||
const env = buildEnv(frontendDir);
|
||||
|
||||
const platforms = resolvePlatforms();
|
||||
const archs = resolveArchs();
|
||||
|
||||
for (const platform of platforms) {
|
||||
for (const arch of archs) {
|
||||
await downloadPython(platform, arch);
|
||||
}
|
||||
}
|
||||
|
||||
runCommand('electron-vite', ['build'], frontendDir, env);
|
||||
|
||||
for (const platform of platforms) {
|
||||
for (const arch of archs) {
|
||||
stageRuntimePackages(frontendDir, platform, arch);
|
||||
}
|
||||
}
|
||||
|
||||
const builderArgs = [...args];
|
||||
const hasPublishFlag = builderArgs.some((arg) => arg === '--publish' || arg.startsWith('--publish='));
|
||||
if (!hasPublishFlag) {
|
||||
builderArgs.push('--publish', 'never');
|
||||
}
|
||||
|
||||
runCommand('electron-builder', builderArgs, frontendDir, env);
|
||||
}
|
||||
|
||||
// Run main() only when this file is executed directly (not when imported for testing)
|
||||
if (require.main === module) {
|
||||
main().catch((err) => {
|
||||
console.error(`[package] Error: ${err.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Export for testing
|
||||
module.exports = { validateArgs, SHELL_METACHARACTERS };
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Type declarations for package-with-python.cjs
|
||||
*/
|
||||
export declare const SHELL_METACHARACTERS: readonly string[];
|
||||
export declare function validateArgs(commandArgs: string[]): void;
|
||||
@@ -52,9 +52,21 @@ vi.mock('child_process', async (importOriginal) => {
|
||||
});
|
||||
|
||||
// Mock claude-profile-manager to bypass auth checks in tests
|
||||
// Profile shape must match ClaudeProfile interface (id, name, isDefault, etc.)
|
||||
const mockProfile = {
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
isDefault: true,
|
||||
oauthToken: 'mock-encrypted-token'
|
||||
};
|
||||
|
||||
const mockProfileManager = {
|
||||
hasValidAuth: () => true,
|
||||
getActiveProfile: () => ({ profileId: 'default', profileName: 'Default' })
|
||||
getActiveProfile: () => mockProfile,
|
||||
getProfile: (_profileId: string) => mockProfile,
|
||||
// Token decryption methods - return mock token for tests
|
||||
getActiveProfileToken: () => 'mock-decrypted-token-for-testing',
|
||||
getProfileToken: (_profileId: string) => 'mock-decrypted-token-for-testing'
|
||||
};
|
||||
|
||||
vi.mock('../../main/claude-profile-manager', () => ({
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Unit tests for package-with-python.cjs security validation
|
||||
*
|
||||
* Tests the validateArgs function which prevents command injection via
|
||||
* shell metacharacters when shell: true is used on Windows.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
||||
import path from 'node:path';
|
||||
// Import from the scripts directory (relative to src/main/__tests__)
|
||||
// @ts-expect-error - TypeScript doesn't auto-resolve .d.ts for .cjs imports (types exist in package-with-python.d.ts)
|
||||
import { validateArgs, SHELL_METACHARACTERS } from '../../../scripts/package-with-python.cjs';
|
||||
|
||||
// Mock the isWindows function from platform.cjs
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
describe('validateArgs', () => {
|
||||
// We need to mock the isWindows function by modifying process.platform
|
||||
// since the platform.cjs module uses process.platform === 'win32' to check
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original platform after each test
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('on Windows (shell injection risk)', () => {
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('should throw for shell metacharacters', () => {
|
||||
// Test each metacharacter individually
|
||||
it.each([
|
||||
['&', 'command & malicious'],
|
||||
['|', 'command | malicious'],
|
||||
['>', 'output > file.txt'],
|
||||
['<', 'command < input.txt'],
|
||||
['^', 'escape ^ character'],
|
||||
['%', '%PATH%'],
|
||||
[';', 'command ; malicious'],
|
||||
['$', '$variable'],
|
||||
['(', 'command (group)'],
|
||||
[')', 'command)after'],
|
||||
['[', 'array[index]'],
|
||||
[']', 'command]after'],
|
||||
['{', '{block}'],
|
||||
['}', 'command}after'],
|
||||
['!', '!delayed!'],
|
||||
['"', '"quoted"'],
|
||||
['`', 'command `subshell`'],
|
||||
['\n', 'command\nnext'],
|
||||
['\r', 'command\rnext'],
|
||||
])('should throw for metacharacter "%s"', (char, arg) => {
|
||||
expect(() => validateArgs([arg])).toThrowError(/shell metacharacter/);
|
||||
expect(() => validateArgs([arg])).toThrowError(new RegExp(`\\${char}`));
|
||||
});
|
||||
|
||||
// Test metacharacters in different positions
|
||||
it('should throw when metacharacter is at the start', () => {
|
||||
expect(() => validateArgs(['& malicious'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when metacharacter is in the middle', () => {
|
||||
expect(() => validateArgs(['config&malicious'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when metacharacter is at the end', () => {
|
||||
expect(() => validateArgs(['config&'])).toThrow();
|
||||
});
|
||||
|
||||
// Test multiple metacharacters
|
||||
it('should throw for multiple metacharacters in one argument', () => {
|
||||
expect(() => validateArgs(['& | >'])).toThrow();
|
||||
});
|
||||
|
||||
// Test metacharacters across multiple arguments
|
||||
it('should throw for metacharacters in different arguments', () => {
|
||||
expect(() => validateArgs(['--flag', 'value&', 'other'])).toThrow();
|
||||
});
|
||||
|
||||
// Test error message includes the offending argument
|
||||
it('should include offending argument in error message', () => {
|
||||
expect(() => validateArgs(['file&evil.exe']))
|
||||
.toThrowError(/Argument: "file&evil\.exe"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should throw for non-string arguments', () => {
|
||||
it('should throw TypeError for null argument', () => {
|
||||
expect(() => validateArgs([null])).toThrowError(TypeError);
|
||||
expect(() => validateArgs([null])).toThrowError(/must be a string/);
|
||||
});
|
||||
|
||||
it('should throw TypeError for undefined argument', () => {
|
||||
expect(() => validateArgs([undefined])).toThrowError(TypeError);
|
||||
expect(() => validateArgs([undefined])).toThrowError(/must be a string/);
|
||||
});
|
||||
|
||||
it('should throw TypeError for number argument', () => {
|
||||
expect(() => validateArgs([123])).toThrowError(TypeError);
|
||||
expect(() => validateArgs([123])).toThrowError(/got number/);
|
||||
});
|
||||
|
||||
it('should throw TypeError for object argument', () => {
|
||||
expect(() => validateArgs([{ key: 'value' }])).toThrowError(TypeError);
|
||||
expect(() => validateArgs([{ key: 'value' }])).toThrowError(/got object/);
|
||||
});
|
||||
|
||||
it('should throw TypeError for mixed valid and invalid arguments', () => {
|
||||
expect(() => validateArgs(['--flag', null])).toThrowError(TypeError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should NOT throw for safe inputs', () => {
|
||||
it('should allow empty array', () => {
|
||||
expect(() => validateArgs([])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow alphanumeric arguments', () => {
|
||||
expect(() => validateArgs(['build', 'test', 'production'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow flag arguments', () => {
|
||||
expect(() => validateArgs(['--win', '--x64', '--publish=never'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow paths with forward slashes', () => {
|
||||
expect(() => validateArgs(['../config/file.txt'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow paths with backslashes', () => {
|
||||
// Use path.win32.join to construct a Windows-style path without hardcoding system locations
|
||||
const windowsPath = path.win32.join('C:', 'Apps', 'App', 'config.txt');
|
||||
expect(() => validateArgs([windowsPath])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow dots and hyphens', () => {
|
||||
expect(() => validateArgs(['--config.file', 'my-config.json'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow underscores', () => {
|
||||
expect(() => validateArgs(['my_config_file', '--output_dir'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow @ symbol', () => {
|
||||
expect(() => validateArgs(['@lydell/node-pty'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow equals sign', () => {
|
||||
expect(() => validateArgs(['--publish=never'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow common electron-builder arguments', () => {
|
||||
expect(() => validateArgs([
|
||||
'--win',
|
||||
'--x64',
|
||||
'--publish',
|
||||
'never',
|
||||
'--config',
|
||||
'config.yml'
|
||||
])).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('on non-Windows platforms', () => {
|
||||
it('should return immediately on macOS without throwing', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Even with metacharacters, should not throw on non-Windows
|
||||
expect(() => validateArgs(['command & malicious'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should return immediately on Linux without throwing', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Even with metacharacters, should not throw on non-Windows
|
||||
expect(() => validateArgs(['command & malicious'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should allow empty array on macOS', () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
expect(() => validateArgs([])).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SHELL_METACHARACTERS constant', () => {
|
||||
it('should contain all expected dangerous characters', () => {
|
||||
const expectedChars = [
|
||||
'&', '|', '>', '<', '^', '%', ';', '$',
|
||||
'(', ')', '[', ']', '{', '}',
|
||||
'!', '"', '`', '\n', '\r'
|
||||
];
|
||||
expect(SHELL_METACHARACTERS).toEqual(expect.arrayContaining(expectedChars));
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,9 @@ import { ipcMain, BrowserWindow, shell, app } from 'electron';
|
||||
import { IPC_CHANNELS, AUTO_BUILD_PATHS, DEFAULT_APP_SETTINGS, DEFAULT_FEATURE_MODELS, DEFAULT_FEATURE_THINKING, MODEL_ID_MAP, THINKING_BUDGET_MAP, getSpecsDir } from '../../../shared/constants';
|
||||
import type { IPCResult, WorktreeStatus, WorktreeDiff, WorktreeDiffFile, WorktreeMergeResult, WorktreeDiscardResult, WorktreeListResult, WorktreeListItem, WorktreeCreatePROptions, WorktreeCreatePRResult, SupportedIDE, SupportedTerminal, AppSettings } from '../../../shared/types';
|
||||
import path from 'path';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
|
||||
import { execSync, execFileSync, spawn, spawnSync, exec, execFile } from 'child_process';
|
||||
import { createRequire } from 'module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const { minimatch } = require('minimatch');
|
||||
import { projectStore } from '../../project-store';
|
||||
import { getConfiguredPythonPath, PythonEnvManager, pythonEnvManager as pythonEnvManagerSingleton } from '../../python-env-manager';
|
||||
import { getEffectiveSourcePath } from '../../updater/path-resolver';
|
||||
|
||||
@@ -77,6 +77,12 @@ vi.mock('../session-handler', () => ({
|
||||
releaseSessionId: mockReleaseSessionId,
|
||||
}));
|
||||
|
||||
// Mock PtyManager.writeToPty - the implementation now uses this instead of terminal.pty.write
|
||||
const mockWriteToPty = vi.fn();
|
||||
vi.mock('../pty-manager', () => ({
|
||||
writeToPty: mockWriteToPty,
|
||||
}));
|
||||
|
||||
vi.mock('os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('os')>();
|
||||
return {
|
||||
@@ -199,6 +205,7 @@ describe('claude-integration-handler', () => {
|
||||
mockGetClaudeProfileManager.mockClear();
|
||||
mockPersistSession.mockClear();
|
||||
mockReleaseSessionId.mockClear();
|
||||
mockWriteToPty.mockClear();
|
||||
vi.mocked(writeFileSync).mockClear();
|
||||
});
|
||||
|
||||
@@ -225,7 +232,7 @@ describe('claude-integration-handler', () => {
|
||||
const { invokeClaude } = await import('../claude-integration-handler');
|
||||
invokeClaude(terminal, '/tmp/project', undefined, () => null, vi.fn());
|
||||
|
||||
const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const written = mockWriteToPty.mock.calls[0][1] as string;
|
||||
expect(written).toContain(buildCdCommand('/tmp/project'));
|
||||
expect(written).toContain(getPathPrefixExpectation(platform, '/opt/claude/bin:/usr/bin'));
|
||||
expect(written).toContain(getQuotedCommand(platform, "/opt/claude bin/claude's"));
|
||||
@@ -268,7 +275,7 @@ describe('claude-integration-handler', () => {
|
||||
expect(tokenPath).toMatch(new RegExp(`^${escapeForRegex(tokenPrefix)}[0-9a-f]{16}${escapeForRegex(tokenExt)}$`));
|
||||
expect(tokenContents).toBe(getTokenFileContent(platform, 'token-value'));
|
||||
|
||||
const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const written = mockWriteToPty.mock.calls[0][1] as string;
|
||||
const clearCmd = getClearCommand(platform);
|
||||
const histPrefix = getHistoryPrefix(platform);
|
||||
const cmdQuote = platform === 'win32' ? '"' : "'";
|
||||
@@ -318,7 +325,7 @@ describe('claude-integration-handler', () => {
|
||||
expect(tokenPath).toMatch(new RegExp(`^${escapeForRegex(tokenPrefix)}[0-9a-f]{16}${escapeForRegex(tokenExt)}$`));
|
||||
expect(tokenContents).toBe(getTokenFileContent(platform, 'token-value'));
|
||||
|
||||
const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const written = mockWriteToPty.mock.calls[0][1] as string;
|
||||
expect(written).toContain(getTempFileInvocation(platform, tokenPath));
|
||||
expect(written).toContain(getTempFileCleanup(platform, tokenPath));
|
||||
expect(written).toContain(getQuotedCommand(platform, command));
|
||||
@@ -350,7 +357,7 @@ describe('claude-integration-handler', () => {
|
||||
const { invokeClaude } = await import('../claude-integration-handler');
|
||||
invokeClaude(terminal, '/tmp/project', 'missing', () => null, vi.fn());
|
||||
|
||||
const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const written = mockWriteToPty.mock.calls[0][1] as string;
|
||||
expect(written).toContain(getQuotedCommand(platform, command));
|
||||
expect(profileManager.getProfile).toHaveBeenCalledWith('missing');
|
||||
expect(profileManager.markProfileUsed).not.toHaveBeenCalled();
|
||||
@@ -381,7 +388,7 @@ describe('claude-integration-handler', () => {
|
||||
const { invokeClaude } = await import('../claude-integration-handler');
|
||||
invokeClaude(terminal, '/tmp/project', 'prof-2', () => null, vi.fn());
|
||||
|
||||
const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const written = mockWriteToPty.mock.calls[0][1] as string;
|
||||
const clearCmd = getClearCommand(platform);
|
||||
const histPrefix = getHistoryPrefix(platform);
|
||||
const configDir = getConfigDirCommand(platform, '/tmp/claude-config');
|
||||
@@ -420,7 +427,7 @@ describe('claude-integration-handler', () => {
|
||||
const { invokeClaude } = await import('../claude-integration-handler');
|
||||
invokeClaude(terminal, '/tmp/project', 'prof-3', () => null, vi.fn());
|
||||
|
||||
const written = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const written = mockWriteToPty.mock.calls[0][1] as string;
|
||||
expect(written).toContain(getQuotedCommand(platform, command));
|
||||
expect(written).toContain(getPathPrefixExpectation(platform, '/opt/claude/bin:/usr/bin'));
|
||||
expect(profileManager.getProfile).toHaveBeenCalledWith('prof-3');
|
||||
@@ -445,7 +452,7 @@ describe('claude-integration-handler', () => {
|
||||
// Even when sessionId is passed, it should be ignored and --continue used
|
||||
resumeClaude(terminal, 'abc123', () => null);
|
||||
|
||||
const resumeCall = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const resumeCall = mockWriteToPty.mock.calls[0][1] as string;
|
||||
expect(resumeCall).toContain(getPathPrefixExpectation(platform, '/opt/claude/bin:/usr/bin'));
|
||||
expect(resumeCall).toContain(getQuotedCommand(platform, '/opt/claude/bin/claude') + ' --continue');
|
||||
expect(resumeCall).not.toContain('--resume');
|
||||
@@ -454,12 +461,12 @@ describe('claude-integration-handler', () => {
|
||||
expect(terminal.isClaudeMode).toBe(true);
|
||||
expect(mockPersistSession).toHaveBeenCalledWith(terminal);
|
||||
|
||||
vi.mocked(terminal.pty.write).mockClear();
|
||||
mockWriteToPty.mockClear();
|
||||
mockPersistSession.mockClear();
|
||||
terminal.projectPath = undefined;
|
||||
terminal.isClaudeMode = false;
|
||||
resumeClaude(terminal, undefined, () => null);
|
||||
const continueCall = vi.mocked(terminal.pty.write).mock.calls[0][0] as string;
|
||||
const continueCall = mockWriteToPty.mock.calls[0][1] as string;
|
||||
expect(continueCall).toContain(getQuotedCommand(platform, '/opt/claude/bin/claude') + ' --continue');
|
||||
expect(terminal.isClaudeMode).toBe(true);
|
||||
expect(terminal.claudeSessionId).toBeUndefined();
|
||||
@@ -484,7 +491,7 @@ describe('claude-integration-handler', () => {
|
||||
const { invokeClaude } = await import('../claude-integration-handler');
|
||||
expect(() => invokeClaude(terminal, '/tmp/project', undefined, () => null, vi.fn())).toThrow('boom');
|
||||
expect(mockReleaseSessionId).toHaveBeenCalledWith('term-err');
|
||||
expect(terminal.pty.write).not.toHaveBeenCalled();
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when resumeClaude cannot resolve the CLI invocation', async () => {
|
||||
@@ -500,7 +507,7 @@ describe('claude-integration-handler', () => {
|
||||
|
||||
const { resumeClaude } = await import('../claude-integration-handler');
|
||||
expect(() => resumeClaude(terminal, 'abc123', () => null)).toThrow('boom');
|
||||
expect(terminal.pty.write).not.toHaveBeenCalled();
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws when writing the OAuth token temp file fails', async () => {
|
||||
@@ -528,7 +535,7 @@ describe('claude-integration-handler', () => {
|
||||
|
||||
const { invokeClaude } = await import('../claude-integration-handler');
|
||||
expect(() => invokeClaude(terminal, '/tmp/project', 'prof-err', () => null, vi.fn())).toThrow('disk full');
|
||||
expect(terminal.pty.write).not.toHaveBeenCalled();
|
||||
expect(mockWriteToPty).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { IPC_CHANNELS } from '../../shared/constants';
|
||||
import { getClaudeProfileManager, initializeClaudeProfileManager } from '../claude-profile-manager';
|
||||
import * as OutputParser from './output-parser';
|
||||
import * as SessionHandler from './session-handler';
|
||||
import * as PtyManager from './pty-manager';
|
||||
import { debugLog, debugError } from '../../shared/utils/debug-logger';
|
||||
import { escapeShellArg, escapeForWindowsDoubleQuote, buildCdCommand } from '../../shared/utils/shell-escape';
|
||||
import { getClaudeCliInvocation, getClaudeCliInvocationAsync } from '../claude-cli-utils';
|
||||
@@ -599,7 +600,8 @@ export function invokeClaude(
|
||||
|
||||
const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'temp-file', tempFile }, extraFlags);
|
||||
debugLog('[ClaudeIntegration:invokeClaude] Executing command (temp file method, history-safe)');
|
||||
terminal.pty.write(command);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, command);
|
||||
profileManager.markProfileUsed(activeProfile.id);
|
||||
finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture);
|
||||
debugLog('[ClaudeIntegration:invokeClaude] ========== INVOKE CLAUDE COMPLETE (temp file) ==========');
|
||||
@@ -607,7 +609,8 @@ export function invokeClaude(
|
||||
} else if (activeProfile.configDir) {
|
||||
const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'config-dir', configDir: activeProfile.configDir }, extraFlags);
|
||||
debugLog('[ClaudeIntegration:invokeClaude] Executing command (configDir method, history-safe)');
|
||||
terminal.pty.write(command);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, command);
|
||||
profileManager.markProfileUsed(activeProfile.id);
|
||||
finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture);
|
||||
debugLog('[ClaudeIntegration:invokeClaude] ========== INVOKE CLAUDE COMPLETE (configDir) ==========');
|
||||
@@ -623,7 +626,8 @@ export function invokeClaude(
|
||||
|
||||
const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags);
|
||||
debugLog('[ClaudeIntegration:invokeClaude] Executing command (default method):', command);
|
||||
terminal.pty.write(command);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, command);
|
||||
|
||||
if (activeProfile) {
|
||||
profileManager.markProfileUsed(activeProfile.id);
|
||||
@@ -690,7 +694,8 @@ export function resumeClaude(
|
||||
|
||||
const command = `${pathPrefix}${escapedClaudeCmd} --continue`;
|
||||
|
||||
terminal.pty.write(`${command}\r`);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, `${command}\r`);
|
||||
|
||||
// Only auto-rename if terminal has default name
|
||||
// This preserves user-customized names and prevents renaming on every resume
|
||||
@@ -817,7 +822,8 @@ export async function invokeClaudeAsync(
|
||||
|
||||
const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'temp-file', tempFile }, extraFlags);
|
||||
debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (temp file method, history-safe)');
|
||||
terminal.pty.write(command);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, command);
|
||||
profileManager.markProfileUsed(activeProfile.id);
|
||||
finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture);
|
||||
debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (temp file) ==========');
|
||||
@@ -825,7 +831,8 @@ export async function invokeClaudeAsync(
|
||||
} else if (activeProfile.configDir) {
|
||||
const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'config-dir', configDir: activeProfile.configDir }, extraFlags);
|
||||
debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (configDir method, history-safe)');
|
||||
terminal.pty.write(command);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, command);
|
||||
profileManager.markProfileUsed(activeProfile.id);
|
||||
finalizeClaudeInvoke(terminal, activeProfile, projectPath, startTime, getWindow, onSessionCapture);
|
||||
debugLog('[ClaudeIntegration:invokeClaudeAsync] ========== INVOKE CLAUDE COMPLETE (configDir) ==========');
|
||||
@@ -841,7 +848,8 @@ export async function invokeClaudeAsync(
|
||||
|
||||
const command = buildClaudeShellCommand(cwdCommand, pathPrefix, escapedClaudeCmd, { method: 'default' }, extraFlags);
|
||||
debugLog('[ClaudeIntegration:invokeClaudeAsync] Executing command (default method):', command);
|
||||
terminal.pty.write(command);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, command);
|
||||
|
||||
if (activeProfile) {
|
||||
profileManager.markProfileUsed(activeProfile.id);
|
||||
@@ -917,7 +925,8 @@ export async function resumeClaudeAsync(
|
||||
|
||||
const command = `${pathPrefix}${escapedClaudeCmd} --continue`;
|
||||
|
||||
terminal.pty.write(`${command}\r`);
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, `${command}\r`);
|
||||
|
||||
// Only auto-rename if terminal has default name
|
||||
// This preserves user-customized names and prevents renaming on every resume
|
||||
@@ -1091,14 +1100,16 @@ export async function switchClaudeProfile(
|
||||
|
||||
// Send Ctrl+C to interrupt any ongoing operation
|
||||
debugLog('[ClaudeIntegration:switchClaudeProfile] Sending Ctrl+C (\\x03)');
|
||||
terminal.pty.write('\x03');
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, '\x03');
|
||||
|
||||
// Wait briefly for Ctrl+C to take effect before sending /exit
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Send /exit command
|
||||
debugLog('[ClaudeIntegration:switchClaudeProfile] Sending /exit command');
|
||||
terminal.pty.write('/exit\r');
|
||||
// Use PtyManager.writeToPty for safer write with error handling
|
||||
PtyManager.writeToPty(terminal, '/exit\r');
|
||||
|
||||
// Wait for Claude to actually exit by monitoring for shell prompt
|
||||
const exitResult = await waitForClaudeExit(terminal, { timeout: 5000, pollInterval: 100 });
|
||||
|
||||
@@ -28,7 +28,7 @@ export const THINKING_BUDGET_MAP: Record<string, number | null> = {
|
||||
low: 1024,
|
||||
medium: 4096,
|
||||
high: 16384,
|
||||
ultrathink: 64000 // Maximum reasoning depth (Claude API hard limit)
|
||||
ultrathink: 63999 // Maximum reasoning depth (API requires max_tokens >= budget + 1, so 63999 + 1 = 64000 limit)
|
||||
} as const;
|
||||
|
||||
// ============================================
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// CommonJS wrapper for build scripts that cannot import TypeScript directly.
|
||||
'use strict';
|
||||
|
||||
function getCurrentPlatform() {
|
||||
const p = process.platform;
|
||||
if (p === 'win32' || p === 'darwin' || p === 'linux') {
|
||||
return p;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function isWindows() {
|
||||
return getCurrentPlatform() === 'win32';
|
||||
}
|
||||
|
||||
function isMacOS() {
|
||||
return getCurrentPlatform() === 'darwin';
|
||||
}
|
||||
|
||||
function isLinux() {
|
||||
return getCurrentPlatform() === 'linux';
|
||||
}
|
||||
|
||||
function isUnix() {
|
||||
return isMacOS() || isLinux();
|
||||
}
|
||||
|
||||
function toNodePlatform(platform) {
|
||||
const map = {
|
||||
mac: 'darwin',
|
||||
win: 'win32',
|
||||
darwin: 'darwin',
|
||||
win32: 'win32',
|
||||
linux: 'linux',
|
||||
};
|
||||
return map[platform] || platform;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCurrentPlatform,
|
||||
isWindows,
|
||||
isMacOS,
|
||||
isLinux,
|
||||
isUnix,
|
||||
toNodePlatform,
|
||||
};
|
||||
Generated
+198
-98
@@ -196,16 +196,6 @@
|
||||
"lru-cache": "^11.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "6.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz",
|
||||
@@ -220,16 +210,6 @@
|
||||
"lru-cache": "^11.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
@@ -337,6 +317,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
@@ -347,6 +337,13 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
@@ -6165,6 +6162,16 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/glob": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||
@@ -6209,6 +6216,33 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/tar": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz",
|
||||
"integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cacache/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
|
||||
@@ -6253,9 +6287,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001765",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
|
||||
"integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
|
||||
"version": "1.0.30001764",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
|
||||
"integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -6366,13 +6400,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-pickle-js": {
|
||||
@@ -6735,16 +6769,6 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle/node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -8408,13 +8432,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
@@ -9510,13 +9527,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
@@ -10642,13 +10659,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass-flush/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minipass-pipeline": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
|
||||
@@ -10675,13 +10685,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass-pipeline/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minipass-sized": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
|
||||
@@ -10708,13 +10711,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass-sized/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
@@ -10729,17 +10725,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/module-details-from-path": {
|
||||
@@ -10893,6 +10888,16 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp/node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp/node_modules/isexe": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
|
||||
@@ -10903,6 +10908,23 @@
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp/node_modules/tar": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz",
|
||||
"integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp/node_modules/which": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
|
||||
@@ -10919,6 +10941,16 @@
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
@@ -12740,30 +12772,84 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.3",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz",
|
||||
"integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"node_modules/tar/node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/temp": {
|
||||
@@ -12830,6 +12916,20 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/temp/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-async-pool": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz",
|
||||
@@ -14331,9 +14431,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
"jsdom": "^27.4.0"
|
||||
},
|
||||
"overrides": {
|
||||
"tar": "^7.5.3",
|
||||
"@electron/rebuild": "4.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ class TestThinkingLevelValidation:
|
||||
assert get_thinking_budget("none") is None
|
||||
|
||||
def test_ultrathink_max_budget(self):
|
||||
"""Test that 'ultrathink' returns maximum budget (Claude API's 64000 token limit)."""
|
||||
assert get_thinking_budget("ultrathink") == 64000
|
||||
"""Test that 'ultrathink' returns maximum budget (63999 so max_tokens = 63999 + 1 = 64000 limit)."""
|
||||
assert get_thinking_budget("ultrathink") == 63999
|
||||
|
||||
def test_invalid_level_logs_warning(self, caplog):
|
||||
"""Test that invalid thinking level logs a warning."""
|
||||
@@ -89,4 +89,4 @@ class TestThinkingLevelValidation:
|
||||
assert get_thinking_budget("low") == 1024
|
||||
assert get_thinking_budget("medium") == 4096
|
||||
assert get_thinking_budget("high") == 16384
|
||||
assert get_thinking_budget("ultrathink") == 64000
|
||||
assert get_thinking_budget("ultrathink") == 63999
|
||||
|
||||
Reference in New Issue
Block a user