chore: bump version to 0.1549

This commit is contained in:
arkon
2026-02-18 23:14:39 +01:00
parent bd188df08d
commit 1833a9ac2d
7 changed files with 71 additions and 28 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`)
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1548 (must match `package.json` for npm publish)
**Version**: 0.1549 (must match `package.json` for npm publish)
## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1548",
"version": "0.1549",
"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",
+1
View File
@@ -70,6 +70,7 @@ export interface TerminalMultiplexer extends EventEmitter {
mode: 'claude' | 'shell',
name?: string,
niceConfig?: NiceConfig,
model?: string,
): Promise<MuxSession>;
/**
+25 -16
View File
@@ -150,7 +150,7 @@ function tryParseJSON(jsonString: string): { success: boolean; data?: unknown; e
// Constants
// ============================================================================
const MODEL = 'opus';
const DEFAULT_MODEL = 'opus';
// ============================================================================
// Main Orchestrator Class
@@ -163,11 +163,20 @@ export class PlanOrchestrator {
private runningSessions: Set<Session> = new Set();
private cancelled = false;
private taskDescription = '';
private researchModel: string;
private plannerModel: string;
constructor(mux: TerminalMultiplexer, workingDir: string = process.cwd(), outputDir?: string) {
constructor(
mux: TerminalMultiplexer,
workingDir: string = process.cwd(),
outputDir?: string,
modelConfig?: { defaultModel?: string; agentTypeOverrides?: Record<string, string> },
) {
this.mux = mux;
this.workingDir = workingDir;
this.outputDir = outputDir;
this.researchModel = modelConfig?.agentTypeOverrides?.explore || modelConfig?.defaultModel || DEFAULT_MODEL;
this.plannerModel = modelConfig?.agentTypeOverrides?.review || modelConfig?.defaultModel || DEFAULT_MODEL;
}
private saveAgentOutput(agentType: string, prompt: string, result: unknown, durationMs: number): void {
@@ -386,7 +395,7 @@ export class PlanOrchestrator {
return { success: false, findings: { externalResources: [], codebasePatterns: [], technicalRecommendations: [], potentialChallenges: [], recommendedTools: [] }, enrichedTaskDescription: taskDescription, error: 'Cancelled', durationMs: 0 };
}
onSubagent?.({ type: 'started', agentId, agentType: 'research', model: MODEL, status: 'running', detail: 'Researching...' });
onSubagent?.({ type: 'started', agentId, agentType: 'research', model: this.researchModel, status: 'running', detail: 'Researching...' });
const session = new Session({
workingDir: this.workingDir,
@@ -402,11 +411,11 @@ export class PlanOrchestrator {
// Start progress interval before try block to ensure cleanup in finally
const progressInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
onSubagent?.({ type: 'progress', agentId, agentType: 'research', model: MODEL, status: 'running', detail: `${elapsed}s elapsed` });
onSubagent?.({ type: 'progress', agentId, agentType: 'research', model: this.researchModel, status: 'running', detail: `${elapsed}s elapsed` });
}, 30000);
try {
const { result: response } = await session.runPrompt(prompt, { model: MODEL });
const { result: response } = await session.runPrompt(prompt, { model: this.researchModel });
this.runningSessions.delete(session);
@@ -415,13 +424,13 @@ export class PlanOrchestrator {
// Extract JSON from response
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
onSubagent?.({ type: 'failed', agentId, agentType: 'research', model: MODEL, status: 'failed', error: 'No JSON found', durationMs });
onSubagent?.({ type: 'failed', agentId, agentType: 'research', model: this.researchModel, status: 'failed', error: 'No JSON found', durationMs });
return { success: false, findings: { externalResources: [], codebasePatterns: [], technicalRecommendations: [], potentialChallenges: [], recommendedTools: [] }, enrichedTaskDescription: taskDescription, error: 'No JSON in response', durationMs };
}
const parsed = tryParseJSON(jsonMatch[0]);
if (!parsed.success) {
onSubagent?.({ type: 'failed', agentId, agentType: 'research', model: MODEL, status: 'failed', error: parsed.error, durationMs });
onSubagent?.({ type: 'failed', agentId, agentType: 'research', model: this.researchModel, status: 'failed', error: parsed.error, durationMs });
return { success: false, findings: { externalResources: [], codebasePatterns: [], technicalRecommendations: [], potentialChallenges: [], recommendedTools: [] }, enrichedTaskDescription: taskDescription, error: parsed.error, durationMs };
}
@@ -440,14 +449,14 @@ export class PlanOrchestrator {
};
this.saveAgentOutput('research', prompt, result, durationMs);
onSubagent?.({ type: 'completed', agentId, agentType: 'research', model: MODEL, status: 'completed', durationMs });
onSubagent?.({ type: 'completed', agentId, agentType: 'research', model: this.researchModel, status: 'completed', durationMs });
return result;
} catch (err) {
this.runningSessions.delete(session);
const durationMs = Date.now() - startTime;
const error = err instanceof Error ? err.message : String(err);
onSubagent?.({ type: 'failed', agentId, agentType: 'research', model: MODEL, status: 'failed', error, durationMs });
onSubagent?.({ type: 'failed', agentId, agentType: 'research', model: this.researchModel, status: 'failed', error, durationMs });
return { success: false, findings: { externalResources: [], codebasePatterns: [], technicalRecommendations: [], potentialChallenges: [], recommendedTools: [] }, enrichedTaskDescription: taskDescription, error, durationMs };
} finally {
// Always clear the progress interval to prevent memory leaks
@@ -468,7 +477,7 @@ export class PlanOrchestrator {
return { success: false, error: 'Cancelled' };
}
onSubagent?.({ type: 'started', agentId, agentType: 'planner', model: MODEL, status: 'running', detail: 'Generating plan...' });
onSubagent?.({ type: 'started', agentId, agentType: 'planner', model: this.plannerModel, status: 'running', detail: 'Generating plan...' });
const session = new Session({
workingDir: this.workingDir,
@@ -486,11 +495,11 @@ export class PlanOrchestrator {
// Start progress interval before try block to ensure cleanup in finally
const progressInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
onSubagent?.({ type: 'progress', agentId, agentType: 'planner', model: MODEL, status: 'running', detail: `${elapsed}s elapsed` });
onSubagent?.({ type: 'progress', agentId, agentType: 'planner', model: this.plannerModel, status: 'running', detail: `${elapsed}s elapsed` });
}, 30000);
try {
const { result: response } = await session.runPrompt(prompt, { model: MODEL });
const { result: response } = await session.runPrompt(prompt, { model: this.plannerModel });
this.runningSessions.delete(session);
@@ -499,13 +508,13 @@ export class PlanOrchestrator {
// Extract JSON from response
const jsonMatch = response.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
onSubagent?.({ type: 'failed', agentId, agentType: 'planner', model: MODEL, status: 'failed', error: 'No JSON found', durationMs });
onSubagent?.({ type: 'failed', agentId, agentType: 'planner', model: this.plannerModel, status: 'failed', error: 'No JSON found', durationMs });
return { success: false, error: 'No JSON in response' };
}
const parsed = tryParseJSON(jsonMatch[0]);
if (!parsed.success) {
onSubagent?.({ type: 'failed', agentId, agentType: 'planner', model: MODEL, status: 'failed', error: parsed.error, durationMs });
onSubagent?.({ type: 'failed', agentId, agentType: 'planner', model: this.plannerModel, status: 'failed', error: parsed.error, durationMs });
return { success: false, error: parsed.error };
}
@@ -515,7 +524,7 @@ export class PlanOrchestrator {
const warnings: string[] = Array.isArray(data.warnings) ? data.warnings : [];
this.saveAgentOutput('planner', prompt, { items, gaps, warnings }, durationMs);
onSubagent?.({ type: 'completed', agentId, agentType: 'planner', model: MODEL, status: 'completed', itemCount: items.length, durationMs });
onSubagent?.({ type: 'completed', agentId, agentType: 'planner', model: this.plannerModel, status: 'completed', itemCount: items.length, durationMs });
onProgress?.('planning', `Generated ${items.length} tasks`);
@@ -524,7 +533,7 @@ export class PlanOrchestrator {
this.runningSessions.delete(session);
const durationMs = Date.now() - startTime;
const error = err instanceof Error ? err.message : String(err);
onSubagent?.({ type: 'failed', agentId, agentType: 'planner', model: MODEL, status: 'failed', error, durationMs });
onSubagent?.({ type: 'failed', agentId, agentType: 'planner', model: this.plannerModel, status: 'failed', error, durationMs });
return { success: false, error };
} finally {
// Always clear the progress interval to prevent memory leaks
+14 -5
View File
@@ -317,6 +317,9 @@ export class Session extends EventEmitter {
// Nice prioritying configuration
private _niceConfig: NiceConfig = { ...DEFAULT_NICE_CONFIG };
// Claude model override (e.g., 'opus', 'sonnet', 'haiku')
private _model: string | undefined;
// Session color for visual differentiation
private _color: import('./types.js').SessionColor = 'default';
@@ -361,6 +364,8 @@ export class Session extends EventEmitter {
/** Existing mux session for restored sessions */
muxSession?: MuxSession;
niceConfig?: NiceConfig; // Nice prioritying configuration
/** Claude model override (e.g., 'opus', 'sonnet', 'haiku') */
model?: string;
}) {
super();
this.setMaxListeners(25);
@@ -391,6 +396,11 @@ export class Session extends EventEmitter {
this._niceConfig = { ...config.niceConfig };
}
// Apply model override if provided
if (config.model) {
this._model = config.model;
}
// Initialize task tracker and forward events (store handlers for cleanup)
this._taskTracker = new TaskTracker();
this._taskTrackerHandlers = {
@@ -845,7 +855,7 @@ export class Session extends EventEmitter {
console.log('[Session] Attaching to existing mux session:', this._muxSession!.muxName);
} else {
// Create a new mux session
this._muxSession = await this._mux.createSession(this.id, this.workingDir, 'claude', this._name, this._niceConfig);
this._muxSession = await this._mux.createSession(this.id, this.workingDir, 'claude', this._name, this._niceConfig, this._model);
console.log('[Session] Created mux session:', this._muxSession.muxName);
// Wait a moment for mux to fully start
@@ -920,10 +930,9 @@ export class Session extends EventEmitter {
try {
// Pass --session-id to use the SAME ID as the Claudeman session
// This ensures subagents can be directly matched to the correct tab
this.ptyProcess = pty.spawn('claude', [
'--dangerously-skip-permissions',
'--session-id', this.id
], {
const args = ['--dangerously-skip-permissions', '--session-id', this.id];
if (this._model) args.push('--model', this._model);
this.ptyProcess = pty.spawn('claude', args, {
name: 'xterm-256color',
cols: 120,
rows: 40,
+5 -1
View File
@@ -201,6 +201,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
mode: 'claude' | 'shell',
name?: string,
niceConfig?: NiceConfig,
model?: string,
): Promise<MuxSession> {
const muxName = `claudeman-${sessionId.slice(0, 8)}`;
@@ -240,8 +241,11 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
`export CLAUDEMAN_API_URL=${process.env.CLAUDEMAN_API_URL || 'http://localhost:3000'}`,
].join(' && ');
// Validate model to prevent command injection (only allow alphanumeric, hyphens, dots)
const safeModel = (model && /^[a-zA-Z0-9._-]+$/.test(model)) ? model : undefined;
const modelFlag = (mode === 'claude' && safeModel) ? ` --model ${safeModel}` : '';
const baseCmd = mode === 'claude'
? `claude --dangerously-skip-permissions --session-id "${sessionId}"`
? `claude --dangerously-skip-permissions --session-id "${sessionId}"${modelFlag}`
: '$SHELL';
const config = niceConfig || DEFAULT_NICE_CONFIG;
+24 -4
View File
@@ -785,6 +785,8 @@ export class WebServer extends EventEmitter {
}
const globalNice = await this.getGlobalNiceConfig();
const modelConfig = await this.getModelConfig();
const model = (body.mode !== 'shell') ? modelConfig?.defaultModel : undefined;
const session = new Session({
workingDir,
mode: body.mode || 'claude',
@@ -792,6 +794,7 @@ export class WebServer extends EventEmitter {
mux: this.mux,
useMux: true,
niceConfig: globalNice,
model,
});
this.sessions.set(session.id, session);
@@ -2569,14 +2572,17 @@ export class WebServer extends EventEmitter {
}
// Create a new session with the case as working directory
// Apply global Nice priority config if enabled in settings
// Apply global Nice priority config and model config from settings
const niceConfig = await this.getGlobalNiceConfig();
const qsModelConfig = await this.getModelConfig();
const qsModel = (mode !== 'shell') ? qsModelConfig?.defaultModel : undefined;
const session = new Session({
workingDir: casePath,
mux: this.mux,
useMux: true,
mode: mode,
niceConfig: niceConfig,
model: qsModel,
});
// Auto-detect completion phrase from CLAUDE.md BEFORE broadcasting
@@ -2750,8 +2756,9 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
mode: 'claude',
});
// Use Opus 4.5 for plan generation (better reasoning)
const modelToUse = 'opus';
// Use configured model for plan generation, falling back to opus
const planModelConfig = await this.getModelConfig();
const modelToUse = planModelConfig?.agentTypeOverrides?.implement || planModelConfig?.defaultModel || 'opus';
try {
const { result, cost } = await session.runPrompt(prompt, { model: modelToUse });
@@ -2866,7 +2873,8 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
}
const orchestrator = new PlanOrchestrator(this.mux, process.cwd(), outputDir);
const detailedModelConfig = await this.getModelConfig();
const orchestrator = new PlanOrchestrator(this.mux, process.cwd(), outputDir, detailedModelConfig ?? undefined);
// Store orchestrator for potential cancellation via API (not on disconnect)
// Plan generation continues even if browser disconnects - only explicit cancel stops it
@@ -4566,6 +4574,18 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return undefined;
}
// Helper to get model configuration from settings
private async getModelConfig(): Promise<{ defaultModel?: string; agentTypeOverrides?: Record<string, string> } | null> {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
try {
const content = await fs.readFile(settingsPath, 'utf-8');
return JSON.parse(content).modelConfig || null;
} catch {
return null;
}
}
private async startScheduledRun(prompt: string, workingDir: string, durationMinutes: number): Promise<ScheduledRun> {
const id = uuidv4();
const now = Date.now();