yuanflow-cli 0.1.0 → 0.1.1

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 (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +34 -50
  3. package/bin/yuanflow-cli.js +26 -0
  4. package/bin/yuanflow-skill.cjs +334 -0
  5. package/generated/registry.json +24641 -0
  6. package/lib/skill-installer/agents.cjs +203 -0
  7. package/lib/skill-installer/discover-skills.cjs +79 -0
  8. package/lib/skill-installer/installer.cjs +300 -0
  9. package/lib/skill-installer/publish.cjs +53 -0
  10. package/lib/skill-installer/repo-source.cjs +157 -0
  11. package/package.json +56 -6
  12. package/scripts/generate-registry.js +174 -0
  13. package/src/agent-protocol.js +169 -0
  14. package/src/cli.js +382 -0
  15. package/src/config.js +31 -0
  16. package/src/registry.js +45 -0
  17. package/src/request.js +97 -0
  18. package/src/shortcuts.js +346 -0
  19. package/cli.js +0 -3
  20. package/src/ycloud/cli.js +0 -29
  21. package/src/ycloud/commands/analysis.js +0 -82
  22. package/src/ycloud/commands/auth.js +0 -191
  23. package/src/ycloud/commands/commands.js +0 -262
  24. package/src/ycloud/commands/compliance.js +0 -146
  25. package/src/ycloud/commands/config.js +0 -103
  26. package/src/ycloud/commands/health.js +0 -35
  27. package/src/ycloud/commands/index.js +0 -381
  28. package/src/ycloud/commands/kb.js +0 -82
  29. package/src/ycloud/commands/schema.js +0 -229
  30. package/src/ycloud/commands/shared.js +0 -30
  31. package/src/ycloud/commands/tool-registry.js +0 -209
  32. package/src/ycloud/commands/tool-runner.js +0 -226
  33. package/src/ycloud/commands/tool.js +0 -178
  34. package/src/ycloud/commands/version.js +0 -84
  35. package/src/ycloud/core/config.js +0 -78
  36. package/src/ycloud/core/http.js +0 -133
  37. package/src/ycloud/core/token.js +0 -30
  38. package/src/ycloud/resources/.gitkeep +0 -1
  39. package/src/ycloud/resources/tool_catalog_full.json +0 -1
@@ -0,0 +1,157 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { execFileSync } = require('node:child_process');
5
+
6
+ function readPackageJson(packageRoot) {
7
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
8
+ }
9
+
10
+ function readRepositoryConfig(packageRoot) {
11
+ const packageJson = readPackageJson(packageRoot);
12
+ const skillRepo = packageJson.yuanflowSkill && packageJson.yuanflowSkill.skillRepo;
13
+
14
+ if (!skillRepo || !skillRepo.owner || !skillRepo.repo || !skillRepo.ref) {
15
+ throw new Error('package.json 缺少 yuanflowSkill.skillRepo 配置。');
16
+ }
17
+
18
+ return {
19
+ owner: skillRepo.owner,
20
+ repo: skillRepo.repo,
21
+ ref: skillRepo.ref,
22
+ };
23
+ }
24
+
25
+ function buildArchiveUrl(repoConfig) {
26
+ if (process.env.YUANFLOW_SKILL_ARCHIVE_URL) {
27
+ return process.env.YUANFLOW_SKILL_ARCHIVE_URL;
28
+ }
29
+
30
+ return `https://codeload.github.com/${repoConfig.owner}/${repoConfig.repo}/tar.gz/refs/heads/${repoConfig.ref}`;
31
+ }
32
+
33
+ function buildGitUrl(repoConfig) {
34
+ if (process.env.YUANFLOW_SKILL_GIT_URL) {
35
+ return process.env.YUANFLOW_SKILL_GIT_URL;
36
+ }
37
+
38
+ return `https://github.com/${repoConfig.owner}/${repoConfig.repo}.git`;
39
+ }
40
+
41
+ async function downloadArchive({ url, targetFile }) {
42
+ const response = await fetch(url, {
43
+ headers: {
44
+ 'user-agent': 'yuanflow-cli',
45
+ },
46
+ });
47
+
48
+ if (!response.ok) {
49
+ throw new Error(`下载 skill 仓库失败:${response.status} ${response.statusText}`);
50
+ }
51
+
52
+ const arrayBuffer = await response.arrayBuffer();
53
+ fs.writeFileSync(targetFile, Buffer.from(arrayBuffer));
54
+ }
55
+
56
+ function extractArchive(archivePath, extractDir) {
57
+ execFileSync('tar', ['-xzf', archivePath, '-C', extractDir], {
58
+ stdio: ['ignore', 'pipe', 'pipe'],
59
+ });
60
+ }
61
+
62
+ function cloneRepository({ repoConfig, targetDir }) {
63
+ execFileSync(
64
+ 'git',
65
+ ['clone', '--depth', '1', '--branch', repoConfig.ref, buildGitUrl(repoConfig), targetDir],
66
+ {
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ }
69
+ );
70
+ }
71
+
72
+ function resolveRepositoryRoot(extractDir) {
73
+ if (fs.existsSync(path.join(extractDir, 'SKILL.md'))) {
74
+ return extractDir;
75
+ }
76
+
77
+ const entries = fs.readdirSync(extractDir, { withFileTypes: true });
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory()) {
80
+ continue;
81
+ }
82
+
83
+ const candidate = path.join(extractDir, entry.name);
84
+ if (fs.existsSync(path.join(candidate, 'SKILL.md'))) {
85
+ return candidate;
86
+ }
87
+ }
88
+
89
+ throw new Error(`解压后的内容里没有找到 SKILL.md:${extractDir}`);
90
+ }
91
+
92
+ function resolveLocalSkillRoot(packageRoot) {
93
+ const localRoot = path.resolve(packageRoot, '..', 'YuanFlow-skill');
94
+ if (fs.existsSync(path.join(localRoot, 'SKILL.md'))) {
95
+ return localRoot;
96
+ }
97
+ return '';
98
+ }
99
+
100
+ async function prepareSkillSource({ packageRoot }) {
101
+ const localRoot = resolveLocalSkillRoot(packageRoot);
102
+ if (localRoot) {
103
+ return {
104
+ sourceRoot: localRoot,
105
+ cleanup() {},
106
+ };
107
+ }
108
+
109
+ const repoConfig = readRepositoryConfig(packageRoot);
110
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'yuanflow-skill-'));
111
+ const archiveFile = path.join(tempRoot, `${repoConfig.repo}-${repoConfig.ref}.tar.gz`);
112
+ const extractDir = path.join(tempRoot, 'extract');
113
+ const cloneDir = path.join(tempRoot, 'clone');
114
+
115
+ fs.mkdirSync(extractDir, { recursive: true });
116
+
117
+ let sourceRoot;
118
+ let archiveError;
119
+
120
+ try {
121
+ await downloadArchive({
122
+ url: buildArchiveUrl(repoConfig),
123
+ targetFile: archiveFile,
124
+ });
125
+ extractArchive(archiveFile, extractDir);
126
+ sourceRoot = resolveRepositoryRoot(extractDir);
127
+ } catch (error) {
128
+ archiveError = error;
129
+ }
130
+
131
+ if (!sourceRoot) {
132
+ try {
133
+ cloneRepository({ repoConfig, targetDir: cloneDir });
134
+ sourceRoot = resolveRepositoryRoot(cloneDir);
135
+ } catch (gitError) {
136
+ const archiveMessage = archiveError instanceof Error ? archiveError.message : String(archiveError);
137
+ const gitMessage = gitError instanceof Error ? gitError.message : String(gitError);
138
+ throw new Error(`下载 skill 仓库失败。archive: ${archiveMessage}; git: ${gitMessage}`);
139
+ }
140
+ }
141
+
142
+ return {
143
+ sourceRoot,
144
+ cleanup() {
145
+ fs.rmSync(tempRoot, { recursive: true, force: true });
146
+ },
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ buildArchiveUrl,
152
+ buildGitUrl,
153
+ prepareSkillSource,
154
+ readRepositoryConfig,
155
+ resolveRepositoryRoot,
156
+ resolveLocalSkillRoot,
157
+ };
package/package.json CHANGED
@@ -1,22 +1,72 @@
1
1
  {
2
2
  "name": "yuanflow-cli",
3
- "version": "0.1.0",
4
- "private": false,
3
+ "version": "0.1.1",
4
+ "description": "YuanFlow API CLI and skill installer for supported AI coding agents.",
5
5
  "type": "module",
6
- "description": "NPM packaging wrapper for YuanFlow CLI",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "author": "zktlove",
7
9
  "publishConfig": {
8
10
  "access": "public"
9
11
  },
10
12
  "bin": {
11
- "ycloud": "cli.js"
13
+ "yuanflow-cli": "bin/yuanflow-cli.js",
14
+ "yuanflow-skill": "bin/yuanflow-skill.cjs"
15
+ },
16
+ "scripts": {
17
+ "postinstall": "node ./bin/yuanflow-skill.cjs install --postinstall",
18
+ "generate": "node ./scripts/generate-registry.js",
19
+ "start": "node ./bin/yuanflow-cli.js",
20
+ "test": "node --test test/*.test.js tests/*.test.cjs",
21
+ "pack:check": "npm pack --dry-run",
22
+ "release:check": "node ./bin/yuanflow-skill.cjs release-check",
23
+ "publish:help": "node ./bin/yuanflow-skill.cjs publish-help"
12
24
  },
13
25
  "files": [
14
- "cli.js",
26
+ "bin",
15
27
  "src",
28
+ "generated",
29
+ "scripts",
30
+ "lib",
16
31
  "README.md",
17
32
  "LICENSE"
18
33
  ],
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/zktlove/yuanflow-cli.git"
37
+ },
38
+ "homepage": "https://github.com/zktlove/yuanflow-cli#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/zktlove/yuanflow-cli/issues"
41
+ },
42
+ "yuanflowSkill": {
43
+ "skillRepo": {
44
+ "owner": "zktlove",
45
+ "repo": "yuanflow-skill",
46
+ "ref": "main"
47
+ }
48
+ },
19
49
  "engines": {
20
50
  "node": ">=20"
21
- }
51
+ },
52
+ "keywords": [
53
+ "skills",
54
+ "ai-agent",
55
+ "codex",
56
+ "claude-code",
57
+ "cursor",
58
+ "opencode",
59
+ "github-copilot",
60
+ "gemini-cli",
61
+ "kimi-cli",
62
+ "openclaw",
63
+ "trae",
64
+ "windsurf",
65
+ "qoder",
66
+ "qwen-code",
67
+ "yuanflow-cli",
68
+ "yuanflow-skill",
69
+ "xiaohongshu",
70
+ "douyin"
71
+ ]
22
72
  }
@@ -0,0 +1,174 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const CLI_ROOT = path.resolve(__dirname, '..');
7
+ const PROJECT_ROOT = path.resolve(CLI_ROOT, '..');
8
+ const OPENAPI_FILE = path.join(
9
+ PROJECT_ROOT,
10
+ 'docs',
11
+ 'social-api-tikhub',
12
+ 'tikhubopenapi.latest.json',
13
+ );
14
+ const OUTPUT_FILE = path.join(CLI_ROOT, 'generated', 'registry.json');
15
+
16
+ const ALIAS_RULES = [
17
+ {
18
+ alias: 'video-detail',
19
+ test: (entry) => {
20
+ return (
21
+ entry.platform === 'douyin' &&
22
+ entry.socialPath === '/douyin/app/v3/fetch_multi_video_v2'
23
+ );
24
+ },
25
+ },
26
+ ];
27
+
28
+ const EXCLUDED_TAG_PATTERNS = [
29
+ /deprecated/i,
30
+ /sora2/i,
31
+ /hybrid/i,
32
+ /demo/i,
33
+ /shortcut/i,
34
+ /temp-mail/i,
35
+ /tikhub-user/i,
36
+ /downloader/i,
37
+ /health/i,
38
+ ];
39
+
40
+ async function main() {
41
+ const api = JSON.parse(await readFile(OPENAPI_FILE, 'utf8'));
42
+ const endpoints = [];
43
+
44
+ for (const [rawPath, pathItem] of Object.entries(api.paths || {})) {
45
+ for (const [method, operation] of Object.entries(pathItem || {})) {
46
+ if (!['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
47
+ continue;
48
+ }
49
+
50
+ const tag = operation.tags?.[0] || 'Other';
51
+ if (shouldSkip(tag, operation)) {
52
+ continue;
53
+ }
54
+
55
+ const socialPath = toSocialPath(rawPath);
56
+ if (!socialPath) {
57
+ continue;
58
+ }
59
+
60
+ const platform = inferPlatform(tag, socialPath);
61
+ const command = toCommand(socialPath, platform);
62
+ const endpoint = {
63
+ id: operation.operationId || `${method}_${socialPath.replaceAll('/', '_')}`,
64
+ platform,
65
+ tag,
66
+ command,
67
+ aliases: [],
68
+ method: method.toUpperCase(),
69
+ upstreamPath: rawPath,
70
+ socialPath,
71
+ summary: cleanText(operation.summary || ''),
72
+ bodyExample: extractBodyExample(operation),
73
+ queryParams: extractQueryParams(operation),
74
+ };
75
+
76
+ endpoint.aliases = ALIAS_RULES.filter((rule) => rule.test(endpoint)).map(
77
+ (rule) => rule.alias,
78
+ );
79
+ endpoints.push(endpoint);
80
+ }
81
+ }
82
+
83
+ endpoints.sort((a, b) => {
84
+ return `${a.platform} ${a.command}`.localeCompare(`${b.platform} ${b.command}`);
85
+ });
86
+
87
+ const registry = {
88
+ generatedAt: new Date().toISOString(),
89
+ source: 'YuanFlow API registry',
90
+ platforms: [...new Set(endpoints.map((endpoint) => endpoint.platform))].sort(),
91
+ endpoints,
92
+ };
93
+
94
+ await mkdir(path.dirname(OUTPUT_FILE), { recursive: true });
95
+ await writeFile(OUTPUT_FILE, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
96
+ console.log(`Generated ${endpoints.length} endpoints -> ${OUTPUT_FILE}`);
97
+ }
98
+
99
+ function shouldSkip(tag, operation) {
100
+ const haystack = `${tag} ${operation.summary || ''} ${operation.description || ''}`;
101
+ return EXCLUDED_TAG_PATTERNS.some((pattern) => pattern.test(haystack));
102
+ }
103
+
104
+ function toSocialPath(rawPath) {
105
+ if (!rawPath.startsWith('/api/v1/')) {
106
+ return null;
107
+ }
108
+ return rawPath.slice('/api/v1'.length);
109
+ }
110
+
111
+ function inferPlatform(tag, socialPath) {
112
+ const lowerTag = tag.toLowerCase();
113
+ const firstSegment = socialPath.split('/').filter(Boolean)[0] || 'other';
114
+
115
+ if (lowerTag.includes('xiaohongshu')) return 'xiaohongshu';
116
+ if (lowerTag.includes('tiktok')) return 'tiktok';
117
+ if (lowerTag.includes('douyin')) return 'douyin';
118
+ if (lowerTag.includes('bilibili')) return 'bilibili';
119
+ if (lowerTag.includes('kuaishou')) return 'kuaishou';
120
+ if (lowerTag.includes('weibo')) return 'weibo';
121
+ if (lowerTag.includes('wechat')) return 'wechat';
122
+ if (lowerTag.includes('youtube')) return 'youtube';
123
+ if (lowerTag.includes('twitter')) return 'twitter';
124
+ if (lowerTag.includes('instagram')) return 'instagram';
125
+ if (lowerTag.includes('threads')) return 'threads';
126
+ if (lowerTag.includes('reddit')) return 'reddit';
127
+ if (lowerTag.includes('zhihu')) return 'zhihu';
128
+ if (lowerTag.includes('lemon8')) return 'lemon8';
129
+ if (lowerTag.includes('xigua')) return 'xigua';
130
+ if (lowerTag.includes('linkedin')) return 'linkedin';
131
+ if (lowerTag.includes('netease')) return 'netease';
132
+
133
+ return normalizeName(firstSegment);
134
+ }
135
+
136
+ function toCommand(socialPath, platform) {
137
+ const segments = socialPath.split('/').filter(Boolean);
138
+ const withoutPlatform = segments[0] === platform ? segments.slice(1) : segments;
139
+ return withoutPlatform.map(normalizeName).join(' ');
140
+ }
141
+
142
+ function normalizeName(value) {
143
+ return String(value)
144
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
145
+ .replace(/_/g, '-')
146
+ .replace(/[^a-zA-Z0-9-]+/g, '-')
147
+ .replace(/-+/g, '-')
148
+ .replace(/^-|-$/g, '')
149
+ .toLowerCase();
150
+ }
151
+
152
+ function cleanText(value) {
153
+ return String(value).replace(/\s+/g, ' ').trim();
154
+ }
155
+
156
+ function extractBodyExample(operation) {
157
+ const content = operation.requestBody?.content?.['application/json'];
158
+ return content?.example || content?.examples?.default?.value || null;
159
+ }
160
+
161
+ function extractQueryParams(operation) {
162
+ return (operation.parameters || [])
163
+ .filter((parameter) => parameter.in === 'query')
164
+ .map((parameter) => ({
165
+ name: parameter.name,
166
+ required: Boolean(parameter.required),
167
+ description: cleanText(parameter.description || ''),
168
+ }));
169
+ }
170
+
171
+ main().catch((error) => {
172
+ console.error(error);
173
+ process.exitCode = 1;
174
+ });
@@ -0,0 +1,169 @@
1
+ import { listEndpoints } from './registry.js';
2
+ import { listShortcuts } from './shortcuts.js';
3
+
4
+ const ERROR_MAP = [
5
+ {
6
+ code: 'TOKEN_MISSING',
7
+ exitCode: 3,
8
+ retryable: false,
9
+ test: (message) => message.includes('缺少 token'),
10
+ },
11
+ {
12
+ code: 'BAD_ARGUMENT',
13
+ exitCode: 2,
14
+ retryable: false,
15
+ test: (message) =>
16
+ message.includes('缺少') ||
17
+ message.includes('未知平台') ||
18
+ message.includes('未找到命令') ||
19
+ message.includes('未知命令') ||
20
+ message.includes('不支持'),
21
+ },
22
+ {
23
+ code: 'AUTH_INVALID',
24
+ exitCode: 3,
25
+ retryable: false,
26
+ test: (message) => message.includes('HTTP 401') || message.includes('HTTP 403'),
27
+ },
28
+ {
29
+ code: 'UPSTREAM_ERROR',
30
+ exitCode: 4,
31
+ retryable: true,
32
+ test: (message) => /HTTP 5\d\d/.test(message),
33
+ },
34
+ {
35
+ code: 'NETWORK_ERROR',
36
+ exitCode: 5,
37
+ retryable: true,
38
+ test: (message) => message.includes('fetch failed') || message.includes('ECONN'),
39
+ },
40
+ ];
41
+
42
+ export function isAgentJsonFormat(options = {}) {
43
+ return options.format === 'agent-json';
44
+ }
45
+
46
+ export function createAgentSuccess(command, data, meta = {}) {
47
+ return {
48
+ success: true,
49
+ command,
50
+ data,
51
+ warnings: [],
52
+ error: null,
53
+ meta,
54
+ };
55
+ }
56
+
57
+ export function createAgentError(command, error) {
58
+ const message = error?.message || String(error);
59
+ const mapped = mapError(message);
60
+ return {
61
+ payload: {
62
+ success: false,
63
+ command: command || 'unknown',
64
+ data: null,
65
+ warnings: [],
66
+ error: {
67
+ code: mapped.code,
68
+ message,
69
+ retryable: mapped.retryable,
70
+ },
71
+ meta: {},
72
+ },
73
+ exitCode: mapped.exitCode,
74
+ };
75
+ }
76
+
77
+ export function getCommandName(platform, command) {
78
+ return [platform, command].filter(Boolean).join(' ') || 'unknown';
79
+ }
80
+
81
+ export function buildCommandRegistry() {
82
+ const shortcuts = listShortcuts().map((shortcut) => shortcutToCommand(shortcut));
83
+ const endpoints = listEndpoints().map((endpoint) => endpointToCommand(endpoint));
84
+ return [...shortcuts, ...endpoints].sort((left, right) =>
85
+ left.key.localeCompare(right.key),
86
+ );
87
+ }
88
+
89
+ export function findCommandByKey(key) {
90
+ return buildCommandRegistry().find((item) => item.key === key);
91
+ }
92
+
93
+ export function commandToSchema(command) {
94
+ if (!command) {
95
+ return null;
96
+ }
97
+
98
+ return {
99
+ key: command.key,
100
+ command: command.command,
101
+ kind: command.kind,
102
+ description: command.description,
103
+ cli: {
104
+ positionals: command.positionals || [],
105
+ options: command.options || [],
106
+ },
107
+ api: {
108
+ method: command.method,
109
+ socialPath: command.socialPath,
110
+ url: `/social${command.socialPath}`,
111
+ queryParams: command.queryParams || [],
112
+ requestBody: command.requestBody || null,
113
+ },
114
+ returns: command.returns || '返回字段以上游接口实际响应为准。',
115
+ };
116
+ }
117
+
118
+ function shortcutToCommand(shortcut) {
119
+ return {
120
+ key: `${shortcut.platform}.${shortcut.command}`,
121
+ command: `${shortcut.platform} ${shortcut.command}`,
122
+ kind: 'shortcut',
123
+ description: shortcut.description,
124
+ method: shortcut.method,
125
+ socialPath: shortcut.socialPath,
126
+ positionals: shortcut.positionals,
127
+ options: shortcut.options.map((item) => ({
128
+ ...item,
129
+ flag: `--${item.flag}`,
130
+ required: false,
131
+ })),
132
+ returns: shortcut.returns,
133
+ alternatives: shortcut.alternatives,
134
+ };
135
+ }
136
+
137
+ function endpointToCommand(endpoint) {
138
+ const command = `${endpoint.platform} ${endpoint.command}`;
139
+ return {
140
+ key: command.replace(/\s+/g, '.'),
141
+ command,
142
+ kind: 'endpoint',
143
+ description: endpoint.summary,
144
+ method: endpoint.method,
145
+ socialPath: endpoint.socialPath,
146
+ positionals: [],
147
+ options: (endpoint.queryParams || []).map((item) => ({
148
+ flag: `--${item.name.replace(/_/g, '-')}`,
149
+ name: item.name,
150
+ required: item.required,
151
+ label: item.description || item.name,
152
+ })),
153
+ queryParams: endpoint.queryParams || [],
154
+ requestBody: endpoint.bodyExample,
155
+ returns: '返回字段以上游接口实际响应为准。',
156
+ };
157
+ }
158
+
159
+ function mapError(message) {
160
+ const mapped = ERROR_MAP.find((item) => item.test(message));
161
+ if (mapped) {
162
+ return mapped;
163
+ }
164
+ return {
165
+ code: 'INTERNAL_ERROR',
166
+ exitCode: 1,
167
+ retryable: false,
168
+ };
169
+ }