rift-ds 1.6.0 → 1.8.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 (133) hide show
  1. package/README.md +34 -13
  2. package/bin/rift-ds.mjs +231 -44
  3. package/bin/templates/chat/.env.chat.example +21 -0
  4. package/bin/templates/chat/app/api/chat/feedback/route.ts +12 -0
  5. package/bin/templates/chat/app/api/chat/followups/route.ts +23 -0
  6. package/bin/templates/chat/app/api/chat/guardrails.ts +18 -0
  7. package/bin/templates/chat/app/api/chat/route.ts +31 -0
  8. package/bin/templates/chat/components/chat/ChatMount.tsx +52 -0
  9. package/bin/templates/chat/components/chat/models.ts +29 -0
  10. package/chat/AssistantTurn.d.ts +5 -0
  11. package/chat/AssistantTurn.js +82 -0
  12. package/chat/ChatAgentPane.css +56 -0
  13. package/chat/ChatAgentPane.d.ts +21 -0
  14. package/chat/ChatAgentPane.js +25 -0
  15. package/chat/ChatLauncher.d.ts +64 -0
  16. package/chat/ChatLauncher.js +347 -0
  17. package/chat/ChatProvider.d.ts +53 -0
  18. package/chat/ChatProvider.js +181 -0
  19. package/chat/ChatTabs.d.ts +13 -0
  20. package/chat/ChatTabs.js +24 -0
  21. package/chat/ChatThreads.d.ts +26 -0
  22. package/chat/ChatThreads.js +55 -0
  23. package/chat/ChatWidget.css +1072 -0
  24. package/chat/ChatWidget.d.ts +113 -0
  25. package/chat/ChatWidget.js +495 -0
  26. package/chat/DemoWelcome.d.ts +13 -0
  27. package/chat/DemoWelcome.js +131 -0
  28. package/chat/FollowupSuggestions.d.ts +23 -0
  29. package/chat/FollowupSuggestions.js +34 -0
  30. package/chat/ResponseActions.d.ts +18 -0
  31. package/chat/ResponseActions.js +79 -0
  32. package/chat/TurnContent.d.ts +9 -0
  33. package/chat/TurnContent.js +12 -0
  34. package/chat/context.d.ts +65 -0
  35. package/chat/context.js +27 -0
  36. package/chat/demo-agent-panes.d.ts +5 -0
  37. package/chat/demo-agent-panes.js +49 -0
  38. package/chat/demo-agent.d.ts +3 -0
  39. package/chat/demo-agent.js +28 -0
  40. package/chat/demo-card-components.d.ts +46 -0
  41. package/chat/demo-card-components.js +59 -0
  42. package/chat/demo-cards.d.ts +32 -0
  43. package/chat/demo-cards.js +59 -0
  44. package/chat/demo-fixtures.d.ts +20 -0
  45. package/chat/demo-fixtures.js +113 -0
  46. package/chat/demo-script.d.ts +6 -0
  47. package/chat/demo-script.js +215 -0
  48. package/chat/demo.css +46 -0
  49. package/chat/demo.d.ts +9 -0
  50. package/chat/demo.js +24 -0
  51. package/chat/followups.d.ts +41 -0
  52. package/chat/followups.js +55 -0
  53. package/chat/greeting.d.ts +11 -0
  54. package/chat/greeting.js +21 -0
  55. package/chat/index.d.ts +34 -0
  56. package/chat/index.js +80 -0
  57. package/chat/placement.d.ts +48 -0
  58. package/chat/placement.js +39 -0
  59. package/chat/queries.d.ts +9 -0
  60. package/chat/queries.js +22 -0
  61. package/chat/registry.d.ts +25 -0
  62. package/chat/registry.js +9 -0
  63. package/chat/registry.json.d.ts +69 -0
  64. package/chat/registry.json.js +11 -0
  65. package/chat/server.d.ts +217 -0
  66. package/chat/server.js +488 -0
  67. package/chat/sim.d.ts +71 -0
  68. package/chat/sim.js +125 -0
  69. package/chat/starters.d.ts +15 -0
  70. package/chat/starters.js +38 -0
  71. package/chat/suggestions.d.ts +22 -0
  72. package/chat/suggestions.js +9 -0
  73. package/chat/threads.d.ts +73 -0
  74. package/chat/threads.js +270 -0
  75. package/chat/transport.d.ts +49 -0
  76. package/chat/transport.js +105 -0
  77. package/chat/types.d.ts +210 -0
  78. package/chat/types.js +16 -0
  79. package/chat/upstash.d.ts +63 -0
  80. package/chat/upstash.js +147 -0
  81. package/chat/useChat.d.ts +64 -0
  82. package/chat/useChat.js +241 -0
  83. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +3 -0
  84. package/cli/templates/chat/app/api/chat/followups/route.d.ts +3 -0
  85. package/cli/templates/chat/app/api/chat/guardrails.d.ts +10 -0
  86. package/cli/templates/chat/app/api/chat/route.d.ts +4 -0
  87. package/cli/templates/chat/components/chat/ChatMount.d.ts +1 -0
  88. package/cli/templates/chat/components/chat/models.d.ts +11 -0
  89. package/components/AgentRail/AgentRail.css +2 -2
  90. package/components/Attachment/Attachment.css +14 -0
  91. package/components/Attachment/AttachmentGroup.d.ts +2 -2
  92. package/components/Attachment/AttachmentGroup.js +7 -2
  93. package/components/Composer/Composer.css +88 -4
  94. package/components/Composer/Composer.d.ts +21 -0
  95. package/components/Composer/Composer.js +199 -14
  96. package/components/Mention/Mention.css +195 -0
  97. package/components/Mention/Mention.d.ts +68 -0
  98. package/components/Mention/Mention.js +145 -0
  99. package/components/Mention/mentions.d.ts +81 -0
  100. package/components/Mention/mentions.js +83 -0
  101. package/components/ShaderField/ShaderField.d.ts +6 -5
  102. package/components/ShaderField/useShaderField.d.ts +2 -1
  103. package/components/ShaderField/useShaderField.js +6 -6
  104. package/components/StatusDot/StatusDot.css +36 -13
  105. package/components/StatusDot/StatusDot.d.ts +16 -4
  106. package/components/StatusDot/StatusDot.js +25 -5
  107. package/components/ThreadPanel/ThreadPanel.css +20 -11
  108. package/components/ThreadPanel/ThreadPanel.d.ts +15 -3
  109. package/components/ThreadPanel/ThreadPanel.js +30 -6
  110. package/components/ThreadTabs/ThreadTabs.css +18 -8
  111. package/components/ThreadTabs/ThreadTabs.d.ts +8 -2
  112. package/components/ThreadTabs/ThreadTabs.js +24 -5
  113. package/components/registry.json +9 -1
  114. package/components/registry.json.d.ts +9 -1
  115. package/components/registry.json.js +1 -1
  116. package/fonts/material-symbols.css +2 -1
  117. package/index.d.ts +1 -0
  118. package/index.js +11 -0
  119. package/package.json +43 -2
  120. package/tokens/presets/coral.css +18 -2
  121. package/tokens/presets/forest.css +18 -2
  122. package/tokens/presets/gold.css +18 -2
  123. package/tokens/presets/mono.css +18 -2
  124. package/tokens/presets/pink.css +18 -2
  125. package/tokens/presets/terminal.css +18 -2
  126. package/tokens/presets/violet.css +18 -2
  127. package/tokens/presets/warm.css +18 -2
  128. package/tokens/presets/zest.css +18 -2
  129. package/tokens/registry.json +6 -0
  130. package/tokens/registry.json.d.ts +6 -0
  131. package/tokens/registry.json.js +1 -1
  132. package/tokens/tokens-dark.css +10 -0
  133. package/tokens/tokens-light.css +10 -0
package/README.md CHANGED
@@ -9,17 +9,15 @@
9
9
  <!-- npm-badge:end -->
10
10
  [![license: MIT](https://img.shields.io/badge/license-MIT-4c1)](LICENSE)
11
11
 
12
- An open source React design system built for AI products and coding agents: components on a three-tier token architecture, complete theme presets that restyle everything with one attribute, and machine surfaces (an MCP endpoint, per-component contracts, an installable agent skill) so your coding agent knows the library as well as you do.
12
+ An open source React design system built for AI products and coding agents: components on a three-tier token architecture, complete theme presets that restyle everything with one attribute, and machine surfaces (an MCP endpoint, per-component contracts, installable agent skills) so your coding agent knows the library as well as you do.
13
13
 
14
14
  ## Why Rift DS
15
15
 
16
- - **One package, zero dependencies.** React is the only required peer dependency; recharts is optional and only for the charts entry. No configuration API, no providers, no build-tool integration: theming is plain CSS custom properties, so it works in any bundler, in any stack, beside anything you already use.
17
- - **Free, open source, consumed your way.** MIT end to end, no pro tier, no paywalled components. Install the npm package, pull single components as source through the shadcn CLI, or clone the repo and own every line; all three are first-class paths.
18
- - **A few primitives re-theme everything.** Every semantic token chains to a primitive, so a handful of overrides cascade through every component, the charts, and the ambient background, in both colour modes. Your brand is a short CSS block, not a fork.
19
- - **The shipped themes prove it.** Each one is the same system under different primitives, saved as a complete look: colour modes, a type pairing with self-hosted fonts, type weight and tracking, icon weight, radius, density, motion, elevation, the chart palette. Setting `data-brand` on the root element applies all of it with zero runtime JavaScript, the build holds every theme's action colours to WCAG AA in both modes, and the playground builds yours the same way.
20
- - **The AI product set is in the box.** Composer, streaming chat thread, tool calls, reasoning, thread panel and tabs: the components an assistant UI needs, the same set this repo's own site chat is built from.
21
- - **Legible to coding agents.** One command installs the agent skill and prints the MCP connect line, and the MCP tools and per-component markdown are generated from the same JSDoc that builds the shipped `.d.ts`: an agent reads the exact contract npm ships, no key, no account.
22
- - **Docs that cannot lie.** Every count, list, and prop table is generated from source registries and build-validated; CI fails on drift, and every component story ships through an accessibility audit. What the docs say is what ships.
16
+ - **<!-- component-count -->150<!-- /component-count --> components.** Buttons, forms and tables through to charts, navigation and overlays, each typed and documented with live examples. React is the only required peer dependency.
17
+ - **AI patterns, the whole chat and templates, ready to use.** Composer, streaming chat thread, tool calls, reasoning and thread panel are in the box, the complete AI chat ships assembled as one widget that one command scaffolds, and the docs site has complete template screens built from them.
18
+ - **Fully themeable.** The shipped themes are complete looks to start from, each in light and dark. One `data-brand` attribute applies a theme, and a few CSS overrides make your own.
19
+ - **Ready for coding agents.** One command installs the agent skills, and a public MCP endpoint serves every component's props, so your agent builds with the library as it actually is. No key, no account.
20
+ - **Free, consumed your way.** MIT, with no pro tier. Install the npm package, pull single components as source through the shadcn CLI, or clone the repo.
23
21
 
24
22
  ## Documentation
25
23
 
@@ -38,11 +36,33 @@ import 'rift-ds/tokens/tokens.css';
38
36
  import { Button, Card, Badge } from 'rift-ds';
39
37
  ```
40
38
 
41
- React 19+ is a peer dependency. The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack) and set TypeScript's `moduleResolution` to `"bundler"` or `"nodenext"`. The Recharts-backed charts live behind `rift-ds/charts`, so the optional `recharts` peer dependency is only needed if you use them. Prefer owning the source? Pull single components through the shadcn CLI (`npx shadcn@latest add https://rift-ds.com/r/button.json`, with details in the [get-started guide](https://rift-ds.com/docs/get-started)), or clone this repo and build on it directly; all of it is MIT.
39
+ React 19+ is a peer dependency. The package is ESM-only and resolved via `exports` subpaths: use a bundler that handles CSS and font imports from `node_modules` (Vite, Next.js, webpack) and set TypeScript's `moduleResolution` to `"bundler"` or `"nodenext"`. The Recharts-backed charts live behind `rift-ds/charts`, so the optional `recharts` peer dependency is only needed if you use them. The chat widget's subpaths bring their own optional peers (`react-markdown` and `remark-gfm` for the panel, `@anthropic-ai/sdk` for the server handlers, `@upstash/ratelimit` and `@upstash/redis` for the guardrails), needed only if you use them. Prefer owning the source? Pull single components through the shadcn CLI (`npx shadcn@latest add https://rift-ds.com/r/button.json`, with details in the [get-started guide](https://rift-ds.com/docs/get-started)), or clone this repo and build on it directly; all of it is MIT.
40
+
41
+ ## The chat widget
42
+
43
+ The whole AI chat ships as one widget: the panel with its docked, overlay and takeover forms, the streaming engine, the threads, the rich cards, and the server handlers that call the model. One command scaffolds it into a Next.js app, and a scripted mode runs it with no key, so you see it working before you wire a model.
44
+
45
+ ```bash
46
+ npx rift-ds init chat
47
+ ```
48
+
49
+ The panel alone, with no route behind it, is three imports:
50
+
51
+ ```tsx
52
+ import 'rift-ds/chat/chat.css';
53
+ import { ChatProvider, ChatLauncher } from 'rift-ds/chat';
54
+ import { createDemoTransport, DEMO_RENDERERS } from 'rift-ds/chat/demo';
55
+
56
+ <ChatProvider transport={createDemoTransport()} renderers={DEMO_RENDERERS}>
57
+ <ChatLauncher label="Ask" />
58
+ </ChatProvider>
59
+ ```
60
+
61
+ Swap the scripted transport for `createFetchTransport('/api/chat')` and the panel talks to your route. Every prop it takes is on [its component page](https://rift-ds.com/components/ai-chat), and the three ways to add it, with the protocol and the server options, are on [the AI chat docs page](https://rift-ds.com/docs/chat).
42
62
 
43
63
  ## Set up your agent
44
64
 
45
- One command teaches a coding agent the system: it installs the generated agent skill into your project and prints the MCP connect line.
65
+ One command teaches a coding agent the system: it installs the generated agent skills into your project and prints the MCP connect line. The skills cover the library's rules, moving existing screens onto the tokens and components, and auditing styles against them.
46
66
 
47
67
  ```bash
48
68
  npx rift-ds init
@@ -54,7 +74,7 @@ The MCP endpoint serves the component catalogue, per-component prop APIs, the to
54
74
  claude mcp add --transport http rift-ds https://rift-ds.com/api/mcp
55
75
  ```
56
76
 
57
- Every component's prop contract is also plain markdown (append `.md` to its docs URL), and [llms.txt](https://rift-ds.com/llms.txt) indexes every machine surface. Then just ask: "Build a settings page with Rift components."
77
+ Every component's prop contract is also plain markdown (append `.md` to its docs URL), and [llms.txt](https://rift-ds.com/llms.txt) indexes every machine surface. A theme made in the [playground](https://rift-ds.com/playground) exports as CSS, a THEME.md and a setup prompt, so the agent can read the look as well as the code. Then just ask: "Build a settings page with Rift components."
58
78
 
59
79
  ## Theming
60
80
 
@@ -67,19 +87,20 @@ import 'rift-ds/tokens/presets/presets.css';
67
87
  ```
68
88
 
69
89
  - **Dark mode**: `data-theme="dark"` on the root element; light is the default.
90
+ - **Page colours**: the token stylesheet defines custom properties and styles no element, so paint the page once: set `background: var(--color-bg-page-primary)`, `color: var(--color-text-primary)` and `font-family: var(--font-family-primary)` on `body`. Without it a dark theme lands as dark components on a white page.
70
91
  - **Your own brand**: every semantic token chains to a primitive, so overriding one primitive re-themes everything built on it. The [playground](https://rift-ds.com/playground) restyles the system live and copies out a complete, paste-ready override.
71
92
  - **Fonts**: the base theme bundles no text face, and the whole scale chains to `--font-family-primary` (split heading and body faces via `--font-family-heading` and `--font-family-body`); point them at any font you load. Only the preset stylesheets carry faces, each self-hosting its own, and only the presets you import pull them in.
72
93
  - **Icons**: a Material Symbols Rounded variable font is bundled and components import it themselves, with every Google axis exposed as a custom property. Every icon prop also takes your own element, so any icon set drops in.
73
94
 
74
95
  ## Components
75
96
 
76
- <!-- component-count -->149<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
97
+ <!-- component-count -->150<!-- /component-count --> components, including a chat set for AI products (Chat thread, Composer, Tool call, Reasoning, Thread panel) and Shader field, a WebGL2 ambient background whose light sources read your colour tokens at runtime, so it re-themes with everything else. Details and live examples are on the [docs site](https://rift-ds.com/components).
77
98
 
78
99
  <details>
79
100
  <summary>The full list</summary>
80
101
 
81
102
  <!-- component-list:start -->
82
- Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
103
+ Accordion · Agent plan · Agent rail · Agent status · AI button · Alert · Alert dialog · Anchor nav · Animated number · App layout · App sidebar · Area chart · Attachment drop zone · Attachment group · Attachment tile · Attachment viewer · Avatar · Avatar group · Badge · Banner · Bar chart · Bottom nav · Breadcrumb · Button · Button group · Card · Card stack · Carousel · Chat header · Chat marker · Chat message · Chat thread · Checkbox · Chip · Circular button · Code block · Code diff · Colour picker · Combo chart · Combobox · Command palette · Composer · Contact card · Context menu · Contribution graph · Data table · Date input · Date picker · Dialog · Divider · Document chip · Drawer · Dropdown · Dropdown menu · Empty state · Entity card · Event calendar · Field · Figure · File input · Filter bar · Funnel chart · Gantt chart · Gauge · Globe · Hover card · Image compare · Input · Inspector colour picker · Inspector dropdown · Inspector input · Inspector section · Inspector segments · Inspector slider · Inspector toggle switch · Instructions · Interrupt card · Kbd · Legend tile · Lightbox · Line chart · Link list · Map callout · Map legend · Mention · Message actions · Message card · Meter · Model picker · Nav · Nav list · Notification centre · Number input · Pagination · Panel · Pie chart · Pin input · Pixel avatar · Popover · Progress bar · Prompt suggestions · Prose · Quote · Radar chart · Radial chart · Radio button · Rating · Reasoning · Rich dropdown · Scatter chart · Section title · Segmented control · Selection card · Shader field · Sidebar panel · Skeleton · Slider · Source chip · Source trail · Sparkline · Spinner · Split button · Split pane · Stacked bar chart · Stat · Status dot · Stepper · Streaming text · Swatch · Table · Tabs · Tag input · Textarea · Thread panel · Thread tabs · Time picker · Timeline · Toast · Toggle group · Toggle switch · Tool call · Toolbar · Tooltip · Top app bar · Tree view · Treemap · Usage card · Waveform · Widget · World map
83
104
  <!-- component-list:end -->
84
105
 
85
106
  </details>
package/bin/rift-ds.mjs CHANGED
@@ -1,70 +1,237 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * The package's one command: `npx rift-ds init`.
3
+ * The package's one bin: `npx rift-ds init` and `npx rift-ds init chat`.
4
4
  *
5
- * Fetches the generated agent skill (SKILL.md + references/components.md)
6
- * from the live site into the project's .claude/skills/ folder, then prints
7
- * the MCP connect line. The files are fetched rather than bundled because
8
- * they regenerate with every site deploy, while the package moves only on a
9
- * release — a bundled copy would always be the staler one. Re-running the
10
- * command overwrites the pair; that is the refresh story.
5
+ * `init` fetches the generated agent skills from the live site into the
6
+ * project's .claude/skills/ folder, one folder per skill, then prints the
7
+ * MCP connect line. Which skills exist and which files each holds comes
8
+ * from the site's /skill/manifest.json, never from a list in this file:
9
+ * the skills regenerate with every site deploy while the package moves
10
+ * only on a release, so a bundled list (or a bundled copy of the files)
11
+ * would always be the staler one. Re-running overwrites what is there;
12
+ * that is the refresh story.
13
+ *
14
+ * `init chat` scaffolds the chat widget into a Next.js app router project:
15
+ * the three route files under app/api/chat/, the guardrails module beside
16
+ * them, the mount component and its model list under components/chat/, and
17
+ * an .env.chat.example at the project root. These templates ride inside
18
+ * the package (dist/bin/templates/chat/, copied from src/cli/templates/chat/
19
+ * by build-package.mjs) rather than being fetched, because they import the
20
+ * installed version's own subpaths and must match it. It never overwrites:
21
+ * a file that exists is skipped and listed, so re-running is safe.
22
+ *
23
+ * The manifest is data from the network that decides where this command
24
+ * writes, so every name and path in it is checked before anything touches
25
+ * the disk: a skill name is one kebab-case folder, a file is a plain
26
+ * relative path, and nothing may climb out of the target folder.
11
27
  *
12
28
  * Zero dependencies by design: global fetch plus node:fs, so npx never
13
- * installs anything beyond the package itself. SITE_ORIGIN, SKILL_NAME
14
- * and MCP_SERVER_NAME are stamped by build-package.mjs from
29
+ * installs anything beyond the package itself. SITE_ORIGIN, SKILL_NAME,
30
+ * MCP_SERVER_NAME and PACKAGE_NAME are stamped by build-package.mjs from
15
31
  * scripts/brand.mjs (the one home for brand facts); running this file
16
32
  * straight from source fails fast rather than guessing a domain.
33
+ * SKILL_NAME is the library skill, kept only for --out without --skill,
34
+ * which has always meant that one.
17
35
  */
18
- import { mkdirSync, writeFileSync } from 'node:fs';
19
- import { dirname, join, resolve } from 'node:path';
36
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
37
+ import { dirname, join, relative, resolve } from 'node:path';
38
+ import { fileURLToPath } from 'node:url';
20
39
 
21
40
  const SITE_ORIGIN = 'https://rift-ds.com';
22
41
  const SKILL_NAME = 'rift-design-system';
23
42
  const MCP_SERVER_NAME = 'rift-ds';
24
- const SKILL_FILES = ['SKILL.md', 'references/components.md'];
25
- const DEFAULT_OUT = join('.claude', 'skills', SKILL_NAME);
43
+ const PACKAGE_NAME = 'rift-ds';
44
+ const MANIFEST_URL = `${SITE_ORIGIN}/skill/manifest.json`;
45
+ const DEFAULT_DIR = join('.claude', 'skills');
46
+ const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'chat');
47
+
48
+ /* What `init chat` needs installed beside the package. The optional peers
49
+ each chat subpath imports; the demo's recharts is only for the scripted
50
+ preview and goes when the demo import does. */
51
+ const CHAT_PEERS = [
52
+ 'react-markdown',
53
+ 'remark-gfm',
54
+ '@anthropic-ai/sdk',
55
+ '@upstash/ratelimit',
56
+ '@upstash/redis',
57
+ 'recharts',
58
+ ];
59
+
60
+ const USAGE = `Usage: npx ${PACKAGE_NAME} init [--skill <name>] [--dir <dir>]
61
+ npx ${PACKAGE_NAME} init chat [--app <dir>] [--dry-run]
62
+
63
+ init
64
+ Fetches the agent skills for this package from ${SITE_ORIGIN} into
65
+ ${DEFAULT_DIR}/, one folder per skill, so a skill-capable coding agent
66
+ carries the library's install, theming and catalogue rules into every
67
+ session. Re-run any time to refresh; the files regenerate with every
68
+ site deploy.
26
69
 
27
- const USAGE = `Usage: npx rift-ds init [--out <dir>]
70
+ --skill <name> Install one skill instead of all of them
71
+ --dir <dir> Folder that receives one folder per skill (default: ${DEFAULT_DIR})
72
+ --out <dir> Deprecated: write one skill's files straight into <dir>
73
+ (${SKILL_NAME} unless --skill names another). Use --dir.
28
74
 
29
- Fetches the agent skill for this package from ${SITE_ORIGIN} into
30
- ${DEFAULT_OUT}/ so a skill-capable coding agent carries the
31
- library's install, theming and catalogue rules into every session.
32
- Re-run any time to refresh; the files regenerate with every site deploy.
75
+ init chat
76
+ Scaffolds the chat widget into a Next.js app router project: the chat,
77
+ follow-ups and feedback routes under app/api/chat/, the guardrails beside
78
+ them, the mount and its model list under components/chat/, and an
79
+ .env.chat.example. Existing files are never overwritten.
33
80
 
34
- Options:
35
- --out <dir> Write the skill somewhere else (default: ${DEFAULT_OUT})
36
- --help Show this message`;
81
+ --app <dir> The app router folder (default: app/ or src/app/, whichever exists)
82
+ --dry-run Print what would be written and write nothing
83
+
84
+ --help Show this message
85
+
86
+ The list of skills is at ${MANIFEST_URL}`;
37
87
 
38
88
  function fail(message) {
39
89
  console.error(`✗ ${message}`);
40
90
  process.exit(1);
41
91
  }
42
92
 
43
- async function init(outDir) {
44
- if (SITE_ORIGIN.startsWith('__')) {
45
- fail('this copy was not built: run the published bin (npx rift-ds init).');
93
+ async function get(url) {
94
+ let response;
95
+ try {
96
+ response = await fetch(url);
97
+ } catch (error) {
98
+ fail(`could not reach ${url} (${error?.message ?? error}). Check your network and retry.`);
99
+ }
100
+ if (!response.ok) {
101
+ fail(`${url} answered ${response.status}. The site may be mid-deploy; retry in a minute.`);
46
102
  }
47
- for (const file of SKILL_FILES) {
48
- const url = `${SITE_ORIGIN}/skill/${SKILL_NAME}/${file}`;
49
- let response;
50
- try {
51
- response = await fetch(url);
52
- } catch (error) {
53
- fail(`could not reach ${url} (${error?.message ?? error}). Check your network and retry.`);
103
+ return response.text();
104
+ }
105
+
106
+ /** The manifest's skills, with every name and path checked before use. */
107
+ async function readManifest() {
108
+ let manifest;
109
+ try {
110
+ manifest = JSON.parse(await get(MANIFEST_URL));
111
+ } catch {
112
+ fail(`${MANIFEST_URL} is not valid JSON. The site may be mid-deploy; retry in a minute.`);
113
+ }
114
+ const skills = manifest?.skills;
115
+ if (!Array.isArray(skills) || skills.length === 0) {
116
+ fail(`${MANIFEST_URL} lists no skills. Update the package (npm install ${PACKAGE_NAME}@latest) and retry.`);
117
+ }
118
+ const segment = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
119
+ for (const skill of skills) {
120
+ const filesOk =
121
+ Array.isArray(skill?.files) &&
122
+ skill.files.length > 0 &&
123
+ skill.files.every(
124
+ (file) => typeof file === 'string' && file.split('/').every((part) => segment.test(part))
125
+ );
126
+ if (typeof skill?.name !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(skill.name) || !filesOk) {
127
+ fail(`${MANIFEST_URL} holds an entry this command will not write to disk. Nothing was installed.`);
54
128
  }
55
- if (!response.ok) {
56
- fail(`${url} answered ${response.status}. The site may be mid-deploy; retry in a minute.`);
129
+ }
130
+ return skills;
131
+ }
132
+
133
+ async function init({ only, dir, out }) {
134
+ const skills = await readManifest();
135
+ const wanted = only ?? (out ? SKILL_NAME : null);
136
+ const selected = wanted ? skills.filter((skill) => skill.name === wanted) : skills;
137
+ if (selected.length === 0) {
138
+ fail(`no skill named "${wanted}". Available: ${skills.map((skill) => skill.name).join(', ')}.`);
139
+ }
140
+ for (const skill of selected) {
141
+ const target = out ?? join(dir, skill.name);
142
+ for (const file of skill.files) {
143
+ const text = await get(`${SITE_ORIGIN}/skill/${skill.name}/${file}`);
144
+ const path = join(target, ...file.split('/'));
145
+ mkdirSync(dirname(path), { recursive: true });
146
+ writeFileSync(path, text);
147
+ console.log(`▸ ${path}`);
57
148
  }
58
- const target = join(outDir, file);
59
- mkdirSync(dirname(target), { recursive: true });
60
- writeFileSync(target, await response.text());
61
- console.log(`▸ ${target}`);
62
149
  }
63
- console.log(`✓ Agent skill installed. Skill-capable agents load it automatically.`);
150
+ const names = selected.map((skill) => skill.name).join(', ');
151
+ console.log(
152
+ selected.length === 1
153
+ ? `✓ Agent skill installed: ${names}. Skill-capable agents load it automatically.`
154
+ : `✓ ${selected.length} agent skills installed: ${names}. Skill-capable agents load them automatically.`
155
+ );
156
+ if (out) {
157
+ console.log(`\n--out is deprecated. Use --dir to choose the folder that holds the skill folders.`);
158
+ }
64
159
  console.log(`\nFor on-demand lookups too, connect the MCP endpoint:`);
65
160
  console.log(` claude mcp add --transport http ${MCP_SERVER_NAME} ${SITE_ORIGIN}/api/mcp`);
66
161
  }
67
162
 
163
+ /** Every template file, as a path relative to the templates folder (forward slashes). */
164
+ function templateFiles(dir = TEMPLATES_DIR) {
165
+ const out = [];
166
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
167
+ const full = join(dir, entry.name);
168
+ if (entry.isDirectory()) out.push(...templateFiles(full));
169
+ else out.push(relative(TEMPLATES_DIR, full).split('\\').join('/'));
170
+ }
171
+ return out.sort();
172
+ }
173
+
174
+ /**
175
+ * Where a template lands. `app/**` goes into the app router folder,
176
+ * `components/**` beside it (src/components when the app is src/app), and
177
+ * the env example at the project root.
178
+ */
179
+ function destinationFor(file, appDir) {
180
+ if (file.startsWith('app/')) return join(appDir, ...file.slice('app/'.length).split('/'));
181
+ if (file.startsWith('components/')) return join(dirname(appDir), ...file.split('/'));
182
+ return join(process.cwd(), ...file.split('/'));
183
+ }
184
+
185
+ function initChat({ app, dryRun }) {
186
+ if (!existsSync(TEMPLATES_DIR)) {
187
+ fail('this copy carries no chat templates: run the published bin (npx ' + PACKAGE_NAME + ' init chat).');
188
+ }
189
+ const appDir = app ?? ['app', join('src', 'app')].map((d) => resolve(d)).find((d) => existsSync(d));
190
+ if (!appDir) {
191
+ fail('no app/ or src/app/ folder found. Run this inside a Next.js app router project, or pass --app <dir>.');
192
+ }
193
+
194
+ const files = templateFiles();
195
+ const written = [];
196
+ const skipped = [];
197
+ for (const file of files) {
198
+ const dest = destinationFor(file, appDir);
199
+ if (existsSync(dest)) {
200
+ skipped.push(dest);
201
+ continue;
202
+ }
203
+ if (!dryRun) {
204
+ mkdirSync(dirname(dest), { recursive: true });
205
+ writeFileSync(dest, readFileSync(join(TEMPLATES_DIR, ...file.split('/'))));
206
+ }
207
+ written.push(dest);
208
+ console.log(`${dryRun ? '○' : '▸'} ${relative(process.cwd(), dest)}`);
209
+ }
210
+ for (const dest of skipped) {
211
+ console.log(`· ${relative(process.cwd(), dest)} exists, left as it is`);
212
+ }
213
+
214
+ console.log(
215
+ dryRun
216
+ ? `\n✓ Dry run: ${written.length} files would be written, ${skipped.length} left alone.`
217
+ : `\n✓ Chat scaffolded: ${written.length} files written, ${skipped.length} left alone.`
218
+ );
219
+ console.log(`\nNext:`);
220
+ console.log(` 1. Install the peers the chat imports:\n npm install ${CHAT_PEERS.join(' ')}`);
221
+ console.log(
222
+ ` 2. Render <ChatMount /> at the end of your root layout's body, after the page content:\n` +
223
+ ` import { ChatMount } from '${relative(appDir, join(dirname(appDir), 'components', 'chat', 'ChatMount')).split('\\').join('/')}';`
224
+ );
225
+ console.log(
226
+ ` 3. Copy .env.chat.example into .env.local. With NEXT_PUBLIC_CHAT_DEMO=true the widget plays a scripted\n` +
227
+ ` conversation and needs no key; set ANTHROPIC_API_KEY and CHAT_ENABLED=true to go live.`
228
+ );
229
+ console.log(
230
+ ` 4. Your layout's viewport should set interactiveWidget: "resizes-content", so the phone takeover ends above the keyboard.`
231
+ );
232
+ console.log(`\nThe widget's props, protocol and server options: ${SITE_ORIGIN}/chat`);
233
+ }
234
+
68
235
  const args = process.argv.slice(2);
69
236
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
70
237
  console.log(USAGE);
@@ -73,10 +240,30 @@ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
73
240
  if (args[0] !== 'init') {
74
241
  fail(`unknown command "${args[0]}"\n\n${USAGE}`);
75
242
  }
76
- const outFlag = args.indexOf('--out');
77
- let outDir = DEFAULT_OUT;
78
- if (outFlag !== -1) {
79
- if (!args[outFlag + 1]) fail('--out needs a directory');
80
- outDir = resolve(args[outFlag + 1]);
243
+ if (SITE_ORIGIN.startsWith('__')) {
244
+ fail(`this copy was not built: run the published bin (npx ${PACKAGE_NAME} init).`);
245
+ }
246
+ /** The value after a flag, or undefined when the flag is absent. */
247
+ function option(flag, what) {
248
+ const at = args.indexOf(flag);
249
+ if (at === -1) return undefined;
250
+ const value = args[at + 1];
251
+ if (!value || value.startsWith('--')) fail(`${flag} needs ${what}`);
252
+ return value;
253
+ }
254
+
255
+ if (args[1] === 'chat') {
256
+ const appFlag = option('--app', 'a directory');
257
+ initChat({ app: appFlag ? resolve(appFlag) : undefined, dryRun: args.includes('--dry-run') });
258
+ } else {
259
+ if (args[1] && !args[1].startsWith('--')) fail(`unknown init target "${args[1]}"\n\n${USAGE}`);
260
+ const only = option('--skill', 'a skill name');
261
+ const dirFlag = option('--dir', 'a directory');
262
+ const outFlag = option('--out', 'a directory');
263
+ if (dirFlag && outFlag) fail('use --dir or --out, not both');
264
+ await init({
265
+ only,
266
+ dir: dirFlag ? resolve(dirFlag) : DEFAULT_DIR,
267
+ out: outFlag ? resolve(outFlag) : undefined,
268
+ });
81
269
  }
82
- await init(outDir);
@@ -0,0 +1,21 @@
1
+ # The chat widget's environment. Copy what you need into .env.local.
2
+
3
+ # Demo mode: the widget plays a scripted conversation in the browser, with
4
+ # no model, route or key. Set to false (or remove) to go live.
5
+ NEXT_PUBLIC_CHAT_DEMO=true
6
+
7
+ # Live mode. Both must be set for the routes to call a model; until then they
8
+ # answer with a polite notice.
9
+ CHAT_ENABLED=false
10
+ ANTHROPIC_API_KEY=
11
+
12
+ # Guardrails over Upstash Redis (optional). Unset, every request is allowed
13
+ # and nothing is logged. Set all three to get per-visitor rate limits, a
14
+ # daily budget with model tiers, the exchange log and the feedback store.
15
+ KV_REST_API_URL=
16
+ KV_REST_API_TOKEN=
17
+ CHAT_VISITOR_SALT=
18
+
19
+ # The daily breaker, in messages and US cents (defaults shown).
20
+ CHAT_DAILY_MESSAGE_CAP=300
21
+ CHAT_DAILY_SPEND_CAP_CENTS=500
@@ -0,0 +1,12 @@
1
+ import { createFeedbackHandler } from 'rift-ds/chat/server';
2
+ import { guardrails } from '../guardrails';
3
+
4
+ export const runtime = 'nodejs';
5
+ export const dynamic = 'force-dynamic';
6
+
7
+ /** Thumbs verdicts on answers, stored against the exchange the chat route logged. */
8
+ const handler = createFeedbackHandler({ guardrails });
9
+
10
+ export async function POST(request: Request): Promise<Response> {
11
+ return handler(request);
12
+ }
@@ -0,0 +1,23 @@
1
+ import { createFollowupsHandler } from 'rift-ds/chat/server';
2
+ import { FOLLOWUP_MODEL } from '../../../../components/chat/models';
3
+ import { guardrails } from '../guardrails';
4
+
5
+ export const runtime = 'nodejs';
6
+ export const dynamic = 'force-dynamic';
7
+
8
+ /**
9
+ * The three follow-up questions offered under a finished answer, written by
10
+ * a small model from the question-and-answer pair. Say what the chat is
11
+ * about in `about`, so the questions stay on your subject.
12
+ */
13
+ const handler = createFollowupsHandler({
14
+ apiKey: process.env.ANTHROPIC_API_KEY,
15
+ enabled: process.env.CHAT_ENABLED === 'true' && Boolean(process.env.ANTHROPIC_API_KEY),
16
+ model: FOLLOWUP_MODEL,
17
+ about: 'this product and its documentation',
18
+ guardrails,
19
+ });
20
+
21
+ export async function POST(request: Request): Promise<Response> {
22
+ return handler(request);
23
+ }
@@ -0,0 +1,18 @@
1
+ import { createUpstashGuardrails } from 'rift-ds/chat/upstash';
2
+
3
+ /**
4
+ * Rate limits, a daily budget with model tiers, the exchange log and the
5
+ * feedback store, over Upstash Redis. Shared by the three chat routes.
6
+ *
7
+ * Fails open: with KV_REST_API_URL and KV_REST_API_TOKEN unset, every
8
+ * request is allowed and nothing is logged, so the chat works on day one
9
+ * and the limits arrive when you add the two variables. Set CHAT_VISITOR_SALT
10
+ * too: it salts the hashed visitor address the limits key on.
11
+ */
12
+ export const guardrails = createUpstashGuardrails({
13
+ url: process.env.KV_REST_API_URL,
14
+ token: process.env.KV_REST_API_TOKEN,
15
+ salt: process.env.CHAT_VISITOR_SALT,
16
+ dailyMessageCap: Number(process.env.CHAT_DAILY_MESSAGE_CAP ?? 300),
17
+ dailySpendCapCents: Number(process.env.CHAT_DAILY_SPEND_CAP_CENTS ?? 500),
18
+ });
@@ -0,0 +1,31 @@
1
+ import { createChatHandler } from 'rift-ds/chat/server';
2
+ import { CHAT_MODELS, DEFAULT_CHAT_MODEL } from '../../../components/chat/models';
3
+ import { guardrails } from './guardrails';
4
+
5
+ export const runtime = 'nodejs';
6
+ export const maxDuration = 60;
7
+ export const dynamic = 'force-dynamic';
8
+
9
+ /**
10
+ * What the assistant knows and how it behaves. Replace this with your own
11
+ * instructions and whatever the model should read: your docs, your product's
12
+ * facts, your rules. Put `cache_control` on the block that holds the bulk
13
+ * of it, so repeat visitors hit the prompt cache.
14
+ */
15
+ const SYSTEM = `You are the assistant on this site. Answer from what you know about the product, plainly and briefly. If you do not know, say so.`;
16
+
17
+ const handler = createChatHandler({
18
+ apiKey: process.env.ANTHROPIC_API_KEY,
19
+ // Off until both are set, so a deploy ahead of its environment answers
20
+ // politely rather than throwing.
21
+ enabled: process.env.CHAT_ENABLED === 'true' && Boolean(process.env.ANTHROPIC_API_KEY),
22
+ system: [{ type: 'text', text: SYSTEM, cache_control: { type: 'ephemeral' } }],
23
+ models: CHAT_MODELS,
24
+ defaultModel: DEFAULT_CHAT_MODEL,
25
+ // tools: [], // Deterministic lookups the model may call; a tool can answer with a card.
26
+ guardrails,
27
+ });
28
+
29
+ export async function POST(request: Request): Promise<Response> {
30
+ return handler(request);
31
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The chat, mounted once. Render <ChatMount /> at the end of your root
5
+ * layout's body, after the page content, so the panel sits last in the tab
6
+ * order and survives client-side navigation.
7
+ *
8
+ * Two modes, switched by NEXT_PUBLIC_CHAT_DEMO in .env.local:
9
+ * - demo: a scripted conversation plays back in the browser. No model, no
10
+ * route, no key. Use it to see the widget working and to show it round.
11
+ * - live: the widget posts to /api/chat, which this scaffold also
12
+ * wrote. Set ANTHROPIC_API_KEY and CHAT_ENABLED=true there.
13
+ *
14
+ * The demo imports rift-ds/chat/demo, which needs the optional `recharts`
15
+ * peer for its chart cards. Once you are live for good, delete the demo
16
+ * import and the peer with it.
17
+ */
18
+ import { ChatLauncher, ChatProvider } from 'rift-ds/chat';
19
+ import { DEMO_RENDERERS, DEMO_STARTERS, createDemoTransport } from 'rift-ds/chat/demo';
20
+ import 'rift-ds/chat/chat.css';
21
+ import { CHAT_MODELS, DEFAULT_CHAT_MODEL } from './models';
22
+
23
+ const DEMO = process.env.NEXT_PUBLIC_CHAT_DEMO === 'true';
24
+
25
+ /** What the widget says about itself. Edit freely. */
26
+ const WIDGET = {
27
+ title: 'Assistant',
28
+ tagline: 'How can I help you today?',
29
+ starters: DEMO
30
+ ? DEMO_STARTERS
31
+ : [
32
+ { id: 'start', label: 'How do I get started?' },
33
+ { id: 'pricing', label: 'What do the plans include?' },
34
+ { id: 'help', label: 'Where can I read more?' },
35
+ ],
36
+ models: CHAT_MODELS,
37
+ defaultModel: DEFAULT_CHAT_MODEL,
38
+ disclaimer: 'Answers may be wrong.',
39
+ };
40
+
41
+ export function ChatMount() {
42
+ return (
43
+ <ChatProvider
44
+ transport={DEMO ? createDemoTransport() : undefined}
45
+ renderers={DEMO ? DEMO_RENDERERS : undefined}
46
+ followups={DEMO ? false : '/api/chat/followups'}
47
+ feedback={DEMO ? false : '/api/chat/feedback'}
48
+ >
49
+ <ChatLauncher label={`Ask ${WIDGET.title}`} widget={WIDGET} />
50
+ </ChatProvider>
51
+ );
52
+ }
@@ -0,0 +1,29 @@
1
+ import type { ChatModelOption } from 'rift-ds/chat';
2
+
3
+ /**
4
+ * The models your chat offers. The widget's picker shows `label` and
5
+ * `description`; the client sends `value`; the server calls `id`. Shared by
6
+ * the route and the mount so the two can never disagree. Edit freely.
7
+ */
8
+ export const CHAT_MODELS: ChatModelOption[] = [
9
+ {
10
+ value: 'haiku',
11
+ id: 'claude-haiku-5-5',
12
+ label: 'Haiku 5.5',
13
+ description: 'Fast answers, the everyday pick.',
14
+ adaptiveThinking: true,
15
+ },
16
+ {
17
+ value: 'sonnet',
18
+ id: 'claude-sonnet-5-5',
19
+ label: 'Sonnet 5.5',
20
+ description: 'More depth for harder questions.',
21
+ adaptiveThinking: true,
22
+ },
23
+ ];
24
+
25
+ /** What serves when the visitor has not chosen. */
26
+ export const DEFAULT_CHAT_MODEL = CHAT_MODELS[0].value;
27
+
28
+ /** The model that writes the three follow-up questions under an answer. */
29
+ export const FOLLOWUP_MODEL = 'claude-haiku-5-5';
@@ -0,0 +1,5 @@
1
+ import { ChatTurn, LiveResponse } from './types';
2
+ export declare function AssistantTurn({ turn, live }: {
3
+ turn?: ChatTurn;
4
+ live?: LiveResponse;
5
+ }): import("react").JSX.Element;