chore: bump version to 0.1529

This commit is contained in:
arkon
2026-02-18 07:28:15 +01:00
parent 31d3e7bc70
commit 50b17184ee
9 changed files with 172 additions and 14 deletions
+63 -4
View File
@@ -542,6 +542,22 @@ export class WebServer extends EventEmitter {
threshold: 1024,
});
// Optional HTTP Basic Auth (set CLAUDEMAN_PASSWORD env var to enable)
const authPassword = process.env.CLAUDEMAN_PASSWORD;
if (authPassword) {
const authUsername = process.env.CLAUDEMAN_USERNAME || 'admin';
const expectedHeader = 'Basic ' + Buffer.from(`${authUsername}:${authPassword}`).toString('base64');
this.app.addHook('onRequest', (req, reply, done) => {
const auth = req.headers.authorization;
if (auth === expectedHeader) {
done();
return;
}
reply.header('WWW-Authenticate', 'Basic realm="Claudeman"');
reply.code(401).send('Unauthorized');
});
}
// Security headers on every response
this.app.addHook('onRequest', (_req, reply, done) => {
reply.header('X-Content-Type-Options', 'nosniff');
@@ -2887,7 +2903,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
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 casePath = join(casesDir, caseName);
let casePath = join(casesDir, caseName);
// Security: Path traversal protection - use relative path check
const resolvedCase = resolve(casePath);
@@ -2897,6 +2913,19 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
// Check linked cases if path doesn't exist
if (!existsSync(casePath)) {
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[caseName]) {
casePath = linkedCases[caseName];
}
} catch {
// No linked cases file
}
}
const wizardDir = join(casePath, 'ralph-wizard');
if (!existsSync(wizardDir)) {
@@ -2935,7 +2964,7 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
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 casePath = join(casesDir, caseName);
let casePath = join(casesDir, caseName);
// Prevent browser caching - prompts change between plan generations
reply.header('Cache-Control', 'no-store, no-cache, must-revalidate');
@@ -2950,6 +2979,19 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Invalid case name');
}
// Check linked cases if path doesn't exist
if (!existsSync(casePath)) {
const linkedCasesFile = join(homedir(), '.claudeman', 'linked-cases.json');
try {
const linkedCases: Record<string, string> = JSON.parse(await fs.readFile(linkedCasesFile, 'utf-8'));
if (linkedCases[caseName]) {
casePath = linkedCases[caseName];
}
} catch {
// No linked cases file
}
}
const wizardDir = join(casePath, 'ralph-wizard');
// Decode the file path (it may be URL encoded)
@@ -2974,13 +3016,30 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
const isJson = filePath.endsWith('.json');
// Parse JSON content safely (may contain invalid JSON)
// Parse JSON content safely (may contain invalid JSON or unescaped control characters)
let parsed: unknown = null;
if (isJson) {
try {
parsed = JSON.parse(content);
} catch {
// Invalid JSON - return null for parsed, content still available as raw string
// Try repairing common JSON issues (unescaped control characters, trailing commas)
try {
let repaired = content;
// Fix trailing commas before closing brackets
repaired = repaired.replace(/,(\s*[\]}])/g, '$1');
// Fix unescaped control characters within JSON strings
repaired = repaired.replace(/"([^"\\]|\\.)*"/g, (match) => {
return match
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x1f]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
});
parsed = JSON.parse(repaired);
} catch {
// Still invalid - return null for parsed, content available as raw string
}
}
}