mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-14 02:10:07 +08:00
fix: port hook session and dashboard safety fixes
Ports suggest-compact session_id isolation and dashboard terminal/document launch safety onto current main.
This commit is contained in:
parent
27508842b1
commit
1abc3fb381
@ -8,10 +8,11 @@ import tkinter as tk
|
|||||||
from tkinter import ttk, scrolledtext, messagebox
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import subprocess
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
from scripts.lib.ecc_dashboard_runtime import build_terminal_launch, maximize_window
|
from scripts.lib.ecc_dashboard_runtime import launch_terminal, maximize_window
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DATA LOADERS - Load ECC data from the project
|
# DATA LOADERS - Load ECC data from the project
|
||||||
@ -793,27 +794,31 @@ Project: github.com/affaan-m/everything-claude-code"""
|
|||||||
|
|
||||||
def open_terminal(self):
|
def open_terminal(self):
|
||||||
"""Open terminal at project path"""
|
"""Open terminal at project path"""
|
||||||
path = self.path_entry.get()
|
path = os.path.realpath(self.path_entry.get())
|
||||||
argv, kwargs = build_terminal_launch(path)
|
try:
|
||||||
subprocess.Popen(argv, **kwargs)
|
launch_terminal(path)
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("Error", f"Could not open terminal: {exc}")
|
||||||
|
|
||||||
|
def _open_project_doc(self, filename: str) -> None:
|
||||||
|
"""Open a project document safely, constrained to the project directory."""
|
||||||
|
base = os.path.realpath(self.path_entry.get())
|
||||||
|
target = os.path.realpath(os.path.join(base, filename))
|
||||||
|
if os.path.commonpath([base, target]) != base:
|
||||||
|
messagebox.showerror("Error", "Access denied: path is outside the project directory")
|
||||||
|
return
|
||||||
|
if os.path.exists(target):
|
||||||
|
webbrowser.open(Path(target).as_uri())
|
||||||
|
else:
|
||||||
|
messagebox.showerror("Error", f"{filename} not found")
|
||||||
|
|
||||||
def open_readme(self):
|
def open_readme(self):
|
||||||
"""Open README in default browser/reader"""
|
"""Open README in default browser/reader"""
|
||||||
import subprocess
|
self._open_project_doc('README.md')
|
||||||
path = os.path.join(self.path_entry.get(), 'README.md')
|
|
||||||
if os.path.exists(path):
|
|
||||||
subprocess.Popen(['xdg-open' if os.name != 'nt' else 'start', path])
|
|
||||||
else:
|
|
||||||
messagebox.showerror("Error", "README.md not found")
|
|
||||||
|
|
||||||
def open_agents(self):
|
def open_agents(self):
|
||||||
"""Open AGENTS.md"""
|
"""Open AGENTS.md"""
|
||||||
import subprocess
|
self._open_project_doc('AGENTS.md')
|
||||||
path = os.path.join(self.path_entry.get(), 'AGENTS.md')
|
|
||||||
if os.path.exists(path):
|
|
||||||
subprocess.Popen(['xdg-open' if os.name != 'nt' else 'start', path])
|
|
||||||
else:
|
|
||||||
messagebox.showerror("Error", "AGENTS.md not found")
|
|
||||||
|
|
||||||
def refresh_data(self):
|
def refresh_data(self):
|
||||||
"""Refresh all data"""
|
"""Refresh all data"""
|
||||||
|
|||||||
@ -18,14 +18,30 @@ const path = require('path');
|
|||||||
const {
|
const {
|
||||||
getTempDir,
|
getTempDir,
|
||||||
writeFile,
|
writeFile,
|
||||||
|
readStdinJson,
|
||||||
log
|
log
|
||||||
} = require('../lib/utils');
|
} = require('../lib/utils');
|
||||||
|
|
||||||
|
async function resolveSessionId() {
|
||||||
|
// Claude Code passes hook input via stdin JSON; session_id is the
|
||||||
|
// canonical field. Fall back to the legacy env var, then 'default'.
|
||||||
|
try {
|
||||||
|
const input = await readStdinJson({ timeoutMs: 1000 });
|
||||||
|
if (input && typeof input.session_id === 'string' && input.session_id) {
|
||||||
|
return input.session_id;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* fall through to env */
|
||||||
|
}
|
||||||
|
return process.env.CLAUDE_SESSION_ID || 'default';
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Track tool call count (increment in a temp file)
|
// Track tool call count (increment in a temp file)
|
||||||
// Use a session-specific counter file based on session ID from environment
|
// Use a session-specific counter file based on session ID from stdin JSON,
|
||||||
// or parent PID as fallback
|
// legacy env var, or 'default' as fallback.
|
||||||
const sessionId = (process.env.CLAUDE_SESSION_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '') || 'default';
|
const rawSessionId = await resolveSessionId();
|
||||||
|
const sessionId = rawSessionId.replace(/[^a-zA-Z0-9_-]/g, '') || 'default';
|
||||||
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
const counterFile = path.join(getTempDir(), `claude-tool-count-${sessionId}`);
|
||||||
const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
||||||
const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
|
const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
|
||||||
|
|||||||
@ -45,7 +45,7 @@ def build_terminal_launch(
|
|||||||
if resolved_os_name == 'nt':
|
if resolved_os_name == 'nt':
|
||||||
creationflags = getattr(subprocess, 'CREATE_NEW_CONSOLE', 0)
|
creationflags = getattr(subprocess, 'CREATE_NEW_CONSOLE', 0)
|
||||||
return (
|
return (
|
||||||
['cmd.exe', '/k', 'cd', '/d', path],
|
['cmd.exe'],
|
||||||
{
|
{
|
||||||
'cwd': path,
|
'cwd': path,
|
||||||
'creationflags': creationflags,
|
'creationflags': creationflags,
|
||||||
@ -59,3 +59,12 @@ def build_terminal_launch(
|
|||||||
['x-terminal-emulator', '-e', 'bash', '-lc', 'cd -- "$1"; exec bash', 'bash', path],
|
['x-terminal-emulator', '-e', 'bash', '-lc', 'cd -- "$1"; exec bash', 'bash', path],
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def launch_terminal(path: str) -> None:
|
||||||
|
"""Open a terminal at the given path after validating the target directory."""
|
||||||
|
canonical = os.path.realpath(path)
|
||||||
|
if not os.path.isdir(canonical):
|
||||||
|
raise ValueError(f"Path is not a valid directory: {canonical!r}")
|
||||||
|
argv, kwargs = build_terminal_launch(canonical)
|
||||||
|
subprocess.Popen(argv, **kwargs) # noqa: S603 - list argv, no shell=True, path validated above
|
||||||
|
|||||||
@ -1178,6 +1178,47 @@ async function runTests() {
|
|||||||
passed++;
|
passed++;
|
||||||
else failed++;
|
else failed++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
await asyncTest('reads session_id from stdin JSON (Claude Code wire format)', async () => {
|
||||||
|
const sessionId = 'test-stdin-' + Date.now();
|
||||||
|
const stdinJson = JSON.stringify({ session_id: sessionId, tool_name: 'Edit' });
|
||||||
|
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), stdinJson, {});
|
||||||
|
assert.strictEqual(result.code, 0, `Exit code should be 0, got ${result.code}`);
|
||||||
|
|
||||||
|
const counterFile = path.join(os.tmpdir(), `claude-tool-count-${sessionId}`);
|
||||||
|
assert.ok(fs.existsSync(counterFile), `Counter file should be created from stdin session_id at ${counterFile}`);
|
||||||
|
const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10);
|
||||||
|
assert.strictEqual(count, 1, `Counter should be 1, got ${count}`);
|
||||||
|
|
||||||
|
fs.unlinkSync(counterFile);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
|
if (
|
||||||
|
await asyncTest('stdin session_id takes precedence over env CLAUDE_SESSION_ID', async () => {
|
||||||
|
const stdinSession = 'stdin-wins-' + Date.now();
|
||||||
|
const envSession = 'env-loses-' + Date.now();
|
||||||
|
const stdinJson = JSON.stringify({ session_id: stdinSession });
|
||||||
|
|
||||||
|
const result = await runScript(path.join(scriptsDir, 'suggest-compact.js'), stdinJson, {
|
||||||
|
CLAUDE_SESSION_ID: envSession
|
||||||
|
});
|
||||||
|
assert.strictEqual(result.code, 0);
|
||||||
|
|
||||||
|
const stdinCounter = path.join(os.tmpdir(), `claude-tool-count-${stdinSession}`);
|
||||||
|
const envCounter = path.join(os.tmpdir(), `claude-tool-count-${envSession}`);
|
||||||
|
assert.ok(fs.existsSync(stdinCounter), 'Stdin session counter must exist');
|
||||||
|
assert.ok(!fs.existsSync(envCounter), 'Env session counter must NOT exist when stdin provides session_id');
|
||||||
|
|
||||||
|
fs.unlinkSync(stdinCounter);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
passed++;
|
||||||
|
else failed++;
|
||||||
|
|
||||||
// evaluate-session.js tests
|
// evaluate-session.js tests
|
||||||
console.log('\nevaluate-session.js:');
|
console.log('\nevaluate-session.js:');
|
||||||
|
|
||||||
|
|||||||
@ -79,13 +79,27 @@ argv, kwargs = module.build_terminal_launch(r'C:\\\\Users\\\\user\\\\proj & del
|
|||||||
print(json.dumps({'argv': argv, 'kwargs': kwargs}))
|
print(json.dumps({'argv': argv, 'kwargs': kwargs}))
|
||||||
`);
|
`);
|
||||||
const parsed = JSON.parse(output);
|
const parsed = JSON.parse(output);
|
||||||
assert.deepStrictEqual(parsed.argv.slice(0, 4), ['cmd.exe', '/k', 'cd', '/d']);
|
assert.deepStrictEqual(parsed.argv, ['cmd.exe']);
|
||||||
assert.strictEqual(parsed.argv[4], parsed.kwargs.cwd);
|
assert.ok(parsed.kwargs.cwd.includes('proj & del'), 'path should remain a literal cwd value');
|
||||||
assert.ok(parsed.argv[4].includes('proj & del'), 'path should remain a literal argv entry');
|
assert.ok(parsed.kwargs.cwd.includes('C:'), 'windows drive prefix should be preserved');
|
||||||
assert.ok(parsed.argv[4].includes('C:'), 'windows drive prefix should be preserved');
|
|
||||||
assert.ok(Object.prototype.hasOwnProperty.call(parsed.kwargs, 'creationflags'));
|
assert.ok(Object.prototype.hasOwnProperty.call(parsed.kwargs, 'creationflags'));
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('launch_terminal rejects missing or non-directory paths', () => {
|
||||||
|
const output = runPython(`
|
||||||
|
import importlib.util, json
|
||||||
|
spec = importlib.util.spec_from_file_location("ecc_dashboard_runtime", r"""${runtimeHelpersPath}""")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
try:
|
||||||
|
module.launch_terminal('/definitely/not/a/real/ecc/path')
|
||||||
|
except ValueError as exc:
|
||||||
|
print(json.dumps({'error': str(exc)}))
|
||||||
|
`);
|
||||||
|
const parsed = JSON.parse(output);
|
||||||
|
assert.ok(parsed.error.includes('Path is not a valid directory'));
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('maximize_window falls back to Linux zoom attribute when zoomed state is unsupported', () => {
|
if (test('maximize_window falls back to Linux zoom attribute when zoomed state is unsupported', () => {
|
||||||
const output = runPython(`
|
const output = runPython(`
|
||||||
import importlib.util, json
|
import importlib.util, json
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user