Merge pull request #1518 from thedotmack/thedotmack/patch-plan-issues
fix: patch 7 critical bugs for v10.6.3
This commit is contained in:
@@ -74,8 +74,8 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "_R=\"${CLAUDE_PLUGIN_ROOT}\"; [ -z \"$_R\" ] && _R=\"$HOME/.claude/plugins/marketplaces/thedotmack/plugin\"; node \"$_R/scripts/bun-runner.js\" \"$_R/scripts/worker-service.cjs\" hook claude-code session-complete",
|
||||
"timeout": 30
|
||||
"command": "node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const{sessionId:s}=JSON.parse(d);if(!s){process.exit(0)}const r=require('http').request({hostname:'127.0.0.1',port:37777,path:'/api/sessions/complete',method:'POST',headers:{'Content-Type':'application/json'}},()=>process.exit(0));r.on('error',()=>process.exit(0));r.end(JSON.stringify({contentSessionId:s}));setTimeout(()=>process.exit(0),3000)}catch{process.exit(0)}})\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -116,7 +116,11 @@ async function buildHooks() {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
},
|
||||
banner: {
|
||||
js: '#!/usr/bin/env bun'
|
||||
js: [
|
||||
'#!/usr/bin/env bun',
|
||||
'var __filename = require("node:url").fileURLToPath(import.meta.url);',
|
||||
'var __dirname = require("node:path").dirname(__filename);'
|
||||
].join('\n')
|
||||
}
|
||||
});
|
||||
|
||||
@@ -176,7 +180,8 @@ async function buildHooks() {
|
||||
external: ['bun:sqlite'],
|
||||
define: {
|
||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||
}
|
||||
},
|
||||
// No banner needed: CJS files under Node.js have __dirname/__filename natively
|
||||
});
|
||||
|
||||
const contextGenStats = fs.statSync(`${hooksDir}/${CONTEXT_GENERATOR.name}.cjs`);
|
||||
|
||||
@@ -35,7 +35,13 @@ export const summarizeHandler: EventHandler = {
|
||||
// Extract last assistant message from transcript (the work Claude did)
|
||||
// Note: "user" messages in transcripts are mostly tool_results, not actual user input.
|
||||
// The user's original request is already stored in user_prompts table.
|
||||
const lastAssistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
|
||||
let lastAssistantMessage = '';
|
||||
try {
|
||||
lastAssistantMessage = extractLastMessage(transcriptPath, 'assistant', true);
|
||||
} catch (err) {
|
||||
logger.warn('HOOK', `Stop hook: failed to extract last assistant message for session ${sessionId}: ${err instanceof Error ? err.message : err}`);
|
||||
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
|
||||
}
|
||||
|
||||
logger.dataIn('HOOK', 'Stop: Requesting summary', {
|
||||
hasLastAssistantMessage: !!lastAssistantMessage
|
||||
|
||||
@@ -646,18 +646,19 @@ export function spawnDaemon(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const escapedRuntimePath = runtimePath.replace(/'/g, "''");
|
||||
const escapedScriptPath = scriptPath.replace(/'/g, "''");
|
||||
const psCommand = `Start-Process -FilePath '${escapedRuntimePath}' -ArgumentList '${escapedScriptPath}','--daemon' -WindowStyle Hidden`;
|
||||
// Use -EncodedCommand to avoid all shell quoting issues with spaces in paths
|
||||
const psScript = `Start-Process -FilePath '${runtimePath.replace(/'/g, "''")}' -ArgumentList @('${scriptPath.replace(/'/g, "''")}','--daemon') -WindowStyle Hidden`;
|
||||
const encodedCommand = Buffer.from(psScript, 'utf16le').toString('base64');
|
||||
|
||||
try {
|
||||
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
|
||||
execSync(`powershell -NoProfile -EncodedCommand ${encodedCommand}`, {
|
||||
stdio: 'ignore',
|
||||
windowsHide: true,
|
||||
env
|
||||
});
|
||||
return 0;
|
||||
} catch (error) {
|
||||
// APPROVED OVERRIDE: Windows daemon spawn is best-effort; log and let callers fall back to health checks/retry flow.
|
||||
logger.error('SYSTEM', 'Failed to spawn worker daemon on Windows', { runtimePath }, error as Error);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -578,6 +578,13 @@ export class WorkerService {
|
||||
'ENOENT',
|
||||
'spawn',
|
||||
'Invalid API key',
|
||||
'API_KEY_INVALID',
|
||||
'API key expired',
|
||||
'API key not valid',
|
||||
'PERMISSION_DENIED',
|
||||
'Gemini API error: 400',
|
||||
'Gemini API error: 401',
|
||||
'Gemini API error: 403',
|
||||
'FOREIGN KEY constraint failed',
|
||||
];
|
||||
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
||||
@@ -1262,7 +1269,10 @@ async function main() {
|
||||
// Check if running as main module in both ESM and CommonJS
|
||||
const isMainModule = typeof require !== 'undefined' && typeof module !== 'undefined'
|
||||
? require.main === module || !module.parent
|
||||
: import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('worker-service');
|
||||
: import.meta.url === `file://${process.argv[1]}`
|
||||
|| process.argv[1]?.endsWith('worker-service')
|
||||
|| process.argv[1]?.endsWith('worker-service.cjs')
|
||||
|| process.argv[1]?.replaceAll('\\', '/') === __filename?.replaceAll('\\', '/');
|
||||
|
||||
if (isMainModule) {
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -6,11 +6,18 @@ export const ENV_EXACT_MATCHES = new Set([
|
||||
'MCP_SESSION_ID',
|
||||
]);
|
||||
|
||||
/** Vars that start with CLAUDE_CODE_ but must be preserved for subprocess auth/tooling */
|
||||
export const ENV_PRESERVE = new Set([
|
||||
'CLAUDE_CODE_OAUTH_TOKEN',
|
||||
'CLAUDE_CODE_GIT_BASH_PATH',
|
||||
]);
|
||||
|
||||
export function sanitizeEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
||||
const sanitized: NodeJS.ProcessEnv = {};
|
||||
|
||||
for (const [key, value] of Object.entries(env)) {
|
||||
if (value === undefined) continue;
|
||||
if (ENV_PRESERVE.has(key)) { sanitized[key] = value; continue; }
|
||||
if (ENV_EXACT_MATCHES.has(key)) continue;
|
||||
if (ENV_PREFIXES.some(prefix => key.startsWith(prefix))) continue;
|
||||
sanitized[key] = value;
|
||||
|
||||
@@ -67,11 +67,14 @@ describe('Plugin Distribution - hooks.json Integrity', () => {
|
||||
expect(parsed.hooks).toBeDefined();
|
||||
});
|
||||
|
||||
it('should reference CLAUDE_PLUGIN_ROOT in all hook commands', () => {
|
||||
it('should reference CLAUDE_PLUGIN_ROOT in all hook commands (except inline hooks)', () => {
|
||||
const hooksPath = path.join(projectRoot, 'plugin/hooks/hooks.json');
|
||||
const parsed = JSON.parse(readFileSync(hooksPath, 'utf-8'));
|
||||
// SessionEnd uses a lightweight inline node -e command (no plugin root needed)
|
||||
const inlineHookEvents = new Set(['SessionEnd']);
|
||||
|
||||
for (const [eventName, matchers] of Object.entries(parsed.hooks)) {
|
||||
if (inlineHookEvents.has(eventName)) continue;
|
||||
for (const matcher of matchers as any[]) {
|
||||
for (const hook of matcher.hooks) {
|
||||
if (hook.type === 'command') {
|
||||
@@ -82,12 +85,14 @@ describe('Plugin Distribution - hooks.json Integrity', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should include CLAUDE_PLUGIN_ROOT fallback in all hook commands (#1215)', () => {
|
||||
it('should include CLAUDE_PLUGIN_ROOT fallback in all hook commands except inline hooks (#1215)', () => {
|
||||
const hooksPath = path.join(projectRoot, 'plugin/hooks/hooks.json');
|
||||
const parsed = JSON.parse(readFileSync(hooksPath, 'utf-8'));
|
||||
const expectedFallbackPath = '$HOME/.claude/plugins/marketplaces/thedotmack/plugin';
|
||||
const inlineHookEvents = new Set(['SessionEnd']);
|
||||
|
||||
for (const [eventName, matchers] of Object.entries(parsed.hooks)) {
|
||||
if (inlineHookEvents.has(eventName)) continue;
|
||||
for (const matcher of matchers as any[]) {
|
||||
for (const hook of matcher.hooks) {
|
||||
if (hook.type === 'command') {
|
||||
@@ -97,6 +102,18 @@ describe('Plugin Distribution - hooks.json Integrity', () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should use lightweight inline node command for SessionEnd hook', () => {
|
||||
const hooksPath = path.join(projectRoot, 'plugin/hooks/hooks.json');
|
||||
const parsed = JSON.parse(readFileSync(hooksPath, 'utf-8'));
|
||||
const sessionEndHooks = parsed.hooks.SessionEnd;
|
||||
expect(sessionEndHooks).toBeDefined();
|
||||
expect(sessionEndHooks.length).toBe(1);
|
||||
const command = sessionEndHooks[0].hooks[0].command;
|
||||
expect(command).toContain('node -e');
|
||||
expect(command).toContain('/api/sessions/complete');
|
||||
expect(sessionEndHooks[0].hooks[0].timeout).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plugin Distribution - package.json Files Field', () => {
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('sanitizeEnv', () => {
|
||||
expect(result.PATH).toBe('/usr/bin');
|
||||
});
|
||||
|
||||
it('strips variables with CLAUDE_CODE_ prefix', () => {
|
||||
it('strips variables with CLAUDE_CODE_ prefix but preserves allowed ones', () => {
|
||||
const result = sanitizeEnv({
|
||||
CLAUDE_CODE_BAR: 'baz',
|
||||
CLAUDE_CODE_OAUTH_TOKEN: 'token',
|
||||
@@ -22,7 +22,7 @@ describe('sanitizeEnv', () => {
|
||||
});
|
||||
|
||||
expect(result.CLAUDE_CODE_BAR).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_OAUTH_TOKEN).toBe('token');
|
||||
expect(result.HOME).toBe('/home/user');
|
||||
});
|
||||
|
||||
@@ -115,9 +115,42 @@ describe('sanitizeEnv', () => {
|
||||
expect(result.CLAUDECODE).toBeUndefined();
|
||||
expect(result.CLAUDECODE_FOO).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_BAR).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_OAUTH_TOKEN).toBe('oauth-token');
|
||||
expect(result.CLAUDE_CODE_SESSION).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_ENTRYPOINT).toBeUndefined();
|
||||
expect(result.MCP_SESSION_ID).toBeUndefined();
|
||||
});
|
||||
|
||||
it('preserves CLAUDE_CODE_GIT_BASH_PATH through sanitization', () => {
|
||||
const result = sanitizeEnv({
|
||||
CLAUDE_CODE_GIT_BASH_PATH: 'C:\\Program Files\\Git\\bin\\bash.exe',
|
||||
PATH: '/usr/bin',
|
||||
HOME: '/home/user'
|
||||
});
|
||||
|
||||
expect(result.CLAUDE_CODE_GIT_BASH_PATH).toBe('C:\\Program Files\\Git\\bin\\bash.exe');
|
||||
expect(result.PATH).toBe('/usr/bin');
|
||||
expect(result.HOME).toBe('/home/user');
|
||||
});
|
||||
|
||||
it('selectively preserves only allowed CLAUDE_CODE_* vars while stripping others', () => {
|
||||
const result = sanitizeEnv({
|
||||
CLAUDE_CODE_OAUTH_TOKEN: 'my-oauth-token',
|
||||
CLAUDE_CODE_GIT_BASH_PATH: '/usr/bin/bash',
|
||||
CLAUDE_CODE_RANDOM_OTHER: 'should-be-stripped',
|
||||
CLAUDE_CODE_INTERNAL_FLAG: 'should-be-stripped',
|
||||
PATH: '/usr/bin'
|
||||
});
|
||||
|
||||
// Preserved: explicitly allowed CLAUDE_CODE_* vars
|
||||
expect(result.CLAUDE_CODE_OAUTH_TOKEN).toBe('my-oauth-token');
|
||||
expect(result.CLAUDE_CODE_GIT_BASH_PATH).toBe('/usr/bin/bash');
|
||||
|
||||
// Stripped: all other CLAUDE_CODE_* vars
|
||||
expect(result.CLAUDE_CODE_RANDOM_OTHER).toBeUndefined();
|
||||
expect(result.CLAUDE_CODE_INTERNAL_FLAG).toBeUndefined();
|
||||
|
||||
// Preserved: normal env vars
|
||||
expect(result.PATH).toBe('/usr/bin');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user