mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
chore: 清理 src 下 33 项死代码和类型断言
删除未使用的文件/目录(mcp/adapter、cli/update.ts 等)、 未使用的重导出文件(design-system/color.ts 等 12 个)、 7 个零引用的导出函数、修复 5 处 as any 为精确类型。 净减少 ~1194 行代码,precheck 4077 测试全部通过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -68,13 +68,3 @@ export class TmuxEngine implements BgEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getTmuxInstallHint(): string {
|
||||
if (process.platform === 'darwin') {
|
||||
return 'Install with: brew install tmux'
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
return 'tmux is not natively available on Windows. Consider using WSL.'
|
||||
}
|
||||
return 'Install with: sudo apt install tmux (or your package manager)'
|
||||
}
|
||||
|
||||
@@ -1,422 +0,0 @@
|
||||
import chalk from 'chalk'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import {
|
||||
getLatestVersion,
|
||||
type InstallStatus,
|
||||
installGlobalPackage,
|
||||
} from 'src/utils/autoUpdater.js'
|
||||
import { regenerateCompletionCache } from 'src/utils/completionCache.js'
|
||||
import {
|
||||
getGlobalConfig,
|
||||
type InstallMethod,
|
||||
saveGlobalConfig,
|
||||
} from 'src/utils/config.js'
|
||||
import { logForDebugging } from 'src/utils/debug.js'
|
||||
import { getDoctorDiagnostic } from 'src/utils/doctorDiagnostic.js'
|
||||
import { gracefulShutdown } from 'src/utils/gracefulShutdown.js'
|
||||
import {
|
||||
installOrUpdateClaudePackage,
|
||||
localInstallationExists,
|
||||
} from 'src/utils/localInstaller.js'
|
||||
import {
|
||||
installLatest as installLatestNative,
|
||||
removeInstalledSymlink,
|
||||
} from 'src/utils/nativeInstaller/index.js'
|
||||
import { getPackageManager } from 'src/utils/nativeInstaller/packageManagers.js'
|
||||
import { writeToStdout } from 'src/utils/process.js'
|
||||
import { gte } from 'src/utils/semver.js'
|
||||
import { getInitialSettings } from 'src/utils/settings/settings.js'
|
||||
|
||||
export async function update() {
|
||||
logEvent('tengu_update_check', {})
|
||||
writeToStdout(`Current version: ${MACRO.VERSION}\n`)
|
||||
|
||||
const channel = getInitialSettings()?.autoUpdatesChannel ?? 'latest'
|
||||
writeToStdout(`Checking for updates to ${channel} version...\n`)
|
||||
|
||||
logForDebugging('update: Starting update check')
|
||||
|
||||
// Run diagnostic to detect potential issues
|
||||
logForDebugging('update: Running diagnostic')
|
||||
const diagnostic = await getDoctorDiagnostic()
|
||||
logForDebugging(`update: Installation type: ${diagnostic.installationType}`)
|
||||
logForDebugging(
|
||||
`update: Config install method: ${diagnostic.configInstallMethod}`,
|
||||
)
|
||||
|
||||
// Check for multiple installations
|
||||
if (diagnostic.multipleInstallations.length > 1) {
|
||||
writeToStdout('\n')
|
||||
writeToStdout(chalk.yellow('Warning: Multiple installations found') + '\n')
|
||||
for (const install of diagnostic.multipleInstallations) {
|
||||
const current =
|
||||
diagnostic.installationType === install.type
|
||||
? ' (currently running)'
|
||||
: ''
|
||||
writeToStdout(`- ${install.type} at ${install.path}${current}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
// Display warnings if any exist
|
||||
if (diagnostic.warnings.length > 0) {
|
||||
writeToStdout('\n')
|
||||
for (const warning of diagnostic.warnings) {
|
||||
logForDebugging(`update: Warning detected: ${warning.issue}`)
|
||||
|
||||
// Don't skip PATH warnings - they're always relevant
|
||||
// The user needs to know that 'which claude' points elsewhere
|
||||
logForDebugging(`update: Showing warning: ${warning.issue}`)
|
||||
|
||||
writeToStdout(chalk.yellow(`Warning: ${warning.issue}\n`))
|
||||
|
||||
writeToStdout(chalk.bold(`Fix: ${warning.fix}\n`))
|
||||
}
|
||||
}
|
||||
|
||||
// Update config if installMethod is not set (but skip for package managers)
|
||||
const config = getGlobalConfig()
|
||||
if (
|
||||
!config.installMethod &&
|
||||
diagnostic.installationType !== 'package-manager'
|
||||
) {
|
||||
writeToStdout('\n')
|
||||
writeToStdout('Updating configuration to track installation method...\n')
|
||||
let detectedMethod: 'local' | 'native' | 'global' | 'unknown' = 'unknown'
|
||||
|
||||
// Map diagnostic installation type to config install method
|
||||
switch (diagnostic.installationType) {
|
||||
case 'npm-local':
|
||||
detectedMethod = 'local'
|
||||
break
|
||||
case 'native':
|
||||
detectedMethod = 'native'
|
||||
break
|
||||
case 'npm-global':
|
||||
detectedMethod = 'global'
|
||||
break
|
||||
default:
|
||||
detectedMethod = 'unknown'
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
installMethod: detectedMethod,
|
||||
}))
|
||||
writeToStdout(`Installation method set to: ${detectedMethod}\n`)
|
||||
}
|
||||
|
||||
// Check if running from development build
|
||||
if (diagnostic.installationType === 'development') {
|
||||
writeToStdout('\n')
|
||||
writeToStdout(
|
||||
chalk.yellow('Warning: Cannot update development build') + '\n',
|
||||
)
|
||||
await gracefulShutdown(1)
|
||||
}
|
||||
|
||||
// Check if running from a package manager
|
||||
if (diagnostic.installationType === 'package-manager') {
|
||||
const packageManager = await getPackageManager()
|
||||
writeToStdout('\n')
|
||||
|
||||
if (packageManager === 'homebrew') {
|
||||
writeToStdout('Claude is managed by Homebrew.\n')
|
||||
const latest = await getLatestVersion(channel)
|
||||
if (latest && !gte(MACRO.VERSION, latest)) {
|
||||
writeToStdout(`Update available: ${MACRO.VERSION} → ${latest}\n`)
|
||||
writeToStdout('\n')
|
||||
writeToStdout('To update, run:\n')
|
||||
writeToStdout(chalk.bold(' brew upgrade claude-code') + '\n')
|
||||
} else {
|
||||
writeToStdout('Claude is up to date!\n')
|
||||
}
|
||||
} else if (packageManager === 'winget') {
|
||||
writeToStdout('Claude is managed by winget.\n')
|
||||
const latest = await getLatestVersion(channel)
|
||||
if (latest && !gte(MACRO.VERSION, latest)) {
|
||||
writeToStdout(`Update available: ${MACRO.VERSION} → ${latest}\n`)
|
||||
writeToStdout('\n')
|
||||
writeToStdout('To update, run:\n')
|
||||
writeToStdout(
|
||||
chalk.bold(' winget upgrade Anthropic.ClaudeCode') + '\n',
|
||||
)
|
||||
} else {
|
||||
writeToStdout('Claude is up to date!\n')
|
||||
}
|
||||
} else if (packageManager === 'apk') {
|
||||
writeToStdout('Claude is managed by apk.\n')
|
||||
const latest = await getLatestVersion(channel)
|
||||
if (latest && !gte(MACRO.VERSION, latest)) {
|
||||
writeToStdout(`Update available: ${MACRO.VERSION} → ${latest}\n`)
|
||||
writeToStdout('\n')
|
||||
writeToStdout('To update, run:\n')
|
||||
writeToStdout(chalk.bold(' apk upgrade claude-code') + '\n')
|
||||
} else {
|
||||
writeToStdout('Claude is up to date!\n')
|
||||
}
|
||||
} else {
|
||||
// pacman, deb, and rpm don't get specific commands because they each have
|
||||
// multiple frontends (pacman: yay/paru/makepkg, deb: apt/apt-get/aptitude/nala,
|
||||
// rpm: dnf/yum/zypper)
|
||||
writeToStdout('Claude is managed by a package manager.\n')
|
||||
writeToStdout('Please use your package manager to update.\n')
|
||||
}
|
||||
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
|
||||
// Check for config/reality mismatch (skip for package-manager installs)
|
||||
if (
|
||||
config.installMethod &&
|
||||
diagnostic.configInstallMethod !== 'not set' &&
|
||||
diagnostic.installationType !== 'package-manager'
|
||||
) {
|
||||
const runningType = diagnostic.installationType
|
||||
const configExpects = diagnostic.configInstallMethod
|
||||
|
||||
// Map installation types for comparison
|
||||
const typeMapping: Record<string, string> = {
|
||||
'npm-local': 'local',
|
||||
'npm-global': 'global',
|
||||
native: 'native',
|
||||
development: 'development',
|
||||
unknown: 'unknown',
|
||||
}
|
||||
|
||||
const normalizedRunningType = typeMapping[runningType] || runningType
|
||||
|
||||
if (
|
||||
normalizedRunningType !== configExpects &&
|
||||
configExpects !== 'unknown'
|
||||
) {
|
||||
writeToStdout('\n')
|
||||
writeToStdout(chalk.yellow('Warning: Configuration mismatch') + '\n')
|
||||
writeToStdout(`Config expects: ${configExpects} installation\n`)
|
||||
writeToStdout(`Currently running: ${runningType}\n`)
|
||||
writeToStdout(
|
||||
chalk.yellow(
|
||||
`Updating the ${runningType} installation you are currently using`,
|
||||
) + '\n',
|
||||
)
|
||||
|
||||
// Update config to match reality
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
installMethod: normalizedRunningType as InstallMethod,
|
||||
}))
|
||||
writeToStdout(
|
||||
`Config updated to reflect current installation method: ${normalizedRunningType}\n`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle native installation updates first
|
||||
if (diagnostic.installationType === 'native') {
|
||||
logForDebugging(
|
||||
'update: Detected native installation, using native updater',
|
||||
)
|
||||
try {
|
||||
const result = await installLatestNative(channel, true)
|
||||
|
||||
// Handle lock contention gracefully
|
||||
if (result.lockFailed) {
|
||||
const pidInfo = result.lockHolderPid
|
||||
? ` (PID ${result.lockHolderPid})`
|
||||
: ''
|
||||
writeToStdout(
|
||||
chalk.yellow(
|
||||
`Another Claude process${pidInfo} is currently running. Please try again in a moment.`,
|
||||
) + '\n',
|
||||
)
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
|
||||
if (!result.latestVersion) {
|
||||
process.stderr.write('Failed to check for updates\n')
|
||||
await gracefulShutdown(1)
|
||||
}
|
||||
|
||||
if (result.latestVersion === MACRO.VERSION) {
|
||||
writeToStdout(
|
||||
chalk.green(`Claude Code is up to date (${MACRO.VERSION})`) + '\n',
|
||||
)
|
||||
} else {
|
||||
writeToStdout(
|
||||
chalk.green(
|
||||
`Successfully updated from ${MACRO.VERSION} to version ${result.latestVersion}`,
|
||||
) + '\n',
|
||||
)
|
||||
await regenerateCompletionCache()
|
||||
}
|
||||
await gracefulShutdown(0)
|
||||
} catch (error) {
|
||||
process.stderr.write('Error: Failed to install native update\n')
|
||||
process.stderr.write(String(error) + '\n')
|
||||
process.stderr.write('Try running "claude doctor" for diagnostics\n')
|
||||
await gracefulShutdown(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to existing JS/npm-based update logic
|
||||
// Remove native installer symlink since we're not using native installation
|
||||
// But only if user hasn't migrated to native installation
|
||||
if (config.installMethod !== 'native') {
|
||||
await removeInstalledSymlink()
|
||||
}
|
||||
|
||||
logForDebugging('update: Checking npm registry for latest version')
|
||||
logForDebugging(`update: Package URL: ${MACRO.PACKAGE_URL}`)
|
||||
const npmTag = channel === 'stable' ? 'stable' : 'latest'
|
||||
const npmCommand = `npm view ${MACRO.PACKAGE_URL}@${npmTag} version`
|
||||
logForDebugging(`update: Running: ${npmCommand}`)
|
||||
const latestVersion = await getLatestVersion(channel)
|
||||
logForDebugging(
|
||||
`update: Latest version from npm: ${latestVersion || 'FAILED'}`,
|
||||
)
|
||||
|
||||
if (!latestVersion) {
|
||||
logForDebugging('update: Failed to get latest version from npm registry')
|
||||
process.stderr.write(chalk.red('Failed to check for updates') + '\n')
|
||||
process.stderr.write('Unable to fetch latest version from npm registry\n')
|
||||
process.stderr.write('\n')
|
||||
process.stderr.write('Possible causes:\n')
|
||||
process.stderr.write(' • Network connectivity issues\n')
|
||||
process.stderr.write(' • npm registry is unreachable\n')
|
||||
process.stderr.write(' • Corporate proxy/firewall blocking npm\n')
|
||||
if (MACRO.PACKAGE_URL && !MACRO.PACKAGE_URL.startsWith('@anthropic')) {
|
||||
process.stderr.write(
|
||||
' • Internal/development build not published to npm\n',
|
||||
)
|
||||
}
|
||||
process.stderr.write('\n')
|
||||
process.stderr.write('Try:\n')
|
||||
process.stderr.write(' • Check your internet connection\n')
|
||||
process.stderr.write(' • Run with --debug flag for more details\n')
|
||||
const packageName =
|
||||
MACRO.PACKAGE_URL ||
|
||||
(process.env.USER_TYPE === 'ant'
|
||||
? '@anthropic-ai/claude-cli'
|
||||
: '@anthropic-ai/claude-code')
|
||||
process.stderr.write(
|
||||
` • Manually check: npm view ${packageName} version\n`,
|
||||
)
|
||||
|
||||
process.stderr.write(' • Check if you need to login: npm whoami\n')
|
||||
await gracefulShutdown(1)
|
||||
}
|
||||
|
||||
// Check if versions match exactly, including any build metadata (like SHA)
|
||||
if (latestVersion === MACRO.VERSION) {
|
||||
writeToStdout(
|
||||
chalk.green(`Claude Code is up to date (${MACRO.VERSION})`) + '\n',
|
||||
)
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
|
||||
writeToStdout(
|
||||
`New version available: ${latestVersion} (current: ${MACRO.VERSION})\n`,
|
||||
)
|
||||
writeToStdout('Installing update...\n')
|
||||
|
||||
// Determine update method based on what's actually running
|
||||
let useLocalUpdate = false
|
||||
let updateMethodName = ''
|
||||
|
||||
switch (diagnostic.installationType) {
|
||||
case 'npm-local':
|
||||
useLocalUpdate = true
|
||||
updateMethodName = 'local'
|
||||
break
|
||||
case 'npm-global':
|
||||
useLocalUpdate = false
|
||||
updateMethodName = 'global'
|
||||
break
|
||||
case 'unknown': {
|
||||
// Fallback to detection if we can't determine installation type
|
||||
const isLocal = await localInstallationExists()
|
||||
useLocalUpdate = isLocal
|
||||
updateMethodName = isLocal ? 'local' : 'global'
|
||||
writeToStdout(
|
||||
chalk.yellow('Warning: Could not determine installation type') + '\n',
|
||||
)
|
||||
writeToStdout(
|
||||
`Attempting ${updateMethodName} update based on file detection...\n`,
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
process.stderr.write(
|
||||
`Error: Cannot update ${diagnostic.installationType} installation\n`,
|
||||
)
|
||||
await gracefulShutdown(1)
|
||||
}
|
||||
|
||||
writeToStdout(`Using ${updateMethodName} installation update method...\n`)
|
||||
|
||||
logForDebugging(`update: Update method determined: ${updateMethodName}`)
|
||||
logForDebugging(`update: useLocalUpdate: ${useLocalUpdate}`)
|
||||
|
||||
let status: InstallStatus
|
||||
|
||||
if (useLocalUpdate) {
|
||||
logForDebugging(
|
||||
'update: Calling installOrUpdateClaudePackage() for local update',
|
||||
)
|
||||
status = await installOrUpdateClaudePackage(channel)
|
||||
} else {
|
||||
logForDebugging('update: Calling installGlobalPackage() for global update')
|
||||
status = await installGlobalPackage()
|
||||
}
|
||||
|
||||
logForDebugging(`update: Installation status: ${status}`)
|
||||
|
||||
switch (status) {
|
||||
case 'success':
|
||||
writeToStdout(
|
||||
chalk.green(
|
||||
`Successfully updated from ${MACRO.VERSION} to version ${latestVersion}`,
|
||||
) + '\n',
|
||||
)
|
||||
await regenerateCompletionCache()
|
||||
break
|
||||
case 'no_permissions':
|
||||
process.stderr.write(
|
||||
'Error: Insufficient permissions to install update\n',
|
||||
)
|
||||
if (useLocalUpdate) {
|
||||
process.stderr.write('Try manually updating with:\n')
|
||||
process.stderr.write(
|
||||
` cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}\n`,
|
||||
)
|
||||
} else {
|
||||
process.stderr.write('Try running with sudo or fix npm permissions\n')
|
||||
process.stderr.write(
|
||||
'Or consider using native installation with: claude install\n',
|
||||
)
|
||||
}
|
||||
await gracefulShutdown(1)
|
||||
break
|
||||
case 'install_failed':
|
||||
process.stderr.write('Error: Failed to install update\n')
|
||||
if (useLocalUpdate) {
|
||||
process.stderr.write('Try manually updating with:\n')
|
||||
process.stderr.write(
|
||||
` cd ~/.claude/local && npm update ${MACRO.PACKAGE_URL}\n`,
|
||||
)
|
||||
} else {
|
||||
process.stderr.write(
|
||||
'Or consider using native installation with: claude install\n',
|
||||
)
|
||||
}
|
||||
await gracefulShutdown(1)
|
||||
break
|
||||
case 'in_progress':
|
||||
process.stderr.write(
|
||||
'Error: Another instance is currently performing an update\n',
|
||||
)
|
||||
process.stderr.write('Please wait and try again later\n')
|
||||
await gracefulShutdown(1)
|
||||
break
|
||||
}
|
||||
await gracefulShutdown(0)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { Divider } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { FuzzyPicker } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { LoadingState } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { Pane } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { ProgressBar } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { Ratchet } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { StatusIcon } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { Tab, Tabs, useTabHeaderFocus, useTabsWidth } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { ThemeProvider, usePreviewTheme, useTheme, useThemeSetting } from '@anthropic/ink';
|
||||
@@ -1 +0,0 @@
|
||||
export { Box as default } from '@anthropic/ink';
|
||||
@@ -1,29 +0,0 @@
|
||||
import { type ColorType, colorize, type Color } from '@anthropic/ink'
|
||||
import { getTheme, type Theme, type ThemeName } from '../../utils/theme.js'
|
||||
|
||||
/**
|
||||
* Curried theme-aware color function. Resolves theme keys to raw color
|
||||
* values before delegating to the ink renderer's colorize.
|
||||
*/
|
||||
export function color(
|
||||
c: keyof Theme | Color | undefined,
|
||||
theme: ThemeName,
|
||||
type: ColorType = 'foreground',
|
||||
): (text: string) => string {
|
||||
return text => {
|
||||
if (!c) {
|
||||
return text
|
||||
}
|
||||
// Raw color values bypass theme lookup
|
||||
if (
|
||||
c.startsWith('rgb(') ||
|
||||
c.startsWith('#') ||
|
||||
c.startsWith('ansi256(') ||
|
||||
c.startsWith('ansi:')
|
||||
) {
|
||||
return colorize(text, c, type)
|
||||
}
|
||||
// Theme key lookup
|
||||
return colorize(text, getTheme(theme)[c as keyof Theme], type)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
// Type re-exports for DreamTask — bridges the component tree to the task registry.
|
||||
// The real implementation lives in src/tasks/DreamTask/DreamTask.ts.
|
||||
// Note: Currently unused — BackgroundTasksDialog.tsx imports directly from
|
||||
// src/tasks/DreamTask/DreamTask.js. Kept for decompilation completeness.
|
||||
|
||||
export type {
|
||||
DreamTaskState,
|
||||
DreamPhase,
|
||||
DreamTurn,
|
||||
} from '../../../../../tasks/DreamTask/DreamTask.js'
|
||||
export {
|
||||
isDreamTask,
|
||||
registerDreamTask,
|
||||
addDreamTurn,
|
||||
completeDreamTask,
|
||||
failDreamTask,
|
||||
DreamTask,
|
||||
} from '../../../../../tasks/DreamTask/DreamTask.js'
|
||||
@@ -1,2 +0,0 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {}
|
||||
@@ -185,8 +185,8 @@ export async function getOutputStyleConfig(): Promise<OutputStyleConfig | null>
|
||||
const forcedStyles = Object.values(allStyles).filter(
|
||||
(style): style is OutputStyleConfig =>
|
||||
style !== null &&
|
||||
(style as any).source === 'plugin' &&
|
||||
(style as any).forceForPlugin === true,
|
||||
style.source === 'plugin' &&
|
||||
style.forceForPlugin === true,
|
||||
)
|
||||
|
||||
const firstForcedStyle = forcedStyles[0]
|
||||
@@ -209,8 +209,3 @@ export async function getOutputStyleConfig(): Promise<OutputStyleConfig | null>
|
||||
|
||||
return allStyles[outputStyle] ?? null
|
||||
}
|
||||
|
||||
export function hasCustomOutputStyle(): boolean {
|
||||
const style = getSettings_DEPRECATED()?.outputStyle
|
||||
return style !== undefined && style !== DEFAULT_OUTPUT_STYLE_NAME
|
||||
}
|
||||
|
||||
@@ -387,13 +387,7 @@ async function getFilesUsingGit(
|
||||
* For example, if the input is ['src/index.js', 'src/utils/helpers.js'],
|
||||
* the output will be ['src/', 'src/utils/'].
|
||||
* @param files An array of file paths
|
||||
* @returns An array of unique directory names with a trailing separator
|
||||
*/
|
||||
export function getDirectoryNames(files: string[]): string[] {
|
||||
const directoryNames = new Set<string>()
|
||||
collectDirectoryNames(files, 0, files.length, directoryNames)
|
||||
return [...directoryNames].map(d => d + path.sep)
|
||||
}
|
||||
|
||||
/**
|
||||
* Async variant: yields every ~10k files so 270k+ file lists don't block
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useNotifications } from 'src/context/notifications.js'
|
||||
import { getIsRemoteMode } from '../../bootstrap/state.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { PermissionMode } from '../../utils/permissions/PermissionMode.js'
|
||||
import {
|
||||
getAutoModeUnavailableNotification,
|
||||
getAutoModeUnavailableReason,
|
||||
} from '../../utils/permissions/permissionSetup.js'
|
||||
import { hasAutoModeOptIn } from '../../utils/settings/settings.js'
|
||||
|
||||
/**
|
||||
* Shows a one-shot notification when the shift-tab carousel wraps past where
|
||||
* auto mode would have been. Covers all reasons (settings, circuit-breaker,
|
||||
* org-allowlist). The startup case (defaultMode: auto silently downgraded) is
|
||||
* handled by verifyAutoModeGateAccess → checkAndDisableAutoModeIfNeeded.
|
||||
*/
|
||||
export function useAutoModeUnavailableNotification(): void {
|
||||
const { addNotification } = useNotifications()
|
||||
const mode = useAppState(s => s.toolPermissionContext.mode)
|
||||
const isAutoModeAvailable = useAppState(
|
||||
s => s.toolPermissionContext.isAutoModeAvailable,
|
||||
)
|
||||
const shownRef = useRef(false)
|
||||
const prevModeRef = useRef<PermissionMode>(mode)
|
||||
|
||||
useEffect(() => {
|
||||
const prevMode = prevModeRef.current
|
||||
prevModeRef.current = mode
|
||||
|
||||
if (!feature('TRANSCRIPT_CLASSIFIER')) return
|
||||
if (getIsRemoteMode()) return
|
||||
if (shownRef.current) return
|
||||
|
||||
const wrappedPastAutoSlot =
|
||||
mode === 'default' &&
|
||||
prevMode !== 'default' &&
|
||||
prevMode !== 'auto' &&
|
||||
!isAutoModeAvailable &&
|
||||
hasAutoModeOptIn()
|
||||
|
||||
if (!wrappedPastAutoSlot) return
|
||||
|
||||
const reason = getAutoModeUnavailableReason()
|
||||
if (!reason) return
|
||||
|
||||
shownRef.current = true
|
||||
addNotification({
|
||||
key: 'auto-mode-unavailable',
|
||||
text: getAutoModeUnavailableNotification(reason),
|
||||
color: 'warning',
|
||||
priority: 'medium',
|
||||
})
|
||||
}, [mode, isAutoModeAvailable, addNotification])
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Re-export from @anthropic/ink keybindings module
|
||||
export { getKeyName, matchesKeystroke, matchesBinding } from '@anthropic/ink'
|
||||
@@ -1,61 +0,0 @@
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js'
|
||||
import { logError } from '../utils/log.js'
|
||||
import {
|
||||
getSettingsForSource,
|
||||
updateSettingsForSource,
|
||||
} from '../utils/settings/settings.js'
|
||||
/**
|
||||
* Migration: Move user-set autoUpdates preference to settings.json env var
|
||||
* Only migrates if user explicitly disabled auto-updates (not for protection)
|
||||
* This preserves user intent while allowing native installations to auto-update
|
||||
*/
|
||||
export function migrateAutoUpdatesToSettings(): void {
|
||||
const globalConfig = getGlobalConfig()
|
||||
|
||||
// Only migrate if autoUpdates was explicitly set to false by user preference
|
||||
// (not automatically for native protection)
|
||||
if (
|
||||
globalConfig.autoUpdates !== false ||
|
||||
globalConfig.autoUpdatesProtectedForNative === true
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const userSettings = getSettingsForSource('userSettings') || {}
|
||||
|
||||
// Always set DISABLE_AUTOUPDATER to preserve user intent
|
||||
// We need to overwrite even if it exists, to ensure the migration is complete
|
||||
updateSettingsForSource('userSettings', {
|
||||
...userSettings,
|
||||
env: {
|
||||
...userSettings.env,
|
||||
DISABLE_AUTOUPDATER: '1',
|
||||
},
|
||||
})
|
||||
|
||||
logEvent('tengu_migrate_autoupdates_to_settings', {
|
||||
was_user_preference: true,
|
||||
already_had_env_var: !!userSettings.env?.DISABLE_AUTOUPDATER,
|
||||
})
|
||||
|
||||
// explicitly set, so this takes effect immediately
|
||||
process.env.DISABLE_AUTOUPDATER = '1'
|
||||
|
||||
// Remove autoUpdates from global config after successful migration
|
||||
saveGlobalConfig(current => {
|
||||
const {
|
||||
autoUpdates: _,
|
||||
autoUpdatesProtectedForNative: __,
|
||||
...updatedConfig
|
||||
} = current
|
||||
return updatedConfig
|
||||
})
|
||||
} catch (error) {
|
||||
logError(new Error(`Failed to migrate auto-updates: ${error}`))
|
||||
logEvent('tengu_migrate_autoupdates_error', {
|
||||
has_error: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
// Host analytics adapter — bridges logEvent to mcp-client's AnalyticsSink interface
|
||||
|
||||
import type { AnalyticsSink } from '@claude-code-best/mcp-client'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../analytics/index.js'
|
||||
|
||||
/**
|
||||
* Creates an AnalyticsSink implementation that delegates to the host's logEvent.
|
||||
*/
|
||||
export function createMcpAnalytics(): AnalyticsSink {
|
||||
return {
|
||||
trackEvent(event: string, metadata: Record<string, unknown>) {
|
||||
logEvent(
|
||||
event,
|
||||
metadata as Record<
|
||||
string,
|
||||
AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
>,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Host auth provider adapter — bridges OAuth token management to mcp-client's AuthProvider interface
|
||||
|
||||
import type { AuthProvider } from '@claude-code-best/mcp-client'
|
||||
import {
|
||||
getClaudeAIOAuthTokens,
|
||||
checkAndRefreshOAuthTokenIfNeeded,
|
||||
handleOAuth401Error,
|
||||
} from '../../../utils/auth.js'
|
||||
|
||||
/**
|
||||
* Creates an AuthProvider implementation using the host's OAuth token management.
|
||||
*/
|
||||
export function createMcpAuth(): AuthProvider {
|
||||
return {
|
||||
async getTokens() {
|
||||
const tokens = getClaudeAIOAuthTokens()
|
||||
if (!tokens) return null
|
||||
return { accessToken: tokens.accessToken }
|
||||
},
|
||||
async refreshTokens() {
|
||||
await checkAndRefreshOAuthTokenIfNeeded()
|
||||
},
|
||||
async handleOAuthError(error: unknown) {
|
||||
const currentToken = getClaudeAIOAuthTokens()?.accessToken ?? ''
|
||||
await handleOAuth401Error(currentToken)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Host feature gate adapter — bridges feature() to mcp-client's FeatureGate interface
|
||||
|
||||
import type { FeatureGate } from '@claude-code-best/mcp-client'
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
/**
|
||||
* Creates a FeatureGate implementation using the host's feature flag system.
|
||||
*/
|
||||
export function createMcpFeatureGate(): FeatureGate {
|
||||
return {
|
||||
isEnabled(flag: string) {
|
||||
return feature(flag)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Host HTTP config adapter — bridges getUserAgent/getSessionId to mcp-client's HttpConfig interface
|
||||
|
||||
import type { HttpConfig } from '@claude-code-best/mcp-client'
|
||||
import { getMCPUserAgent } from '../../../utils/http.js'
|
||||
import { getSessionId } from '../../../bootstrap/state.js'
|
||||
|
||||
/**
|
||||
* Creates an HttpConfig implementation using the host's user agent and session ID.
|
||||
*/
|
||||
export function createMcpHttpConfig(): HttpConfig {
|
||||
return {
|
||||
getUserAgent: () => getMCPUserAgent(),
|
||||
getSessionId: () => getSessionId(),
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Host image processor adapter — bridges maybeResizeAndDownsampleImageBuffer to mcp-client's ImageProcessor interface
|
||||
|
||||
import type { ImageProcessor } from '@claude-code-best/mcp-client'
|
||||
import { maybeResizeAndDownsampleImageBuffer } from '../../../utils/imageResizer.js'
|
||||
|
||||
/**
|
||||
* Creates an ImageProcessor implementation using the host's image resizing.
|
||||
*/
|
||||
export function createMcpImageProcessor(): ImageProcessor {
|
||||
return {
|
||||
async resizeAndDownsample(buffer: Buffer) {
|
||||
const result = await maybeResizeAndDownsampleImageBuffer(
|
||||
buffer,
|
||||
buffer.length,
|
||||
'png',
|
||||
)
|
||||
return result.buffer
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Host dependency injection — assembles McpClientDependencies from host infrastructure
|
||||
// This is the single entry point for creating the dependencies object used by createMcpManager()
|
||||
|
||||
import type { McpClientDependencies } from '@claude-code-best/mcp-client'
|
||||
import { createMcpLogger } from './logger.js'
|
||||
import { createMcpHttpConfig } from './httpConfig.js'
|
||||
import { createMcpProxyConfig } from './proxy.js'
|
||||
import { createMcpAnalytics } from './analytics.js'
|
||||
import { createMcpSubprocessEnv } from './subprocessEnv.js'
|
||||
import { createMcpStorage } from './storage.js'
|
||||
import { createMcpImageProcessor } from './imageProcessor.js'
|
||||
import { createMcpAuth } from './auth.js'
|
||||
/**
|
||||
* Creates the full set of MCP client dependencies using host infrastructure.
|
||||
* All adapters are lazy — they only call into host modules when invoked.
|
||||
*
|
||||
* Note: featureGate is omitted because Bun's feature() requires string-literal
|
||||
* arguments at compile time and cannot accept runtime variables. The interface
|
||||
* field is optional and the mcp-client package does not use it currently.
|
||||
*/
|
||||
export function createMcpDependencies(): McpClientDependencies {
|
||||
return {
|
||||
logger: createMcpLogger(),
|
||||
httpConfig: createMcpHttpConfig(),
|
||||
proxy: createMcpProxyConfig(),
|
||||
analytics: createMcpAnalytics(),
|
||||
subprocessEnv: createMcpSubprocessEnv(),
|
||||
storage: createMcpStorage(),
|
||||
imageProcessor: createMcpImageProcessor(),
|
||||
auth: createMcpAuth(),
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Host logger adapter — bridges logMCPDebug/logMCPError to mcp-client's Logger interface
|
||||
|
||||
import type { Logger } from '@claude-code-best/mcp-client'
|
||||
import { logMCPDebug, logMCPError } from '../../../utils/log.js'
|
||||
|
||||
/**
|
||||
* Creates a Logger implementation that delegates to the host's MCP logging system.
|
||||
*/
|
||||
export function createMcpLogger(): Logger {
|
||||
return {
|
||||
debug(message: string, ...args: unknown[]) {
|
||||
// Extract server name from bracketed prefix if present: [serverName] message
|
||||
const match = message.match(/^\[([^\]]+)\]\s*(.*)/)
|
||||
if (match) {
|
||||
logMCPDebug(match[1], match[2])
|
||||
}
|
||||
// Silently ignore messages without server name prefix
|
||||
},
|
||||
info(message: string, ...args: unknown[]) {
|
||||
const match = message.match(/^\[([^\]]+)\]\s*(.*)/)
|
||||
if (match) {
|
||||
logMCPDebug(match[1], match[2])
|
||||
}
|
||||
},
|
||||
warn(message: string, ...args: unknown[]) {
|
||||
const match = message.match(/^\[([^\]]+)\]\s*(.*)/)
|
||||
if (match) {
|
||||
logMCPError(match[1], message)
|
||||
}
|
||||
},
|
||||
error(message: string, ...args: unknown[]) {
|
||||
const match = message.match(/^\[([^\]]+)\]\s*(.*)/)
|
||||
if (match) {
|
||||
logMCPError(match[1], args[0] ?? message)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
// Host proxy config adapter — bridges proxy/MTLS to mcp-client's ProxyConfig interface
|
||||
|
||||
import type { ProxyConfig } from '@claude-code-best/mcp-client'
|
||||
import {
|
||||
getProxyFetchOptions,
|
||||
getWebSocketProxyAgent,
|
||||
getWebSocketProxyUrl,
|
||||
} from '../../../utils/proxy.js'
|
||||
import { getWebSocketTLSOptions } from '../../../utils/mtls.js'
|
||||
|
||||
/**
|
||||
* Creates a ProxyConfig implementation using the host's proxy and TLS settings.
|
||||
*/
|
||||
export function createMcpProxyConfig(): ProxyConfig {
|
||||
return {
|
||||
getFetchOptions() {
|
||||
return getProxyFetchOptions() as Record<string, unknown>
|
||||
},
|
||||
getWebSocketAgent(url: string) {
|
||||
return getWebSocketProxyAgent(url)
|
||||
},
|
||||
getWebSocketUrl(url: string) {
|
||||
return getWebSocketProxyUrl(url)
|
||||
},
|
||||
getTLSOptions() {
|
||||
const opts = getWebSocketTLSOptions()
|
||||
return opts as Record<string, unknown> | undefined
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Host content storage adapter — bridges persistBinaryContent to mcp-client's ContentStorage interface
|
||||
|
||||
import type { ContentStorage } from '@claude-code-best/mcp-client'
|
||||
import { persistBinaryContent } from '../../../utils/mcpOutputStorage.js'
|
||||
import {
|
||||
persistToolResult,
|
||||
isPersistError,
|
||||
} from '../../../utils/toolResultStorage.js'
|
||||
|
||||
/**
|
||||
* Creates a ContentStorage implementation using the host's binary persistence.
|
||||
*/
|
||||
export function createMcpStorage(): ContentStorage {
|
||||
return {
|
||||
async persistBinaryContent(data: Buffer, ext: string) {
|
||||
const result = await persistBinaryContent(
|
||||
data,
|
||||
ext,
|
||||
`mcp-adapter-${Date.now()}`,
|
||||
)
|
||||
if ('error' in result) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
return result.filepath
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Host subprocess environment adapter
|
||||
|
||||
import type { SubprocessEnvProvider } from '@claude-code-best/mcp-client'
|
||||
import { subprocessEnv } from '../../../utils/subprocessEnv.js'
|
||||
|
||||
/**
|
||||
* Creates a SubprocessEnvProvider using the host's subprocess environment logic.
|
||||
*/
|
||||
export function createMcpSubprocessEnv(): SubprocessEnvProvider {
|
||||
return {
|
||||
getEnv(additional?: Record<string, string>) {
|
||||
return { ...subprocessEnv(), ...additional } as Record<string, string>
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -66,34 +66,3 @@ export function generateRequestId(
|
||||
const timestamp = Date.now()
|
||||
return `${requestType}-${timestamp}@${agentId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a request ID into its components.
|
||||
* Returns null if the request ID doesn't match the expected format.
|
||||
*/
|
||||
export function parseRequestId(
|
||||
requestId: string,
|
||||
): { requestType: string; timestamp: number; agentId: string } | null {
|
||||
const atIndex = requestId.indexOf('@')
|
||||
if (atIndex === -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const prefix = requestId.slice(0, atIndex)
|
||||
const agentId = requestId.slice(atIndex + 1)
|
||||
|
||||
const lastDashIndex = prefix.lastIndexOf('-')
|
||||
if (lastDashIndex === -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
const requestType = prefix.slice(0, lastDashIndex)
|
||||
const timestampStr = prefix.slice(lastDashIndex + 1)
|
||||
const timestamp = parseInt(timestampStr, 10)
|
||||
|
||||
if (isNaN(timestamp)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return { requestType, timestamp, agentId }
|
||||
}
|
||||
|
||||
@@ -48,31 +48,6 @@ export function _resetRecordingStateForTesting(): void {
|
||||
recordingState.timestamp = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all .cast files for the current session.
|
||||
* Returns paths sorted by filename (chronological by timestamp suffix).
|
||||
*/
|
||||
export function getSessionRecordingPaths(): string[] {
|
||||
const sessionId = getSessionId()
|
||||
const projectsDir = join(getClaudeConfigHomeDir(), 'projects')
|
||||
const projectDir = join(projectsDir, sanitizePath(getOriginalCwd()))
|
||||
try {
|
||||
// eslint-disable-next-line custom-rules/no-sync-fs -- called during /share before upload, not in hot path
|
||||
const entries = getFsImplementation().readdirSync(projectDir)
|
||||
const names = (
|
||||
typeof entries[0] === 'string'
|
||||
? entries
|
||||
: (entries as { name: string }[]).map(e => e.name)
|
||||
) as string[]
|
||||
const files = names
|
||||
.filter(f => f.startsWith(sessionId) && f.endsWith('.cast'))
|
||||
.sort()
|
||||
return files.map(f => join(projectDir, f))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the recording file to match the current session ID.
|
||||
* Called after --resume/--continue changes the session ID via switchSession().
|
||||
@@ -124,14 +99,6 @@ function getTerminalSize(): { cols: number; rows: number } {
|
||||
return { cols, rows }
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush pending recording data to disk.
|
||||
* Call before reading the .cast file (e.g., during /share).
|
||||
*/
|
||||
export async function flushAsciicastRecorder(): Promise<void> {
|
||||
await recorder?.flush()
|
||||
}
|
||||
|
||||
/**
|
||||
* Install the asciicast recorder.
|
||||
* Wraps process.stdout.write to capture all terminal output with timestamps.
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
/**
|
||||
* OCR module using Windows.Media.Ocr.OcrEngine via PowerShell.
|
||||
* Captures a screen region or window, then runs WinRT OCR to extract text.
|
||||
*/
|
||||
|
||||
import { ps as runPs } from './shared.js'
|
||||
|
||||
export interface OcrLine {
|
||||
text: string
|
||||
bounds: { x: number; y: number; w: number; h: number }
|
||||
}
|
||||
|
||||
export interface OcrResult {
|
||||
text: string
|
||||
lines: OcrLine[]
|
||||
language: string
|
||||
}
|
||||
|
||||
function emptyResult(language: string): OcrResult {
|
||||
return { text: '', lines: [], language }
|
||||
}
|
||||
|
||||
/**
|
||||
* PowerShell script that:
|
||||
* 1. Screenshots a screen region using CopyFromScreen
|
||||
* 2. Saves to temp PNG
|
||||
* 3. Loads via WinRT BitmapDecoder -> SoftwareBitmap
|
||||
* 4. Runs OcrEngine.RecognizeAsync
|
||||
* 5. Outputs JSON with text, lines, and bounding rects
|
||||
*/
|
||||
function buildOcrRegionScript(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
lang: string,
|
||||
): string {
|
||||
return `
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||||
|
||||
# Load WinRT types
|
||||
$null = [Windows.Media.Ocr.OcrEngine, Windows.Foundation, ContentType = WindowsRuntime]
|
||||
$null = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Foundation, ContentType = WindowsRuntime]
|
||||
$null = [Windows.Graphics.Imaging.BitmapDecoder, Windows.Foundation, ContentType = WindowsRuntime]
|
||||
$null = [Windows.Storage.StorageFile, Windows.Foundation, ContentType = WindowsRuntime]
|
||||
$null = [Windows.Storage.Streams.RandomAccessStream, Windows.Foundation, ContentType = WindowsRuntime]
|
||||
$null = [Windows.Globalization.Language, Windows.Foundation, ContentType = WindowsRuntime]
|
||||
|
||||
# Await helper for WinRT async operations
|
||||
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | Where-Object {
|
||||
$_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and
|
||||
$_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation\`1'
|
||||
})[0]
|
||||
Function Await($WinRtTask, $ResultType) {
|
||||
$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
|
||||
$netTask = $asTask.Invoke($null, @($WinRtTask))
|
||||
$netTask.Wait(-1) | Out-Null
|
||||
$netTask.Result
|
||||
}
|
||||
|
||||
try {
|
||||
# Step 1: Screenshot region
|
||||
$bmp = New-Object System.Drawing.Bitmap(${w}, ${h})
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen(${x}, ${y}, 0, 0, (New-Object System.Drawing.Size(${w}, ${h})))
|
||||
$g.Dispose()
|
||||
|
||||
# Step 2: Save to temp file
|
||||
$tmpFile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "ocrtemp_$([guid]::NewGuid().ToString('N')).png")
|
||||
$bmp.Save($tmpFile, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$bmp.Dispose()
|
||||
|
||||
# Step 3: Open as StorageFile -> BitmapDecoder -> SoftwareBitmap
|
||||
$storageFile = Await ([Windows.Storage.StorageFile]::GetFileFromPathAsync($tmpFile)) ([Windows.Storage.StorageFile])
|
||||
$stream = Await ($storageFile.OpenAsync([Windows.Storage.FileAccessMode]::Read)) ([Windows.Storage.Streams.IRandomAccessStream])
|
||||
$decoder = Await ([Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream)) ([Windows.Graphics.Imaging.BitmapDecoder])
|
||||
$softwareBmp = Await ($decoder.GetSoftwareBitmapAsync()) ([Windows.Graphics.Imaging.SoftwareBitmap])
|
||||
|
||||
# Step 4: Create OCR engine
|
||||
$ocrLang = New-Object Windows.Globalization.Language('${lang}')
|
||||
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($ocrLang)
|
||||
if ($engine -eq $null) {
|
||||
# Fallback to en-US
|
||||
$ocrLang = New-Object Windows.Globalization.Language('en-US')
|
||||
$engine = [Windows.Media.Ocr.OcrEngine]::TryCreateFromLanguage($ocrLang)
|
||||
}
|
||||
if ($engine -eq $null) {
|
||||
Write-Output '{"text":"","lines":[],"language":"${lang}"}'
|
||||
return
|
||||
}
|
||||
|
||||
# Step 5: Run OCR
|
||||
$ocrResult = Await ($engine.RecognizeAsync($softwareBmp)) ([Windows.Media.Ocr.OcrResult])
|
||||
|
||||
# Step 6: Extract lines with bounding rects
|
||||
$lines = @()
|
||||
foreach ($line in $ocrResult.Lines) {
|
||||
$minX = [double]::MaxValue; $minY = [double]::MaxValue
|
||||
$maxX = 0.0; $maxY = 0.0
|
||||
foreach ($word in $line.Words) {
|
||||
$r = $word.BoundingRect
|
||||
if ($r.X -lt $minX) { $minX = $r.X }
|
||||
if ($r.Y -lt $minY) { $minY = $r.Y }
|
||||
if (($r.X + $r.Width) -gt $maxX) { $maxX = $r.X + $r.Width }
|
||||
if (($r.Y + $r.Height) -gt $maxY) { $maxY = $r.Y + $r.Height }
|
||||
}
|
||||
$lines += @{
|
||||
text = $line.Text
|
||||
bounds = @{
|
||||
x = [int]$minX
|
||||
y = [int]$minY
|
||||
w = [int]($maxX - $minX)
|
||||
h = [int]($maxY - $minY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$output = @{
|
||||
text = $ocrResult.Text
|
||||
lines = $lines
|
||||
language = $ocrLang.LanguageTag
|
||||
}
|
||||
Write-Output (ConvertTo-Json $output -Depth 4 -Compress)
|
||||
|
||||
# Cleanup
|
||||
$stream.Dispose()
|
||||
Remove-Item $tmpFile -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Output '{"text":"","lines":[],"language":"${lang}"}'
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* PowerShell script to get a window's bounding rect by title.
|
||||
*/
|
||||
function buildGetWindowRectScript(windowTitle: string): string {
|
||||
const escaped = windowTitle.replace(/'/g, "''")
|
||||
return `
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class WinRect {
|
||||
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
|
||||
public static extern IntPtr FindWindow(string c, string t);
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT { public int L, T, R, B; }
|
||||
public static string Get(string title) {
|
||||
IntPtr hwnd = FindWindow(null, title);
|
||||
if (hwnd == IntPtr.Zero) return "NOT_FOUND";
|
||||
RECT r; GetWindowRect(hwnd, out r);
|
||||
int w = r.R - r.L; int h = r.B - r.T;
|
||||
if (w <= 0 || h <= 0) return "INVALID_SIZE";
|
||||
return r.L + "," + r.T + "," + w + "," + h;
|
||||
}
|
||||
}
|
||||
'@
|
||||
[WinRect]::Get('${escaped}')
|
||||
`
|
||||
}
|
||||
|
||||
function parseOcrOutput(raw: string, lang: string): OcrResult {
|
||||
if (!raw) return emptyResult(lang)
|
||||
try {
|
||||
const parsed = JSON.parse(raw)
|
||||
return {
|
||||
text: parsed.text ?? '',
|
||||
lines: Array.isArray(parsed.lines)
|
||||
? parsed.lines.map((l: any) => ({
|
||||
text: l.text ?? '',
|
||||
bounds: {
|
||||
x: l.bounds?.x ?? 0,
|
||||
y: l.bounds?.y ?? 0,
|
||||
w: l.bounds?.w ?? 0,
|
||||
h: l.bounds?.h ?? 0,
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
language: parsed.language ?? lang,
|
||||
}
|
||||
} catch {
|
||||
return emptyResult(lang)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OCR on a screen region.
|
||||
* Screenshots the specified rectangle, then runs WinRT OcrEngine.
|
||||
*
|
||||
* @param x - Left coordinate
|
||||
* @param y - Top coordinate
|
||||
* @param w - Width in pixels
|
||||
* @param h - Height in pixels
|
||||
* @param lang - BCP-47 language tag (default 'en-US'). Confirmed: 'en-US', 'zh-Hans-CN'
|
||||
*/
|
||||
export async function ocrRegion(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
lang?: string,
|
||||
): Promise<OcrResult> {
|
||||
const language = lang ?? 'en-US'
|
||||
if (w <= 0 || h <= 0) return emptyResult(language)
|
||||
|
||||
try {
|
||||
const script = buildOcrRegionScript(x, y, w, h, language)
|
||||
const raw = runPs(script)
|
||||
return parseOcrOutput(raw, language)
|
||||
} catch {
|
||||
return emptyResult(language)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OCR on a specific window by its title.
|
||||
* Gets the window rect, then delegates to ocrRegion.
|
||||
*
|
||||
* @param windowTitle - Exact window title to find via FindWindow
|
||||
* @param lang - BCP-47 language tag (default 'en-US')
|
||||
*/
|
||||
export async function ocrWindow(
|
||||
windowTitle: string,
|
||||
lang?: string,
|
||||
): Promise<OcrResult> {
|
||||
const language = lang ?? 'en-US'
|
||||
|
||||
try {
|
||||
const rectScript = buildGetWindowRectScript(windowTitle)
|
||||
const raw = runPs(rectScript)
|
||||
const trimmed = raw.trim()
|
||||
|
||||
if (!trimmed || trimmed === 'NOT_FOUND' || trimmed === 'INVALID_SIZE') {
|
||||
return emptyResult(language)
|
||||
}
|
||||
|
||||
const parts = trimmed.split(',')
|
||||
if (parts.length !== 4) return emptyResult(language)
|
||||
|
||||
const [x, y, w, h] = parts.map(Number)
|
||||
if (!w || !h) return emptyResult(language)
|
||||
|
||||
return ocrRegion(x, y, w, h, lang)
|
||||
} catch {
|
||||
return emptyResult(language)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ export async function validateManifest(
|
||||
const errors = parseResult.error.flatten()
|
||||
const errorMessages = [
|
||||
...Object.entries(errors.fieldErrors).map(
|
||||
([field, errs]) => `${field}: ${(errs as any)?.join(', ')}`,
|
||||
([field, errs]) =>
|
||||
`${field}: ${(errs as string[] | undefined)?.join(', ')}`,
|
||||
),
|
||||
...(errors.formErrors || []),
|
||||
]
|
||||
|
||||
@@ -65,8 +65,9 @@ export function getMcpInstructionsDelta(
|
||||
attachmentCount++
|
||||
if (msg.attachment!.type !== 'mcp_instructions_delta') continue
|
||||
midCount++
|
||||
for (const n of (msg.attachment! as any).addedNames) announced.add(n)
|
||||
for (const n of (msg.attachment! as any).removedNames) announced.delete(n)
|
||||
const delta = msg.attachment! as unknown as McpInstructionsDelta
|
||||
for (const n of delta.addedNames) announced.add(n)
|
||||
for (const n of delta.removedNames) announced.delete(n)
|
||||
}
|
||||
|
||||
const connected = mcpClients.filter(
|
||||
|
||||
@@ -37,8 +37,8 @@ export function extractConversationText(messages: Message[]): string {
|
||||
if ('isMeta' in msg && msg.isMeta) continue
|
||||
if (
|
||||
'origin' in msg &&
|
||||
(msg as any).origin &&
|
||||
(msg as any).origin.kind !== 'human'
|
||||
(msg as unknown as { origin?: { kind?: string } }).origin &&
|
||||
(msg as unknown as { origin: { kind?: string } }).origin.kind !== 'human'
|
||||
)
|
||||
continue
|
||||
const content = msg.message!.content
|
||||
@@ -116,7 +116,9 @@ export async function generateSessionTitle(
|
||||
},
|
||||
})
|
||||
|
||||
const text = extractTextContent(result.message.content as any)
|
||||
const text = extractTextContent(
|
||||
result.message.content as readonly { readonly type: string }[],
|
||||
)
|
||||
|
||||
const parsed = titleSchema().safeParse(safeParseJSON(text))
|
||||
const title = parsed.success ? parsed.data.title.trim() || null : null
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
export {}
|
||||
export const watchSystemTheme: (
|
||||
querier: unknown,
|
||||
setTheme: React.Dispatch<
|
||||
React.SetStateAction<import('./systemTheme.js').SystemTheme>
|
||||
>,
|
||||
) => () => void = () => () => {}
|
||||
@@ -48,15 +48,6 @@ function isInternalWarning(warning: Error): boolean {
|
||||
// Store reference to our warning handler so we can detect if it's already installed
|
||||
let warningHandler: ((warning: Error) => void) | null = null
|
||||
|
||||
// For testing only - allows resetting the warning handler state
|
||||
export function resetWarningHandler(): void {
|
||||
if (warningHandler) {
|
||||
process.removeListener('warning', warningHandler)
|
||||
}
|
||||
warningHandler = null
|
||||
warningCounts.clear()
|
||||
}
|
||||
|
||||
export function initializeWarningHandler(): void {
|
||||
// Only set up handler once - check if our handler is already installed
|
||||
const currentListeners = process.listeners('warning')
|
||||
|
||||
Reference in New Issue
Block a user