fix: jq fail-closed guard, HTTP 24h cache, forced-eval dedup

This commit is contained in:
kochetkov-ma
2026-04-05 18:56:15 +01:00
parent bf2688d305
commit d5d30a9146
3 changed files with 56 additions and 43 deletions
+7 -42
View File
@@ -21,54 +21,19 @@
* }
*/
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
// --- stdin/stdout helpers ---
async function readStdin() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
}
function output(response) {
console.log(JSON.stringify(response));
}
import { readStdin, output, getActiveMode } from './lib/utils.mjs';
// --- Skill evaluation reminder ---
const SKILL_CHECK = '[SKILL?] Check available skills. If one matches, use Skill tool before responding.';
const DEFAULT_MODE = '[DELEGATE] You are a MANAGER. Delegate implementation to sub-agents via Task tool. Never implement directly.';
// NOTE: Mirrors getActiveMode() from lib/utils.mjs — kept inline to avoid import dependency.
// utils.mjs version supports 3-scope resolution (CLAUDE_PLUGIN_DATA/modes.json: session > project > global).
// This inline version only uses legacy state file fallback. Keep in sync if STATE_FILE path changes.
function getModeReminder(cwd) {
if (!cwd) return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
try {
const statePath = join(cwd, '.claude', 'tasks', 'cfg', 'brewcode.state.json');
if (!existsSync(statePath)) return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
const state = JSON.parse(readFileSync(statePath, 'utf8'));
if (!state.mode) return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || '';
if (!pluginRoot) return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
const modePath = join(pluginRoot, 'modes', `${state.mode}.md`);
if (!existsSync(modePath)) return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
const instructions = readFileSync(modePath, 'utf8').trim();
if (!instructions) return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
return `${SKILL_CHECK}\n[MODE: ${state.mode}] ${instructions}`;
} catch {
return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
function getModeReminder(cwd, sessionId) {
const activeMode = getActiveMode(cwd, sessionId);
if (activeMode) {
return `${SKILL_CHECK}\n[MODE: ${activeMode.name}] ${activeMode.instructions}`;
}
return `${SKILL_CHECK}\n${DEFAULT_MODE}`;
}
// --- Main ---
@@ -111,7 +76,7 @@ async function main() {
}
// Prepend skill-check reminder to prompt
const modifiedPrompt = `${getModeReminder(cwd)}\n\n---\n\n${prompt}`;
const modifiedPrompt = `${getModeReminder(cwd, session_id)}\n\n---\n\n${prompt}`;
output({
updatedInput: {
+2
View File
@@ -1,6 +1,8 @@
#!/bin/bash
set -euo pipefail
command -v jq >/dev/null 2>&1 || { echo '{"decision":"block","reason":"jq is required for permission checks but not installed"}'; exit 0; }
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
+47 -1
View File
@@ -20,12 +20,14 @@
*
* Cleanup: /brewcode:teardown removes .claude/plans/ directory
*/
import { readStdin, output, log, getActiveTaskPath, getLock, getActiveMode } from './lib/utils.mjs';
import { readStdin, output, log, getActiveTaskPath, getLock, getActiveMode, getState, saveState } from './lib/utils.mjs';
import { readFileSync, readdirSync, statSync, mkdirSync, symlinkSync, unlinkSync, existsSync } from 'fs';
import { execFileSync } from 'child_process';
import { join, dirname } from 'path';
import { homedir } from 'os';
const VERSION_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
const PLAN_FRESHNESS_MS = 60_000;
/**
@@ -107,10 +109,26 @@ async function checkLatestVersion(pluginRoot, cwd, sessionId) {
}
log('debug', '[version]', `Local brewcode: ${local}`, cwd, sessionId);
// Check 24h TTL cache
const state = getState(cwd);
const cache = state._versionCache?.brewcode;
if (cache?.remote && cache?.checkedAt) {
const age = Date.now() - new Date(cache.checkedAt).getTime();
if (age < VERSION_CACHE_TTL_MS) {
log('debug', '[version]', `Using cached brewcode remote=${cache.remote} (age=${Math.round(age / 60000)}m)`, cwd, sessionId);
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
}
}
const url = 'https://api.github.com/repos/kochetkov-ma/claude-brewcode/releases/latest';
const data = await fetchJson(url, 1000);
if (!data) {
log('debug', '[version]', `GitHub API fetch failed (timeout or error)`, cwd, sessionId);
// Fallback to stale cache if available
if (cache?.remote) {
log('debug', '[version]', `Falling back to stale cache: remote=${cache.remote}`, cwd, sessionId);
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
}
return { updateAvailable: false, local, remote: null, remoteFailed: true };
}
@@ -120,6 +138,12 @@ async function checkLatestVersion(pluginRoot, cwd, sessionId) {
return { updateAvailable: false, local, remote: null, remoteFailed: true };
}
// Update cache
const updatedState = getState(cwd);
updatedState._versionCache = updatedState._versionCache || {};
updatedState._versionCache.brewcode = { remote, checkedAt: new Date().toISOString() };
saveState(cwd, updatedState);
const result = { updateAvailable: isNewer(remote, local), local, remote };
log('debug', '[version]', `brewcode: local=${local}, remote=${remote}, update=${result.updateAvailable}`, cwd, sessionId);
return result;
@@ -146,13 +170,35 @@ async function checkClaudeCodeVersion(cwd, sessionId) {
}
log('debug', '[version]', `Claude Code local: ${local}`, cwd, sessionId);
// Check 24h TTL cache
const state = getState(cwd);
const cache = state._versionCache?.claudeCode;
if (cache?.remote && cache?.checkedAt) {
const age = Date.now() - new Date(cache.checkedAt).getTime();
if (age < VERSION_CACHE_TTL_MS) {
log('debug', '[version]', `Using cached claude-code remote=${cache.remote} (age=${Math.round(age / 60000)}m)`, cwd, sessionId);
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
}
}
const url = 'https://registry.npmjs.org/@anthropic-ai/claude-code/latest';
const data = await fetchJson(url, 1000);
if (!data?.version) {
log('debug', '[version]', `npm fetch failed or no version in response`, cwd, sessionId);
// Fallback to stale cache if available
if (cache?.remote) {
log('debug', '[version]', `Falling back to stale cache: remote=${cache.remote}`, cwd, sessionId);
return { updateAvailable: isNewer(cache.remote, local), local, remote: cache.remote };
}
return null;
}
// Update cache
const updatedState = getState(cwd);
updatedState._versionCache = updatedState._versionCache || {};
updatedState._versionCache.claudeCode = { remote: data.version, checkedAt: new Date().toISOString() };
saveState(cwd, updatedState);
const result = { updateAvailable: isNewer(data.version, local), local, remote: data.version };
log('debug', '[version]', `Claude Code: local=${local}, remote=${data.version}, update=${result.updateAvailable}`, cwd, sessionId);
return result;