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
+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);