mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
feat: add configuration backup, sync, and version checker services
- Implemented ConfigBackupService for backing up local configuration files. - Added ConfigSyncService to download configuration files from GitHub with remote-first conflict resolution. - Created VersionChecker to check application version against the latest GitHub release with caching. - Introduced security validation utilities for input validation to prevent common vulnerabilities. - Developed utility functions to start and stop Docusaurus documentation server.
This commit is contained in:
@@ -266,6 +266,21 @@ export class JsonLinesParser implements IOutputParser {
|
||||
return units;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug logging helper for CLI output parsing
|
||||
* Enable with DEBUG_CLI_OUTPUT=true environment variable
|
||||
*/
|
||||
private debugLog(event: string, data: Record<string, unknown>): void {
|
||||
if (process.env.DEBUG_CLI_OUTPUT) {
|
||||
const logEntry = {
|
||||
ts: new Date().toISOString(),
|
||||
event,
|
||||
...data
|
||||
};
|
||||
console.error(`[CLI_OUTPUT_DEBUG] ${JSON.stringify(logEntry)}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map parsed JSON object to appropriate IR type
|
||||
* Handles various JSON event formats from different CLI tools:
|
||||
@@ -275,6 +290,7 @@ export class JsonLinesParser implements IOutputParser {
|
||||
* - OpenCode CLI: --format json (step_start, text, step_finish)
|
||||
*/
|
||||
private mapJsonToIR(json: any, fallbackStreamType: 'stdout' | 'stderr'): CliOutputUnit | null {
|
||||
this.debugLog('mapJsonToIR_input', { type: json.type, role: json.role, keys: Object.keys(json) });
|
||||
// Handle numeric timestamp (milliseconds) from OpenCode
|
||||
const timestamp = typeof json.timestamp === 'number'
|
||||
? new Date(json.timestamp).toISOString()
|
||||
@@ -772,6 +788,14 @@ export class JsonLinesParser implements IOutputParser {
|
||||
|
||||
// Default: treat as stdout/stderr based on fallback
|
||||
if (json.content || json.message || json.text) {
|
||||
this.debugLog('mapJsonToIR_fallback_stdout', {
|
||||
type: json.type,
|
||||
fallbackType: fallbackStreamType,
|
||||
hasContent: !!json.content,
|
||||
hasMessage: !!json.message,
|
||||
hasText: !!json.text,
|
||||
contentPreview: (json.content || json.message || json.text || '').substring(0, 100)
|
||||
});
|
||||
return {
|
||||
type: fallbackStreamType,
|
||||
content: json.content || json.message || json.text,
|
||||
@@ -780,6 +804,7 @@ export class JsonLinesParser implements IOutputParser {
|
||||
}
|
||||
|
||||
// Unrecognized structure, return as metadata
|
||||
this.debugLog('mapJsonToIR_fallback_metadata', { type: json.type, keys: Object.keys(json) });
|
||||
return {
|
||||
type: 'metadata',
|
||||
content: json,
|
||||
@@ -1171,6 +1196,41 @@ export function createOutputParser(format: 'text' | 'json-lines'): IOutputParser
|
||||
|
||||
// ========== Utility Functions ==========
|
||||
|
||||
/**
|
||||
* Find the start index of the last streaming_content group
|
||||
* Groups are separated by non-streaming events (tool_call, metadata, etc.)
|
||||
* This helps filter out intermediate assistant messages in multi-turn executions
|
||||
*
|
||||
* @param units - All output units
|
||||
* @returns Index of the last streaming_content group start
|
||||
*/
|
||||
function findLastStreamingGroup(units: CliOutputUnit[]): number {
|
||||
let lastGroupStart = 0;
|
||||
|
||||
for (let i = units.length - 1; i >= 0; i--) {
|
||||
const unit = units[i];
|
||||
|
||||
// streaming_content found, this could be part of the last group
|
||||
if (unit.type === 'streaming_content') {
|
||||
lastGroupStart = i;
|
||||
|
||||
// Look backwards to find the start of this group
|
||||
// (first streaming_content after a non-streaming event)
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (units[j].type === 'streaming_content') {
|
||||
lastGroupStart = j;
|
||||
} else {
|
||||
// Found a separator (tool_call, metadata, etc.)
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lastGroupStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten output units into plain text string
|
||||
* Useful for Resume scenario where we need concatenated context
|
||||
@@ -1197,12 +1257,23 @@ export function flattenOutputUnits(
|
||||
stripCommandJsonBlocks = false
|
||||
} = options || {};
|
||||
|
||||
// Debug logging for output unit analysis
|
||||
if (process.env.DEBUG_CLI_OUTPUT) {
|
||||
const typeCounts: Record<string, number> = {};
|
||||
for (const u of units) {
|
||||
typeCounts[u.type] = (typeCounts[u.type] || 0) + 1;
|
||||
}
|
||||
console.error(`[CLI_OUTPUT_DEBUG] flattenOutputUnits_input: ${JSON.stringify({ unitCount: units.length, typeCounts, includeTypes, excludeTypes })}`);
|
||||
}
|
||||
|
||||
// Special handling for streaming_content: concatenate all into a single agent_message unit
|
||||
// Gemini delta messages are incremental (each contains partial content to append)
|
||||
let processedUnits = units;
|
||||
const streamingUnits = units.filter(u => u.type === 'streaming_content');
|
||||
const agentMessages = units.filter(u => u.type === 'agent_message');
|
||||
|
||||
if (streamingUnits.length > 0) {
|
||||
const hasAgentMessage = units.some(u => u.type === 'agent_message');
|
||||
const hasAgentMessage = agentMessages.length > 0;
|
||||
|
||||
// If a non-delta final agent_message already exists, prefer it and simply drop streaming_content.
|
||||
// This avoids duplicated final output when providers emit BOTH streaming deltas and a final message frame.
|
||||
@@ -1210,18 +1281,38 @@ export function flattenOutputUnits(
|
||||
|
||||
// If no agent_message exists, synthesize one from streaming_content (delta-only streams).
|
||||
if (!hasAgentMessage) {
|
||||
const concatenatedContent = streamingUnits
|
||||
// For multi-turn executions, only keep the LAST group of streaming_content
|
||||
// (separated by tool_call/tool_result/metadata events)
|
||||
// This filters out intermediate planning/status messages
|
||||
const lastGroupStartIndex = findLastStreamingGroup(units);
|
||||
const lastGroupStreamingUnits = streamingUnits.filter((_, idx) => {
|
||||
const unitIndex = units.indexOf(streamingUnits[idx]);
|
||||
return unitIndex >= lastGroupStartIndex;
|
||||
});
|
||||
|
||||
const concatenatedContent = lastGroupStreamingUnits
|
||||
.map(u => typeof u.content === 'string' ? u.content : '')
|
||||
.join('');
|
||||
|
||||
processedUnits.push({
|
||||
type: 'agent_message',
|
||||
content: concatenatedContent,
|
||||
timestamp: streamingUnits[streamingUnits.length - 1].timestamp
|
||||
});
|
||||
if (concatenatedContent) {
|
||||
processedUnits.push({
|
||||
type: 'agent_message',
|
||||
content: concatenatedContent,
|
||||
timestamp: lastGroupStreamingUnits[lastGroupStreamingUnits.length - 1].timestamp
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For multi-turn executions with multiple agent_message units (Codex/Claude),
|
||||
// only keep the LAST agent_message (final result)
|
||||
if (agentMessages.length > 1) {
|
||||
const lastAgentMessage = agentMessages[agentMessages.length - 1];
|
||||
processedUnits = processedUnits.filter(u =>
|
||||
u.type !== 'agent_message' || u === lastAgentMessage
|
||||
);
|
||||
}
|
||||
|
||||
// Filter units by type
|
||||
let filtered = processedUnits;
|
||||
if (includeTypes && includeTypes.length > 0) {
|
||||
@@ -1231,6 +1322,15 @@ export function flattenOutputUnits(
|
||||
filtered = filtered.filter(u => !excludeTypes.includes(u.type));
|
||||
}
|
||||
|
||||
// Debug logging for filtered output
|
||||
if (process.env.DEBUG_CLI_OUTPUT) {
|
||||
const filteredTypeCounts: Record<string, number> = {};
|
||||
for (const u of filtered) {
|
||||
filteredTypeCounts[u.type] = (filteredTypeCounts[u.type] || 0) + 1;
|
||||
}
|
||||
console.error(`[CLI_OUTPUT_DEBUG] flattenOutputUnits_filtered: ${JSON.stringify({ filteredCount: filtered.length, filteredTypeCounts })}`);
|
||||
}
|
||||
|
||||
// Convert to text
|
||||
const lines = filtered.map(unit => {
|
||||
let text = '';
|
||||
|
||||
Reference in New Issue
Block a user