App Store customerReviews / customerReviewResponses and Google Play reviews.list / reviews.reply. draft writes one Markdown file per unanswered review to .storebound/replies (front matter approved: false, never overwritten); send only sends approved, unsent drafts that pass the checks (Play 350 characters, no incentives or rating requests, personal data / link / tone warnings) after a confirmation, and marks them sent. MCP: reviews_list, reviews_draft; send is never exposed.
34 lines
1.6 KiB
TypeScript
34 lines
1.6 KiB
TypeScript
// Minimal front matter for the local Markdown drafts storebound writes (review replies): "---", one `key: value` per
|
|
// line (strings, numbers, true / false, null; strings JSON-quoted when needed), "---", then the body. Pure.
|
|
|
|
export type FmValue = string | number | boolean | null;
|
|
|
|
export function parseFrontMatter(text: string): { data: Record<string, FmValue>; body: string } {
|
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
if (!m) return { data: {}, body: text };
|
|
const data: Record<string, FmValue> = {};
|
|
for (const line of m[1]!.split(/\r?\n/)) {
|
|
const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
|
|
if (!kv) continue;
|
|
data[kv[1]!] = scalar(kv[2]!.trim());
|
|
}
|
|
return { data, body: text.slice(m[0].length) };
|
|
}
|
|
|
|
function scalar(v: string): FmValue {
|
|
if (v === 'true') return true;
|
|
if (v === 'false') return false;
|
|
if (v === 'null' || v === '~' || v === '') return null;
|
|
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
if (v.startsWith('"')) { try { return JSON.parse(v) as string; } catch { return v; } }
|
|
if (v.startsWith("'") && v.endsWith("'")) return v.slice(1, -1).replace(/''/g, "'");
|
|
return v;
|
|
}
|
|
|
|
const plain = (s: string) => /^[\w./@+-][\w ./@:+-]*$/.test(s) && !/^(true|false|null|~|-?\d+(\.\d+)?)$/.test(s) && !/ $/.test(s);
|
|
|
|
export function stringifyFrontMatter(data: Record<string, FmValue | undefined>, body: string): string {
|
|
const lines = Object.entries(data).filter(([, v]) => v !== undefined)
|
|
.map(([k, v]) => `${k}: ${v === null ? 'null' : typeof v === 'string' ? (plain(v) ? v : JSON.stringify(v)) : String(v)}`);
|
|
return `---\n${lines.join('\n')}\n---\n${body.startsWith('\n') ? body : '\n' + body}`;
|
|
}
|