@opentiny/tiny-robot-cli 0.4.2-alpha.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 (36) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +21 -0
  3. package/bin/cli.js +187 -0
  4. package/package.json +37 -0
  5. package/templates/basic/.env.example +2 -0
  6. package/templates/basic/README.md +45 -0
  7. package/templates/basic/index.html +13 -0
  8. package/templates/basic/package.json +29 -0
  9. package/templates/basic/public/favicon.ico +0 -0
  10. package/templates/basic/public/modelcontextprotocol.png +0 -0
  11. package/templates/basic/src/App.vue +130 -0
  12. package/templates/basic/src/components/ChatList.vue +82 -0
  13. package/templates/basic/src/components/ChatSender.vue +125 -0
  14. package/templates/basic/src/components/ConversationHistory.vue +136 -0
  15. package/templates/basic/src/components/HistoryDrawerButton.vue +43 -0
  16. package/templates/basic/src/components/McpServerPickerButton.vue +278 -0
  17. package/templates/basic/src/components/ThemeToggleButton.vue +44 -0
  18. package/templates/basic/src/components/icons/IconDeepThink.vue +29 -0
  19. package/templates/basic/src/components/icons/IconModelAliyunBailian.vue +51 -0
  20. package/templates/basic/src/components/icons/IconModelDeepseek.vue +29 -0
  21. package/templates/basic/src/components/icons/IconMoon.vue +29 -0
  22. package/templates/basic/src/components/icons/IconPlugin.vue +29 -0
  23. package/templates/basic/src/components/icons/IconSun.vue +35 -0
  24. package/templates/basic/src/components/icons/IconWebSearch.vue +36 -0
  25. package/templates/basic/src/components/icons/index.ts +7 -0
  26. package/templates/basic/src/composables/useChat.ts +129 -0
  27. package/templates/basic/src/composables/useMcp.ts +170 -0
  28. package/templates/basic/src/composables/useModel.ts +82 -0
  29. package/templates/basic/src/main.ts +7 -0
  30. package/templates/basic/src/mcpServers.ts +40 -0
  31. package/templates/basic/src/models.ts +81 -0
  32. package/templates/basic/src/style.css +21 -0
  33. package/templates/basic/tsconfig.app.json +16 -0
  34. package/templates/basic/tsconfig.json +7 -0
  35. package/templates/basic/tsconfig.node.json +26 -0
  36. package/templates/basic/vite.config.ts +16 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 - present OpenTiny Authors.
4
+ Copyright (c) 2025 - present Huawei Cloud Computing Technologies Co., Ltd.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @opentiny/tiny-robot-cli
2
+
3
+ A lightweight CLI for scaffolding TinyRobot-based product projects.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx @opentiny/tiny-robot-cli create my-app
9
+ pnpm dlx @opentiny/tiny-robot-cli create my-app
10
+ ```
11
+
12
+ ## Options
13
+
14
+ - `-t, --template <name>`: template name, currently supports `basic`
15
+ - `-h, --help`: show help
16
+
17
+ ## Template Documentation
18
+
19
+ Template-specific features and environment variables are documented in each template directory, for example:
20
+
21
+ - `packages/cli/templates/basic/README.md`
package/bin/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { input, select } from '@inquirer/prompts'
8
+ import { Command } from 'commander'
9
+
10
+ const TEMPLATE_PLACEHOLDER = '__PROJECT_NAME__'
11
+ const DEFAULT_TEMPLATE = 'basic'
12
+ const DEFAULT_PROJECT_NAME = 'tiny-robot-app'
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = path.dirname(__filename)
15
+ const templatesRoot = path.resolve(__dirname, '../templates')
16
+
17
+ function getAvailableTemplates() {
18
+ if (!fs.existsSync(templatesRoot)) {
19
+ return []
20
+ }
21
+
22
+ return fs
23
+ .readdirSync(templatesRoot, { withFileTypes: true })
24
+ .filter((entry) => entry.isDirectory())
25
+ .map((entry) => entry.name)
26
+ }
27
+
28
+ function validateProjectName(name) {
29
+ // Keep project naming rules strict for npm package compatibility.
30
+ const npmSafePattern = /^[a-z0-9-]+$/
31
+ return npmSafePattern.test(name)
32
+ }
33
+
34
+ function getTemplateDir(templateName) {
35
+ return path.join(templatesRoot, templateName)
36
+ }
37
+
38
+ async function resolveProjectName(initialProjectName, skipPrompts) {
39
+ if (initialProjectName) {
40
+ return initialProjectName
41
+ }
42
+
43
+ if (skipPrompts) {
44
+ return DEFAULT_PROJECT_NAME
45
+ }
46
+
47
+ return input({
48
+ message: 'Project name:',
49
+ default: DEFAULT_PROJECT_NAME,
50
+ validate: (value) => {
51
+ if (!value) {
52
+ return 'Project name is required.'
53
+ }
54
+ if (!validateProjectName(value)) {
55
+ return 'Project name can only contain lowercase letters, numbers, and dashes.'
56
+ }
57
+ const targetDir = path.resolve(process.cwd(), value)
58
+ if (fs.existsSync(targetDir)) {
59
+ return `Target directory already exists: ${targetDir}`
60
+ }
61
+ return true
62
+ },
63
+ })
64
+ }
65
+
66
+ async function resolveTemplateName(initialTemplateName, availableTemplates, skipPrompts) {
67
+ if (initialTemplateName) {
68
+ return initialTemplateName
69
+ }
70
+
71
+ if (skipPrompts) {
72
+ return availableTemplates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : availableTemplates[0]
73
+ }
74
+
75
+ return select({
76
+ message: 'Template:',
77
+ default: availableTemplates.includes(DEFAULT_TEMPLATE) ? DEFAULT_TEMPLATE : availableTemplates[0],
78
+ choices: availableTemplates.map((templateName) => ({
79
+ name: templateName,
80
+ value: templateName,
81
+ })),
82
+ })
83
+ }
84
+
85
+ function copyTemplate(sourceDir, targetDir) {
86
+ fs.cpSync(sourceDir, targetDir, {
87
+ recursive: true,
88
+ filter: (source) => {
89
+ const name = path.basename(source)
90
+ // Ignore local build artifacts to keep generated projects clean.
91
+ return !['node_modules', '.git', 'dist', '.DS_Store', '.vite'].includes(name)
92
+ },
93
+ })
94
+ }
95
+
96
+ function renameSpecialFiles(targetDir) {
97
+ const from = path.join(targetDir, '_gitignore')
98
+ const to = path.join(targetDir, '.gitignore')
99
+
100
+ if (fs.existsSync(from)) {
101
+ fs.renameSync(from, to)
102
+ }
103
+ }
104
+
105
+ function replaceProjectName(targetDir, projectName) {
106
+ const filesToReplace = ['package.json', 'README.md']
107
+
108
+ for (const relativeFile of filesToReplace) {
109
+ const absoluteFile = path.join(targetDir, relativeFile)
110
+
111
+ if (!fs.existsSync(absoluteFile)) {
112
+ continue
113
+ }
114
+
115
+ const content = fs.readFileSync(absoluteFile, 'utf-8')
116
+ const nextContent = content.replaceAll(TEMPLATE_PLACEHOLDER, projectName)
117
+ fs.writeFileSync(absoluteFile, nextContent, 'utf-8')
118
+ }
119
+ }
120
+
121
+ async function createProject(initialProjectName, initialTemplateName, skipPrompts) {
122
+ const availableTemplates = getAvailableTemplates()
123
+ if (availableTemplates.length === 0) {
124
+ console.error('Error: no templates found.')
125
+ process.exit(1)
126
+ }
127
+
128
+ const projectName = await resolveProjectName(initialProjectName, skipPrompts)
129
+ const templateName = await resolveTemplateName(initialTemplateName, availableTemplates, skipPrompts)
130
+
131
+ if (!validateProjectName(projectName)) {
132
+ console.error('Error: project name can only contain lowercase letters, numbers, and dashes.')
133
+ process.exit(1)
134
+ }
135
+
136
+ const templateDir = getTemplateDir(templateName)
137
+ const targetDir = path.resolve(process.cwd(), projectName)
138
+
139
+ if (!fs.existsSync(templateDir)) {
140
+ console.error(`Error: template "${templateName}" does not exist. Available: ${availableTemplates.join(', ')}`)
141
+ process.exit(1)
142
+ }
143
+
144
+ if (fs.existsSync(targetDir)) {
145
+ console.error(`Error: target directory already exists: ${targetDir}`)
146
+ process.exit(1)
147
+ }
148
+
149
+ copyTemplate(templateDir, targetDir)
150
+ renameSpecialFiles(targetDir)
151
+ replaceProjectName(targetDir, projectName)
152
+
153
+ console.log('\nProject created successfully!')
154
+ console.log(`\nNext steps:`)
155
+ console.log(` cd ${projectName}`)
156
+ console.log(' pnpm install')
157
+ console.log(' pnpm dev\n')
158
+ }
159
+
160
+ function run() {
161
+ const program = new Command()
162
+ program
163
+ .name('tiny-robot-cli')
164
+ .description('CLI to scaffold TinyRobot product projects')
165
+ .showHelpAfterError()
166
+
167
+ program
168
+ .command('create [project-name]')
169
+ .description('Create a TinyRobot project from template')
170
+ .option('-t, --template <name>', 'template name')
171
+ .action((projectName, options) => {
172
+ const skipPrompts = !process.stdout.isTTY
173
+ createProject(projectName ?? '', options.template ?? '', skipPrompts).catch((error) => {
174
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`)
175
+ process.exit(1)
176
+ })
177
+ })
178
+
179
+ if (process.argv.length <= 2) {
180
+ program.outputHelp()
181
+ return
182
+ }
183
+
184
+ program.parse(process.argv)
185
+ }
186
+
187
+ run()
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@opentiny/tiny-robot-cli",
3
+ "version": "0.4.2-alpha.0",
4
+ "description": "CLI to scaffold TinyRobot product projects",
5
+ "type": "module",
6
+ "homepage": "https://docs.opentiny.design/tiny-robot/",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/opentiny/tiny-robot.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/opentiny/tiny-robot/issues"
13
+ },
14
+ "bin": {
15
+ "tiny-robot-cli": "./bin/cli.js"
16
+ },
17
+ "files": [
18
+ "bin",
19
+ "templates",
20
+ "README.md"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "start": "node ./bin/cli.js"
27
+ },
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=20.13.0"
31
+ },
32
+ "dependencies": {
33
+ "@inquirer/prompts": "^8.3.2",
34
+ "commander": "^14.0.3"
35
+ },
36
+ "gitHead": "9d5703d90bf641988103e5a665b63fcaeb1f9394"
37
+ }
@@ -0,0 +1,2 @@
1
+ VITE_ALIYUN_DASHSCOPE_KEY=
2
+ VITE_DEEPSEEK_API_KEY=
@@ -0,0 +1,45 @@
1
+ # __PROJECT_NAME__
2
+
3
+ TinyRobot AI chat starter built with Vue 3 + Vite.
4
+
5
+ ## Features
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
15
+
16
+ ## Setup
17
+
18
+ 1. Copy environment variables:
19
+
20
+ ```bash
21
+ cp .env.example .env
22
+ ```
23
+
24
+ 2. Fill your provider keys in `.env`:
25
+
26
+ ```env
27
+ VITE_ALIYUN_DASHSCOPE_KEY=your_dashscope_key
28
+ VITE_DEEPSEEK_API_KEY=your_deepseek_key
29
+ ```
30
+
31
+ `VITE_ALIYUN_DASHSCOPE_KEY` is also used by configured MCP servers that require DashScope authorization.
32
+
33
+ ## Development
34
+
35
+ ```bash
36
+ pnpm install
37
+ pnpm dev
38
+ ```
39
+
40
+ ## Build
41
+
42
+ ```bash
43
+ pnpm build
44
+ pnpm preview
45
+ ```
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/x-icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>TinyRobot AI Chat</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vue-tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "type-check": "vue-tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@modelcontextprotocol/sdk": "^1.18.1",
14
+ "@opentiny/tiny-robot": "^0.4.1",
15
+ "@opentiny/tiny-robot-kit": "^0.4.1",
16
+ "@opentiny/tiny-robot-svgs": "^0.4.1",
17
+ "dompurify": "^3.3.1",
18
+ "markdown-it": "^14.1.0",
19
+ "vue": "^3.5.30"
20
+ },
21
+ "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"
28
+ }
29
+ }
@@ -0,0 +1,130 @@
1
+ <template>
2
+ <TrTheme>
3
+ <main class="app chat-card">
4
+ <ConversationHistory />
5
+
6
+ <section class="chat-panel">
7
+ <div class="chat-panel-content">
8
+ <p v-if="!hasApiConfig" class="config-warning">
9
+ 缺少 API 配置,请在 <code>.env</code> 中设置当前模型服务商对应的 Key。
10
+ </p>
11
+ <header class="chat-header">
12
+ <HistoryDrawerButton @click="historyDrawerOpen = true" />
13
+ <h3 v-if="currentConversationTitle">{{ currentConversationTitle }}</h3>
14
+ <ThemeToggleButton v-if="isWelcomePage" />
15
+ </header>
16
+
17
+ <ChatList />
18
+
19
+ <ChatSender />
20
+ </div>
21
+ </section>
22
+ </main>
23
+ </TrTheme>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { TrThemeProvider as TrTheme } from '@opentiny/tiny-robot'
28
+ import { computed, provide, ref } from 'vue'
29
+ import ChatList from './components/ChatList.vue'
30
+ import ChatSender from './components/ChatSender.vue'
31
+ import ConversationHistory from './components/ConversationHistory.vue'
32
+ import HistoryDrawerButton from './components/HistoryDrawerButton.vue'
33
+ import ThemeToggleButton from './components/ThemeToggleButton.vue'
34
+ import { useChat } from './composables/useChat'
35
+ import { useModel } from './composables/useModel'
36
+
37
+ const { activeConversation, messages } = useChat()
38
+ const { hasApiConfig } = useModel()
39
+ const historyDrawerOpen = ref(false)
40
+ provide('historyDrawerOpen', historyDrawerOpen)
41
+
42
+ const isWelcomePage = computed(() => {
43
+ return messages.value.filter((item) => item.role !== 'system').length === 0
44
+ })
45
+
46
+ const currentConversationTitle = computed(() => {
47
+ return activeConversation.value?.title
48
+ })
49
+ </script>
50
+
51
+ <style scoped>
52
+ .app {
53
+ height: 100vh;
54
+ display: flex;
55
+ justify-content: center;
56
+ align-items: stretch;
57
+ padding: 0;
58
+ }
59
+
60
+ .chat-card {
61
+ width: 100%;
62
+ height: 100vh;
63
+ display: flex;
64
+ gap: 0;
65
+ overflow: hidden;
66
+ align-items: stretch;
67
+ flex-direction: row;
68
+ padding: 0;
69
+ margin: 0;
70
+ border-radius: 0;
71
+ background: var(--tr-container-bg-default);
72
+ box-shadow: none;
73
+ }
74
+
75
+ .chat-panel {
76
+ flex: 1;
77
+ min-width: 0;
78
+ display: flex;
79
+ padding: 12px;
80
+ }
81
+
82
+ .chat-panel-content {
83
+ width: 100%;
84
+ max-width: 980px;
85
+ margin: 0 auto;
86
+ display: flex;
87
+ flex-direction: column;
88
+ gap: 12px;
89
+ min-height: 0;
90
+ }
91
+
92
+ .chat-header h3 {
93
+ margin: 0;
94
+ flex: 1;
95
+ min-width: 0;
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ }
100
+
101
+ .chat-header {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ }
106
+
107
+ .config-warning {
108
+ margin: 0;
109
+ padding: 10px 12px;
110
+ border-radius: 8px;
111
+ background: var(--tr-color-warning-light);
112
+ color: var(--tr-color-warning);
113
+ font-size: 14px;
114
+ }
115
+
116
+ @media (max-width: 959px) {
117
+ .chat-header {
118
+ position: relative;
119
+ justify-content: center;
120
+ min-height: 32px;
121
+ }
122
+
123
+ .chat-header h3 {
124
+ width: 100%;
125
+ text-align: center;
126
+ padding: 0 48px;
127
+ flex: none;
128
+ }
129
+ }
130
+ </style>
@@ -0,0 +1,82 @@
1
+ <template>
2
+ <tr-bubble-provider :fallback-content-renderer="BubbleRenderers.Markdown">
3
+ <tr-welcome
4
+ v-if="visibleMessages.length === 0"
5
+ title="TinyRobot AI 助手"
6
+ description="您好,我是TinyRobot,您专属的 AI 智能专家"
7
+ :icon="welcomeIcon"
8
+ class="chat-list chat-welcome"
9
+ />
10
+ <tr-bubble-list
11
+ v-else
12
+ :messages="messages"
13
+ :role-configs="roles"
14
+ :auto-scroll="true"
15
+ class="chat-list"
16
+ ></tr-bubble-list>
17
+ </tr-bubble-provider>
18
+ </template>
19
+
20
+ <script setup lang="ts">
21
+ import { BubbleRenderers, TrBubbleList, TrBubbleProvider, TrWelcome, type BubbleRoleConfig } from '@opentiny/tiny-robot'
22
+ import { IconAi, IconUser } from '@opentiny/tiny-robot-svgs'
23
+ import { computed, h } from 'vue'
24
+ import { useChat } from '../composables/useChat'
25
+
26
+ const { messages } = useChat()
27
+
28
+ const aiAvatar = h(IconAi, { style: { fontSize: '28px' } })
29
+ const userAvatar = h(IconUser, { style: { fontSize: '28px' } })
30
+ const welcomeIcon = h(IconAi, { style: { fontSize: '40px' } })
31
+ const visibleMessages = computed(() => messages.value.filter((item) => item.role !== 'system'))
32
+
33
+ const roles: Record<string, BubbleRoleConfig> = {
34
+ assistant: {
35
+ placement: 'start',
36
+ avatar: aiAvatar,
37
+ },
38
+ user: {
39
+ placement: 'end',
40
+ avatar: userAvatar,
41
+ },
42
+ system: {
43
+ hidden: true,
44
+ },
45
+ }
46
+ </script>
47
+
48
+ <style scoped>
49
+ .chat-list {
50
+ flex: 1;
51
+ min-height: 0;
52
+ overflow: auto;
53
+ border-radius: 10px;
54
+ padding: 8px;
55
+ }
56
+
57
+ .chat-welcome {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ margin-bottom: 10%;
62
+
63
+ &.tr-welcome {
64
+ --title-color: var(--tr-text-primary);
65
+ --description-color: var(--tr-text-secondary);
66
+ }
67
+ }
68
+
69
+ :deep() {
70
+ [data-box-type='box'][data-role='user'] {
71
+ --tr-bubble-box-bg: var(--tr-color-primary-light);
72
+ }
73
+
74
+ [data-box-type='box']:not([data-role='user']) {
75
+ --tr-bubble-box-bg: transparent;
76
+ }
77
+
78
+ [data-type='markdown'] p {
79
+ margin: 0;
80
+ }
81
+ }
82
+ </style>