hermes-agent/apps/shared/src/skill-scaffold.ts
Brooklyn Nicholson e682a9c0a7 refactor: drop the shared subpath export, keeping package.json untouched
The subpath entry existed only so the TUI could import the client-side
projection fallback. The TUI spawns its gateway from this same checkout and
cannot version-skew with it, so that fallback was dead weight — it reads
`display` directly now. With its last caller gone the dispatch helper folds
back into the desktop, where an older backend is genuinely reachable.

apps/shared/package.json is byte-identical to main again.
2026-07-28 03:54:49 -05:00

69 lines
2.7 KiB
TypeScript

/**
* A `/skill` invocation expands into a model-facing message that embeds the
* whole skill body. That payload is for the agent — the UI shows the
* invocation the user typed (`/work fix the leak`) and nothing else.
*
* The gateway already projects this (see `_skill_scaffold_projection` in
* tui_gateway/server.py) and ships the result as `display` on a dispatch and
* as the `text` of a `skill_invocation` history row. This module is the
* client-side twin so a desktop/TUI talking to an older gateway — or any
* future path that hands raw scaffolding to a bubble — still renders the
* invocation instead of the body.
*
* The markers below mirror `agent/skill_commands.py` byte for byte.
*/
const INVOCATION_PREFIX = '[IMPORTANT: The user has invoked the '
const SINGLE_MARKER = 'The full skill content is loaded below.]'
const SINGLE_INSTRUCTION = 'The user has provided the following instruction alongside the skill invocation: '
const RUNTIME_NOTE = '\n\n[Runtime note:'
const BUNDLE_MARKER = ' skill bundle,'
const BUNDLE_INSTRUCTION = '\nUser instruction: '
const BUNDLE_SKILL_BLOCK = '\n\n[Loaded as part of the '
// The skill name is the first quoted span of the activation note, for both the
// single-skill (`work`) and the bundle (`/clean /work`) header.
const NAME_RE = new RegExp(`^${INVOCATION_PREFIX.replace(/[[\]]/g, '\\$&')}"([^"]*)"`)
/** Text between `marker` and `end`, or '' when the marker is absent. */
function between(text: string, marker: string, end: string, fromEnd = false): string {
const index = fromEnd ? text.lastIndexOf(marker) : text.indexOf(marker)
if (index < 0) {
return ''
}
const tail = text.slice(index + marker.length)
const stop = tail.indexOf(end)
return (stop >= 0 ? tail.slice(0, stop) : tail).trim()
}
/**
* The invocation a scaffolded turn came from (`/work fix the leak`), or null
* when `text` is ordinary user prose that should render as written.
*/
export function skillInvocationText(text: string): null | string {
if (!text.startsWith(INVOCATION_PREFIX)) {
return null
}
const name = (NAME_RE.exec(text)?.[1] ?? '').trim()
if (!name) {
return null
}
// Bundle headers already carry their typed "/a /b" keys; a single skill is
// a bare name. The single-skill instruction trails the body (which may quote
// the marker), so match it from the end.
const label = name.startsWith('/') ? name : `/${name}`
const instruction = text.includes(BUNDLE_MARKER)
? between(text, BUNDLE_INSTRUCTION, BUNDLE_SKILL_BLOCK)
: text.includes(SINGLE_MARKER)
? between(text, SINGLE_INSTRUCTION, RUNTIME_NOTE, true)
: ''
return instruction ? `${label} ${instruction.replace(/\s+/g, ' ')}` : label
}