-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiff.ts
More file actions
186 lines (160 loc) · 4.7 KB
/
Copy pathdiff.ts
File metadata and controls
186 lines (160 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import { z } from "zod";
import { FileOperationSchema } from "../schemas.js";
import type { FileOperation } from "../types.js";
const FileOperationsArraySchema = z.array(FileOperationSchema);
/**
* Parse LLM compile output into file operations.
*
* The LLM should return a JSON array of {op, path, content} objects.
* This parser handles various edge cases:
* - JSON wrapped in markdown code fences
* - Extra text before/after the JSON
* - Minor formatting issues
*/
export function parseCompileOutput(raw: string): FileOperation[] {
const cleaned = extractJson(raw);
try {
const parsed = JSON.parse(cleaned);
return FileOperationsArraySchema.parse(parsed);
} catch (err) {
throw new Error(
`Failed to parse LLM compile output: ${err instanceof Error ? err.message : err}\n\nRaw output (first 500 chars):\n${raw.slice(0, 500)}`,
);
}
}
/**
* Extract JSON array from LLM output that may contain surrounding text.
* Handles: code fences, leading/trailing prose, truncated output, nested brackets in strings.
*/
function extractJson(raw: string): string {
let text = raw.trim();
// Strip all markdown code fences (including nested ones wrapping the whole output)
text = text.replace(/^```(?:json)?\s*\n?/gi, "").replace(/\n?```\s*$/gi, "");
text = text.trim();
// Find the first top-level [ and walk to its matching ]
const arrayStart = text.indexOf("[");
if (arrayStart === -1) return text;
let depth = 0;
let inString = false;
let escaped = false;
let arrayEnd = -1;
for (let i = arrayStart; i < text.length; i++) {
const ch = text[i];
if (escaped) {
escaped = false;
continue;
}
if (ch === "\\") {
escaped = true;
continue;
}
if (ch === '"') {
inString = !inString;
continue;
}
if (inString) continue;
if (ch === "[" || ch === "{") depth++;
else if (ch === "]" || ch === "}") {
depth--;
if (depth === 0) {
arrayEnd = i;
break;
}
}
}
if (arrayEnd !== -1) {
return text.slice(arrayStart, arrayEnd + 1);
}
// Truncated output — try to repair by closing open structures
if (depth > 0) {
let repaired = text.slice(arrayStart);
// If we're inside a string, close it
if (inString) repaired += '"';
// Strip any trailing incomplete key-value pair
repaired = repaired.replace(/,\s*"[^"]*"?\s*:?\s*"?[^"]*$/, "");
// Close remaining open structures
// Find what's still open by re-scanning
let s = false;
let esc = false;
const stack: string[] = [];
for (const c of repaired) {
if (esc) {
esc = false;
continue;
}
if (c === "\\") {
esc = true;
continue;
}
if (c === '"') {
s = !s;
continue;
}
if (s) continue;
if (c === "[") stack.push("]");
else if (c === "{") stack.push("}");
else if (c === "]" || c === "}") stack.pop();
}
// Close in reverse order
repaired += stack.reverse().join("");
return repaired;
}
return text;
}
/**
* Extract YAML frontmatter from a markdown article string.
* Returns the frontmatter fields as a Record and the body content.
*/
export function parseFrontmatter(markdown: string): {
frontmatter: Record<string, unknown>;
body: string;
} {
const match = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
if (!match) {
return { frontmatter: {}, body: markdown };
}
const rawFrontmatter = match[1]!;
const body = match[2]!;
// Simple YAML-like parser for frontmatter (handles common cases)
const frontmatter: Record<string, unknown> = {};
for (const line of rawFrontmatter.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
// Handle continuation lines (for summary: >)
const colonIdx = trimmed.indexOf(":");
if (colonIdx === -1) continue;
const key = trimmed.slice(0, colonIdx).trim();
let value: unknown = trimmed.slice(colonIdx + 1).trim();
// Parse arrays: [tag1, tag2]
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) {
value = value
.slice(1, -1)
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
// Parse booleans
else if (value === "true") value = true;
else if (value === "false") value = false;
// Remove quotes
else if (typeof value === "string" && value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
if (key) {
frontmatter[key] = value;
}
}
return { frontmatter, body: body.trim() };
}
/**
* Extract [[wikilinks]] from markdown content.
* Returns an array of slug strings.
*/
export function extractWikilinks(content: string): string[] {
const matches = content.matchAll(/\[\[([^\]]+)\]\]/g);
const links: string[] = [];
for (const match of matches) {
links.push(match[1]!.trim().toLowerCase().replace(/\s+/g, "-"));
}
return [...new Set(links)]; // deduplicate
}