@opentiny/tiny-robot-cli 0.5.2-alpha.15 → 0.5.2-alpha.17

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 (51) hide show
  1. package/README.md +1 -38
  2. package/bin/commands/add.js +268 -552
  3. package/bin/utils.js +44 -144
  4. package/package.json +3 -5
  5. package/templates/basic/.env.example +0 -8
  6. package/templates/basic/README.md +22 -53
  7. package/templates/basic/index.html +2 -2
  8. package/templates/basic/package.json +16 -12
  9. package/templates/basic/public/modelcontextprotocol.png +0 -0
  10. package/templates/basic/src/App.vue +119 -112
  11. package/templates/basic/src/components/ChatList.vue +82 -0
  12. package/templates/basic/src/components/ChatSender.vue +125 -0
  13. package/templates/basic/src/components/ConversationHistory.vue +136 -0
  14. package/templates/basic/src/components/HistoryDrawerButton.vue +43 -0
  15. package/templates/basic/src/components/McpServerPickerButton.vue +278 -0
  16. package/templates/basic/src/components/ThemeToggleButton.vue +44 -0
  17. package/templates/basic/src/components/icons/IconDeepThink.vue +29 -0
  18. package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +51 -0
  19. package/templates/basic/src/components/icons/IconModelDeepseek.vue +29 -0
  20. package/templates/basic/src/components/icons/IconMoon.vue +29 -0
  21. package/templates/basic/src/components/icons/IconPlugin.vue +29 -0
  22. package/templates/basic/src/components/icons/IconSun.vue +35 -0
  23. package/templates/basic/src/components/icons/IconWebSearch.vue +36 -0
  24. package/templates/basic/src/components/icons/index.ts +7 -0
  25. package/templates/basic/src/composables/useChat.ts +129 -0
  26. package/templates/basic/src/composables/useMcp.ts +170 -0
  27. package/templates/basic/src/composables/useModel.ts +82 -0
  28. package/templates/basic/src/main.ts +2 -2
  29. package/templates/basic/src/mcpServers.ts +40 -0
  30. package/templates/basic/src/models.ts +81 -0
  31. package/templates/basic/tsconfig.app.json +5 -2
  32. package/templates/basic/tsconfig.node.json +6 -3
  33. package/templates/basic/vite.config.ts +1 -6
  34. package/templates/chat/.env.example +0 -9
  35. package/templates/chat/src/TinyRobotChat.vue +27 -88
  36. package/templates/basic/public/favicon.svg +0 -1
  37. package/templates/basic/public/icons.svg +0 -24
  38. package/templates/chat/README.md +0 -46
  39. package/templates/chat/index.html +0 -12
  40. package/templates/chat/package.json +0 -28
  41. package/templates/chat/src/components/ComposerTools.vue +0 -99
  42. package/templates/chat/src/components/WindowHeader.vue +0 -192
  43. package/templates/chat/src/composables/useWindow.ts +0 -83
  44. package/templates/chat/src/config/chat-runtime.ts +0 -82
  45. package/templates/chat/src/config/chat-ui.ts +0 -290
  46. package/templates/chat/src/index.css +0 -116
  47. package/templates/chat/src/main.ts +0 -7
  48. package/templates/chat/tsconfig.app.json +0 -13
  49. package/templates/chat/tsconfig.json +0 -7
  50. package/templates/chat/tsconfig.node.json +0 -19
  51. package/templates/chat/vite.config.ts +0 -16
package/bin/utils.js CHANGED
@@ -36,7 +36,10 @@ export function logError(message) {
36
36
  }
37
37
 
38
38
  export function invariant(condition, message) {
39
- if (!condition) throw new Error(message)
39
+ if (!condition) {
40
+ console.error(`Error: ${message}`)
41
+ process.exit(1)
42
+ }
40
43
  }
41
44
 
42
45
  export function exists(file) {
@@ -85,10 +88,6 @@ export function validateProjectName(name) {
85
88
  }
86
89
 
87
90
  export function copyTemplate(sourceDir, targetDir) {
88
- copyDirectory(sourceDir, targetDir)
89
- }
90
-
91
- export function copyDirectory(sourceDir, targetDir) {
92
91
  fs.cpSync(sourceDir, targetDir, {
93
92
  recursive: true,
94
93
  filter: (source) => {
@@ -111,7 +110,7 @@ function renameSpecialFiles(targetDir) {
111
110
  }
112
111
 
113
112
  function replaceTemplateVariables(targetDir, variables) {
114
- const replaceFiles = ['package.json', 'README.md', 'index.html']
113
+ const replaceFiles = ['package.json', 'README.md']
115
114
 
116
115
  for (const relativePath of replaceFiles) {
117
116
  const file = path.join(targetDir, relativePath)
@@ -167,24 +166,20 @@ export function findProjectRoot(cwd) {
167
166
  }
168
167
 
169
168
  export function findSubPackageRoot(cwd, workspaceRoot) {
170
- let dir = path.resolve(cwd)
171
- const root = path.resolve(workspaceRoot)
172
-
173
- for (;;) {
174
- if (dir === root) return null
175
- if (isPackageDir(dir)) return dir
169
+ return findUp(cwd, (dir) => {
170
+ if (dir === workspaceRoot) {
171
+ return false
172
+ }
176
173
 
177
- const parent = path.dirname(dir)
178
- if (parent === dir) return null
179
- dir = parent
180
- }
174
+ return isPackageDir(dir)
175
+ })
181
176
  }
182
177
 
183
178
  export function findWorkspacePackages(workspaceRoot) {
184
179
  const workspaceFile = resolveWorkspaceFile(workspaceRoot)
185
180
 
186
181
  if (!workspaceFile) {
187
- return undefined
182
+ return []
188
183
  }
189
184
 
190
185
  try {
@@ -192,139 +187,56 @@ export function findWorkspacePackages(workspaceRoot) {
192
187
 
193
188
  const config = yaml.parse(content)
194
189
 
195
- if (config === null || typeof config !== 'object' || Array.isArray(config)) {
196
- throw new Error('workspace configuration must be a YAML mapping')
197
- }
198
-
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}`)
190
+ return Array.isArray(config?.packages)
191
+ ? config.packages.filter(
192
+ (pattern) => typeof pattern === 'string' && pattern.length > 0 && !pattern.startsWith('!'),
193
+ )
194
+ : []
195
+ } catch {
196
+ return []
215
197
  }
216
198
  }
217
199
 
218
- const WORKSPACE_SCAN_IGNORES = new Set(['.git', 'node_modules'])
219
-
220
200
  function normalizeWorkspacePattern(pattern) {
221
- let normalized = pattern.replaceAll('\\', '/').trim()
222
- if (normalized.startsWith('./')) normalized = normalized.slice(2)
223
- return normalized.replace(/\/$/, '')
201
+ return pattern.replace(/\*\*?/g, '').replace(/\/$/, '')
224
202
  }
225
203
 
226
- function globSource(pattern) {
227
- let source = ''
228
-
229
- for (let index = 0; index < pattern.length; index += 1) {
230
- const char = pattern[index]
231
-
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
- }
246
-
247
- if (char === '?') {
248
- source += '[^/]'
249
- continue
250
- }
251
-
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
- }
204
+ export function listPackages(workspaceRoot, patterns) {
205
+ const packageDirs = []
264
206
 
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
271
- }
207
+ const addPackage = (dir) => {
208
+ if (isPackageDir(dir)) {
209
+ packageDirs.push(dir)
272
210
  }
273
-
274
- source += char.replace(/[\\.^$+()|]/g, '\\$&')
275
211
  }
276
212
 
277
- return source
278
- }
213
+ for (const pattern of patterns) {
214
+ const base = normalizeWorkspacePattern(pattern)
279
215
 
280
- function matchesWorkspacePattern(relativePath, pattern) {
281
- const normalized = normalizeWorkspacePattern(pattern)
282
- if (normalized === '.') return relativePath === ''
283
- return new RegExp(`^${globSource(normalized)}$`).test(relativePath)
284
- }
216
+ const fullPath = path.join(workspaceRoot, base)
285
217
 
286
- function collectPackageDirs(workspaceRoot) {
287
- const packageDirs = []
218
+ if (!exists(fullPath)) {
219
+ continue
220
+ }
288
221
 
289
- const visit = (dir) => {
290
- if (isPackageDir(dir)) packageDirs.push(dir)
222
+ if (pattern.includes('*')) {
223
+ const entries = fs.readdirSync(fullPath, {
224
+ withFileTypes: true,
225
+ })
291
226
 
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))
227
+ for (const entry of entries) {
228
+ if (entry.isDirectory()) {
229
+ addPackage(path.join(fullPath, entry.name))
230
+ }
231
+ }
232
+ } else {
233
+ addPackage(fullPath)
295
234
  }
296
235
  }
297
236
 
298
- visit(workspaceRoot)
299
237
  return packageDirs
300
238
  }
301
239
 
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
-
328
240
  function ensureDir(file) {
329
241
  fs.mkdirSync(path.dirname(file), {
330
242
  recursive: true,
@@ -374,19 +286,6 @@ export function mergeEnvFile(templateFile, targetFile) {
374
286
 
375
287
  const targetContent = fs.readFileSync(targetFile, 'utf-8')
376
288
 
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) {
390
289
  const templateEnv = parseEnv(templateContent)
391
290
 
392
291
  const targetEnv = parseEnv(targetContent)
@@ -409,9 +308,10 @@ export function mergeEnvContent(templateContent, targetContent) {
409
308
 
410
309
  const nextContent = targetTrimmed ? `${targetTrimmed}\n${appendLines.join('\n')}\n` : `${appendLines.join('\n')}\n`
411
310
 
311
+ fs.writeFileSync(targetFile, nextContent)
312
+
412
313
  return {
413
314
  type: 'merged',
414
315
  added: appendLines.length,
415
- content: nextContent,
416
316
  }
417
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentiny/tiny-robot-cli",
3
- "version": "0.5.2-alpha.15",
3
+ "version": "0.5.2-alpha.17",
4
4
  "description": "CLI to scaffold TinyRobot product projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,8 +19,7 @@
19
19
  "README.md"
20
20
  ],
21
21
  "scripts": {
22
- "start": "node ./bin/cli.js",
23
- "test": "node --test test/*.test.js"
22
+ "start": "node ./bin/cli.js"
24
23
  },
25
24
  "license": "MIT",
26
25
  "engines": {
@@ -28,11 +27,10 @@
28
27
  },
29
28
  "dependencies": {
30
29
  "@inquirer/prompts": "^8.3.2",
31
- "@vue/compiler-sfc": "^3.5.40",
32
30
  "commander": "^14.0.3",
33
31
  "picocolors": "^1.1.1",
34
32
  "semver": "^7.8.1",
35
33
  "yaml": "^2.9.0"
36
34
  },
37
- "gitHead": "e6908488f65b8615cb9f40eacc8e216405a66d2c"
35
+ "gitHead": "b98ac146f0d6f7deb0834cb5d0c1f53d144d2ba2"
38
36
  }
@@ -1,10 +1,2 @@
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.
9
1
  VITE_ALIYUN_DASHSCOPE_KEY=
10
2
  VITE_DEEPSEEK_API_KEY=
@@ -1,71 +1,39 @@
1
- # **PROJECT_NAME**
1
+ # __PROJECT_NAME__
2
2
 
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.
3
+ TinyRobot AI chat starter built with Vue 3 + Vite.
4
4
 
5
- ## MCP support
5
+ ## Features
6
6
 
7
- The demo declares MCP Servers through `mcpServers`:
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
8
15
 
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
- ```
20
-
21
- `installed: true` means initially installed only. The Server remains disabled and does not connect or discover Tools until enabled.
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.
16
+ ## Setup
39
17
 
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.
18
+ 1. Copy environment variables:
41
19
 
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
20
+ ```bash
21
+ cp .env.example .env
22
+ ```
51
23
 
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:
24
+ 2. Fill your provider keys in `.env`:
53
25
 
54
26
  ```env
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=
27
+ VITE_ALIYUN_DASHSCOPE_KEY=your_dashscope_key
28
+ VITE_DEEPSEEK_API_KEY=your_deepseek_key
60
29
  ```
61
30
 
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.
31
+ `VITE_ALIYUN_DASHSCOPE_KEY` is also used by configured MCP servers that require DashScope authorization.
65
32
 
66
33
  ## Development
67
34
 
68
35
  ```bash
36
+ pnpm install
69
37
  pnpm dev
70
38
  ```
71
39
 
@@ -73,4 +41,5 @@ pnpm dev
73
41
 
74
42
  ```bash
75
43
  pnpm build
44
+ pnpm preview
76
45
  ```
@@ -2,9 +2,9 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
5
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>__PROJECT_NAME__</title>
7
+ <title>TinyRobot AI Chat</title>
8
8
  </head>
9
9
  <body>
10
10
  <div id="app"></div>
@@ -1,25 +1,29 @@
1
1
  {
2
2
  "name": "__PROJECT_NAME__",
3
3
  "private": true,
4
- "version": "0.0.0",
4
+ "version": "0.1.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "build": "vue-tsc -b && vite build",
9
- "preview": "vite preview"
9
+ "preview": "vite preview",
10
+ "type-check": "vue-tsc --noEmit"
10
11
  },
11
12
  "dependencies": {
12
- "@opentiny/tiny-robot-chat": "0.5.2-alpha.15",
13
- "@opentiny/tiny-robot": "0.5.2-alpha.15",
14
- "@opentiny/tiny-robot-svgs": "0.5.2-alpha.15",
15
- "vue": "^3.5.40"
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"
16
20
  },
17
21
  "devDependencies": {
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"
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"
24
28
  }
25
29
  }