@opentiny/tiny-robot-cli 0.5.2-alpha.13 → 0.5.2-alpha.14

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 (48) hide show
  1. package/README.md +32 -5
  2. package/bin/commands/add.js +589 -198
  3. package/bin/utils.js +139 -47
  4. package/package.json +4 -3
  5. package/templates/basic/.env.example +8 -0
  6. package/templates/basic/README.md +53 -22
  7. package/templates/basic/index.html +2 -2
  8. package/templates/basic/package.json +12 -16
  9. package/templates/basic/public/favicon.svg +1 -0
  10. package/templates/basic/public/icons.svg +24 -0
  11. package/templates/basic/src/App.vue +112 -119
  12. package/templates/basic/src/main.ts +2 -2
  13. package/templates/basic/tsconfig.app.json +2 -5
  14. package/templates/basic/tsconfig.node.json +3 -6
  15. package/templates/basic/vite.config.ts +6 -1
  16. package/templates/chat/.env.example +8 -0
  17. package/templates/chat/README.md +21 -13
  18. package/templates/chat/package.json +5 -5
  19. package/templates/chat/src/TinyRobotChat.vue +52 -132
  20. package/templates/chat/src/components/ComposerTools.vue +99 -0
  21. package/templates/chat/src/{tiny-robot-chat/components → components}/WindowHeader.vue +1 -1
  22. package/templates/chat/src/{tiny-robot-chat/composables → composables}/useWindow.ts +8 -18
  23. package/templates/chat/src/config/chat-runtime.ts +82 -0
  24. package/templates/chat/src/{tiny-robot-chat/config → config}/chat-ui.ts +22 -4
  25. package/templates/chat/src/index.css +116 -0
  26. package/templates/chat/src/main.ts +3 -0
  27. package/templates/basic/public/modelcontextprotocol.png +0 -0
  28. package/templates/basic/src/components/ChatList.vue +0 -82
  29. package/templates/basic/src/components/ChatSender.vue +0 -125
  30. package/templates/basic/src/components/ConversationHistory.vue +0 -136
  31. package/templates/basic/src/components/HistoryDrawerButton.vue +0 -43
  32. package/templates/basic/src/components/McpServerPickerButton.vue +0 -278
  33. package/templates/basic/src/components/ThemeToggleButton.vue +0 -44
  34. package/templates/basic/src/components/icons/IconDeepThink.vue +0 -29
  35. package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +0 -51
  36. package/templates/basic/src/components/icons/IconModelDeepseek.vue +0 -29
  37. package/templates/basic/src/components/icons/IconMoon.vue +0 -29
  38. package/templates/basic/src/components/icons/IconPlugin.vue +0 -29
  39. package/templates/basic/src/components/icons/IconSun.vue +0 -35
  40. package/templates/basic/src/components/icons/IconWebSearch.vue +0 -36
  41. package/templates/basic/src/components/icons/index.ts +0 -7
  42. package/templates/basic/src/composables/useChat.ts +0 -129
  43. package/templates/basic/src/composables/useMcp.ts +0 -170
  44. package/templates/basic/src/composables/useModel.ts +0 -82
  45. package/templates/basic/src/mcpServers.ts +0 -40
  46. package/templates/basic/src/models.ts +0 -81
  47. package/templates/chat/src/tiny-robot-chat/components/ComposerTools.vue +0 -72
  48. package/templates/chat/src/tiny-robot-chat/config/chat-runtime.ts +0 -26
package/bin/utils.js CHANGED
@@ -36,10 +36,7 @@ export function logError(message) {
36
36
  }
37
37
 
38
38
  export function invariant(condition, message) {
39
- if (!condition) {
40
- console.error(`Error: ${message}`)
41
- process.exit(1)
42
- }
39
+ if (!condition) throw new Error(message)
43
40
  }
44
41
 
45
42
  export function exists(file) {
@@ -114,7 +111,7 @@ function renameSpecialFiles(targetDir) {
114
111
  }
115
112
 
116
113
  function replaceTemplateVariables(targetDir, variables) {
117
- const replaceFiles = ['package.json', 'README.md']
114
+ const replaceFiles = ['package.json', 'README.md', 'index.html']
118
115
 
119
116
  for (const relativePath of replaceFiles) {
120
117
  const file = path.join(targetDir, relativePath)
@@ -170,20 +167,24 @@ export function findProjectRoot(cwd) {
170
167
  }
171
168
 
172
169
  export function findSubPackageRoot(cwd, workspaceRoot) {
173
- return findUp(cwd, (dir) => {
174
- if (dir === workspaceRoot) {
175
- return false
176
- }
170
+ let dir = path.resolve(cwd)
171
+ const root = path.resolve(workspaceRoot)
177
172
 
178
- return isPackageDir(dir)
179
- })
173
+ for (;;) {
174
+ if (dir === root) return null
175
+ if (isPackageDir(dir)) return dir
176
+
177
+ const parent = path.dirname(dir)
178
+ if (parent === dir) return null
179
+ dir = parent
180
+ }
180
181
  }
181
182
 
182
183
  export function findWorkspacePackages(workspaceRoot) {
183
184
  const workspaceFile = resolveWorkspaceFile(workspaceRoot)
184
185
 
185
186
  if (!workspaceFile) {
186
- return []
187
+ return undefined
187
188
  }
188
189
 
189
190
  try {
@@ -191,60 +192,139 @@ export function findWorkspacePackages(workspaceRoot) {
191
192
 
192
193
  const config = yaml.parse(content)
193
194
 
194
- return Array.isArray(config?.packages)
195
- ? config.packages.filter(
196
- (pattern) => typeof pattern === 'string' && pattern.length > 0 && !pattern.startsWith('!'),
197
- )
198
- : []
199
- } catch (error) {
200
- const reason = error instanceof Error ? error.message : String(error)
195
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
196
+ throw new Error('workspace configuration must be a YAML mapping')
197
+ }
201
198
 
202
- throw new Error(`Invalid pnpm workspace configuration in ${workspaceFile}: ${reason}`, {
203
- cause: error,
204
- })
199
+ if (!Object.prototype.hasOwnProperty.call(config, 'packages')) {
200
+ return undefined
201
+ }
202
+
203
+ if (!Array.isArray(config.packages)) {
204
+ throw new Error('packages must be an array')
205
+ }
206
+
207
+ if (config.packages.some((pattern) => typeof pattern !== 'string' || pattern.trim().length === 0)) {
208
+ throw new Error('packages entries must be non-empty strings')
209
+ }
210
+
211
+ return config.packages.map((pattern) => pattern.trim())
212
+ } catch (error) {
213
+ const detail = error instanceof Error ? `: ${error.message}` : ''
214
+ throw new Error(`Failed to read ${path.basename(workspaceFile)}${detail}`)
205
215
  }
206
216
  }
207
217
 
218
+ const WORKSPACE_SCAN_IGNORES = new Set(['.git', 'node_modules'])
219
+
208
220
  function normalizeWorkspacePattern(pattern) {
209
- return pattern.replace(/\*\*?/g, '').replace(/\/$/, '')
221
+ let normalized = pattern.replaceAll('\\', '/').trim()
222
+ if (normalized.startsWith('./')) normalized = normalized.slice(2)
223
+ return normalized.replace(/\/$/, '')
210
224
  }
211
225
 
212
- export function listPackages(workspaceRoot, patterns) {
213
- const packageDirs = []
214
-
215
- const addPackage = (dir) => {
216
- if (isPackageDir(dir)) {
217
- packageDirs.push(dir)
218
- }
219
- }
226
+ function globSource(pattern) {
227
+ let source = ''
220
228
 
221
- for (const pattern of patterns) {
222
- const base = normalizeWorkspacePattern(pattern)
229
+ for (let index = 0; index < pattern.length; index += 1) {
230
+ const char = pattern[index]
223
231
 
224
- const fullPath = path.join(workspaceRoot, base)
232
+ if (char === '*') {
233
+ if (pattern[index + 1] === '*') {
234
+ while (pattern[index + 1] === '*') index += 1
235
+ if (pattern[index + 1] === '/') {
236
+ source += '(?:.*/)?'
237
+ index += 1
238
+ } else {
239
+ source += '.*'
240
+ }
241
+ } else {
242
+ source += '[^/]*'
243
+ }
244
+ continue
245
+ }
225
246
 
226
- if (!exists(fullPath)) {
247
+ if (char === '?') {
248
+ source += '[^/]'
227
249
  continue
228
250
  }
229
251
 
230
- if (pattern.includes('*')) {
231
- const entries = fs.readdirSync(fullPath, {
232
- withFileTypes: true,
233
- })
252
+ if (char === '{') {
253
+ const close = pattern.indexOf('}', index + 1)
254
+ if (close !== -1) {
255
+ const alternatives = pattern
256
+ .slice(index + 1, close)
257
+ .split(',')
258
+ .map((item) => globSource(item))
259
+ source += `(?:${alternatives.join('|')})`
260
+ index = close
261
+ continue
262
+ }
263
+ }
234
264
 
235
- for (const entry of entries) {
236
- if (entry.isDirectory()) {
237
- addPackage(path.join(fullPath, entry.name))
238
- }
265
+ if (char === '[') {
266
+ const close = pattern.indexOf(']', index + 1)
267
+ if (close !== -1) {
268
+ source += pattern.slice(index, close + 1)
269
+ index = close
270
+ continue
239
271
  }
240
- } else {
241
- addPackage(fullPath)
272
+ }
273
+
274
+ source += char.replace(/[\\.^$+()|]/g, '\\$&')
275
+ }
276
+
277
+ return source
278
+ }
279
+
280
+ function matchesWorkspacePattern(relativePath, pattern) {
281
+ const normalized = normalizeWorkspacePattern(pattern)
282
+ if (normalized === '.') return relativePath === ''
283
+ return new RegExp(`^${globSource(normalized)}$`).test(relativePath)
284
+ }
285
+
286
+ function collectPackageDirs(workspaceRoot) {
287
+ const packageDirs = []
288
+
289
+ const visit = (dir) => {
290
+ if (isPackageDir(dir)) packageDirs.push(dir)
291
+
292
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
293
+ if (!entry.isDirectory() || WORKSPACE_SCAN_IGNORES.has(entry.name)) continue
294
+ visit(path.join(dir, entry.name))
242
295
  }
243
296
  }
244
297
 
298
+ visit(workspaceRoot)
245
299
  return packageDirs
246
300
  }
247
301
 
302
+ export function listPackages(workspaceRoot, patterns = undefined) {
303
+ const configuredPatterns = patterns ?? ['**']
304
+ const includePatterns = configuredPatterns.filter((pattern) => !pattern.startsWith('!'))
305
+ const excludePatterns = configuredPatterns
306
+ .filter((pattern) => pattern.startsWith('!'))
307
+ .map((pattern) => pattern.slice(1))
308
+
309
+ if (includePatterns.length === 0) return []
310
+
311
+ const root = path.resolve(workspaceRoot)
312
+ const packageDirs = collectPackageDirs(root)
313
+ const matched = packageDirs.filter((dir) => {
314
+ const relativePath = path.relative(root, dir).replaceAll('\\', '/')
315
+ return (
316
+ includePatterns.some((pattern) => matchesWorkspacePattern(relativePath, pattern)) &&
317
+ !excludePatterns.some((pattern) => matchesWorkspacePattern(relativePath, pattern))
318
+ )
319
+ })
320
+
321
+ return [...new Set(matched)].sort((left, right) => {
322
+ const leftRelative = path.relative(root, left)
323
+ const rightRelative = path.relative(root, right)
324
+ return leftRelative.localeCompare(rightRelative)
325
+ })
326
+ }
327
+
248
328
  function ensureDir(file) {
249
329
  fs.mkdirSync(path.dirname(file), {
250
330
  recursive: true,
@@ -294,6 +374,19 @@ export function mergeEnvFile(templateFile, targetFile) {
294
374
 
295
375
  const targetContent = fs.readFileSync(targetFile, 'utf-8')
296
376
 
377
+ const result = mergeEnvContent(templateContent, targetContent)
378
+
379
+ if (result.type === 'merged') {
380
+ fs.writeFileSync(targetFile, result.content)
381
+ }
382
+
383
+ return {
384
+ type: result.type,
385
+ ...(result.added === undefined ? {} : { added: result.added }),
386
+ }
387
+ }
388
+
389
+ export function mergeEnvContent(templateContent, targetContent) {
297
390
  const templateEnv = parseEnv(templateContent)
298
391
 
299
392
  const targetEnv = parseEnv(targetContent)
@@ -316,10 +409,9 @@ export function mergeEnvFile(templateFile, targetFile) {
316
409
 
317
410
  const nextContent = targetTrimmed ? `${targetTrimmed}\n${appendLines.join('\n')}\n` : `${appendLines.join('\n')}\n`
318
411
 
319
- fs.writeFileSync(targetFile, nextContent)
320
-
321
412
  return {
322
413
  type: 'merged',
323
414
  added: appendLines.length,
415
+ content: nextContent,
324
416
  }
325
417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentiny/tiny-robot-cli",
3
- "version": "0.5.2-alpha.13",
3
+ "version": "0.5.2-alpha.14",
4
4
  "description": "CLI to scaffold TinyRobot product projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "start": "node ./bin/cli.js",
23
- "test": "node --test ./test/*.test.js"
23
+ "test": "node --test test/*.test.js"
24
24
  },
25
25
  "license": "MIT",
26
26
  "engines": {
@@ -28,10 +28,11 @@
28
28
  },
29
29
  "dependencies": {
30
30
  "@inquirer/prompts": "^8.3.2",
31
+ "@vue/compiler-sfc": "^3.5.40",
31
32
  "commander": "^14.0.3",
32
33
  "picocolors": "^1.1.1",
33
34
  "semver": "^7.8.1",
34
35
  "yaml": "^2.9.0"
35
36
  },
36
- "gitHead": "afeda129637cd33e0771ff9fee90a268a1ff55d5"
37
+ "gitHead": "28e62a60d81f851ff6f07fb8fd9869a37fe6f097"
37
38
  }
@@ -1,2 +1,10 @@
1
+ # Optional custom model endpoints. Leave blank to use official Provider defaults.
2
+ VITE_QWEN_API_URL=
3
+ VITE_DEEPSEEK_API_URL=
4
+
5
+ # Optional custom MCP endpoint. Leave blank to use the official endpoint.
6
+ VITE_AMAP_MCP_URL=
7
+
8
+ # Optional local direct-connection credentials.
1
9
  VITE_ALIYUN_DASHSCOPE_KEY=
2
10
  VITE_DEEPSEEK_API_KEY=
@@ -1,39 +1,71 @@
1
- # __PROJECT_NAME__
1
+ # **PROJECT_NAME**
2
2
 
3
- TinyRobot AI chat starter built with Vue 3 + Vite.
3
+ This project is generated from the TinyRobot Chat Basic template. It covers conversations, streaming responses, cancellation, Markdown rendering, responsive history, themes, model capabilities, and MCP Server/Tool management.
4
4
 
5
- ## Features
5
+ ## MCP support
6
6
 
7
- - Vue 3 + Vite + TypeScript project scaffold
8
- - TinyRobot chat UI with `TrBubbleList`, `TrSender`, and markdown rendering
9
- - Conversation management via `useConversation`
10
- - Model switch with thinking/search capability toggles
11
- - MCP server picker for add/toggle/delete server usage
12
- - MCP transport support for both `sse` and `streamableHttp`
13
- - Tool calling pipeline through `toolPlugin` + MCP `listTools` / `callTool`
14
- - Theme toggle and responsive layout for desktop/mobile
7
+ The demo declares MCP Servers through `mcpServers`:
15
8
 
16
- ## Setup
9
+ ```ts
10
+ const mcpServers = [
11
+ { id: 'amap-maps', name: '高德地图', baseUrl: 'https://...' },
12
+ {
13
+ id: 'model-context-protocol-mcp',
14
+ name: 'Model Context Protocol MCP',
15
+ baseUrl: '/modelcontextprotocol-mcp',
16
+ installed: true,
17
+ },
18
+ ]
19
+ ```
17
20
 
18
- 1. Copy environment variables:
21
+ `installed: true` means initially installed only. The Server remains disabled and does not connect or discover Tools until enabled.
19
22
 
20
- ```bash
21
- cp .env.example .env
22
- ```
23
+ | Server | Transport | Authentication |
24
+ | -------------------------- | --------------- | ---------------- |
25
+ | 高德地图 | Streamable HTTP | Optional API Key |
26
+ | Model Context Protocol MCP | Streamable HTTP | None |
27
+
28
+ Tools are discovered dynamically with `client.listTools()`. Tool choices are stored in the next user message's `runConfig` metadata snapshot, so changing a Tool affects subsequent turns without modifying a request already in progress.
29
+
30
+ Server installation, enabling, Tool loading, retry, de-duplication, and Tool calls are handled by the default adapter in `@opentiny/tiny-robot-chat`. If a Server cannot load its Tools, it remains installed but is automatically disabled. Other enabled Servers and normal chat remain available.
31
+
32
+ ## Model connection modes
33
+
34
+ - API Key only: connects directly to the official Qwen or DeepSeek endpoint.
35
+ - `apiUrl` + API Key: connects to a custom service with frontend authentication.
36
+ - `apiUrl` only: connects to a backend proxy without frontend authentication.
37
+
38
+ The Chat package does not prompt for or validate missing API Keys. The upstream service or backend proxy decides whether authentication is required.
23
39
 
24
- 2. Fill your provider keys in `.env`:
40
+ MCP follows the same three modes: the official endpoint with an optional API Key, a custom `baseUrl` with optional headers, or a proxy `baseUrl` without headers.
41
+
42
+ The basic template already includes the Model Context MCP proxy in `vite.config.ts`. If you use `add chat` in an existing project, the CLI does not modify that project's Vite configuration; add the same `/modelcontextprotocol-mcp` proxy under `server.proxy` manually and restart Vite.
43
+
44
+ ## Model capabilities
45
+
46
+ Qwen models support deep thinking and web search. DeepSeek models support deep thinking. Unsupported capabilities remain visible as disabled buttons so the available model capabilities are clear.
47
+
48
+ DeepSeek models inherit the provider's thinking effort options: `low`, `high`, and `max`, with `high` as the default. The effort selector appears only after `深度思考` is enabled. To customize a model, set `efforts` and `defaultEffort` on that model in `src/App.vue`; use `thinkingRequired: true` only for models that cannot disable thinking.
49
+
50
+ ## Environment variables
51
+
52
+ Copy `.env.example` to `.env.local`. Leave endpoint variables blank for official Provider/MCP defaults, or set them for custom services and backend proxies:
25
53
 
26
54
  ```env
27
- VITE_ALIYUN_DASHSCOPE_KEY=your_dashscope_key
28
- VITE_DEEPSEEK_API_KEY=your_deepseek_key
55
+ VITE_QWEN_API_URL=
56
+ VITE_DEEPSEEK_API_URL=
57
+ VITE_AMAP_MCP_URL=
58
+ VITE_ALIYUN_DASHSCOPE_KEY=
59
+ VITE_DEEPSEEK_API_KEY=
29
60
  ```
30
61
 
31
- `VITE_ALIYUN_DASHSCOPE_KEY` is also used by configured MCP servers that require DashScope authorization.
62
+ For a proxy setup, set for example `VITE_QWEN_API_URL=/api/chat/qwen`, `VITE_DEEPSEEK_API_URL=/api/chat/deepseek`, and `VITE_AMAP_MCP_URL=/api/mcp/amap-maps`, then leave the API Key variables blank.
63
+
64
+ Variables prefixed with `VITE_` are exposed to browser code. Direct browser connections are intended only for servers without secrets and with CORS enabled. Production applications should keep credentials behind a backend or BFF proxy.
32
65
 
33
66
  ## Development
34
67
 
35
68
  ```bash
36
- pnpm install
37
69
  pnpm dev
38
70
  ```
39
71
 
@@ -41,5 +73,4 @@ pnpm dev
41
73
 
42
74
  ```bash
43
75
  pnpm build
44
- pnpm preview
45
76
  ```
@@ -2,9 +2,9 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/x-icon" href="/favicon.ico" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>TinyRobot AI Chat</title>
7
+ <title>__PROJECT_NAME__</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="app"></div>
@@ -1,29 +1,25 @@
1
1
  {
2
2
  "name": "__PROJECT_NAME__",
3
3
  "private": true,
4
- "version": "0.1.0",
4
+ "version": "0.0.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "build": "vue-tsc -b && vite build",
9
- "preview": "vite preview",
10
- "type-check": "vue-tsc --noEmit"
9
+ "preview": "vite preview"
11
10
  },
12
11
  "dependencies": {
13
- "@modelcontextprotocol/sdk": "^1.18.1",
14
- "@opentiny/tiny-robot": "^0.5.0",
15
- "@opentiny/tiny-robot-kit": "^0.5.0",
16
- "@opentiny/tiny-robot-svgs": "^0.5.0",
17
- "dompurify": "^3.3.1",
18
- "markdown-it": "^14.1.0",
19
- "vue": "^3.5.30"
12
+ "@opentiny/tiny-robot-chat": "0.5.2-alpha.14",
13
+ "@opentiny/tiny-robot": "0.5.2-alpha.14",
14
+ "@opentiny/tiny-robot-svgs": "0.5.2-alpha.14",
15
+ "vue": "^3.5.40"
20
16
  },
21
17
  "devDependencies": {
22
- "@types/node": "^24.12.0",
23
- "@vitejs/plugin-vue": "^6.0.5",
24
- "@vue/tsconfig": "^0.9.0",
25
- "typescript": "~5.9.3",
26
- "vite": "^8.0.1",
27
- "vue-tsc": "^3.2.5"
18
+ "@types/node": "^24.13.3",
19
+ "@vitejs/plugin-vue": "^6.0.8",
20
+ "@vue/tsconfig": "^0.9.1",
21
+ "typescript": "~6.0.2",
22
+ "vite": "^8.2.0",
23
+ "vue-tsc": "^3.3.8"
28
24
  }
29
25
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg">
2
+ <symbol id="bluesky-icon" viewBox="0 0 16 17">
3
+ <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
4
+ <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
5
+ </symbol>
6
+ <symbol id="discord-icon" viewBox="0 0 20 19">
7
+ <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
8
+ </symbol>
9
+ <symbol id="documentation-icon" viewBox="0 0 21 20">
10
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
11
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
12
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
13
+ </symbol>
14
+ <symbol id="github-icon" viewBox="0 0 19 19">
15
+ <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
16
+ </symbol>
17
+ <symbol id="social-icon" viewBox="0 0 20 20">
18
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
19
+ <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
20
+ </symbol>
21
+ <symbol id="x-icon" viewBox="0 0 19 19">
22
+ <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
23
+ </symbol>
24
+ </svg>