@xelth/eck-snapshot 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -4
- package/setup.json +10 -30
- package/src/cli/cli.js +11 -54
- package/src/cli/commands/autoDocs.js +1 -32
- package/src/cli/commands/createSnapshot.js +125 -6
- package/src/cli/commands/doctor.js +60 -0
- package/src/cli/commands/updateSnapshot.js +3 -3
- package/src/config.js +44 -0
- package/src/core/skeletonizer.js +77 -52
- package/src/services/claudeCliService.js +5 -0
- package/src/templates/agent-prompt.template.md +104 -7
- package/src/templates/architect-prompt.template.md +112 -23
- package/src/templates/multiAgent.md +9 -0
- package/src/utils/aiHeader.js +147 -95
- package/src/utils/eckProtocolParser.js +221 -0
- package/src/utils/fileUtils.js +181 -185
- package/src/utils/tokenEstimator.js +4 -1
- package/src/cli/commands/askGpt.js +0 -29
- package/src/services/authService.js +0 -20
- package/src/services/dispatcherService.js +0 -33
- package/src/services/gptService.js +0 -302
- package/src/services/gptService.test.js +0 -120
- package/src/templates/vectorMode.md +0 -22
package/src/utils/aiHeader.js
CHANGED
|
@@ -39,39 +39,109 @@ function buildAgentDefinitions(executionAgents) {
|
|
|
39
39
|
return definitions;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Parse journal entries from JOURNAL.md content
|
|
44
|
+
* @param {string} journalContent - Raw content of JOURNAL.md
|
|
45
|
+
* @returns {Array} Array of parsed journal entries
|
|
46
|
+
*/
|
|
47
|
+
function parseJournalEntries(journalContent) {
|
|
48
|
+
if (!journalContent || typeof journalContent !== 'string') {
|
|
49
|
+
return [];
|
|
45
50
|
}
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// Split by --- separators, filter empty blocks
|
|
53
|
+
const blocks = journalContent.split(/^---$/m).filter(b => b.trim());
|
|
54
|
+
const entries = [];
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < blocks.length; i += 2) {
|
|
57
|
+
const frontmatter = blocks[i];
|
|
58
|
+
const body = blocks[i + 1] || '';
|
|
59
|
+
|
|
60
|
+
// Parse frontmatter
|
|
61
|
+
const typeMatch = frontmatter.match(/^type:\s*(.+)$/m);
|
|
62
|
+
const scopeMatch = frontmatter.match(/^scope:\s*(.+)$/m);
|
|
63
|
+
const summaryMatch = frontmatter.match(/^summary:\s*(.+)$/m);
|
|
64
|
+
const dateMatch = frontmatter.match(/^(?:date|timestamp):\s*(.+)$/m);
|
|
65
|
+
const taskIdMatch = frontmatter.match(/^task_id:\s*(.+)$/m);
|
|
66
|
+
|
|
67
|
+
// Extract title from body (first # heading)
|
|
68
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
69
|
+
|
|
70
|
+
entries.push({
|
|
71
|
+
type: typeMatch ? typeMatch[1].trim() : 'unknown',
|
|
72
|
+
scope: scopeMatch ? scopeMatch[1].trim() : '',
|
|
73
|
+
summary: summaryMatch ? summaryMatch[1].trim() : (titleMatch ? titleMatch[1].trim() : ''),
|
|
74
|
+
date: dateMatch ? dateMatch[1].trim() : '',
|
|
75
|
+
taskId: taskIdMatch ? taskIdMatch[1].trim() : '',
|
|
76
|
+
body: body.trim()
|
|
77
|
+
});
|
|
53
78
|
}
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
return entries;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build a compact journal summary for the architect
|
|
85
|
+
* Shows: last entry (full) + 5 previous (headers only) + total count
|
|
86
|
+
*/
|
|
87
|
+
function buildJournalSummary(journalContent) {
|
|
88
|
+
const entries = parseJournalEntries(journalContent);
|
|
89
|
+
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
return 'No journal entries found.';
|
|
58
92
|
}
|
|
59
93
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
let summary = '';
|
|
95
|
+
|
|
96
|
+
// Last entry - show full details
|
|
97
|
+
const lastEntry = entries[0];
|
|
98
|
+
summary += `**Latest Entry** (${lastEntry.date || 'no date'}):\n`;
|
|
99
|
+
summary += `- Type: \`${lastEntry.type}\` | Scope: \`${lastEntry.scope}\`\n`;
|
|
100
|
+
summary += `- ${lastEntry.summary}\n`;
|
|
101
|
+
if (lastEntry.body) {
|
|
102
|
+
// Include body but limit to first 3 lines
|
|
103
|
+
const bodyLines = lastEntry.body.split('\n').filter(l => l.trim()).slice(0, 4);
|
|
104
|
+
summary += bodyLines.map(l => ` ${l}`).join('\n') + '\n';
|
|
63
105
|
}
|
|
64
106
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
// Previous 5 entries - headers only
|
|
108
|
+
if (entries.length > 1) {
|
|
109
|
+
summary += '\n**Previous entries:**\n';
|
|
110
|
+
const previousEntries = entries.slice(1, 6);
|
|
111
|
+
for (const entry of previousEntries) {
|
|
112
|
+
summary += `- \`${entry.type}(${entry.scope})\`: ${entry.summary}\n`;
|
|
70
113
|
}
|
|
71
|
-
section += '\n';
|
|
72
114
|
}
|
|
73
115
|
|
|
74
|
-
|
|
116
|
+
// Total count
|
|
117
|
+
if (entries.length > 6) {
|
|
118
|
+
summary += `\n*...and ${entries.length - 6} more entries in .eck/JOURNAL.md*\n`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return summary;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildEckManifestSection(eckManifest) {
|
|
125
|
+
if (!eckManifest) {
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let section = '\n## Project Context (.eck Directory)\n\n';
|
|
130
|
+
section += 'This project has a `.eck/` directory with project-specific context files.\n';
|
|
131
|
+
section += 'The coder agent can read these files when needed. Available files:\n\n';
|
|
132
|
+
section += '- `CONTEXT.md` - Project overview and architecture\n';
|
|
133
|
+
section += '- `OPERATIONS.md` - Common commands and workflows\n';
|
|
134
|
+
section += '- `JOURNAL.md` - Development history\n';
|
|
135
|
+
section += '- `ROADMAP.md` - Planned features\n';
|
|
136
|
+
section += '- `TECH_DEBT.md` - Known issues and refactoring needs\n';
|
|
137
|
+
section += '- `ENVIRONMENT.md` - Environment-specific settings\n\n';
|
|
138
|
+
|
|
139
|
+
// Add journal summary (compact view for architect)
|
|
140
|
+
if (eckManifest.journal) {
|
|
141
|
+
section += '### Recent Development Activity\n\n';
|
|
142
|
+
section += buildJournalSummary(eckManifest.journal) + '\n';
|
|
143
|
+
}
|
|
144
|
+
|
|
75
145
|
section += '---\n\n';
|
|
76
146
|
|
|
77
147
|
return section;
|
|
@@ -255,7 +325,7 @@ Your primary role is **Senior Architect**. You formulate high-level strategy. Fo
|
|
|
255
325
|
|
|
256
326
|
- **Senior Architect (You):** Sets strategy, defines high-level tasks.
|
|
257
327
|
- **Junior Architect (\`gemini_wsl\`):** Receives strategic tasks, analyzes the \`_ja.md\` snapshot, breaks the task down, and commands the Coder.
|
|
258
|
-
- **Coder (\`claude\`):** Receives small, precise coding tasks from the Junior Architect. **Claude is
|
|
328
|
+
- **Coder (\`claude\`):** Receives small, precise coding tasks from the Junior Architect. **Claude is responsible for keeping the .eck/ manifest files accurate and synchronized with the code.**`;
|
|
259
329
|
|
|
260
330
|
commandFormats = `### COMMAND FORMATS
|
|
261
331
|
|
|
@@ -339,92 +409,74 @@ Use \`execute_strategic_task\` for complex features. The JA will use its own sna
|
|
|
339
409
|
} else {
|
|
340
410
|
hierarchicalWorkflow = `### AGENT WORKFLOW
|
|
341
411
|
|
|
342
|
-
Your role is **Architect**. You formulate technical plans and delegate code implementation tasks directly to the **Coder** agents
|
|
412
|
+
Your role is **Architect**. You formulate technical plans and delegate code implementation tasks directly to the **Coder** agents.
|
|
343
413
|
|
|
344
|
-
|
|
345
|
-
- **Coder (e.g., \`local_dev\`):** Receives precise coding tasks and executes them.`;
|
|
414
|
+
**Your secondary duty is DOCUMENTATION INTEGRITY.** You must ensure the Coder updates .eck/ files whenever the project structure, roadmap, or debt changes.
|
|
346
415
|
|
|
347
|
-
|
|
416
|
+
- **Architect (You):** Sets strategy, defines tasks, enforces manifest maintenance.
|
|
417
|
+
- **Coder (e.g., \`local_dev\`):** Receives precise coding tasks and executes them, including manifest updates.`;
|
|
348
418
|
|
|
349
|
-
|
|
419
|
+
commandFormats = `### COMMAND FORMATS (Eck-Protocol v2)
|
|
350
420
|
|
|
351
|
-
**
|
|
352
|
-
Use \`apply_code_changes\` for direct tasks where you provide all details.
|
|
421
|
+
You MUST use the **Eck-Protocol v2** format for all code execution tasks. This format combines Markdown for analysis, XML tags for file operations, and JSON for routing metadata.
|
|
353
422
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
"specific command to verify functionality"
|
|
386
|
-
],
|
|
387
|
-
"expected_outcome": "what should work after changes",
|
|
388
|
-
"post_execution_steps": {
|
|
389
|
-
"journal_entry": {
|
|
390
|
-
"type": "feat",
|
|
391
|
-
"scope": "authentication",
|
|
392
|
-
"summary": "Brief description of what was accomplished",
|
|
393
|
-
"details": "Detailed explanation of changes, impacts, and technical notes"
|
|
394
|
-
},
|
|
395
|
-
"mcp_feedback": {
|
|
396
|
-
"success": true,
|
|
397
|
-
"errors": [],
|
|
398
|
-
"mcp_version": "1.0"
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
\`\`\``;
|
|
423
|
+
**CRITICAL DISPLAY RULE (THE 4-BACKTICK WRAPPER):**
|
|
424
|
+
To ensure your command is copy-pasteable without breaking UI rendering, you **MUST** wrap the ENTIRE protocol output in a \`text\` block using **QUADRUPLE BACKTICKS** (\` \`\`\`\` \`).
|
|
425
|
+
|
|
426
|
+
**Why?** Your command contains internal code blocks with 3 backticks. To escape them, the outer container needs 4.
|
|
427
|
+
|
|
428
|
+
**Required Output Format:**
|
|
429
|
+
|
|
430
|
+
\`\`\`\`text
|
|
431
|
+
# Analysis
|
|
432
|
+
[Your reasoning...]
|
|
433
|
+
|
|
434
|
+
## Changes
|
|
435
|
+
<file path="example.js" action="replace">
|
|
436
|
+
\\\`\\\`\\\`javascript
|
|
437
|
+
// Internal code block uses 3 backticks
|
|
438
|
+
const x = 1;
|
|
439
|
+
\\\`\\\`\\\`
|
|
440
|
+
</file>
|
|
441
|
+
|
|
442
|
+
## Metadata
|
|
443
|
+
\\\`\\\`\\\`json
|
|
444
|
+
{ ... }
|
|
445
|
+
\\\`\\\`\\\`
|
|
446
|
+
\`\`\`\`
|
|
447
|
+
|
|
448
|
+
**File Actions:**
|
|
449
|
+
- \`create\`: Create a new file (requires full content)
|
|
450
|
+
- \`replace\`: Overwrite existing file (requires full content)
|
|
451
|
+
- \`modify\`: Replace specific sections (provide context)
|
|
452
|
+
- \`delete\`: Delete the file
|
|
453
|
+
`;
|
|
404
454
|
}
|
|
405
455
|
|
|
406
456
|
// --- This is the main/Senior Architect prompt logic ---
|
|
407
457
|
let template;
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
458
|
+
template = await loadTemplate(promptTemplates.multiAgent);
|
|
459
|
+
// --- INJECT DYNAMIC CONTEXT ---
|
|
460
|
+
template = template.replace('{{projectOverview}}', projectOverview);
|
|
461
|
+
template = template.replace('{{eckManifestSection}}', eckManifestSection);
|
|
462
|
+
// --- END INJECT ---
|
|
463
|
+
|
|
464
|
+
// Filter out gemini agents if not in JA mode
|
|
465
|
+
const filteredExecutionAgents = {};
|
|
466
|
+
for (const [key, agent] of Object.entries(executionAgents)) {
|
|
467
|
+
const isGeminiAgent = key.includes('gemini') || (agent.name && agent.name.toLowerCase().includes('gemini'));
|
|
468
|
+
if (isGeminiAgent) {
|
|
469
|
+
if (withJa && agent.active) filteredExecutionAgents[key] = agent;
|
|
470
|
+
} else {
|
|
471
|
+
if (agent.active) filteredExecutionAgents[key] = agent;
|
|
472
|
+
}
|
|
421
473
|
}
|
|
422
474
|
|
|
423
|
-
const agentDefinitions = buildAgentDefinitions(
|
|
475
|
+
const agentDefinitions = buildAgentDefinitions(filteredExecutionAgents);
|
|
424
476
|
|
|
425
477
|
const data = {
|
|
426
478
|
...context,
|
|
427
|
-
timestamp: new Date().
|
|
479
|
+
timestamp: new Date().toLocaleString(),
|
|
428
480
|
architectPersona,
|
|
429
481
|
agentDefinitions,
|
|
430
482
|
hierarchicalWorkflow,
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eck-Protocol v2 Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses hybrid Markdown/XML/JSON format for agent communication.
|
|
5
|
+
* This is a pure text parser - no shell commands are involved.
|
|
6
|
+
*
|
|
7
|
+
* Format specification:
|
|
8
|
+
* - Markdown for human-readable analysis/thinking
|
|
9
|
+
* - XML-like <file> tags for code changes (with code in standard fences)
|
|
10
|
+
* - JSON in fenced blocks for structured metadata
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parses Eck-Protocol v2 response from an agent.
|
|
15
|
+
* @param {string} text - Raw text response from the agent
|
|
16
|
+
* @returns {object} Parsed structure with thought, files, and metadata
|
|
17
|
+
*/
|
|
18
|
+
export function parseEckResponse(text) {
|
|
19
|
+
const result = {
|
|
20
|
+
thought: '',
|
|
21
|
+
files: [],
|
|
22
|
+
metadata: {},
|
|
23
|
+
raw: text
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (!text || typeof text !== 'string') {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
result.files = extractFiles(text);
|
|
31
|
+
result.metadata = extractMetadata(text);
|
|
32
|
+
result.thought = extractThought(text);
|
|
33
|
+
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extracts file changes from <file> tags.
|
|
39
|
+
* @param {string} text - Raw text
|
|
40
|
+
* @returns {Array<{path: string, action: string, content: string}>}
|
|
41
|
+
*/
|
|
42
|
+
function extractFiles(text) {
|
|
43
|
+
const files = [];
|
|
44
|
+
|
|
45
|
+
// Pattern: <file path="..." action="..."> content </file>
|
|
46
|
+
const fileRegex = /<file\s+path=["']([^"']+)["']\s+action=["']([^"']+)["']>\s*([\s\S]*?)\s*<\/file>/gi;
|
|
47
|
+
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = fileRegex.exec(text)) !== null) {
|
|
50
|
+
const path = match[1];
|
|
51
|
+
const action = match[2];
|
|
52
|
+
let content = match[3].trim();
|
|
53
|
+
|
|
54
|
+
// Strip markdown code fences if present
|
|
55
|
+
const fenceMatch = content.match(/^```[\w]*\s*\n?([\s\S]*?)\n?\s*```$/);
|
|
56
|
+
if (fenceMatch) {
|
|
57
|
+
content = fenceMatch[1];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
content = content.replace(/\r\n/g, '\n');
|
|
61
|
+
files.push({ path, action, content });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Extracts metadata from JSON fenced blocks.
|
|
69
|
+
* @param {string} text - Raw text
|
|
70
|
+
* @returns {object} Parsed metadata object
|
|
71
|
+
*/
|
|
72
|
+
function extractMetadata(text) {
|
|
73
|
+
let metadata = {};
|
|
74
|
+
|
|
75
|
+
// Try: ## Metadata section with JSON block
|
|
76
|
+
const metadataSectionMatch = text.match(/##\s*Metadata\s*\n+```json\s*\n([\s\S]*?)\n```/i);
|
|
77
|
+
if (metadataSectionMatch) {
|
|
78
|
+
try {
|
|
79
|
+
metadata = JSON.parse(metadataSectionMatch[1].trim());
|
|
80
|
+
return metadata;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.warn('Failed to parse Metadata JSON:', e.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Try: <journal> tag with JSON
|
|
87
|
+
const journalMatch = text.match(/<journal>\s*```json\s*\n?([\s\S]*?)\n?\s*```\s*<\/journal>/i);
|
|
88
|
+
if (journalMatch) {
|
|
89
|
+
try {
|
|
90
|
+
metadata.journal = JSON.parse(journalMatch[1].trim());
|
|
91
|
+
return metadata;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.warn('Failed to parse journal JSON:', e.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try: simple <journal type="..." scope="...">summary</journal>
|
|
98
|
+
const simpleJournalMatch = text.match(/<journal\s+type=["']([^"']+)["']\s+scope=["']([^"']+)["']>([^<]+)<\/journal>/i);
|
|
99
|
+
if (simpleJournalMatch) {
|
|
100
|
+
metadata.journal = {
|
|
101
|
+
type: simpleJournalMatch[1],
|
|
102
|
+
scope: simpleJournalMatch[2],
|
|
103
|
+
summary: simpleJournalMatch[3].trim()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return metadata;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extracts the thought/analysis section.
|
|
112
|
+
* @param {string} text - Raw text
|
|
113
|
+
* @returns {string} The thought/analysis content
|
|
114
|
+
*/
|
|
115
|
+
function extractThought(text) {
|
|
116
|
+
const changesIndex = text.search(/##\s*Changes/i);
|
|
117
|
+
const fileIndex = text.search(/<file\s+path=/i);
|
|
118
|
+
|
|
119
|
+
let endIndex = text.length;
|
|
120
|
+
if (changesIndex !== -1 && fileIndex !== -1) {
|
|
121
|
+
endIndex = Math.min(changesIndex, fileIndex);
|
|
122
|
+
} else if (changesIndex !== -1) {
|
|
123
|
+
endIndex = changesIndex;
|
|
124
|
+
} else if (fileIndex !== -1) {
|
|
125
|
+
endIndex = fileIndex;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let thought = text.substring(0, endIndex).trim();
|
|
129
|
+
thought = thought.replace(/^#\s*(Analysis|Thinking|Plan)\s*\n*/i, '').trim();
|
|
130
|
+
return thought;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validates if a response contains valid Eck-Protocol v2 structure.
|
|
135
|
+
* @param {string} text - Raw text to validate
|
|
136
|
+
* @returns {{valid: boolean, hasFiles: boolean, hasMetadata: boolean, errors: string[]}}
|
|
137
|
+
*/
|
|
138
|
+
export function validateEckResponse(text) {
|
|
139
|
+
const result = {
|
|
140
|
+
valid: true,
|
|
141
|
+
hasFiles: false,
|
|
142
|
+
hasMetadata: false,
|
|
143
|
+
errors: []
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!text || typeof text !== 'string') {
|
|
147
|
+
result.valid = false;
|
|
148
|
+
result.errors.push('Response is empty or not a string');
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const hasFileTags = /<file\s+path=/.test(text);
|
|
153
|
+
if (hasFileTags) {
|
|
154
|
+
result.hasFiles = true;
|
|
155
|
+
const openTags = (text.match(/<file\s+/g) || []).length;
|
|
156
|
+
const closeTags = (text.match(/<\/file>/g) || []).length;
|
|
157
|
+
if (openTags !== closeTags) {
|
|
158
|
+
result.valid = false;
|
|
159
|
+
result.errors.push(`Mismatched file tags: ${openTags} opening, ${closeTags} closing`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const hasMetadata = /##\s*Metadata|<journal/i.test(text);
|
|
164
|
+
if (hasMetadata) {
|
|
165
|
+
result.hasMetadata = true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parses response with fallback to legacy JSON format.
|
|
173
|
+
* @param {string} text - Raw response text
|
|
174
|
+
* @returns {object} Parsed result with format indicator
|
|
175
|
+
*/
|
|
176
|
+
export function parseWithFallback(text) {
|
|
177
|
+
const validation = validateEckResponse(text);
|
|
178
|
+
|
|
179
|
+
if (validation.hasFiles || validation.hasMetadata) {
|
|
180
|
+
const parsed = parseEckResponse(text);
|
|
181
|
+
return { format: 'eck-v2', ...parsed };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fallback: try to parse as JSON
|
|
185
|
+
try {
|
|
186
|
+
const jsonMatch = text.match(/```json\s*\n([\s\S]*?)\n```/);
|
|
187
|
+
if (jsonMatch) {
|
|
188
|
+
const jsonData = JSON.parse(jsonMatch[1]);
|
|
189
|
+
return {
|
|
190
|
+
format: 'legacy-json',
|
|
191
|
+
thought: text.split('```json')[0].trim(),
|
|
192
|
+
data: jsonData,
|
|
193
|
+
files: [],
|
|
194
|
+
metadata: {}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const jsonData = JSON.parse(text.trim());
|
|
199
|
+
return {
|
|
200
|
+
format: 'pure-json',
|
|
201
|
+
thought: '',
|
|
202
|
+
data: jsonData,
|
|
203
|
+
files: [],
|
|
204
|
+
metadata: {}
|
|
205
|
+
};
|
|
206
|
+
} catch (e) {
|
|
207
|
+
return {
|
|
208
|
+
format: 'plain-text',
|
|
209
|
+
thought: text,
|
|
210
|
+
files: [],
|
|
211
|
+
metadata: {},
|
|
212
|
+
raw: text
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export default {
|
|
218
|
+
parseEckResponse,
|
|
219
|
+
validateEckResponse,
|
|
220
|
+
parseWithFallback
|
|
221
|
+
};
|