chore: merge origin/develop into feat/bmad-adopter-integration

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Test User
2026-01-19 14:53:41 +01:00
co-authored by Claude Opus 4.5
18 changed files with 947 additions and 174 deletions
+6 -3
View File
@@ -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
)
+6 -6
View File
@@ -16,17 +16,17 @@
### Stable Release
<!-- STABLE_VERSION_BADGE -->
[![Stable](https://img.shields.io/badge/stable-2.7.5-blue?style=flat-square)](https://github.com/AndyMik90/Auto-Claude/releases/tag/v2.7.5)
[![Stable](https://img.shields.io/badge/stable-2.7.4-blue?style=flat-square)](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 -->
+1 -1
View File
@@ -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
+9 -1
View File
@@ -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: {
+8 -6
View File
@@ -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",
+106 -28
View File
@@ -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 };
+5
View File
@@ -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 });
+1 -1
View File
@@ -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;
// ============================================
+46
View File
@@ -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,
};
+198 -98
View File
@@ -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"
},
-1
View File
@@ -45,7 +45,6 @@
"jsdom": "^27.4.0"
},
"overrides": {
"tar": "^7.5.3",
"@electron/rebuild": "4.0.2"
}
}
+3 -3
View File
@@ -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