mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
refactor(hooks): deprecate block-image-read, add CLIProxy fallback
- Remove redundant block-image-read.cjs hook (image-analyzer handles all) - Add fallback blocking when CLIProxy unavailable (prevents context overflow) - Simplify fallback message to prevent context pollution/hallucination - Remove image-read-block-hook-env.ts and related exports - Update comments per PR #442 review suggestions - Add defensive check for empty models array Closes #426
This commit is contained in:
1 parent
26f4021770
commit
51b719ef34
11 files changed
+47
-362
No files matched your search
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* CCS Image Read Blocking Hook
|
|
||||||
*
|
|
||||||
* Blocks Claude's Read tool from reading image files to prevent context overflow.
|
|
||||||
* Each image can consume 100K+ tokens, causing immediate context exhaustion.
|
|
||||||
*
|
|
||||||
* This is a PreToolUse hook that runs BEFORE the tool is executed.
|
|
||||||
*
|
|
||||||
* Behavior (matches WebSearch pattern):
|
|
||||||
* - ENABLED by default for third-party profiles (settings, cliproxy)
|
|
||||||
* - DISABLED for native Claude accounts (account, default profiles)
|
|
||||||
* - User can override via config: hooks.block_image_read.enabled: false
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* Configured in ~/.claude/settings.json:
|
|
||||||
* {
|
|
||||||
* "hooks": {
|
|
||||||
* "PreToolUse": [{
|
|
||||||
* "matcher": "Read",
|
|
||||||
* "hooks": [{
|
|
||||||
* "type": "command",
|
|
||||||
* "command": "node ~/.ccs/hooks/block-image-read.cjs",
|
|
||||||
* "timeout": 5
|
|
||||||
* }]
|
|
||||||
* }]
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Environment Variables (set by CCS):
|
|
||||||
* CCS_BLOCK_IMAGE_READ=1 - Enable blocking (default for third-party)
|
|
||||||
* CCS_BLOCK_IMAGE_READ=0 - Disable blocking
|
|
||||||
* CCS_PROFILE_TYPE - Profile type (account, default, settings, cliproxy)
|
|
||||||
* CCS_DEBUG=1 - Enable debug output
|
|
||||||
*
|
|
||||||
* Exit codes:
|
|
||||||
* 0 - Allow tool (pass-through)
|
|
||||||
* 2 - Block tool (deny with message)
|
|
||||||
*
|
|
||||||
* @module hooks/block-image-read
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Image file extensions to block
|
|
||||||
const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|webp|gif|bmp|tiff|tif|ico|svg|heic|heif|avif)$/i;
|
|
||||||
|
|
||||||
// Read input from stdin
|
|
||||||
let input = '';
|
|
||||||
process.stdin.setEncoding('utf8');
|
|
||||||
process.stdin.on('data', (chunk) => {
|
|
||||||
input += chunk;
|
|
||||||
});
|
|
||||||
process.stdin.on('end', () => {
|
|
||||||
processHook();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle stdin not being available
|
|
||||||
process.stdin.on('error', () => {
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if hook should skip (for native Claude accounts).
|
|
||||||
* Matches WebSearch hook pattern.
|
|
||||||
*/
|
|
||||||
function shouldSkipHook() {
|
|
||||||
// Account/default profiles use native Claude - don't block
|
|
||||||
const profileType = process.env.CCS_PROFILE_TYPE;
|
|
||||||
if (profileType === 'account' || profileType === 'default') {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(`[CCS Hook] Skipping image block for profile type: ${profileType}`);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicit disable via config
|
|
||||||
if (process.env.CCS_BLOCK_IMAGE_READ === '0') {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error('[CCS Hook] Image read blocking disabled by config');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main hook processing logic
|
|
||||||
*/
|
|
||||||
function processHook() {
|
|
||||||
try {
|
|
||||||
// Skip for native accounts or explicit disable
|
|
||||||
if (shouldSkipHook()) {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.parse(input);
|
|
||||||
|
|
||||||
// Only handle Read tool
|
|
||||||
if (data.tool_name !== 'Read') {
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = data.tool_input?.file_path || '';
|
|
||||||
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(`[CCS Hook] Read intercepted: ${filePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if file is an image
|
|
||||||
if (IMAGE_EXTENSIONS.test(filePath)) {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(`[CCS Hook] Blocking image read: ${filePath}`);
|
|
||||||
}
|
|
||||||
outputBlock(filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow non-image files
|
|
||||||
process.exit(0);
|
|
||||||
} catch (err) {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error('[CCS Hook] Parse error:', err.message);
|
|
||||||
}
|
|
||||||
// Don't block on parse errors
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Output block response and exit
|
|
||||||
*/
|
|
||||||
function outputBlock(filePath) {
|
|
||||||
// Extract just the filename for cleaner display
|
|
||||||
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
|
||||||
|
|
||||||
const message = [
|
|
||||||
'[Image Read Blocked - Context Protection]',
|
|
||||||
'',
|
|
||||||
`File: ${fileName}`,
|
|
||||||
`Path: ${filePath}`,
|
|
||||||
'',
|
|
||||||
'Image files consume 100K+ tokens each and will exhaust context.',
|
|
||||||
'',
|
|
||||||
'The image was generated successfully. To view it:',
|
|
||||||
' - Open the file path above in your image viewer',
|
|
||||||
' - Use your file manager to navigate to the location',
|
|
||||||
' - On macOS: open "' + filePath + '"',
|
|
||||||
' - On Linux: xdg-open "' + filePath + '"',
|
|
||||||
' - On Windows: start "" "' + filePath + '"',
|
|
||||||
'',
|
|
||||||
'If you need to analyze the image, use the ai-multimodal skill',
|
|
||||||
'which processes images via Gemini API without loading into context.',
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
const output = {
|
|
||||||
decision: 'block',
|
|
||||||
reason: 'Image file blocked to prevent context overflow',
|
|
||||||
// User-facing message (shows in CLI output)
|
|
||||||
systemMessage: `[Image Read Blocked] ${fileName} - Open file directly to view.`,
|
|
||||||
hookSpecificOutput: {
|
|
||||||
hookEventName: 'PreToolUse',
|
|
||||||
permissionDecision: 'deny',
|
|
||||||
// Claude reads this - explains what happened and alternatives
|
|
||||||
permissionDecisionReason: message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(JSON.stringify(output));
|
|
||||||
process.exit(2);
|
|
||||||
}
|
|
||||||
@@ -196,6 +196,10 @@ function getModelsToTry() {
|
|||||||
*/
|
*/
|
||||||
async function analyzeWithRetry(base64Data, mediaType, timeoutMs) {
|
async function analyzeWithRetry(base64Data, mediaType, timeoutMs) {
|
||||||
const models = getModelsToTry();
|
const models = getModelsToTry();
|
||||||
|
// Defensive check - should never happen but provides clear error
|
||||||
|
if (models.length === 0) {
|
||||||
|
throw new Error('No models configured for image analysis');
|
||||||
|
}
|
||||||
let lastError = null;
|
let lastError = null;
|
||||||
|
|
||||||
for (let i = 0; i < models.length; i++) {
|
for (let i = 0; i < models.length; i++) {
|
||||||
@@ -636,6 +640,38 @@ function outputUnknownError(filePath, error) {
|
|||||||
process.exit(2);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CLIProxy unavailable fallback - blocks Read to prevent context overflow
|
||||||
|
* When CLIProxy is not running, we cannot analyze the image.
|
||||||
|
* Blocking prevents the image from loading into Claude's context (100K+ tokens).
|
||||||
|
*/
|
||||||
|
function outputCliProxyUnavailableFallback(filePath) {
|
||||||
|
const fileName = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
|
|
||||||
|
// Keep message minimal to avoid context pollution and hallucination
|
||||||
|
const message = [
|
||||||
|
'[Image Read Blocked]',
|
||||||
|
'',
|
||||||
|
`File: ${fileName}`,
|
||||||
|
'',
|
||||||
|
'CLIProxy unavailable. Image blocked to prevent context overflow.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
decision: 'block',
|
||||||
|
reason: 'CLIProxy unavailable - image blocked to prevent context overflow',
|
||||||
|
systemMessage: `[Image Blocked] ${fileName} - CLIProxy unavailable. Start: ccs config`,
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: 'PreToolUse',
|
||||||
|
permissionDecision: 'deny',
|
||||||
|
permissionDecisionReason: message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(output));
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Output success response and exit
|
* Output success response and exit
|
||||||
*/
|
*/
|
||||||
@@ -763,12 +799,12 @@ async function processHook() {
|
|||||||
// Check CLIProxy availability
|
// Check CLIProxy availability
|
||||||
const cliProxyAvailable = await isCliProxyAvailable();
|
const cliProxyAvailable = await isCliProxyAvailable();
|
||||||
if (!cliProxyAvailable) {
|
if (!cliProxyAvailable) {
|
||||||
debugLog('Skipping: CLIProxy not available', {
|
debugLog('Blocking: CLIProxy not available', {
|
||||||
endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`,
|
endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`,
|
||||||
action: 'passing through to native Read',
|
action: 'blocking to prevent context overflow',
|
||||||
});
|
});
|
||||||
// Pass through to native Read
|
outputCliProxyUnavailableFallback(filePath);
|
||||||
process.exit(0);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = getModelForProvider();
|
const model = getModelForProvider();
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
ensureProfileHooks,
|
ensureProfileHooks,
|
||||||
} from './utils/websearch-manager';
|
} from './utils/websearch-manager';
|
||||||
import { getGlobalEnvConfig } from './config/unified-config-loader';
|
import { getGlobalEnvConfig } from './config/unified-config-loader';
|
||||||
import { getImageReadBlockHookEnv } from './utils/hooks/image-read-block-hook-env';
|
|
||||||
import { ensureProfileHooks as ensureImageAnalyzerHooks } from './utils/hooks/image-analyzer-profile-hook-injector';
|
import { ensureProfileHooks as ensureImageAnalyzerHooks } from './utils/hooks/image-analyzer-profile-hook-injector';
|
||||||
import { fail, info } from './utils/ui';
|
import { fail, info } from './utils/ui';
|
||||||
|
|
||||||
@@ -166,12 +165,10 @@ async function execClaudeWithProxy(
|
|||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
const imageReadBlockEnv = getImageReadBlockHookEnv();
|
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
...envVars,
|
...envVars,
|
||||||
...webSearchEnv,
|
...webSearchEnv,
|
||||||
...imageReadBlockEnv,
|
|
||||||
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
|
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -622,7 +619,6 @@ async function main(): Promise<void> {
|
|||||||
// Use --settings flag (backward compatible)
|
// Use --settings flag (backward compatible)
|
||||||
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
const expandedSettingsPath = getSettingsPath(profileInfo.name);
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
const imageReadBlockEnv = getImageReadBlockHookEnv();
|
|
||||||
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
// Get global env vars (DISABLE_TELEMETRY, etc.) for third-party profiles
|
||||||
const globalEnvConfig = getGlobalEnvConfig();
|
const globalEnvConfig = getGlobalEnvConfig();
|
||||||
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
|
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
|
||||||
@@ -644,7 +640,6 @@ async function main(): Promise<void> {
|
|||||||
...globalEnv,
|
...globalEnv,
|
||||||
...settingsEnv, // Explicitly inject all settings env vars
|
...settingsEnv, // Explicitly inject all settings env vars
|
||||||
...webSearchEnv,
|
...webSearchEnv,
|
||||||
...imageReadBlockEnv,
|
|
||||||
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
|
CCS_PROFILE_TYPE: 'settings', // Signal to WebSearch hook this is a third-party provider
|
||||||
};
|
};
|
||||||
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars);
|
execClaude(claudeCli, ['--settings', expandedSettingsPath, ...remainingArgs], envVars);
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import { DEFAULT_BACKEND } from './platform-detector';
|
|||||||
import { configureProviderModel, getCurrentModel } from './model-config';
|
import { configureProviderModel, getCurrentModel } from './model-config';
|
||||||
import { resolveProxyConfig, PROXY_CLI_FLAGS } from './proxy-config-resolver';
|
import { resolveProxyConfig, PROXY_CLI_FLAGS } from './proxy-config-resolver';
|
||||||
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
import { getWebSearchHookEnv } from '../utils/websearch-manager';
|
||||||
import { getImageReadBlockHookEnv } from '../utils/hooks/image-read-block-hook-env';
|
|
||||||
import { getImageAnalysisHookEnv } from '../utils/hooks/get-image-analysis-hook-env';
|
import { getImageAnalysisHookEnv } from '../utils/hooks/get-image-analysis-hook-env';
|
||||||
import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from './model-catalog';
|
import { supportsModelConfig, isModelBroken, getModelIssueUrl, findModel } from './model-catalog';
|
||||||
import { CodexReasoningProxy } from './codex-reasoning-proxy';
|
import { CodexReasoningProxy } from './codex-reasoning-proxy';
|
||||||
@@ -969,13 +968,11 @@ export async function execClaudeWithCLIProxy(
|
|||||||
ANTHROPIC_BASE_URL: finalBaseUrl,
|
ANTHROPIC_BASE_URL: finalBaseUrl,
|
||||||
};
|
};
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
const imageReadBlockEnv = getImageReadBlockHookEnv();
|
|
||||||
const imageAnalysisEnv = getImageAnalysisHookEnv(provider);
|
const imageAnalysisEnv = getImageAnalysisHookEnv(provider);
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
...effectiveEnvVars,
|
...effectiveEnvVars,
|
||||||
...webSearchEnv,
|
...webSearchEnv,
|
||||||
...imageReadBlockEnv,
|
|
||||||
...imageAnalysisEnv,
|
...imageAnalysisEnv,
|
||||||
CCS_PROFILE_TYPE: 'cliproxy', // Signal to WebSearch hook this is a third-party provider
|
CCS_PROFILE_TYPE: 'cliproxy', // Signal to WebSearch hook this is a third-party provider
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ export const DEFAULT_IMAGE_ANALYSIS_CONFIG: ImageAnalysisConfig = {
|
|||||||
kiro: 'kiro-claude-haiku-4-5',
|
kiro: 'kiro-claude-haiku-4-5',
|
||||||
ghcp: 'claude-haiku-4.5',
|
ghcp: 'claude-haiku-4.5',
|
||||||
claude: 'claude-haiku-4-5-20251001',
|
claude: 'claude-haiku-4-5-20251001',
|
||||||
|
// 'vision-model' is a generic placeholder - users can override via config.yaml
|
||||||
qwen: 'vision-model',
|
qwen: 'vision-model',
|
||||||
iflow: 'qwen3-vl-plus',
|
iflow: 'qwen3-vl-plus',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Injects image analyzer hooks into per-profile settings files.
|
* Injects image analyzer hooks into per-profile settings files.
|
||||||
* This replaces the global ~/.claude/settings.json approach.
|
* This replaces the global ~/.claude/settings.json approach.
|
||||||
*
|
*
|
||||||
* Only injects for CLIProxy profiles (agy, gemini) that support vision analysis.
|
* Injects for profiles configured in image_analysis.provider_models.
|
||||||
*
|
*
|
||||||
* @module utils/hooks/image-analyzer-profile-injector
|
* @module utils/hooks/image-analyzer-profile-injector
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
/**
|
|
||||||
* Image Read Block Hook Environment Variables
|
|
||||||
*
|
|
||||||
* Provides environment variables for image read blocking hook configuration.
|
|
||||||
* Prevents context overflow when skills generate images and agent tries to read them.
|
|
||||||
*
|
|
||||||
* Enabled by default for third-party profiles (settings, cliproxy).
|
|
||||||
* Disabled for native Claude accounts where context is managed server-side.
|
|
||||||
*
|
|
||||||
* @module utils/hooks/image-read-block-hook-env
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { loadOrCreateUnifiedConfig } from '../../config/unified-config-loader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for image read blocking.
|
|
||||||
*/
|
|
||||||
export interface ImageReadBlockConfig {
|
|
||||||
/** Whether blocking is enabled (default: true) */
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get image read block configuration from unified config.
|
|
||||||
* Defaults to ENABLED (opt-out feature) - matches WebSearch pattern.
|
|
||||||
*/
|
|
||||||
export function getImageReadBlockConfig(): ImageReadBlockConfig {
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
|
||||||
// Access hooks config via type assertion since it's a new field not yet in UnifiedConfig type
|
|
||||||
const hooksConfig = (
|
|
||||||
config as unknown as { hooks?: { block_image_read?: { enabled?: boolean } } }
|
|
||||||
).hooks;
|
|
||||||
return {
|
|
||||||
// Default to TRUE - enabled by default, user can opt-out
|
|
||||||
enabled: hooksConfig?.block_image_read?.enabled ?? true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get environment variables for image read block hook configuration.
|
|
||||||
*
|
|
||||||
* Like WebSearch, this respects CCS_PROFILE_TYPE:
|
|
||||||
* - 'account' or 'default' profiles: Skip blocking (native Claude)
|
|
||||||
* - 'settings' or 'cliproxy' profiles: Apply blocking
|
|
||||||
*
|
|
||||||
* @returns Record of environment variables to set before spawning Claude
|
|
||||||
*/
|
|
||||||
export function getImageReadBlockHookEnv(): Record<string, string> {
|
|
||||||
const config = getImageReadBlockConfig();
|
|
||||||
const env: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (config.enabled) {
|
|
||||||
env.CCS_BLOCK_IMAGE_READ = '1';
|
|
||||||
} else {
|
|
||||||
// Explicit disable signal
|
|
||||||
env.CCS_BLOCK_IMAGE_READ = '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
* @module utils/hooks
|
* @module utils/hooks
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { getImageReadBlockHookEnv, getImageReadBlockConfig } from './image-read-block-hook-env';
|
|
||||||
export { getImageAnalysisHookEnv } from './get-image-analysis-hook-env';
|
export { getImageAnalysisHookEnv } from './get-image-analysis-hook-env';
|
||||||
export {
|
export {
|
||||||
getImageAnalyzerHookPath,
|
getImageAnalyzerHookPath,
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Image Analysis Hook Installer
|
* Image Analysis Hook Installer
|
||||||
*
|
*
|
||||||
* Manages installation of:
|
* Manages installation of prompt templates for image analysis (user-customizable).
|
||||||
* 1. block-image-read.cjs hook (blocks image reads to prevent context overflow)
|
|
||||||
* 2. Prompt templates for image analysis (user-customizable)
|
|
||||||
*
|
*
|
||||||
* @module utils/image-analysis/hook-installer
|
* @module utils/image-analysis/hook-installer
|
||||||
*/
|
*/
|
||||||
@@ -13,16 +11,6 @@ import * as path from 'path';
|
|||||||
import { info, warn } from '../ui';
|
import { info, warn } from '../ui';
|
||||||
import { getCcsDir } from '../config-manager';
|
import { getCcsDir } from '../config-manager';
|
||||||
|
|
||||||
// Hook file name
|
|
||||||
const IMAGE_BLOCK_HOOK = 'block-image-read.cjs';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get path to installed hook script
|
|
||||||
*/
|
|
||||||
export function getHookPath(): string {
|
|
||||||
return path.join(getCcsDir(), 'hooks', IMAGE_BLOCK_HOOK);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get CCS hooks directory
|
* Get CCS hooks directory
|
||||||
*/
|
*/
|
||||||
@@ -37,72 +25,6 @@ export function getPromptsDir(): string {
|
|||||||
return path.join(getCcsDir(), 'prompts', 'image-analysis');
|
return path.join(getCcsDir(), 'prompts', 'image-analysis');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if image block hook is installed
|
|
||||||
*/
|
|
||||||
export function hasImageBlockHook(): boolean {
|
|
||||||
return fs.existsSync(getHookPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install image block hook to ~/.ccs/hooks/
|
|
||||||
*
|
|
||||||
* This hook intercepts Read tool calls for image files and blocks them
|
|
||||||
* to prevent context overflow (images consume 100K+ tokens each).
|
|
||||||
*
|
|
||||||
* @returns true if hook installed successfully
|
|
||||||
*/
|
|
||||||
export function installImageBlockHook(): boolean {
|
|
||||||
try {
|
|
||||||
// Ensure hooks directory exists
|
|
||||||
const hooksDir = getCcsHooksDir();
|
|
||||||
if (!fs.existsSync(hooksDir)) {
|
|
||||||
fs.mkdirSync(hooksDir, { recursive: true, mode: 0o700 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const hookPath = getHookPath();
|
|
||||||
|
|
||||||
// Find the bundled hook script
|
|
||||||
// In npm package: node_modules/ccs/lib/hooks/
|
|
||||||
// In development: lib/hooks/
|
|
||||||
const possiblePaths = [
|
|
||||||
path.join(__dirname, '..', '..', '..', 'lib', 'hooks', IMAGE_BLOCK_HOOK),
|
|
||||||
path.join(__dirname, '..', '..', 'lib', 'hooks', IMAGE_BLOCK_HOOK),
|
|
||||||
path.join(__dirname, '..', 'lib', 'hooks', IMAGE_BLOCK_HOOK),
|
|
||||||
];
|
|
||||||
|
|
||||||
let sourcePath: string | null = null;
|
|
||||||
for (const p of possiblePaths) {
|
|
||||||
if (fs.existsSync(p)) {
|
|
||||||
sourcePath = p;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sourcePath) {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(warn(`Image block hook source not found: ${IMAGE_BLOCK_HOOK}`));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy hook to ~/.ccs/hooks/
|
|
||||||
fs.copyFileSync(sourcePath, hookPath);
|
|
||||||
fs.chmodSync(hookPath, 0o755);
|
|
||||||
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(info(`Installed image block hook: ${hookPath}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(warn(`Failed to install image block hook: ${(error as Error).message}`));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install prompt templates to ~/.ccs/prompts/image-analysis/
|
* Install prompt templates to ~/.ccs/prompts/image-analysis/
|
||||||
* Only installs if directory doesn't exist (doesn't overwrite user edits)
|
* Only installs if directory doesn't exist (doesn't overwrite user edits)
|
||||||
@@ -179,28 +101,3 @@ export function installImageAnalysisPrompts(): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Uninstall image block hook from ~/.ccs/hooks/
|
|
||||||
*
|
|
||||||
* @returns true if hook uninstalled successfully
|
|
||||||
*/
|
|
||||||
export function uninstallImageBlockHook(): boolean {
|
|
||||||
try {
|
|
||||||
const hookPath = getHookPath();
|
|
||||||
|
|
||||||
if (fs.existsSync(hookPath)) {
|
|
||||||
fs.unlinkSync(hookPath);
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(info(`Uninstalled image block hook: ${hookPath}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
if (process.env.CCS_DEBUG) {
|
|
||||||
console.error(warn(`Failed to uninstall image block hook: ${(error as Error).message}`));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Image Analysis Utilities
|
* Image Analysis Utilities
|
||||||
*
|
*
|
||||||
* Exports hook installer functions for image blocking and prompt management
|
* Exports hook installer functions for prompt management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export { getCcsHooksDir, getPromptsDir, installImageAnalysisPrompts } from './hook-installer';
|
||||||
getHookPath,
|
|
||||||
getCcsHooksDir,
|
|
||||||
getPromptsDir,
|
|
||||||
hasImageBlockHook,
|
|
||||||
installImageBlockHook,
|
|
||||||
installImageAnalysisPrompts,
|
|
||||||
uninstallImageBlockHook,
|
|
||||||
} from './hook-installer';
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
import { ErrorManager } from './error-manager';
|
import { ErrorManager } from './error-manager';
|
||||||
import { getWebSearchHookEnv } from './websearch-manager';
|
import { getWebSearchHookEnv } from './websearch-manager';
|
||||||
import { getImageReadBlockHookEnv } from './hooks/image-read-block-hook-env';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape arguments for shell execution (Windows compatibility)
|
* Escape arguments for shell execution (Windows compatibility)
|
||||||
@@ -53,12 +52,11 @@ export function execClaude(
|
|||||||
|
|
||||||
// Get WebSearch hook config env vars
|
// Get WebSearch hook config env vars
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
const imageReadBlockEnv = getImageReadBlockHookEnv();
|
|
||||||
|
|
||||||
// Prepare environment (merge with process.env if envVars provided)
|
// Prepare environment (merge with process.env if envVars provided)
|
||||||
const env = envVars
|
const env = envVars
|
||||||
? { ...process.env, ...envVars, ...webSearchEnv, ...imageReadBlockEnv }
|
? { ...process.env, ...envVars, ...webSearchEnv }
|
||||||
: { ...process.env, ...webSearchEnv, ...imageReadBlockEnv };
|
: { ...process.env, ...webSearchEnv };
|
||||||
|
|
||||||
let child: ChildProcess;
|
let child: ChildProcess;
|
||||||
if (needsShell) {
|
if (needsShell) {
|
||||||
|
|||||||
Reference in new issue
Block a user