fix: address GitHub issues #511, #517, #527, #531

- #511: Add gemini-3-flash model to GeminiAgent (type, RPM limits, validation)
- #517: Replace PowerShell with WMIC for Windows process management (fixes Git Bash/WSL)
- #527: Add Apple Silicon Homebrew paths for bun and uv detection
- #531: Remove duplicate type definitions from export-memories.ts using bridge file

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-01-04 00:57:48 -05:00
parent bb033b95f1
commit 4d0a10c458
7 changed files with 134 additions and 96 deletions
+5 -5
View File
@@ -32,7 +32,7 @@ function isBunInstalled() {
// Check common installation paths (handles fresh installs before PATH reload) // Check common installation paths (handles fresh installs before PATH reload)
const bunPaths = IS_WINDOWS const bunPaths = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')] ? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun']; : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
return bunPaths.some(existsSync); return bunPaths.some(existsSync);
} }
@@ -56,7 +56,7 @@ function getBunPath() {
// Check common installation paths // Check common installation paths
const bunPaths = IS_WINDOWS const bunPaths = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')] ? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun']; : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
for (const bunPath of bunPaths) { for (const bunPath of bunPaths) {
if (existsSync(bunPath)) return bunPath; if (existsSync(bunPath)) return bunPath;
@@ -102,7 +102,7 @@ function isUvInstalled() {
// Check common installation paths (handles fresh installs before PATH reload) // Check common installation paths (handles fresh installs before PATH reload)
const uvPaths = IS_WINDOWS const uvPaths = IS_WINDOWS
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')] ? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv']; : [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
return uvPaths.some(existsSync); return uvPaths.some(existsSync);
} }
@@ -156,7 +156,7 @@ function installBun() {
// Try common installation paths // Try common installation paths
const bunPaths = IS_WINDOWS const bunPaths = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')] ? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun']; : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
for (const bunPath of bunPaths) { for (const bunPath of bunPaths) {
if (existsSync(bunPath)) { if (existsSync(bunPath)) {
@@ -221,7 +221,7 @@ function installUv() {
// Try common installation paths // Try common installation paths
const uvPaths = IS_WINDOWS const uvPaths = IS_WINDOWS
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')] ? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv']; : [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
for (const uvPath of uvPaths) { for (const uvPath of uvPaths) {
if (existsSync(uvPath)) { if (existsSync(uvPath)) {
File diff suppressed because one or more lines are too long
+7 -74
View File
@@ -9,80 +9,13 @@ import { writeFileSync } from 'fs';
import { homedir } from 'os'; import { homedir } from 'os';
import { join } from 'path'; import { join } from 'path';
import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager'; import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
import type {
interface ObservationRecord { ObservationRecord,
id: number; SdkSessionRecord,
memory_session_id: string; SessionSummaryRecord,
project: string; UserPromptRecord,
text: string | null; ExportData
type: string; } from './types/export.js';
title: string;
subtitle: string | null;
facts: string | null;
narrative: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
interface SdkSessionRecord {
id: number;
content_session_id: string;
memory_session_id: string;
project: string;
user_prompt: string;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: string;
}
interface SessionSummaryRecord {
id: number;
memory_session_id: string;
project: string;
request: string | null;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
files_read: string | null;
files_edited: string | null;
notes: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
interface UserPromptRecord {
id: number;
content_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
created_at_epoch: number;
}
interface ExportData {
exportedAt: string;
exportedAtEpoch: number;
query: string;
project?: string;
totalObservations: number;
totalSessions: number;
totalSummaries: number;
totalPrompts: number;
observations: ObservationRecord[];
sessions: SdkSessionRecord[];
summaries: SessionSummaryRecord[];
prompts: UserPromptRecord[];
}
async function exportMemories(query: string, outputFile: string, project?: string) { async function exportMemories(query: string, outputFile: string, project?: string) {
try { try {
+2 -2
View File
@@ -17,11 +17,11 @@ const IS_WINDOWS = process.platform === 'win32';
// Common installation paths (handles fresh installs before PATH reload) // Common installation paths (handles fresh installs before PATH reload)
const BUN_COMMON_PATHS = IS_WINDOWS const BUN_COMMON_PATHS = IS_WINDOWS
? [join(homedir(), '.bun', 'bin', 'bun.exe')] ? [join(homedir(), '.bun', 'bin', 'bun.exe')]
: [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun']; : [join(homedir(), '.bun', 'bin', 'bun'), '/usr/local/bin/bun', '/opt/homebrew/bin/bun'];
const UV_COMMON_PATHS = IS_WINDOWS const UV_COMMON_PATHS = IS_WINDOWS
? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')] ? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
: [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv']; : [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv'), '/usr/local/bin/uv', '/opt/homebrew/bin/uv'];
/** /**
* Get the Bun executable path (from PATH or common install locations) * Get the Bun executable path (from PATH or common install locations)
+96
View File
@@ -0,0 +1,96 @@
/**
* Export/Import types for memory data
*
* These types represent the structure of exported memory data.
* They are aligned with the actual database schema and include all fields
* needed for complete data export and import operations.
*/
/**
* Observation record as stored in the database and exported
*/
export interface ObservationRecord {
id: number;
memory_session_id: string;
project: string;
text: string | null;
type: string;
title: string;
subtitle: string | null;
facts: string | null;
narrative: string | null;
concepts: string | null;
files_read: string | null;
files_modified: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
/**
* SDK Session record as stored in the database and exported
*/
export interface SdkSessionRecord {
id: number;
content_session_id: string;
memory_session_id: string;
project: string;
user_prompt: string;
started_at: string;
started_at_epoch: number;
completed_at: string | null;
completed_at_epoch: number | null;
status: string;
}
/**
* Session Summary record as stored in the database and exported
*/
export interface SessionSummaryRecord {
id: number;
memory_session_id: string;
project: string;
request: string | null;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
files_read: string | null;
files_edited: string | null;
notes: string | null;
prompt_number: number;
discovery_tokens: number | null;
created_at: string;
created_at_epoch: number;
}
/**
* User Prompt record as stored in the database and exported
*/
export interface UserPromptRecord {
id: number;
content_session_id: string;
prompt_number: number;
prompt_text: string;
created_at: string;
created_at_epoch: number;
}
/**
* Complete export data structure
*/
export interface ExportData {
exportedAt: string;
exportedAtEpoch: number;
query: string;
project?: string;
totalObservations: number;
totalSessions: number;
totalSummaries: number;
totalPrompts: number;
observations: ObservationRecord[];
sessions: SdkSessionRecord[];
summaries: SessionSummaryRecord[];
prompts: UserPromptRecord[];
}
+16 -10
View File
@@ -88,12 +88,15 @@ export async function getChildProcesses(parentPid: number): Promise<number[]> {
} }
try { try {
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty ProcessId"`; const cmd = `wmic process where "parentprocessid=${parentPid}" get processid /format:list`;
const { stdout } = await execAsync(cmd, { timeout: 60000 }); const { stdout } = await execAsync(cmd, { timeout: 60000 });
return stdout return stdout
.trim() .trim()
.split('\n') .split('\n')
.map(s => parseInt(s.trim(), 10)) .map(line => {
const match = line.match(/ProcessId=(\d+)/i);
return match ? parseInt(match[1], 10) : NaN;
})
.filter(n => !isNaN(n) && Number.isInteger(n) && n > 0); .filter(n => !isNaN(n) && Number.isInteger(n) && n > 0);
} catch (error) { } catch (error) {
// Shutdown cleanup - failure is non-critical, continue without child process cleanup // Shutdown cleanup - failure is non-critical, continue without child process cleanup
@@ -167,8 +170,8 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
try { try {
if (isWindows) { if (isWindows) {
// Windows: Use PowerShell Get-CimInstance to find chroma-mcp processes // Windows: Use WMIC to find chroma-mcp processes (avoids PowerShell $_ issues in Git Bash/WSL)
const cmd = `powershell -Command "Get-CimInstance Win32_Process | Where-Object { $_.Name -like '*python*' -and $_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`; const cmd = `wmic process where "name like '%python%' and commandline like '%chroma-mcp%'" get processid /format:list`;
const { stdout } = await execAsync(cmd, { timeout: 60000 }); const { stdout } = await execAsync(cmd, { timeout: 60000 });
if (!stdout.trim()) { if (!stdout.trim()) {
@@ -176,12 +179,15 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
return; return;
} }
const pidStrings = stdout.trim().split('\n'); const lines = stdout.trim().split('\n');
for (const pidStr of pidStrings) { for (const line of lines) {
const pid = parseInt(pidStr.trim(), 10); const match = line.match(/ProcessId=(\d+)/i);
// SECURITY: Validate PID is positive integer before adding to list if (match) {
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) { const pid = parseInt(match[1], 10);
pids.push(pid); // SECURITY: Validate PID is positive integer before adding to list
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
pids.push(pid);
}
} }
} }
} else { } else {
+4 -1
View File
@@ -36,7 +36,8 @@ export type GeminiModel =
| 'gemini-2.5-flash' | 'gemini-2.5-flash'
| 'gemini-2.5-pro' | 'gemini-2.5-pro'
| 'gemini-2.0-flash' | 'gemini-2.0-flash'
| 'gemini-2.0-flash-lite'; | 'gemini-2.0-flash-lite'
| 'gemini-3-flash';
// Free tier RPM limits by model (requests per minute) // Free tier RPM limits by model (requests per minute)
const GEMINI_RPM_LIMITS: Record<GeminiModel, number> = { const GEMINI_RPM_LIMITS: Record<GeminiModel, number> = {
@@ -45,6 +46,7 @@ const GEMINI_RPM_LIMITS: Record<GeminiModel, number> = {
'gemini-2.5-pro': 5, 'gemini-2.5-pro': 5,
'gemini-2.0-flash': 15, 'gemini-2.0-flash': 15,
'gemini-2.0-flash-lite': 30, 'gemini-2.0-flash-lite': 30,
'gemini-3-flash': 5,
}; };
// Track last request time for rate limiting // Track last request time for rate limiting
@@ -373,6 +375,7 @@ export class GeminiAgent {
'gemini-2.5-pro', 'gemini-2.5-pro',
'gemini-2.0-flash', 'gemini-2.0-flash',
'gemini-2.0-flash-lite', 'gemini-2.0-flash-lite',
'gemini-3-flash',
]; ];
let model: GeminiModel; let model: GeminiModel;