From 926e77b1c2ba25a024ac18731ceb266b6e071157 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 19 Feb 2026 08:49:36 +0100 Subject: [PATCH] chore: bump version to 0.1550 --- CLAUDE.md | 116 +++++- docs/cleanup-findings.md | 105 ++++++ package.json | 2 +- src/ai-checker-base.ts | 13 +- src/ai-idle-checker.ts | 14 - src/ai-plan-checker.ts | 14 - src/config/map-limits.ts | 60 --- src/plan-orchestrator.ts | 2 +- src/prompts/code-reviewer.ts | 46 --- src/prompts/index.ts | 1 - src/run-summary.ts | 8 - src/session-manager.ts | 10 - src/session.ts | 4 +- src/task-queue.ts | 23 +- src/tmux-manager.ts | 37 +- src/transcript-watcher.ts | 73 ++-- src/types.ts | 145 ------- src/utils/buffer-accumulator.ts | 2 - src/utils/cleanup-manager.ts | 2 - src/utils/index.ts | 4 - src/utils/lru-map.ts | 2 - src/utils/regex-patterns.ts | 10 +- src/utils/stale-expiration-map.ts | 2 - src/web/public/app.js | 26 -- src/web/public/styles.css | 601 ------------------------------ src/web/schemas.ts | 13 +- src/web/server.ts | 22 +- 27 files changed, 290 insertions(+), 1067 deletions(-) create mode 100644 docs/cleanup-findings.md delete mode 100644 src/prompts/code-reviewer.ts diff --git a/CLAUDE.md b/CLAUDE.md index bea3c127..854fd953 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`) 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1549 (must match `package.json` for npm publish) +**Version**: 0.1550 (must match `package.json` for npm publish) ## Project Overview @@ -81,6 +81,10 @@ journalctl --user -u claudeman-web -f - **Single-line prompts only** — `writeViaMux()` sends text and Enter separately; multi-line breaks Ink - **Don't kill tmux sessions blindly** — Check `$CLAUDEMAN_TMUX` first; you might be inside one - **Never run full test suite** — `npx vitest run` spawns/kills tmux sessions and will crash your Claudeman session. Run individual test files only. +- **Global regex `lastIndex` sharing** — `ANSI_ESCAPE_PATTERN_FULL/SIMPLE` have `g` flag; use `createAnsiPatternFull/Simple()` factory functions for fresh instances in loops +- **DEC 2026 sync blocks** — Never discard incomplete sync blocks (START without END); buffer up to 50ms then flush. See `app.js:extractSyncSegments()` +- **Terminal writes during buffer load** — Live SSE writes are queued while `_isLoadingBuffer` is true to prevent interleaving with historical data +- **Local echo prompt scanning** — Does NOT use `buffer.cursorY` (Ink moves it); scans buffer bottom-up for visible `>` prompt marker ## Import Conventions @@ -123,12 +127,12 @@ journalctl --user -u claudeman-web -f | `src/prompts/*.ts` | Agent prompts (research-agent, code-reviewer, planner) | | `src/templates/claude-md.ts` | CLAUDE.md generation for new cases | | `src/cli.ts` | Command-line interface handlers | -| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` | -| `src/web/schemas.ts` | Zod v4 validation schemas for API request bodies | -| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows | -| `src/types.ts` | All TypeScript interfaces | +| `src/web/server.ts` | Fastify REST API + SSE at `/api/events` (~90 routes) | +| `src/web/schemas.ts` | Zod v4 validation schemas with path/env security allowlists | +| `src/web/public/app.js` | Frontend: xterm.js, tab management, subagent windows, mobile support (~16K lines) | +| `src/types.ts` | All TypeScript interfaces (~100 types, ~1500 lines) | -**Large files** (>50KB): `ralph-tracker.ts`, `respawn-controller.ts`, `session.ts`, `subagent-watcher.ts` — these contain complex state machines; read `docs/respawn-state-machine.md` before modifying. +**Large files** (>50KB): `app.js`, `ralph-tracker.ts`, `respawn-controller.ts`, `session.ts`, `subagent-watcher.ts` — these contain complex state machines; read `docs/respawn-state-machine.md` before modifying. ### Config Files (`src/config/`) @@ -170,18 +174,89 @@ journalctl --user -u claudeman-web -f **Token tracking**: Interactive mode parses status line ("123.4k tokens"), estimates 60/40 input/output split. -**Hook events**: Claude Code hooks trigger notifications via `/api/hook-event`. Key events: `permission_prompt` (tool approval needed), `elicitation_dialog` (Claude asking question), `idle_prompt` (waiting for input), `stop` (response complete). See `src/hooks-config.ts`. +**Hook events**: Claude Code hooks trigger notifications via `/api/hook-event`. Key events: `permission_prompt` (tool approval needed), `elicitation_dialog` (Claude asking question), `idle_prompt` (waiting for input), `stop` (response complete), `teammate_idle` (Agent Teams), `task_completed` (Agent Teams). See `src/hooks-config.ts`. **Agent Teams (experimental)**: `TeamWatcher` polls `~/.claude/teams/` for team configs and matches teams to sessions via `leadSessionId`. Teammates are in-process threads (not separate OS processes) and appear as standard subagents. RespawnController checks `TeamWatcher.hasActiveTeammates()` before triggering respawn. Enable via `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` env var in `settings.local.json`. See `agent-teams/` for full docs. +**Circuit breaker**: Prevents respawn thrashing when Claude is stuck. States: `CLOSED` (normal) → `HALF_OPEN` (testing) → `OPEN` (blocked). Tracks consecutive no-progress, same-error-repeated, and tests-failing-too-long. Reset via API at `/api/sessions/:id/ralph-circuit-breaker/reset`. + +**Respawn cycle metrics & health scoring**: `RespawnCycleMetrics` tracks per-cycle outcomes (success, stuck_recovery, blocked, error). `RalphLoopHealthScore` computes 0-100 health with component scores (cycleSuccess, circuitBreaker, iterationProgress, aiChecker, stuckRecovery). Available via respawn status API. + +**Subagent-session correlation**: Session parses Task tool output via `BashToolParser` → `SubagentWatcher` discovers new agent → calls `session.findTaskDescriptionNear()` to match description for window title. + +### Frontend Architecture (`app.js`) + +The frontend is a single 16K-line vanilla JS file with these key systems: + +| System | Key Classes/Functions | Purpose | +|--------|----------------------|---------| +| **Terminal rendering** | `batchTerminalWrite()`, `flushPendingWrites()`, `chunkedTerminalWrite()` | 60fps batched writes with DEC 2026 sync | +| **Local echo overlay** | `LocalEchoOverlay` class | DOM overlay for instant mobile keystroke feedback | +| **Mobile support** | `MobileDetection`, `KeyboardHandler`, `SwipeHandler`, `KeyboardAccessoryBar` | Touch input, viewport adaptation, swipe navigation | +| **Subagent windows** | `openSubagentWindow()`, `closeSubagentWindow()`, `updateConnectionLines()` | Floating terminal windows with parent connection lines | +| **Notifications** | `NotificationManager` class | 4-layer: in-app drawer, tab flash, browser API, audio beep | +| **SSE connection** | `connectSSE()`, `addListener()` | EventSource with exponential backoff (1-30s), offline queue (64KB) | +| **Settings** | `openAppSettings()`, `apply*Visibility()` | Server-backed + localStorage persistence | +| **Focus management** | `FocusTrap` class | Modal keyboard navigation with focus restore | + +**Z-index layers**: subagent windows (1000), plan agents (1100), log viewers (2000), image popups (3000), local echo overlay (7). + +**Built-in respawn presets**: `solo-work` (3s idle, 60min), `subagent-workflow` (45s idle, 240min), `team-lead` (90s idle, 480min), `overnight-autonomous` (10s idle, 480min, full reset). + +**Keyboard shortcuts**: Escape (close panels), Ctrl+? (help), Ctrl+Enter (quick start), Ctrl+W (kill session), Ctrl+Tab (next session), Ctrl+K (kill all), Ctrl+L (clear), Ctrl+Shift+R (restore size), Ctrl/Cmd +/- (font size). + +### Security + +- **HTTP Basic Auth**: Optional via `CLAUDEMAN_USERNAME`/`CLAUDEMAN_PASSWORD` env vars +- **CORS**: Restricted to localhost only +- **Security headers**: X-Content-Type-Options, X-Frame-Options, CSP; HSTS if HTTPS +- **Path validation** (`schemas.ts`): Strict allowlist regex, no shell metacharacters, no traversal, must be absolute +- **Env var allowlist**: Only `CLAUDE_CODE_*` prefixes allowed; blocks `PATH`, `LD_PRELOAD`, `NODE_OPTIONS`, `CLAUDEMAN_*` keys +- **File streaming TOCTOU protection**: `FileStreamManager` calls `realpathSync()` twice (at validation and before spawn) to catch symlink swaps + +### SSE Event Categories + +~80+ event types broadcast via `broadcast()`. Key categories: + +| Category | Events | Purpose | +|----------|--------|---------| +| Session | `session:created/updated/deleted/working/idle/exit/error/completion` | Lifecycle | +| Terminal | `session:terminal`, `session:clearTerminal`, `session:needsRefresh` | Output streaming | +| Respawn | `respawn:stateChanged/cycleStarted/blocked/aiCheck*/planCheck*/timer*` | Respawn state machine | +| Subagent | `subagent:discovered/updated/completed/tool_call/progress` | Background agents | +| Ralph | `session:ralphLoopUpdate/ralphTodoUpdate/ralphCompletionDetected` | Ralph tracking | +| Hooks | `hook:{eventName}` (dynamic) | Claude Code hook events | +| Plan | `plan:started/progress/completed/cancelled/subagent` | Plan orchestration | +| Mux | `mux:created/killed/died/statsUpdated` | tmux process monitor | +| Image | `image:detected` | Screenshot detection | + +### API Route Categories + +~90 routes in `server.ts:buildServer()`. Key groups: + +| Group | Prefix | Count | Key endpoints | +|-------|--------|-------|---------------| +| Sessions | `/api/sessions` | ~20 | CRUD, input, resize, interactive, shell | +| Respawn | `/api/sessions/:id/respawn` | 5 | start, stop, enable, config | +| Ralph | `/api/sessions/:id/ralph-*` | 6 | state, status, config, circuit-breaker | +| Plan | `/api/sessions/:id/plan/*` | 5 | task CRUD, checkpoint, history, rollback | +| Subagents | `/api/subagents` | 7 | list, transcript, kill, cleanup | +| Cases | `/api/cases` | 5 | CRUD, link, fix-plan | +| Scheduled | `/api/scheduled` | 4 | CRUD for scheduled runs | +| System | `/api/status`, `/api/stats`, `/api/config`, `/api/settings` | 8 | App state, config | +| Files | `/api/sessions/:id/file*`, `tail-file` | 5 | Browser, preview, raw, tail stream | +| Mux | `/api/mux-sessions` | 4 | tmux management, stats | + ## Adding Features -- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()`. Validate request bodies with Zod schemas. -- **SSE event**: Emit via `broadcast()`, handle in `app.js:handleSSEEvent()` +- **API endpoint**: Types in `types.ts`, route in `server.ts:buildServer()`, use `createErrorResponse()`. Validate request bodies with Zod schemas in `schemas.ts`. +- **SSE event**: Emit via `broadcast()`, handle in `app.js` SSE listener section (search `addListener(`) - **Session setting**: Add to `SessionState` in `types.ts`, include in `session.toState()`, call `persistSessionState()` -- **New test**: Pick unique port (see below), add port comment to test file header +- **Hook event**: Add to `HookEventType` in `types.ts`, add hook command in `hooks-config.ts:generateHooksConfig()`, update `HookEventSchema` in `schemas.ts` +- **Mobile feature**: Add to relevant mobile singleton (`KeyboardHandler`, `KeyboardAccessoryBar`, etc.), test with `MobileDetection.isMobile()` guard +- **New test**: Pick unique port (search `const PORT =`), add port comment to test file header. Tests use ports 3150+. -**Validation**: Uses Zod v4 for request validation. Define schemas near route handlers and use `.parse()` or `.safeParse()`. Note: Zod v4 has different API from v3 (e.g., `z.object()` options changed, error formatting differs). +**Validation**: Uses Zod v4 for request validation. Define schemas in `schemas.ts` and use `.parse()` or `.safeParse()`. Note: Zod v4 has different API from v3 (e.g., `z.object()` options changed, error formatting differs). ## State Files @@ -261,7 +336,10 @@ The app must stay fast with 20 sessions and 50 agent windows: - 60fps terminal (16ms batching + `requestAnimationFrame`) - Auto-trimming buffers (2MB terminal max) - Debounced state persistence (500ms) -- SSE batching (16ms) +- SSE adaptive batching: 16ms (normal), 32ms (moderate), 50ms (rapid); immediate flush at 32KB +- SSE backpressure handling: skip writes to backpressured clients, recover via `session:needsRefresh` on drain +- Cached endpoints: `/api/sessions` and `/api/status` use 1s TTL caches to avoid expensive serialization +- Frontend buffer loads: 128KB chunks via `requestAnimationFrame` to prevent UI jank ## Terminal Anti-Flicker System @@ -319,6 +397,11 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base | **Plan orchestrator** | `src/plan-orchestrator.ts` file header | | **Agent prompts** | `src/prompts/` directory | | **Agent Teams (experimental)** | `agent-teams/README.md`, `agent-teams/design.md` | +| **Local echo overlay** | `docs/local-echo-overlay-plan.md` | +| **Browser testing** | `docs/browser-testing-guide.md` | +| **Mobile testing** | `docs/mobile-testing-report.md` | +| **Run summary design** | `docs/run-summary-plan.md` | +| **Performance audit** | `docs/perf-audit-first-load.md` | ## Scripts @@ -339,6 +422,7 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base | `scripts/test-links-browser.mjs` | Browser test for clickable terminal file links | | `scripts/test-patterns.mjs` | Test file path link detection regex patterns | | `scripts/watch-subagents.ts` | Real-time subagent transcript watcher (list, follow by session/agent ID) | +| `scripts/capture-readme-screenshots.mjs` | Capture screenshots for README | | `scripts/claudeman-web.service` | systemd service file for production deployment | ## Memory Leak Prevention @@ -351,9 +435,9 @@ When adding new event listeners or timers: 2. Add cleanup to appropriate `stop()` or `cleanup*()` method 3. For singleton watchers, store refs in class properties and remove in server `stop()` -**Backend**: Clear Maps in `stop()`, null promise callbacks on error, remove watcher listeners on shutdown. +**Backend**: Clear Maps in `stop()`, null promise callbacks on error, remove watcher listeners on shutdown. Use `CleanupManager` for centralized disposal — supports timers, intervals, watchers, listeners, streams. Guard async callbacks with `if (this.cleanup.isStopped) return`. -**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state. +**Frontend**: Store drag/resize handlers on elements, clean up in `close*()` functions. SSE reconnect calls `handleInit()` which resets state. SSE listeners are tracked in an array and removed on reconnect to prevent accumulation. Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns. @@ -364,3 +448,7 @@ Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns. **Adding a new API endpoint**: Define types in `types.ts`, add route in `server.ts:buildServer()`, broadcast SSE events if needed, handle in `app.js:handleSSEEvent()`. **Modifying respawn behavior**: Study `docs/respawn-state-machine.md` first. The state machine is in `respawn-controller.ts`. Use MockSession from `test/respawn-test-utils.ts` for testing. + +**Modifying mobile behavior**: Mobile singletons (`MobileDetection`, `KeyboardHandler`, `SwipeHandler`, `KeyboardAccessoryBar`) all have `init()`/`cleanup()` lifecycle. KeyboardHandler uses `visualViewport` API for iOS keyboard detection (100px threshold for address bar drift). All mobile handlers are re-initialized after SSE reconnect to prevent stale closures. + +**Adding a file watcher**: Use `ImageWatcher` as a template pattern — chokidar with `awaitWriteFinish`, burst throttling (max 20/10s), debouncing (200ms), and auto-ignore of `node_modules/.git/dist/`. diff --git a/docs/cleanup-findings.md b/docs/cleanup-findings.md new file mode 100644 index 00000000..2fa1f8f6 --- /dev/null +++ b/docs/cleanup-findings.md @@ -0,0 +1,105 @@ +# Codebase Cleanup Findings + +Compiled from parallel analysis of the entire Claudeman codebase by 3 research agents (2026-02-19). + +## P0 — Bug Fix + +### 1. `{WORKING_DIR}` placeholder never replaced in plan-orchestrator.ts +- **File:** `src/plan-orchestrator.ts:409` +- `RESEARCH_AGENT_PROMPT` has `{WORKING_DIR}` placeholder but only `{TASK}` is replaced +- The literal string `{WORKING_DIR}` gets sent to the AI model +- **Fix:** Add `.replace('{WORKING_DIR}', this.workingDir)` after the `{TASK}` replacement + +## P1 — Dead Code Removal (High Impact) + +### 2. ~66 dead functions in app.js +- Functions never called: `clearAll()`, `toggleSubagentDropdown()`, `goHome()`, `showRalphWizard()`, `minimizeRalphWizard()`, `restoreRalphWizard()`, `ralphWizardNext()`, `ralphWizardBack()`, `skipPlanGeneration()`, `regeneratePlan()`, `incrementTabCount()`, `decrementTabCount()`, `incrementShellCount()`, `decrementShellCount()`, `stopClaude()`, and ~50 more +- Many are remnants of abandoned features (Ralph wizard, plan version history) +- **Estimated savings:** 300-500 lines + +### 3. ~74 dead CSS selectors in styles.css +- Major dead blocks: Task Panel System (`.task-panel`), Process Panel System (`.process-panel`), Monitor Tabs (`.monitor-tabs`), Ralph Metadata (`.ralph-progress-section`, `.ralph-meta`), Plan Editor Toolbar, Plan Version History +- Plus ~30 minor unused utility/component selectors +- **Estimated savings:** ~400 lines + +### 4. 13 dead type definitions in types.ts (~150 lines) +- Dead request interfaces (superseded by Zod schemas): `CreateSessionRequest`, `RunPromptRequest`, `SessionInputRequest`, `ResizeRequest`, `CreateCaseRequest`, `QuickStartRequest`, `CreateScheduledRunRequest`, `QuickRunRequest`, `HookEventRequest` +- Other dead types: `TaskAssignment`, `MemoryMetrics`, `RalphStateRecord` +- Dead function: `createSuccessResponse` (exported, never imported) +- **Estimated savings:** ~150 lines + +### 5. 9 unused constants in map-limits.ts +- `MAX_PENDING_HOOKS`, `MAX_SESSION_HISTORY`, `MAX_SSE_CLIENTS_PER_SESSION`, `MAX_TOTAL_SSE_CLIENTS`, `FILE_WATCHER_WARNING_THRESHOLD`, `MAX_QUEUED_TASKS`, `MAX_COMPLETED_TASKS_HISTORY`, `COMPLETED_TODO_TTL_MS`, `MAX_CONCURRENT_SESSIONS` +- 9 of 14 exports are dead — only 5 are actually imported + +### 6. Dead `SessionInputSchema` in schemas.ts +- `SessionInputSchema` (line 87) is defined/exported but never imported +- `SessionInputWithLimitSchema` is the one actually used + +### 7. Dead `code-reviewer.ts` prompt file +- `src/prompts/code-reviewer.ts` — entire file is dead, `CODE_REVIEWER_PROMPT` never imported +- Re-exported in `src/prompts/index.ts` but no consumer + +### 8. Dead utility exports +- **Default exports** (4 files): `lru-map.ts`, `cleanup-manager.ts`, `stale-expiration-map.ts`, `buffer-accumulator.ts` — all have `export default` that's never used +- **`stripAnsiSimple`** in `regex-patterns.ts` — exported, never imported (only `stripAnsi` used) +- **String similarity**: `isSimilar`, `isSimilarByDistance`, `stringSimilarity`, `levenshteinDistance` — none imported externally +- **LRUMap methods**: `oldest()`, `newest()`, `peek()`, `expireOlderThan()`, `valuesInOrder()`, `maxEntries`, `freeSlots` — never called +- **StaleExpirationMap methods**: `touch()`, `getAge()`, `getRemainingTtl()`, `peek()` — never called +- **CleanupManager methods**: `registerWatcher()`, `registerListener()`, `registerStream()`, `getRegistrations()`, `resourceCounts` — never called + +### 9. Dead backend functions +- `resetSessionManager()` in session-manager.ts:300 — never imported +- `getStoredTasks()` in task-queue.ts:264 — never called +- `start()` in session.ts:1918 — no-op legacy method +- Empty `updateStatsFromEvent()` in run-summary.ts:397 — called every event, does nothing + +### 10. Dead TS type exports +- `AiCheckerEvents`, `AiIdleCheckerEvents`, `AiPlanCheckerEvents` — never imported +- `AiCheckStatus`, `AiPlanCheckStatus` — backwards compat aliases, never imported + +## P2 — Performance & Efficiency + +### 11. task-queue.ts `getCount()` iterates all tasks 5 times +- Called every Ralph Loop tick — creates array from Map, then filters 4 times +- **Fix:** Single-pass counting like `TaskTracker.getStats()` does + +### 12. transcript-watcher.ts double file read +- `readNewEntries()` reads the file twice: once for CRLF detection, once for parsing +- `crlfDelay: Infinity` already handles both line endings +- **Fix:** Remove the raw buffer CRLF check, read once + +### 13. tmux-manager.ts `saveSessions()` no debounce +- Rapid calls can overlap; no in-flight guard unlike `StateStore` +- **Fix:** Add debouncing or in-flight tracking + +## P3 — Consolidation & Consistency + +### 14. Duplicate `SAFE_PATH_PATTERN` regex +- `schemas.ts:15` and `tmux-manager.ts:81` — identical regex +- **Fix:** Share from one location + +### 15. Duplicate `MAX_CONCURRENT_SESSIONS` +- `map-limits.ts:57` (dead) vs `server.ts:131` (used, hardcoded) +- **Fix:** server.ts should import from map-limits + +### 16. Duplicate cache TTLs in server.ts +- `SESSIONS_LIST_CACHE_TTL` and `LIGHT_STATE_CACHE_TTL_MS` — both 1000ms +- **Fix:** Consolidate into one constant + +### 17. Inconsistent path import in server.ts +- Imports both `path` default and destructured `{ join, dirname, resolve, relative, isAbsolute }` +- 3 lines use `path.join()` while everywhere else uses `join()` +- **Fix:** Remove default import, use `join()` consistently + +### 18. Re-export indirection for `getAugmentedPath` +- `session.ts:89` re-exports from `claude-cli-resolver.ts` for backwards compat +- `ai-checker-base.ts` should import directly from source + +### 19. `cliInfoUpdated` event missing from SessionEvents interface +- Emitted in `session.ts:1742`, handled in `server.ts:4214`, but not in the interface +- Type safety gap — handlers aren't type-checked + +### 20. Array instead of Set for `_childAgentIds` in session.ts +- Uses `includes()`/`indexOf()` for lookups (O(n)) +- Small lists in practice, but Set is more appropriate diff --git a/package.json b/package.json index 79fd17be..2fd61b50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1549", + "version": "0.1550", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/ai-checker-base.ts b/src/ai-checker-base.ts index e6bcfa80..f67f778d 100644 --- a/src/ai-checker-base.ts +++ b/src/ai-checker-base.ts @@ -28,7 +28,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { EventEmitter } from 'node:events'; -import { getAugmentedPath } from './session.js'; +import { getAugmentedPath } from './utils/claude-cli-resolver.js'; import { ANSI_ESCAPE_PATTERN_SIMPLE } from './utils/index.js'; // ========== Security Validation ========== @@ -95,17 +95,6 @@ export interface AiCheckerStateBase { disabledReason: string | null; } -/** Events emitted by AI checkers */ -export interface AiCheckerEvents { - checkStarted: () => void; - checkCompleted: (result: R) => void; - checkFailed: (error: string) => void; - cooldownStarted: (endsAt: number) => void; - cooldownEnded: () => void; - disabled: (reason: string) => void; - log: (message: string) => void; -} - // ========== Constants ========== /** Poll interval for checking temp file completion */ diff --git a/src/ai-idle-checker.ts b/src/ai-idle-checker.ts index e565604c..89aaf409 100644 --- a/src/ai-idle-checker.ts +++ b/src/ai-idle-checker.ts @@ -29,32 +29,18 @@ import { type AiCheckerConfigBase, type AiCheckerResultBase, type AiCheckerStateBase, - type AiCheckerStatus, } from './ai-checker-base.js'; // ========== Types ========== export interface AiIdleCheckConfig extends AiCheckerConfigBase {} -// Re-export the status type for backwards compatibility -export type AiCheckStatus = AiCheckerStatus; export type AiCheckVerdict = 'IDLE' | 'WORKING' | 'ERROR'; export interface AiCheckResult extends AiCheckerResultBase {} export interface AiCheckState extends AiCheckerStateBase {} -/** Events emitted by AiIdleChecker */ -export interface AiIdleCheckerEvents { - checkStarted: () => void; - checkCompleted: (result: AiCheckResult) => void; - checkFailed: (error: string) => void; - cooldownStarted: (endsAt: number) => void; - cooldownEnded: () => void; - disabled: (reason: string) => void; - log: (message: string) => void; -} - // ========== Constants ========== const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = { diff --git a/src/ai-plan-checker.ts b/src/ai-plan-checker.ts index 7a0376e0..eec922ef 100644 --- a/src/ai-plan-checker.ts +++ b/src/ai-plan-checker.ts @@ -28,32 +28,18 @@ import { type AiCheckerConfigBase, type AiCheckerResultBase, type AiCheckerStateBase, - type AiCheckerStatus, } from './ai-checker-base.js'; // ========== Types ========== export interface AiPlanCheckConfig extends AiCheckerConfigBase {} -// Re-export the status type for backwards compatibility -export type AiPlanCheckStatus = AiCheckerStatus; export type AiPlanCheckVerdict = 'PLAN_MODE' | 'NOT_PLAN_MODE' | 'ERROR'; export interface AiPlanCheckResult extends AiCheckerResultBase {} export interface AiPlanCheckState extends AiCheckerStateBase {} -/** Events emitted by AiPlanChecker */ -export interface AiPlanCheckerEvents { - checkStarted: () => void; - checkCompleted: (result: AiPlanCheckResult) => void; - checkFailed: (error: string) => void; - cooldownStarted: (endsAt: number) => void; - cooldownEnded: () => void; - disabled: (reason: string) => void; - log: (message: string) => void; -} - // ========== Constants ========== const DEFAULT_PLAN_CHECK_CONFIG: AiPlanCheckConfig = { diff --git a/src/config/map-limits.ts b/src/config/map-limits.ts index 4c35be0f..1ea35543 100644 --- a/src/config/map-limits.ts +++ b/src/config/map-limits.ts @@ -36,16 +36,6 @@ export const MAX_SUBAGENT_ACTIVITY_PER_AGENT = 100; */ export const MAX_TOOL_RESULTS_PER_AGENT = 200; -// ============================================================================ -// Hook Event Limits -// ============================================================================ - -/** - * Maximum pending hook events to queue. - * Prevents unbounded growth if hook processing is slow. - */ -export const MAX_PENDING_HOOKS = 50; - // ============================================================================ // Session Tracking Limits // ============================================================================ @@ -56,56 +46,6 @@ export const MAX_PENDING_HOOKS = 50; */ export const MAX_CONCURRENT_SESSIONS = 50; -/** - * Maximum session history entries to keep (for analytics). - */ -export const MAX_SESSION_HISTORY = 100; - -// ============================================================================ -// SSE Client Limits -// ============================================================================ - -/** - * Maximum SSE clients per session. - * Prevents resource exhaustion from many browser tabs. - */ -export const MAX_SSE_CLIENTS_PER_SESSION = 10; - -/** - * Maximum total SSE clients across all sessions. - */ -export const MAX_TOTAL_SSE_CLIENTS = 100; - -// ============================================================================ -// File Watcher Limits -// ============================================================================ - -/** - * Maximum file watchers (FSWatcher) to allow. - * Linux default max_user_watches is 8192-65536. - * We warn at 80% capacity and evict idle watchers. - */ -export const MAX_FILE_WATCHERS = 500; - -/** - * Warning threshold as percentage of max watchers. - */ -export const FILE_WATCHER_WARNING_THRESHOLD = 0.8; - -// ============================================================================ -// Task Tracking Limits -// ============================================================================ - -/** - * Maximum tasks to keep in the task queue. - */ -export const MAX_QUEUED_TASKS = 100; - -/** - * Maximum completed tasks to keep for history. - */ -export const MAX_COMPLETED_TASKS_HISTORY = 50; - // ============================================================================ // Todo Item Limits (Ralph Tracker) // ============================================================================ diff --git a/src/plan-orchestrator.ts b/src/plan-orchestrator.ts index dbbb6faa..98bf9141 100644 --- a/src/plan-orchestrator.ts +++ b/src/plan-orchestrator.ts @@ -406,7 +406,7 @@ export class PlanOrchestrator { this.runningSessions.add(session); - const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription); + const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription).replace('{WORKING_DIR}', this.workingDir); // Start progress interval before try block to ensure cleanup in finally const progressInterval = setInterval(() => { diff --git a/src/prompts/code-reviewer.ts b/src/prompts/code-reviewer.ts deleted file mode 100644 index 59e0ff54..00000000 --- a/src/prompts/code-reviewer.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Code Reviewer Prompt - * - * Designs post-implementation review tasks. - * Reserved for future use. - * - * Placeholders: {TASK} - */ - -export const CODE_REVIEWER_PROMPT = `You are a Code Review Specialist designing post-implementation review tasks. - -## YOUR TASK -Design code review steps for implementations in this task: - -## TASK DESCRIPTION -{TASK} - -## INSTRUCTIONS -For each implementation identified, create a review task that checks: -1. **Best Practices**: Language-specific conventions and idioms -2. **Security**: OWASP top 10, input validation, authentication -3. **Performance**: Time complexity, memory usage, N+1 queries -4. **Error Handling**: Edge cases covered, meaningful error messages -5. **Code Quality**: DRY, SOLID principles, readability -6. **Type Safety**: Proper typing, no implicit any, null checks - -## REVIEW TASK GUIDELINES -- Review tasks run AFTER implementation, BEFORE merge -- Each review should be specific and actionable -- Include what to look for and how to verify -- Reference language-specific linting tools where applicable - -## OUTPUT FORMAT -Return ONLY a JSON array: -[ - { - "category": "security|performance|quality|error-handling|best-practices|type-safety", - "content": "Review authentication handler for XSS vulnerabilities", - "rationale": "User input flows through auth - must sanitize", - "verificationCriteria": "No unescaped user input, all inputs validated", - "reviewChecklist": ["Check input sanitization", "Verify CSRF tokens", "Review session handling"], - "implToReview": "Implement authentication handler" - } -] - -Generate 5-10 review tasks. Code review catches bugs that tests miss.`; diff --git a/src/prompts/index.ts b/src/prompts/index.ts index 3919206d..0c91a154 100644 --- a/src/prompts/index.ts +++ b/src/prompts/index.ts @@ -7,4 +7,3 @@ export { RESEARCH_AGENT_PROMPT } from './research-agent.js'; export { PLANNER_PROMPT } from './planner.js'; -export { CODE_REVIEWER_PROMPT } from './code-reviewer.js'; diff --git a/src/run-summary.ts b/src/run-summary.ts index d58330ec..cbb21981 100644 --- a/src/run-summary.ts +++ b/src/run-summary.ts @@ -112,9 +112,6 @@ export class RunSummaryTracker { this.events.push(event); this.lastUpdatedAt = event.timestamp; - // Update stats based on event type - this.updateStatsFromEvent(event); - // Trim if needed if (this.events.length > MAX_EVENTS) { this.events = this.events.slice(-TRIM_TO_EVENTS); @@ -394,11 +391,6 @@ export class RunSummaryTracker { // ========== Private Methods ========== - private updateStatsFromEvent(_event: RunSummaryEvent): void { - // Most stats are updated in the specific record* methods - // This is for any additional cross-cutting concerns - } - private startStateStuckDetection(): void { this.stateStuckCheckTimer = setInterval(() => { if (!this.currentState || !this.stateEnteredAt || this.stateStuckWarned) { diff --git a/src/session-manager.ts b/src/session-manager.ts index 4ac6adf1..70420d89 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -293,13 +293,3 @@ export function getSessionManager(): SessionManager { return managerInstance; } -/** - * Resets the singleton SessionManager instance. - * Primarily used for testing to ensure test isolation. - */ -export async function resetSessionManager(): Promise { - if (managerInstance) { - await managerInstance.stopAllSessions(); - managerInstance = null; - } -} diff --git a/src/session.ts b/src/session.ts index efc8827c..ef0616cb 100644 --- a/src/session.ts +++ b/src/session.ts @@ -85,8 +85,6 @@ const NEWLINE_SPLIT_PATTERN = /\r?\n/; // Claude CLI PATH resolution — shared utility import { getAugmentedPath } from './utils/claude-cli-resolver.js'; -// Re-export for backward compatibility (ai-checker-base imports from session) -export { getAugmentedPath } from './utils/claude-cli-resolver.js'; /** * Wraps a promise with a timeout to prevent indefinite hangs. @@ -193,6 +191,8 @@ export interface SessionEvents { bashToolEnd: (tool: ActiveBashTool) => void; /** Active Bash tools list updated */ bashToolsUpdate: (tools: ActiveBashTool[]) => void; + /** CLI info (version, model, account) updated */ + cliInfoUpdated: (info: { version: string | null; model: string | null; accountType: string | null; latestVersion: string | null }) => void; } /** diff --git a/src/task-queue.ts b/src/task-queue.ts index 6617aa83..23850cad 100644 --- a/src/task-queue.ts +++ b/src/task-queue.ts @@ -13,7 +13,6 @@ import { EventEmitter } from 'node:events'; import { Task, CreateTaskOptions } from './task.js'; import { getStore } from './state-store.js'; -import { TaskState } from './types.js'; /** * Events emitted by TaskQueue @@ -214,16 +213,16 @@ export class TaskQueue extends EventEmitter { ) || null; } - /** Gets counts of tasks by status. */ + /** Gets counts of tasks by status (single-pass). */ getCount(): { total: number; pending: number; running: number; completed: number; failed: number } { - const tasks = this.getAllTasks(); - return { - total: tasks.length, - pending: tasks.filter((t) => t.isPending()).length, - running: tasks.filter((t) => t.isRunning()).length, - completed: tasks.filter((t) => t.isCompleted()).length, - failed: tasks.filter((t) => t.isFailed()).length, - }; + let pending = 0, running = 0, completed = 0, failed = 0; + for (const task of this.tasks.values()) { + if (task.isPending()) pending++; + else if (task.isRunning()) running++; + else if (task.isCompleted()) completed++; + else if (task.isFailed()) failed++; + } + return { total: this.tasks.size, pending, running, completed, failed }; } /** Removes all completed tasks. Returns count removed. */ @@ -260,10 +259,6 @@ export class TaskQueue extends EventEmitter { return count; } - /** Gets tasks from persistent storage. */ - getStoredTasks(): Record { - return this.store.getTasks(); - } } // Singleton instance diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index f19d8353..a79502c9 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -32,6 +32,7 @@ import { dirname, join } from 'node:path'; import { homedir } from 'node:os'; import { ProcessStats, PersistedRespawnConfig, getErrorMessage, NiceConfig, DEFAULT_NICE_CONFIG, type PaneInfo } from './types.js'; import { wrapWithNice } from './utils/nice-wrapper.js'; +import { SAFE_PATH_PATTERN } from './utils/regex-patterns.js'; import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats } from './mux-interface.js'; // Claude CLI PATH resolution — shared utility @@ -77,9 +78,6 @@ const MUX_SESSIONS_FILE = join(homedir(), '.claudeman', 'mux-sessions.json'); /** Regex to validate tmux session names (only allow safe characters) */ const SAFE_MUX_NAME_PATTERN = /^claudeman-[a-f0-9-]+$/; -/** Regex to validate working directory paths (no shell metacharacters) */ -const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/; - /** Regex to validate tmux pane targets (e.g., "%0", "%1", "0", "1") */ const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/; @@ -795,7 +793,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { /** * Start periodic mouse mode sync for all tracked sessions. * Polls pane counts every 5s and toggles mouse on/off as needed. - * Only calls tmux set-option when the pane count actually changes. + * Polls every 5s. On pane count change, toggles mouse on (>1 pane) or off (1 pane). + * If enableMouseMode/disableMouseMode fails, lastPaneCount is NOT updated so it retries next poll. */ startMouseModeSync(intervalMs: number = 5000): void { if (this.mouseSyncInterval) { @@ -804,18 +803,24 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { this.mouseSyncInterval = setInterval(() => { if (IS_TEST_MODE) return; + for (const session of this.sessions.values()) { const panes = this.listPanes(session.muxName); const count = panes.length; - const prev = this.lastPaneCount.get(session.muxName); + if (count === 0) continue; - // Only toggle when pane count crosses the 1↔N boundary - if (prev !== count && count > 0) { - this.lastPaneCount.set(session.muxName, count); - if (count > 1) { - this.enableMouseMode(session.muxName); - } else { - this.disableMouseMode(session.muxName); + const prev = this.lastPaneCount.get(session.muxName); + if (prev === count) continue; + + // Pane count changed — toggle mouse mode + if (count > 1) { + if (this.enableMouseMode(session.muxName)) { + this.lastPaneCount.set(session.muxName, count); + } + // If enableMouseMode fails, DON'T update lastPaneCount — retry next poll + } else { + if (this.disableMouseMode(session.muxName)) { + this.lastPaneCount.set(session.muxName, count); } } } @@ -950,8 +955,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { `tmux set-option -t "${muxName}" mouse on`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS } ); + console.log(`[TmuxManager] Mouse mode ON for ${muxName}`); return true; - } catch { + } catch (err) { + console.error(`[TmuxManager] Failed to enable mouse mode for ${muxName}:`, err); return false; } } @@ -972,8 +979,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { `tmux set-option -t "${muxName}" mouse off`, { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS } ); + console.log(`[TmuxManager] Mouse mode OFF for ${muxName}`); return true; - } catch { + } catch (err) { + console.error(`[TmuxManager] Failed to disable mouse mode for ${muxName}:`, err); return false; } } diff --git a/src/transcript-watcher.ts b/src/transcript-watcher.ts index 28d6a461..42f68699 100644 --- a/src/transcript-watcher.ts +++ b/src/transcript-watcher.ts @@ -291,54 +291,41 @@ export class TranscriptWatcher extends EventEmitter { } const entries: TranscriptEntry[] = []; - // Read raw buffer to detect actual line endings (LF vs CRLF) const transcriptPath = this.transcriptPath; - let rawChunks: Buffer[] = []; - const rawStream = createReadStream(transcriptPath, { + + const stream = createReadStream(transcriptPath, { start: this.filePosition, + encoding: 'utf-8', }); - rawStream.on('data', (chunk: Buffer | string) => rawChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))); - rawStream.on('end', () => { - // Detect if file uses CRLF - const raw = Buffer.concat(rawChunks); - rawChunks = []; // Free memory - const hasCRLF = raw.includes(0x0d); // 0x0d = \r - const lineEndingSize = hasCRLF ? 2 : 1; - const stream = createReadStream(transcriptPath, { - start: this.filePosition, - encoding: 'utf-8', - }); - - const rl = createInterface({ - input: stream, - crlfDelay: Infinity, - }); - - let bytesRead = this.filePosition; - - rl.on('line', (line) => { - bytesRead += Buffer.byteLength(line, 'utf-8') + lineEndingSize; - - if (!line.trim()) return; - - try { - const entry = JSON.parse(line) as TranscriptEntry; - entries.push(entry); - } catch { - // Skip malformed lines - } - }); - - rl.on('close', () => { - this.filePosition = bytesRead; - resolve(entries); - }); - - rl.on('error', reject); - stream.on('error', reject); + const rl = createInterface({ + input: stream, + crlfDelay: Infinity, // Handles both LF and CRLF }); - rawStream.on('error', reject); + + rl.on('line', (line) => { + if (!line.trim()) return; + + try { + const entry = JSON.parse(line) as TranscriptEntry; + entries.push(entry); + } catch { + // Skip malformed lines + } + }); + + rl.on('close', () => { + // Update position to current file size (accounts for any line ending style) + try { + this.filePosition = statSync(transcriptPath).size; + } catch { + // File may have been deleted between read and stat + } + resolve(entries); + }); + + rl.on('error', reject); + stream.on('error', reject); }); } diff --git a/src/types.ts b/src/types.ts index 16ca7606..db9f1bfa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,29 +37,6 @@ export interface BufferConfig { onTrim?: (trimmedBytes: number) => void; } -/** - * Memory metrics for monitoring and debugging. - * Extends Node.js process.memoryUsage() with application-specific tracking. - */ -export interface MemoryMetrics { - /** Heap memory used by V8 (bytes) */ - heapUsed: number; - /** Total heap size allocated by V8 (bytes) */ - heapTotal: number; - /** Memory used by C++ objects bound to JavaScript (bytes) */ - external: number; - /** Memory used by ArrayBuffers and SharedArrayBuffers (bytes) */ - arrayBuffers: number; - /** Sizes of tracked Maps by name */ - mapSizes: Record; - /** Number of active timers (setTimeout/setInterval) */ - timerCount: number; - /** Number of active file system watchers */ - watcherCount: number; - /** Timestamp when metrics were collected */ - timestamp: number; -} - /** * Resource types that can be registered for cleanup. */ @@ -621,18 +598,6 @@ export interface SessionOutput { exitCode: number | null; } -/** - * Task assignment record - */ -export interface TaskAssignment { - /** Session ID that task is assigned to */ - sessionId: string; - /** Task ID being assigned */ - taskId: string; - /** Timestamp of assignment */ - assignedAt: number; -} - // ========== API Error Handling ========== /** @@ -665,101 +630,11 @@ export const ErrorMessages: Record = { [ApiErrorCode.INTERNAL_ERROR]: 'An internal error occurred', }; -// ========== API Request Types ========== - -/** - * Request to create a new session - */ -export interface CreateSessionRequest { - /** Optional working directory path */ - workingDir?: string; -} - -/** - * Request to run a prompt in a session - */ -export interface RunPromptRequest { - /** Prompt to send to Claude */ - prompt: string; -} - -/** - * Request to send input to a session - */ -export interface SessionInputRequest { - /** Input string to send */ - input: string; -} - -/** - * Request to resize terminal - */ -export interface ResizeRequest { - /** Number of columns */ - cols: number; - /** Number of rows */ - rows: number; -} - -/** - * Request to create a new case - */ -export interface CreateCaseRequest { - /** Case name (alphanumeric with hyphens/underscores) */ - name: string; - /** Optional case description */ - description?: string; -} - -/** - * Request for quick start (create case + session) - */ -export interface QuickStartRequest { - /** Optional case name, defaults to 'testcase' */ - caseName?: string; - /** Session mode: 'claude' for Claude CLI, 'shell' for bash shell */ - mode?: 'claude' | 'shell'; -} - -/** - * Request to create a scheduled run - */ -export interface CreateScheduledRunRequest { - /** Prompt to run */ - prompt: string; - /** Optional working directory */ - workingDir?: string; - /** Duration in minutes */ - durationMinutes: number; -} - -/** - * Request for quick run (one-shot prompt execution) - */ -export interface QuickRunRequest { - /** Prompt to run */ - prompt: string; - /** Optional working directory */ - workingDir?: string; -} - /** * Hook event types triggered by Claude Code's hooks system */ export type HookEventType = 'idle_prompt' | 'permission_prompt' | 'elicitation_dialog' | 'stop' | 'teammate_idle' | 'task_completed'; -/** - * Request body for the hook-event API endpoint - */ -export interface HookEventRequest { - /** Type of hook event that fired */ - event: HookEventType; - /** Session ID from CLAUDEMAN_SESSION_ID env var */ - sessionId: string; - /** Additional event data (tool name, command, question, etc.) */ - data?: Record; -} - // ========== API Response Types ========== /** @@ -784,19 +659,6 @@ export function createErrorResponse(code: ApiErrorCode, details?: string): ApiRe }; } -/** - * Creates a standardized success response - * @template T Type of the data payload - * @param data Optional response data - * @returns Formatted success response - */ -export function createSuccessResponse(data?: T): ApiResponse { - return { - success: true, - data, - }; -} - /** * Response for session operations */ @@ -1074,13 +936,6 @@ export interface RalphSessionState { lastUpdated: number; } -/** - * Map of session ID to inner state - */ -export interface RalphStateRecord { - [sessionId: string]: RalphSessionState; -} - // ========== RALPH_STATUS Block Types ========== /** diff --git a/src/utils/buffer-accumulator.ts b/src/utils/buffer-accumulator.ts index 9ca6ab56..d61b949b 100644 --- a/src/utils/buffer-accumulator.ts +++ b/src/utils/buffer-accumulator.ts @@ -193,5 +193,3 @@ export class BufferAccumulator { } } } - -export default BufferAccumulator; diff --git a/src/utils/cleanup-manager.ts b/src/utils/cleanup-manager.ts index 21b6e64f..67a4a0f1 100644 --- a/src/utils/cleanup-manager.ts +++ b/src/utils/cleanup-manager.ts @@ -326,5 +326,3 @@ export class CleanupManager implements Disposable { } } } - -export default CleanupManager; diff --git a/src/utils/index.ts b/src/utils/index.ts index 1c28ea45..d5ed6e06 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,7 +17,6 @@ export { createAnsiPatternFull, createAnsiPatternSimple, stripAnsi, - stripAnsiSimple, } from './regex-patterns.js'; export { MAX_SESSION_TOKENS, @@ -25,10 +24,7 @@ export { validateTokensAndCost, } from './token-validation.js'; export { - levenshteinDistance, stringSimilarity, - isSimilar, - isSimilarByDistance, normalizePhrase, fuzzyPhraseMatch, todoContentHash, diff --git a/src/utils/lru-map.ts b/src/utils/lru-map.ts index e7b44b69..2fe4757b 100644 --- a/src/utils/lru-map.ts +++ b/src/utils/lru-map.ts @@ -261,5 +261,3 @@ export class LRUMap extends Map { return Math.max(0, this.maxSize - super.size); } } - -export default LRUMap; diff --git a/src/utils/regex-patterns.ts b/src/utils/regex-patterns.ts index 172e583e..2fb409b7 100644 --- a/src/utils/regex-patterns.ts +++ b/src/utils/regex-patterns.ts @@ -64,10 +64,8 @@ export function stripAnsi(text: string): string { } /** - * Strips simple ANSI CSI codes from text (faster, less comprehensive). - * @param text - Text containing ANSI escape codes - * @returns Text with ANSI CSI codes removed + * Regex to validate safe file paths (no shell metacharacters). + * Shared by schemas.ts and tmux-manager.ts for consistent path validation. */ -export function stripAnsiSimple(text: string): string { - return text.replace(ANSI_ESCAPE_PATTERN_SIMPLE, ''); -} +export const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/; + diff --git a/src/utils/stale-expiration-map.ts b/src/utils/stale-expiration-map.ts index 89653911..1862d120 100644 --- a/src/utils/stale-expiration-map.ts +++ b/src/utils/stale-expiration-map.ts @@ -334,5 +334,3 @@ export class StaleExpirationMap implements Disposable { } } } - -export default StaleExpirationMap; diff --git a/src/web/public/app.js b/src/web/public/app.js index dc0d4155..a92a6d36 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -4384,11 +4384,6 @@ class ClaudemanApp { } } - // Legacy toggle for backwards compat - toggleSubagentDropdown(badgeEl) { - this.pinSubagentDropdown(badgeEl); - } - // Permanently close a minimized subagent (remove from DOM and minimized set) permanentlyCloseMinimizedSubagent(agentId, sessionId) { // Remove from minimized set @@ -7307,27 +7302,6 @@ class ClaudemanApp { } } - skipPlanGeneration() { - // Stop any running timers - if (this.planLoadingTimer) { - clearInterval(this.planLoadingTimer); - this.planLoadingTimer = null; - } - if (this.planPhaseTimer) { - clearInterval(this.planPhaseTimer); - this.planPhaseTimer = null; - } - - this.ralphWizardConfig.skipPlanGeneration = true; - this.ralphWizardConfig.planGenerated = false; - this.ralphWizardConfig.generatedPlan = null; - - // Generate preview and go to step 3 - this.updateRalphPromptPreview(); - this.ralphWizardStep = 3; - this.updateRalphWizardUI(); - } - regeneratePlan() { this.ralphWizardConfig.generatedPlan = null; this.ralphWizardConfig.planGenerated = false; diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 12a7303c..a09d6a18 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -39,9 +39,6 @@ * { box-sizing: border-box; margin: 0; padding: 0; } -/* Utility classes */ -.mt-sm { margin-top: 0.5rem; } - /* ========== Accessibility: Focus Styles ========== */ /* Skip link for keyboard users */ @@ -1319,22 +1316,7 @@ body { white-space: nowrap; } -/* Legacy classes for backward compatibility */ -.ralph-progress-section { - display: flex; - align-items: flex-start; - gap: 1.5rem; - margin-bottom: 1rem; -} - /* Large Progress Ring */ -.ralph-ring { - position: relative; - width: 100px; - height: 100px; - flex-shrink: 0; -} - .ralph-ring-svg-large { width: 100%; height: 100%; @@ -1378,66 +1360,6 @@ body { margin-top: 0.15rem; } -/* Meta Info */ -.ralph-meta { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.ralph-meta-item { - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -.ralph-meta-label { - font-size: 0.65rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.ralph-meta code { - background: var(--bg-input); - padding: 0.25rem 0.5rem; - border-radius: 4px; - font-family: 'SF Mono', Monaco, monospace; - font-size: 0.75rem; - color: var(--accent); - word-break: break-all; -} - -.ralph-meta-row { - display: flex; - gap: 1.5rem; -} - -.ralph-meta-item span:not(.ralph-meta-label) { - font-size: 0.9rem; - font-weight: 500; - color: var(--text); -} - -/* Task Cards Section */ -.ralph-tasks-section { - background: var(--bg-card); - border-radius: 6px; - overflow: hidden; -} - -.ralph-tasks-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.5rem 0.75rem; - background: var(--bg-input); - font-weight: 500; - color: var(--text); - font-size: 0.75rem; -} - .ralph-tasks-count { color: var(--text-dim); font-weight: 400; @@ -1886,13 +1808,6 @@ body { gap: 0; } -.toolbar-divider { - width: 1px; - height: 20px; - background: var(--border); - margin: 0 0.5rem; -} - .btn-toolbar { display: flex; align-items: center; @@ -2339,15 +2254,6 @@ body { background: var(--bg-hover); } -.btn-danger-sm { - color: #ff6b6b; -} - -.btn-danger-sm:hover { - color: #fff; - background: #dc3545; -} - .settings-section { margin-bottom: 1.25rem; padding-bottom: 1rem; @@ -2360,15 +2266,6 @@ body { border-bottom: none; } -.settings-section-title { - font-size: 0.75rem; - font-weight: 600; - color: var(--text); - margin-bottom: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; -} - .form-row { margin-bottom: 0.75rem; } @@ -2403,17 +2300,6 @@ body { border-color: var(--accent); } -.form-row-inline { - display: flex; - gap: 0.5rem; - margin-bottom: 0.75rem; - align-items: center; -} - -.form-row-inline input[type="text"] { - flex: 1; -} - .form-col { flex: 1; } @@ -2447,43 +2333,6 @@ body { font-size: 0.75rem; } -.checkbox-group { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.checkbox-label { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - cursor: pointer; - transition: border-color 0.15s; -} - -.checkbox-label:hover { - border-color: var(--border-light); -} - -.checkbox-label input[type="checkbox"] { - accent-color: var(--accent); -} - -.checkbox-label span:first-of-type { - font-size: 0.8rem; - color: var(--text); -} - -.checkbox-hint { - font-size: 0.7rem; - color: var(--text-muted); - margin-left: auto; -} - .form-hint { display: block; font-size: 0.65rem; @@ -2667,10 +2516,6 @@ body { font-size: 0.75rem; } -.settings-item-full { - grid-column: 1 / -1; -} - .settings-item-label { color: var(--text); white-space: nowrap; @@ -2726,26 +2571,6 @@ body { color: var(--red); } -/* CPU limit status indicator */ -.cpu-limit-status { - font-size: 0.7rem; - padding: 2px 6px; - border-radius: 3px; - background: var(--bg-hover); - color: var(--text-dim); - margin-left: 6px; -} - -.cpu-limit-status.available { - background: rgba(34, 197, 94, 0.2); - color: var(--green); -} - -.cpu-limit-status.unavailable { - background: rgba(239, 68, 68, 0.2); - color: var(--red); -} - /* Event Type Notification Grid */ .event-type-grid { display: grid; @@ -2824,24 +2649,6 @@ body { border-top: 1px solid var(--border); } -.form-actions-stack { - flex-direction: column; - align-items: stretch; -} - -.form-actions-stack button { - width: 100%; -} - -.btn-warning { - background: var(--yellow); - color: #000; -} - -.btn-warning:hover { - background: #d4a107; -} - /* Modal */ .modal { display: none; @@ -3251,13 +3058,6 @@ body { padding: 0.25rem 0.5rem; } -.modal-footer-actions { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - .auto-refresh-label { display: flex; align-items: center; @@ -3297,32 +3097,6 @@ body { font-family: 'SF Mono', Monaco, monospace; } -#runSummaryModal .modal-footer-actions { - gap: 0.5rem; -} - -#runSummaryModal .modal-footer-actions .btn-toolbar { - font-size: 0.7rem; - padding: 0.25rem 0.5rem; -} - -/* Tab summary icon */ -.tab-summary { - opacity: 0; - cursor: pointer; - font-size: 0.85rem; - transition: opacity 0.15s; - margin-right: 0.25rem; -} - -.session-tab:hover .tab-summary { - opacity: 0.5; -} - -.session-tab:hover .tab-summary:hover { - opacity: 1; -} - .modal-warning-text { color: var(--text-dim); font-size: 0.85rem; @@ -3399,35 +3173,12 @@ body { border: 1px solid var(--border); } -.session-dir-display { - padding: 0.5rem 0.75rem; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text-dim); - font-size: 0.8rem; - font-family: 'SF Mono', Monaco, monospace; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - /* Session respawn section in options modal */ .session-respawn-section { padding-top: 0; margin-top: 0; } -.section-divider { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.5rem; - font-size: 0.75rem; - font-weight: 500; - color: var(--text-dim); -} - .session-respawn-status { display: flex; align-items: center; @@ -3459,11 +3210,6 @@ body { } /* Inline checkbox group */ -.checkbox-group-inline { - display: flex; - gap: 1rem; -} - .checkbox-inline { display: flex; align-items: center; @@ -3481,13 +3227,6 @@ body { color: var(--text); } -/* Auto-clear row */ -.auto-clear-row { - display: flex; - align-items: center; - gap: 0.75rem; -} - .input-suffix-sm { display: flex; align-items: center; @@ -3583,11 +3322,6 @@ body { gap: 0.5rem; } -.session-respawn-actions { - display: flex; - gap: 0.5rem; -} - /* Modal Tabs */ .modal-tabs { display: flex; @@ -4349,59 +4083,6 @@ kbd { transition: opacity 0.1s, color 0.1s; } -/* Task Panel */ -.task-panel { - position: fixed; - bottom: var(--toolbar-height); - right: 0.5rem; - width: 350px; - max-width: calc(100vw - 1rem); - /* Performance: GPU-accelerated transforms and isolated paint */ - contain: layout style; - will-change: transform; - max-height: 50vh; - background: var(--bg-card); - border: 1px solid var(--border); - border-bottom: none; - border-radius: 8px 8px 0 0; - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4); - z-index: 100; - transform: translateY(100%); - transition: transform 0.2s ease; - display: flex; - flex-direction: column; -} - -.task-panel.open { - transform: translateY(0); -} - -.task-panel-header { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.6rem 1rem; - border-bottom: 1px solid var(--border); - font-size: 0.8rem; - font-weight: 500; - flex-shrink: 0; -} - -.task-panel-stats { - margin-left: auto; - font-size: 0.7rem; - color: var(--text-dim); - padding: 0.2rem 0.5rem; - background: var(--bg-input); - border-radius: 10px; -} - -.task-panel-body { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - .task-empty { padding: 1rem; text-align: center; @@ -4522,53 +4203,6 @@ kbd { background: var(--border-light); } -/* Process Monitor Panel */ -.process-panel { - position: fixed; - bottom: var(--toolbar-height); - left: 0.5rem; - width: 400px; - max-width: calc(100vw - 1rem); - max-height: 50vh; - background: var(--bg-card); - border: 1px solid var(--border); - border-bottom: none; - border-radius: 8px 8px 0 0; - box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4); - z-index: 100; - transform: translateY(100%); - transition: transform 0.2s ease; - display: flex; - flex-direction: column; -} - -.process-panel.open { - transform: translateY(0); -} - -.process-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.6rem 1rem; - border-bottom: 1px solid var(--border); - font-size: 0.8rem; - font-weight: 500; - flex-shrink: 0; -} - -.process-panel-actions { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.process-panel-body { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - .process-empty { padding: 1rem; text-align: center; @@ -4714,55 +4348,6 @@ kbd { color: var(--text); } -.monitor-tabs { - display: flex; - gap: 0.25rem; -} - -.monitor-tab { - background: transparent; - border: none; - color: var(--text-muted); - padding: 0.3rem 0.6rem; - font-size: 0.75rem; - cursor: pointer; - border-radius: 4px; - transition: all 0.15s; -} - -.monitor-tab:hover { - color: var(--text); - background: var(--bg-hover); -} - -.monitor-tab.active { - color: white; - background: var(--accent); -} - -.monitor-tab-badge { - display: inline-block; - background: var(--success); - color: white; - font-size: 0.6rem; - padding: 0.1rem 0.35rem; - border-radius: 8px; - margin-left: 0.25rem; - font-weight: 600; -} - -.monitor-tab-badge:empty { - display: none; -} - -.monitor-tab-content { - display: none; -} - -.monitor-tab-content.active { - display: flex; -} - .monitor-panel-actions { display: flex; align-items: center; @@ -4864,47 +4449,6 @@ kbd { /* ========== Subagent Panel (Claude Code Background Agents) ========== */ -/* Toolbar button for subagents tab */ -.toolbar-btn-subagents { - display: flex; - align-items: center; - gap: 0.4rem; - background: var(--bg-hover); - border: 1px solid var(--border); - color: var(--text-dim); - font-size: 0.75rem; - padding: 0.4rem 0.75rem; - border-radius: 4px; - cursor: pointer; - transition: all 0.15s; -} - -.toolbar-btn-subagents:hover { - background: var(--bg-card); - border-color: var(--accent); - color: var(--text); -} - -.toolbar-btn-subagents.active { - background: var(--accent); - border-color: var(--accent); - color: white; -} - -.toolbar-badge { - background: var(--accent); - color: white; - font-size: 0.65rem; - padding: 0.1rem 0.4rem; - border-radius: 8px; - min-width: 1.2em; - text-align: center; -} - -.toolbar-badge:empty { - display: none; -} - /* ========== Separate Subagents Panel ========== */ .subagents-panel { position: fixed; @@ -6312,25 +5856,6 @@ kbd { cursor: pointer; } -/* Custom tooltip for file links */ -.file-link-tooltip { - position: fixed; - background: var(--bg-card); - border: 1px solid var(--accent); - border-radius: 4px; - padding: 0.3rem 0.5rem; - font-size: 0.75rem; - color: var(--text); - z-index: 10000; - pointer-events: none; - box-shadow: var(--shadow-md); -} - -.file-link-tooltip::before { - content: "📄 Click to stream: "; - color: var(--accent); -} - /* ========== Ralph Loop Wizard ========== */ /* Wizard Progress Indicator */ @@ -6767,28 +6292,11 @@ kbd { margin: 0 0 1rem 0; } -.plan-skip-btn { - opacity: 0.6; -} - -.plan-skip-btn:hover { - opacity: 1; -} - .plan-error-msg { color: var(--red); margin-bottom: 1rem; } -/* Plan Options (detail level, model) */ -.plan-options { - display: flex; - gap: 1rem; - justify-content: center; - margin-bottom: 1rem; - flex-wrap: wrap; -} - .plan-option-group { display: flex; align-items: center; @@ -6987,10 +6495,6 @@ kbd { color: var(--text); } -.plan-actions-single { - justify-content: center; -} - .plan-skip-hint { font-size: 0.75rem; color: var(--text-muted); @@ -7446,11 +6950,6 @@ kbd { font-weight: 500; } -.mode-changed { - color: var(--accent); - font-weight: 500; -} - /* Plan warnings and gaps display */ #planWarnings { margin-bottom: 0.75rem; @@ -7762,55 +7261,6 @@ kbd { border-color: var(--accent); } -/* Verification criteria tooltip expansion */ -.plan-item-verify-expanded { - position: absolute; - left: 0; - right: 0; - bottom: calc(100% + 4px); - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 4px; - padding: 0.5rem; - font-size: 0.75rem; - color: var(--text); - z-index: 10; - box-shadow: 0 -2px 8px rgba(0,0,0,0.2); - max-width: 300px; -} - -.plan-item-verify-expanded::after { - content: ''; - position: absolute; - bottom: -6px; - left: 50%; - transform: translateX(-50%); - border: 5px solid transparent; - border-top-color: var(--border); -} - -/* Plan toolbar improvements */ -.plan-editor-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.5rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--border); -} - -.plan-toolbar-left { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.plan-toolbar-right { - display: flex; - gap: 0.5rem; - align-items: center; -} - /* Plan item inline actions */ .plan-item-actions { display: flex; @@ -7909,36 +7359,6 @@ kbd { border-color: var(--accent); } -/* Plan version indicator */ -.plan-version-indicator { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.75rem; - color: var(--text-muted); - padding: 0.25rem 0.5rem; - background: var(--bg-input); - border-radius: 4px; -} - -.plan-version-badge { - background: var(--accent); - color: white; - padding: 0.1rem 0.3rem; - border-radius: 3px; - font-size: 0.65rem; - font-weight: 500; -} - -.plan-version-history { - cursor: pointer; - color: var(--accent); -} - -.plan-version-history:hover { - text-decoration: underline; -} - /* Plan history dropdown */ .plan-history-dropdown { position: absolute; @@ -8061,27 +7481,6 @@ kbd { border-color: var(--accent); } -/* Phrase Input Group */ -.phrase-input-group { - position: relative; -} - -.phrase-input-group input { - width: 100%; - padding: 0.5rem 0.75rem; - background: var(--bg-input); - border: 1px solid var(--border); - border-radius: 4px; - color: var(--text); - font-size: 0.85rem; - font-family: 'SF Mono', Monaco, 'Courier New', monospace; -} - -.phrase-input-group input:focus { - outline: none; - border-color: var(--accent); -} - /* Iteration Presets */ .iteration-presets { display: flex; diff --git a/src/web/schemas.ts b/src/web/schemas.ts index 1b34aee8..a6c10aaf 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -8,12 +8,10 @@ */ import { z } from 'zod'; +import { SAFE_PATH_PATTERN } from '../utils/regex-patterns.js'; // ========== Path Validation ========== -/** Regex to validate working directory paths (no shell metacharacters) — matches tmux-manager.ts */ -const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/; - /** Validate a path string: no shell metacharacters, no traversal, must be absolute */ export function isValidWorkingDir(p: string): boolean { if (!p || !p.startsWith('/')) return false; @@ -80,15 +78,6 @@ export const RunPromptSchema = z.object({ prompt: z.string().min(1).max(100000), }); -/** - * Schema for POST /api/sessions/:id/input - * Sends input to an interactive session. - */ -export const SessionInputSchema = z.object({ - input: z.string(), - useMux: z.boolean().optional(), -}); - /** * Schema for POST /api/sessions/:id/resize * Resizes a session's terminal. diff --git a/src/web/server.ts b/src/web/server.ts index 10daa0eb..6a5045fb 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -13,7 +13,7 @@ import Fastify, { FastifyInstance, FastifyReply } from 'fastify'; import fastifyCompress from '@fastify/compress'; import fastifyStatic from '@fastify/static'; -import path, { join, dirname, resolve, relative, isAbsolute } from 'node:path'; +import { join, dirname, resolve, relative, isAbsolute } from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync, statSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync } from 'node:fs'; import fs from 'node:fs/promises'; @@ -89,6 +89,7 @@ import { RespawnEnableSchema, } from './schemas.js'; import { StaleExpirationMap } from '../utils/index.js'; +import { MAX_CONCURRENT_SESSIONS } from '../config/map-limits.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -127,8 +128,6 @@ const SESSIONS_LIST_CACHE_TTL = 1000; const SCHEDULED_CLEANUP_INTERVAL = 5 * 60 * 1000; // Completed scheduled runs max age (1 hour) const SCHEDULED_RUN_MAX_AGE = 60 * 60 * 1000; -// Maximum concurrent sessions to prevent resource exhaustion -const MAX_CONCURRENT_SESSIONS = 50; // SSE client health check interval (every 30 seconds) const SSE_HEALTH_CHECK_INTERVAL = 30 * 1000; // Maximum allowed input length for session write (64KB) @@ -1461,7 +1460,7 @@ export class WebServer extends EventEmitter { } const content = session.ralphTracker.generateFixPlanMarkdown(); - const filePath = path.join(workingDir, '@fix_plan.md'); + const filePath = join(workingDir, '@fix_plan.md'); try { await fs.writeFile(filePath, content, 'utf-8'); @@ -1491,7 +1490,7 @@ export class WebServer extends EventEmitter { return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Session has no working directory'); } - const filePath = path.join(workingDir, '@fix_plan.md'); + const filePath = join(workingDir, '@fix_plan.md'); try { const content = await fs.readFile(filePath, 'utf-8'); @@ -1534,7 +1533,7 @@ export class WebServer extends EventEmitter { return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Session has no working directory'); } - const filePath = path.join(workingDir, '@ralph_prompt.md'); + const filePath = join(workingDir, '@ralph_prompt.md'); try { await fs.writeFile(filePath, content, 'utf-8'); @@ -5335,12 +5334,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`; // Start stats collection for mux sessions this.mux.startStatsCollection(STATS_COLLECTION_INTERVAL_MS); + } - // Start mouse mode sync (tmux only) — toggles mouse on/off based on pane count. - // Mouse off = native xterm.js selection; mouse on = tmux pane clicking (split layouts). - if ('startMouseModeSync' in this.mux) { - (this.mux as { startMouseModeSync: (ms?: number) => void }).startMouseModeSync(); - } + // Start mouse mode sync (tmux only) — toggles mouse on/off based on pane count. + // Mouse off = native xterm.js selection; mouse on = tmux pane clicking (split layouts). + // Always start, even with no sessions — new sessions may be created later. + if ('startMouseModeSync' in this.mux) { + (this.mux as { startMouseModeSync: (ms?: number) => void }).startMouseModeSync(); } if (dead.length > 0) {