Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.5 kB · 193 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194/** * Claude Code hints protocol. * * CLIs and SDKs running under Claude Code can emit a self-closing * `<claude-code-hint />` tag to stderr (merged into stdout by the shell * tools). The harness scans tool output for these tags, strips them before * the output reaches the model, and surfaces an install prompt to the * user — no inference, no proactive execution. * * This file provides both the parser and a small module-level store for * the pending hint. The store is a single slot (not a queue) — we surface * at most one prompt per session, so there's no reason to accumulate. * React subscribes via useSyncExternalStore. * * See docs/claude-code-hints.md for the vendor-facing spec. */
import { logForDebugging } from './debug.js'import { createSignal } from './signal.js'
export type ClaudeCodeHintType = 'plugin'
export type ClaudeCodeHint = { /** Spec version declared by the emitter. Unknown versions are dropped. */ v: number /** Hint discriminator. v1 defines only `plugin`. */ type: ClaudeCodeHintType /** * Hint payload. For `type: 'plugin'`: a `name@marketplace` slug * matching the form accepted by `parsePluginIdentifier`. */ value: string /** * First token of the shell command that produced this hint. Shown in the * install prompt so the user can spot a mismatch between the tool that * emitted the hint and the plugin it recommends. */ sourceCommand: string}
/** Spec versions this harness understands. */const SUPPORTED_VERSIONS = new Set([1])
/** Hint types this harness understands at the supported versions. */const SUPPORTED_TYPES = new Set<string>(['plugin'])
/** * Outer tag match. Anchored to whole lines (multiline mode) so that a * hint marker buried in a larger line — e.g. a log statement quoting the * tag — is ignored. Leading and trailing whitespace on the line is * tolerated since some SDKs pad stderr. */const HINT_TAG_RE = /^[ \t]*<claude-code-hint\s+([^>]*?)\s*\/>[ \t]*$/gm
/** * Attribute matcher. Accepts `key="value"` and `key=value` (terminated by * whitespace or `/>` closing sequence). Values containing whitespace or `"` must use the quoted * form. The quoted form does not support escape sequences; raise the spec * version if that becomes necessary. */const ATTR_RE = /(\w+)=(?:"([^"]*)"|([^\s/>]+))/g
/** * Scan shell tool output for hint tags, returning the parsed hints and * the output with hint lines removed. The stripped output is what the * model sees — hints are a harness-only side channel. * * @param output - Raw command output (stdout with stderr interleaved). * @param command - The command that produced the output; its first * whitespace-separated token is recorded as `sourceCommand`. */export function extractClaudeCodeHints( output: string, command: string,): { hints: ClaudeCodeHint[]; stripped: string } { // Fast path: no tag open sequence → no work, no allocation. if (!output.includes('<claude-code-hint')) { return { hints: [], stripped: output } }
const sourceCommand = firstCommandToken(command) const hints: ClaudeCodeHint[] = []
const stripped = output.replace(HINT_TAG_RE, rawLine => { const attrs = parseAttrs(rawLine) const v = Number(attrs.v) const type = attrs.type const value = attrs.value
if (!SUPPORTED_VERSIONS.has(v)) { logForDebugging( `[claudeCodeHints] dropped hint with unsupported v=${attrs.v}`, ) return '' } if (!type || !SUPPORTED_TYPES.has(type)) { logForDebugging( `[claudeCodeHints] dropped hint with unsupported type=${type}`, ) return '' } if (!value) { logForDebugging('[claudeCodeHints] dropped hint with empty value') return '' }
hints.push({ v, type: type as ClaudeCodeHintType, value, sourceCommand }) return '' })
// Dropping a matched line leaves a blank line (the surrounding newlines // remain). Collapse runs of blank lines introduced by the replace so the // model-visible output doesn't grow vertical whitespace. const collapsed = hints.length > 0 || stripped !== output ? stripped.replace(/\n{3,}/g, '\n\n') : stripped
return { hints, stripped: collapsed }}
function parseAttrs(tagBody: string): Record<string, string> { const attrs: Record<string, string> = {} for (const m of tagBody.matchAll(ATTR_RE)) { attrs[m[1]!] = m[2] ?? m[3] ?? '' } return attrs}
function firstCommandToken(command: string): string { const trimmed = command.trim() const spaceIdx = trimmed.search(/\s/) return spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx)}
// ============================================================================// Pending-hint store (useSyncExternalStore interface)//// Single-slot: write wins if the slot is already full (a CLI that emits on// every invocation would otherwise pile up). The dialog is shown at most// once per session; after that, setPendingHint becomes a no-op.//// Callers should gate before writing (installed? already shown? cap hit?) —// see maybeRecordPluginHint in hintRecommendation.ts for the plugin-type// gate. This module stays plugin-agnostic so future hint types can reuse// the same store.// ============================================================================
let pendingHint: ClaudeCodeHint | null = nulllet shownThisSession = falseconst pendingHintChanged = createSignal()const notify = pendingHintChanged.emit
/** Raw store write. Callers should gate first (see module comment). */export function setPendingHint(hint: ClaudeCodeHint): void { if (shownThisSession) return pendingHint = hint notify()}
/** Clear the slot without flipping the session flag — for rejected hints. */export function clearPendingHint(): void { if (pendingHint !== null) { pendingHint = null notify() }}
/** Flip the once-per-session flag. Call only when a dialog is actually shown. */export function markShownThisSession(): void { shownThisSession = true}
export const subscribeToPendingHint = pendingHintChanged.subscribe
export function getPendingHintSnapshot(): ClaudeCodeHint | null { return pendingHint}
export function hasShownHintThisSession(): boolean { return shownThisSession}
/** Test-only reset. */export function _resetClaudeCodeHintStore(): void { pendingHint = null shownThisSession = false}
export const _test = { parseAttrs, firstCommandToken,}