coding-tool-x 3.6.7 → 3.6.9
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/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-BuBlsbcI.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-DQczxuOX.js} +1 -1
- package/dist/web/assets/Home-BvAzzWnB.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-C8gAlaps.js} +1 -1
- package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-nqgi6_cR.js} +1 -1
- package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-D8AxMYy6.js} +1 -1
- package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-BDbNbNkK.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-DGGaGW2c.js} +1 -1
- package/dist/web/assets/index-Dsh4Q5Cg.css +1 -0
- package/dist/web/assets/index-y-eEayxR.js +2 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +46 -69
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +1137 -86
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +198 -65
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-fWgvlZQx.js +0 -2
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const {
|
|
2
|
+
recordRequest: recordSharedRequest,
|
|
3
|
+
getStatistics: getSharedStatistics,
|
|
4
|
+
getDailyStatistics: getSharedDailyStatistics,
|
|
5
|
+
getTodayStatistics: getSharedTodayStatistics
|
|
6
|
+
} = require('./statistics-service');
|
|
7
|
+
const { normalizeUsageTokens, toNumber } = require('./proxy-log-helper');
|
|
8
|
+
|
|
9
|
+
const TOOL_TYPE = 'claude-code';
|
|
10
|
+
const USAGE_SOURCE = 'claude';
|
|
11
|
+
|
|
12
|
+
function toLegacyEntryShape(entry = {}, includeName = false) {
|
|
13
|
+
const normalized = normalizeUsageTokens(USAGE_SOURCE, entry.tokens || {});
|
|
14
|
+
const result = {
|
|
15
|
+
requests: toNumber(entry.requests),
|
|
16
|
+
tokens: {
|
|
17
|
+
input: normalized.input,
|
|
18
|
+
output: normalized.output,
|
|
19
|
+
cacheCreation: normalized.cacheCreation,
|
|
20
|
+
cacheRead: normalized.cacheRead,
|
|
21
|
+
cached: normalized.cached,
|
|
22
|
+
total: normalized.total
|
|
23
|
+
},
|
|
24
|
+
cost: toNumber(entry.cost)
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (includeName) {
|
|
28
|
+
result.name = entry.name || '';
|
|
29
|
+
if (entry.firstUsed) result.firstUsed = entry.firstUsed;
|
|
30
|
+
if (entry.lastUsed) result.lastUsed = entry.lastUsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pickToolScope(sharedStats = {}) {
|
|
37
|
+
const byToolType = sharedStats.byToolType || {};
|
|
38
|
+
const toolScope = byToolType[TOOL_TYPE] || {};
|
|
39
|
+
|
|
40
|
+
let byChannel = {};
|
|
41
|
+
let byModel = {};
|
|
42
|
+
|
|
43
|
+
if (toolScope.channels && typeof toolScope.channels === 'object') {
|
|
44
|
+
byChannel = toolScope.channels;
|
|
45
|
+
} else {
|
|
46
|
+
byChannel = Object.fromEntries(
|
|
47
|
+
Object.entries(sharedStats.byChannel || {}).filter(([, value]) => value?.toolType === TOOL_TYPE)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (toolScope.models && typeof toolScope.models === 'object') {
|
|
52
|
+
byModel = toolScope.models;
|
|
53
|
+
} else {
|
|
54
|
+
byModel = Object.fromEntries(
|
|
55
|
+
Object.entries(sharedStats.byModel || {}).filter(([, value]) => value?.toolType === TOOL_TYPE)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { toolScope, byChannel, byModel };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSummaryStatistics(sharedStats = {}) {
|
|
63
|
+
const { toolScope, byChannel, byModel } = pickToolScope(sharedStats);
|
|
64
|
+
const normalized = toLegacyEntryShape(toolScope);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
version: '1.0',
|
|
68
|
+
lastUpdated: sharedStats.lastUpdated || new Date().toISOString(),
|
|
69
|
+
global: {
|
|
70
|
+
totalRequests: normalized.requests,
|
|
71
|
+
totalTokens: normalized.tokens.total,
|
|
72
|
+
totalCost: normalized.cost
|
|
73
|
+
},
|
|
74
|
+
byChannel: Object.fromEntries(
|
|
75
|
+
Object.entries(byChannel).map(([key, value]) => [key, toLegacyEntryShape(value, true)])
|
|
76
|
+
),
|
|
77
|
+
byModel: Object.fromEntries(
|
|
78
|
+
Object.entries(byModel).map(([key, value]) => [key, toLegacyEntryShape(value)])
|
|
79
|
+
)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildDailyStatistics(sharedDaily = {}, fallbackDate) {
|
|
84
|
+
const { byToolType = {} } = sharedDaily;
|
|
85
|
+
const toolScope = byToolType[TOOL_TYPE] || {};
|
|
86
|
+
const normalized = toLegacyEntryShape(toolScope);
|
|
87
|
+
const byChannel = toolScope.channels || {};
|
|
88
|
+
const byModel = toolScope.models || {};
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
date: sharedDaily.date || fallbackDate,
|
|
92
|
+
summary: {
|
|
93
|
+
requests: normalized.requests,
|
|
94
|
+
tokens: normalized.tokens.total,
|
|
95
|
+
cost: normalized.cost
|
|
96
|
+
},
|
|
97
|
+
byChannel: Object.fromEntries(
|
|
98
|
+
Object.entries(byChannel).map(([key, value]) => [key, toLegacyEntryShape(value, true)])
|
|
99
|
+
),
|
|
100
|
+
byModel: Object.fromEntries(
|
|
101
|
+
Object.entries(byModel).map(([key, value]) => [key, toLegacyEntryShape(value)])
|
|
102
|
+
)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function recordRequest(requestData = {}) {
|
|
107
|
+
const normalizedTokens = normalizeUsageTokens(USAGE_SOURCE, requestData.tokens || {});
|
|
108
|
+
return recordSharedRequest({
|
|
109
|
+
...requestData,
|
|
110
|
+
toolType: TOOL_TYPE,
|
|
111
|
+
tokens: {
|
|
112
|
+
input: normalizedTokens.input,
|
|
113
|
+
output: normalizedTokens.output,
|
|
114
|
+
cached: normalizedTokens.cached,
|
|
115
|
+
cacheCreation: normalizedTokens.cacheCreation,
|
|
116
|
+
cacheRead: normalizedTokens.cacheRead,
|
|
117
|
+
total: normalizedTokens.total
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getStatistics() {
|
|
123
|
+
return buildSummaryStatistics(getSharedStatistics());
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getDailyStatistics(date) {
|
|
127
|
+
return buildDailyStatistics(getSharedDailyStatistics(date), date);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getTodayStatistics() {
|
|
131
|
+
return buildDailyStatistics(getSharedTodayStatistics());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
recordRequest,
|
|
136
|
+
getStatistics,
|
|
137
|
+
getDailyStatistics,
|
|
138
|
+
getTodayStatistics
|
|
139
|
+
};
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commands 服务
|
|
3
3
|
*
|
|
4
|
-
* 管理 Claude/OpenCode 自定义命令的 CRUD 操作
|
|
4
|
+
* 管理 Claude/Codex/Gemini/OpenCode 自定义命令的 CRUD 操作
|
|
5
5
|
* 支持从 GitHub 仓库扫描和安装命令
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
|
+
const toml = require('toml');
|
|
11
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
10
12
|
const { RepoScannerBase } = require('./repo-scanner-base');
|
|
11
13
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
12
14
|
const {
|
|
13
|
-
parseCommandContent,
|
|
14
15
|
parseFrontmatter
|
|
15
16
|
} = require('./format-converter');
|
|
17
|
+
const {
|
|
18
|
+
normalizeSafeFileStem,
|
|
19
|
+
normalizeSafeRelativePath,
|
|
20
|
+
resolveInsideRoot
|
|
21
|
+
} = require('./config-artifact-paths');
|
|
16
22
|
|
|
17
23
|
// 默认仓库源
|
|
18
24
|
const DEFAULT_REPOS = [];
|
|
19
|
-
const SUPPORTED_PLATFORMS = ['claude', 'opencode'];
|
|
25
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
20
26
|
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
21
27
|
const CLAUDE_COMMANDS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'commands');
|
|
28
|
+
const CODEX_COMMANDS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'commands');
|
|
29
|
+
const GEMINI_COMMANDS_DIR = path.join(NATIVE_PATHS.gemini.dir, 'commands');
|
|
22
30
|
|
|
23
31
|
const PLATFORM_CONFIG = {
|
|
24
32
|
claude: {
|
|
@@ -26,6 +34,11 @@ const PLATFORM_CONFIG = {
|
|
|
26
34
|
projectCommandsDir: (projectPath) => path.join(projectPath, '.claude', 'commands'),
|
|
27
35
|
repoType: 'commands'
|
|
28
36
|
},
|
|
37
|
+
codex: {
|
|
38
|
+
userCommandsDir: CODEX_COMMANDS_DIR,
|
|
39
|
+
projectCommandsDir: (projectPath) => path.join(projectPath, '.codex', 'commands'),
|
|
40
|
+
repoType: 'codex-commands'
|
|
41
|
+
},
|
|
29
42
|
opencode: {
|
|
30
43
|
userCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'commands'),
|
|
31
44
|
legacyUserCommandsDir: path.join(OPENCODE_CONFIG_DIR, 'command'),
|
|
@@ -38,6 +51,12 @@ const PLATFORM_CONFIG = {
|
|
|
38
51
|
return modern;
|
|
39
52
|
},
|
|
40
53
|
repoType: 'opencode-commands'
|
|
54
|
+
},
|
|
55
|
+
gemini: {
|
|
56
|
+
userCommandsDir: GEMINI_COMMANDS_DIR,
|
|
57
|
+
projectCommandsDir: (projectPath) => path.join(projectPath, '.gemini', 'commands'),
|
|
58
|
+
repoType: 'gemini-commands',
|
|
59
|
+
fileExtension: '.toml'
|
|
41
60
|
}
|
|
42
61
|
};
|
|
43
62
|
|
|
@@ -86,11 +105,75 @@ function generateCommandFrontmatter(data) {
|
|
|
86
105
|
return lines.join('\n');
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
function getCommandFileExtension(platform) {
|
|
109
|
+
return PLATFORM_CONFIG[platform]?.fileExtension || '.md';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getCommandTargetName(name, platform) {
|
|
113
|
+
return `${name}${getCommandFileExtension(platform)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeCommandName(name) {
|
|
117
|
+
if (!name || !String(name).trim()) {
|
|
118
|
+
throw new Error('命令名称不能为空');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
return normalizeSafeFileStem(name, 'command name', {
|
|
123
|
+
allowDots: false,
|
|
124
|
+
pattern: /^[a-zA-Z0-9_-]+$/
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error('命令名只能包含字母、数字、横杠和下划线');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeCommandNamespace(namespace) {
|
|
132
|
+
return normalizeSafeRelativePath(namespace || '', 'command namespace', {
|
|
133
|
+
allowEmpty: true,
|
|
134
|
+
allowHiddenSegments: false
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildCommandRelativePath(name, namespace, platform) {
|
|
139
|
+
const safeName = normalizeCommandName(name);
|
|
140
|
+
const safeNamespace = normalizeCommandNamespace(namespace);
|
|
141
|
+
const fileName = getCommandTargetName(safeName, platform);
|
|
142
|
+
return safeNamespace ? path.posix.join(safeNamespace, fileName) : fileName;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function parseGeminiCommandToml(content) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = toml.parse(content);
|
|
148
|
+
return {
|
|
149
|
+
description: parsed.description || '',
|
|
150
|
+
body: parsed.prompt || ''
|
|
151
|
+
};
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return {
|
|
154
|
+
description: '',
|
|
155
|
+
body: content,
|
|
156
|
+
parseError: err.message
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function generateGeminiCommandToml({ description, body }) {
|
|
162
|
+
const data = {
|
|
163
|
+
prompt: body || ''
|
|
164
|
+
};
|
|
165
|
+
if (description) {
|
|
166
|
+
data.description = description;
|
|
167
|
+
}
|
|
168
|
+
return tomlStringify(data);
|
|
169
|
+
}
|
|
170
|
+
|
|
89
171
|
/**
|
|
90
172
|
* 递归扫描目录获取命令文件
|
|
91
173
|
*/
|
|
92
|
-
function scanCommandsDir(dir, basePath, scope) {
|
|
174
|
+
function scanCommandsDir(dir, basePath, scope, platform = 'claude') {
|
|
93
175
|
const commands = [];
|
|
176
|
+
const fileExtension = getCommandFileExtension(platform);
|
|
94
177
|
|
|
95
178
|
if (!fs.existsSync(dir)) {
|
|
96
179
|
return commands;
|
|
@@ -104,17 +187,19 @@ function scanCommandsDir(dir, basePath, scope) {
|
|
|
104
187
|
|
|
105
188
|
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
106
189
|
// 递归扫描子目录
|
|
107
|
-
const subCommands = scanCommandsDir(fullPath, basePath, scope);
|
|
190
|
+
const subCommands = scanCommandsDir(fullPath, basePath, scope, platform);
|
|
108
191
|
commands.push(...subCommands);
|
|
109
|
-
} else if (entry.isFile() && entry.name.endsWith(
|
|
192
|
+
} else if (entry.isFile() && entry.name.endsWith(fileExtension)) {
|
|
110
193
|
// 解析命令文件
|
|
111
194
|
try {
|
|
112
195
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
113
|
-
const
|
|
196
|
+
const parsed = platform === 'gemini'
|
|
197
|
+
? { frontmatter: {}, body: '', gemini: parseGeminiCommandToml(content) }
|
|
198
|
+
: { ...parseFrontmatter(content), gemini: null };
|
|
114
199
|
|
|
115
200
|
// 计算相对路径和命令名
|
|
116
201
|
const relativePath = path.relative(basePath, fullPath);
|
|
117
|
-
const commandName = entry.name.
|
|
202
|
+
const commandName = entry.name.slice(0, -fileExtension.length);
|
|
118
203
|
const namespace = path.dirname(relativePath);
|
|
119
204
|
|
|
120
205
|
commands.push({
|
|
@@ -123,13 +208,14 @@ function scanCommandsDir(dir, basePath, scope) {
|
|
|
123
208
|
scope,
|
|
124
209
|
path: relativePath,
|
|
125
210
|
fullPath,
|
|
126
|
-
description: frontmatter.description || '',
|
|
127
|
-
allowedTools: frontmatter['allowed-tools'] || '',
|
|
128
|
-
argumentHint: frontmatter['argument-hint'] || '',
|
|
129
|
-
agent: frontmatter.agent || '',
|
|
130
|
-
model: frontmatter.model || '',
|
|
131
|
-
subtask: frontmatter.subtask || '',
|
|
132
|
-
body,
|
|
211
|
+
description: parsed.gemini?.description || parsed.frontmatter.description || '',
|
|
212
|
+
allowedTools: parsed.frontmatter['allowed-tools'] || '',
|
|
213
|
+
argumentHint: parsed.frontmatter['argument-hint'] || '',
|
|
214
|
+
agent: parsed.frontmatter.agent || '',
|
|
215
|
+
model: parsed.frontmatter.model || '',
|
|
216
|
+
subtask: parsed.frontmatter.subtask || '',
|
|
217
|
+
body: parsed.gemini?.body ?? parsed.body,
|
|
218
|
+
parseError: parsed.gemini?.parseError || '',
|
|
133
219
|
fullContent: content,
|
|
134
220
|
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
135
221
|
});
|
|
@@ -150,13 +236,15 @@ function scanCommandsDir(dir, basePath, scope) {
|
|
|
150
236
|
*/
|
|
151
237
|
class CommandsRepoScanner extends RepoScannerBase {
|
|
152
238
|
constructor(platform, installDir) {
|
|
239
|
+
const normalizedPlatform = normalizePlatform(platform);
|
|
153
240
|
super({
|
|
154
|
-
type: PLATFORM_CONFIG[
|
|
241
|
+
type: PLATFORM_CONFIG[normalizedPlatform]?.repoType || 'commands',
|
|
155
242
|
installDir,
|
|
156
243
|
markerFile: null, // 直接扫描 .md 文件
|
|
157
|
-
fileExtension:
|
|
244
|
+
fileExtension: getCommandFileExtension(normalizedPlatform),
|
|
158
245
|
defaultRepos: DEFAULT_REPOS
|
|
159
246
|
});
|
|
247
|
+
this.platform = normalizedPlatform;
|
|
160
248
|
}
|
|
161
249
|
|
|
162
250
|
/**
|
|
@@ -166,12 +254,15 @@ class CommandsRepoScanner extends RepoScannerBase {
|
|
|
166
254
|
try {
|
|
167
255
|
// 计算相对路径
|
|
168
256
|
const relativePath = baseDir ? file.path.slice(baseDir.length + 1) : file.path;
|
|
169
|
-
const
|
|
257
|
+
const fileExtension = getCommandFileExtension(this.platform);
|
|
258
|
+
const fileName = path.basename(file.path, fileExtension);
|
|
170
259
|
const namespace = path.dirname(relativePath);
|
|
171
260
|
|
|
172
261
|
// 获取文件内容
|
|
173
262
|
const content = await this.fetchRawContent(repo, file.path);
|
|
174
|
-
const
|
|
263
|
+
const parsed = this.platform === 'gemini'
|
|
264
|
+
? { frontmatter: {}, body: '', gemini: parseGeminiCommandToml(content) }
|
|
265
|
+
: { ...this.parseFrontmatter(content), gemini: null };
|
|
175
266
|
|
|
176
267
|
return {
|
|
177
268
|
key: `${repo.owner}/${repo.name}:${relativePath}`,
|
|
@@ -180,13 +271,14 @@ class CommandsRepoScanner extends RepoScannerBase {
|
|
|
180
271
|
scope: 'remote',
|
|
181
272
|
path: relativePath,
|
|
182
273
|
repoPath: file.path,
|
|
183
|
-
description: frontmatter.description || '',
|
|
184
|
-
allowedTools: frontmatter['allowed-tools'] || '',
|
|
185
|
-
argumentHint: frontmatter['argument-hint'] || '',
|
|
186
|
-
agent: frontmatter.agent || '',
|
|
187
|
-
model: frontmatter.model || '',
|
|
188
|
-
subtask: frontmatter.subtask || '',
|
|
189
|
-
body,
|
|
274
|
+
description: parsed.gemini?.description || parsed.frontmatter.description || '',
|
|
275
|
+
allowedTools: parsed.frontmatter['allowed-tools'] || '',
|
|
276
|
+
argumentHint: parsed.frontmatter['argument-hint'] || '',
|
|
277
|
+
agent: parsed.frontmatter.agent || '',
|
|
278
|
+
model: parsed.frontmatter.model || '',
|
|
279
|
+
subtask: parsed.frontmatter.subtask || '',
|
|
280
|
+
body: parsed.gemini?.body ?? parsed.body,
|
|
281
|
+
parseError: parsed.gemini?.parseError || '',
|
|
190
282
|
fullContent: content,
|
|
191
283
|
installed: this.isInstalled(relativePath),
|
|
192
284
|
readmeUrl: `https://github.com/${repo.owner}/${repo.name}/blob/${repo.branch}/${file.path}`,
|
|
@@ -205,8 +297,13 @@ class CommandsRepoScanner extends RepoScannerBase {
|
|
|
205
297
|
* 检查命令是否已安装
|
|
206
298
|
*/
|
|
207
299
|
isInstalled(relativePath) {
|
|
208
|
-
|
|
209
|
-
|
|
300
|
+
try {
|
|
301
|
+
const safeRelativePath = normalizeSafeRelativePath(relativePath, 'command path');
|
|
302
|
+
const fullPath = resolveInsideRoot(this.installDir, safeRelativePath, 'Command path');
|
|
303
|
+
return fs.existsSync(fullPath);
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
210
307
|
}
|
|
211
308
|
|
|
212
309
|
/**
|
|
@@ -257,6 +354,44 @@ class CommandsService {
|
|
|
257
354
|
return this.projectCommandsDir(projectPath);
|
|
258
355
|
}
|
|
259
356
|
|
|
357
|
+
_getBaseDir(scope, projectPath = null) {
|
|
358
|
+
return scope === 'user'
|
|
359
|
+
? this.userCommandsDir
|
|
360
|
+
: this.getProjectCommandsDir(projectPath);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_resolveCommandPath(baseDir, name, namespace = null) {
|
|
364
|
+
if (!baseDir) {
|
|
365
|
+
throw new Error('项目路径不能为空');
|
|
366
|
+
}
|
|
367
|
+
const relativePath = buildCommandRelativePath(name, namespace, this.platform);
|
|
368
|
+
const fullPath = resolveInsideRoot(baseDir, relativePath, 'Command path');
|
|
369
|
+
return { relativePath, fullPath };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_generateCommandContent({ description, allowedTools, argumentHint, agent, model, subtask, body }) {
|
|
373
|
+
if (this.platform === 'gemini') {
|
|
374
|
+
return generateGeminiCommandToml({ description, body });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const frontmatterData = {};
|
|
378
|
+
if (description) frontmatterData.description = description;
|
|
379
|
+
if (this.platform !== 'opencode') {
|
|
380
|
+
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
381
|
+
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
382
|
+
}
|
|
383
|
+
if (agent) frontmatterData.agent = agent;
|
|
384
|
+
if (model) frontmatterData.model = model;
|
|
385
|
+
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
386
|
+
|
|
387
|
+
let content = '';
|
|
388
|
+
if (Object.keys(frontmatterData).length > 0) {
|
|
389
|
+
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
390
|
+
}
|
|
391
|
+
content += body || '';
|
|
392
|
+
return content;
|
|
393
|
+
}
|
|
394
|
+
|
|
260
395
|
/**
|
|
261
396
|
* 获取所有命令列表
|
|
262
397
|
* @param {string} projectPath - 项目路径(可选,用于获取项目级命令)
|
|
@@ -265,13 +400,13 @@ class CommandsService {
|
|
|
265
400
|
const commands = [];
|
|
266
401
|
|
|
267
402
|
// 获取用户级命令
|
|
268
|
-
const userCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user');
|
|
403
|
+
const userCommands = scanCommandsDir(this.userCommandsDir, this.userCommandsDir, 'user', this.platform);
|
|
269
404
|
commands.push(...userCommands);
|
|
270
405
|
|
|
271
406
|
// 获取项目级命令(如果提供了项目路径)
|
|
272
407
|
if (projectPath) {
|
|
273
408
|
const projectCommandsDir = this.getProjectCommandsDir(projectPath);
|
|
274
|
-
const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project');
|
|
409
|
+
const projectCommands = scanCommandsDir(projectCommandsDir, projectCommandsDir, 'project', this.platform);
|
|
275
410
|
commands.push(...projectCommands);
|
|
276
411
|
}
|
|
277
412
|
|
|
@@ -336,36 +471,34 @@ class CommandsService {
|
|
|
336
471
|
* 获取单个命令详情
|
|
337
472
|
*/
|
|
338
473
|
getCommand(name, scope, projectPath = null, namespace = null) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const relativePath = namespace
|
|
344
|
-
? path.join(namespace, `${name}.md`)
|
|
345
|
-
: `${name}.md`;
|
|
346
|
-
|
|
347
|
-
const fullPath = path.join(baseDir, relativePath);
|
|
474
|
+
const safeName = normalizeCommandName(name);
|
|
475
|
+
const safeNamespace = normalizeCommandNamespace(namespace);
|
|
476
|
+
const baseDir = this._getBaseDir(scope, projectPath);
|
|
477
|
+
const { relativePath, fullPath } = this._resolveCommandPath(baseDir, safeName, safeNamespace);
|
|
348
478
|
|
|
349
479
|
if (!fs.existsSync(fullPath)) {
|
|
350
480
|
return null;
|
|
351
481
|
}
|
|
352
482
|
|
|
353
483
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
354
|
-
const
|
|
484
|
+
const parsed = this.platform === 'gemini'
|
|
485
|
+
? { frontmatter: {}, body: '', gemini: parseGeminiCommandToml(content) }
|
|
486
|
+
: { ...parseFrontmatter(content), gemini: null };
|
|
355
487
|
|
|
356
488
|
return {
|
|
357
|
-
name,
|
|
358
|
-
namespace,
|
|
489
|
+
name: safeName,
|
|
490
|
+
namespace: safeNamespace || null,
|
|
359
491
|
scope,
|
|
360
492
|
path: relativePath,
|
|
361
493
|
fullPath,
|
|
362
|
-
description: frontmatter.description || '',
|
|
363
|
-
allowedTools: frontmatter['allowed-tools'] || '',
|
|
364
|
-
argumentHint: frontmatter['argument-hint'] || '',
|
|
365
|
-
agent: frontmatter.agent || '',
|
|
366
|
-
model: frontmatter.model || '',
|
|
367
|
-
subtask: frontmatter.subtask || '',
|
|
368
|
-
body,
|
|
494
|
+
description: parsed.gemini?.description || parsed.frontmatter.description || '',
|
|
495
|
+
allowedTools: parsed.frontmatter['allowed-tools'] || '',
|
|
496
|
+
argumentHint: parsed.frontmatter['argument-hint'] || '',
|
|
497
|
+
agent: parsed.frontmatter.agent || '',
|
|
498
|
+
model: parsed.frontmatter.model || '',
|
|
499
|
+
subtask: parsed.frontmatter.subtask || '',
|
|
500
|
+
body: parsed.gemini?.body ?? parsed.body,
|
|
501
|
+
parseError: parsed.gemini?.parseError || '',
|
|
369
502
|
fullContent: content,
|
|
370
503
|
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
371
504
|
};
|
|
@@ -375,104 +508,53 @@ class CommandsService {
|
|
|
375
508
|
* 创建命令
|
|
376
509
|
*/
|
|
377
510
|
createCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
384
|
-
throw new Error('命令名只能包含字母、数字、横杠和下划线');
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const baseDir = scope === 'user'
|
|
388
|
-
? this.userCommandsDir
|
|
389
|
-
: this.getProjectCommandsDir(projectPath);
|
|
390
|
-
|
|
391
|
-
const targetDir = namespace ? path.join(baseDir, namespace) : baseDir;
|
|
511
|
+
const safeName = normalizeCommandName(name);
|
|
512
|
+
const safeNamespace = normalizeCommandNamespace(namespace);
|
|
513
|
+
const baseDir = this._getBaseDir(scope, projectPath);
|
|
514
|
+
const { fullPath: filePath } = this._resolveCommandPath(baseDir, safeName, safeNamespace);
|
|
515
|
+
const targetDir = path.dirname(filePath);
|
|
392
516
|
ensureDir(targetDir);
|
|
393
517
|
|
|
394
|
-
const filePath = path.join(targetDir, `${name}.md`);
|
|
395
|
-
|
|
396
518
|
// 检查是否已存在
|
|
397
519
|
if (fs.existsSync(filePath)) {
|
|
398
|
-
throw new Error(`命令 "${
|
|
520
|
+
throw new Error(`命令 "${safeName}" 已存在`);
|
|
399
521
|
}
|
|
400
522
|
|
|
401
|
-
|
|
402
|
-
const frontmatterData = {};
|
|
403
|
-
if (description) frontmatterData.description = description;
|
|
404
|
-
if (this.platform !== 'opencode') {
|
|
405
|
-
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
406
|
-
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
407
|
-
}
|
|
408
|
-
if (agent) frontmatterData.agent = agent;
|
|
409
|
-
if (model) frontmatterData.model = model;
|
|
410
|
-
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
411
|
-
|
|
412
|
-
let content = '';
|
|
413
|
-
if (Object.keys(frontmatterData).length > 0) {
|
|
414
|
-
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
415
|
-
}
|
|
416
|
-
content += body || '';
|
|
523
|
+
const content = this._generateCommandContent({ description, allowedTools, argumentHint, agent, model, subtask, body });
|
|
417
524
|
|
|
418
525
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
419
526
|
|
|
420
|
-
return this.getCommand(
|
|
527
|
+
return this.getCommand(safeName, scope, projectPath, safeNamespace);
|
|
421
528
|
}
|
|
422
529
|
|
|
423
530
|
/**
|
|
424
531
|
* 更新命令
|
|
425
532
|
*/
|
|
426
533
|
updateCommand({ name, scope, projectPath, namespace, description, allowedTools, argumentHint, agent, model, subtask, body }) {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const relativePath = namespace
|
|
432
|
-
? path.join(namespace, `${name}.md`)
|
|
433
|
-
: `${name}.md`;
|
|
434
|
-
|
|
435
|
-
const filePath = path.join(baseDir, relativePath);
|
|
534
|
+
const safeName = normalizeCommandName(name);
|
|
535
|
+
const safeNamespace = normalizeCommandNamespace(namespace);
|
|
536
|
+
const baseDir = this._getBaseDir(scope, projectPath);
|
|
537
|
+
const { fullPath: filePath } = this._resolveCommandPath(baseDir, safeName, safeNamespace);
|
|
436
538
|
|
|
437
539
|
if (!fs.existsSync(filePath)) {
|
|
438
|
-
throw new Error(`命令 "${
|
|
540
|
+
throw new Error(`命令 "${safeName}" 不存在`);
|
|
439
541
|
}
|
|
440
542
|
|
|
441
|
-
|
|
442
|
-
const frontmatterData = {};
|
|
443
|
-
if (description) frontmatterData.description = description;
|
|
444
|
-
if (this.platform !== 'opencode') {
|
|
445
|
-
if (allowedTools) frontmatterData['allowed-tools'] = allowedTools;
|
|
446
|
-
if (argumentHint) frontmatterData['argument-hint'] = argumentHint;
|
|
447
|
-
}
|
|
448
|
-
if (agent) frontmatterData.agent = agent;
|
|
449
|
-
if (model) frontmatterData.model = model;
|
|
450
|
-
if (typeof subtask === 'boolean') frontmatterData.subtask = subtask;
|
|
451
|
-
|
|
452
|
-
let content = '';
|
|
453
|
-
if (Object.keys(frontmatterData).length > 0) {
|
|
454
|
-
content = generateCommandFrontmatter(frontmatterData) + '\n\n';
|
|
455
|
-
}
|
|
456
|
-
content += body || '';
|
|
543
|
+
const content = this._generateCommandContent({ description, allowedTools, argumentHint, agent, model, subtask, body });
|
|
457
544
|
|
|
458
545
|
fs.writeFileSync(filePath, content, 'utf-8');
|
|
459
546
|
|
|
460
|
-
return this.getCommand(
|
|
547
|
+
return this.getCommand(safeName, scope, projectPath, safeNamespace);
|
|
461
548
|
}
|
|
462
549
|
|
|
463
550
|
/**
|
|
464
551
|
* 删除命令
|
|
465
552
|
*/
|
|
466
553
|
deleteCommand(name, scope, projectPath = null, namespace = null) {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const relativePath = namespace
|
|
472
|
-
? path.join(namespace, `${name}.md`)
|
|
473
|
-
: `${name}.md`;
|
|
474
|
-
|
|
475
|
-
const filePath = path.join(baseDir, relativePath);
|
|
554
|
+
const safeName = normalizeCommandName(name);
|
|
555
|
+
const safeNamespace = normalizeCommandNamespace(namespace);
|
|
556
|
+
const baseDir = this._getBaseDir(scope, projectPath);
|
|
557
|
+
const { fullPath: filePath } = this._resolveCommandPath(baseDir, safeName, safeNamespace);
|
|
476
558
|
|
|
477
559
|
if (!fs.existsSync(filePath)) {
|
|
478
560
|
return { success: false, message: '命令不存在' };
|
|
@@ -481,8 +563,8 @@ class CommandsService {
|
|
|
481
563
|
fs.unlinkSync(filePath);
|
|
482
564
|
|
|
483
565
|
// 如果目录为空,删除目录
|
|
484
|
-
if (
|
|
485
|
-
const namespaceDir =
|
|
566
|
+
if (safeNamespace) {
|
|
567
|
+
const namespaceDir = resolveInsideRoot(baseDir, safeNamespace, 'Command namespace');
|
|
486
568
|
try {
|
|
487
569
|
const remaining = fs.readdirSync(namespaceDir);
|
|
488
570
|
if (remaining.length === 0) {
|