mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
chore: bump version to 0.1529
This commit is contained in:
@@ -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`)
|
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 claudeman-web`
|
||||||
|
|
||||||
**Version**: 0.1528 (must match `package.json` for npm publish)
|
**Version**: 0.1529 (must match `package.json` for npm publish)
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claudeman",
|
"name": "claudeman",
|
||||||
"version": "0.1528",
|
"version": "0.1529",
|
||||||
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+34
-1
@@ -6,9 +6,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { existsSync } from 'fs';
|
import { chmodSync, existsSync } from 'fs';
|
||||||
import { homedir, platform } from 'os';
|
import { homedir, platform } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import { createRequire } from 'module';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Configuration
|
// Configuration
|
||||||
@@ -115,6 +116,38 @@ if (majorVersion < MIN_NODE_VERSION) {
|
|||||||
console.log(colors.green(`✓ Node.js v${nodeVersion}`) + colors.dim(` (meets >=v${MIN_NODE_VERSION} requirement)`));
|
console.log(colors.green(`✓ Node.js v${nodeVersion}`) + colors.dim(` (meets >=v${MIN_NODE_VERSION} requirement)`));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 1b. Fix node-pty spawn-helper permissions (macOS posix_spawnp fix)
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
try {
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const ptyPath = join(require.resolve('node-pty'), '..');
|
||||||
|
const spawnHelper = join(ptyPath, 'build', 'Release', 'spawn-helper');
|
||||||
|
if (existsSync(spawnHelper)) {
|
||||||
|
chmodSync(spawnHelper, 0o755);
|
||||||
|
console.log(colors.green('✓ node-pty spawn-helper permissions fixed'));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Non-critical — only affects macOS with prebuilt binaries
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// 1c. Rebuild node-pty from source for Node.js 22+ compatibility
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (majorVersion >= 22) {
|
||||||
|
try {
|
||||||
|
console.log(colors.dim(' Rebuilding node-pty from source for Node.js 22+...'));
|
||||||
|
execSync('npm rebuild node-pty --build-from-source', { stdio: 'pipe', timeout: 120000 });
|
||||||
|
console.log(colors.green('✓ node-pty rebuilt from source'));
|
||||||
|
} catch {
|
||||||
|
hasWarnings = true;
|
||||||
|
console.log(colors.yellow('⚠ Failed to rebuild node-pty from source'));
|
||||||
|
console.log(colors.dim(' You may need to run: npm rebuild node-pty --build-from-source'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// 2. Check if terminal multiplexer is installed (tmux preferred, screen fallback)
|
// 2. Check if terminal multiplexer is installed (tmux preferred, screen fallback)
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|||||||
+7
-2
@@ -869,7 +869,7 @@ export class Session extends EventEmitter {
|
|||||||
cols: 120,
|
cols: 120,
|
||||||
rows: 40,
|
rows: 40,
|
||||||
cwd: this.workingDir,
|
cwd: this.workingDir,
|
||||||
env: { ...process.env, TERM: 'xterm-256color' },
|
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set claudeSessionId immediately since we passed --session-id to Claude
|
// Set claudeSessionId immediately since we passed --session-id to Claude
|
||||||
@@ -939,6 +939,8 @@ export class Session extends EventEmitter {
|
|||||||
...process.env,
|
...process.env,
|
||||||
PATH: getAugmentedPath(),
|
PATH: getAugmentedPath(),
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
|
COLORTERM: undefined,
|
||||||
|
CLAUDECODE: undefined,
|
||||||
// Inform Claude it's running within Claudeman (helps prevent self-termination)
|
// Inform Claude it's running within Claudeman (helps prevent self-termination)
|
||||||
CLAUDEMAN_SCREEN: '1',
|
CLAUDEMAN_SCREEN: '1',
|
||||||
CLAUDEMAN_SESSION_ID: this.id,
|
CLAUDEMAN_SESSION_ID: this.id,
|
||||||
@@ -1147,7 +1149,7 @@ export class Session extends EventEmitter {
|
|||||||
cols: 120,
|
cols: 120,
|
||||||
rows: 40,
|
rows: 40,
|
||||||
cwd: this.workingDir,
|
cwd: this.workingDir,
|
||||||
env: { ...process.env, TERM: 'xterm-256color' },
|
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
|
||||||
});
|
});
|
||||||
} catch (spawnErr) {
|
} catch (spawnErr) {
|
||||||
console.error('[Session] Failed to spawn PTY for shell mux attachment:', spawnErr);
|
console.error('[Session] Failed to spawn PTY for shell mux attachment:', spawnErr);
|
||||||
@@ -1293,6 +1295,7 @@ export class Session extends EventEmitter {
|
|||||||
|
|
||||||
const args = [
|
const args = [
|
||||||
'-p',
|
'-p',
|
||||||
|
'--verbose',
|
||||||
'--dangerously-skip-permissions',
|
'--dangerously-skip-permissions',
|
||||||
'--output-format', 'stream-json',
|
'--output-format', 'stream-json',
|
||||||
];
|
];
|
||||||
@@ -1311,6 +1314,8 @@ export class Session extends EventEmitter {
|
|||||||
...process.env,
|
...process.env,
|
||||||
PATH: getAugmentedPath(),
|
PATH: getAugmentedPath(),
|
||||||
TERM: 'xterm-256color',
|
TERM: 'xterm-256color',
|
||||||
|
COLORTERM: undefined,
|
||||||
|
CLAUDECODE: undefined,
|
||||||
// Inform Claude it's running within Claudeman
|
// Inform Claude it's running within Claudeman
|
||||||
CLAUDEMAN_SCREEN: '1',
|
CLAUDEMAN_SCREEN: '1',
|
||||||
CLAUDEMAN_SESSION_ID: this.id,
|
CLAUDEMAN_SESSION_ID: this.id,
|
||||||
|
|||||||
@@ -257,6 +257,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : '';
|
const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : '';
|
||||||
|
|
||||||
const envExports = [
|
const envExports = [
|
||||||
|
'unset CLAUDECODE',
|
||||||
|
'unset COLORTERM',
|
||||||
'export CLAUDEMAN_SCREEN=1',
|
'export CLAUDEMAN_SCREEN=1',
|
||||||
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
|
`export CLAUDEMAN_SESSION_ID=${sessionId}`,
|
||||||
`export CLAUDEMAN_SCREEN_NAME=${muxName}`,
|
`export CLAUDEMAN_SCREEN_NAME=${muxName}`,
|
||||||
|
|||||||
+29
-3
@@ -3719,10 +3719,19 @@ class ClaudemanApp {
|
|||||||
const minimizedCount = minimizedAgents?.size || 0;
|
const minimizedCount = minimizedAgents?.size || 0;
|
||||||
const subagentBadge = minimizedCount > 0 ? this.renderSubagentTabBadge(id, minimizedAgents) : '';
|
const subagentBadge = minimizedCount > 0 ? this.renderSubagentTabBadge(id, minimizedAgents) : '';
|
||||||
|
|
||||||
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}" data-id="${id}" data-color="${color}" onclick="app.selectSession('${this.escapeHtml(id)}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${this.escapeHtml(id)}')" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-label="${this.escapeHtml(name)} session">
|
// Show folder name if session has a custom name (to still show the directory context)
|
||||||
|
const folderName = session.workingDir ? session.workingDir.split('/').pop() || '' : '';
|
||||||
|
const showFolder = session.name && folderName && folderName !== name;
|
||||||
|
|
||||||
|
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}" data-id="${id}" data-color="${color}" onclick="app.selectSession('${this.escapeHtml(id)}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${this.escapeHtml(id)}')" tabindex="0" role="tab" aria-selected="${isActive ? 'true' : 'false'}" aria-label="${this.escapeHtml(name)} session" ${session.workingDir ? `title="${this.escapeHtml(session.workingDir)}"` : ''}>
|
||||||
<span class="tab-status ${status}" aria-hidden="true"></span>
|
<span class="tab-status ${status}" aria-hidden="true"></span>
|
||||||
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : ''}
|
<span class="tab-info">
|
||||||
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
|
<span class="tab-name-row">
|
||||||
|
${mode === 'shell' ? '<span class="tab-mode shell" aria-hidden="true">sh</span>' : ''}
|
||||||
|
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
|
||||||
|
</span>
|
||||||
|
${showFolder ? `<span class="tab-folder">\u{1F4C1} ${this.escapeHtml(folderName)}</span>` : ''}
|
||||||
|
</span>
|
||||||
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()" aria-label="${taskStats.running} running tasks">${taskStats.running}</span>` : ''}
|
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()" aria-label="${taskStats.running} running tasks">${taskStats.running}</span>` : ''}
|
||||||
${subagentBadge}
|
${subagentBadge}
|
||||||
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${this.escapeHtml(id)}')" title="Session options" aria-label="Session options" tabindex="0">⚙</span>
|
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${this.escapeHtml(id)}')" title="Session options" aria-label="Session options" tabindex="0">⚙</span>
|
||||||
@@ -8482,6 +8491,9 @@ class ClaudemanApp {
|
|||||||
document.getElementById('modalImageWatcherEnabled').checked = session.imageWatcherEnabled ?? true;
|
document.getElementById('modalImageWatcherEnabled').checked = session.imageWatcherEnabled ?? true;
|
||||||
document.getElementById('modalFlickerFilterEnabled').checked = session.flickerFilterEnabled ?? false;
|
document.getElementById('modalFlickerFilterEnabled').checked = session.flickerFilterEnabled ?? false;
|
||||||
|
|
||||||
|
// Populate session name input
|
||||||
|
document.getElementById('modalSessionName').value = session.name || '';
|
||||||
|
|
||||||
// Initialize color picker with current session color
|
// Initialize color picker with current session color
|
||||||
const currentColor = session.color || 'default';
|
const currentColor = session.color || 'default';
|
||||||
const colorPicker = document.getElementById('sessionColorPicker');
|
const colorPicker = document.getElementById('sessionColorPicker');
|
||||||
@@ -8510,6 +8522,20 @@ class ClaudemanApp {
|
|||||||
this.activeFocusTrap.activate();
|
this.activeFocusTrap.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveSessionName() {
|
||||||
|
if (!this.editingSessionId) return;
|
||||||
|
const name = document.getElementById('modalSessionName').value.trim();
|
||||||
|
try {
|
||||||
|
await fetch(`/api/sessions/${this.editingSessionId}/name`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name })
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this.showToast('Failed to save session name: ' + err.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async autoSaveAutoCompact() {
|
async autoSaveAutoCompact() {
|
||||||
if (!this.editingSessionId) return;
|
if (!this.editingSessionId) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -548,6 +548,11 @@
|
|||||||
<!-- Context Management Tab Content -->
|
<!-- Context Management Tab Content -->
|
||||||
<div class="modal-tab-content hidden" id="context-tab">
|
<div class="modal-tab-content hidden" id="context-tab">
|
||||||
<div class="form-section-header">Appearance</div>
|
<div class="form-section-header">Appearance</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Session Name</label>
|
||||||
|
<input type="text" id="modalSessionName" maxlength="128" placeholder="Auto (directory name)" onblur="app.saveSessionName()">
|
||||||
|
<span class="form-hint">Custom name shown in the tab (right-click tab to rename inline)</span>
|
||||||
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label>Session Color</label>
|
<label>Session Color</label>
|
||||||
<div class="color-picker" id="sessionColorPicker">
|
<div class="color-picker" id="sessionColorPicker">
|
||||||
|
|||||||
@@ -284,11 +284,39 @@ body {
|
|||||||
.session-tab.active[data-color="purple"] { background: rgba(168, 85, 247, 0.1); }
|
.session-tab.active[data-color="purple"] { background: rgba(168, 85, 247, 0.1); }
|
||||||
.session-tab.active[data-color="pink"] { background: rgba(236, 72, 153, 0.1); }
|
.session-tab.active[data-color="pink"] { background: rgba(236, 72, 153, 0.1); }
|
||||||
|
|
||||||
|
.session-tab .tab-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-tab .tab-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.session-tab .tab-name {
|
.session-tab .tab-name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-tab .tab-folder {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.session-tab .tab-folder {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.session-tab .tab-close {
|
.session-tab .tab-close {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
@@ -2785,7 +2813,7 @@ body {
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding-top: 0.75rem;
|
padding: 0.75rem;
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2846,7 +2874,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-content.modal-lg {
|
.modal-content.modal-lg {
|
||||||
max-width: 520px;
|
max-width: 540px;
|
||||||
max-height: 85vh;
|
max-height: 85vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
+63
-4
@@ -542,6 +542,22 @@ export class WebServer extends EventEmitter {
|
|||||||
threshold: 1024,
|
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
|
// Security headers on every response
|
||||||
this.app.addHook('onRequest', (_req, reply, done) => {
|
this.app.addHook('onRequest', (_req, reply, done) => {
|
||||||
reply.header('X-Content-Type-Options', 'nosniff');
|
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) => {
|
this.app.get('/api/cases/:caseName/ralph-wizard/files', async (req) => {
|
||||||
const { caseName } = req.params as { caseName: string };
|
const { caseName } = req.params as { caseName: string };
|
||||||
const casesDir = join(homedir(), 'claudeman-cases');
|
const casesDir = join(homedir(), 'claudeman-cases');
|
||||||
const casePath = join(casesDir, caseName);
|
let casePath = join(casesDir, caseName);
|
||||||
|
|
||||||
// Security: Path traversal protection - use relative path check
|
// Security: Path traversal protection - use relative path check
|
||||||
const resolvedCase = resolve(casePath);
|
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');
|
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');
|
const wizardDir = join(casePath, 'ralph-wizard');
|
||||||
|
|
||||||
if (!existsSync(wizardDir)) {
|
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) => {
|
this.app.get('/api/cases/:caseName/ralph-wizard/file/:filePath', async (req, reply) => {
|
||||||
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
|
const { caseName, filePath } = req.params as { caseName: string; filePath: string };
|
||||||
const casesDir = join(homedir(), 'claudeman-cases');
|
const casesDir = join(homedir(), 'claudeman-cases');
|
||||||
const casePath = join(casesDir, caseName);
|
let casePath = join(casesDir, caseName);
|
||||||
|
|
||||||
// Prevent browser caching - prompts change between plan generations
|
// Prevent browser caching - prompts change between plan generations
|
||||||
reply.header('Cache-Control', 'no-store, no-cache, must-revalidate');
|
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');
|
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');
|
const wizardDir = join(casePath, 'ralph-wizard');
|
||||||
|
|
||||||
// Decode the file path (it may be URL encoded)
|
// 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');
|
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;
|
let parsed: unknown = null;
|
||||||
if (isJson) {
|
if (isJson) {
|
||||||
try {
|
try {
|
||||||
parsed = JSON.parse(content);
|
parsed = JSON.parse(content);
|
||||||
} catch {
|
} 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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user