chore: rename Claudeman to Codeman

Full product rename across 109 files (~834 occurrences):
- Env vars: CLAUDEMAN_* → CODEMAN_*
- Data dirs: ~/.claudeman/ → ~/.codeman/, ~/claudeman-cases/ → ~/codeman-cases/
- tmux prefix: claudeman- → codeman-
- localStorage: claudeman-* → codeman-*
- Package/CLI: claudeman → codeman
- GitHub repo: Ark0N/Claudeman → Ark0N/Codeman
- systemd service: claudeman-web → codeman-web
- Class: ClaudemanApp → CodemanApp

Migration infrastructure for seamless transition:
- state-store.ts: auto-migrates data directories on startup
- tmux-manager.ts: dual-prefix detection (legacy claudeman- sessions)
- app.js: localStorage key migration (preserves old keys)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-26 16:44:34 +01:00
co-authored by Claude Opus 4.6
parent 3ccd22161c
commit 3795c45cc1
109 changed files with 711 additions and 679 deletions
+30 -30
View File
@@ -9,13 +9,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
| Dev server | `npx tsx src/index.ts web` |
| Type check | `tsc --noEmit` |
| Single test | `npx vitest run test/<file>.test.ts` |
| Production | `npm run build && systemctl --user restart claudeman-web` |
| Production | `npm run build && systemctl --user restart codeman-web` |
## CRITICAL: Session Safety
**You may be running inside a Claudeman-managed tmux session.** Before killing ANY tmux or Claude process:
**You may be running inside a Codeman-managed tmux session.** Before killing ANY tmux or Claude process:
1. Check: `echo $CLAUDEMAN_TMUX` - if `1`, you're in a managed session
1. Check: `echo $CODEMAN_TMUX` - if `1`, you're in a managed session
2. **NEVER** run `tmux kill-session`, `pkill tmux`, or `pkill claude` without confirming
3. Use the web UI or `./scripts/tmux-manager.sh` instead of direct kill commands
@@ -33,13 +33,13 @@ The production server caches static files for 1 hour (`maxAge: '1h'` in `server.
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`
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart codeman-web`
**Version**: 0.1650 (must match `package.json`)
## Project Overview
Claudeman is a Claude Code session manager with web interface and autonomous Ralph Loop. Spawns Claude CLI via PTY, streams via SSE, supports respawn cycling for 24+ hour autonomous runs.
Codeman is a Claude Code session manager with web interface and autonomous Ralph Loop. Spawns Claude CLI via PTY, streams via SSE, supports respawn cycling for 24+ hour autonomous runs.
**Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, node-pty, xterm.js
@@ -63,24 +63,24 @@ npx tsx src/index.ts web --https # With TLS (only needed for remote access)
npm run typecheck # Type check
tsc --noEmit --watch # Continuous type checking
# Testing (NEVER run full suite from inside Claudeman — kills tmux sessions)
# npx vitest run # ALL tests — DANGEROUS inside Claudeman
# Testing (NEVER run full suite from inside Codeman — kills tmux sessions)
# npx vitest run # ALL tests — DANGEROUS inside Codeman
npx vitest run test/<file>.test.ts # Single file (SAFE)
npx vitest run -t "pattern" # Tests matching name
npm run test:coverage # With coverage report
# Production
npm run build
systemctl --user restart claudeman-web
journalctl --user -u claudeman-web -f
systemctl --user restart codeman-web
journalctl --user -u codeman-web -f
```
## Common Gotchas
- **`npm run dev` is NOT the web server** — it shows CLI help. Use `npx tsx src/index.ts web`
- **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.
- **Don't kill tmux sessions blindly** — Check `$CODEMAN_TMUX` first; you might be inside one
- **Never run full test suite** — `npx vitest run` spawns/kills tmux sessions and will crash your Codeman 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
@@ -103,7 +103,7 @@ journalctl --user -u claudeman-web -f
| `src/mux-factory.ts` | Create tmux multiplexer instance |
| `src/tmux-manager.ts` | tmux session management |
| `src/session-manager.ts` | Session lifecycle, cleanup |
| `src/state-store.ts` | State persistence to `~/.claudeman/state.json` |
| `src/state-store.ts` | State persistence to `~/.codeman/state.json` |
| `src/respawn-controller.ts` | State machine for autonomous cycling |
| `src/ralph-tracker.ts` | Detects `<promise>PHRASE</promise>`, todos |
| `src/ralph-loop.ts` | Autonomous task execution loop (polls queue, assigns tasks) |
@@ -120,7 +120,7 @@ journalctl --user -u claudeman-web -f
| `src/bash-tool-parser.ts` | Parses Claude's bash tool invocations from output |
| `src/transcript-watcher.ts` | Watches Claude's transcript files for changes |
| `src/hooks-config.ts` | Manages `.claude/settings.local.json` hook configuration |
| `src/session-lifecycle-log.ts` | Append-only JSONL audit log at `~/.claudeman/session-lifecycle.jsonl` |
| `src/session-lifecycle-log.ts` | Append-only JSONL audit log at `~/.codeman/session-lifecycle.jsonl` |
| `src/image-watcher.ts` | Watches for image file creation (screenshots, etc.) |
| `src/file-stream-manager.ts` | Manages `tail -f` processes for live log viewing |
| `src/plan-orchestrator.ts` | Multi-agent plan generation with research and planning phases |
@@ -169,7 +169,7 @@ journalctl --user -u claudeman-web -f
1. Session spawns `claude --dangerously-skip-permissions` via node-pty
2. PTY output buffered, ANSI stripped, parsed for JSON messages
3. WebServer broadcasts to SSE clients at `/api/events`
4. State persists to `~/.claudeman/state.json` via StateStore
4. State persists to `~/.codeman/state.json` via StateStore
### Key Patterns
@@ -214,11 +214,11 @@ The frontend is a single 16K-line vanilla JS file with these key systems:
### Security
- **HTTP Basic Auth**: Optional via `CLAUDEMAN_USERNAME`/`CLAUDEMAN_PASSWORD` env vars
- **HTTP Basic Auth**: Optional via `CODEMAN_USERNAME`/`CODEMAN_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
- **Env var allowlist**: Only `CLAUDE_CODE_*` prefixes allowed; blocks `PATH`, `LD_PRELOAD`, `NODE_OPTIONS`, `CODEMAN_*` keys
- **File streaming TOCTOU protection**: `FileStreamManager` calls `realpathSync()` twice (at validation and before spawn) to catch symlink swaps
### SSE Event Categories
@@ -269,9 +269,9 @@ The frontend is a single 16K-line vanilla JS file with these key systems:
| File | Purpose |
|------|---------|
| `~/.claudeman/state.json` | Sessions, settings, tokens, respawn config |
| `~/.claudeman/mux-sessions.json` | Tmux session metadata for recovery |
| `~/.claudeman/settings.json` | User preferences |
| `~/.codeman/state.json` | Sessions, settings, tokens, respawn config |
| `~/.codeman/mux-sessions.json` | Tmux session metadata for recovery |
| `~/.codeman/settings.json` | User preferences |
## Default Settings
@@ -281,7 +281,7 @@ UI defaults are set in `src/web/public/app.js` using `??` fallbacks. To change d
## Testing
**CRITICAL: You are running inside a Claudeman-managed tmux session.** Never run `npx vitest run` (full suite) — it spawns/kills tmux sessions and will crash your own session. Instead:
**CRITICAL: You are running inside a Codeman-managed tmux session.** Never run `npx vitest run` (full suite) — it spawns/kills tmux sessions and will crash your own session. Instead:
```bash
# Safe: run individual test files
@@ -290,7 +290,7 @@ npx vitest run test/<specific-file>.test.ts
# Safe: run tests matching a pattern
npx vitest run -t "pattern"
# DANGEROUS from inside Claudeman — will kill your tmux session:
# DANGEROUS from inside Codeman — will kill your tmux session:
# npx vitest run ← DON'T DO THIS
```
@@ -306,10 +306,10 @@ npx vitest run -t "pattern"
## Screenshots ("sc")
When the user says "check the sc", "screenshot", or "sc", they mean uploaded screenshots from their mobile device. Screenshots are saved to `~/.claudeman/screenshots/` and uploaded via `/upload.html` on the Claudeman web UI. To view them, use the Read tool on the image files:
When the user says "check the sc", "screenshot", or "sc", they mean uploaded screenshots from their mobile device. Screenshots are saved to `~/.codeman/screenshots/` and uploaded via `/upload.html` on the Codeman web UI. To view them, use the Read tool on the image files:
```bash
ls ~/.claudeman/screenshots/ # List uploaded screenshots
ls ~/.codeman/screenshots/ # List uploaded screenshots
# Then use Read tool on individual files — Claude Code can view images natively
```
@@ -322,7 +322,7 @@ tmux list-sessions # List tmux sessions
tmux attach-session -t <name> # Attach (Ctrl+B D to detach)
curl localhost:3000/api/sessions # Check sessions
curl localhost:3000/api/status | jq # Full app state
cat ~/.claudeman/state.json | jq # View persisted state
cat ~/.codeman/state.json | jq # View persisted state
curl localhost:3000/api/subagents # List background agents
curl localhost:3000/api/sessions/:id/run-summary | jq # Session timeline
```
@@ -337,7 +337,7 @@ curl localhost:3000/api/sessions/:id/run-summary | jq # Session timeline
| Respawn not triggering | Session settings → Respawn enabled? | Enable respawn, check idle timeout config |
| Terminal blank on tab switch | Network tab for `/api/sessions/:id/buffer` | Check session exists, restart server |
| Tests failing on session limits | `tmux list-sessions \| wc -l` | Clean up: `tmux list-sessions \| grep test \| awk -F: '{print $1}' \| xargs -I{} tmux kill-session -t {}` |
| State not persisting | `cat ~/.claudeman/state.json` | Check file permissions, disk space |
| State not persisting | `cat ~/.codeman/state.json` | Check file permissions, disk space |
## Performance Constraints
@@ -352,7 +352,7 @@ The app must stay fast with 20 sessions and 50 agent windows:
## Terminal Anti-Flicker System
Claude Code uses Ink (React for terminals), which redraws the screen on every state change. Claudeman implements a 6-layer anti-flicker pipeline for smooth 60fps output:
Claude Code uses Ink (React for terminals), which redraws the screen on every state change. Codeman implements a 6-layer anti-flicker pipeline for smooth 60fps output:
```
PTY Output → Server Batching (16-50ms) → DEC 2026 Wrap → SSE → Client rAF → xterm.js
@@ -395,14 +395,14 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| **Terminal anti-flicker** | `docs/terminal-anti-flicker.md` |
| **API routes** | `src/web/server.ts:buildServer()` or README.md (full endpoint tables) |
| **SSE events** | Search `broadcast(` in `server.ts` |
| **CLI commands** | `claudeman --help` |
| **CLI commands** | `codeman --help` |
| **Frontend patterns** | `src/web/public/app.js` (subagent windows, notifications) |
| **Session statuses** | `SessionStatus` type in `src/types.ts` |
| **Error codes** | `createErrorResponse()` in `src/types.ts` |
| **Test utilities** | `test/respawn-test-utils.ts` |
| **Memory leak patterns** | `test/memory-leak-prevention.test.ts` |
| **Keyboard shortcuts** | README.md or App Settings in web UI |
| **Mobile/SSH access** | README.md (Claudeman Sessions / `sc` command) |
| **Mobile/SSH access** | README.md (Codeman Sessions / `sc` command) |
| **Plan orchestrator** | `src/plan-orchestrator.ts` file header |
| **Agent prompts** | `src/prompts/` directory |
| **Agent Teams (experimental)** | `agent-teams/README.md`, `agent-teams/design.md` |
@@ -444,7 +444,7 @@ Use `LRUMap` for bounded caches with eviction, `StaleExpirationMap` for TTL-base
| `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 |
| `scripts/codeman-web.service` | systemd service file for production deployment |
## Memory Leak Prevention
@@ -464,7 +464,7 @@ Run `npx vitest run test/memory-leak-prevention.test.ts` to verify patterns.
## Common Workflows
**Investigating a bug**: Start dev server (`npx tsx src/index.ts web`), reproduce in browser, check terminal output and `~/.claudeman/state.json` for clues.
**Investigating a bug**: Start dev server (`npx tsx src/index.ts web`), reproduce in browser, check terminal output and `~/.codeman/state.json` for clues.
**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()`.
+16 -16
View File
@@ -1,5 +1,5 @@
<p align="center">
<img src="docs/images/claudeman-title.svg" alt="Claudeman" height="60">
<img src="docs/images/codeman-title.svg" alt="Codeman" height="60">
</p>
<h2 align="center">The missing control plane for Claude Code</h2>
@@ -17,7 +17,7 @@
</p>
<p align="center">
<img src="docs/images/subagent-demo.gif" alt="Claudeman — parallel subagent visualization" width="900">
<img src="docs/images/subagent-demo.gif" alt="Codeman — parallel subagent visualization" width="900">
</p>
---
@@ -25,19 +25,19 @@
## Quick Start - Installation
```bash
curl -fsSL https://raw.githubusercontent.com/Ark0N/claudeman/master/install.sh | bash
curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | bash
```
This installs Node.js, tmux, and Claude CLI if missing, clones Claudeman to `~/.claudeman/app`, builds it. After install:
This installs Node.js, tmux, and Claude CLI if missing, clones Codeman to `~/.codeman/app`, builds it. After install:
```bash
claudeman web
codeman web
# Open http://localhost:3000 — press Ctrl+Enter to start your first session
```
**Update to latest version:**
```bash
curl -fsSL https://raw.githubusercontent.com/Ark0N/claudeman/master/install.sh | bash -s update
curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | bash -s update
```
<details>
@@ -45,12 +45,12 @@ curl -fsSL https://raw.githubusercontent.com/Ark0N/claudeman/master/install.sh |
**Linux (systemd):**
```bash
mkdir -p ~/.config/systemd/user && printf '[Unit]\nDescription=Claudeman Web Server\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=%s %s/dist/index.js web\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=default.target\n' "$(which node)" "$HOME/.claudeman/app" > ~/.config/systemd/user/claudeman-web.service && systemctl --user daemon-reload && systemctl --user enable --now claudeman-web && loginctl enable-linger $USER
mkdir -p ~/.config/systemd/user && printf '[Unit]\nDescription=Codeman Web Server\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=%s %s/dist/index.js web\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=default.target\n' "$(which node)" "$HOME/.codeman/app" > ~/.config/systemd/user/codeman-web.service && systemctl --user daemon-reload && systemctl --user enable --now codeman-web && loginctl enable-linger $USER
```
**macOS (launchd):**
```bash
mkdir -p ~/Library/LaunchAgents && printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>com.claudeman.web</string><key>ProgramArguments</key><array><string>%s</string><string>%s/dist/index.js</string><string>web</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>/tmp/claudeman.log</string><key>StandardErrorPath</key><string>/tmp/claudeman.log</string></dict></plist>\n' "$(which node)" "$HOME/.claudeman/app" > ~/Library/LaunchAgents/com.claudeman.web.plist && launchctl load ~/Library/LaunchAgents/com.claudeman.web.plist
mkdir -p ~/Library/LaunchAgents && printf '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>com.codeman.web</string><key>ProgramArguments</key><array><string>%s</string><string>%s/dist/index.js</string><string>web</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>/tmp/codeman.log</string><key>StandardErrorPath</key><string>/tmp/codeman.log</string></dict></plist>\n' "$(which node)" "$HOME/.codeman/app" > ~/Library/LaunchAgents/com.codeman.web.plist && launchctl load ~/Library/LaunchAgents/com.codeman.web.plist
```
</details>
@@ -58,10 +58,10 @@ mkdir -p ~/Library/LaunchAgents && printf '<?xml version="1.0" encoding="UTF-8"?
<summary><strong>Windows (WSL)</strong></summary>
```powershell
wsl bash -c "curl -fsSL https://raw.githubusercontent.com/Ark0N/claudeman/master/install.sh | bash"
wsl bash -c "curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | bash"
```
Claudeman requires tmux, so Windows users need [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). If you don't have WSL yet: run `wsl --install` in an admin PowerShell, reboot, open Ubuntu, then install Claude CLI inside WSL: `curl -fsSL https://claude.ai/install.sh | bash`. After installing, `http://localhost:3000` is accessible from your Windows browser.
Codeman requires tmux, so Windows users need [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). If you don't have WSL yet: run `wsl --install` in an admin PowerShell, reboot, open Ubuntu, then install Claude CLI inside WSL: `curl -fsSL https://claude.ai/install.sh | bash`. After installing, `http://localhost:3000` is accessible from your Windows browser.
</details>
---
@@ -74,7 +74,7 @@ The most responsive Claude Code experience on any phone. Full xterm.js terminal
<tr>
<td rowspan="8" width="320"><img src="docs/screenshots/mobile-keyboard-open.png" alt="Mobile — keyboard open" width="300"></td>
<th>Terminal Apps</th>
<th>Claudeman Mobile</th>
<th>Codeman Mobile</th>
</tr>
<tr><td>200-300ms input lag over remote</td><td><b>Local echo — instant feedback</b></td></tr>
<tr><td>Tiny text, no context</td><td>Full xterm.js terminal</td></tr>
@@ -94,7 +94,7 @@ The most responsive Claude Code experience on any phone. Full xterm.js terminal
- **Native momentum scrolling** — `-webkit-overflow-scrolling: touch` for buttery scroll
```bash
claudeman web --https
codeman web --https
# Open on your phone: https://<your-ip>:3000
```
@@ -104,7 +104,7 @@ claudeman web --https
## Live Agent Visualization
Watch Claude's background agents work in real-time. Claudeman monitors every `Task` tool invocation and displays each agent in a draggable floating window with animated Matrix-style connection lines back to the parent session.
Watch Claude's background agents work in real-time. Codeman monitors every `Task` tool invocation and displays each agent in a draggable floating window with animated Matrix-style connection lines back to the parent session.
<p align="center">
<img src="docs/images/subagent-spawn.png" alt="Subagent Visualization" width="900">
@@ -120,7 +120,7 @@ Watch Claude's background agents work in real-time. Claudeman monitors every `Ta
## Zero-Lag Input Overlay
When accessing Claude Code remotely (VPN, Tailscale, SSH tunnel), every keystroke normally takes 200-300ms to round-trip. Claudeman implements a **Mosh-inspired local echo system** that makes typing feel instant regardless of latency.
When accessing Claude Code remotely (VPN, Tailscale, SSH tunnel), every keystroke normally takes 200-300ms to round-trip. Codeman implements a **Mosh-inspired local echo system** that makes typing feel instant regardless of latency.
A pixel-perfect DOM overlay inside xterm.js renders keystrokes at 0ms. Background forwarding silently sends every character to the PTY in 50ms debounced batches, so Tab completion, `Ctrl+R` history search, and all shell features work normally. When the server echo arrives 200-300ms later, the overlay seamlessly disappears and the real terminal text takes over — the transition is invisible.
@@ -186,7 +186,7 @@ Click the chart icon on any session tab to see a timeline of everything that hap
### Zero-Flicker Terminal
Claude Code uses Ink (React for terminals) which redraws the entire screen on every state change. Claudeman implements a 6-layer anti-flicker pipeline for smooth 60fps output across all sessions:
Claude Code uses Ink (React for terminals) which redraws the entire screen on every state change. Codeman implements a 6-layer anti-flicker pipeline for smooth 60fps output across all sessions:
```
PTY Output → 16ms Server Batch → DEC 2026 Wrap → SSE → Client rAF → xterm.js (60fps)
@@ -268,7 +268,7 @@ Single-digit selection (1-9), color-coded status, token counts, auto-refresh. De
```mermaid
flowchart TB
subgraph Claudeman["CLAUDEMAN"]
subgraph Codeman["CODEMAN"]
subgraph Frontend["Frontend Layer"]
UI["Web UI<br/><small>xterm.js + Agent Windows</small>"]
API["REST API<br/><small>Fastify</small>"]
+5 -5
View File
@@ -68,7 +68,7 @@ Note: Teammate transcripts appear in the **standard subagent directory** under t
}
```
Key fields: `agentId` format is `{name}@{teamName}`, `leadSessionId` links to Claudeman session, `backendType` indicates display mode, `color` for UI theming.
Key fields: `agentId` format is `{name}@{teamName}`, `leadSessionId` links to Codeman session, `backendType` indicates display mode, `color` for UI theming.
### Task file format (verified)
@@ -122,7 +122,7 @@ Message types observed:
**Teammates are IN-PROCESS THREADS, not separate OS processes.**
In `in-process` mode (the default), all teammates run as threads within the single `claude` process. Only 1 claude process exists per Claudeman session, regardless of team size.
In `in-process` mode (the default), all teammates run as threads within the single `claude` process. Only 1 claude process exists per Codeman session, regardless of team size.
This means:
- No separate PIDs to track per teammate
@@ -138,7 +138,7 @@ This means:
| **tmux** | `--teammate-mode tmux` | Split panes | tmux installed |
| **iTerm2** | Auto-detected | Native split panes | iTerm2 + `it2` CLI |
**For Claudeman: use `in-process` only.** Claudeman manages its own tmux sessions externally.
**For Codeman: use `in-process` only.** Codeman manages its own tmux sessions externally.
**In-process UI elements:**
- Status bar: `@main @teammate1 @teammate2 ...` with `shift+↑ to expand`
@@ -159,7 +159,7 @@ Fires when a task is being marked complete.
- Exit code 0: Allow completion
- Exit code 2: Prevent completion, send feedback
These are configured in `.claude/settings.local.json` alongside existing Claudeman hooks.
These are configured in `.claude/settings.local.json` alongside existing Codeman hooks.
## Subagent-Watcher Compatibility (Verified)
@@ -168,7 +168,7 @@ These are configured in `.claude/settings.local.json` alongside existing Claudem
~/.claude/projects/{hash}/{leadSessionId}/subagents/agent-{id}.jsonl
```
Claudeman's existing `subagent-watcher.ts` discovers them automatically. They appear in `/api/subagents` with status "active".
Codeman's existing `subagent-watcher.ts` discovers them automatically. They appear in `/api/subagents` with status "active".
**Distinguishing teammates from regular subagents:**
- Description field starts with `<teammate-message teammate_id= team`
+6 -6
View File
@@ -1,4 +1,4 @@
# Claudeman Agent Teams Integration — Design (Approach C: Hybrid)
# Codeman Agent Teams Integration — Design (Approach C: Hybrid)
> Updated 2026-02-12 with experiment findings. See `experiment-log.md` for raw data.
@@ -15,7 +15,7 @@ Monitors `~/.claude/teams/` for team creation/removal and tracks active teams.
**Discovery mechanism:**
- Poll `~/.claude/teams/` for directories (team names) every 3-5 seconds
- When found: parse `config.json` to get:
- `leadSessionId` → map to Claudeman session
- `leadSessionId` → map to Codeman session
- `members` array → teammate names, agentIds, colors, models
- Watch for directory deletion (cleanup signal)
@@ -25,7 +25,7 @@ Monitors `~/.claude/teams/` for team creation/removal and tracks active teams.
- Teammate transcripts are at `subagents/agent-{id}.jsonl` (standard subagent path), NOT separate session transcripts
**Association:**
- `config.json.leadSessionId` → Claudeman session ID (direct match!)
- `config.json.leadSessionId` → Codeman session ID (direct match!)
- Each member's `agentId` (e.g., `fs-researcher@research-watchers`) → links to subagent files
- `agentType: "team-lead"` vs `"general-purpose"` distinguishes lead from teammates
@@ -37,7 +37,7 @@ Monitors `~/.claude/teams/` for team creation/removal and tracks active teams.
### 2. Team-Aware Idle Detection (HIGHEST PRIORITY)
**Problem (confirmed by experiment):** Lead session shows status "idle" in Claudeman while teammates are actively working. Token count continues climbing but Claudeman thinks the session is inactive.
**Problem (confirmed by experiment):** Lead session shows status "idle" in Codeman while teammates are actively working. Token count continues climbing but Codeman thinks the session is inactive.
**Solution:**
- Before declaring a session idle, check if it's a team lead
@@ -125,7 +125,7 @@ Communication uses filesystem inbox files at `~/.claude/teams/{name}/inboxes/{te
**Potential for interaction (not tested, future work):**
- Write to teammate inbox files to inject messages
- Must respect `.json.lock` directory locking protocol
- Could enable "nudge" or "redirect" functionality from Claudeman UI
- Could enable "nudge" or "redirect" functionality from Codeman UI
## Answered Questions (from experiment)
@@ -161,7 +161,7 @@ Communication uses filesystem inbox files at `~/.claude/teams/{name}/inboxes/{te
3. **Teammate badge in subagent windows** — mark teammate subagents with name/color
4. **Team tasks API + UI** — `GET /api/sessions/:id/team-tasks` + task list panel
5. **Inbox monitoring** — watch inbox files, display message timeline
6. **TeammateIdle/TaskCompleted hooks** — add to Claudeman's hooks config generator
6. **TeammateIdle/TaskCompleted hooks** — add to Codeman's hooks config generator
## What We DON'T Need to Build
+21 -21
View File
@@ -1,7 +1,7 @@
# Agent Teams Experiment Log
> Experiment date: 2026-02-12
> Test case: `~/claudeman-cases/agent-teams-test/`
> Test case: `~/codeman-cases/agent-teams-test/`
> Team name: `research-watchers`
> Teammates: 3 (fs-researcher, perf-researcher, api-researcher)
> Lead session: `461daa80-94ec-4e5e-a1bb-0518f78311bc`
@@ -56,7 +56,7 @@ Name the team research-watchers.
Subagent transcripts also appear in the standard subagent directory:
```
~/.claude/projects/-home-arkon-claudeman-cases-agent-teams-test/
~/.claude/projects/-home-arkon-codeman-cases-agent-teams-test/
└── 461daa80.../
├── 461daa80...jsonl # Lead session transcript
└── subagents/
@@ -102,7 +102,7 @@ Key observations:
- **File locking** via `.json.lock` directories (mkdir-based atomic lock, created then deleted)
- Lead also has an inbox (`team-lead.json`) for receiving messages FROM teammates
**Implication for Claudeman:** We CAN intercept messages by watching inbox JSON files! We can also potentially inject messages by writing to inbox files.
**Implication for Codeman:** We CAN intercept messages by watching inbox JSON files! We can also potentially inject messages by writing to inbox files.
---
@@ -119,7 +119,7 @@ Teammates create transcript files at:
This is the **exact same path pattern** that regular Task tool subagents use. The existing `subagent-watcher.ts` successfully discovers them.
Claudeman's `/api/subagents` endpoint returned them with status "active":
Codeman's `/api/subagents` endpoint returned them with status "active":
```
Agent: ae50544 Status: active Tools: 8 Model: claude-opus-4-6
Desc: <teammate-message teammate_id= team
@@ -156,7 +156,7 @@ Agent: a29de32 Status: active Tools: 7 Model: claude-opus-4-6
"model": "claude-opus-4-6",
"joinedAt": 1770875105373,
"tmuxPaneId": "",
"cwd": "/home/arkon/claudeman-cases/agent-teams-test",
"cwd": "/home/arkon/codeman-cases/agent-teams-test",
"subscriptions": []
},
{
@@ -169,7 +169,7 @@ Agent: a29de32 Status: active Tools: 7 Model: claude-opus-4-6
"planModeRequired": false,
"joinedAt": 1770875126680,
"tmuxPaneId": "in-process",
"cwd": "/home/arkon/claudeman-cases/agent-teams-test",
"cwd": "/home/arkon/codeman-cases/agent-teams-test",
"subscriptions": [],
"backendType": "in-process"
},
@@ -183,7 +183,7 @@ Agent: a29de32 Status: active Tools: 7 Model: claude-opus-4-6
"planModeRequired": false,
"joinedAt": 1770875130344,
"tmuxPaneId": "in-process",
"cwd": "/home/arkon/claudeman-cases/agent-teams-test",
"cwd": "/home/arkon/codeman-cases/agent-teams-test",
"subscriptions": [],
"backendType": "in-process"
},
@@ -197,7 +197,7 @@ Agent: a29de32 Status: active Tools: 7 Model: claude-opus-4-6
"planModeRequired": false,
"joinedAt": 1770875134997,
"tmuxPaneId": "in-process",
"cwd": "/home/arkon/claudeman-cases/agent-teams-test",
"cwd": "/home/arkon/codeman-cases/agent-teams-test",
"subscriptions": [],
"backendType": "in-process"
}
@@ -280,21 +280,21 @@ Evidence supporting external writes:
---
## Question 7: What happens to Claudeman's idle detection with active teammates?
## Question 7: What happens to Codeman's idle detection with active teammates?
**Expected:** Lead may appear idle while teammates work
**Actual: Lead stays "idle" in Claudeman's view, but terminal shows active status**
**Actual: Lead stays "idle" in Codeman's view, but terminal shows active status**
Observations:
- Claudeman session status showed `"idle"` throughout the experiment
- Codeman session status showed `"idle"` throughout the experiment
- The terminal output continued updating (task list checkboxes, teammate progress messages)
- Lead displayed "Befuddling..." spinner while waiting for teammates
- Token count climbed from 27k → 33k during the experiment
- The `stop` hook DID fire at the end when the team was cleaned up
**Implication:** Current idle detection may trigger prematurely if:
- It only checks Claudeman's session status (which stays "idle")
- It only checks Codeman's session status (which stays "idle")
- It doesn't account for active teammates
**What we need:** Check `~/.claude/teams/*/config.json` for active members before declaring idle.
@@ -308,7 +308,7 @@ Observations:
**Actual: ZERO separate processes! Teammates are in-process threads.**
```
# Only 2 claude processes (both Claudeman sessions, none for teammates):
# Only 2 claude processes (both Codeman sessions, none for teammates):
25405 claude --dangerously-skip-permissions --session-id 236f004f... (our main session)
383633 claude --dangerously-skip-permissions --session-id 461daa80... (test session + 3 teammates)
@@ -327,7 +327,7 @@ claude(383633)─┬─{claude}(383635)
---
## Question 9: Do teammates inherit Claudeman env vars (hook events)?
## Question 9: Do teammates inherit Codeman env vars (hook events)?
**Expected:** Yes, if child processes
@@ -335,10 +335,10 @@ claude(383633)─┬─{claude}(383635)
Since teammates are threads in the lead's process (PID 383633), they share the exact same environment:
```
CLAUDEMAN_SCREEN=1
CLAUDEMAN_SESSION_ID=461daa80-94ec-4e5e-a1bb-0518f78311bc
CLAUDEMAN_SCREEN_NAME=claudeman-461daa80
CLAUDEMAN_API_URL=http://localhost:3000
CODEMAN_SCREEN=1
CODEMAN_SESSION_ID=461daa80-94ec-4e5e-a1bb-0518f78311bc
CODEMAN_SCREEN_NAME=codeman-461daa80
CODEMAN_API_URL=http://localhost:3000
```
The `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` env var was set via `settings.local.json`'s `env` key, which Claude Code reads at startup and sets on its process.
@@ -358,7 +358,7 @@ Teammates create transcript files in the standard subagent path:
~/.claude/projects/{hash}/{leadSessionId}/subagents/agent-{id}.jsonl
```
Claudeman's `/api/subagents` endpoint returned all 3 teammates as active subagents. They're indistinguishable from regular Task tool subagents except:
Codeman's `/api/subagents` endpoint returned all 3 teammates as active subagents. They're indistinguishable from regular Task tool subagents except:
1. Their `description` field starts with `<teammate-message teammate_id= team`
2. They can be cross-referenced with `~/.claude/teams/{name}/config.json`
3. They tend to be longer-lived than regular subagents
@@ -419,7 +419,7 @@ Claudeman's `/api/subagents` endpoint returned all 3 teammates as active subagen
- Standard bypass permissions and token count still visible
**Subagent floating windows:**
- Teammates DID appear as subagent floating windows in Claudeman's web UI
- Teammates DID appear as subagent floating windows in Codeman's web UI
- They show the standard subagent info (model, tool calls, description)
- Sub-subagents (teammates' own Task tool usage) also appear
@@ -443,7 +443,7 @@ Claudeman's `/api/subagents` endpoint returned all 3 teammates as active subagen
7. **Sub-subagents** — teammates can spawn their own Task tool subagents (3-level hierarchy).
8. **`teammateMode` is NOT a valid settings key** — display mode defaults to `in-process`.
### Design implications for Claudeman
### Design implications for Codeman
1. **TeamWatcher can be simple** — just poll `~/.claude/teams/` for directories + parse config.json
2. **Subagent-watcher already works** — no new infrastructure needed for teammate transcript tailing
@@ -15,7 +15,7 @@ When local echo is enabled, keystrokes accumulate in the `LocalEchoOverlay.pendi
When local echo is enabled, keystrokes accumulate **only** in `LocalEchoOverlay.pendingText` (a client-side string). Nothing reaches the server PTY until Enter is pressed. This creates three failure modes:
1. **Tab switch loses PTY state** — switching sessions saves overlay text to `localEchoTextCache` (a Map), but the actual Claude Code Ink process has no knowledge of what was typed. If respawn or `/clear` fires on that session, the cached text is meaningless.
2. **Session death loses input** — if the session crashes or respawns while text is pending in the overlay, that input is gone (localStorage backup `claudeman_local_echo_pending` only survives page reloads, not session resets).
2. **Session death loses input** — if the session crashes or respawns while text is pending in the overlay, that input is gone (localStorage backup `codeman_local_echo_pending` only survives page reloads, not session resets).
3. **Tab completion impossible** — pressing Tab with pending overlay text sends the raw Tab character to a PTY that has no knowledge of the typed text, so completion fails.
## Goal
+1 -1
View File
@@ -5,7 +5,7 @@
When local echo is enabled, keystrokes accumulate **only** in `LocalEchoOverlay.pendingText` (a client-side string). Nothing reaches the server PTY until Enter is pressed. This creates three failure modes:
1. **Tab switch loses PTY state** — switching sessions saves overlay text to `localEchoTextCache` (a Map), but the actual Claude Code Ink process has no knowledge of what was typed. If respawn or `/clear` fires on that session, the cached text is meaningless.
2. **Session death loses input** — if the session crashes or respawns while text is pending in the overlay, that input is gone (localStorage backup `claudeman_local_echo_pending` only survives page reloads, not session resets).
2. **Session death loses input** — if the session crashes or respawns while text is pending in the overlay, that input is gone (localStorage backup `codeman_local_echo_pending` only survives page reloads, not session resets).
3. **Tab completion impossible** — pressing Tab with pending overlay text sends the raw Tab character to a PTY that has no knowledge of the typed text, so completion fails.
## Goal
+4 -4
View File
@@ -1,6 +1,6 @@
# Browser Testing Guide for Claudeman
# Browser Testing Guide for Codeman
This guide documents the browser testing infrastructure, framework comparison results, and best practices for testing the Claudeman web UI.
This guide documents the browser testing infrastructure, framework comparison results, and best practices for testing the Codeman web UI.
## Quick Start
@@ -14,7 +14,7 @@ npm test -- test/browser-e2e.test.ts
## Framework Comparison Results
We tested three browser automation frameworks against the Claudeman web UI:
We tested three browser automation frameworks against the Codeman web UI:
| Framework | Avg Duration | Best For |
|-----------|--------------|----------|
@@ -114,7 +114,7 @@ await page.waitForSelector('.session-tab', { state: 'visible' });
// Assertions with expect
await expect(page.locator('.header')).toBeVisible();
await expect(page).toHaveTitle('Claudeman');
await expect(page).toHaveTitle('Codeman');
await browser.close();
```
+1 -1
View File
@@ -1,6 +1,6 @@
# Codebase Cleanup Findings
Compiled from parallel analysis of the entire Claudeman codebase by 3 research agents (2026-02-19).
Compiled from parallel analysis of the entire Codeman codebase by 3 research agents (2026-02-19).
## P0 — Bug Fix
+1 -1
View File
@@ -2,7 +2,7 @@
**Date**: 2026-02-18
**Audit by**: 4-agent team (css-analyst, js-analyst, server-analyst, deps-analyst)
**Scope**: First browser load of Claudeman web UI at `/`
**Scope**: First browser load of Codeman web UI at `/`
---

Before

Width:  |  Height:  |  Size: 859 KiB

After

Width:  |  Height:  |  Size: 859 KiB

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 60">
<text x="160" y="48" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="52" font-weight="700" fill="#60a5fa" text-anchor="middle">Claudeman</text>
<text x="160" y="48" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" font-size="52" font-weight="700" fill="#60a5fa" text-anchor="middle">Codeman</text>
</svg>

Before

Width:  |  Height:  |  Size: 249 B

After

Width:  |  Height:  |  Size: 247 B

+1 -1
View File
@@ -2,7 +2,7 @@
## Context
User accesses Claudeman remotely from Thailand to Switzerland over Tailscale (~200-300ms RTT).
User accesses Codeman remotely from Thailand to Switzerland over Tailscale (~200-300ms RTT).
Every keystroke is invisible for 200-300ms before the server echoes it back. This makes typing
painfully slow on mobile. Previous attempts to write directly to xterm.js buffer failed because
Ink (Claude Code's terminal framework) does full-screen redraws that corrupt injected characters.
+57 -57
View File
@@ -1,4 +1,4 @@
# OpenCode Integration Plan for Claudeman
# OpenCode Integration Plan for Codeman
> **Author**: Claude Opus 4.6 | **Date**: 2026-02-26
> **Status**: Draft — Re-reviewed, MVP scope finalized. NOT pushed to GitHub
@@ -36,13 +36,13 @@
23. [Appendix A: OpenCode CLI Reference](#appendix-a-opencode-cli-reference)
24. [Appendix B: OpenCode Plugin Events](#appendix-b-opencode-plugin-events)
25. [Appendix C: OpenCode Permission Config](#appendix-c-opencode-permission-config)
26. [Appendix D: Current Claudeman Session Spawn Flow (Annotated)](#appendix-d-current-claudeman-session-spawn-flow-annotated)
26. [Appendix D: Current Codeman Session Spawn Flow (Annotated)](#appendix-d-current-codeman-session-spawn-flow-annotated)
---
## 1. Executive Summary
This plan details how to integrate [OpenCode](https://opencode.ai) — the popular open-source AI coding CLI (111k+ GitHub stars, 75+ model providers) — into Claudeman as a first-class session type alongside Claude Code and shell sessions.
This plan details how to integrate [OpenCode](https://opencode.ai) — the popular open-source AI coding CLI (111k+ GitHub stars, 75+ model providers) — into Codeman as a first-class session type alongside Claude Code and shell sessions.
### Core Approach
@@ -53,7 +53,7 @@ Extend the existing `SessionMode` type from `'claude' | 'shell'` to `'claude' |
| Strategy | Approach | Complexity | Value |
|----------|----------|------------|-------|
| **A: TUI-in-tmux** | Spawn `opencode` CLI in tmux, render in xterm.js | Medium | Full visual parity with Claude Code |
| **B: Server API bridge** | Run `opencode serve` + proxy its API through Claudeman | High | Structured data, session control, token tracking |
| **B: Server API bridge** | Run `opencode serve` + proxy its API through Codeman | High | Structured data, session control, token tracking |
**Recommended path**: Start with Strategy A (TUI-in-tmux) since it mirrors the existing Claude Code pattern exactly. Then layer Strategy B on top for advanced features like structured token tracking and model switching.
@@ -71,7 +71,7 @@ Extend the existing `SessionMode` type from `'claude' | 'shell'` to `'claude' |
> | **Hooks plugin bridge** | Plugin event names are speculative/unverified. Requires Phase 0 validation that hasn't happened. | Phase 0 plugin verification |
> | **AI idle checker** | Spawns `claude -p` for analysis. Won't work if only OpenCode is installed. | Claude CLI availability or `opencode run` fallback |
>
> **What ships in the MVP**: Phases 0-3 (spawn in tmux, render in xterm.js) + Phase 5 (API routes, mode selector, tab badges, create/kill sessions). Users can interact with OpenCode manually — type prompts, see output, manage sessions from the Claudeman web UI. This alone is the core value: multi-model AI sessions in one management interface.
> **What ships in the MVP**: Phases 0-3 (spawn in tmux, render in xterm.js) + Phase 5 (API routes, mode selector, tab badges, create/kill sessions). Users can interact with OpenCode manually — type prompts, see output, manage sessions from the Codeman web UI. This alone is the core value: multi-model AI sessions in one management interface.
### Why OpenCode?
@@ -79,7 +79,7 @@ Extend the existing `SessionMode` type from `'claude' | 'shell'` to `'claude' |
- **Privacy-first**: Can run fully local via Ollama — no code ever leaves the machine
- **Open source**: MIT licensed, active community (700+ contributors)
- **Client/server**: Built-in `opencode serve` mode enables richer programmatic integration
- **Plugin system**: JS/TS plugins with rich event hooks (including `session.idle` — perfect for Claudeman)
- **Plugin system**: JS/TS plugins with rich event hooks (including `session.idle` — perfect for Codeman)
---
@@ -222,7 +222,7 @@ OPENCODE_CLIENT=... # Client identifier (default: "cli")
## 3. Architecture Comparison: Claude Code vs OpenCode
### Current Claudeman Session Flow (Claude Code)
### Current Codeman Session Flow (Claude Code)
```
POST /api/sessions → new Session({mode: 'claude', mux: TmuxManager})
@@ -230,10 +230,10 @@ POST /api/sessions/:id/interactive → session.startInteractive()
↓
TmuxManager.createSession()
↓
tmux new-session -ds "claudeman-<id>"
tmux new-session -ds "codeman-<id>"
tmux respawn-pane -k -t ... "claude --dangerously-skip-permissions --session-id <id>"
↓
pty.spawn('tmux', ['attach-session', '-t', 'claudeman-<id>'])
pty.spawn('tmux', ['attach-session', '-t', 'codeman-<id>'])
↓
ptyProcess.onData() → emit('terminal') → SSE broadcast → xterm.js
```
@@ -246,10 +246,10 @@ POST /api/sessions/:id/interactive → session.startInteractive()
↓
TmuxManager.createSession()
↓
tmux new-session -ds "claudeman-<id>"
tmux new-session -ds "codeman-<id>"
tmux respawn-pane -k -t ... "opencode --model <model>"
↓
pty.spawn('tmux', ['attach-session', '-t', 'claudeman-<id>'])
pty.spawn('tmux', ['attach-session', '-t', 'codeman-<id>'])
↓
ptyProcess.onData() → emit('terminal') → SSE broadcast → xterm.js
```
@@ -268,7 +268,7 @@ Strategy A (TUI-in-tmux) for terminal rendering
+
opencode serve (background, port 4096+N)
↓
Claudeman proxy routes → GET /session/current, POST /session/message, SSE /events
Codeman proxy routes → GET /session/current, POST /session/message, SSE /events
↓
Structured data for: token tracking, session management, model switching
```
@@ -301,7 +301,7 @@ These systems work identically for OpenCode sessions:
- Terminal data streaming via `ptyProcess.onData()`
- SSE event broadcasting via `broadcast()`
- xterm.js terminal rendering in the browser
- State persistence to `~/.claudeman/state.json`
- State persistence to `~/.codeman/state.json`
- Session CRUD API routes (create, get, delete)
- Tab management in frontend
- Session kill/cleanup logic (`TmuxManager.killSession()`)
@@ -324,7 +324,7 @@ These systems work identically for OpenCode sessions:
| Subagent detection | `BashToolParser` + `SubagentWatcher` | Different tool output format | DEFERRED |
| Ralph completion | `<promise>PHRASE</promise>` tags | Not applicable (needs alternative) | DEFERRED |
| Hooks events | `permission_prompt`, `idle_prompt`, `stop` | `permission.asked`, `session.idle`, `session.status` | DEFERRED |
| Auto-compact | Claudeman sends `/compact` at token threshold | OpenCode has built-in `compaction.auto: true` | DEFERRED |
| Auto-compact | Codeman sends `/compact` at token threshold | OpenCode has built-in `compaction.auto: true` | DEFERRED |
---
@@ -346,7 +346,7 @@ opencode --version
# 3. Test interactive TUI
opencode
# 4. Test in tmux (simulating Claudeman's spawn pattern)
# 4. Test in tmux (simulating Codeman's spawn pattern)
tmux new-session -ds "test-opencode" -c /tmp -x 120 -y 40
tmux set-option -t "test-opencode" remain-on-exit on
tmux respawn-pane -k -t "test-opencode" 'opencode --model anthropic/claude-sonnet-4-5'
@@ -774,10 +774,10 @@ function buildEnvExports(mode: string, sessionId: string, muxName: string, openC
`export LANG=en_US.UTF-8`,
`export LC_ALL=en_US.UTF-8`,
`unset COLORTERM`, // Prevent color issues in tmux
`export CLAUDEMAN_MUX=1`,
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
`export CLAUDEMAN_MUX_NAME=${muxName}`,
`export CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`,
`export CODEMAN_MUX=1`,
`export CODEMAN_SESSION_ID=${sessionId}`,
`export CODEMAN_MUX_NAME=${muxName}`,
`export CODEMAN_API_URL=${process.env.CODEMAN_API_URL || 'http://localhost:3000'}`,
];
if (mode === 'opencode') {
@@ -905,7 +905,7 @@ const mode = cmd.includes('opencode') ? 'opencode' : 'claude';
### Known Limitations (from review)
> **[REVIEW P1-6]** OpenCode does not have `--session-id <claudeman-id>` for initial session creation (only `--session <id>` for continuing existing sessions). This means:
> **[REVIEW P1-6]** OpenCode does not have `--session-id <codeman-id>` for initial session creation (only `--session <id>` for continuing existing sessions). This means:
> - `_claudeSessionId = this.id` correlation (session.ts:405) won't work for OpenCode
> - Subagent-session correlation via Session ID matching won't work
> - Transcript watching via Claude session ID path won't work
@@ -1121,7 +1121,7 @@ private parseTokens(data: string): void {
### Working/Idle State Tracking
For Claude, Claudeman uses spinner characters and keywords. For OpenCode:
For Claude, Codeman uses spinner characters and keywords. For OpenCode:
> **[REVIEW]** The original logic had a bug: `Date.now() - this._lastActivityAt > 100` is checked AFTER setting `_lastActivityAt = Date.now()`, so the condition would never be true (0 > 100 = false). Fixed below.
@@ -1373,7 +1373,7 @@ if (session.mode === 'claude' && session.pid) {
> **[REVIEW C3] Phase reordered**: This was originally Phase 7 but has been moved before Respawn. The plugin bridge provides the reliable `session.idle` event that the respawn controller needs. Without it, respawn relies on output-silence-only detection, which is unreliable with Bubble Tea TUIs (see Review C1). **Do not implement respawn (Phase 7) until this phase is complete and tested.**
### Goal
Bridge Claudeman's hook system with OpenCode's plugin system for rich event forwarding.
Bridge Codeman's hook system with OpenCode's plugin system for rich event forwarding.
### Background: Two Different Approaches
@@ -1383,19 +1383,19 @@ Bridge Claudeman's hook system with OpenCode's plugin system for rich event forw
| Trigger | Hook name matches event type | Event name subscription |
| Key events | `stop`, `idle_prompt`, `permission_prompt`, `elicitation_dialog` | `session.idle`, `permission.asked`, `session.status` |
| Communication | Exit codes + environment variables | Function context + return values |
| Install | Auto-generated by Claudeman | Must be placed in `.opencode/plugins/` |
| Install | Auto-generated by Codeman | Must be placed in `.opencode/plugins/` |
### Claudeman Plugin for OpenCode
### Codeman Plugin for OpenCode
Create a Claudeman plugin that OpenCode loads, which communicates back to Claudeman's API:
Create a Codeman plugin that OpenCode loads, which communicates back to Codeman's API:
**File: `.opencode/plugins/claudeman-bridge.js`** (generated per session)
**File: `.opencode/plugins/codeman-bridge.js`** (generated per session)
```javascript
// This plugin bridges OpenCode events to Claudeman's API
export const claudemanBridge = async ({ project, $ }) => {
const apiUrl = process.env.CLAUDEMAN_API_URL || 'http://localhost:3000';
const sessionId = process.env.CLAUDEMAN_SESSION_ID;
// This plugin bridges OpenCode events to Codeman's API
export const codemanBridge = async ({ project, $ }) => {
const apiUrl = process.env.CODEMAN_API_URL || 'http://localhost:3000';
const sessionId = process.env.CODEMAN_SESSION_ID;
if (!sessionId) return {};
@@ -1440,7 +1440,7 @@ export const claudemanBridge = async ({ project, $ }) => {
### Plugin Installation
When creating an OpenCode session, Claudeman generates this plugin in the project's `.opencode/plugins/` directory:
When creating an OpenCode session, Codeman generates this plugin in the project's `.opencode/plugins/` directory:
```typescript
// In session.ts or a new opencode-hooks.ts:
@@ -1448,8 +1448,8 @@ async function installOpenCodePlugin(workingDir: string, sessionId: string): Pro
const pluginDir = join(workingDir, '.opencode', 'plugins');
await mkdirp(pluginDir);
const pluginContent = generateClaudemanBridgePlugin(sessionId);
await writeFile(join(pluginDir, 'claudeman-bridge.js'), pluginContent);
const pluginContent = generateCodemanBridgePlugin(sessionId);
await writeFile(join(pluginDir, 'codeman-bridge.js'), pluginContent);
}
```
@@ -1458,7 +1458,7 @@ async function installOpenCodePlugin(workingDir: string, sessionId: string): Pro
> **[REVIEW]** Add `session.compacted` to the plugin bridge — it's free and useful for tracking when OpenCode auto-compacts (relevant for token tracking and respawn timing).
```javascript
// Add to claudeman-bridge.js return object:
// Add to codeman-bridge.js return object:
'session.compacted': async (info) => {
await notifyClademan('session_compacted', { info });
},
@@ -1472,12 +1472,12 @@ async function installOpenCodePlugin(workingDir: string, sessionId: string): Pro
### Benefits of the Plugin Bridge
Once installed, Claudeman receives structured events from OpenCode:
Once installed, Codeman receives structured events from OpenCode:
- **`session.idle`** → Definitive idle detection (replaces output-silence guessing)
- **`permission.asked`** → Show permission prompts in Claudeman UI
- **`permission.asked`** → Show permission prompts in Codeman UI
- **`tool.execute.*`** → Tool call tracking (similar to BashToolParser for Claude)
- **`todo.updated`** → OpenCode's built-in todo system → Claudeman can display it
- **`session.error`** → Error surfacing in Claudeman UI
- **`todo.updated`** → OpenCode's built-in todo system → Codeman can display it
- **`session.error`** → Error surfacing in Codeman UI
- **`session.compacted`** → Track auto-compaction events
---
@@ -1664,7 +1664,7 @@ tmux session ─── opencode TUI (interactive, port N/A)
│
├── xterm.js (terminal rendering, Strategy A)
│
Claudeman ──── opencode serve (port 4096+N, background process)
Codeman ──── opencode serve (port 4096+N, background process)
│
├── GET /session/* → structured session data
├── POST /session/message → send prompt programmatically
@@ -1678,7 +1678,7 @@ Claudeman ──── opencode serve (port 4096+N, background process)
> **[REVIEW M8]** Running both `opencode` (TUI) and `opencode serve` in the same project directory creates a **shared SQLite state problem**. The TUI creates sessions in SQLite; the server reads from the same SQLite. But they're separate processes — there's no guarantee of session ID consistency between them.
>
> **Consider alternatives**:
> - (a) Use `opencode serve` INSTEAD of the TUI (not alongside it) — server becomes the single backend, Claudeman renders its own UI
> - (a) Use `opencode serve` INSTEAD of the TUI (not alongside it) — server becomes the single backend, Codeman renders its own UI
> - (b) Use `opencode attach` to connect the TUI to the server, making the server the single source of truth
> - (c) Accept the dual-process model with explicit documentation of limitations
>
@@ -1805,7 +1805,7 @@ if (this.mode === 'opencode' && this._openCodeConfig?.serverPort) {
|------|---------|-------|--------|
| `src/utils/ansi-content-filter.ts` | Strip cosmetic ANSI sequences for idle detection | 4 | DEFERRED |
| `src/completion-detector.ts` | `CompletionDetector` interface + implementations | 7 | DEFERRED |
| `src/opencode-plugin-generator.ts` | Generate `.opencode/plugins/claudeman-bridge.js` | 6 | DEFERRED |
| `src/opencode-plugin-generator.ts` | Generate `.opencode/plugins/codeman-bridge.js` | 6 | DEFERRED |
| `src/opencode-api-client.ts` | Client for OpenCode's REST API (Strategy B) | 8 | DEFERRED |
| `test/opencode-respawn.test.ts` | Tests for OpenCode respawn cycle (port 3156) | 7 | DEFERRED |
| `test/ansi-content-filter.test.ts` | Tests for ANSI content filter | 4 | DEFERRED |
@@ -1898,7 +1898,7 @@ npx vitest run test/opencode-respawn.test.ts
### Safety Rules
- **Never run OpenCode tests that spawn real tmux sessions inside Claudeman** (same safety rule as Claude tests)
- **Never run OpenCode tests that spawn real tmux sessions inside Codeman** (same safety rule as Claude tests)
- **Use MockSession** from `test/respawn-test-utils.ts` for respawn testing
- **Mock the opencode binary** for unit tests (`jest.mock` or stub)
- **Use unique test ports** (3155+) — never port 3000
@@ -1939,12 +1939,12 @@ npx vitest run test/opencode-respawn.test.ts
5. **Should we auto-generate `opencode.json` in the working directory?**
- Option A: Let OpenCode use existing project config (respect user settings)
- Option B: Generate a temporary one with Claudeman's settings
- Recommendation: Option A (use `OPENCODE_CONFIG_CONTENT` env var for Claudeman-specific overrides, don't modify project files)
- Option B: Generate a temporary one with Codeman's settings
- Recommendation: Option A (use `OPENCODE_CONFIG_CONTENT` env var for Codeman-specific overrides, don't modify project files)
6. **How should OpenCode session IDs map to Claudeman session IDs?**
6. **How should OpenCode session IDs map to Codeman session IDs?**
- OpenCode manages its own sessions (SQLite DB)
- We could pass `--session <claudeman-id>` but OpenCode IDs have different format
- We could pass `--session <codeman-id>` but OpenCode IDs have different format
- Recommendation: Let OpenCode manage its own sessions, store the mapping in SessionState
---
@@ -1970,7 +1970,7 @@ Phase 3: Tmux spawn (2-3 hours) ← CRITICAL INTEGRATION POINT
↓
Phase 5: API + frontend (2-3 hours) ← FIRST USER-VISIBLE RESULT
↓
✅ MVP COMPLETE — OpenCode sessions managed from Claudeman web UI
✅ MVP COMPLETE — OpenCode sessions managed from Codeman web UI
═══════════════════════════════════════════════════════
DEFERRED (requires real PTY data + verified APIs)
@@ -1991,7 +1991,7 @@ Phase 8: Server API — DEFERRED, optional advanced feature
| Milestone | Phase | What You Can Do | Scope |
|-----------|-------|-----------------|-------|
| **M1: "It renders"** | 0-3 | OpenCode TUI visible in xterm.js via Claudeman | **MVP** |
| **M1: "It renders"** | 0-3 | OpenCode TUI visible in xterm.js via Codeman | **MVP** |
| **M2: "It's usable"** | 5 | Create OpenCode sessions from web UI, type and interact, manage tabs | **MVP** |
| **M3: "It's observable"** | 4 | Idle/working state detection, ANSI content filter | Deferred |
| **M4: "It's smart"** | 6 | Plugin bridge provides definitive idle/permission/tool events | Deferred |
@@ -2012,7 +2012,7 @@ Phase 8: Server API — DEFERRED, optional advanced feature
## 21. Review Findings
> **Reviewed**: 2026-02-26 by a 4-agent team. Each agent reviewed a different area of the plan against the actual Claudeman codebase.
> **Reviewed**: 2026-02-26 by a 4-agent team. Each agent reviewed a different area of the plan against the actual Codeman codebase.
### Review Team
@@ -2182,7 +2182,7 @@ Environment Variables:
Full list of subscribable events in OpenCode's plugin system:
| Category | Event | Description | Claudeman Relevance |
| Category | Event | Description | Codeman Relevance |
|----------|-------|-------------|---------------------|
| **Command** | `command.executed` | Slash command run | Low |
| **Files** | `file.edited` | File modified | Medium (track changes) |
@@ -2194,7 +2194,7 @@ Full list of subscribable events in OpenCode's plugin system:
| | `message.updated` | Complete message | High (completion detection) |
| | `message.removed` | Message deleted | Low |
| | `message.part.removed` | Part deleted | Low |
| **Permissions** | `permission.asked` | Tool approval needed | **Critical** (show in Claudeman) |
| **Permissions** | `permission.asked` | Tool approval needed | **Critical** (show in Codeman) |
| | `permission.replied` | User responded | High (track approvals) |
| **Server** | `server.connected` | Server started | Medium |
| **Sessions** | `session.idle` | Agent finished working | **Critical** (idle detection!) |
@@ -2254,7 +2254,7 @@ Full list of subscribable events in OpenCode's plugin system:
- `"ask"` — Prompt user for approval
- `"deny"` — Block the action
### Delivery Method for Claudeman
### Delivery Method for Codeman
Use `OPENCODE_CONFIG_CONTENT` environment variable to inject permissions without modifying project files:
@@ -2265,7 +2265,7 @@ opencode --model anthropic/claude-sonnet-4-5
---
## Appendix D: Current Claudeman Session Spawn Flow (Annotated)
## Appendix D: Current Codeman Session Spawn Flow (Annotated)
### Exact Code Path (for reference during implementation)
@@ -2285,15 +2285,15 @@ opencode --model anthropic/claude-sonnet-4-5
→ If none: this._mux.createSession(id, workingDir, 'claude', ...)
4. TmuxManager.createSession() (tmux-manager.ts:225)
→ tmux new-session -ds "claudeman-<shortId>" -c <workingDir> -x 120 -y 40
→ tmux set-option -t "claudeman-<shortId>" remain-on-exit on
→ tmux new-session -ds "codeman-<shortId>" -c <workingDir> -x 120 -y 40
→ tmux set-option -t "codeman-<shortId>" remain-on-exit on
→ Build command: "export PATH=... && export LANG=... && claude --dangerously-skip-permissions --session-id <id>"
→ tmux respawn-pane -k -t "claudeman-<shortId>" '<command>'
→ tmux respawn-pane -k -t "codeman-<shortId>" '<command>'
→ Wait 100ms, configure tmux, get PID
→ Return MuxSession { muxName, pid, mode }
5. Back in startInteractive() (session.ts:~950)
→ pty.spawn('tmux', ['attach-session', '-t', 'claudeman-<shortId>'], {
→ pty.spawn('tmux', ['attach-session', '-t', 'codeman-<shortId>'], {
name: 'xterm-256color',
cols: 120, rows: 40,
env: { LANG, LC_ALL, TERM }
+2 -2
View File
@@ -1,7 +1,7 @@
# Performance Audit: First Page Load
**Date**: 2026-02-18
**Scope**: Browser first-load of Claudeman web UI (`/`)
**Scope**: Browser first-load of Codeman web UI (`/`)
**Method**: Static analysis by 4 parallel audit agents (server, frontend, SSE/xterm, asset pipeline)
---
@@ -40,7 +40,7 @@ Browser hits /
│
│ [FIRST PAINT blocked until ALL CSS downloaded + parsed]
│
├── JS executes: new ClaudemanApp().init()
├── JS executes: new CodemanApp().init()
│ ├── initTerminal() ................... (SYNC: new Terminal() + terminal.open() → canvas creation)
│ ├── connectSSE() → /api/events ....... (SSE → fires 'init' with getLightState())
│ ├── loadState() → /api/status ........ (DUPLICATE #1: same data as SSE init!)
+3 -3
View File
@@ -1,14 +1,14 @@
# Claudeman Performance Investigation Report
# Codeman Performance Investigation Report
**Date**: 2026-02-20
**Scope**: Why Claudeman feels sluggish when multiple Claude tabs are very busy
**Scope**: Why Codeman feels sluggish when multiple Claude tabs are very busy
**Method**: 4-agent parallel analysis of server, PTY pipeline, frontend, and background systems
---
## Executive Summary
When multiple Claude sessions are actively producing heavy terminal output (e.g., building, writing files, running tests), Claudeman's UI becomes sluggish. This investigation identified **14 bottlenecks** across 4 layers of the stack. The root cause is **cumulative event loop blocking** — no single operation is catastrophically slow, but dozens of small synchronous operations run on every PTY data chunk, and with N busy sessions producing chunks every few milliseconds, the event loop gets saturated.
When multiple Claude sessions are actively producing heavy terminal output (e.g., building, writing files, running tests), Codeman's UI becomes sluggish. This investigation identified **14 bottlenecks** across 4 layers of the stack. The root cause is **cumulative event loop blocking** — no single operation is catastrophically slow, but dozens of small synchronous operations run on every PTY data chunk, and with N busy sessions producing chunks every few milliseconds, the event loop gets saturated.
The most impactful findings are ranked by severity below.
+1 -1
View File
@@ -244,4 +244,4 @@ At iterations 5, 10, 20, 30, 50:
---
*This document is part of the Claudeman project. See [CLAUDE.md](../CLAUDE.md) for main documentation.*
*This document is part of the Codeman project. See [CLAUDE.md](../CLAUDE.md) for main documentation.*
+2 -2
View File
@@ -2,7 +2,7 @@
## Overview
This plan details improvements to Claudeman's Ralph Loop system based on best practices from the Ralph Claude Code repository (https://github.com/frankbria/ralph-claude-code).
This plan details improvements to Codeman's Ralph Loop system based on best practices from the Ralph Claude Code repository (https://github.com/frankbria/ralph-claude-code).
## Key Concepts to Implement
@@ -181,7 +181,7 @@ Three states: CLOSED → HALF_OPEN → OPEN
### 3.1 Template Library
- Bug Fix, Feature, Refactoring, Test Coverage, Documentation templates
- Template selector in wizard
- Custom templates in `~/.claudeman/templates/`
- Custom templates in `~/.codeman/templates/`
### 3.2 Tool Permissions
- Configure allowed Claude tools per loop
+8 -8
View File
@@ -19,7 +19,7 @@
8. [Prompt Templates](#prompt-templates)
9. [When to Use (and Not Use)](#when-to-use-and-not-use)
10. [Real-World Examples](#real-world-examples)
11. [Claudeman Implementation](#claudeman-implementation)
11. [Codeman Implementation](#codeman-implementation)
12. [Troubleshooting](#troubleshooting)
---
@@ -148,18 +148,18 @@ The completion phrase pattern is the core contract between Claude and the loop s
### False Positive Prevention
The official implementation (and Claudeman) prevents false positives when completion phrases appear in:
The official implementation (and Codeman) prevents false positives when completion phrases appear in:
- Initial prompts
- Documentation or examples
- Comments
**Solution**: Claudeman uses **occurrence-based detection** to distinguish prompts from actual completions:
**Solution**: Codeman uses **occurrence-based detection** to distinguish prompts from actual completions:
- **1st occurrence**: Store as expected phrase (likely in the prompt)
- **2nd occurrence**: Emit `completionDetected` (actual completion)
- **If loop already active**: Emit immediately (explicit loop start via `/ralph-loop:ralph-loop`)
```typescript
// From claudeman/src/ralph-tracker.ts
// From codeman/src/ralph-tracker.ts
private handleCompletionPhrase(phrase: string): void {
const count = (this._completionPhraseCount.get(phrase) || 0) + 1;
this._completionPhraseCount.set(phrase, count);
@@ -631,9 +631,9 @@ Ask yourself:
---
## Claudeman Implementation
## Codeman Implementation
Claudeman implements Ralph Wiggum tracking via the `RalphTracker` class in `src/ralph-tracker.ts`.
Codeman implements Ralph Wiggum tracking via the `RalphTracker` class in `src/ralph-tracker.ts`.
### Auto-Detection Patterns
@@ -833,7 +833,7 @@ POST /api/sessions/:id/auto-clear
- [Claude Fast - Autonomous Agent Loops](https://claudefa.st/blog/guide/mechanics/autonomous-agent-loops)
- [DeepWiki - Ralph Loop](https://deepwiki.com/anthropics/claude-plugins-official/5.2.2-ralph-loop)
### Related Claudeman Files
### Related Codeman Files
- `src/ralph-tracker.ts` - Core detection engine
- `src/ralph-loop.ts` - Task orchestration
- `src/respawn-controller.ts` - Session cycling
@@ -843,4 +843,4 @@ POST /api/sessions/:id/auto-clear
---
*This documentation is maintained as part of the Claudeman project. For updates, see the main [CLAUDE.md](../CLAUDE.md).*
*This documentation is maintained as part of the Codeman project. For updates, see the main [CLAUDE.md](../CLAUDE.md).*
+1 -1
View File
@@ -4,7 +4,7 @@ Date: 2026-02-17
## Scope
Full audit of the Claudeman notification system covering:
Full audit of the Codeman notification system covering:
- Backend event pipeline (server.ts, hooks-config.ts, team-watcher.ts, subagent-watcher.ts)
- Frontend notification manager (app.js NotificationManager class, 4-layer architecture)
- Settings UI and persistence (localStorage + server backup)
+7 -7
View File
@@ -1,10 +1,10 @@
# Claudeman Notification System - Backend Research Report
# Codeman Notification System - Backend Research Report
Date: 2026-02-17
## Executive Summary
The Claudeman notification system is a **multi-layer, event-driven pipeline** that flows from backend event emitters, through SSE broadcasts, to a frontend `NotificationManager` class. The backend itself has no concept of "notifications" -- it broadcasts structured SSE events, and the frontend decides which events warrant user notification (browser notifications, audio alerts, tab title flashing, in-app notification drawer, tab alert badges).
The Codeman notification system is a **multi-layer, event-driven pipeline** that flows from backend event emitters, through SSE broadcasts, to a frontend `NotificationManager` class. The backend itself has no concept of "notifications" -- it broadcasts structured SSE events, and the frontend decides which events warrant user notification (browser notifications, audio alerts, tab title flashing, in-app notification drawer, tab alert badges).
The system handles ~25 distinct notification-triggering SSE events across 5 categories: hook events, session lifecycle, respawn state machine, Ralph Loop, and UI actions.
@@ -96,14 +96,14 @@ Terminal and output data use separate batching pipelines that bypass `broadcast(
### 2.1 Hook Configuration Generator (Lines 24-67)
The `generateHooksConfig()` function creates `.claude/settings.local.json` entries that make Claude Code POST to Claudeman when hooks fire:
The `generateHooksConfig()` function creates `.claude/settings.local.json` entries that make Claude Code POST to Codeman when hooks fire:
```typescript
const curlCmd = (event: HookEventType) =>
`HOOK_DATA=$(cat 2>/dev/null || echo '{}'); ` +
`curl -s -X POST "$CLAUDEMAN_API_URL/api/hook-event" ` +
`curl -s -X POST "$CODEMAN_API_URL/api/hook-event" ` +
`-H 'Content-Type: application/json' ` +
`-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CLAUDEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` +
`-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CODEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` +
`2>/dev/null || true`;
```
@@ -143,8 +143,8 @@ The `sanitizeHookData()` function limits what gets broadcast:
### 2.4 Environment Variables (Lines 70-101)
Two env vars are set per case directory via `updateCaseEnvVars()`:
- `CLAUDEMAN_API_URL` -- server URL (e.g., `http://localhost:3000`)
- `CLAUDEMAN_SESSION_ID` -- session identifier
- `CODEMAN_API_URL` -- server URL (e.g., `http://localhost:3000`)
- `CODEMAN_SESSION_ID` -- session identifier
These are resolved at runtime by the shell, so the hook config is static per case.
+7 -7
View File
@@ -2,7 +2,7 @@
## Overview
Claudeman implements a **4-layer notification system** managed by the `NotificationManager` class (app.js lines 860-1265). The layers are:
Codeman implements a **4-layer notification system** managed by the `NotificationManager` class (app.js lines 860-1265). The layers are:
1. **In-app notification drawer** (Layer 1) - badge + list UI
2. **Document title flashing** (Layer 2) - tab title blinks when hidden
@@ -32,19 +32,19 @@ updateTabTitle() {
this.titleFlashInterval = setInterval(() => {
this.titleFlashState = !this.titleFlashState;
document.title = this.titleFlashState
? `\u26A0\uFE0F (${this.unreadCount}) Claudeman`
? `\u26A0\uFE0F (${this.unreadCount}) Codeman`
: this.originalTitle;
}, TITLE_FLASH_INTERVAL_MS);
// Set immediately
document.title = `\u26A0\uFE0F (${this.unreadCount}) Claudeman`;
document.title = `\u26A0\uFE0F (${this.unreadCount}) Codeman`;
}
}
}
```
The title alternates between:
- Warning emoji + unread count: `"(3) Claudeman"`
- Original title: `"Claudeman"`
- Warning emoji + unread count: `"(3) Codeman"`
- Original title: `"Codeman"`
### What Triggers It
Title flashing starts when `notify()` is called **while the tab is not visible** (`!this.isTabVisible`). The check is at lines 1026-1028:
@@ -126,7 +126,7 @@ It shows a lightning bolt icon on a dark background. The favicon is **static** a
Browser notifications reference `/favicon.ico` as their icon (line 1130):
```js
const notif = new Notification(`Claudeman: ${title}`, {
const notif = new Notification(`Codeman: ${title}`, {
body,
tag,
icon: '/favicon.ico',
@@ -306,7 +306,7 @@ Key detail: Title flash always stops when tab becomes visible, but **unread coun
## 5. Focus/Blur Handling
### No window focus/blur listeners
Claudeman does **not** use `window.addEventListener('focus')` or `window.addEventListener('blur')`. It relies solely on the Page Visibility API (`visibilitychange` + `pageshow`).
Codeman does **not** use `window.addEventListener('focus')` or `window.addEventListener('blur')`. It relies solely on the Page Visibility API (`visibilitychange` + `pageshow`).
This is the correct modern approach. The `focus`/`blur` events are unreliable (fire for devtools, iframe changes, etc.) while `visibilitychange` accurately reflects whether the user can see the tab.
+13 -13
View File
@@ -1,11 +1,11 @@
# Claudeman Frontend Notification System -- Detailed Report
# Codeman Frontend Notification System -- Detailed Report
> Generated: 2026-02-17
> Source files analyzed:
> - `/home/arkon/default/claudeman/src/web/public/app.js` (main frontend, ~15k lines)
> - `/home/arkon/default/claudeman/src/web/public/index.html`
> - `/home/arkon/default/claudeman/src/web/public/styles.css`
> - `/home/arkon/default/claudeman/src/web/public/mobile.css`
> - `/home/arkon/default/codeman/src/web/public/app.js` (main frontend, ~15k lines)
> - `/home/arkon/default/codeman/src/web/public/index.html`
> - `/home/arkon/default/codeman/src/web/public/styles.css`
> - `/home/arkon/default/codeman/src/web/public/mobile.css`
---
@@ -87,8 +87,8 @@ const defaults = {
#### Storage Keys (lines 952-956)
Device-specific localStorage keys prevent mobile settings from overriding desktop settings:
- Desktop: `claudeman-notification-prefs`
- Mobile: `claudeman-notification-prefs-mobile`
- Desktop: `codeman-notification-prefs`
- Mobile: `codeman-notification-prefs-mobile`
#### Version Migrations (lines 928-940)
@@ -260,8 +260,8 @@ Mobile override: full-width with safe area padding (mobile.css lines 1049-1058).
When the tab is not visible and there are unread notifications:
1. `setInterval` at 1500ms toggles between:
- Warning emoji + unread count: `"(3) Claudeman"`
- Original title: `"Claudeman"`
- Warning emoji + unread count: `"(3) Codeman"`
- Original title: `"Codeman"`
2. Set immediately on first notification (no wait for first interval tick)
### Stopping
@@ -311,7 +311,7 @@ sendBrowserNotif() called
### Notification Object (lines 1127-1143)
```js
new Notification(`Claudeman: ${title}`, {
new Notification(`Codeman: ${title}`, {
body,
tag, // Groups same-tag notifications (replaces previous with same tag)
icon: '/favicon.ico',
@@ -341,10 +341,10 @@ In settings (index.html line 993), a `<span class="settings-status" id="notifPer
The settings UI shows a hint (index.html line 996):
```
For remote access, HTTPS is required. Start with: claudeman web --https
For remote access, HTTPS is required. Start with: codeman web --https
```
Browser Notification API requires a secure context (HTTPS or localhost). This hint warns users who access Claudeman remotely over HTTP.
Browser Notification API requires a secure context (HTTPS or localhost). This hint warns users who access Codeman remotely over HTTP.
---
@@ -579,7 +579,7 @@ On mobile devices:
### Mobile Storage Key (lines 952-956)
Mobile uses a separate localStorage key (`claudeman-notification-prefs-mobile`) so that enabling notifications on desktop does not accidentally enable them on a mobile device viewing the same Claudeman instance.
Mobile uses a separate localStorage key (`codeman-notification-prefs-mobile`) so that enabling notifications on desktop does not accidentally enable them on a mobile device viewing the same Codeman instance.
### Mobile Drawer Styling (mobile.css lines 1049-1058)
+10 -10
View File
@@ -17,7 +17,7 @@ The App Settings modal has tabs: Display, Claude CLI, Models, Paths, **Notificat
The Browser row includes an "Ask" button that calls `requestPermission()` and a status badge showing the current `Notification.permission` state.
There is also a hint: _"For remote access, HTTPS is required. Start with: `claudeman web --https`"_
There is also a hint: _"For remote access, HTTPS is required. Start with: `codeman web --https`"_
#### Alerts Section
| Setting | Element ID | Type | Default |
@@ -54,16 +54,16 @@ Notification preferences are stored in **two places simultaneously**:
#### Layer 1: localStorage (primary, device-specific)
- **Storage key**: `claudeman-notification-prefs` (desktop) or `claudeman-notification-prefs-mobile` (mobile)
- **Storage key**: `codeman-notification-prefs` (desktop) or `codeman-notification-prefs-mobile` (mobile)
- Determined by `NotificationManager.getStorageKey()` at line 953, which calls `MobileDetection.getDeviceType()`
- Device type is based on `window.innerWidth`: `<430` = mobile, `430-768` = tablet, `>=768` = desktop
- Read in `loadPreferences()` (line 896), written in `savePreferences()` (line 958)
#### Layer 2: Server-side (`~/.claudeman/settings.json`)
#### Layer 2: Server-side (`~/.codeman/settings.json`)
- On save, notification prefs are bundled with app settings: `{ ...settings, notificationPreferences: notifPrefsToSave }` (line 9475)
- Sent via `PUT /api/settings` to the Fastify server
- Server does a shallow merge: `const merged = { ...existing, ...settings }` then writes to `~/.claudeman/settings.json` (line 3098 of server.ts)
- Server does a shallow merge: `const merged = { ...existing, ...settings }` then writes to `~/.codeman/settings.json` (line 3098 of server.ts)
- The `notificationPreferences` key sits at the top level of the settings JSON alongside app settings
#### Load priority
@@ -109,8 +109,8 @@ On startup, `loadAppSettingsFromServer()` (line 9787) fetches from server and:
### App settings (separate from notification prefs)
App settings use a different device-specific localStorage key:
- Desktop: `claudeman-app-settings`
- Mobile: `claudeman-app-settings-mobile`
- Desktop: `codeman-app-settings`
- Mobile: `codeman-app-settings-mobile`
- Determined by `getSettingsStorageKey()` at line 9562
@@ -213,8 +213,8 @@ const AUTO_CLOSE_NOTIFICATION_MS = 8000; // Auto-close browser notifications
### Separate storage keys - YES
Desktop and mobile use completely separate localStorage keys:
- **Notification prefs**: `claudeman-notification-prefs` vs `claudeman-notification-prefs-mobile`
- **App settings**: `claudeman-app-settings` vs `claudeman-app-settings-mobile`
- **Notification prefs**: `codeman-notification-prefs` vs `codeman-notification-prefs-mobile`
- **App settings**: `codeman-app-settings` vs `codeman-app-settings-mobile`
### Different defaults - YES
@@ -303,7 +303,7 @@ The settings UI shows the current permission state via a status badge:
### HTTPS requirement
Browser notifications require HTTPS for remote access. The settings UI includes a hint: _"For remote access, HTTPS is required. Start with: `claudeman web --https`"_. On localhost, HTTP works fine.
Browser notifications require HTTPS for remote access. The settings UI includes a hint: _"For remote access, HTTPS is required. Start with: `codeman web --https`"_. On localhost, HTTP works fine.
## 7. Audio Setting
@@ -354,7 +354,7 @@ Yes, given the prerequisites above are met. However, due to the category key mis
Notification preferences are **strictly global**. There is no per-session notification configuration.
- The `NotificationManager` is a singleton on the `ClaudemanApp` instance (line 1404)
- The `NotificationManager` is a singleton on the `CodemanApp` instance (line 1404)
- Preferences are loaded once from localStorage (line 878)
- All sessions share the same notification rules
+1 -1
View File
@@ -1,6 +1,6 @@
# Terminal Anti-Flicker System
Claude Code uses [Ink](https://github.com/vadimdemedes/ink) (React for terminals), which redraws the entire screen on every state change. Without special handling, users see constant flickering. Claudeman implements a 6-layer anti-flicker pipeline.
Claude Code uses [Ink](https://github.com/vadimdemedes/ink) (React for terminals), which redraws the entire screen on every state change. Without special handling, users see constant flickering. Codeman implements a 6-layer anti-flicker pipeline.
## Pipeline Overview
+8 -8
View File
@@ -1,4 +1,4 @@
# Claudeman TypeScript Improvement Suggestions
# Codeman TypeScript Improvement Suggestions
**Generated**: February 2026
**Based on**: Research into TypeScript best practices (2024-2025) and codebase analysis
@@ -229,33 +229,33 @@ Replace string-based errors with typed errors:
```typescript
// src/errors.ts
export class ClaudemanError extends Error {
export class CodemanError extends Error {
constructor(
message: string,
public code: string,
public context?: Record<string, unknown>
) {
super(message);
Object.setPrototypeOf(this, ClaudemanError.prototype);
this.name = 'ClaudemanError';
Object.setPrototypeOf(this, CodemanError.prototype);
this.name = 'CodemanError';
}
}
export class SessionError extends ClaudemanError {
export class SessionError extends CodemanError {
constructor(message: string, code: string, public sessionId: string) {
super(message, code, { sessionId });
this.name = 'SessionError';
}
}
export class ValidationError extends ClaudemanError {
export class ValidationError extends CodemanError {
constructor(message: string, public field: string, public value: unknown) {
super(message, 'VALIDATION_ERROR', { field, value });
this.name = 'ValidationError';
}
}
export class ScreenError extends ClaudemanError {
export class ScreenError extends CodemanError {
constructor(message: string, public screenName: string, public operation: string) {
super(message, 'SCREEN_ERROR', { screenName, operation });
this.name = 'ScreenError';
@@ -324,7 +324,7 @@ Enforce ID formats at compile time:
```typescript
type CycleIdFormat = `${string}:cycle-${number}`;
type ScreenSessionName = `claudeman-${string}`;
type ScreenSessionName = `codeman-${string}`;
interface RespawnCycleMetrics {
cycleId: CycleIdFormat; // Enforces format at compile time
+1 -1
View File
@@ -56,7 +56,7 @@ The blue-tinted voice button in the accessory bar is distinctive but subtle. Whe
Confirmed by research: Web Speech API is the right choice.
- **Free, fast (150-300ms interim), trivial complexity**
- Chrome + Safari = ~70% of users, ~95% of Claudeman's target audience (devs on Chrome)
- Chrome + Safari = ~70% of users, ~95% of Codeman's target audience (devs on Chrome)
- Works on localhost without HTTPS
- Accuracy is adequate for English command dictation
- Deepgram streaming (Phase 2 optional) only if accuracy complaints arise
+40 -40
View File
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
# Claudeman Universal Installer
# https://github.com/Ark0N/claudeman
# Codeman Universal Installer
# https://github.com/Ark0N/Codeman
#
# Usage: curl -fsSL https://raw.githubusercontent.com/Ark0N/claudeman/master/install.sh | bash
# Usage: curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | bash
#
# Environment variables:
# CLAUDEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation)
# CLAUDEMAN_INSTALL_DIR - Custom install directory (default: ~/.claudeman/app)
# CLAUDEMAN_SKIP_SYSTEMD=1 - Skip systemd service setup prompt
# CLAUDEMAN_NODE_VERSION - Node.js major version to install (default: 22)
# CODEMAN_NONINTERACTIVE=1 - Skip all prompts (for CI/automation)
# CODEMAN_INSTALL_DIR - Custom install directory (default: ~/.codeman/app)
# CODEMAN_SKIP_SYSTEMD=1 - Skip systemd service setup prompt
# CODEMAN_NODE_VERSION - Node.js major version to install (default: 22)
set -euo pipefail
@@ -16,12 +16,12 @@ set -euo pipefail
# Configuration
# ============================================================================
INSTALL_DIR="${CLAUDEMAN_INSTALL_DIR:-$HOME/.claudeman/app}"
REPO_URL="https://github.com/Ark0N/claudeman.git"
INSTALL_DIR="${CODEMAN_INSTALL_DIR:-$HOME/.codeman/app}"
REPO_URL="https://github.com/Ark0N/Codeman.git"
MIN_NODE_VERSION=18
TARGET_NODE_VERSION="${CLAUDEMAN_NODE_VERSION:-22}"
NONINTERACTIVE="${CLAUDEMAN_NONINTERACTIVE:-0}"
SKIP_SYSTEMD="${CLAUDEMAN_SKIP_SYSTEMD:-0}"
TARGET_NODE_VERSION="${CODEMAN_NODE_VERSION:-22}"
NONINTERACTIVE="${CODEMAN_NONINTERACTIVE:-0}"
SKIP_SYSTEMD="${CODEMAN_SKIP_SYSTEMD:-0}"
# Claude CLI search paths (from src/session.ts)
CLAUDE_SEARCH_PATHS=(
@@ -611,15 +611,15 @@ add_to_path() {
if [[ "$shell_name" == "fish" ]]; then
echo "" >> "$profile"
echo "# Added by Claudeman installer" >> "$profile"
echo "# Added by Codeman installer" >> "$profile"
echo "fish_add_path $bin_dir" >> "$profile"
else
echo "" >> "$profile"
echo "# Added by Claudeman installer" >> "$profile"
echo "# Added by Codeman installer" >> "$profile"
echo "export PATH=\"$bin_dir:\$PATH\"" >> "$profile"
fi
# Also export for the current process so claudeman works immediately
# Also export for the current process so codeman works immediately
export PATH="$bin_dir:$PATH"
success "Added to $profile"
@@ -636,7 +636,7 @@ setup_sc_alias() {
fi
echo "" >> "$profile"
echo "# Claudeman tmux session shortcut" >> "$profile"
echo "# Codeman tmux session shortcut" >> "$profile"
echo "alias sc='tmux-chooser'" >> "$profile"
info "Added 'sc' alias for tmux-chooser"
@@ -648,7 +648,7 @@ setup_sc_alias() {
setup_systemd_service() {
local service_dir="$HOME/.config/systemd/user"
local service_file="$service_dir/claudeman-web.service"
local service_file="$service_dir/codeman-web.service"
info "Setting up systemd user service..."
@@ -661,7 +661,7 @@ setup_systemd_service() {
# Create service file
cat > "$service_file" << EOF
[Unit]
Description=Claudeman Web Server
Description=Codeman Web Server
After=network.target
[Service]
@@ -681,7 +681,7 @@ EOF
systemctl --user daemon-reload
# Enable service
systemctl --user enable claudeman-web.service 2>/dev/null || true
systemctl --user enable codeman-web.service 2>/dev/null || true
# Enable lingering (allows service to run after logout)
if command -v loginctl &>/dev/null; then
@@ -689,7 +689,7 @@ EOF
fi
success "Systemd service installed at $service_file"
info "Start with: systemctl --user start claudeman-web"
info "Start with: systemctl --user start codeman-web"
}
# ============================================================================
@@ -760,7 +760,7 @@ main() {
if prompt_yes_no "Git is not installed. Install it now?"; then
install_dependency "git" "$os" "$distro"
else
die "Git is required to install Claudeman."
die "Git is required to install Codeman."
fi
else
success "Git is installed"
@@ -781,7 +781,7 @@ main() {
# Rehash to pick up new node
hash -r 2>/dev/null || true
else
die "Node.js $MIN_NODE_VERSION+ is required to run Claudeman."
die "Node.js $MIN_NODE_VERSION+ is required to run Codeman."
fi
else
local node_ver
@@ -832,7 +832,7 @@ main() {
# Clone/Update Repository
# ========================================================================
info "Installing Claudeman to $INSTALL_DIR..."
info "Installing Codeman to $INSTALL_DIR..."
if [[ -d "$INSTALL_DIR/.git" ]]; then
info "Existing installation found, updating..."
@@ -882,8 +882,8 @@ main() {
local symlink_dir="$HOME/.local/bin"
mkdir -p "$symlink_dir" 2>/dev/null || true
if [[ -d "$symlink_dir" ]]; then
ln -sf "$INSTALL_DIR/dist/index.js" "$symlink_dir/claudeman"
info "Created symlink: $symlink_dir/claudeman"
ln -sf "$INSTALL_DIR/dist/index.js" "$symlink_dir/codeman"
info "Created symlink: $symlink_dir/codeman"
# Install tmux-chooser as 'tmux-chooser' command
if [[ -f "$INSTALL_DIR/scripts/tmux-chooser.sh" ]]; then
@@ -916,16 +916,16 @@ main() {
echo ""
echo -e "${GREEN}${BOLD}============================================================${NC}"
echo -e "${GREEN}${BOLD} Claudeman installed successfully!${NC}"
echo -e "${GREEN}${BOLD} Codeman installed successfully!${NC}"
echo -e "${GREEN}${BOLD}============================================================${NC}"
echo ""
echo -e " ${BOLD}Quick Start:${NC}"
echo ""
echo -e " ${CYAN}# Start the web server${NC}"
echo -e " claudeman web"
echo -e " codeman web"
echo ""
echo -e " ${CYAN}# Start with HTTPS (only needed for remote access)${NC}"
echo -e " claudeman web --https"
echo -e " codeman web --https"
echo ""
echo -e " ${CYAN}# Open in browser${NC}"
echo -e " http://localhost:3000"
@@ -937,21 +937,21 @@ main() {
echo -e " ${CYAN}sc -h${NC} # Help"
echo ""
if [[ "$os" == "linux" ]] && [[ -f "$HOME/.config/systemd/user/claudeman-web.service" ]]; then
if [[ "$os" == "linux" ]] && [[ -f "$HOME/.config/systemd/user/codeman-web.service" ]]; then
echo -e " ${BOLD}Systemd Service:${NC}"
echo ""
echo -e " ${CYAN}systemctl --user start claudeman-web${NC} # Start"
echo -e " ${CYAN}systemctl --user status claudeman-web${NC} # Check status"
echo -e " ${CYAN}journalctl --user -u claudeman-web -f${NC} # View logs"
echo -e " ${CYAN}systemctl --user start codeman-web${NC} # Start"
echo -e " ${CYAN}systemctl --user status codeman-web${NC} # Check status"
echo -e " ${CYAN}journalctl --user -u codeman-web -f${NC} # View logs"
echo ""
fi
echo -e " ${BOLD}Documentation:${NC}"
echo -e " https://github.com/Ark0N/claudeman"
echo -e " https://github.com/Ark0N/Codeman"
echo ""
if ! check_claude; then
echo -e " ${YELLOW}${BOLD}Reminder:${NC} Install Claude CLI to start using Claudeman:"
echo -e " ${YELLOW}${BOLD}Reminder:${NC} Install Claude CLI to start using Codeman:"
echo -e " ${CYAN}curl -fsSL https://claude.ai/install.sh | bash${NC}"
echo ""
fi
@@ -959,20 +959,20 @@ main() {
# Check if PATH needs reload in user's shell
local profile
profile=$(detect_shell_profile)
if ! command -v claudeman &>/dev/null 2>&1; then
echo -e " ${YELLOW}Run this to start using claudeman now:${NC}"
if ! command -v codeman &>/dev/null 2>&1; then
echo -e " ${YELLOW}Run this to start using codeman now:${NC}"
echo ""
echo -e " ${CYAN}source $profile && claudeman web${NC}"
echo -e " ${CYAN}source $profile && codeman web${NC}"
echo ""
fi
}
update() {
if [[ ! -d "$INSTALL_DIR/.git" ]]; then
die "Claudeman is not installed at $INSTALL_DIR. Run the installer first."
die "Codeman is not installed at $INSTALL_DIR. Run the installer first."
fi
info "Updating Claudeman..."
info "Updating Codeman..."
cd "$INSTALL_DIR"
git fetch --quiet origin
git reset --hard origin/master --quiet
@@ -980,7 +980,7 @@ update() {
npm run build --quiet 2>/dev/null || npm run build
success "Updated to $(node -e "console.log(require('./package.json').version)")"
echo ""
echo -e " ${DIM}Restart claudeman web to use the new version.${NC}"
echo -e " ${DIM}Restart codeman web to use the new version.${NC}"
echo ""
}
+3 -3
View File
@@ -1,12 +1,12 @@
# Claudeman Mobile Test Suite
# Codeman Mobile Test Suite
Comprehensive mobile UI testing for Claudeman's web interface using Playwright with dual-engine support (Chromium + WebKit).
Comprehensive mobile UI testing for Codeman's web interface using Playwright with dual-engine support (Chromium + WebKit).
**325 tests across 135 devices — all passing.**
## Purpose
Validates Claudeman's mobile UI across 135 devices, covering:
Validates Codeman's mobile UI across 135 devices, covering:
- **Keyboard simulation** — 3-layer approach to emulate virtual keyboards in headless browsers
- **Touch/swipe interactions** — CDP trusted events (Chromium) + synthetic fallback (WebKit)
+3 -3
View File
@@ -126,7 +126,7 @@ export const BODY_CLASSES = {
// Settings localStorage keys
export const STORAGE_KEYS = {
SETTINGS_MOBILE: 'claudeman-app-settings-mobile',
SETTINGS_DESKTOP: 'claudeman-app-settings',
NOTIFICATION_PREFS_MOBILE: 'claudeman-notification-prefs-mobile',
SETTINGS_MOBILE: 'codeman-app-settings-mobile',
SETTINGS_DESKTOP: 'codeman-app-settings',
NOTIFICATION_PREFS_MOBILE: 'codeman-notification-prefs-mobile',
} as const;
+3 -3
View File
@@ -43,7 +43,7 @@ describe('Settings Modal', () => {
await context.close();
});
it('uses claudeman-app-settings-mobile localStorage key', async () => {
it('uses codeman-app-settings-mobile localStorage key', async () => {
const key = await page.evaluate(() => {
return (window as any).app?.getSettingsStorageKey?.() ?? null;
});
@@ -53,10 +53,10 @@ describe('Settings Modal', () => {
} else {
// Verify by checking what key is used when saving settings
await page.evaluate(() => {
localStorage.setItem('claudeman-app-settings-mobile', JSON.stringify({ testFlag: true }));
localStorage.setItem('codeman-app-settings-mobile', JSON.stringify({ testFlag: true }));
});
const stored = await page.evaluate(() => {
return localStorage.getItem('claudeman-app-settings-mobile');
return localStorage.getItem('codeman-app-settings-mobile');
});
expect(stored).toContain('testFlag');
}
+3 -3
View File
@@ -1,11 +1,11 @@
{
"name": "claudeman",
"name": "codeman",
"version": "0.1619",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claudeman",
"name": "codeman",
"version": "0.1619",
"hasInstallScript": true,
"license": "MIT",
@@ -25,7 +25,7 @@
"zod": "^4.3.6"
},
"bin": {
"claudeman": "dist/index.js"
"codeman": "dist/index.js"
},
"devDependencies": {
"@types/node": "^20.19.33",
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "claudeman",
"name": "codeman",
"version": "0.1650",
"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",
"types": "dist/index.d.ts",
"bin": {
"claudeman": "./dist/index.js"
"codeman": "./dist/index.js"
},
"scripts": {
"postinstall": "node scripts/postinstall.js",
@@ -71,12 +71,12 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Ark0N/claudeman.git"
"url": "git+https://github.com/Ark0N/Codeman.git"
},
"bugs": {
"url": "https://github.com/Ark0N/claudeman/issues"
"url": "https://github.com/Ark0N/Codeman/issues"
},
"homepage": "https://github.com/Ark0N/claudeman#readme",
"homepage": "https://github.com/Ark0N/Codeman#readme",
"files": [
"dist",
"scripts/postinstall.js",
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 Claudeman Contributors
Copyright (c) 2026 Codeman Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+2 -2
View File
@@ -39,7 +39,7 @@ Server echoes 'h' ←───────────────────
## Origin
This library was extracted from [Claudeman](https://github.com/Ark0N/Claudeman), a Claude Code session manager that runs 20+ autonomous AI agents over 24-hour sessions. The local echo system was built to make mobile and remote access feel instant, then battle-tested across thousands of hours of real usage. After 3 deep code audits, it was extracted into this standalone library with 78 tests covering every state transition.
This library was extracted from [Codeman](https://github.com/Ark0N/Codeman), a Claude Code session manager that runs 20+ autonomous AI agents over 24-hour sessions. The local echo system was built to make mobile and remote access feel instant, then battle-tested across thousands of hours of real usage. After 3 deep code audits, it was extracted into this standalone library with 78 tests covering every state transition.
## Install
@@ -375,4 +375,4 @@ Overlay hides when scrolled up (`viewportY !== baseY`). Debounced re-render when
## License
MIT — [Claudeman](https://github.com/Ark0N/Claudeman) Contributors
MIT — [Codeman](https://github.com/Ark0N/Codeman) Contributors
+2 -2
View File
@@ -32,10 +32,10 @@
"addon"
],
"license": "MIT",
"homepage": "https://github.com/Ark0N/Claudeman/tree/master/packages/xterm-zerolag-input#readme",
"homepage": "https://github.com/Ark0N/Codeman/tree/master/packages/xterm-zerolag-input#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/Ark0N/Claudeman.git",
"url": "git+https://github.com/Ark0N/Codeman.git",
"directory": "packages/xterm-zerolag-input"
},
"devDependencies": {
+8 -8
View File
@@ -2,7 +2,7 @@
/**
* Browser Framework Comparison Script
*
* Compares Playwright, Puppeteer, and Agent-Browser for testing Claudeman web UI.
* Compares Playwright, Puppeteer, and Agent-Browser for testing Codeman web UI.
* Run: node scripts/browser-comparison.mjs
*/
@@ -88,7 +88,7 @@ async function main() {
const page = await playwrightBrowser.newPage();
await page.goto(`http://localhost:${PORTS.playwright}`);
const title = await page.title();
if (title !== 'Claudeman') throw new Error(`Expected Claudeman, got ${title}`);
if (title !== 'Codeman') throw new Error(`Expected Codeman, got ${title}`);
await page.close();
});
@@ -98,7 +98,7 @@ async function main() {
await page.goto(`http://localhost:${PORTS.playwright}`);
const logo = await page.locator('.header-brand .logo').textContent();
const version = await page.locator('#versionDisplay').textContent();
if (logo !== 'Claudeman') throw new Error('Logo mismatch');
if (logo !== 'Codeman') throw new Error('Logo mismatch');
if (!version.match(/v\d+/)) throw new Error('Version mismatch');
await page.close();
});
@@ -149,7 +149,7 @@ async function main() {
const page = await puppeteerBrowser.newPage();
await page.goto(`http://localhost:${PORTS.puppeteer}`);
const title = await page.title();
if (title !== 'Claudeman') throw new Error(`Expected Claudeman, got ${title}`);
if (title !== 'Codeman') throw new Error(`Expected Codeman, got ${title}`);
await page.close();
});
@@ -159,7 +159,7 @@ async function main() {
await page.goto(`http://localhost:${PORTS.puppeteer}`);
const logo = await page.$eval('.header-brand .logo', el => el.textContent);
const version = await page.$eval('#versionDisplay', el => el.textContent);
if (logo !== 'Claudeman') throw new Error('Logo mismatch');
if (logo !== 'Codeman') throw new Error('Logo mismatch');
if (!version.match(/v\d+/)) throw new Error('Version mismatch');
await page.close();
});
@@ -202,7 +202,7 @@ async function main() {
agentBrowser(`open http://localhost:${PORTS.agentBrowser}`);
await new Promise(r => setTimeout(r, 2000));
const title = agentBrowserJson('get title');
agentBrowserAvailable = title.title === 'Claudeman';
agentBrowserAvailable = title.title === 'Codeman';
console.log(' Browser launched');
// Test 1: Page load
@@ -210,14 +210,14 @@ async function main() {
agentBrowser(`open http://localhost:${PORTS.agentBrowser}`);
await new Promise(r => setTimeout(r, 1000));
const title = agentBrowserJson('get title');
if (title.title !== 'Claudeman') throw new Error(`Expected Claudeman, got ${title.title}`);
if (title.title !== 'Codeman') throw new Error(`Expected Codeman, got ${title.title}`);
});
// Test 2: Element selection
await runBenchmark('element-selection', 'agent-browser', async () => {
const logo = agentBrowserJson('get text ".header-brand .logo"');
const version = agentBrowserJson('get text "#versionDisplay"');
if (logo.text !== 'Claudeman') throw new Error('Logo mismatch');
if (logo.text !== 'Codeman') throw new Error('Logo mismatch');
if (!version.text.match(/v\d+/)) throw new Error('Version mismatch');
});
+10 -10
View File
@@ -98,7 +98,7 @@ function makeSession(id, name, mode, status, extra = {}) {
id,
pid: status === 'idle' ? null : 12345 + Math.floor(Math.random() * 10000),
status,
workingDir: '/home/arkon/claudeman-cases/testcase',
workingDir: '/home/arkon/codeman-cases/testcase',
currentTaskId: null,
createdAt: Date.now() - 3600000,
lastActivityAt: Date.now() - (status === 'idle' ? 60000 : 5000),
@@ -336,7 +336,7 @@ const TERMINAL_INIT = [
'',
` ${RED}████${RST} ${BOLD}Claude Code${RST} v2.1.47`,
` ${RED}████${RST} ${GRY}Opus 4.6${RST} ${DIM}·${RST} ${GRY}Claude Max${RST}`,
` ${RED}██${RST} ${RED}██${RST} ${GRY}~/claudeman-cases/testcase${RST}`,
` ${RED}██${RST} ${RED}██${RST} ${GRY}~/codeman-cases/testcase${RST}`,
'',
'',
`${GRY}❯ Try "how do I log an error?"${RST}`,
@@ -353,7 +353,7 @@ const TERMINAL_WORKING = [
`${GRN}●${RST} The codebase is essentially a template/scaffold with just a CLAUDE.md file and empty ${BOLD}src/${RST} directory. Since there's already a comprehensive`,
`CLAUDE.md file present, let me read it fully and suggest improvements based on the /init command requirements:`,
'',
`${GRN}●${RST} ${BOLD}Read${RST}(/home/arkon/claudeman-cases/testcase/CLAUDE.md)`,
`${GRN}●${RST} ${BOLD}Read${RST}(/home/arkon/codeman-cases/testcase/CLAUDE.md)`,
` ${GRY}░${RST} Read ${BOLD}407${RST} lines`,
'',
`${GRN}●${RST} Now I understand the situation. This is a ${BOLD}template CLAUDE.md file${RST} designed for the Claude Code framework with the "Ralph Loop" autonomous work`,
@@ -420,15 +420,15 @@ const TERMINAL_RALPH = [
const SUBAGENT_ACTIVITY = {
'agent-001': [
{ type: 'tool', tool: 'Glob', input: { pattern: 'src/**/*.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-001' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/claudeman/src/web/server.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-001' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/src/web/server.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-001' },
{ type: 'tool', tool: 'Grep', input: { pattern: 'app\\.get|app\\.post|app\\.put|app\\.delete', path: 'src/' }, timestamp: new Date().toISOString(), agentId: 'agent-001' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/claudeman/src/web/schemas.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-001' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/src/web/schemas.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-001' },
{ type: 'message', role: 'assistant', text: 'Found 47 API endpoints across server.ts. Documenting REST paths...', timestamp: new Date().toISOString(), agentId: 'agent-001' },
],
'agent-002': [
{ type: 'tool', tool: 'Glob', input: { pattern: 'test/**/*.test.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/claudeman/test/respawn-test-utils.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/claudeman/vitest.config.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/test/respawn-test-utils.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'tool', tool: 'Read', input: { file_path: '/home/arkon/codeman/vitest.config.ts' }, timestamp: new Date().toISOString(), agentId: 'agent-002' },
{ type: 'message', role: 'assistant', text: 'Analyzing test patterns: MockSession, unique ports, fileParallelism: false...', timestamp: new Date().toISOString(), agentId: 'agent-002' },
],
};
@@ -436,9 +436,9 @@ const SUBAGENT_ACTIVITY = {
// Subagent spawn terminal content
const TERMINAL_SUBAGENT = [
'',
`${GRN}●${RST} Working on ${CYN}/home/arkon/claudeman-cases/testcase${RST} - I'll use the ${BOLD}Task tool${RST} to spawn parallel agents.`,
`${GRN}●${RST} Working on ${CYN}/home/arkon/codeman-cases/testcase${RST} - I'll use the ${BOLD}Task tool${RST} to spawn parallel agents.`,
'',
`${GRN}●${RST} ${BOLD}Read${RST}(/home/arkon/claudeman-cases/testcase/CLAUDE.md)`,
`${GRN}●${RST} ${BOLD}Read${RST}(/home/arkon/codeman-cases/testcase/CLAUDE.md)`,
` ${GRY}░${RST} Read ${BOLD}127${RST} lines ${GRY}│${RST} ${CYN}1.2KB${RST}`,
'',
`${GRN}●${RST} ${BOLD}Bash${RST}(find . -name "*.ts" -not -path "*/node_modules/*" | head -20)`,
@@ -864,7 +864,7 @@ async function captureMultiSessionMonitor(page) {
async function main() {
console.log('='.repeat(60));
console.log('Claudeman README Screenshot Capture');
console.log('Codeman README Screenshot Capture');
console.log('='.repeat(60));
console.log(`Port: ${PORT} | Viewport: ${VIEWPORT.width}x${VIEWPORT.height}`);
console.log('');
+6 -6
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Capture a GIF of subagent spawning on the LIVE Claudeman server.
* Capture a GIF of subagent spawning on the LIVE Codeman server.
*
* Connects to the running server on port 3000, creates a test session,
* sends a prompt that spawns 3 parallel agents, and captures a tight
@@ -69,7 +69,7 @@ async function capture() {
try {
// Create case and seed with real source files
console.log(`Creating case: ${CASE_NAME}`);
const caseDir = join(process.env.HOME, 'claudeman-cases', CASE_NAME);
const caseDir = join(process.env.HOME, 'codeman-cases', CASE_NAME);
const caseSrc = join(caseDir, 'src');
const caseTest = join(caseDir, 'test');
mkdirSync(caseSrc, { recursive: true });
@@ -123,7 +123,7 @@ async function capture() {
// Configure settings: enable subagents, disable Files pane
await setupPage.evaluate(() => {
const existing = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const existing = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
Object.assign(existing, {
showSubagents: true,
subagentTrackingEnabled: true,
@@ -132,7 +132,7 @@ async function capture() {
showProjectInsights: false,
showFileBrowser: false,
});
localStorage.setItem('claudeman-app-settings', JSON.stringify(existing));
localStorage.setItem('codeman-app-settings', JSON.stringify(existing));
});
await setupPage.reload({ waitUntil: 'domcontentloaded' });
await sleep(1000);
@@ -227,7 +227,7 @@ async function capture() {
// Re-apply settings (new context = fresh localStorage)
await recordPage.evaluate(() => {
const existing = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const existing = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
Object.assign(existing, {
showSubagents: true,
subagentTrackingEnabled: true,
@@ -236,7 +236,7 @@ async function capture() {
showProjectInsights: false,
showFileBrowser: false,
});
localStorage.setItem('claudeman-app-settings', JSON.stringify(existing));
localStorage.setItem('codeman-app-settings', JSON.stringify(existing));
});
await recordPage.reload({ waitUntil: 'domcontentloaded' });
await sleep(1000);
+8 -8
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Capture screenshots and GIFs of Claudeman's subagent visualization
* Capture screenshots and GIFs of Codeman's subagent visualization
*
* Uses real Claude sessions that spawn actual subagents via the Task tool.
*
@@ -23,7 +23,7 @@ import { mkdirSync, existsSync, unlinkSync, rmSync } from 'fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = join(__dirname, '..');
const OUTPUT_DIR = join(PROJECT_ROOT, 'docs', 'images');
const CASES_DIR = join(process.env.HOME, 'claudeman-cases');
const CASES_DIR = join(process.env.HOME, 'codeman-cases');
// Configuration
const PORT = 3198; // Unique port for capture script (avoid 3199 conflicts)
@@ -58,10 +58,10 @@ let serverProcess = null;
let browser = null;
/**
* Start the Claudeman web server
* Start the Codeman web server
*/
async function startServer() {
console.log(`Starting Claudeman server on port ${PORT}...`);
console.log(`Starting Codeman server on port ${PORT}...`);
serverProcess = spawn('npx', ['tsx', 'src/index.ts', 'web', '--port', String(PORT)], {
cwd: PROJECT_ROOT,
@@ -222,7 +222,7 @@ async function configureSettings(page) {
subagentActiveTabOnly: false, // Show all subagents regardless of active tab
showMonitor: true,
};
localStorage.setItem('claudeman-app-settings', JSON.stringify(settings));
localStorage.setItem('codeman-app-settings', JSON.stringify(settings));
});
await page.reload({ waitUntil: 'domcontentloaded' });
await sleep(500);
@@ -292,7 +292,7 @@ function convertToGif(inputPath, outputPath) {
*/
async function capture() {
console.log('='.repeat(60));
console.log('Claudeman Subagent Screenshot Capture');
console.log('Codeman Subagent Screenshot Capture');
console.log('='.repeat(60));
console.log(`Output directory: ${OUTPUT_DIR}`);
console.log(`Case name: ${CASE_NAME}`);
@@ -323,8 +323,8 @@ async function capture() {
const page = await context.newPage();
page.setDefaultTimeout(30000);
// Navigate to Claudeman
console.log('Loading Claudeman...');
// Navigate to Codeman
console.log('Loading Codeman...');
await page.goto(BASE_URL, { waitUntil: 'domcontentloaded' });
await sleep(1000);
@@ -1,10 +1,10 @@
[Unit]
Description=Claudeman Web Server
Description=Codeman Web Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/arkon/default/claudeman
WorkingDirectory=/home/arkon/default/codeman
ExecStart=/usr/bin/node dist/index.js web --https
Restart=always
RestartSec=5
@@ -15,7 +15,7 @@ Environment=HOME=/home/arkon
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=claudeman
SyslogIdentifier=codeman
# Resource limits
LimitNOFILE=65536
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Take mobile screenshots of Claudeman using Playwright
* Take mobile screenshots of Codeman using Playwright
* Emulates iPhone 15 Pro (closest to iPhone 17 Pro)
* Includes keyboard simulation
*/
+3 -3
View File
@@ -4,8 +4,8 @@
SESSION_ID="${1:-5ecdf859-3605-42d8-b0a2-f8f0a1b5ff98}"
DURATION_HOURS="${2:-12}"
ISSUES_FILE="/home/arkon/default/claudeman/respawn-monitor-issues.md"
FIXES_FILE="/home/arkon/default/claudeman/respawn-monitor-fixes.md"
ISSUES_FILE="/home/arkon/default/codeman/respawn-monitor-issues.md"
FIXES_FILE="/home/arkon/default/codeman/respawn-monitor-fixes.md"
API_URL="https://localhost:3000"
POLL_INTERVAL=60 # seconds
@@ -54,7 +54,7 @@ while [ $(date +%s) -lt $END_TIME ]; do
if [ -z "$RESP" ]; then
log_issue "ERROR" "API not responding - server may be down"
add_fix "API not responding" "Check if claudeman-web service is running:\n\`\`\`bash\nsystemctl --user status claudeman-web\nsystemctl --user restart claudeman-web\n\`\`\`"
add_fix "API not responding" "Check if codeman-web service is running:\n\`\`\`bash\nsystemctl --user status codeman-web\nsystemctl --user restart codeman-web\n\`\`\`"
sleep $POLL_INTERVAL
continue
fi
+4 -4
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Claudeman postinstall verification script
* Codeman postinstall verification script
* Runs after `npm install` to check environment readiness
*/
@@ -94,7 +94,7 @@ function getTmuxInstallInstructions() {
// Main Checks
// ============================================================================
console.log(colors.bold('Claudeman postinstall check...'));
console.log(colors.bold('Codeman postinstall check...'));
console.log('');
let hasWarnings = false;
@@ -259,11 +259,11 @@ if (hasErrors) {
console.log(colors.bold('Next steps:'));
if (isGlobalInstall) {
console.log(colors.dim(' 1. Start: ') + colors.cyan('claudeman web'));
console.log(colors.dim(' 1. Start: ') + colors.cyan('codeman web'));
console.log(colors.dim(' 2. Open: ') + colors.cyan('http://localhost:3000'));
} else {
console.log(colors.dim(' 1. Build: ') + colors.cyan('npm run build'));
console.log(colors.dim(' 2. Start: ') + colors.cyan('claudeman web'));
console.log(colors.dim(' 2. Start: ') + colors.cyan('codeman web'));
console.log(colors.dim(' 3. Open: ') + colors.cyan('http://localhost:3000'));
}
+4 -4
View File
@@ -8,7 +8,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const BASE_URL = 'https://localhost:3000';
const CASE_NAME = 'claudeman-ios';
const CASE_NAME = 'codeman-ios';
async function main() {
console.log('🎬 Starting Ralph Loop Wizard demo...\n');
@@ -25,7 +25,7 @@ async function main() {
});
const page = await context.newPage();
console.log('📱 Opening Claudeman web UI...');
console.log('📱 Opening Codeman web UI...');
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
@@ -41,8 +41,8 @@ async function main() {
await page.selectOption('#ralphCaseSelect', CASE_NAME);
// Load initprompt.md if it exists
const initPromptPath = join(homedir(), 'claudeman-cases', CASE_NAME, 'initprompt.md');
let taskDescription = 'Build an awesome iOS app for Claudeman!';
const initPromptPath = join(homedir(), 'codeman-cases', CASE_NAME, 'initprompt.md');
let taskDescription = 'Build an awesome iOS app for Codeman!';
if (existsSync(initPromptPath)) {
taskDescription = readFileSync(initPromptPath, 'utf-8').trim();
console.log(`📝 Loaded initprompt.md (${taskDescription.length} chars)`);
+5 -5
View File
@@ -10,8 +10,8 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const BASE_URL = 'https://localhost:3000';
const CASE_NAME = 'claudeman-ios';
const CASE_PATH = join(homedir(), 'claudeman-cases', CASE_NAME);
const CASE_NAME = 'codeman-ios';
const CASE_PATH = join(homedir(), 'codeman-cases', CASE_NAME);
async function main() {
// Read initprompt.md
@@ -49,7 +49,7 @@ async function main() {
page.on('pageerror', err => console.log(`[Browser Error] ${err.message}`));
try {
console.log('[1/10] Navigating to Claudeman...');
console.log('[1/10] Navigating to Codeman...');
await page.goto(BASE_URL, { timeout: 10000 });
console.log('[2/10] Clicking Ralph Loop button (.btn-ralph)...');
@@ -122,7 +122,7 @@ async function main() {
});
session = sessions
.filter(s => s.workingDir?.includes('claudeman-ios'))
.filter(s => s.workingDir?.includes('codeman-ios'))
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))[0];
if (session) {
@@ -151,7 +151,7 @@ async function main() {
console.log('SUCCESS! Ralph Loop started.');
console.log('========================================');
console.log(`Session ID: ${session?.id}`);
console.log(`Screen: claudeman-${session?.id?.slice(0, 8)}`);
console.log(`Screen: codeman-${session?.id?.slice(0, 8)}`);
console.log(`Status: ${session?.status}`);
console.log('');
console.log('Session is now running. View at: https://localhost:3000');
+6 -6
View File
@@ -8,7 +8,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const BASE_URL = 'https://localhost:3000';
const CASE_NAME = 'claudeman-ios';
const CASE_NAME = 'codeman-ios';
async function main() {
console.log('Starting Ralph Loop via wizard on production server...\n');
@@ -21,7 +21,7 @@ async function main() {
const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
console.log('1. Opening Claudeman web UI...');
console.log('1. Opening Codeman web UI...');
await page.goto(BASE_URL);
await page.waitForLoadState('networkidle');
@@ -36,8 +36,8 @@ async function main() {
await page.selectOption('#ralphCaseSelect', CASE_NAME);
// Load initprompt.md
const initPromptPath = join(homedir(), 'claudeman-cases', CASE_NAME, 'initprompt.md');
let taskDescription = 'Build an awesome iOS app for Claudeman!';
const initPromptPath = join(homedir(), 'codeman-cases', CASE_NAME, 'initprompt.md');
let taskDescription = 'Build an awesome iOS app for Codeman!';
if (existsSync(initPromptPath)) {
taskDescription = readFileSync(initPromptPath, 'utf-8').trim();
console.log(`5. Loaded initprompt.md (${taskDescription.length} chars)`);
@@ -87,13 +87,13 @@ async function main() {
return res.json();
});
const iosSession = sessions.find(s => s.workingDir?.includes('claudeman-ios'));
const iosSession = sessions.find(s => s.workingDir?.includes('codeman-ios'));
console.log('\n========================================');
console.log('SUCCESS! Ralph Loop started.');
console.log('========================================');
console.log(`Session ID: ${iosSession?.id}`);
console.log(`Screen: claudeman-${iosSession?.id?.slice(0, 8)}`);
console.log(`Screen: codeman-${iosSession?.id?.slice(0, 8)}`);
console.log(`Working Dir: ${iosSession?.workingDir}`);
console.log('\nThe session is now running on your production server.');
console.log('View it at: https://localhost:3000');
+12 -12
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
/**
* Browser test for clickable file links in Claudeman terminal
* Browser test for clickable file links in Codeman terminal
* Uses Playwright directly to ignore HTTPS errors
*/
@@ -33,8 +33,8 @@ async function main() {
const page = await context.newPage();
try {
// Navigate to Claudeman
console.log('Navigating to Claudeman...');
// Navigate to Codeman
console.log('Navigating to Codeman...');
await page.goto(BASE_URL, { waitUntil: 'networkidle', timeout: 30000 });
console.log('Page loaded: ' + await page.title());
@@ -63,9 +63,9 @@ async function main() {
await page.waitForTimeout(2000);
}
// Get active session (use claudeman session if exists)
const claudemanSession = sessions.find(s => s.workingDir?.includes('claudeman'));
const sessionId = claudemanSession?.id || sessions[0]?.id;
// Get active session (use codeman session if exists)
const codemanSession = sessions.find(s => s.workingDir?.includes('codeman'));
const sessionId = codemanSession?.id || sessions[0]?.id;
console.log(`Using session: ${sessionId}\n`);
// Send a command that outputs file paths
@@ -81,8 +81,8 @@ async function main() {
await page.waitForTimeout(2000);
// Take screenshot
await page.screenshot({ path: '/tmp/claudeman-link-test-1.png' });
console.log('Screenshot saved: /tmp/claudeman-link-test-1.png\n');
await page.screenshot({ path: '/tmp/codeman-link-test-1.png' });
console.log('Screenshot saved: /tmp/codeman-link-test-1.png\n');
// Check if terminal contains our file path
const terminalContent = await page.evaluate(() => {
@@ -132,8 +132,8 @@ async function main() {
console.log('Log viewer windows:', logViewerCount);
// Take final screenshot
await page.screenshot({ path: '/tmp/claudeman-link-test-2.png' });
console.log('\nFinal screenshot: /tmp/claudeman-link-test-2.png');
await page.screenshot({ path: '/tmp/codeman-link-test-2.png' });
console.log('\nFinal screenshot: /tmp/codeman-link-test-2.png');
// Test the link provider registration
console.log('\n--- Link Provider Debug ---');
@@ -155,8 +155,8 @@ async function main() {
} catch (error) {
console.error('Error:', error.message);
await page.screenshot({ path: '/tmp/claudeman-link-test-error.png' });
console.log('Error screenshot saved to /tmp/claudeman-link-test-error.png');
await page.screenshot({ path: '/tmp/codeman-link-test-error.png' });
console.log('Error screenshot saved to /tmp/codeman-link-test-error.png');
} finally {
await browser.close();
console.log('\nBrowser closed');
+2 -2
View File
@@ -1,8 +1,8 @@
#!/bin/bash
# Test script for clickable file links in Claudeman terminal
# Test script for clickable file links in Codeman terminal
# Creates a log file and continuously writes timestamped data
LOGFILE="/tmp/claudeman-test-$(date +%s).log"
LOGFILE="/tmp/codeman-test-$(date +%s).log"
echo "Created log file: $LOGFILE"
echo ""
+29 -29
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# ============================================================================
# Claudeman Sessions - Mobile-friendly Tmux Session Chooser
# Codeman Sessions - Mobile-friendly Tmux Session Chooser
# Optimized for iPhone/Termius (portrait ~45 chars, landscape ~95 chars)
# ============================================================================
#
@@ -8,7 +8,7 @@
# - Single-digit selection (1-9) for fast thumb typing
# - Compact display, no wasted space
# - Color-coded status for quick scanning
# - Names pulled from Claudeman state.json
# - Names pulled from Codeman state.json
# - Minimal keystrokes to attach
#
# Usage:
@@ -29,8 +29,8 @@ set -e
# Configuration
# ============================================================================
CLAUDEMAN_STATE="$HOME/.claudeman/state.json"
CLAUDEMAN_SESSIONS="$HOME/.claudeman/mux-sessions.json"
CODEMAN_STATE="$HOME/.codeman/state.json"
CODEMAN_SESSIONS="$HOME/.codeman/mux-sessions.json"
# iPhone 17 Pro portrait width (conservative)
@@ -106,22 +106,22 @@ truncate() {
find_full_session_id() {
local short_id="$1"
if [ -f "$CLAUDEMAN_STATE" ]; then
if [ -f "$CODEMAN_STATE" ]; then
local full_id
full_id=$(jq -r --arg short "$short_id" '
.sessions | keys[] | select(startswith($short))
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$full_id" ]; then
echo "$full_id"
return
fi
fi
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
if [ -f "$CODEMAN_SESSIONS" ]; then
local full_id
full_id=$(jq -r --arg short "$short_id" '
.[] | select(.sessionId | startswith($short)) | .sessionId
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ -n "$full_id" ]; then
echo "$full_id"
return
@@ -136,22 +136,22 @@ get_session_name() {
local name=""
local workdir=""
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
if [ -f "$CODEMAN_SESSIONS" ]; then
local result
result=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | "\(.name // "")\t\(.workingDir // "")"
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ -n "$result" ]; then
name="${result%% *}"
workdir="${result#* }"
fi
fi
if [ -z "$name" ] && [ -f "$CLAUDEMAN_STATE" ]; then
if [ -z "$name" ] && [ -f "$CODEMAN_STATE" ]; then
local result
result=$(jq -r --arg id "$session_id" '
.sessions | to_entries[] | select(.key | startswith($id)) | "\(.value.name // "")\t\(.value.workingDir // "")"
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$result" ]; then
name="${result%% *}"
[ -z "$workdir" ] && workdir="${result#* }"
@@ -174,22 +174,22 @@ get_session_name() {
get_working_dir() {
local session_id="$1"
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
if [ -f "$CODEMAN_SESSIONS" ]; then
local dir
dir=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | .workingDir // empty
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
echo "${dir/#$HOME/~}"
return
fi
fi
if [ -f "$CLAUDEMAN_STATE" ]; then
if [ -f "$CODEMAN_STATE" ]; then
local dir
dir=$(jq -r --arg id "$session_id" '
.sessions | to_entries[] | select(.key | startswith($id)) | .value.workingDir // empty
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$dir" ] && [ "$dir" != "null" ]; then
echo "${dir/#$HOME/~}"
return
@@ -201,12 +201,12 @@ get_working_dir() {
get_tokens() {
local session_id="$1"
if [ -f "$CLAUDEMAN_STATE" ]; then
if [ -f "$CODEMAN_STATE" ]; then
local tokens
tokens=$(jq -r --arg id "$session_id" '
.sessions | to_entries[] | select(.key | startswith($id)) |
((.value.inputTokens // 0) + (.value.outputTokens // 0))
' "$CLAUDEMAN_STATE" 2>/dev/null | head -1)
' "$CODEMAN_STATE" 2>/dev/null | head -1)
if [ -n "$tokens" ] && [ "$tokens" != "null" ] && [ "$tokens" -gt 0 ] 2>/dev/null; then
if [ "$tokens" -gt 1000 ]; then
@@ -223,11 +223,11 @@ get_tokens() {
get_respawn_status() {
local session_id="$1"
if [ -f "$CLAUDEMAN_SESSIONS" ]; then
if [ -f "$CODEMAN_SESSIONS" ]; then
local respawn_enabled
respawn_enabled=$(jq -r --arg id "$session_id" '
.[] | select(.sessionId | startswith($id)) | .respawnConfig.enabled // false
' "$CLAUDEMAN_SESSIONS" 2>/dev/null | head -1)
' "$CODEMAN_SESSIONS" 2>/dev/null | head -1)
if [ "$respawn_enabled" = "true" ]; then
echo "R"
@@ -273,8 +273,8 @@ parse_sessions() {
while IFS= read -r line; do
local session_name="${line%%:*}"
# Only show claudeman sessions
if [[ "$session_name" != claudeman-* ]]; then
# Only show codeman sessions
if [[ "$session_name" != codeman-* ]]; then
continue
fi
@@ -292,9 +292,9 @@ parse_sessions() {
MUX_NAMES+=("$session_name")
SESSION_STATES+=("$state")
# Extract session ID from claudeman session name
# Extract session ID from codeman session name
local session_id=""
local cm_regex='^claudeman-(.+)$'
local cm_regex='^codeman-(.+)$'
if [[ "$session_name" =~ $cm_regex ]]; then
session_id="${BASH_REMATCH[1]}"
fi
@@ -327,7 +327,7 @@ clear_screen() {
print_header() {
local count=${#SESSION_PIDS[@]}
echo -e "${B}${CYAN}Claudeman Sessions${R} ${D}($count)${R}"
echo -e "${B}${CYAN}Codeman Sessions${R} ${D}($count)${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
}
@@ -395,13 +395,13 @@ print_footer() {
print_no_sessions() {
clear_screen
echo -e "${B}${CYAN}Claudeman Sessions${R}"
echo -e "${B}${CYAN}Codeman Sessions${R}"
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
echo ""
echo -e " ${YELLOW}No tmux sessions found${R}"
echo ""
echo -e " ${D}Start one with:${R}"
echo -e " ${WHITE}claudeman web${R}"
echo -e " ${WHITE}codeman web${R}"
echo ""
echo -e "${D}$(printf '%.0s─' {1..32})${R}"
echo -e " ${GRAY}[${WHITE}r${GRAY}]efresh [${WHITE}q${GRAY}]uit${R}"
@@ -584,7 +584,7 @@ quick_attach() {
show_help() {
cat << 'EOF'
Claudeman Sessions - Mobile-friendly Tmux Session Chooser
Codeman Sessions - Mobile-friendly Tmux Session Chooser
USAGE:
sc Interactive chooser
@@ -607,7 +607,7 @@ INDICATORS:
TIPS:
- Detach from tmux: Ctrl+B D
- Session names from Claudeman state
- Session names from Codeman state
- Optimized for Termius/iPhone
EOF
+8 -8
View File
@@ -1,8 +1,8 @@
#!/bin/bash
#
# Claudeman Tmux Session Manager
# Codeman Tmux Session Manager
# Interactive tool with arrow key navigation
# Reads from ~/.claudeman/mux-sessions.json
# Reads from ~/.codeman/mux-sessions.json
#
# Colors
@@ -17,8 +17,8 @@ DIM='\033[2m'
NC='\033[0m'
REVERSE='\033[7m'
# Use the same path as claudeman (src/tmux-manager.ts)
SESSIONS_FILE="${HOME}/.claudeman/mux-sessions.json"
# Use the same path as codeman (src/tmux-manager.ts)
SESSIONS_FILE="${HOME}/.codeman/mux-sessions.json"
# Cached data
@@ -133,7 +133,7 @@ kill_session() {
# Draw header (only once)
draw_header() {
echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}${CYAN}║ Claudeman Tmux Session Manager ║${NC}"
echo -e "${BOLD}${CYAN}║ Codeman Tmux Session Manager ║${NC}"
echo -e "${BOLD}${CYAN}║ ${DIM}Press q or Esc to exit${CYAN} ║${NC}"
echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════╝${NC}"
echo ""
@@ -401,11 +401,11 @@ quick_list() {
local now=$(date +%s)
if [[ $CACHED_COUNT -eq 0 ]]; then
echo "No claudeman sessions found."
echo "No codeman sessions found."
exit 0
fi
echo "Claudeman Sessions ($SESSIONS_FILE):"
echo "Codeman Sessions ($SESSIONS_FILE):"
echo ""
printf "%-4s %-22s %-14s %-10s %-8s\n" "#" "NAME" "RUNNING" "STATUS" "MODE"
echo "─────────────────────────────────────────────────────────────────"
@@ -432,7 +432,7 @@ quick_list() {
# Usage
usage() {
cat << EOF
Claudeman Tmux Session Manager
Codeman Tmux Session Manager
Reads from: $SESSIONS_FILE
+4 -4
View File
@@ -11,9 +11,9 @@
* - Event emission for state changes
*
* Subclasses implement:
* - `muxNamePrefix`: Prefix for mux session names (e.g., 'claudeman-aicheck-')
* - `muxNamePrefix`: Prefix for mux session names (e.g., 'codeman-aicheck-')
* - `doneMarker`: Completion marker in output file (e.g., '__AICHECK_DONE__')
* - `tempFilePrefix`: Prefix for temp files (e.g., 'claudeman-aicheck')
* - `tempFilePrefix`: Prefix for temp files (e.g., 'codeman-aicheck')
* - `logPrefix`: Prefix for log messages (e.g., '[AiIdleChecker]')
* - `buildPrompt()`: Build the prompt from terminal buffer
* - `parseVerdict()`: Parse the verdict from AI output
@@ -143,13 +143,13 @@ export abstract class AiCheckerBase<
// ========== Abstract Properties ==========
/** Prefix for mux session names (e.g., 'claudeman-aicheck-') */
/** Prefix for mux session names (e.g., 'codeman-aicheck-') */
protected abstract readonly muxNamePrefix: string;
/** Marker written to temp file when check is complete */
protected abstract readonly doneMarker: string;
/** Prefix for temp files (e.g., 'claudeman-aicheck') */
/** Prefix for temp files (e.g., 'codeman-aicheck') */
protected abstract readonly tempFilePrefix: string;
/** Prefix for log messages (e.g., '[AiIdleChecker]') */
+3 -3
View File
@@ -8,7 +8,7 @@
* ## How It Works
*
* 1. Generate temp file path for output capture
* 2. Spawn tmux: `tmux new-session -d -s claudeman-aicheck-<short> bash -c 'claude -p ...'`
* 2. Spawn tmux: `tmux new-session -d -s codeman-aicheck-<short> bash -c 'claude -p ...'`
* 3. Poll the temp file every 500ms for `__AICHECK_DONE__` marker
* 4. Parse the file content for IDLE/WORKING on the first word
* 5. Kill tmux session and delete temp file
@@ -129,9 +129,9 @@ export class AiIdleChecker extends AiCheckerBase<
AiCheckResult,
AiCheckState
> {
protected readonly muxNamePrefix = 'claudeman-aicheck-';
protected readonly muxNamePrefix = 'codeman-aicheck-';
protected readonly doneMarker = '__AICHECK_DONE__';
protected readonly tempFilePrefix = 'claudeman-aicheck';
protected readonly tempFilePrefix = 'codeman-aicheck';
protected readonly logPrefix = '[AiIdleChecker]';
protected readonly checkDescription = 'AI idle check';
+3 -3
View File
@@ -8,7 +8,7 @@
* ## How It Works
*
* 1. Generate temp file path for output capture
* 2. Spawn tmux: `tmux new-session -d -s claudeman-plancheck-<short> bash -c 'claude -p ...'`
* 2. Spawn tmux: `tmux new-session -d -s codeman-plancheck-<short> bash -c 'claude -p ...'`
* 3. Poll the temp file every 500ms for `__PLANCHECK_DONE__` marker
* 4. Parse the file content for PLAN_MODE/NOT_PLAN_MODE on the first line
* 5. Kill tmux session and delete temp file
@@ -91,9 +91,9 @@ export class AiPlanChecker extends AiCheckerBase<
AiPlanCheckResult,
AiPlanCheckState
> {
protected readonly muxNamePrefix = 'claudeman-plancheck-';
protected readonly muxNamePrefix = 'codeman-plancheck-';
protected readonly doneMarker = '__PLANCHECK_DONE__';
protected readonly tempFilePrefix = 'claudeman-plancheck';
protected readonly tempFilePrefix = 'codeman-plancheck';
protected readonly logPrefix = '[AiPlanChecker]';
protected readonly checkDescription = 'AI plan check';
+4 -4
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Claudeman CLI command definitions
* @fileoverview Codeman CLI command definitions
*
* Defines all CLI commands and subcommands for managing Claude sessions,
* tasks, Ralph loops, and the web server.
@@ -22,7 +22,7 @@ const pkg = require('../package.json') as { version: string };
const program = new Command();
program
.name('claudeman')
.name('codeman')
.description('Claude Code session manager with autonomous Ralph Loop')
.version(pkg.version);
@@ -407,7 +407,7 @@ program
? sessions.filter((s) => s.isBusy()).length
: storedValues.filter((s) => s.status === 'busy').length;
console.log(chalk.bold('\nClaudeman Status'));
console.log(chalk.bold('\nCodeman Status'));
console.log('─'.repeat(40));
console.log(chalk.bold('\nSessions:'));
@@ -504,7 +504,7 @@ program
const https = !!options.https;
const protocol = https ? 'https' : 'http';
console.log(chalk.cyan(`Starting Claudeman web interface on port ${port}${https ? ' (HTTPS)' : ''}...`));
console.log(chalk.cyan(`Starting Codeman web interface on port ${port}${https ? ' (HTTPS)' : ''}...`));
try {
const server = await startWebServer(port, https);
+1 -1
View File
@@ -2,7 +2,7 @@
* @fileoverview Centralized buffer size limits for memory management.
*
* These constants define the maximum sizes for various buffers throughout
* Claudeman. Consolidating them here ensures consistent limits and makes
* Codeman. Consolidating them here ensures consistent limits and makes
* it easy to tune memory usage.
*
* Memory Budget Rationale (for 20 concurrent sessions):
+6 -6
View File
@@ -2,9 +2,9 @@
* @fileoverview Claude Code hooks configuration generator
*
* Generates .claude/settings.local.json with hook definitions that POST
* to Claudeman's /api/hook-event endpoint when Claude Code fires
* notification or stop hooks. Uses $CLAUDEMAN_API_URL and
* $CLAUDEMAN_SESSION_ID env vars (set on every managed session) so the
* to Codeman's /api/hook-event endpoint when Claude Code fires
* notification or stop hooks. Uses $CODEMAN_API_URL and
* $CODEMAN_SESSION_ID env vars (set on every managed session) so the
* config is static per case directory.
*/
@@ -18,7 +18,7 @@ import type { HookEventType } from './types.js';
* Generates the hooks section for .claude/settings.local.json
*
* The hook commands read stdin JSON from Claude Code (contains tool_name,
* tool_input, etc.) and forward it as the `data` field to Claudeman's API.
* tool_input, etc.) and forward it as the `data` field to Codeman's API.
* Env vars are resolved at runtime by the shell, so the config is static
* per case directory.
*/
@@ -27,9 +27,9 @@ export function generateHooksConfig(): { hooks: Record<string, unknown[]> } {
// Falls back to empty object if stdin is unavailable or malformed.
const curlCmd = (event: HookEventType) =>
`HOOK_DATA=$(cat 2>/dev/null || echo '{}'); ` +
`curl -s -X POST "$CLAUDEMAN_API_URL/api/hook-event" ` +
`curl -s -X POST "$CODEMAN_API_URL/api/hook-event" ` +
`-H 'Content-Type: application/json' ` +
`-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CLAUDEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` +
`-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CODEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` +
`2>/dev/null || true`;
return {
+2 -2
View File
@@ -137,7 +137,7 @@ export class ImageWatcher extends EventEmitter {
/**
* Start watching a session's working directory for new images.
*
* @param sessionId - Claudeman session ID
* @param sessionId - Codeman session ID
* @param workingDir - Path to the session's working directory
*/
watchSession(sessionId: string, workingDir: string): void {
@@ -200,7 +200,7 @@ export class ImageWatcher extends EventEmitter {
/**
* Stop watching a session's working directory.
*
* @param sessionId - Claudeman session ID to stop watching
* @param sessionId - Codeman session ID to stop watching
*/
unwatchSession(sessionId: string): void {
const watcher = this.sessionWatchers.get(sessionId);
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
/**
* @fileoverview Claudeman CLI entry point
* @fileoverview Codeman CLI entry point
*
* This is the main executable entry point for the Claudeman CLI.
* This is the main executable entry point for the Codeman CLI.
* It sets up global error handlers and invokes the CLI parser.
*
* @module index
+3 -3
View File
@@ -13,9 +13,9 @@ import type { ProcessStats, PersistedRespawnConfig, NiceConfig, ClaudeMode, Sess
* Multiplexer session metadata.
*/
export interface MuxSession {
/** Claudeman session ID */
/** Codeman session ID */
sessionId: string;
/** Multiplexer session name (e.g., "claudeman-abc12345") */
/** Multiplexer session name (e.g., "codeman-abc12345") */
muxName: string;
/** Process PID */
pid: number;
@@ -105,7 +105,7 @@ export interface TerminalMultiplexer extends EventEmitter {
/** Get all tracked sessions */
getSessions(): MuxSession[];
/** Get a session by Claudeman session ID */
/** Get a session by Codeman session ID */
getSession(sessionId: string): MuxSession | undefined;
/** Get all sessions with process resource statistics */
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Run Summary Tracker for Claudeman sessions
* @fileoverview Run Summary Tracker for Codeman sessions
*
* Tracks significant events during a session's lifetime to provide users
* with a consolidated "what happened while I was away" view.
+2 -2
View File
@@ -2,7 +2,7 @@
* @fileoverview Append-only JSONL audit log for session lifecycle events.
*
* Records every session creation, start, exit, deletion, recovery, and server
* start/stop to ~/.claudeman/session-lifecycle.jsonl. Survives server restarts
* start/stop to ~/.codeman/session-lifecycle.jsonl. Survives server restarts
* (unlike RunSummary which is in-memory only).
*
* @module session-lifecycle-log
@@ -22,7 +22,7 @@ export class SessionLifecycleLog {
private writeQueue: Promise<void> = Promise.resolve();
constructor(filePath?: string) {
this.filePath = filePath || join(homedir(), '.claudeman', 'session-lifecycle.jsonl');
this.filePath = filePath || join(homedir(), '.codeman', 'session-lifecycle.jsonl');
const dir = dirname(this.filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true, mode: 0o700 });
+15 -15
View File
@@ -400,8 +400,8 @@ export class Session extends EventEmitter {
this.mode = config.mode || 'claude';
this._name = config.name || '';
this._lastActivityAt = this.createdAt;
// Set claudeSessionId immediately — Claudeman always passes --session-id ${this.id}
// to Claude CLI, so the Claude session ID always matches the Claudeman session ID.
// Set claudeSessionId immediately — Codeman always passes --session-id ${this.id}
// to Claude CLI, so the Claude session ID always matches the Codeman session ID.
// This ensures subagent matching works even for recovered sessions (where
// startInteractive() hasn't been called yet).
this._claudeSessionId = this.id;
@@ -1041,7 +1041,7 @@ export class Session extends EventEmitter {
throw new Error('OpenCode sessions require tmux. Direct PTY fallback is not supported.');
}
try {
// Pass --session-id to use the SAME ID as the Claudeman session
// Pass --session-id to use the SAME ID as the Codeman session
// This ensures subagents can be directly matched to the correct tab
const args = [...this._buildPermissionArgs(), '--session-id', this.id];
if (this._model) args.push('--model', this._model);
@@ -1058,10 +1058,10 @@ export class Session extends EventEmitter {
TERM: 'xterm-256color',
COLORTERM: undefined,
CLAUDECODE: undefined,
// Inform Claude it's running within Claudeman (helps prevent self-termination)
CLAUDEMAN_MUX: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
// Inform Claude it's running within Codeman (helps prevent self-termination)
CODEMAN_MUX: '1',
CODEMAN_SESSION_ID: this.id,
CODEMAN_API_URL: process.env.CODEMAN_API_URL || 'http://localhost:3000',
},
});
} catch (spawnErr) {
@@ -1376,9 +1376,9 @@ export class Session extends EventEmitter {
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
TERM: 'xterm-256color',
CLAUDEMAN_MUX: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
CODEMAN_MUX: '1',
CODEMAN_SESSION_ID: this.id,
CODEMAN_API_URL: process.env.CODEMAN_API_URL || 'http://localhost:3000',
},
});
} catch (spawnErr) {
@@ -1509,10 +1509,10 @@ export class Session extends EventEmitter {
TERM: 'xterm-256color',
COLORTERM: undefined,
CLAUDECODE: undefined,
// Inform Claude it's running within Claudeman
CLAUDEMAN_MUX: '1',
CLAUDEMAN_SESSION_ID: this.id,
CLAUDEMAN_API_URL: process.env.CLAUDEMAN_API_URL || 'http://localhost:3000',
// Inform Claude it's running within Codeman
CODEMAN_MUX: '1',
CODEMAN_SESSION_ID: this.id,
CODEMAN_API_URL: process.env.CODEMAN_API_URL || 'http://localhost:3000',
},
});
} catch (spawnErr) {
@@ -1867,7 +1867,7 @@ export class Session extends EventEmitter {
}
// Parse Claude Code CLI info from terminal startup output
// Extracts version, model, and account type for display in Claudeman UI
// Extracts version, model, and account type for display in Codeman UI
// Note: Expects cleanData with ANSI codes already stripped by caller
private parseClaudeCodeInfo(cleanData: string): void {
// Only parse once per session (during startup)
+19 -3
View File
@@ -1,8 +1,8 @@
/**
* @fileoverview Persistent JSON state storage for Claudeman.
* @fileoverview Persistent JSON state storage for Codeman.
*
* This module provides the StateStore class which persists application state
* to `~/.claudeman/state.json` with debounced writes to prevent excessive disk I/O.
* to `~/.codeman/state.json` with debounced writes to prevent excessive disk I/O.
*
* State is split into two files:
* - `state.json`: Main app state (sessions, tasks, config)
@@ -68,7 +68,23 @@ export class StateStore {
private _saveInFlight: Promise<void> | null = null;
constructor(filePath?: string) {
this.filePath = filePath || join(homedir(), '.claudeman', 'state.json');
// Migrate legacy data directory (~/.claudeman → ~/.codeman)
if (!filePath) {
const legacyDir = join(homedir(), '.claudeman');
const newDir = join(homedir(), '.codeman');
if (existsSync(legacyDir) && !existsSync(newDir)) {
console.log(`[state-store] Migrating data directory: ${legacyDir} → ${newDir}`);
renameSync(legacyDir, newDir);
}
const legacyCasesDir = join(homedir(), 'claudeman-cases');
const newCasesDir = join(homedir(), 'codeman-cases');
if (existsSync(legacyCasesDir) && !existsSync(newCasesDir)) {
console.log(`[state-store] Migrating cases directory: ${legacyCasesDir} → ${newCasesDir}`);
renameSync(legacyCasesDir, newCasesDir);
}
}
this.filePath = filePath || join(homedir(), '.codeman', 'state.json');
this.ralphStatePath = this.filePath.replace('.json', '-inner.json');
this.state = this.load();
this.state.config.stateFilePath = this.filePath;
+10 -10
View File
@@ -310,7 +310,7 @@ export class SubagentWatcher extends EventEmitter {
/**
* Run pgrep once and read /proc info for all Claude PIDs in parallel.
* Returns a Map of pid -> { environ, cmdline } for subagent processes only.
* Excludes main Claudeman-managed Claude processes (CLAUDEMAN_MUX=1).
* Excludes main Codeman-managed Claude processes (CODEMAN_MUX=1).
* Also updates cached PIDs on tracked agents when a match is found.
*/
private async getClaudePids(): Promise<Map<number, { environ: string; cmdline: string }>> {
@@ -330,8 +330,8 @@ export class SubagentWatcher extends EventEmitter {
let cmdline = '';
try { environ = await readFile(`/proc/${pid}/environ`, 'utf8'); } catch { /* skip */ }
try { cmdline = await readFile(`/proc/${pid}/cmdline`, 'utf8'); } catch { /* skip */ }
// Skip main Claudeman-managed Claude processes — only track subagents
if (environ.includes('CLAUDEMAN_MUX=1')) return;
// Skip main Codeman-managed Claude processes — only track subagents
if (environ.includes('CODEMAN_MUX=1')) return;
if (environ || cmdline) {
result.set(pid, { environ, cmdline });
}
@@ -562,8 +562,8 @@ export class SubagentWatcher extends EventEmitter {
}
/**
* Get subagents for a specific Claudeman session
* Maps Claudeman working directory to Claude's project hash
* Get subagents for a specific Codeman session
* Maps Codeman working directory to Claude's project hash
*/
getSubagentsForSession(workingDir: string): SubagentInfo[] {
const projectHash = this.getProjectHash(workingDir);
@@ -677,7 +677,7 @@ export class SubagentWatcher extends EventEmitter {
}
/**
* Kill all subagents for a specific Claudeman session.
* Kill all subagents for a specific Codeman session.
* IMPORTANT: Must scope to sessionId to avoid cross-session kills.
* All sessions in the same workingDir share a projectHash, so filtering
* by workingDir alone would kill subagents belonging to OTHER sessions.
@@ -696,7 +696,7 @@ export class SubagentWatcher extends EventEmitter {
/**
* Find the process ID of a Claude subagent by its session ID.
* Searches /proc for claude processes with matching session ID in environment.
* Skips main Claudeman-managed Claude processes (identified by CLAUDEMAN_MUX=1).
* Skips main Codeman-managed Claude processes (identified by CODEMAN_MUX=1).
* Caches the discovered PID on the matching agent info for future fast checks.
*/
private async findSubagentProcess(sessionId: string): Promise<number | null> {
@@ -721,9 +721,9 @@ export class SubagentWatcher extends EventEmitter {
// Can't read this process's environ - skip
}
// Defense-in-depth: never kill a main Claudeman-managed Claude process.
// Main processes have CLAUDEMAN_MUX=1 in their environment; subagents don't.
if (environ.includes('CLAUDEMAN_MUX=1')) continue;
// Defense-in-depth: never kill a main Codeman-managed Claude process.
// Main processes have CODEMAN_MUX=1 in their environment; subagents don't.
if (environ.includes('CODEMAN_MUX=1')) continue;
if (environ.includes(sessionId)) {
// Cache PID on the matching agent
+2 -2
View File
@@ -2,7 +2,7 @@
* @fileoverview Agent Teams Watcher
*
* Polls ~/.claude/teams/ and ~/.claude/tasks/ for agent team activity.
* Matches teams to Claudeman sessions via leadSessionId and emits
* Matches teams to Codeman sessions via leadSessionId and emits
* events for UI updates and team-aware idle detection.
*
* @module team-watcher
@@ -70,7 +70,7 @@ export class TeamWatcher extends EventEmitter {
return Array.from(this.teams.values());
}
/** Get team associated with a Claudeman session (matched by leadSessionId) */
/** Get team associated with a Codeman session (matched by leadSessionId) */
getTeamForSession(sessionId: string): TeamConfig | undefined {
const teamName = this.sessionToTeam.get(sessionId);
if (teamName) {
+3 -3
View File
@@ -18,11 +18,11 @@ Then update the Project Overview section below.
---
## Claudeman Environment
## Codeman Environment
This session is managed by **Claudeman** and runs within a tmux session.
This session is managed by **Codeman** and runs within a tmux session.
**Important**: Check for `CLAUDEMAN_MUX=1` environment variable to confirm.
**Important**: Check for `CODEMAN_MUX=1` environment variable to confirm.
- Do NOT attempt to kill your own tmux session
- The session persists across disconnects - your work is safe
- Token usage, costs, and background tasks are tracked externally
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* @fileoverview CLAUDE.md template generation
*
* Generates CLAUDE.md configuration files for new Claudeman projects.
* Generates CLAUDE.md configuration files for new Codeman projects.
* Reads the bundled case-template.md and performs placeholder substitution.
* Supports custom templates via settings.json override.
*
+23 -20
View File
@@ -10,7 +10,7 @@
* - **Reliable input**: `send-keys -l` sends literal text in a single command
* - **Teammate support**: Immutable pane IDs enable targeting individual teammates
*
* tmux sessions are named `claudeman-{sessionId}` and stored in ~/.claudeman/mux-sessions.json.
* tmux sessions are named `codeman-{sessionId}` and stored in ~/.codeman/mux-sessions.json.
*
* Key features:
* - `send-keys 'text' Enter` sends literal text in a single command
@@ -74,15 +74,18 @@ const DEFAULT_STATS_INTERVAL_MS = 2000;
* - Create a tmux session
* - Send input to a tmux session
* - Discover/reconcile real tmux sessions
* - Read/write ~/.claudeman/mux-sessions.json
* - Read/write ~/.codeman/mux-sessions.json
*/
const IS_TEST_MODE = !!process.env.VITEST;
/** Path to persisted mux session metadata */
const MUX_SESSIONS_FILE = join(homedir(), '.claudeman', 'mux-sessions.json');
const MUX_SESSIONS_FILE = join(homedir(), '.codeman', 'mux-sessions.json');
/** Regex to validate tmux session names (only allow safe characters) */
const SAFE_MUX_NAME_PATTERN = /^claudeman-[a-f0-9-]+$/;
const SAFE_MUX_NAME_PATTERN = /^codeman-[a-f0-9-]+$/;
/** Legacy pattern for pre-rename sessions (claudeman- prefix) */
const LEGACY_MUX_NAME_PATTERN = /^claudeman-[a-f0-9-]+$/;
/** Regex to validate tmux pane targets (e.g., "%0", "%1", "0", "1") */
const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
@@ -92,7 +95,7 @@ const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
* Prevents command injection via malformed session IDs.
*/
function isValidMuxName(name: string): boolean {
return SAFE_MUX_NAME_PATTERN.test(name);
return SAFE_MUX_NAME_PATTERN.test(name) || LEGACY_MUX_NAME_PATTERN.test(name);
}
/**
@@ -336,7 +339,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
*/
async createSession(options: CreateSessionOptions): Promise<MuxSession> {
const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig } = options;
const muxName = `claudeman-${sessionId.slice(0, 8)}`;
const muxName = `codeman-${sessionId.slice(0, 8)}`;
if (!isValidMuxName(muxName)) {
throw new Error('Invalid session name: contains unsafe characters');
@@ -382,10 +385,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
'export LANG=en_US.UTF-8',
'export LC_ALL=en_US.UTF-8',
'unset COLORTERM',
'export CLAUDEMAN_MUX=1',
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
`export CLAUDEMAN_MUX_NAME=${muxName}`,
`export CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`,
'export CODEMAN_MUX=1',
`export CODEMAN_SESSION_ID=${sessionId}`,
`export CODEMAN_MUX_NAME=${muxName}`,
`export CODEMAN_API_URL=${process.env.CODEMAN_API_URL || 'http://localhost:3000'}`,
];
// Only unset CLAUDECODE for Claude sessions
if (mode === 'claude') envExports.splice(2, 0, 'unset CLAUDECODE');
@@ -440,7 +443,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// Mouse mode is OFF by default so xterm.js handles text selection natively.
// It gets enabled dynamically when panes are split (agent teams).
const configPromises: Promise<void>[] = [
// Disable tmux status bar — Claudeman's web UI provides session info
// Disable tmux status bar — Codeman's web UI provides session info
execAsync(`tmux set-option -t "${muxName}" status off`, { timeout: EXEC_TIMEOUT_MS })
.then(() => {}).catch(() => { /* Non-critical — session still works with status bar */ }),
// Override global remain-on-exit with session-level setting
@@ -567,10 +570,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
'export LANG=en_US.UTF-8',
'export LC_ALL=en_US.UTF-8',
'unset COLORTERM',
'export CLAUDEMAN_MUX=1',
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
`export CLAUDEMAN_MUX_NAME=${muxName}`,
`export CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`,
'export CODEMAN_MUX=1',
`export CODEMAN_SESSION_ID=${sessionId}`,
`export CODEMAN_MUX_NAME=${muxName}`,
`export CODEMAN_API_URL=${process.env.CODEMAN_API_URL || 'http://localhost:3000'}`,
];
if (mode === 'claude') envExports.splice(2, 0, 'unset CLAUDECODE');
const envExportsStr = envExports.join(' && ');
@@ -685,7 +688,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
}
// SAFETY: Never kill the tmux session we're running inside of
const currentMuxName = process.env.CLAUDEMAN_MUX_NAME;
const currentMuxName = process.env.CODEMAN_MUX_NAME;
if (currentMuxName && session.muxName === currentMuxName) {
console.error(`[TmuxManager] BLOCKED: Refusing to kill own tmux session: ${session.muxName}`);
return false;
@@ -824,7 +827,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
}
}
// Discover unknown claudeman sessions
// Discover unknown codeman sessions
try {
const output = execSync(
"tmux list-sessions -F '#{session_name}' 2>/dev/null || true",
@@ -833,7 +836,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
for (const line of output.split('\n')) {
const sessionName = line.trim();
if (!sessionName || !sessionName.startsWith('claudeman-')) continue;
if (!sessionName || (!sessionName.startsWith('codeman-') && !sessionName.startsWith('claudeman-'))) continue;
// Check if this session is already known
let isKnown = false;
@@ -846,7 +849,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
if (!isKnown) {
// Extract session ID fragment from name
const fragment = sessionName.replace('claudeman-', '');
const fragment = sessionName.replace(/^(?:codeman|claudeman)-/, '');
const sessionId = `restored-${fragment}`;
const pid = this.getPanePid(sessionName);
@@ -1310,7 +1313,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
return false;
}
// Build target: sessionName.paneId (e.g., "claudeman-abc12345.%1")
// Build target: sessionName.paneId (e.g., "codeman-abc12345.%1")
const target = paneTarget.startsWith('%')
? `${muxName}.${paneTarget}`
: `${muxName}.%${paneTarget}`;
+4 -4
View File
@@ -1,8 +1,8 @@
/**
* @fileoverview Type definitions for Claudeman
* @fileoverview Type definitions for Codeman
*
* This module contains all TypeScript interfaces, types, and enums used
* throughout the Claudeman application. It provides type safety for:
* throughout the Codeman application. It provides type safety for:
* - Session management
* - Task queue operations
* - Ralph Loop configuration
@@ -859,7 +859,7 @@ export const DEFAULT_CONFIG: AppConfig = {
* Types for tracking Ralph Wiggum loops and todo lists
* running inside Claude Code sessions.
*
* This allows Claudeman to detect and display when Claude Code
* This allows Codeman to detect and display when Claude Code
* is running its own autonomous loops internally.
*/
@@ -1369,7 +1369,7 @@ export interface ActiveBashTool {
* Used to trigger automatic image popup display in the web UI.
*/
export interface ImageDetectedEvent {
/** Claudeman session ID where the image was detected */
/** Codeman session ID where the image was detected */
sessionId: string;
/** Full path to the detected image file */
filePath: string;
+57 -44
View File
@@ -1,4 +1,4 @@
// Claudeman App - Tab-based Terminal UI
// Codeman App - Tab-based Terminal UI
// ============================================================================
// Constants
@@ -852,14 +852,14 @@ const VoiceInput = {
_getDeepgramConfig() {
try {
return JSON.parse(localStorage.getItem('claudeman-voice-settings') || '{}');
return JSON.parse(localStorage.getItem('codeman-voice-settings') || '{}');
} catch (_e) {
return {};
}
},
_saveDeepgramConfig(config) {
localStorage.setItem('claudeman-voice-settings', JSON.stringify(config));
localStorage.setItem('codeman-voice-settings', JSON.stringify(config));
},
_shouldUseDeepgram() {
@@ -2294,7 +2294,7 @@ class NotificationManager {
// Get storage key for notification prefs (device-specific)
getStorageKey() {
const isMobile = MobileDetection.getDeviceType() === 'mobile';
return isMobile ? 'claudeman-notification-prefs-mobile' : 'claudeman-notification-prefs';
return isMobile ? 'codeman-notification-prefs-mobile' : 'codeman-notification-prefs';
}
savePreferences() {
@@ -2449,11 +2449,11 @@ class NotificationManager {
this.titleFlashInterval = setInterval(() => {
this.titleFlashState = !this.titleFlashState;
document.title = this.titleFlashState
? `\u26A0\uFE0F (${this.unreadCount}) Claudeman`
? `\u26A0\uFE0F (${this.unreadCount}) Codeman`
: this.originalTitle;
}, TITLE_FLASH_INTERVAL_MS);
// Set immediately
document.title = `\u26A0\uFE0F (${this.unreadCount}) Claudeman`;
document.title = `\u26A0\uFE0F (${this.unreadCount}) Codeman`;
}
}
}
@@ -2488,7 +2488,7 @@ class NotificationManager {
if (now - this.lastBrowserNotifTime < 3000) return;
this.lastBrowserNotifTime = now;
const notif = new Notification(`Claudeman: ${title}`, {
const notif = new Notification(`Codeman: ${title}`, {
body,
tag, // Groups same-tag notifications
icon: '/favicon.ico',
@@ -2634,7 +2634,7 @@ class NotificationManager {
}
}
class ClaudemanApp {
class CodemanApp {
constructor() {
this.sessions = new Map();
this.sessionOrder = []; // Track tab order for drag-and-drop reordering
@@ -2919,7 +2919,7 @@ class ClaudemanApp {
initTerminal() {
// Load scrollback setting from localStorage (default 5000)
const scrollback = parseInt(localStorage.getItem('claudeman-scrollback')) || DEFAULT_SCROLLBACK;
const scrollback = parseInt(localStorage.getItem('codeman-scrollback')) || DEFAULT_SCROLLBACK;
this.terminal = new Terminal({
theme: {
@@ -4023,7 +4023,7 @@ class ClaudemanApp {
this._cleanupSessionData(data.id);
if (this.activeSessionId === data.id) {
this.activeSessionId = null;
try { localStorage.removeItem('claudeman-active-session'); } catch {}
try { localStorage.removeItem('codeman-active-session'); } catch {}
this.terminal.clear();
this.showWelcome();
}
@@ -4709,11 +4709,11 @@ class ClaudemanApp {
}
this.renderSubagentPanel();
// Find which Claudeman session owns this subagent (direct claudeSessionId match only)
// Find which Codeman session owns this subagent (direct claudeSessionId match only)
this.findParentSessionForSubagent(data.agentId);
// Auto-open window for new active agents — but ONLY if they belong to a Claudeman session tab.
// Agents from external Claude sessions (not managed by Claudeman) should not pop up.
// Auto-open window for new active agents — but ONLY if they belong to a Codeman session tab.
// Agents from external Claude sessions (not managed by Codeman) should not pop up.
if (data.status === 'active') {
const agentForCheck = this.subagents.get(data.agentId);
const hasMatchingTab = agentForCheck?.sessionId &&
@@ -5106,11 +5106,11 @@ class ClaudemanApp {
const headerVersionEl = this.$('headerVersion');
if (versionEl) {
versionEl.textContent = `v${data.version}`;
versionEl.title = `Claudeman v${data.version}`;
versionEl.title = `Codeman v${data.version}`;
}
if (headerVersionEl) {
headerVersionEl.textContent = `v${data.version}`;
headerVersionEl.title = `Claudeman v${data.version}`;
headerVersionEl.title = `Codeman v${data.version}`;
}
}
@@ -5299,7 +5299,7 @@ class ClaudemanApp {
// Priority: current active > localStorage > first session
let restoreId = previousActiveId;
if (!restoreId || !this.sessions.has(restoreId)) {
try { restoreId = localStorage.getItem('claudeman-active-session'); } catch {}
try { restoreId = localStorage.getItem('codeman-active-session'); } catch {}
}
if (restoreId && this.sessions.has(restoreId)) {
this.selectSession(restoreId);
@@ -5605,7 +5605,7 @@ class ClaudemanApp {
// Load session order from localStorage
loadSessionOrder() {
try {
const saved = localStorage.getItem('claudeman-session-order');
const saved = localStorage.getItem('codeman-session-order');
return saved ? JSON.parse(saved) : [];
} catch {
return [];
@@ -5615,7 +5615,7 @@ class ClaudemanApp {
// Save session order to localStorage
saveSessionOrder() {
try {
localStorage.setItem('claudeman-session-order', JSON.stringify(this.sessionOrder));
localStorage.setItem('codeman-session-order', JSON.stringify(this.sessionOrder));
} catch {
// Ignore storage errors
}
@@ -5924,7 +5924,7 @@ class ClaudemanApp {
this._localEchoOverlay.suppressBufferDetection();
}
this.activeSessionId = sessionId;
try { localStorage.setItem('claudeman-active-session', sessionId); } catch {}
try { localStorage.setItem('codeman-active-session', sessionId); } catch {}
this.hideWelcome();
// Clear idle hooks on view, but keep action hooks until user interacts
this.clearPendingHooks(sessionId, 'idle_prompt');
@@ -6160,7 +6160,7 @@ class ClaudemanApp {
if (this.activeSessionId === sessionId) {
this.activeSessionId = null;
try { localStorage.removeItem('claudeman-active-session'); } catch {}
try { localStorage.removeItem('codeman-active-session'); } catch {}
// Select another session or show welcome (use sessionOrder for consistent ordering)
if (this.sessionOrder.length > 0 && this.sessions.size > 0) {
const nextSessionId = this.sessionOrder[0];
@@ -6242,7 +6242,7 @@ class ClaudemanApp {
goHome() {
// Deselect active session and show welcome screen
this.activeSessionId = null;
try { localStorage.removeItem('claudeman-active-session'); } catch {}
try { localStorage.removeItem('codeman-active-session'); } catch {}
this.terminal.clear();
this.showWelcome();
this.renderSessionTabs();
@@ -9250,7 +9250,7 @@ class ClaudemanApp {
} else {
// No cases exist yet - show the default case name as directory
select.value = 'testcase';
document.getElementById('dirDisplay').textContent = '~/claudeman-cases/testcase';
document.getElementById('dirDisplay').textContent = '~/codeman-cases/testcase';
this.updateMobileCaseLabel('testcase');
}
@@ -9317,7 +9317,7 @@ class ClaudemanApp {
setRunMode(mode) {
this._runMode = mode;
try { localStorage.setItem('claudeman_runMode', mode); } catch {}
try { localStorage.setItem('codeman_runMode', mode); } catch {}
this._applyRunMode();
// Close menu
document.getElementById('runModeMenu')?.classList.remove('active');
@@ -9361,7 +9361,7 @@ class ClaudemanApp {
}
_initRunMode() {
try { this._runMode = localStorage.getItem('claudeman_runMode') || 'claude'; } catch { this._runMode = 'claude'; }
try { this._runMode = localStorage.getItem('codeman_runMode') || 'claude'; } catch { this._runMode = 'claude'; }
this._applyRunMode();
}
@@ -10132,7 +10132,7 @@ class ClaudemanApp {
this.terminalBuffers.clear();
this.terminalBufferCache.clear();
this.activeSessionId = null;
try { localStorage.removeItem('claudeman-active-session'); } catch {}
try { localStorage.removeItem('codeman-active-session'); } catch {}
this.respawnStatus = {};
this.respawnCountdownTimers = {};
this.respawnActionLogs = {};
@@ -10239,13 +10239,13 @@ class ClaudemanApp {
this.terminal.options.fontSize = size;
document.getElementById('fontSizeDisplay').textContent = size;
this.fitAddon.fit();
localStorage.setItem('claudeman-font-size', size);
localStorage.setItem('codeman-font-size', size);
// Update overlay font cache and re-render at new cell dimensions
this._localEchoOverlay?.refreshFont();
}
loadFontSize() {
const saved = localStorage.getItem('claudeman-font-size');
const saved = localStorage.getItem('codeman-font-size');
if (saved) {
const size = parseInt(saved, 10);
if (size >= 10 && size <= 24) {
@@ -10676,7 +10676,7 @@ class ClaudemanApp {
// ========== Respawn Presets ==========
loadRespawnPresets() {
const saved = localStorage.getItem('claudeman-respawn-presets');
const saved = localStorage.getItem('codeman-respawn-presets');
const custom = saved ? JSON.parse(saved) : [];
return [...BUILTIN_RESPAWN_PRESETS, ...custom];
}
@@ -10684,7 +10684,7 @@ class ClaudemanApp {
saveRespawnPresets(presets) {
// Only save custom presets (not built-in)
const custom = presets.filter(p => !p.builtIn);
localStorage.setItem('claudeman-respawn-presets', JSON.stringify(custom));
localStorage.setItem('codeman-respawn-presets', JSON.stringify(custom));
}
renderPresetDropdown() {
@@ -11842,7 +11842,7 @@ class ClaudemanApp {
// Get the settings storage key based on device type (mobile vs desktop)
getSettingsStorageKey() {
const isMobile = MobileDetection.getDeviceType() === 'mobile';
return isMobile ? 'claudeman-app-settings-mobile' : 'claudeman-app-settings';
return isMobile ? 'codeman-app-settings-mobile' : 'codeman-app-settings';
}
// Get default settings based on device type
@@ -12138,7 +12138,7 @@ class ClaudemanApp {
// Sync voice settings from server (seed localStorage if no local API key)
if (voiceSettings) {
const localVoice = localStorage.getItem('claudeman-voice-settings');
const localVoice = localStorage.getItem('codeman-voice-settings');
if (!localVoice || !JSON.parse(localVoice).apiKey) {
VoiceInput._saveDeepgramConfig(voiceSettings);
}
@@ -12179,7 +12179,7 @@ class ClaudemanApp {
const windowStates = { minimized: minimizedState, open: openWindows };
// Save to localStorage for quick restore
localStorage.setItem('claudeman-subagent-window-states', JSON.stringify(windowStates));
localStorage.setItem('codeman-subagent-window-states', JSON.stringify(windowStates));
// Save to server for cross-browser persistence
try {
@@ -12206,7 +12206,7 @@ class ClaudemanApp {
if (res.ok) {
states = await res.json();
// Also update localStorage
localStorage.setItem('claudeman-subagent-window-states', JSON.stringify(states));
localStorage.setItem('codeman-subagent-window-states', JSON.stringify(states));
}
} catch (err) {
console.error('Failed to load subagent window states from server:', err);
@@ -12215,7 +12215,7 @@ class ClaudemanApp {
// Fallback to localStorage
if (!states) {
try {
const saved = localStorage.getItem('claudeman-subagent-window-states');
const saved = localStorage.getItem('codeman-subagent-window-states');
if (saved) {
states = JSON.parse(saved);
}
@@ -12321,7 +12321,7 @@ class ClaudemanApp {
// Save to localStorage for instant recovery
try {
localStorage.setItem('claudeman-subagent-parents', JSON.stringify(mapData));
localStorage.setItem('codeman-subagent-parents', JSON.stringify(mapData));
} catch (err) {
console.error('Failed to save subagent parents to localStorage:', err);
}
@@ -12351,7 +12351,7 @@ class ClaudemanApp {
if (res.ok) {
mapData = await res.json();
// Update localStorage as cache
localStorage.setItem('claudeman-subagent-parents', JSON.stringify(mapData));
localStorage.setItem('codeman-subagent-parents', JSON.stringify(mapData));
}
} catch (err) {
console.error('Failed to load subagent parents from server:', err);
@@ -12360,7 +12360,7 @@ class ClaudemanApp {
// Fallback to localStorage
if (!mapData) {
try {
const saved = localStorage.getItem('claudeman-subagent-parents');
const saved = localStorage.getItem('codeman-subagent-parents');
if (saved) {
mapData = JSON.parse(saved);
}
@@ -14670,8 +14670,8 @@ class ClaudemanApp {
const agent = this.subagents.get(agentId);
if (!agent) return;
// Only open windows for agents that belong to a Claudeman-managed session tab.
// Agents from external Claude sessions (not tracked by Claudeman) should not pop up.
// Only open windows for agents that belong to a Codeman-managed session tab.
// Agents from external Claude sessions (not tracked by Codeman) should not pop up.
if (agent.sessionId) {
const hasMatchingTab = Array.from(this.sessions.values()).some(s => s.claudeSessionId === agent.sessionId);
if (!hasMatchingTab) return;
@@ -15610,7 +15610,7 @@ class ClaudemanApp {
/** Open a standalone terminal window for a tmux-pane teammate (no subagent entry needed) */
openTeammateTerminalWindow(paneData) {
// Only open if the session has a tab in Claudeman
// Only open if the session has a tab in Codeman
if (!this.sessions.has(paneData.sessionId)) return;
// Use pane target as the unique ID for this window
@@ -16831,7 +16831,7 @@ class ClaudemanApp {
this.sessions.clear();
this.muxSessions = [];
this.activeSessionId = null;
try { localStorage.removeItem('claudeman-active-session'); } catch {}
try { localStorage.removeItem('codeman-active-session'); } catch {}
this.renderSessionTabs();
this.renderMuxSessions();
this.terminal.clear();
@@ -16842,7 +16842,7 @@ class ClaudemanApp {
// Just remove tabs, keep mux sessions running
this.sessions.clear();
this.activeSessionId = null;
try { localStorage.removeItem('claudeman-active-session'); } catch {}
try { localStorage.removeItem('codeman-active-session'); } catch {}
this.renderSessionTabs();
this.terminal.clear();
this.terminal.reset();
@@ -17554,12 +17554,25 @@ class ClaudemanApp {
escapeHtml(text) {
if (!text) return '';
return text.replace(ClaudemanApp._htmlEscapePattern, char => ClaudemanApp._htmlEscapeMap[char]);
return text.replace(CodemanApp._htmlEscapePattern, char => CodemanApp._htmlEscapeMap[char]);
}
}
// Migrate legacy localStorage keys (claudeman-* → codeman-*)
try {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && (key.startsWith('claudeman-') || key.startsWith('claudeman_'))) {
const newKey = key.replace(/^claudeman[-_]/, (m) => 'codeman' + m.charAt(m.length - 1));
if (localStorage.getItem(newKey) === null) {
localStorage.setItem(newKey, localStorage.getItem(key));
}
}
}
} catch {}
// Initialize
const app = new ClaudemanApp();
const app = new CodemanApp();
// Expose for debugging/testing
window.app = app;
+9 -9
View File
@@ -6,7 +6,7 @@
<meta name="description" content="Claude Code session manager with web interface">
<meta name="theme-color" content="#0a0a0a">
<meta name="google" content="notranslate">
<title>Claudeman</title>
<title>Codeman</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
<link rel="stylesheet" href="styles.css?v=0.1631">
<link rel="stylesheet" href="mobile.css?v=0.1631" media="(max-width: 1023px)">
@@ -37,7 +37,7 @@
<!-- Loading skeleton — visible instantly before CSS/JS loads, hidden by app-loaded class -->
<div class="loading-skeleton">
<div class="skeleton-header">
<span class="skeleton-brand">Claudeman</span>
<span class="skeleton-brand">Codeman</span>
<div class="skeleton-tabs"><div class="skeleton-tab"></div></div>
</div>
<div class="skeleton-terminal"></div>
@@ -49,7 +49,7 @@
<!-- Compact Header with Session Tabs -->
<header class="header">
<div class="header-brand">
<span class="logo" onclick="app.goHome()" title="Go to main page">Claudeman</span>
<span class="logo" onclick="app.goHome()" title="Go to main page">Codeman</span>
</div>
<!-- Session Tabs -->
@@ -231,7 +231,7 @@
<!-- Welcome Overlay (shown when no session active) -->
<div class="welcome-overlay" id="welcomeOverlay">
<div class="welcome-content">
<h1 class="welcome-title">Claudeman</h1>
<h1 class="welcome-title">Codeman</h1>
<p class="welcome-desc">Manage AI Coding tools in persistent tmux sessions.</p>
<div class="welcome-actions">
<button class="welcome-btn welcome-btn-claude" onclick="app.setRunMode('claude'); app.runClaude()">
@@ -379,7 +379,7 @@
</div>
<div class="toolbar-center">
<span class="version-display" id="versionDisplay" title="Claudeman version">v0.0.0</span>
<span class="version-display" id="versionDisplay" title="Codeman version">v0.0.0</span>
</div>
<div class="toolbar-right">
@@ -1086,7 +1086,7 @@
<span class="settings-status" id="notifPermissionStatus">?</span>
</div>
</div>
<span class="form-hint" style="grid-column: 1 / -1; margin-top: -8px;">For remote access, HTTPS is required. Start with: <code>claudeman web --https</code></span>
<span class="form-hint" style="grid-column: 1 / -1; margin-top: -8px;">For remote access, HTTPS is required. Start with: <code>codeman web --https</code></span>
<!-- Alerts Section -->
<div class="settings-section-header">Alerts</div>
@@ -1222,7 +1222,7 @@
<div class="form-row" style="grid-column: 1 / -1;">
<label for="voiceKeyterms">Domain Keywords</label>
<input type="text" id="voiceKeyterms" placeholder="claudeman, tmux, respawn, subagent" style="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.8rem; font-family: inherit;">
<input type="text" id="voiceKeyterms" placeholder="codeman, tmux, respawn, subagent" style="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.8rem; font-family: inherit;">
<span class="form-hint">Comma-separated terms to boost recognition accuracy</span>
</div>
</div>
@@ -1253,7 +1253,7 @@
<div class="form-row">
<label>Case Name</label>
<input type="text" id="newCaseName" placeholder="my-project" pattern="[a-zA-Z0-9_-]+" autocomplete="off" autocapitalize="off" spellcheck="false">
<span class="form-hint">Letters, numbers, hyphens, underscores only. Created in ~/claudeman-cases/</span>
<span class="form-hint">Letters, numbers, hyphens, underscores only. Created in ~/codeman-cases/</span>
</div>
<div class="form-row">
<label>Description (optional)</label>
@@ -1265,7 +1265,7 @@
<div class="form-row">
<label>Case Name</label>
<input type="text" id="linkCaseName" placeholder="my-project" pattern="[a-zA-Z0-9_-]+" autocomplete="off" autocapitalize="off" spellcheck="false">
<span class="form-hint">Name to identify this case in Claudeman</span>
<span class="form-hint">Name to identify this case in Codeman</span>
</div>
<div class="form-row">
<label>Folder Path</label>
+1 -1
View File
@@ -1,4 +1,4 @@
/* Claudeman - Terminal-First UI */
/* Codeman - Terminal-First UI */
:root {
--bg-dark: #0a0a0a;
+2 -2
View File
@@ -2,7 +2,7 @@
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Upload Screenshot - Claudeman</title>
<title>Upload Screenshot - Codeman</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
@@ -49,7 +49,7 @@
</style>
</head><body>
<div class="container">
<a class="back" href="/">&larr; Back to Claudeman</a>
<a class="back" href="/">&larr; Back to Codeman</a>
<h1>Upload Screenshot</h1>
<div class="drop-zone" id="drop">
<p>Tap to select image</p>
+1 -1
View File
@@ -39,7 +39,7 @@ const ALLOWED_ENV_PREFIXES = ['CLAUDE_CODE_', 'OPENCODE_'];
/** Env var keys that are always blocked (security-sensitive) */
const BLOCKED_ENV_KEYS = new Set([
'PATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'NODE_OPTIONS',
'CLAUDEMAN_MUX_NAME', 'CLAUDEMAN_TMUX',
'CODEMAN_MUX_NAME', 'CODEMAN_TMUX',
'OPENCODE_SERVER_PASSWORD', // Security-sensitive: server auth password
]);
+35 -35
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview Claudeman web server and REST API
* @fileoverview Codeman web server and REST API
*
* Provides a Fastify-based web server with:
* - REST API for session management, respawn control, and monitoring
@@ -142,7 +142,7 @@ const MAX_HOOK_DATA_SIZE = 8 * 1024;
// Maximum screenshot upload size (10MB)
const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
// Screenshots directory
const SCREENSHOTS_DIR = join(homedir(), '.claudeman', 'screenshots');
const SCREENSHOTS_DIR = join(homedir(), '.codeman', 'screenshots');
// Stats collection interval (2 seconds)
const STATS_COLLECTION_INTERVAL_MS = 2000;
// Session limit wait time before retrying (5 seconds)
@@ -272,10 +272,10 @@ function autoConfigureRalph(session: Session, workingDir: string, broadcast: (ev
/**
* Get or generate a self-signed TLS certificate for HTTPS.
* Certs are stored in ~/.claudeman/certs/ and reused across restarts.
* Certs are stored in ~/.codeman/certs/ and reused across restarts.
*/
function getOrCreateSelfSignedCert(): { key: string; cert: string } {
const certsDir = join(homedir(), '.claudeman', 'certs');
const certsDir = join(homedir(), '.codeman', 'certs');
const keyPath = join(certsDir, 'server.key');
const certPath = join(certsDir, 'server.crt');
@@ -292,7 +292,7 @@ function getOrCreateSelfSignedCert(): { key: string; cert: string } {
execSync(
`openssl req -x509 -newkey rsa:2048 -nodes ` +
`-keyout "${keyPath}" -out "${certPath}" ` +
`-days 365 -subj "/CN=claudeman" ` +
`-days 365 -subj "/CN=codeman" ` +
`-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,IP:0.0.0.0"`,
{ stdio: 'pipe' }
);
@@ -582,10 +582,10 @@ export class WebServer extends EventEmitter {
threshold: 1024,
});
// Optional HTTP Basic Auth (set CLAUDEMAN_PASSWORD env var to enable)
const authPassword = process.env.CLAUDEMAN_PASSWORD;
// Optional HTTP Basic Auth (set CODEMAN_PASSWORD env var to enable)
const authPassword = process.env.CODEMAN_PASSWORD;
if (authPassword) {
const authUsername = process.env.CLAUDEMAN_USERNAME || 'admin';
const authUsername = process.env.CODEMAN_USERNAME || 'admin';
const expectedHeader = 'Basic ' + Buffer.from(`${authUsername}:${authPassword}`).toString('base64');
this.app.addHook('onRequest', (req, reply, done) => {
const auth = req.headers.authorization;
@@ -593,7 +593,7 @@ export class WebServer extends EventEmitter {
done();
return;
}
reply.header('WWW-Authenticate', 'Basic realm="Claudeman"');
reply.header('WWW-Authenticate', 'Basic realm="Codeman"');
reply.code(401).send('Unauthorized');
});
}
@@ -2350,7 +2350,7 @@ export class WebServer extends EventEmitter {
});
// Case management
const casesDir = join(homedir(), 'claudeman-cases');
const casesDir = join(homedir(), 'codeman-cases');
this.app.get('/api/cases', async (): Promise<CaseInfo[]> => {
const cases: CaseInfo[] = [];
@@ -2372,7 +2372,7 @@ export class WebServer extends EventEmitter {
}
// Get linked cases
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
for (const [name, path] of Object.entries(linkedCases)) {
@@ -2456,11 +2456,11 @@ export class WebServer extends EventEmitter {
// Check if case name already exists in casesDir
const casePath = join(casesDir, name);
if (existsSync(casePath)) {
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in claudeman-cases.');
return createErrorResponse(ApiErrorCode.ALREADY_EXISTS, 'A case with this name already exists in codeman-cases.');
}
// Load existing linked cases
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
let linkedCases: Record<string, string> = {};
try {
linkedCases = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
@@ -2478,9 +2478,9 @@ export class WebServer extends EventEmitter {
// Save the linked case
linkedCases[name] = expandedPath;
try {
const claudemanDir = join(homedir(), '.claudeman');
if (!existsSync(claudemanDir)) {
mkdirSync(claudemanDir, { recursive: true });
const codemanDir = join(homedir(), '.codeman');
if (!existsSync(codemanDir)) {
mkdirSync(codemanDir, { recursive: true });
}
await fs.writeFile(linkedCasesFile, JSON.stringify(linkedCases, null, 2));
this.broadcast('case:linked', { name, path: expandedPath });
@@ -2494,7 +2494,7 @@ export class WebServer extends EventEmitter {
const { name } = req.params as { name: string };
// First check linked cases
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[name]) {
@@ -2531,7 +2531,7 @@ export class WebServer extends EventEmitter {
// Get case path (check linked cases first, then casesDir)
let casePath: string | null = null;
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[name]) {
@@ -2737,7 +2737,7 @@ export class WebServer extends EventEmitter {
// Save lastUsedCase to settings for TUI/web sync
try {
const settingsFilePath = join(homedir(), '.claudeman', 'settings.json');
const settingsFilePath = join(homedir(), '.codeman', 'settings.json');
let settings: Record<string, unknown> = {};
try {
settings = JSON.parse(await fs.readFile(settingsFilePath, 'utf-8'));
@@ -2974,7 +2974,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Determine output directory for saving wizard results
let outputDir: string | undefined;
if (caseName) {
const casesDir = join(homedir(), 'claudeman-cases');
const casesDir = join(homedir(), 'codeman-cases');
const casePath = join(casesDir, caseName);
// Security: Path traversal protection - use relative path check
const resolvedCase = resolve(casePath);
@@ -3101,7 +3101,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Get ralph-wizard files for a case (prompts and results)
this.app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => {
const { caseName } = req.params as { caseName: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casesDir = join(homedir(), 'codeman-cases');
let casePath = join(casesDir, caseName);
// Security: Path traversal protection - use relative path check
@@ -3114,7 +3114,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Check linked cases if path doesn't exist
if (!existsSync(casePath)) {
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[caseName]) {
@@ -3162,7 +3162,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Cache disabled to ensure fresh prompts when starting new plan generations
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => {
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
const casesDir = join(homedir(), 'claudeman-cases');
const casesDir = join(homedir(), 'codeman-cases');
let casePath = join(casesDir, caseName);
// Prevent browser caching - prompts change between plan generations
@@ -3180,7 +3180,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Check linked cases if path doesn't exist
if (!existsSync(casePath)) {
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
const linkedCasesFile = join(homedir(), '.codeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[caseName]) {
@@ -3369,7 +3369,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
});
// ============ App Settings Endpoints ============
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
const settingsPath = join(homedir(), '.codeman', 'settings.json');
this.app.get('/api/settings', async () => {
try {
@@ -3520,7 +3520,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// ============ Subagent Window State Endpoints ============
// Persists minimized/open window states for cross-browser sync
const windowStatesPath = join(homedir(), '.claudeman', 'subagent-window-states.json');
const windowStatesPath = join(homedir(), '.codeman', 'subagent-window-states.json');
this.app.get('/api/subagent-window-states', async () => {
try {
@@ -3555,7 +3555,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// ============ Subagent Parent Associations ============
// Persists which TAB each agent window connects to.
// This is the PERMANENT record of agent -> tab associations.
const parentMapPath = join(homedir(), '.claudeman', 'subagent-parents.json');
const parentMapPath = join(homedir(), '.codeman', 'subagent-parents.json');
this.app.get('/api/subagent-parents', async () => {
try {
@@ -3836,7 +3836,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
ext = map[ctMatch[1].toLowerCase()] ?? ext;
}
// Save to ~/.claudeman/screenshots/
// Save to ~/.codeman/screenshots/
if (!existsSync(SCREENSHOTS_DIR)) {
mkdirSync(SCREENSHOTS_DIR, { recursive: true });
}
@@ -4707,7 +4707,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Helper to get custom CLAUDE.md template path from settings
private async getDefaultClaudeMdPath(): Promise<string | undefined> {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
const settingsPath = join(homedir(), '.codeman', 'settings.json');
try {
const content = await fs.readFile(settingsPath, 'utf-8');
@@ -4723,7 +4723,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return undefined;
}
// Read ~/.claudeman/settings.json once and return the parsed object.
// Read ~/.codeman/settings.json once and return the parsed object.
// Cached for 2s to avoid redundant reads during session creation bursts.
private _settingsCache: { data: Record<string, unknown>; ts: number } | null = null;
private async readSettings(): Promise<Record<string, unknown>> {
@@ -4731,7 +4731,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
if (this._settingsCache && now - this._settingsCache.ts < 2000) {
return this._settingsCache.data;
}
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
const settingsPath = join(homedir(), '.codeman', 'settings.json');
try {
const content = await fs.readFile(settingsPath, 'utf-8');
const data = JSON.parse(content) as Record<string, unknown>;
@@ -5293,10 +5293,10 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
await this.app.listen({ port: this.port, host: '0.0.0.0' });
const protocol = this.https ? 'https' : 'http';
console.log(`Claudeman web interface running at ${protocol}://localhost:${this.port}`);
console.log(`Codeman web interface running at ${protocol}://localhost:${this.port}`);
// Set API URL for child processes (MCP server, spawned sessions)
process.env.CLAUDEMAN_API_URL = `${protocol}://localhost:${this.port}`;
process.env.CODEMAN_API_URL = `${protocol}://localhost:${this.port}`;
// Start scheduled runs cleanup timer
this.scheduledCleanupTimer = setInterval(() => {
@@ -5339,7 +5339,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
* Check if subagent tracking is enabled in settings (default: true)
*/
private async isSubagentTrackingEnabled(): Promise<boolean> {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
const settingsPath = join(homedir(), '.codeman', 'settings.json');
try {
const content = await fs.readFile(settingsPath, 'utf-8');
const settings = JSON.parse(content);
@@ -5357,7 +5357,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
* Check if image watcher is enabled in settings (default: false)
*/
private async isImageWatcherEnabled(): Promise<boolean> {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
const settingsPath = join(homedir(), '.codeman', 'settings.json');
try {
const content = await fs.readFile(settingsPath, 'utf-8');
const settings = JSON.parse(content);
+3 -3
View File
@@ -149,7 +149,7 @@ describe('AiIdleChecker', () => {
// Verify tmux was spawned with correct args
expect(mockedSpawn).toHaveBeenCalledWith(
'tmux',
expect.arrayContaining(['new-session', '-d', '-s', expect.stringContaining('claudeman-aicheck-')]),
expect.arrayContaining(['new-session', '-d', '-s', expect.stringContaining('codeman-aicheck-')]),
expect.objectContaining({ detached: true, stdio: 'ignore' })
);
});
@@ -163,7 +163,7 @@ describe('AiIdleChecker', () => {
// First call should try to kill existing session
expect(mockedExecSync).toHaveBeenCalledWith(
expect.stringContaining('tmux kill-session -t "claudeman-aicheck-'),
expect.stringContaining('tmux kill-session -t "codeman-aicheck-'),
expect.any(Object)
);
});
@@ -176,7 +176,7 @@ describe('AiIdleChecker', () => {
await checkPromise;
expect(mockedWriteFileSync).toHaveBeenCalledWith(
expect.stringContaining('claudeman-aicheck-'),
expect.stringContaining('codeman-aicheck-'),
''
);
});
+2 -2
View File
@@ -537,7 +537,7 @@ describe('API Response Structures', () => {
it('should validate case info', () => {
const caseInfo: CaseInfo = {
name: 'my-project',
path: '/home/user/claudeman-cases/my-project',
path: '/home/user/codeman-cases/my-project',
hasClaudeMd: true,
};
@@ -549,7 +549,7 @@ describe('API Response Structures', () => {
it('should handle optional hasClaudeMd', () => {
const caseInfo: CaseInfo = {
name: 'new-project',
path: '/home/user/claudeman-cases/new-project',
path: '/home/user/codeman-cases/new-project',
};
expect(caseInfo.hasClaudeMd).toBeUndefined();
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* @fileoverview Tests for buffer management and memory limits
*
* Tests the buffer management strategies used in Claudeman sessions
* Tests the buffer management strategies used in Codeman sessions
* for handling large terminal output and message storage.
*/
+6 -6
View File
@@ -318,7 +318,7 @@ describe('CLI Command Parsing', () => {
describe('Help Text Generation', () => {
const generateHelp = (command: string, description: string, options: string[]): string => {
let help = `Usage: claudeman ${command}\n\n`;
let help = `Usage: codeman ${command}\n\n`;
help += `${description}\n`;
if (options.length > 0) {
help += '\nOptions:\n';
@@ -331,7 +331,7 @@ describe('CLI Command Parsing', () => {
it('should generate basic help', () => {
const help = generateHelp('status', 'Show overall status', []);
expect(help).toContain('Usage: claudeman status');
expect(help).toContain('Usage: codeman status');
expect(help).toContain('Show overall status');
});
@@ -724,8 +724,8 @@ describe('CLI Configuration', () => {
maxConcurrentSessions: 5,
defaultPort: 3000,
defaultHost: '0.0.0.0',
casesDirectory: '~/claudeman-cases',
stateDirectory: '~/.claudeman',
casesDirectory: '~/codeman-cases',
stateDirectory: '~/.codeman',
};
const expandPath = (path: string): string => {
@@ -742,8 +742,8 @@ describe('CLI Configuration', () => {
});
it('should expand home directory paths', () => {
expect(expandPath('~/claudeman-cases')).toBe('/home/user/claudeman-cases');
expect(expandPath('~/.claudeman')).toBe('/home/user/.claudeman');
expect(expandPath('~/codeman-cases')).toBe('/home/user/codeman-cases');
expect(expandPath('~/.codeman')).toBe('/home/user/.codeman');
});
it('should not modify absolute paths', () => {
+2 -2
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const TEST_PORT = 3110;
const CASES_DIR = join(homedir(), 'claudeman-cases');
const CASES_DIR = join(homedir(), 'codeman-cases');
describe('Edge Cases and Error Handling', () => {
let server: WebServer;
@@ -349,7 +349,7 @@ describe('Concurrent Session Handling', () => {
const { join } = await import('node:path');
const { homedir } = await import('node:os');
for (const name of createdCases) {
const path = join(homedir(), 'claudeman-cases', name);
const path = join(homedir(), 'codeman-cases', name);
if (existsSync(path)) {
rmSync(path, { recursive: true, force: true });
}
+3 -3
View File
@@ -1,5 +1,5 @@
/**
* File Link Click Tests for Claudeman Web UI
* File Link Click Tests for Codeman Web UI
*
* Tests that file paths displayed in terminal output are clickable
* and open the log viewer window correctly.
@@ -88,7 +88,7 @@ describe('File Link Click Tests', () => {
closeBrowser();
// Create test directory and log file
testDir = join(tmpdir(), `claudeman-link-test-${Date.now()}`);
testDir = join(tmpdir(), `codeman-link-test-${Date.now()}`);
mkdirSync(testDir, { recursive: true });
testLogFile = join(testDir, 'test.log');
writeFileSync(testLogFile, '=== Test Log Started ===\n');
@@ -102,7 +102,7 @@ describe('File Link Click Tests', () => {
browser(`open ${baseUrl}`);
await new Promise(r => setTimeout(r, 2000));
const title = browserJson<{ title: string }>('get title');
browserAvailable = title.title === 'Claudeman';
browserAvailable = title.title === 'Codeman';
} catch (e) {
console.warn('Browser not available, skipping browser tests:', (e as Error).message);
browserAvailable = false;
+5 -5
View File
@@ -54,8 +54,8 @@ describe('generateHooksConfig', () => {
const config = generateHooksConfig();
const notifHooks = config.hooks.Notification as Array<{ hooks: Array<{ command: string }> }>;
const cmd = notifHooks[0].hooks[0].command;
expect(cmd).toContain('$CLAUDEMAN_API_URL');
expect(cmd).toContain('$CLAUDEMAN_SESSION_ID');
expect(cmd).toContain('$CODEMAN_API_URL');
expect(cmd).toContain('$CODEMAN_SESSION_ID');
expect(cmd).not.toContain('localhost');
});
@@ -99,7 +99,7 @@ describe('generateHooksConfig', () => {
});
describe('writeHooksConfig', () => {
const testDir = join(tmpdir(), 'claudeman-hooks-test-' + Date.now());
const testDir = join(tmpdir(), 'codeman-hooks-test-' + Date.now());
beforeEach(() => {
if (!existsSync(testDir)) {
@@ -648,8 +648,8 @@ describe('Hook Config Generation - Extended', () => {
const config = generateHooksConfig();
const notifHooks = config.hooks.Notification as Array<{ hooks: Array<{ command: string }> }>;
const cmd = notifHooks[0].hooks[0].command;
expect(cmd).toContain('$CLAUDEMAN_API_URL');
expect(cmd).toContain('$CLAUDEMAN_SESSION_ID');
expect(cmd).toContain('$CODEMAN_API_URL');
expect(cmd).toContain('$CODEMAN_SESSION_ID');
});
it('should generate POST curl commands', () => {
+1 -1
View File
@@ -96,7 +96,7 @@ async function main() {
page.on('console', msg => consoleLogs.push(`[${msg.type()}] ${msg.text()}`));
try {
console.log('Opening Claudeman...');
console.log('Opening Codeman...');
await page.goto(BASE);
await page.waitForSelector('.xterm-screen', { timeout: 10000 });
await page.waitForTimeout(2000);
+1 -1
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const TEST_PORT = 3115;
const CASES_DIR = join(homedir(), 'claudeman-cases');
const CASES_DIR = join(homedir(), 'codeman-cases');
/**
* Integration tests for complete user flows
+2 -2
View File
@@ -149,9 +149,9 @@ async function main() {
// Enable local echo
await page.evaluate(() => {
const s = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const s = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
s.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(s));
localStorage.setItem('codeman-app-settings', JSON.stringify(s));
window.app?._updateLocalEchoState?.();
});
+2 -2
View File
@@ -117,9 +117,9 @@ async function main() {
// Enable local echo
await page.evaluate(() => {
const s = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const s = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
s.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(s));
localStorage.setItem('codeman-app-settings', JSON.stringify(s));
window.app?._updateLocalEchoState?.();
});
+3 -3
View File
@@ -120,9 +120,9 @@ async function main() {
// Enable local echo
await page.evaluate(() => {
const s = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const s = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
s.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(s));
localStorage.setItem('codeman-app-settings', JSON.stringify(s));
window.app?._updateLocalEchoState?.();
});
@@ -307,7 +307,7 @@ async function main() {
await page.keyboard.type('will-be-lost', { delay: 20 });
await page.waitForTimeout(200);
const lsBefore = await page.evaluate(() => localStorage.getItem('claudeman_local_echo_pending'));
const lsBefore = await page.evaluate(() => localStorage.getItem('codeman_local_echo_pending'));
if (!lsBefore) pass('localStorage not written (persist is no-op)');
else fail('localStorage written', `value="${lsBefore}"`);
+3 -3
View File
@@ -227,7 +227,7 @@ async function main() {
try {
// ── SETUP ──
console.log('Opening Claudeman...');
console.log('Opening Codeman...');
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.xterm-screen', { timeout: 10000 });
await page.waitForTimeout(2000);
@@ -235,9 +235,9 @@ async function main() {
// Enable local echo via settings
console.log('Enabling local echo...');
await page.evaluate(() => {
const settings = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const settings = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
settings.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(settings));
localStorage.setItem('codeman-app-settings', JSON.stringify(settings));
window.app?._updateLocalEchoState?.();
});
await page.waitForTimeout(300);
+2 -2
View File
@@ -126,9 +126,9 @@ async function main() {
// Enable local echo
await page.evaluate(() => {
const s = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const s = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
s.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(s));
localStorage.setItem('codeman-app-settings', JSON.stringify(s));
window.app?._updateLocalEchoState?.();
});
+2 -2
View File
@@ -144,7 +144,7 @@ async function main() {
try {
// ── SETUP ──
console.log('Opening Claudeman...');
console.log('Opening Codeman...');
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('.xterm-screen', { timeout: 10000 });
await page.waitForTimeout(2500);
@@ -152,7 +152,7 @@ async function main() {
// Enable local echo
console.log('Enabling local echo...');
await page.evaluate(() => {
const key = window.app?.getSettingsStorageKey?.() || 'claudeman-app-settings';
const key = window.app?.getSettingsStorageKey?.() || 'codeman-app-settings';
const settings = JSON.parse(localStorage.getItem(key) || '{}');
settings.localEchoEnabled = true;
localStorage.setItem(key, JSON.stringify(settings));
+4 -4
View File
@@ -241,9 +241,9 @@ async function main() {
// Enable local echo
await page.evaluate(() => {
const s = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const s = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
s.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(s));
localStorage.setItem('codeman-app-settings', JSON.stringify(s));
window.app?._updateLocalEchoState?.();
});
@@ -916,9 +916,9 @@ async function main() {
await page.evaluate((id) => document.querySelector(`.session-tab[data-id="${id}"]`)?.click(), sA);
await page.waitForTimeout(2000);
await page.evaluate(() => {
const s = JSON.parse(localStorage.getItem('claudeman-app-settings') || '{}');
const s = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
s.localEchoEnabled = true;
localStorage.setItem('claudeman-app-settings', JSON.stringify(s));
localStorage.setItem('codeman-app-settings', JSON.stringify(s));
window.app?._updateLocalEchoState?.();
});
await page.waitForTimeout(300);
+1 -1
View File
@@ -2,7 +2,7 @@
* @fileoverview Tests for memory leak prevention patterns
*
* These tests verify the cleanup patterns used to prevent memory leaks
* in long-running Claudeman sessions (P1 improvements from P0 fixes).
* in long-running Codeman sessions (P1 improvements from P0 fixes).
*
* Port: N/A (unit tests, no server)
*/
+2 -2
View File
@@ -1,5 +1,5 @@
/**
* Performance tests: Claudeman responsiveness under subagent load
* Performance tests: Codeman responsiveness under subagent load
*
* Measures event loop lag caused by:
* 1. SubagentWatcher processing many agent files simultaneously
@@ -112,7 +112,7 @@ describe('SubagentWatcher performance', () => {
let projectsDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'claudeman-perf-'));
tmpDir = mkdtempSync(join(tmpdir(), 'codeman-perf-'));
projectsDir = join(tmpDir, 'projects');
mkdirSync(projectsDir, { recursive: true });
});
+1 -1
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const TEST_PORT = 3099;
const CASES_DIR = join(homedir(), 'claudeman-cases');
const CASES_DIR = join(homedir(), 'codeman-cases');
describe('Quick Start API', () => {
let server: WebServer;
+1 -1
View File
@@ -18,7 +18,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const TEST_PORT = 3125;
const CASES_DIR = join(homedir(), 'claudeman-cases');
const CASES_DIR = join(homedir(), 'codeman-cases');
describe('Ralph Integration Tests', () => {
let server: WebServer;
+11 -11
View File
@@ -2,10 +2,10 @@
**Date**: 2026-01-25
**Analyzed Files**:
- `/home/arkon/default/claudeman/src/respawn-controller.ts`
- `/home/arkon/default/claudeman/src/ai-idle-checker.ts`
- `/home/arkon/default/claudeman/src/ai-plan-checker.ts`
- `/home/arkon/default/claudeman/test/respawn-controller.test.ts`
- `/home/arkon/default/codeman/src/respawn-controller.ts`
- `/home/arkon/default/codeman/src/ai-idle-checker.ts`
- `/home/arkon/default/codeman/src/ai-plan-checker.ts`
- `/home/arkon/default/codeman/test/respawn-controller.test.ts`
---
@@ -55,7 +55,7 @@ The following areas lack test coverage:
### Issue 1: E2BIG Error in ai-plan-checker.ts (HIGH)
**File**: `/home/arkon/default/claudeman/src/ai-plan-checker.ts`
**File**: `/home/arkon/default/codeman/src/ai-plan-checker.ts`
**Lines**: 341-346
**Severity**: HIGH
@@ -84,7 +84,7 @@ const claudeCmd = `cat "${this.checkPromptFile}" | claude -p ${modelArg} --outpu
### Issue 2: Missing Prompt File Cleanup on Timeout (MEDIUM)
**File**: `/home/arkon/default/claudeman/src/ai-idle-checker.ts`
**File**: `/home/arkon/default/codeman/src/ai-idle-checker.ts`
**Lines**: 392-398
**Severity**: MEDIUM
@@ -98,7 +98,7 @@ const claudeCmd = `cat "${this.checkPromptFile}" | claude -p ${modelArg} --outpu
### Issue 3: Race Condition in cancel() with Promise Resolution (MEDIUM)
**File**: `/home/arkon/default/claudeman/src/ai-idle-checker.ts`
**File**: `/home/arkon/default/codeman/src/ai-idle-checker.ts`
**Lines**: 265-279
**Severity**: MEDIUM
@@ -127,7 +127,7 @@ cancel(): void {
### Issue 4: Stale Screen Name Collision (LOW)
**File**: `/home/arkon/default/claudeman/src/ai-idle-checker.ts`, `ai-plan-checker.ts`
**File**: `/home/arkon/default/codeman/src/ai-idle-checker.ts`, `ai-plan-checker.ts`
**Lines**: 326-329 (idle), 333-336 (plan)
**Severity**: LOW
@@ -135,21 +135,21 @@ cancel(): void {
**Code**:
```typescript
this.checkScreenName = `claudeman-aicheck-${shortId}`;
this.checkScreenName = `codeman-aicheck-${shortId}`;
```
**Impact**: Very unlikely in practice since checks have cooldowns, but theoretically possible.
**Suggested Fix**: Already partially mitigated by the `timestamp` in temp file names. Could add timestamp to screen name too:
```typescript
this.checkScreenName = `claudeman-aicheck-${shortId}-${timestamp}`;
this.checkScreenName = `codeman-aicheck-${shortId}-${timestamp}`;
```
---
### Issue 5: DetectionStatus Calculation During ai_checking State (LOW)
**File**: `/home/arkon/default/claudeman/src/respawn-controller.ts`
**File**: `/home/arkon/default/codeman/src/respawn-controller.ts`
**Lines**: 759-762
**Severity**: LOW
+1 -1
View File
@@ -243,7 +243,7 @@ afterEach(() => {
The global `test/setup.ts` handles:
1. **Screen Cleanup**: Kills any orphaned `claudeman-*` screens created during tests
1. **Screen Cleanup**: Kills any orphaned `codeman-*` screens created during tests
2. **Process Cleanup**: Kills any Claude processes spawned by tests
3. **Pre-existing Protection**: Never kills screens that existed before tests started
+1 -1
View File
@@ -98,7 +98,7 @@ export class MockSession extends EventEmitter {
constructor(id: string = 'mock-session-id') {
super();
this.id = id;
this._muxName = `claudeman-test-${id.slice(0, 8)}`;
this._muxName = `codeman-test-${id.slice(0, 8)}`;
}
/** Direct PTY write (used by session.write()) */
+1 -1
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { homedir } from 'node:os';
const TEST_PORT = 3120;
const CASES_DIR = join(homedir(), 'claudeman-cases');
const CASES_DIR = join(homedir(), 'codeman-cases');
/**
* Session Cleanup Tests

Some files were not shown because too many files have changed in this diff Show More