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
|
-- Split a Markdown profile into its YAML frontmatter and its body.
--
-- The format is the common one other agent harnesses use: if the very first
-- line of the file is exactly `---`, everything up to the next line that is
-- exactly `---` is a YAML mapping, and everything after that closing fence is
-- the body. Trailing carriage returns are tolerated so CRLF files parse.
--
-- Anything unusual degrades to "no metadata, body only" with a warning rather
-- than an error, because the body is the part the user cannot afford to lose:
--
-- * no opening fence -> the whole file is the body, no warning
-- * unterminated opening fence-> the whole file is the body, no warning
-- (a lone `---` at the top of a prose file is a horizontal rule, not a
-- broken header, so this case is deliberately silent)
-- * empty fenced block -> empty mapping, no warning
-- * lyaml missing or erroring -> body after the fence, warning returned
-- * YAML document not a map -> body after the fence, warning returned
--
-- In the warning cases the fenced block is dropped rather than folded back
-- into the body: an unparseable header is noise the child agent should not be
-- asked to read. The body itself is never rewritten — no trimming, no
-- normalisation — so a prompt round-trips verbatim.
local M = {}
local function trim(s)
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
-- Iterate lines, yielding the line plus its start offset and the offset just
-- past its newline, so the caller can slice the original text exactly.
local function lines(text)
local pos = 1
return function()
if pos > #text then
return nil
end
local start = pos
local nl = text:find("\n", pos, true)
local line
if nl then
line = text:sub(start, nl - 1)
pos = nl + 1
else
line = text:sub(start)
pos = #text + 1
end
return line, start, pos
end
end
-- parse(text) -> data|nil, body, warning|nil
function M.parse(text)
if type(text) ~= "string" or text == "" then
return nil, "", nil
end
local next_line = lines(text)
local first, _, after_first = next_line()
if first == nil or trim(first) ~= "---" then
return nil, text, nil
end
local block_stop, body_start
for line, start, after in next_line do
if trim(line) == "---" then
block_stop = start - 1
body_start = after
break
end
end
if body_start == nil then
return nil, text, nil
end
local block = text:sub(after_first, block_stop)
local body = text:sub(body_start)
if trim(block) == "" then
return {}, body, nil
end
local ok_lyaml, lyaml = pcall(require, "lyaml")
if not ok_lyaml then
return nil, body, "lyaml is not installed; ignoring the YAML frontmatter"
end
local ok, data = pcall(lyaml.load, block)
if not ok then
return nil, body, "YAML frontmatter did not parse: " .. tostring(data)
end
if type(data) ~= "table" then
return nil, body, "YAML frontmatter is not a mapping; ignoring it"
end
return data, body, nil
end
return M
|