import { parseSkill } from './registry'; import { boardPatch, owliverPatch, patchFrontmatter } from './skillFields'; /** * Account-authored skills, as stored. * * A custom skill is its Markdown source and nothing else — the same artefact a * file in `src/skills/` is, read back by the same parser. These helpers exist so * the Add Skill dialog and the Skills page write that list identically; two * writers with two shapes would be a second skill system by accident. */ /** * The starting definitions offered to an author. * * Two templates, because there are two jobs and one of them was being learned * from the other's example. A UI skill's first draft declares a section; an * Owliver skill's declares triggers and the shapes of an answer. Both are the * same format, read by the same parser — what differs is which half of it the * author is being handed. */ const frontMatter = ({ id, name, description, pages, fallback }) => [ `id: ${id || fallback.id}`, `name: ${name || fallback.name}`, `description: ${description || fallback.description}`, 'pages:', (pages?.length ? pages : ['positions']).map((p) => ` - ${p}`).join('\n'), 'status: active', ].join('\n'); /** * The identity block every definition opens with, and nothing else. * * The configuration that follows it — the `ui:` section, the `owliver:` block — * is written by `boardPatch` and `owliverPatch`, which is the same writer the * editors use on an existing file. A template that composed its own YAML was a * second writer with a second field set, and the fields one knew about were not * the fields the other did. */ const skeleton = (fields, fallback, body) => `--- ${frontMatter({ ...fields, fallback })} --- ${body} `; /** A definition that draws a section on the pages it names. */ export const uiSkillTemplate = ({ id = '', name = '', description = '', pages = [], /** * `candidates.activity`, not `position.activity`. * * The default placement on the default page is `after-position-list-summary` * — above the grid, with no position in context — so the old default composed * a definition that drew a titled card and then reported that it needed a * record the page never had. A template must produce something that works * before it is edited, so the default reads a source that needs nothing. */ type = 'flow', placement = '', title = '', source = 'candidates.activity', periods = [], } = {}) => { const identity = { id, name, description, pages }; const fallback = { id: 'my-ui-skill', name: 'My UI Skill', description: 'What this skill adds to the page.', }; const heading = name || fallback.name; return patchFrontmatter( skeleton(identity, fallback, `# ${heading} ## Purpose Describe what this section shows, and why it belongs on these pages. ## Capabilities - Describe one thing the section reports. - Add more as needed.`), boardPatch({ ...identity, type, placement, /* A card with no title of its own is headed by the skill's name, which is what the previous template wrote out. Kept, so a fresh draft reads the same as it always did. */ title: title || heading, source, periods, }) ); }; /** A definition that teaches Owliver what it can be asked for. */ export const owliverSkillTemplate = ({ id = '', name = '', description = '', pages = [], triggers = [], suggestions = [], capabilities = [], responses = {}, } = {}) => { const identity = { id, name, description, pages }; const fallback = { id: 'my-owliver-skill', name: 'My Owliver Skill', description: 'What this skill helps Owliver answer.', }; const label = name || fallback.name; return patchFrontmatter( skeleton(identity, fallback, `# ${label} ## Purpose Describe what Owliver should be able to answer on these pages. ## Capabilities - Describe one thing Owliver can be asked for. - Add more as needed.`), owliverPatch({ ...identity, /* A definition that claims no phrase of its own still answers to its name — written out so the author can see what it will match on. */ triggers: triggers.length ? triggers : [label.toLowerCase()], suggestions, capabilities, responses, }) ); }; /** * The template the Add Skill dialog offers. * * That dialog opens from Owliver's own header, mid-conversation, so what it * hands the author is an Owliver skill. Kept under its original name because * it is what the dialog already imports. */ export const skillTemplate = owliverSkillTemplate; /** Parses a stored entry, tolerating the bare-string form. */ const sourceOf = (entry) => (typeof entry === 'string' ? entry : entry?.raw ?? ''); /** * The stored list with `source` added or replaced. * * Matching is by skill id, so editing a skill overwrites its own entry rather * than adding a near-duplicate beside it. */ export function upsertCustomSkill(existing = [], source) { const skill = parseSkill(source, { custom: true }); const rest = existing.filter((entry) => { try { return parseSkill(sourceOf(entry), { custom: true }).id !== skill.id; } catch { return true; } }); return { skill, next: [...rest, { path: `custom/${skill.id}.md`, raw: source }] }; } /** The stored list without the skill of this id. */ export function removeCustomSkill(existing = [], id) { return existing.filter((entry) => { try { return parseSkill(sourceOf(entry), { custom: true }).id !== id; } catch { return true; } }); } /** The stored Markdown for one custom skill, or null if the account has none. */ export function customSkillSource(existing = [], id) { for (const entry of existing) { try { if (parseSkill(sourceOf(entry), { custom: true }).id === id) return sourceOf(entry); } catch { /* An unparseable entry cannot be the one being edited. */ } } return null; }