@adversity/coding-tool-x 2.2.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/CHANGELOG.md +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rules 服务
|
|
3
|
+
*
|
|
4
|
+
* 管理 Claude Code 规则文件的 CRUD 操作
|
|
5
|
+
* 规则目录:
|
|
6
|
+
* - 用户级: ~/.claude/rules/
|
|
7
|
+
* - 项目级: .claude/rules/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
// 规则目录路径
|
|
15
|
+
const USER_RULES_DIR = path.join(os.homedir(), '.claude', 'rules');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 确保目录存在
|
|
19
|
+
*/
|
|
20
|
+
function ensureDir(dirPath) {
|
|
21
|
+
if (!fs.existsSync(dirPath)) {
|
|
22
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 解析 YAML frontmatter
|
|
28
|
+
*/
|
|
29
|
+
function parseFrontmatter(content) {
|
|
30
|
+
const result = {
|
|
31
|
+
frontmatter: {},
|
|
32
|
+
body: content
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// 移除 BOM
|
|
36
|
+
content = content.trim().replace(/^\uFEFF/, '');
|
|
37
|
+
|
|
38
|
+
// 解析 YAML frontmatter
|
|
39
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const frontmatterText = match[1];
|
|
45
|
+
result.body = match[2].trim();
|
|
46
|
+
|
|
47
|
+
// 简单解析 YAML(支持基本字段)
|
|
48
|
+
const lines = frontmatterText.split('\n');
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const colonIndex = line.indexOf(':');
|
|
51
|
+
if (colonIndex === -1) continue;
|
|
52
|
+
|
|
53
|
+
const key = line.slice(0, colonIndex).trim();
|
|
54
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
55
|
+
|
|
56
|
+
// 去除引号
|
|
57
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
58
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
59
|
+
value = value.slice(1, -1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result.frontmatter[key] = value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 生成 frontmatter 字符串
|
|
70
|
+
*/
|
|
71
|
+
function generateFrontmatter(data) {
|
|
72
|
+
const lines = ['---'];
|
|
73
|
+
|
|
74
|
+
if (data.paths) {
|
|
75
|
+
lines.push(`paths: ${data.paths}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push('---');
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 递归扫描目录获取规则文件
|
|
84
|
+
*/
|
|
85
|
+
function scanRulesDir(dir, basePath, scope) {
|
|
86
|
+
const rules = [];
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
return rules;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const fullPath = path.join(dir, entry.name);
|
|
97
|
+
|
|
98
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
99
|
+
// 递归扫描子目录
|
|
100
|
+
const subRules = scanRulesDir(fullPath, basePath, scope);
|
|
101
|
+
rules.push(...subRules);
|
|
102
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
103
|
+
// 解析规则文件
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
106
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
107
|
+
|
|
108
|
+
// 计算相对路径
|
|
109
|
+
const relativePath = path.relative(basePath, fullPath);
|
|
110
|
+
const fileName = entry.name.replace(/\.md$/, '');
|
|
111
|
+
const directory = path.dirname(relativePath);
|
|
112
|
+
|
|
113
|
+
rules.push({
|
|
114
|
+
name: fileName,
|
|
115
|
+
fileName,
|
|
116
|
+
directory: directory === '.' ? null : directory,
|
|
117
|
+
scope,
|
|
118
|
+
path: relativePath,
|
|
119
|
+
fullPath,
|
|
120
|
+
paths: frontmatter.paths || '', // 条件规则的路径模式
|
|
121
|
+
body,
|
|
122
|
+
fullContent: content,
|
|
123
|
+
updatedAt: fs.statSync(fullPath).mtime.getTime()
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.warn(`[RulesService] Failed to parse ${fullPath}:`, err.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(`[RulesService] Failed to scan ${dir}:`, err.message);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return rules;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Rules 服务类
|
|
139
|
+
*/
|
|
140
|
+
class RulesService {
|
|
141
|
+
constructor() {
|
|
142
|
+
this.userRulesDir = USER_RULES_DIR;
|
|
143
|
+
ensureDir(this.userRulesDir);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 获取所有规则列表
|
|
148
|
+
* @param {string} projectPath - 项目路径(可选,用于获取项目级规则)
|
|
149
|
+
*/
|
|
150
|
+
listRules(projectPath = null) {
|
|
151
|
+
const rules = [];
|
|
152
|
+
|
|
153
|
+
// 获取用户级规则
|
|
154
|
+
const userRules = scanRulesDir(this.userRulesDir, this.userRulesDir, 'user');
|
|
155
|
+
rules.push(...userRules);
|
|
156
|
+
|
|
157
|
+
// 获取项目级规则(如果提供了项目路径)
|
|
158
|
+
if (projectPath) {
|
|
159
|
+
const projectRulesDir = path.join(projectPath, '.claude', 'rules');
|
|
160
|
+
const projectRules = scanRulesDir(projectRulesDir, projectRulesDir, 'project');
|
|
161
|
+
rules.push(...projectRules);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 按路径排序
|
|
165
|
+
rules.sort((a, b) => a.path.toLowerCase().localeCompare(b.path.toLowerCase()));
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
rules,
|
|
169
|
+
total: rules.length,
|
|
170
|
+
userCount: userRules.length,
|
|
171
|
+
projectCount: rules.length - userRules.length
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 获取单个规则详情
|
|
177
|
+
*/
|
|
178
|
+
getRule(relativePath, scope, projectPath = null) {
|
|
179
|
+
const baseDir = scope === 'user'
|
|
180
|
+
? this.userRulesDir
|
|
181
|
+
: path.join(projectPath, '.claude', 'rules');
|
|
182
|
+
|
|
183
|
+
// 确保路径以 .md 结尾
|
|
184
|
+
const filePath = relativePath.endsWith('.md')
|
|
185
|
+
? path.join(baseDir, relativePath)
|
|
186
|
+
: path.join(baseDir, `${relativePath}.md`);
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(filePath)) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
193
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
194
|
+
|
|
195
|
+
const actualRelativePath = path.relative(baseDir, filePath);
|
|
196
|
+
const fileName = path.basename(filePath, '.md');
|
|
197
|
+
const directory = path.dirname(actualRelativePath);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
name: fileName,
|
|
201
|
+
fileName,
|
|
202
|
+
directory: directory === '.' ? null : directory,
|
|
203
|
+
scope,
|
|
204
|
+
path: actualRelativePath,
|
|
205
|
+
fullPath: filePath,
|
|
206
|
+
paths: frontmatter.paths || '',
|
|
207
|
+
body,
|
|
208
|
+
fullContent: content,
|
|
209
|
+
updatedAt: fs.statSync(filePath).mtime.getTime()
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 创建规则
|
|
215
|
+
*/
|
|
216
|
+
createRule({ fileName, scope, projectPath, directory, paths, body }) {
|
|
217
|
+
if (!fileName || !fileName.trim()) {
|
|
218
|
+
throw new Error('规则文件名不能为空');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// 验证文件名:只允许字母、数字、横杠、下划线
|
|
222
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(fileName)) {
|
|
223
|
+
throw new Error('规则文件名只能包含字母、数字、横杠和下划线');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const baseDir = scope === 'user'
|
|
227
|
+
? this.userRulesDir
|
|
228
|
+
: path.join(projectPath, '.claude', 'rules');
|
|
229
|
+
|
|
230
|
+
const targetDir = directory ? path.join(baseDir, directory) : baseDir;
|
|
231
|
+
ensureDir(targetDir);
|
|
232
|
+
|
|
233
|
+
const filePath = path.join(targetDir, `${fileName}.md`);
|
|
234
|
+
|
|
235
|
+
// 检查是否已存在
|
|
236
|
+
if (fs.existsSync(filePath)) {
|
|
237
|
+
throw new Error(`规则 "${fileName}" 已存在`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 生成文件内容
|
|
241
|
+
let content = '';
|
|
242
|
+
if (paths) {
|
|
243
|
+
content = generateFrontmatter({ paths }) + '\n\n';
|
|
244
|
+
}
|
|
245
|
+
content += body || '';
|
|
246
|
+
|
|
247
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
248
|
+
|
|
249
|
+
const relativePath = directory
|
|
250
|
+
? path.join(directory, `${fileName}.md`)
|
|
251
|
+
: `${fileName}.md`;
|
|
252
|
+
|
|
253
|
+
return this.getRule(relativePath, scope, projectPath);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 更新规则
|
|
258
|
+
*/
|
|
259
|
+
updateRule({ relativePath, scope, projectPath, paths, body }) {
|
|
260
|
+
const baseDir = scope === 'user'
|
|
261
|
+
? this.userRulesDir
|
|
262
|
+
: path.join(projectPath, '.claude', 'rules');
|
|
263
|
+
|
|
264
|
+
const filePath = path.join(baseDir, relativePath);
|
|
265
|
+
|
|
266
|
+
if (!fs.existsSync(filePath)) {
|
|
267
|
+
throw new Error(`规则 "${relativePath}" 不存在`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 生成文件内容
|
|
271
|
+
let content = '';
|
|
272
|
+
if (paths) {
|
|
273
|
+
content = generateFrontmatter({ paths }) + '\n\n';
|
|
274
|
+
}
|
|
275
|
+
content += body || '';
|
|
276
|
+
|
|
277
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
278
|
+
|
|
279
|
+
return this.getRule(relativePath, scope, projectPath);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* 删除规则
|
|
284
|
+
*/
|
|
285
|
+
deleteRule(relativePath, scope, projectPath = null) {
|
|
286
|
+
const baseDir = scope === 'user'
|
|
287
|
+
? this.userRulesDir
|
|
288
|
+
: path.join(projectPath, '.claude', 'rules');
|
|
289
|
+
|
|
290
|
+
const filePath = path.join(baseDir, relativePath);
|
|
291
|
+
|
|
292
|
+
if (!fs.existsSync(filePath)) {
|
|
293
|
+
return { success: false, message: '规则不存在' };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fs.unlinkSync(filePath);
|
|
297
|
+
|
|
298
|
+
// 如果目录为空,删除目录
|
|
299
|
+
const directory = path.dirname(relativePath);
|
|
300
|
+
if (directory && directory !== '.') {
|
|
301
|
+
const dirPath = path.join(baseDir, directory);
|
|
302
|
+
try {
|
|
303
|
+
const remaining = fs.readdirSync(dirPath);
|
|
304
|
+
if (remaining.length === 0) {
|
|
305
|
+
fs.rmdirSync(dirPath);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
// 忽略删除目录错误
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { success: true, message: '规则已删除' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 获取目录结构(树形)
|
|
317
|
+
*/
|
|
318
|
+
getDirectoryTree(projectPath = null) {
|
|
319
|
+
const tree = {
|
|
320
|
+
user: this.buildTree(this.userRulesDir),
|
|
321
|
+
project: null
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (projectPath) {
|
|
325
|
+
const projectRulesDir = path.join(projectPath, '.claude', 'rules');
|
|
326
|
+
tree.project = this.buildTree(projectRulesDir);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return tree;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 构建目录树
|
|
334
|
+
*/
|
|
335
|
+
buildTree(dir) {
|
|
336
|
+
if (!fs.existsSync(dir)) {
|
|
337
|
+
return { directories: [], files: [] };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const result = {
|
|
341
|
+
directories: [],
|
|
342
|
+
files: []
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
347
|
+
|
|
348
|
+
for (const entry of entries) {
|
|
349
|
+
if (entry.name.startsWith('.')) continue;
|
|
350
|
+
|
|
351
|
+
if (entry.isDirectory()) {
|
|
352
|
+
result.directories.push({
|
|
353
|
+
name: entry.name,
|
|
354
|
+
children: this.buildTree(path.join(dir, entry.name))
|
|
355
|
+
});
|
|
356
|
+
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
357
|
+
result.files.push(entry.name.replace(/\.md$/, ''));
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(`[RulesService] Failed to build tree for ${dir}:`, err.message);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* 获取统计信息
|
|
369
|
+
*/
|
|
370
|
+
getStats(projectPath = null) {
|
|
371
|
+
const { rules, userCount, projectCount } = this.listRules(projectPath);
|
|
372
|
+
|
|
373
|
+
// 按目录分组
|
|
374
|
+
const directories = {};
|
|
375
|
+
let conditionalCount = 0;
|
|
376
|
+
|
|
377
|
+
for (const rule of rules) {
|
|
378
|
+
const dir = rule.directory || '(root)';
|
|
379
|
+
if (!directories[dir]) {
|
|
380
|
+
directories[dir] = 0;
|
|
381
|
+
}
|
|
382
|
+
directories[dir]++;
|
|
383
|
+
|
|
384
|
+
if (rule.paths) {
|
|
385
|
+
conditionalCount++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
total: rules.length,
|
|
391
|
+
userCount,
|
|
392
|
+
projectCount,
|
|
393
|
+
conditionalCount,
|
|
394
|
+
directories
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
RulesService
|
|
401
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const PROJECTS_CACHE_TTL = 30 * 1000; // 30s
|
|
6
|
+
const projectsCache = new Map();
|
|
7
|
+
|
|
8
|
+
const HAS_MESSAGES_CACHE_LIMIT = 50000;
|
|
9
|
+
const hasMessagesCache = new Map();
|
|
10
|
+
let hasMessagesPersisted = {};
|
|
11
|
+
let hasMessagesPersistTimer = null;
|
|
12
|
+
|
|
13
|
+
function getCcToolDir() {
|
|
14
|
+
return path.join(os.homedir(), '.claude', 'cc-tool');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDirExists(dir) {
|
|
18
|
+
if (!fs.existsSync(dir)) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getProjectsCacheKey(config) {
|
|
24
|
+
return config?.projectsDir || '__default__';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getCachedProjects(config) {
|
|
28
|
+
const cacheEntry = projectsCache.get(getProjectsCacheKey(config));
|
|
29
|
+
if (!cacheEntry) return null;
|
|
30
|
+
if ((Date.now() - cacheEntry.timestamp) > PROJECTS_CACHE_TTL) {
|
|
31
|
+
projectsCache.delete(getProjectsCacheKey(config));
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return cacheEntry.data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setCachedProjects(config, data) {
|
|
38
|
+
projectsCache.set(getProjectsCacheKey(config), {
|
|
39
|
+
data,
|
|
40
|
+
timestamp: Date.now()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function invalidateProjectsCache(configOrPath) {
|
|
45
|
+
if (!configOrPath) {
|
|
46
|
+
projectsCache.clear();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const key = typeof configOrPath === 'string'
|
|
50
|
+
? configOrPath
|
|
51
|
+
: getProjectsCacheKey(configOrPath);
|
|
52
|
+
projectsCache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hasMessagesCacheFile = path.join(getCcToolDir(), 'session-has-cache.json');
|
|
56
|
+
loadHasMessagesCacheFromDisk();
|
|
57
|
+
|
|
58
|
+
function loadHasMessagesCacheFromDisk() {
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(hasMessagesCacheFile)) {
|
|
61
|
+
hasMessagesPersisted = {};
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const raw = fs.readFileSync(hasMessagesCacheFile, 'utf8');
|
|
65
|
+
const parsed = JSON.parse(raw) || {};
|
|
66
|
+
hasMessagesPersisted = parsed;
|
|
67
|
+
Object.entries(parsed).forEach(([filePath, entry]) => {
|
|
68
|
+
if (!entry || typeof entry !== 'object') return;
|
|
69
|
+
const { size, mtimeMs, value } = entry;
|
|
70
|
+
if (typeof size === 'number' && typeof mtimeMs === 'number' && typeof value === 'boolean') {
|
|
71
|
+
hasMessagesCache.set(`${filePath}:${size}:${mtimeMs}`, value);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
hasMessagesPersisted = {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function checkHasMessagesCache(filePath, stats) {
|
|
80
|
+
if (!filePath || !stats) return undefined;
|
|
81
|
+
const cacheKey = `${filePath}:${stats.size}:${stats.mtimeMs}`;
|
|
82
|
+
if (!hasMessagesCache.has(cacheKey)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return hasMessagesCache.get(cacheKey);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function rememberHasMessages(filePath, stats, value) {
|
|
89
|
+
if (!filePath || !stats) return;
|
|
90
|
+
const cacheKey = `${filePath}:${stats.size}:${stats.mtimeMs}`;
|
|
91
|
+
if (hasMessagesCache.size >= HAS_MESSAGES_CACHE_LIMIT) {
|
|
92
|
+
const firstKey = hasMessagesCache.keys().next().value;
|
|
93
|
+
if (firstKey) {
|
|
94
|
+
hasMessagesCache.delete(firstKey);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
hasMessagesCache.set(cacheKey, value);
|
|
98
|
+
|
|
99
|
+
hasMessagesPersisted[filePath] = {
|
|
100
|
+
size: stats.size,
|
|
101
|
+
mtimeMs: stats.mtimeMs,
|
|
102
|
+
value
|
|
103
|
+
};
|
|
104
|
+
schedulePersistHasMessagesCache();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function schedulePersistHasMessagesCache() {
|
|
108
|
+
if (hasMessagesPersistTimer) return;
|
|
109
|
+
hasMessagesPersistTimer = setTimeout(() => {
|
|
110
|
+
try {
|
|
111
|
+
ensureDirExists(path.dirname(hasMessagesCacheFile));
|
|
112
|
+
fs.writeFileSync(hasMessagesCacheFile, JSON.stringify(hasMessagesPersisted, null, 2), 'utf8');
|
|
113
|
+
} catch (err) {
|
|
114
|
+
// ignore persistence errors
|
|
115
|
+
} finally {
|
|
116
|
+
hasMessagesPersistTimer = null;
|
|
117
|
+
}
|
|
118
|
+
}, 1000);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
getCachedProjects,
|
|
123
|
+
setCachedProjects,
|
|
124
|
+
invalidateProjectsCache,
|
|
125
|
+
checkHasMessagesCache,
|
|
126
|
+
rememberHasMessages
|
|
127
|
+
};
|