tolingcode 1.0.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.
package/QUICKSTART.md ADDED
@@ -0,0 +1,196 @@
1
+ # TolingCode 快速开始指南
2
+
3
+ ## 🎯 目标
4
+
5
+ 创建一个类似 `clawhub` 的 CLI 工具,通过 `toling.me` 托管和分发 skills 和 apps。
6
+
7
+ ## 📦 使用流程
8
+
9
+ ### 1. 用户安装 CLI
10
+
11
+ ```bash
12
+ npm install -g tolingcode@latest
13
+ # 或指定版本
14
+ npm install -g tolingcode@2026.03.06
15
+ ```
16
+
17
+ ### 2. 用户安装 Skill
18
+
19
+ ```bash
20
+ tolingcode install skills weather
21
+ tolingcode install skills weather -v 2026.03.06
22
+ ```
23
+
24
+ ### 3. 开发者发布 Skill
25
+
26
+ ```bash
27
+ # 方法 1: 使用 publish 命令(需要 API 支持)
28
+ tolingcode publish ./my-skill --type skills --name my-skill --version 2026.03.06
29
+
30
+ # 方法 2: 手动上传
31
+ tar -czf my-skill-2026.03.06.tar.gz -C ./my-skill .
32
+ scp my-skill-2026.03.06.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
33
+ # 然后更新 registry.json
34
+ ```
35
+
36
+ ## 🚀 部署步骤
37
+
38
+ ### 第一步:准备 toling.me 服务器
39
+
40
+ ```bash
41
+ # SSH 登录服务器
42
+ ssh user@toling.me
43
+
44
+ # 创建目录
45
+ sudo mkdir -p /var/www/toling.me/{packages/skills,packages/apps,api}
46
+ sudo chown -R $USER:$USER /var/www/toling.me
47
+
48
+ # 安装 Node.js (如果没有)
49
+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
50
+ sudo apt-get install -y nodejs
51
+
52
+ # 安装依赖
53
+ cd /var/www/toling.me/api
54
+ npm init -y
55
+ npm install express multer
56
+ ```
57
+
58
+ ### 第二步:部署服务端代码
59
+
60
+ ```bash
61
+ # 上传 registry.js 到服务器
62
+ scp registry.js user@toling.me:/var/www/toling.me/api/
63
+
64
+ # 创建 systemd 服务
65
+ sudo tee /etc/systemd/system/tolingcode-registry.service > /dev/null <<EOF
66
+ [Unit]
67
+ Description=TolingCode Registry Server
68
+ After=network.target
69
+
70
+ [Service]
71
+ Type=simple
72
+ User=www-data
73
+ WorkingDirectory=/var/www/toling.me/api
74
+ ExecStart=/usr/bin/node registry.js
75
+ Restart=on-failure
76
+
77
+ [Install]
78
+ WantedBy=multi-user.target
79
+ EOF
80
+
81
+ # 启动服务
82
+ sudo systemctl daemon-reload
83
+ sudo systemctl enable tolingcode-registry
84
+ sudo systemctl start tolingcode-registry
85
+ sudo systemctl status tolingcode-registry
86
+ ```
87
+
88
+ ### 第三步:配置 Nginx
89
+
90
+ ```bash
91
+ # 编辑 Nginx 配置
92
+ sudo nano /etc/nginx/sites-available/toling.me
93
+
94
+ # 添加以下内容:
95
+ server {
96
+ listen 443 ssl;
97
+ server_name toling.me;
98
+
99
+ ssl_certificate /etc/letsencrypt/live/toling.me/fullchain.pem;
100
+ ssl_certificate_key /etc/letsencrypt/live/toling.me/privkey.pem;
101
+
102
+ root /var/www/toling.me;
103
+
104
+ # API
105
+ location /api/ {
106
+ proxy_pass http://localhost:3000;
107
+ proxy_set_header Host $host;
108
+ proxy_set_header X-Real-IP $remote_addr;
109
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
110
+ }
111
+
112
+ # Packages
113
+ location /packages/ {
114
+ alias /var/www/toling.me/packages/;
115
+ autoindex on;
116
+ add_header Content-Type application/octet-stream;
117
+ }
118
+ }
119
+
120
+ # 测试并重载
121
+ sudo nginx -t
122
+ sudo systemctl reload nginx
123
+ ```
124
+
125
+ ### 第四步:发布 CLI 到 npm
126
+
127
+ ```bash
128
+ # 在本地开发机器上
129
+ cd tolingcode
130
+
131
+ # 登录 npm
132
+ npm login
133
+
134
+ # 发布
135
+ npm version 1.0.0
136
+ npm publish
137
+
138
+ # 验证
139
+ npm view tolingcode
140
+ ```
141
+
142
+ ### 第五步:发布第一个 Skill
143
+
144
+ ```bash
145
+ # 创建测试 skill
146
+ mkdir -p test-skill
147
+ echo '{"name": "test", "description": "Test skill"}' > test-skill/SKILL.md
148
+
149
+ # 发布
150
+ cd tolingcode
151
+ node bin/tolingcode.js publish ../test-skill --type skills --name test-skill --version 2026.03.06
152
+
153
+ # 或者手动上传
154
+ tar -czf test-skill-2026.03.06.tar.gz -C ../test-skill .
155
+ scp test-skill-2026.03.06.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
156
+
157
+ # 更新 registry.json (SSH 到服务器编辑)
158
+ ```
159
+
160
+ ## ✅ 测试
161
+
162
+ ```bash
163
+ # 安装 CLI
164
+ npm install -g tolingcode@latest
165
+
166
+ # 列出可用技能
167
+ tolingcode list skills
168
+
169
+ # 安装技能
170
+ tolingcode install skills test-skill
171
+
172
+ # 验证安装
173
+ ls ~/.openclaw/workspace/skills/
174
+ ```
175
+
176
+ ## 📝 注意事项
177
+
178
+ 1. **版本命名**: 使用 `YYYY.MM.DD` 格式,便于追踪发布日期
179
+ 2. **认证**: 生产环境需要添加 API 认证(API Key 或 JWT)
180
+ 3. **HTTPS**: 确保 toling.me 配置了 SSL 证书
181
+ 4. **备份**: 定期备份 registry.json 和 packages 目录
182
+ 5. **监控**: 添加日志和监控,跟踪下载量和错误
183
+
184
+ ## 🔧 后续扩展
185
+
186
+ - [ ] 添加用户认证系统
187
+ - [ ] 支持私有包(需要 token)
188
+ - [ ] 添加下载统计
189
+ - [ ] Web 管理界面
190
+ - [ ] 自动版本递增
191
+ - [ ] 依赖管理
192
+ - [ ] 包签名验证
193
+
194
+ ---
195
+
196
+ 有问题?查看 `docs/SERVER.md` 获取详细服务端文档。
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # TolingCode CLI
2
+
3
+ 通过 npm 安装和管理 TolingCode 的 skills 和 apps。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ # 安装最新版本
9
+ npm install -g tolingcode@latest
10
+
11
+ # 安装指定版本
12
+ npm install -g tolingcode@2026.02.26
13
+ ```
14
+
15
+ ## 使用
16
+
17
+ ### 安装 Skill
18
+
19
+ ```bash
20
+ # 安装最新版本
21
+ tolingcode install skills weather
22
+
23
+ # 安装指定版本
24
+ tolingcode install skills weather -v 2026.03.06
25
+ ```
26
+
27
+ ### 安装 App
28
+
29
+ ```bash
30
+ # 安装到当前目录
31
+ tolingcode install apps myapp
32
+
33
+ # 全局安装
34
+ tolingcode install apps myapp -g
35
+ ```
36
+
37
+ ### 列出可用包
38
+
39
+ ```bash
40
+ # 列出所有
41
+ tolingcode list
42
+
43
+ # 只列出 skills
44
+ tolingcode list skills
45
+
46
+ # 只列出 apps
47
+ tolingcode list apps
48
+ ```
49
+
50
+ ### 搜索
51
+
52
+ ```bash
53
+ tolingcode search weather
54
+ ```
55
+
56
+ ### 发布 (开发者)
57
+
58
+ ```bash
59
+ tolingcode publish ./my-skill --type skills --name my-skill --version 2026.03.06
60
+ ```
61
+
62
+ ## 版本命名
63
+
64
+ 推荐使用日期格式:`YYYY.MM.DD`
65
+
66
+ - `2026.02.26` - 2026 年 2 月 26 日发布的版本
67
+ - `latest` - 最新版本
68
+
69
+ ## 环境变量
70
+
71
+ ```bash
72
+ # 自定义 registry 地址(默认:https://toling.me/api/registry)
73
+ export TOLINGCODE_REGISTRY=https://registry.toling.me/api/registry
74
+
75
+ # 自定义安装路径
76
+ export OPENCLAW_WORKSPACE=/path/to/workspace
77
+ ```
78
+
79
+ ## 开发
80
+
81
+ ```bash
82
+ # 本地测试
83
+ cd tolingcode
84
+ npm link
85
+ tolingcode --help
86
+
87
+ # 发布到 npm
88
+ npm version patch # 或 minor/major
89
+ npm publish
90
+ ```
91
+
92
+ ## 目录结构
93
+
94
+ ```
95
+ tolingcode/
96
+ ├── bin/
97
+ │ └── tolingcode.js # CLI 入口
98
+ ├── docs/
99
+ │ └── SERVER.md # 服务端部署文档
100
+ ├── package.json
101
+ └── README.md
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+ const axios = require('axios');
7
+ const tar = require('tar');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execSync } = require('child_process');
11
+
12
+ const program = new Command();
13
+ const REGISTRY_URL = 'https://toling.me/api/registry';
14
+
15
+ program
16
+ .name('tolingcode')
17
+ .description('TolingCode CLI - Install skills and apps from toling.me')
18
+ .version('1.0.0');
19
+
20
+ // install command
21
+ program
22
+ .command('install <type> <name>')
23
+ .description('Install a skill or app')
24
+ .option('-v, --version <version>', 'Specify version (e.g., 2026.02.26 or latest)')
25
+ .option('-g, --global', 'Install globally (for apps)')
26
+ .action(async (type, name, options) => {
27
+ const version = options.version || 'latest';
28
+
29
+ console.log(chalk.blue(`\n🔍 Searching for ${type}: ${name}@${version}...`));
30
+
31
+ try {
32
+ // Fetch package info from registry
33
+ const spinner = ora('Fetching package info...').start();
34
+ const response = await axios.get(`${REGISTRY_URL}/${type}/${name}`, {
35
+ params: { version }
36
+ });
37
+ spinner.stop();
38
+
39
+ const pkg = response.data;
40
+ console.log(chalk.green(`✓ Found: ${pkg.name}@${pkg.version}`));
41
+ console.log(chalk.gray(` Description: ${pkg.description}`));
42
+ console.log(chalk.gray(` Download URL: ${pkg.downloadUrl}`));
43
+
44
+ // Determine install path
45
+ let installPath;
46
+ if (type === 'skills') {
47
+ const workspace = process.env.OPENCLAW_WORKSPACE || path.join(process.env.HOME || process.env.USERPROFILE, '.openclaw', 'workspace');
48
+ installPath = path.join(workspace, 'skills');
49
+ } else if (type === 'apps') {
50
+ if (options.global) {
51
+ installPath = path.join(process.env.APPDATA || path.join(process.env.HOME || '', '.local'), 'tolingcode', 'apps');
52
+ } else {
53
+ installPath = path.join(process.cwd(), 'tolingcode-apps');
54
+ }
55
+ } else {
56
+ console.log(chalk.red(`✗ Unknown type: ${type}`));
57
+ process.exit(1);
58
+ }
59
+
60
+ // Create directory if not exists
61
+ if (!fs.existsSync(installPath)) {
62
+ fs.mkdirSync(installPath, { recursive: true });
63
+ }
64
+
65
+ // Download and extract
66
+ const downloadSpinner = ora('Downloading...').start();
67
+ const tarballResponse = await axios.get(pkg.downloadUrl, {
68
+ responseType: 'stream'
69
+ });
70
+ downloadSpinner.stop();
71
+ console.log(chalk.green('✓ Downloaded'));
72
+
73
+ const extractSpinner = ora('Installing...').start();
74
+ await new Promise((resolve, reject) => {
75
+ tarballResponse.data
76
+ .pipe(tar.x({ C: installPath, strip: 1 }))
77
+ .on('end', () => {
78
+ extractSpinner.stop();
79
+ console.log(chalk.green(`✓ Installed to: ${installPath}`));
80
+ resolve();
81
+ })
82
+ .on('error', (err) => {
83
+ extractSpinner.stop();
84
+ reject(err);
85
+ });
86
+ });
87
+
88
+ console.log(chalk.green(`\n🎉 ${name} installed successfully!`));
89
+
90
+ // Show next steps
91
+ if (type === 'skills') {
92
+ console.log(chalk.yellow('\n💡 Next steps:'));
93
+ console.log(' Restart OpenClaw or reload skills to use the new skill.');
94
+ }
95
+
96
+ } catch (error) {
97
+ console.log(chalk.red(`\n✗ Error: ${error.message}`));
98
+ if (error.response?.status === 404) {
99
+ console.log(chalk.yellow(' Package not found. Check the name and version.'));
100
+ }
101
+ process.exit(1);
102
+ }
103
+ });
104
+
105
+ // list command - list available skills/apps
106
+ program
107
+ .command('list [type]')
108
+ .description('List available skills or apps')
109
+ .action(async (type) => {
110
+ console.log(chalk.blue('\n📦 Available packages:\n'));
111
+
112
+ try {
113
+ const targetType = type || 'all';
114
+ const response = await axios.get(`${REGISTRY_URL}/list`, {
115
+ params: { type: targetType }
116
+ });
117
+
118
+ const packages = response.data;
119
+ if (packages.length === 0) {
120
+ console.log(chalk.yellow(' No packages found.'));
121
+ return;
122
+ }
123
+
124
+ packages.forEach(pkg => {
125
+ console.log(chalk.green(` ${pkg.type}/${pkg.name}`));
126
+ console.log(chalk.gray(` v${pkg.version} - ${pkg.description}`));
127
+ console.log(chalk.gray(` Latest: ${pkg.latestVersion}\n`));
128
+ });
129
+
130
+ } catch (error) {
131
+ console.log(chalk.red(`✗ Error: ${error.message}`));
132
+ process.exit(1);
133
+ }
134
+ });
135
+
136
+ // search command
137
+ program
138
+ .command('search <query>')
139
+ .description('Search for skills or apps')
140
+ .action(async (query) => {
141
+ console.log(chalk.blue(`\n🔍 Searching for: ${query}\n`));
142
+
143
+ try {
144
+ const response = await axios.get(`${REGISTRY_URL}/search`, {
145
+ params: { q: query }
146
+ });
147
+
148
+ const results = response.data;
149
+ if (results.length === 0) {
150
+ console.log(chalk.yellow(' No results found.'));
151
+ return;
152
+ }
153
+
154
+ results.forEach(pkg => {
155
+ console.log(chalk.green(` ${pkg.type}/${pkg.name}`));
156
+ console.log(chalk.gray(` v${pkg.version} - ${pkg.description}\n`));
157
+ });
158
+
159
+ } catch (error) {
160
+ console.log(chalk.red(`✗ Error: ${error.message}`));
161
+ process.exit(1);
162
+ }
163
+ });
164
+
165
+ // publish command - for publishing skills/apps to registry
166
+ program
167
+ .command('publish <path>')
168
+ .description('Publish a skill or app to the registry')
169
+ .option('--type <type>', 'Package type (skills or apps)', 'skills')
170
+ .option('--name <name>', 'Package name')
171
+ .option('--version <version>', 'Package version')
172
+ .action(async (pkgPath, options) => {
173
+ const absPath = path.resolve(pkgPath);
174
+
175
+ if (!fs.existsSync(absPath)) {
176
+ console.log(chalk.red(`✗ Path not found: ${absPath}`));
177
+ process.exit(1);
178
+ }
179
+
180
+ console.log(chalk.blue(`\n📤 Publishing ${options.type}: ${options.name || '(auto)'}...`));
181
+
182
+ // Read package.json if exists
183
+ let pkgInfo = {};
184
+ const pkgJsonPath = path.join(absPath, 'package.json');
185
+ if (fs.existsSync(pkgJsonPath)) {
186
+ pkgInfo = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
187
+ }
188
+
189
+ const name = options.name || pkgInfo.name || path.basename(absPath);
190
+ const version = options.version || pkgInfo.version || new Date().toISOString().split('T')[0].replace(/-/g, '.');
191
+
192
+ console.log(chalk.gray(` Name: ${name}`));
193
+ console.log(chalk.gray(` Version: ${version}`));
194
+ console.log(chalk.gray(` Type: ${options.type}`));
195
+
196
+ // TODO: Implement actual publish logic
197
+ // This would:
198
+ // 1. Create tarball
199
+ // 2. Upload to toling.me server
200
+ // 3. Update registry index
201
+
202
+ console.log(chalk.yellow('\n⚠️ Publish endpoint not implemented yet.'));
203
+ console.log(chalk.gray(' You need to implement the server-side API on toling.me'));
204
+ console.log(chalk.gray(' See: docs/PUBLISH.md for details'));
205
+ });
206
+
207
+ program.parse();
package/docs/SERVER.md ADDED
@@ -0,0 +1,253 @@
1
+ # TolingCode Registry Server
2
+
3
+ 这是在 `toling.me` 上需要部署的服务端代码,用于托管 skill 和 app 包。
4
+
5
+ ## 目录结构
6
+
7
+ ```
8
+ /var/www/toling.me/
9
+ ├── api/
10
+ │ └── registry/ # Registry API
11
+ │ ├── index.php # 或 node.js 入口
12
+ │ └── packages/ # 包存储目录
13
+ │ ├── skills/
14
+ │ └── apps/
15
+ ├── packages/ # 下载的 tarball 存储
16
+ │ ├── skills/
17
+ │ └── apps/
18
+ └── registry.json # 包索引(或用数据库)
19
+ ```
20
+
21
+ ## API 端点
22
+
23
+ ### 1. 获取包信息
24
+ ```
25
+ GET /api/registry/{type}/{name}?version=latest
26
+ ```
27
+
28
+ 响应:
29
+ ```json
30
+ {
31
+ "name": "weather",
32
+ "type": "skills",
33
+ "version": "2026.03.06",
34
+ "description": "Get weather via wttr.in",
35
+ "downloadUrl": "https://toling.me/packages/skills/weather-2026.03.06.tar.gz",
36
+ "latestVersion": "2026.03.06",
37
+ "versions": ["2026.03.06", "2026.03.01", "2026.02.26"]
38
+ }
39
+ ```
40
+
41
+ ### 2. 列出所有包
42
+ ```
43
+ GET /api/registry/list?type=skills
44
+ ```
45
+
46
+ 响应:
47
+ ```json
48
+ [
49
+ {
50
+ "name": "weather",
51
+ "type": "skills",
52
+ "version": "2026.03.06",
53
+ "description": "Get weather via wttr.in",
54
+ "latestVersion": "2026.03.06"
55
+ }
56
+ ]
57
+ ```
58
+
59
+ ### 3. 搜索
60
+ ```
61
+ GET /api/registry/search?q=weather
62
+ ```
63
+
64
+ ### 4. 下载 tarball
65
+ ```
66
+ GET /packages/skills/weather-2026.03.06.tar.gz
67
+ ```
68
+
69
+ ## 简单实现 (Node.js + Express)
70
+
71
+ ```javascript
72
+ // api/registry/server.js
73
+ const express = require('express');
74
+ const fs = require('fs');
75
+ const path = require('path');
76
+ const app = express();
77
+
78
+ const REGISTRY_FILE = '/var/www/toling.me/registry.json';
79
+ const PACKAGES_DIR = '/var/www/toling.me/packages';
80
+
81
+ // 读取注册表
82
+ function getRegistry() {
83
+ if (fs.existsSync(REGISTRY_FILE)) {
84
+ return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
85
+ }
86
+ return { skills: {}, apps: {} };
87
+ }
88
+
89
+ // 获取包信息
90
+ app.get('/api/registry/:type/:name', (req, res) => {
91
+ const { type, name } = req.params;
92
+ const { version = 'latest' } = req.query;
93
+
94
+ const registry = getRegistry();
95
+ const pkg = registry[type]?.[name];
96
+
97
+ if (!pkg) {
98
+ return res.status(404).json({ error: 'Package not found' });
99
+ }
100
+
101
+ const ver = version === 'latest' ? pkg.latestVersion : version;
102
+ const verInfo = pkg.versions?.[ver];
103
+
104
+ if (!verInfo) {
105
+ return res.status(404).json({ error: 'Version not found' });
106
+ }
107
+
108
+ res.json({
109
+ name,
110
+ type,
111
+ version: ver,
112
+ description: pkg.description,
113
+ downloadUrl: `https://toling.me/packages/${type}/${name}-${ver}.tar.gz`,
114
+ latestVersion: pkg.latestVersion,
115
+ versions: Object.keys(pkg.versions || {})
116
+ });
117
+ });
118
+
119
+ // 列出包
120
+ app.get('/api/registry/list', (req, res) => {
121
+ const { type = 'all' } = req.query;
122
+ const registry = getRegistry();
123
+ const result = [];
124
+
125
+ const types = type === 'all' ? ['skills', 'apps'] : [type];
126
+
127
+ types.forEach(t => {
128
+ if (registry[t]) {
129
+ Object.entries(registry[t]).forEach(([name, pkg]) => {
130
+ result.push({
131
+ name,
132
+ type: t,
133
+ version: pkg.latestVersion,
134
+ description: pkg.description,
135
+ latestVersion: pkg.latestVersion
136
+ });
137
+ });
138
+ }
139
+ });
140
+
141
+ res.json(result);
142
+ });
143
+
144
+ // 搜索
145
+ app.get('/api/registry/search', (req, res) => {
146
+ const { q } = req.query;
147
+ const registry = getRegistry();
148
+ const result = [];
149
+
150
+ ['skills', 'apps'].forEach(type => {
151
+ if (registry[type]) {
152
+ Object.entries(registry[type]).forEach(([name, pkg]) => {
153
+ if (name.toLowerCase().includes(q.toLowerCase()) ||
154
+ pkg.description?.toLowerCase().includes(q.toLowerCase())) {
155
+ result.push({
156
+ name,
157
+ type,
158
+ version: pkg.latestVersion,
159
+ description: pkg.description
160
+ });
161
+ }
162
+ });
163
+ }
164
+ });
165
+
166
+ res.json(result);
167
+ });
168
+
169
+ // 提供静态文件(tarball 下载)
170
+ app.use('/packages', express.static(PACKAGES_DIR));
171
+
172
+ app.listen(3000, () => {
173
+ console.log('TolingCode Registry running on port 3000');
174
+ });
175
+ ```
176
+
177
+ ## 注册表格式 (registry.json)
178
+
179
+ ```json
180
+ {
181
+ "skills": {
182
+ "weather": {
183
+ "description": "Get weather via wttr.in",
184
+ "latestVersion": "2026.03.06",
185
+ "versions": {
186
+ "2026.03.06": {
187
+ "publishedAt": "2026-03-06T10:00:00Z",
188
+ "hash": "sha256:abc123..."
189
+ },
190
+ "2026.03.01": {
191
+ "publishedAt": "2026-03-01T10:00:00Z",
192
+ "hash": "sha256:def456..."
193
+ }
194
+ }
195
+ }
196
+ },
197
+ "apps": {}
198
+ }
199
+ ```
200
+
201
+ ## 发布脚本示例
202
+
203
+ ```bash
204
+ #!/bin/bash
205
+ # publish.sh
206
+
207
+ SKILL_PATH=$1
208
+ NAME=$2
209
+ VERSION=${3:-$(date +%Y.%m.%d)}
210
+
211
+ # 创建 tarball
212
+ tar -czf ${NAME}-${VERSION}.tar.gz -C $(dirname $SKILL_PATH) $(basename $SKILL_PATH)
213
+
214
+ # 上传到服务器
215
+ scp ${NAME}-${VERSION}.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
216
+
217
+ # 更新 registry.json (需要 API 或 SSH)
218
+ # 或者通过 API 发布
219
+ curl -X POST https://toling.me/api/registry/publish \
220
+ -F "type=skills" \
221
+ -F "name=${NAME}" \
222
+ -F "version=${VERSION}" \
223
+ -F "package=@${NAME}-${VERSION}.tar.gz"
224
+
225
+ echo "Published ${NAME}@${VERSION}"
226
+ ```
227
+
228
+ ## Nginx 配置
229
+
230
+ ```nginx
231
+ server {
232
+ listen 443 ssl;
233
+ server_name toling.me;
234
+
235
+ ssl_certificate /etc/letsencrypt/live/toling.me/fullchain.pem;
236
+ ssl_certificate_key /etc/letsencrypt/live/toling.me/privkey.pem;
237
+
238
+ root /var/www/toling.me;
239
+
240
+ # API
241
+ location /api/ {
242
+ proxy_pass http://localhost:3000;
243
+ proxy_set_header Host $host;
244
+ proxy_set_header X-Real-IP $remote_addr;
245
+ }
246
+
247
+ # Packages
248
+ location /packages/ {
249
+ alias /var/www/toling.me/packages/;
250
+ autoindex on;
251
+ }
252
+ }
253
+ ```
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "tolingcode",
3
+ "version": "1.0.0",
4
+ "description": "TolingCode CLI - Install skills and apps from toling.me",
5
+ "main": "bin/tolingcode.js",
6
+ "bin": {
7
+ "tolingcode": "bin/tolingcode.js",
8
+ "tlc": "bin/tolingcode.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1",
12
+ "publish": "npm publish"
13
+ },
14
+ "keywords": [
15
+ "tolingcode",
16
+ "openclaw",
17
+ "skills",
18
+ "cli"
19
+ ],
20
+ "author": "TolingCode",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "commander": "^11.0.0",
24
+ "chalk": "^4.1.2",
25
+ "ora": "^5.4.1",
26
+ "axios": "^1.6.0",
27
+ "tar": "^6.2.0"
28
+ },
29
+ "engines": {
30
+ "node": ">=16.0.0"
31
+ }
32
+ }
@@ -0,0 +1,30 @@
1
+ @echo off
2
+ REM TolingCode Publish Script (Windows)
3
+ REM Usage: publish.bat <skill-path> <name> [version]
4
+
5
+ SET SKILL_PATH=%1
6
+ SET NAME=%2
7
+ SET VERSION=%3
8
+
9
+ IF "%VERSION%"=="" (
10
+ REM Use today's date as version
11
+ FOR /F "tokens=2-4 delims=/ " %%A IN ('date /T') DO (SET VERSION=%%C.%%B.%%A)
12
+ )
13
+
14
+ ECHO Publishing %NAME%@%VERSION%...
15
+
16
+ REM Create tarball (requires tar.exe on Windows 10+)
17
+ tar -czf %NAME%-%VERSION%.tar.gz -C %SKILL_PATH% .
18
+
19
+ ECHO Created: %NAME%-%VERSION%.tar.gz
20
+ ECHO.
21
+ ECHO Next steps:
22
+ ECHO 1. Upload to toling.me: scp %NAME%-%VERSION%.tar.gz user@toling.me:/var/www/toling.me/packages/skills/
23
+ ECHO 2. Update registry.json on server
24
+ ECHO.
25
+ ECHO Or use the API (when implemented):
26
+ ECHO curl -X POST https://toling.me/api/registry/publish ^
27
+ ECHO -F "type=skills" ^
28
+ ECHO -F "name=%NAME%" ^
29
+ ECHO -F "version=%VERSION%" ^
30
+ ECHO -F "package=@%NAME%-%VERSION%.tar.gz"
@@ -0,0 +1,220 @@
1
+ // TolingCode Registry Server - Simple Node.js Implementation
2
+ // Deploy this on toling.me
3
+
4
+ const express = require('express');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const multer = require('multer');
8
+ const crypto = require('crypto');
9
+
10
+ const app = express();
11
+ const PORT = process.env.PORT || 3000;
12
+
13
+ // Paths
14
+ const BASE_DIR = process.env.REGISTRY_BASE_DIR || '/var/www/toling.me';
15
+ const REGISTRY_FILE = path.join(BASE_DIR, 'registry.json');
16
+ const PACKAGES_DIR = path.join(BASE_DIR, 'packages');
17
+
18
+ // Ensure directories exist
19
+ if (!fs.existsSync(PACKAGES_DIR)) {
20
+ fs.mkdirSync(path.join(PACKAGES_DIR, 'skills'), { recursive: true });
21
+ fs.mkdirSync(path.join(PACKAGES_DIR, 'apps'), { recursive: true });
22
+ }
23
+
24
+ // Initialize registry if not exists
25
+ if (!fs.existsSync(REGISTRY_FILE)) {
26
+ fs.writeFileSync(REGISTRY_FILE, JSON.stringify({ skills: {}, apps: {} }, null, 2));
27
+ }
28
+
29
+ // Middleware
30
+ app.use(express.json());
31
+ const upload = multer({ dest: path.join(BASE_DIR, 'uploads') });
32
+
33
+ // Helper: Read registry
34
+ function getRegistry() {
35
+ return JSON.parse(fs.readFileSync(REGISTRY_FILE, 'utf-8'));
36
+ }
37
+
38
+ // Helper: Write registry
39
+ function saveRegistry(data) {
40
+ fs.writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2));
41
+ }
42
+
43
+ // Helper: Calculate file hash
44
+ function hashFile(filePath) {
45
+ const content = fs.readFileSync(filePath);
46
+ return crypto.createHash('sha256').update(content).digest('hex');
47
+ }
48
+
49
+ // GET /api/registry/:type/:name - Get package info
50
+ app.get('/api/registry/:type/:name', (req, res) => {
51
+ const { type, name } = req.params;
52
+ const { version = 'latest' } = req.query;
53
+
54
+ if (!['skills', 'apps'].includes(type)) {
55
+ return res.status(400).json({ error: 'Invalid type. Use "skills" or "apps"' });
56
+ }
57
+
58
+ const registry = getRegistry();
59
+ const pkg = registry[type]?.[name];
60
+
61
+ if (!pkg) {
62
+ return res.status(404).json({ error: `Package ${name} not found` });
63
+ }
64
+
65
+ const ver = version === 'latest' ? pkg.latestVersion : version;
66
+ const verInfo = pkg.versions?.[ver];
67
+
68
+ if (!verInfo) {
69
+ return res.status(404).json({ error: `Version ${ver} not found` });
70
+ }
71
+
72
+ res.json({
73
+ name,
74
+ type,
75
+ version: ver,
76
+ description: pkg.description || '',
77
+ downloadUrl: `https://toling.me/packages/${type}/${name}-${ver}.tar.gz`,
78
+ latestVersion: pkg.latestVersion,
79
+ versions: Object.keys(pkg.versions || {}).sort().reverse()
80
+ });
81
+ });
82
+
83
+ // GET /api/registry/list - List all packages
84
+ app.get('/api/registry/list', (req, res) => {
85
+ const { type = 'all' } = req.query;
86
+ const registry = getRegistry();
87
+ const result = [];
88
+
89
+ const types = type === 'all' ? ['skills', 'apps'] : [type];
90
+
91
+ types.forEach(t => {
92
+ if (registry[t]) {
93
+ Object.entries(registry[t]).forEach(([name, pkg]) => {
94
+ result.push({
95
+ name,
96
+ type: t,
97
+ version: pkg.latestVersion,
98
+ description: pkg.description || '',
99
+ latestVersion: pkg.latestVersion
100
+ });
101
+ });
102
+ }
103
+ });
104
+
105
+ res.json(result);
106
+ });
107
+
108
+ // GET /api/registry/search - Search packages
109
+ app.get('/api/registry/search', (req, res) => {
110
+ const { q } = req.query;
111
+ if (!q) {
112
+ return res.status(400).json({ error: 'Query parameter "q" is required' });
113
+ }
114
+
115
+ const registry = getRegistry();
116
+ const result = [];
117
+ const query = q.toLowerCase();
118
+
119
+ ['skills', 'apps'].forEach(type => {
120
+ if (registry[type]) {
121
+ Object.entries(registry[type]).forEach(([name, pkg]) => {
122
+ if (name.toLowerCase().includes(query) ||
123
+ (pkg.description && pkg.description.toLowerCase().includes(query))) {
124
+ result.push({
125
+ name,
126
+ type,
127
+ version: pkg.latestVersion,
128
+ description: pkg.description || ''
129
+ });
130
+ }
131
+ });
132
+ }
133
+ });
134
+
135
+ res.json(result);
136
+ });
137
+
138
+ // POST /api/registry/publish - Publish a package (requires auth)
139
+ app.post('/api/registry/publish', upload.single('package'), (req, res) => {
140
+ // TODO: Add authentication (API key, JWT, etc.)
141
+ const { type, name, version } = req.body;
142
+
143
+ if (!type || !name || !version) {
144
+ return res.status(400).json({ error: 'Missing required fields: type, name, version' });
145
+ }
146
+
147
+ if (!['skills', 'apps'].includes(type)) {
148
+ return res.status(400).json({ error: 'Invalid type. Use "skills" or "apps"' });
149
+ }
150
+
151
+ if (!req.file) {
152
+ return res.status(400).json({ error: 'No package file uploaded' });
153
+ }
154
+
155
+ const registry = getRegistry();
156
+
157
+ // Initialize type if not exists
158
+ if (!registry[type]) {
159
+ registry[type] = {};
160
+ }
161
+
162
+ // Initialize package if not exists
163
+ if (!registry[type][name]) {
164
+ registry[type][name] = {
165
+ description: req.body.description || '',
166
+ latestVersion: version,
167
+ versions: {}
168
+ };
169
+ }
170
+
171
+ // Add version
172
+ const fileHash = hashFile(req.file.path);
173
+ registry[type][name].versions[version] = {
174
+ publishedAt: new Date().toISOString(),
175
+ hash: `sha256:${fileHash}`,
176
+ size: req.file.size
177
+ };
178
+
179
+ // Update latest version
180
+ registry[type][name].latestVersion = version;
181
+
182
+ // Move file to packages directory
183
+ const destDir = path.join(PACKAGES_DIR, type);
184
+ const destFile = path.join(destDir, `${name}-${version}.tar.gz`);
185
+
186
+ if (!fs.existsSync(destDir)) {
187
+ fs.mkdirSync(destDir, { recursive: true });
188
+ }
189
+
190
+ fs.renameSync(req.file.path, destFile);
191
+
192
+ // Save registry
193
+ saveRegistry(registry);
194
+
195
+ // Cleanup uploads dir
196
+ try {
197
+ fs.unlinkSync(req.file.path);
198
+ } catch (e) {}
199
+
200
+ res.json({
201
+ success: true,
202
+ name,
203
+ type,
204
+ version,
205
+ downloadUrl: `https://toling.me/packages/${type}/${name}-${version}.tar.gz`
206
+ });
207
+ });
208
+
209
+ // Serve static packages
210
+ app.use('/packages', express.static(path.join(BASE_DIR, 'packages')));
211
+
212
+ // Health check
213
+ app.get('/health', (req, res) => {
214
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
215
+ });
216
+
217
+ app.listen(PORT, () => {
218
+ console.log(`TolingCode Registry Server running on port ${PORT}`);
219
+ console.log(`Base directory: ${BASE_DIR}`);
220
+ });