+127
-47
@@ -20,3 +20,3 @@ #!/usr/bin/env node | ||
| const API_URL = process.env.CLAWFIX_API || 'https://clawfix.dev'; | ||
| const VERSION = '0.6.0'; | ||
| const VERSION = '0.7.0'; | ||
@@ -29,2 +29,3 @@ // --- Flags --- | ||
| const SHOW_HELP = args.includes('--help') || args.includes('-h'); | ||
| const SHOW_VERSION = args.includes('--version') || args.includes('-v') || args.includes('-V'); | ||
| const ONE_SHOT = args.includes('--scan') || args.includes('--no-interactive') || DRY_RUN; | ||
@@ -635,2 +636,9 @@ | ||
| // --- Concurrency guard --- | ||
| let busy = false; | ||
| // --- Paste detection: batch rapid lines into one message --- | ||
| let pasteBuffer = []; | ||
| let pasteTimer = null; | ||
| const PASTE_DELAY_MS = 80; // lines arriving within 80ms = paste | ||
| // --- Clear screen and show header --- | ||
@@ -688,10 +696,5 @@ process.stdout.write('\x1b[2J\x1b[H'); | ||
| rl.prompt(); | ||
| rl.on('line', async (line) => { | ||
| const input = line.trim(); | ||
| // --- Process a single input (command or chat) --- | ||
| async function handleInput(input) { | ||
| if (!input) { | ||
| // Empty enter → show issues summary | ||
| renderIssues(issues, serverIssues); | ||
| rl.prompt(); | ||
@@ -728,6 +731,7 @@ return; | ||
| try { | ||
| const payload = { ...diagnostic, _localIssues: issues.map(i => ({ severity: i.severity, text: i.text })) }; | ||
| const resp = await fetch(`${API_URL}/api/diagnose`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(diagnostic), | ||
| body: JSON.stringify(payload), | ||
| }); | ||
@@ -859,5 +863,48 @@ if (resp.ok) { | ||
| console.log(''); | ||
| await streamChat(input, diagnosticId, conversationId, rl); | ||
| busy = true; | ||
| try { | ||
| await streamChat(input, diagnosticId, conversationId, rl); | ||
| } finally { | ||
| busy = false; | ||
| } | ||
| console.log(''); | ||
| rl.prompt(); | ||
| } | ||
| // --- Flush paste buffer as a single combined message --- | ||
| function flushPasteBuffer() { | ||
| pasteTimer = null; | ||
| if (pasteBuffer.length === 0) return; | ||
| // Combine all buffered lines into one message | ||
| const combined = pasteBuffer.join('\n').trim(); | ||
| pasteBuffer = []; | ||
| if (!combined) { | ||
| rl.prompt(); | ||
| return; | ||
| } | ||
| // If the combined paste looks like a single command, handle as command | ||
| const firstLine = combined.split('\n')[0].trim(); | ||
| if (combined.split('\n').length === 1 || /^(exit|quit|q|help|\?|scan|rescan|issues?|status|fix\s+\d+|apply\s+\d+)$/i.test(firstLine)) { | ||
| handleInput(firstLine); | ||
| } else { | ||
| // Multi-line paste → send as one chat message | ||
| handleInput(combined); | ||
| } | ||
| } | ||
| rl.prompt(); | ||
| rl.on('line', (line) => { | ||
| const input = line.trim(); | ||
| // If busy streaming, silently drop input | ||
| if (busy) return; | ||
| // Paste detection: buffer rapid lines and flush after a delay | ||
| pasteBuffer.push(input); | ||
| if (pasteTimer) clearTimeout(pasteTimer); | ||
| pasteTimer = setTimeout(flushPasteBuffer, PASTE_DELAY_MS); | ||
| }); | ||
@@ -1008,53 +1055,80 @@ | ||
| // SSE streaming | ||
| // SSE streaming — collect full response, then render | ||
| // Buffer approach: collect content chunks, flush periodically for progressive display | ||
| process.stdout.write('\r\x1b[K'); | ||
| process.stdout.write(' '); | ||
| const reader = resp.body.getReader(); | ||
| const decoder = new TextDecoder(); | ||
| let buffer = ''; | ||
| let sseBuffer = ''; | ||
| let contentBuffer = ''; // accumulate content between flushes | ||
| let col = 2; // Current column (2 for indent) | ||
| let started = false; // whether we've written any content yet | ||
| let hadError = false; | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| // Flush accumulated content to screen | ||
| function flushContent() { | ||
| if (!contentBuffer) return; | ||
| if (!started) { | ||
| process.stdout.write(' '); | ||
| started = true; | ||
| } | ||
| for (const ch of contentBuffer) { | ||
| if (ch === '\n') { | ||
| process.stdout.write('\n '); | ||
| col = 2; | ||
| } else { | ||
| process.stdout.write(ch); | ||
| col++; | ||
| if (col > 76 && ch === ' ') { | ||
| process.stdout.write('\n '); | ||
| col = 2; | ||
| } | ||
| } | ||
| } | ||
| contentBuffer = ''; | ||
| } | ||
| buffer += decoder.decode(value, { stream: true }); | ||
| const lines = buffer.split('\n'); | ||
| buffer = lines.pop() || ''; | ||
| // Set up periodic flush (every 50ms) for smooth progressive rendering | ||
| const flushInterval = setInterval(flushContent, 50); | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || !trimmed.startsWith('data: ')) continue; | ||
| try { | ||
| while (true) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| const data = trimmed.slice(6); | ||
| if (data === '[DONE]') break; | ||
| sseBuffer += decoder.decode(value, { stream: true }); | ||
| const lines = sseBuffer.split('\n'); | ||
| sseBuffer = lines.pop() || ''; | ||
| try { | ||
| const parsed = JSON.parse(data); | ||
| if (parsed.error) { | ||
| process.stdout.write(c.red(parsed.error)); | ||
| break; | ||
| } | ||
| if (parsed.content) { | ||
| // Word-wrap at ~76 cols | ||
| for (const ch of parsed.content) { | ||
| if (ch === '\n') { | ||
| process.stdout.write('\n '); | ||
| col = 2; | ||
| } else { | ||
| process.stdout.write(ch); | ||
| col++; | ||
| if (col > 76 && ch === ' ') { | ||
| process.stdout.write('\n '); | ||
| col = 2; | ||
| } | ||
| } | ||
| for (const line of lines) { | ||
| const trimmed = line.trim(); | ||
| if (!trimmed || !trimmed.startsWith('data: ')) continue; | ||
| const data = trimmed.slice(6); | ||
| if (data === '[DONE]') break; | ||
| try { | ||
| const parsed = JSON.parse(data); | ||
| if (parsed.error) { | ||
| flushContent(); | ||
| process.stdout.write(c.red(parsed.error)); | ||
| hadError = true; | ||
| break; | ||
| } | ||
| } | ||
| } catch {} | ||
| if (parsed.content) { | ||
| contentBuffer += parsed.content; | ||
| } | ||
| } catch {} | ||
| } | ||
| if (hadError) break; | ||
| } | ||
| } finally { | ||
| clearInterval(flushInterval); | ||
| } | ||
| process.stdout.write('\n'); | ||
| // Final flush of any remaining content | ||
| flushContent(); | ||
| if (started || hadError) { | ||
| process.stdout.write('\n'); | ||
| } | ||
| } catch (err) { | ||
@@ -1100,2 +1174,7 @@ process.stdout.write('\r\x1b[K'); | ||
| async function main() { | ||
| if (SHOW_VERSION) { | ||
| console.log(`clawfix v${VERSION}`); | ||
| return; | ||
| } | ||
| if (SHOW_HELP) { | ||
@@ -1116,2 +1195,3 @@ console.log(` | ||
| --yes, -y Skip confirmation prompt and send automatically | ||
| --version, -v Show version | ||
| --help, -h Show this help message | ||
@@ -1118,0 +1198,0 @@ |
+1
-1
| { | ||
| "name": "clawfix", | ||
| "version": "0.6.1", | ||
| "version": "0.7.0", | ||
| "description": "AI-powered diagnostic and repair for OpenClaw installations", | ||
@@ -5,0 +5,0 @@ "bin": { |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
45048
5.94%1072
6.99%4
-20%