vocs 2.5.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/internal/llms.d.ts +5 -0
  6. package/dist/internal/llms.d.ts.map +1 -1
  7. package/dist/internal/llms.js +22 -2
  8. package/dist/internal/llms.js.map +1 -1
  9. package/dist/internal/mcp.d.ts.map +1 -1
  10. package/dist/internal/mcp.js +49 -0
  11. package/dist/internal/mcp.js.map +1 -1
  12. package/dist/internal/mdx.d.ts +9 -1
  13. package/dist/internal/mdx.d.ts.map +1 -1
  14. package/dist/internal/mdx.js +85 -1
  15. package/dist/internal/mdx.js.map +1 -1
  16. package/dist/internal/openapi/openapi.d.ts +3 -3
  17. package/dist/internal/openapi/sidebar.d.ts +2 -2
  18. package/dist/internal/openapi/sidebar.d.ts.map +1 -1
  19. package/dist/internal/openapi/sidebar.js +5 -6
  20. package/dist/internal/openapi/sidebar.js.map +1 -1
  21. package/dist/internal/search.d.ts.map +1 -1
  22. package/dist/internal/search.js +6 -2
  23. package/dist/internal/search.js.map +1 -1
  24. package/dist/internal/vite-plugins.d.ts.map +1 -1
  25. package/dist/internal/vite-plugins.js +1 -0
  26. package/dist/internal/vite-plugins.js.map +1 -1
  27. package/dist/mdx.d.ts.map +1 -1
  28. package/dist/mdx.js +4 -1
  29. package/dist/mdx.js.map +1 -1
  30. package/dist/react/Prompt.d.ts +8 -0
  31. package/dist/react/Prompt.d.ts.map +1 -0
  32. package/dist/react/Prompt.js +16 -0
  33. package/dist/react/Prompt.js.map +1 -0
  34. package/dist/react/internal/Prompt.client.d.ts +10 -0
  35. package/dist/react/internal/Prompt.client.d.ts.map +1 -0
  36. package/dist/react/internal/Prompt.client.js +50 -0
  37. package/dist/react/internal/Prompt.client.js.map +1 -0
  38. package/dist/react/internal/prompt.d.ts +9 -0
  39. package/dist/react/internal/prompt.d.ts.map +1 -0
  40. package/dist/react/internal/prompt.js +63 -0
  41. package/dist/react/internal/prompt.js.map +1 -0
  42. package/dist/server/openapi/assets.generated.js +3 -3
  43. package/dist/server/openapi/assets.generated.js.map +1 -1
  44. package/dist/styles/markdown.css +132 -0
  45. package/dist/vite.d.ts.map +1 -1
  46. package/dist/vite.js +2 -0
  47. package/dist/vite.js.map +1 -1
  48. package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
  49. package/dist/waku/internal/middleware/md-router.js +1 -0
  50. package/dist/waku/internal/middleware/md-router.js.map +1 -1
  51. package/package.json +2 -1
  52. package/src/index.ts +1 -0
  53. package/src/internal/llms.test.ts +43 -1
  54. package/src/internal/llms.ts +31 -3
  55. package/src/internal/mcp.test.ts +75 -1
  56. package/src/internal/mcp.ts +54 -0
  57. package/src/internal/mdx.test.ts +55 -2
  58. package/src/internal/mdx.ts +102 -2
  59. package/src/internal/openapi/openapi.ts +3 -3
  60. package/src/internal/openapi/sidebar.test.ts +3 -5
  61. package/src/internal/openapi/sidebar.ts +7 -8
  62. package/src/internal/search.test.ts +38 -0
  63. package/src/internal/search.ts +12 -3
  64. package/src/internal/vite-plugins.ts +1 -0
  65. package/src/mdx.tsx +9 -1
  66. package/src/react/Prompt.test.tsx +27 -0
  67. package/src/react/Prompt.tsx +47 -0
  68. package/src/react/internal/Prompt.client.test.tsx +115 -0
  69. package/src/react/internal/Prompt.client.tsx +160 -0
  70. package/src/react/internal/prompt.test.ts +45 -0
  71. package/src/react/internal/prompt.ts +78 -0
  72. package/src/server/openapi/assets.generated.ts +3 -3
  73. package/src/styles/markdown.css +132 -0
  74. package/src/vite.ts +2 -0
  75. package/src/waku/internal/middleware/md-router.ts +1 -0
@@ -0,0 +1,160 @@
1
+ 'use client'
2
+
3
+ import { Tooltip } from '@base-ui/react/tooltip'
4
+ import * as React from 'react'
5
+ import LucideScanText from '~icons/lucide/scan-text'
6
+ import RiSparkling2Fill from '~icons/ri/sparkling-2-fill'
7
+ import SimpleIconsClaude from '~icons/simple-icons/claude'
8
+ import SimpleIconsOpenai from '~icons/simple-icons/openai'
9
+
10
+ type State = 'copied' | 'error' | 'idle'
11
+
12
+ export function PromptFrame(props: PromptFrame.Props) {
13
+ const { children, className, value } = props
14
+ const contentId = React.useId()
15
+ const encodedValue = encodeURIComponent(value)
16
+ const [expanded, setExpanded] = React.useState(false)
17
+ const [state, setState] = React.useState<State>('idle')
18
+
19
+ React.useEffect(() => {
20
+ if (state !== 'copied' && state !== 'error') return
21
+ const timeout = setTimeout(() => setState('idle'), 2_000)
22
+ return () => clearTimeout(timeout)
23
+ }, [state])
24
+
25
+ const copy = React.useCallback(() => {
26
+ try {
27
+ navigator.clipboard.writeText(value).then(
28
+ () => setState('copied'),
29
+ () => setState('error'),
30
+ )
31
+ } catch {
32
+ setState('error')
33
+ }
34
+ }, [value])
35
+
36
+ const copyFromFrame = React.useCallback(
37
+ (event: React.MouseEvent<HTMLElement>) => {
38
+ if (event.defaultPrevented) return
39
+ if (event.target instanceof Element && event.target.closest('a, button')) return
40
+ if (window.getSelection()?.isCollapsed === false) return
41
+ copy()
42
+ },
43
+ [copy],
44
+ )
45
+
46
+ const label = {
47
+ copied: 'Copied instructions',
48
+ error: 'Copy failed',
49
+ idle: 'Copy instructions for agent',
50
+ }[state]
51
+
52
+ return (
53
+ // biome-ignore lint/a11y/useKeyWithClickEvents: _
54
+ <figure
55
+ className={className}
56
+ data-expanded={expanded}
57
+ data-state={state}
58
+ data-v-prompt
59
+ onClick={copyFromFrame}
60
+ >
61
+ <figcaption data-v-prompt-header>
62
+ <button
63
+ aria-label={label}
64
+ data-state={state}
65
+ data-v-prompt-copy
66
+ onClick={copy}
67
+ type="button"
68
+ >
69
+ <RiSparkling2Fill aria-hidden data-v-prompt-icon />
70
+ <span aria-live="polite">{label}</span>
71
+ </button>
72
+ <Tooltip.Provider delay={300}>
73
+ <span data-v-prompt-actions>
74
+ <PromptAction
75
+ label="Open in ChatGPT"
76
+ render={
77
+ // biome-ignore lint/a11y/useAnchorContent: _
78
+ <a
79
+ aria-label="Open in ChatGPT"
80
+ href={`https://chatgpt.com?hints=search&q=${encodedValue}`}
81
+ rel="noopener noreferrer"
82
+ target="_blank"
83
+ />
84
+ }
85
+ >
86
+ <SimpleIconsOpenai aria-hidden />
87
+ </PromptAction>
88
+ <PromptAction
89
+ label="Open in Claude"
90
+ render={
91
+ // biome-ignore lint/a11y/useAnchorContent: _
92
+ <a
93
+ aria-label="Open in Claude"
94
+ href={`https://claude.ai/new?q=${encodedValue}`}
95
+ rel="noopener noreferrer"
96
+ target="_blank"
97
+ />
98
+ }
99
+ >
100
+ <SimpleIconsClaude aria-hidden />
101
+ </PromptAction>
102
+ <PromptAction
103
+ label={expanded ? 'Hide Prompt' : 'View Prompt'}
104
+ render={
105
+ <button
106
+ aria-controls={contentId}
107
+ aria-expanded={expanded}
108
+ aria-label={expanded ? 'Hide Prompt' : 'View Prompt'}
109
+ data-v-prompt-toggle
110
+ onClick={() => setExpanded((expanded) => !expanded)}
111
+ type="button"
112
+ />
113
+ }
114
+ >
115
+ <LucideScanText aria-hidden />
116
+ </PromptAction>
117
+ </span>
118
+ </Tooltip.Provider>
119
+ </figcaption>
120
+ <div data-v-prompt-content hidden={!expanded} id={contentId}>
121
+ <pre data-v-prompt-body>
122
+ <code>{children}</code>
123
+ </pre>
124
+ </div>
125
+ </figure>
126
+ )
127
+ }
128
+
129
+ export declare namespace PromptFrame {
130
+ type Props = {
131
+ children: React.ReactNode
132
+ className?: string | undefined
133
+ value: string
134
+ }
135
+ }
136
+
137
+ // biome-ignore lint/correctness/noUnusedVariables: _
138
+ function PromptAction(props: PromptAction.Props) {
139
+ const { children, label, render } = props
140
+ return (
141
+ <Tooltip.Root>
142
+ <Tooltip.Trigger data-v-prompt-action render={render}>
143
+ {children}
144
+ </Tooltip.Trigger>
145
+ <Tooltip.Portal>
146
+ <Tooltip.Positioner className="vocs:z-50" side="top" sideOffset={6}>
147
+ <Tooltip.Popup data-v-prompt-tooltip>{label}</Tooltip.Popup>
148
+ </Tooltip.Positioner>
149
+ </Tooltip.Portal>
150
+ </Tooltip.Root>
151
+ )
152
+ }
153
+
154
+ declare namespace PromptAction {
155
+ type Props = {
156
+ children: React.ReactNode
157
+ label: string
158
+ render: React.ReactElement
159
+ }
160
+ }
@@ -0,0 +1,45 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { tokenize } from './prompt.js'
3
+
4
+ describe('tokenize', () => {
5
+ it('detects prompt structure without changing its text', () => {
6
+ const value = [
7
+ 'Read https://vocs.dev/introduction/getting-started and update `vocs.config.ts`.',
8
+ '',
9
+ 'Requirements:',
10
+ '- Preserve existing navigation',
11
+ '- Replace <PROJECT_NAME> with {{package}} and run $COMMAND.',
12
+ ].join('\n')
13
+ const tokens = tokenize(value)
14
+
15
+ expect(tokens.map((token) => token.value).join('')).toBe(value)
16
+ expect(tokens.filter((token) => token.type !== 'text')).toEqual([
17
+ { type: 'url', value: 'https://vocs.dev/introduction/getting-started' },
18
+ { type: 'code', value: '`vocs.config.ts`' },
19
+ { type: 'label', value: 'Requirements:' },
20
+ { type: 'marker', value: '-' },
21
+ { type: 'marker', value: '-' },
22
+ { type: 'placeholder', value: '<PROJECT_NAME>' },
23
+ { type: 'placeholder', value: '{{package}}' },
24
+ { type: 'placeholder', value: '$COMMAND' },
25
+ ])
26
+ })
27
+
28
+ it('keeps trailing URL punctuation outside the link', () => {
29
+ const value = 'Read (https://vocs.dev/docs), then continue.'
30
+ const tokens = tokenize(value)
31
+
32
+ expect(tokens.map((token) => token.value).join('')).toBe(value)
33
+ expect(tokens.find((token) => token.type === 'url')).toEqual({
34
+ type: 'url',
35
+ value: 'https://vocs.dev/docs',
36
+ })
37
+ })
38
+
39
+ it('does not tokenize prompt syntax inside inline code', () => {
40
+ const value = 'Keep `https://example.com/<PROJECT>` literal.'
41
+ expect(tokenize(value).filter((token) => token.type !== 'text')).toEqual([
42
+ { type: 'code', value: '`https://example.com/<PROJECT>`' },
43
+ ])
44
+ })
45
+ })
@@ -0,0 +1,78 @@
1
+ export type Token = {
2
+ type: 'code' | 'label' | 'marker' | 'placeholder' | 'text' | 'url'
3
+ value: string
4
+ }
5
+
6
+ const labelRegex = /^(\s*)([A-Z][A-Za-z0-9 /_-]{0,31}:)(?=\s|$)/
7
+ const listMarkerRegex = /^(\s*)([-*+]|\d+[.)])(?=\s)/
8
+ const trailingUrlPunctuationRegex = /[\]})>,.;:!?]+$/
9
+
10
+ export function tokenize(value: string): tokenize.ReturnType {
11
+ const tokens: Token[] = []
12
+ const lines = value.split('\n')
13
+
14
+ for (const [index, line] of lines.entries()) {
15
+ tokenizeLine(line, tokens)
16
+ if (index < lines.length - 1) push(tokens, 'text', '\n')
17
+ }
18
+
19
+ return tokens
20
+ }
21
+
22
+ export declare namespace tokenize {
23
+ type ReturnType = Token[]
24
+ }
25
+
26
+ function tokenizeLine(line: string, tokens: Token[]) {
27
+ const listMarker = line.match(listMarkerRegex)
28
+ if (listMarker) {
29
+ push(tokens, 'text', listMarker[1] ?? '')
30
+ push(tokens, 'marker', listMarker[2] ?? '')
31
+ tokenizeInline(line.slice(listMarker[0].length), tokens)
32
+ return
33
+ }
34
+
35
+ const label = line.match(labelRegex)
36
+ if (label) {
37
+ push(tokens, 'text', label[1] ?? '')
38
+ push(tokens, 'label', label[2] ?? '')
39
+ tokenizeInline(line.slice(label[0].length), tokens)
40
+ return
41
+ }
42
+
43
+ tokenizeInline(line, tokens)
44
+ }
45
+
46
+ function tokenizeInline(value: string, tokens: Token[]) {
47
+ const tokenRegex =
48
+ /`[^`\n]+`|https?:\/\/[^\s`]+|\{\{[^{}\n]+\}\}|\$\{[^{}\n]+\}|<[A-Z][A-Z0-9_-]*>|\$[A-Z][A-Z0-9_]*/g
49
+ let cursor = 0
50
+
51
+ for (const match of value.matchAll(tokenRegex)) {
52
+ const index = match.index
53
+ const raw = match[0]
54
+ push(tokens, 'text', value.slice(cursor, index))
55
+
56
+ if (raw.startsWith('`')) push(tokens, 'code', raw)
57
+ else if (raw.startsWith('http')) pushUrl(tokens, raw)
58
+ else push(tokens, 'placeholder', raw)
59
+
60
+ cursor = index + raw.length
61
+ }
62
+
63
+ push(tokens, 'text', value.slice(cursor))
64
+ }
65
+
66
+ function pushUrl(tokens: Token[], value: string) {
67
+ const trailing = value.match(trailingUrlPunctuationRegex)?.[0] ?? ''
68
+ const url = trailing ? value.slice(0, -trailing.length) : value
69
+ push(tokens, 'url', url)
70
+ push(tokens, 'text', trailing)
71
+ }
72
+
73
+ function push(tokens: Token[], type: Token['type'], value: string) {
74
+ if (!value) return
75
+ const previous = tokens.at(-1)
76
+ if (type === 'text' && previous?.type === 'text') previous.value += value
77
+ else tokens.push({ type, value })
78
+ }