autosnippet 3.0.11 → 3.1.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/bin/api-server.js +2 -0
- package/bin/cli.js +84 -16
- package/config/default.json +10 -1
- package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
- package/dashboard/dist/index.html +1 -1
- package/lib/bootstrap.js +4 -4
- package/lib/cli/SetupService.js +116 -29
- package/lib/cli/UpgradeService.js +16 -6
- package/lib/core/AstAnalyzer.js +1 -1
- package/lib/core/ast/ensure-grammars.js +1 -1
- package/lib/core/ast/index.js +62 -11
- package/lib/core/ast/lang-dart.js +27 -21
- package/lib/core/ast/lang-go.js +6 -20
- package/lib/core/ast/lang-rust.js +53 -28
- package/lib/core/ast/parser-init.js +9 -5
- package/lib/core/discovery/DartDiscoverer.js +4 -10
- package/lib/core/discovery/GenericDiscoverer.js +4 -28
- package/lib/core/discovery/GoDiscoverer.js +45 -25
- package/lib/core/discovery/NodeDiscoverer.js +1 -3
- package/lib/core/discovery/PythonDiscoverer.js +7 -1
- package/lib/core/discovery/RustDiscoverer.js +111 -38
- package/lib/core/discovery/index.js +2 -2
- package/lib/core/enhancement/django-enhancement.js +10 -4
- package/lib/core/enhancement/fastapi-enhancement.js +16 -9
- package/lib/core/enhancement/go-grpc-enhancement.js +2 -1
- package/lib/core/enhancement/go-web-enhancement.js +3 -6
- package/lib/core/enhancement/ml-enhancement.js +6 -3
- package/lib/core/enhancement/nextjs-enhancement.js +17 -7
- package/lib/core/enhancement/node-server-enhancement.js +4 -2
- package/lib/core/enhancement/react-enhancement.js +6 -3
- package/lib/core/enhancement/rust-tokio-enhancement.js +6 -2
- package/lib/core/enhancement/rust-web-enhancement.js +13 -7
- package/lib/core/enhancement/vue-enhancement.js +10 -5
- package/lib/external/ai/AiFactory.js +3 -1
- package/lib/external/ai/AiProvider.js +3 -1
- package/lib/external/mcp/McpServer.js +2 -0
- package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +245 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +86 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +629 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +131 -348
- package/lib/external/mcp/handlers/bootstrap/refine.js +364 -0
- package/lib/external/mcp/handlers/bootstrap.js +7 -597
- package/lib/external/mcp/handlers/browse.js +123 -9
- package/lib/external/mcp/handlers/guard.js +29 -6
- package/lib/external/mcp/handlers/search.js +56 -24
- package/lib/external/mcp/handlers/skill.js +6 -2
- package/lib/http/HttpServer.js +1 -1
- package/lib/http/routes/candidates.js +3 -1
- package/lib/http/routes/extract.js +4 -5
- package/lib/http/routes/guardRules.js +9 -17
- package/lib/http/routes/modules.js +9 -3
- package/lib/http/routes/skills.js +54 -6
- package/lib/http/routes/violations.js +4 -3
- package/lib/infrastructure/external/ClipboardManager.js +24 -7
- package/lib/infrastructure/external/NativeUi.js +3 -1
- package/lib/infrastructure/external/OpenBrowser.js +1 -0
- package/lib/infrastructure/external/XcodeAutomation.js +5 -5
- package/lib/infrastructure/vector/IndexingPipeline.js +14 -5
- package/lib/injection/ServiceContainer.js +45 -13
- package/lib/platform/ios/index.js +20 -25
- package/lib/platform/ios/routes/spm.js +6 -3
- package/lib/platform/ios/snippet/PlaceholderConverter.js +6 -2
- package/lib/platform/ios/snippet/XcodeCodec.js +4 -2
- package/lib/platform/ios/spm/SpmDiscoverer.js +1 -1
- package/lib/platform/ios/spm/SpmService.js +3 -1
- package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
- package/lib/platform/ios/xcode/XcodeIntegration.js +43 -664
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +225 -0
- package/lib/service/automation/FileWatcher.js +1 -3
- package/lib/service/automation/handlers/CreateHandler.js +3 -5
- package/lib/service/automation/handlers/GuardHandler.js +11 -32
- package/lib/service/automation/handlers/SearchHandler.js +9 -9
- package/lib/service/chat/CandidateGuardrail.js +11 -6
- package/lib/service/chat/ChatAgent.js +51 -421
- package/lib/service/chat/ChatAgentPrompts.js +149 -0
- package/lib/service/chat/ChatAgentTasks.js +297 -0
- package/lib/service/chat/HandoffProtocol.js +5 -2
- package/lib/service/chat/tools/_shared.js +61 -0
- package/lib/service/chat/tools/ai-analysis.js +284 -0
- package/lib/service/chat/tools/ast-graph.js +681 -0
- package/lib/service/chat/tools/composite.js +497 -0
- package/lib/service/chat/tools/guard.js +265 -0
- package/lib/service/chat/tools/index.js +239 -0
- package/lib/service/chat/tools/infrastructure.js +227 -0
- package/lib/service/chat/tools/knowledge-graph.js +234 -0
- package/lib/service/chat/tools/lifecycle.js +486 -0
- package/lib/service/chat/tools/project-access.js +919 -0
- package/lib/service/chat/tools/query.js +264 -0
- package/lib/service/chat/tools.js +13 -3994
- package/lib/service/cursor/AgentInstructionsGenerator.js +413 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +71 -11
- package/lib/service/cursor/FileProtection.js +116 -0
- package/lib/service/cursor/KnowledgeCompressor.js +70 -11
- package/lib/service/cursor/SkillsSyncer.js +5 -3
- package/lib/service/cursor/TopicClassifier.js +19 -3
- package/lib/service/guard/ComplianceReporter.js +5 -2
- package/lib/service/guard/ExclusionManager.js +26 -2
- package/lib/service/guard/GuardCheckEngine.js +83 -388
- package/lib/service/guard/GuardCodeChecks.js +391 -0
- package/lib/service/guard/GuardCrossFileChecks.js +326 -0
- package/lib/service/guard/GuardPatternUtils.js +187 -0
- package/lib/service/guard/GuardService.js +80 -38
- package/lib/service/module/ModuleService.js +181 -56
- package/lib/service/recipe/RecipeCandidateValidator.js +11 -8
- package/lib/service/search/SearchEngine.js +10 -2
- package/lib/service/snippet/SnippetFactory.js +3 -3
- package/lib/service/snippet/SnippetInstaller.js +35 -11
- package/lib/service/snippet/codecs/VSCodeCodec.js +2 -2
- package/lib/service/wiki/WikiGenerator.js +247 -1535
- package/lib/service/wiki/WikiRenderers.js +1903 -0
- package/lib/service/wiki/WikiUtils.js +1044 -0
- package/lib/shared/LanguageService.js +359 -2
- package/lib/shared/PathGuard.js +0 -8
- package/package.json +3 -9
- package/scripts/bench-real-projects.mjs +29 -29
- package/scripts/generate-recipe-drafts.js +17 -27
- package/scripts/init-snippets.js +43 -24
- package/scripts/install-vscode-copilot.js +3 -19
- package/scripts/setup-mcp-config.js +0 -4
|
@@ -6,26 +6,69 @@
|
|
|
6
6
|
* 语言特有操作(如 SPM 依赖管理)由对应的 Discoverer / Service 直接暴露,不经此类代理。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
9
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
10
|
+
import {
|
|
11
|
+
basename as _pathBasename,
|
|
12
|
+
extname as _pathExtname,
|
|
13
|
+
isAbsolute as _pathIsAbsolute,
|
|
14
|
+
join as _pathJoin,
|
|
15
|
+
relative,
|
|
16
|
+
} from 'node:path';
|
|
12
17
|
import { getDiscovererRegistry } from '../../core/discovery/index.js';
|
|
13
18
|
import { inferLang } from '../../external/mcp/handlers/LanguageExtensions.js';
|
|
19
|
+
import Logger from '../../infrastructure/logging/Logger.js';
|
|
14
20
|
|
|
15
21
|
/** 全局排除目录 */
|
|
16
22
|
const SCAN_EXCLUDE_DIRS = new Set([
|
|
17
|
-
'node_modules',
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
23
|
+
'node_modules',
|
|
24
|
+
'.git',
|
|
25
|
+
'dist',
|
|
26
|
+
'build',
|
|
27
|
+
'.next',
|
|
28
|
+
'Pods',
|
|
29
|
+
'Carthage',
|
|
30
|
+
'.build',
|
|
31
|
+
'DerivedData',
|
|
32
|
+
'vendor',
|
|
33
|
+
'__pycache__',
|
|
34
|
+
'.venv',
|
|
35
|
+
'venv',
|
|
36
|
+
'target',
|
|
37
|
+
'.gradle',
|
|
38
|
+
'.idea',
|
|
39
|
+
'out',
|
|
40
|
+
'coverage',
|
|
41
|
+
'.cache',
|
|
42
|
+
'.tox',
|
|
43
|
+
'.mypy_cache',
|
|
44
|
+
'.pytest_cache',
|
|
45
|
+
'AutoSnippet',
|
|
21
46
|
]);
|
|
22
47
|
|
|
23
48
|
/** 源码文件扩展名 */
|
|
24
49
|
const SOURCE_CODE_EXTS = new Set([
|
|
25
|
-
'.swift',
|
|
26
|
-
'.
|
|
27
|
-
'.
|
|
28
|
-
'.
|
|
50
|
+
'.swift',
|
|
51
|
+
'.m',
|
|
52
|
+
'.mm',
|
|
53
|
+
'.h',
|
|
54
|
+
'.js',
|
|
55
|
+
'.ts',
|
|
56
|
+
'.tsx',
|
|
57
|
+
'.jsx',
|
|
58
|
+
'.mjs',
|
|
59
|
+
'.cjs',
|
|
60
|
+
'.py',
|
|
61
|
+
'.java',
|
|
62
|
+
'.kt',
|
|
63
|
+
'.kts',
|
|
64
|
+
'.go',
|
|
65
|
+
'.rs',
|
|
66
|
+
'.rb',
|
|
67
|
+
'.vue',
|
|
68
|
+
'.svelte',
|
|
69
|
+
'.c',
|
|
70
|
+
'.cpp',
|
|
71
|
+
'.cs',
|
|
29
72
|
]);
|
|
30
73
|
|
|
31
74
|
export class ModuleService {
|
|
@@ -82,7 +125,9 @@ export class ModuleService {
|
|
|
82
125
|
* 自动检测项目类型并加载所有匹配的 Discoverer
|
|
83
126
|
*/
|
|
84
127
|
async load() {
|
|
85
|
-
if (this.#loaded)
|
|
128
|
+
if (this.#loaded) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
86
131
|
|
|
87
132
|
const matches = await this.#registry.detectAll(this.#projectRoot);
|
|
88
133
|
this.#activeDiscoverers = [];
|
|
@@ -143,12 +188,16 @@ export class ModuleService {
|
|
|
143
188
|
|
|
144
189
|
// 第一遍:加载非 generic 的 Discoverer(真实项目结构识别器)
|
|
145
190
|
for (const { discoverer } of this.#activeDiscoverers) {
|
|
146
|
-
if (discoverer.id === 'generic')
|
|
191
|
+
if (discoverer.id === 'generic') {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
147
194
|
try {
|
|
148
195
|
const targets = await discoverer.listTargets();
|
|
149
196
|
for (const t of targets) {
|
|
150
197
|
const key = `${discoverer.id}::${t.name}`;
|
|
151
|
-
if (seenNames.has(key))
|
|
198
|
+
if (seenNames.has(key)) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
152
201
|
seenNames.add(key);
|
|
153
202
|
allTargets.push(this.#normalizeTarget(t, discoverer));
|
|
154
203
|
hasRealDiscovererTargets = true;
|
|
@@ -163,12 +212,16 @@ export class ModuleService {
|
|
|
163
212
|
// 第二遍:仅当没有真实 Discoverer 产出 target 时,才加载 GenericDiscoverer 的结果(兜底)
|
|
164
213
|
if (!hasRealDiscovererTargets) {
|
|
165
214
|
for (const { discoverer } of this.#activeDiscoverers) {
|
|
166
|
-
if (discoverer.id !== 'generic')
|
|
215
|
+
if (discoverer.id !== 'generic') {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
167
218
|
try {
|
|
168
219
|
const targets = await discoverer.listTargets();
|
|
169
220
|
for (const t of targets) {
|
|
170
221
|
const key = `${discoverer.id}::${t.name}`;
|
|
171
|
-
if (seenNames.has(key))
|
|
222
|
+
if (seenNames.has(key)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
172
225
|
seenNames.add(key);
|
|
173
226
|
allTargets.push(this.#normalizeTarget(t, discoverer));
|
|
174
227
|
}
|
|
@@ -235,14 +288,14 @@ export class ModuleService {
|
|
|
235
288
|
if (targets.some((t) => t.name === targetObj.name)) {
|
|
236
289
|
return discoverer.getTargetFiles(targetObj);
|
|
237
290
|
}
|
|
238
|
-
} catch {
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
291
|
+
} catch {}
|
|
241
292
|
}
|
|
242
293
|
|
|
243
294
|
// 兜底:如果 target 有 path 属性且目录存在,直接收集
|
|
244
295
|
if (targetObj.path && existsSync(targetObj.path)) {
|
|
245
|
-
this.#logger.info(
|
|
296
|
+
this.#logger.info(
|
|
297
|
+
`[ModuleService] getTargetFiles fallback: collecting from ${targetObj.path}`
|
|
298
|
+
);
|
|
246
299
|
return this.#collectFolderFiles(targetObj.path);
|
|
247
300
|
}
|
|
248
301
|
|
|
@@ -313,6 +366,7 @@ export class ModuleService {
|
|
|
313
366
|
|
|
314
367
|
return {
|
|
315
368
|
projectRoot: this.#projectRoot,
|
|
369
|
+
projectName: _pathBasename(this.#projectRoot) || '',
|
|
316
370
|
primaryLanguage: primaryDiscoverer
|
|
317
371
|
? this.#discovererToLanguage(primaryDiscoverer.id)
|
|
318
372
|
: 'unknown',
|
|
@@ -458,7 +512,9 @@ export class ModuleService {
|
|
|
458
512
|
const fileList = await this.getTargetFiles(t);
|
|
459
513
|
for (const f of fileList) {
|
|
460
514
|
const fp = typeof f === 'string' ? f : f.path;
|
|
461
|
-
if (seenPaths.has(fp))
|
|
515
|
+
if (seenPaths.has(fp)) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
462
518
|
seenPaths.add(fp);
|
|
463
519
|
try {
|
|
464
520
|
const content = readFileSync(fp, 'utf8');
|
|
@@ -469,19 +525,27 @@ export class ModuleService {
|
|
|
469
525
|
content,
|
|
470
526
|
targetName: t.name,
|
|
471
527
|
});
|
|
472
|
-
} catch {
|
|
473
|
-
|
|
528
|
+
} catch {
|
|
529
|
+
/* unreadable */
|
|
530
|
+
}
|
|
531
|
+
if (allFiles.length >= MAX_FILES) {
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
474
534
|
}
|
|
475
535
|
} catch (e) {
|
|
476
536
|
this.#logger.warn(`[ModuleService] scanProject: skipping module ${t.name}: ${e.message}`);
|
|
477
537
|
}
|
|
478
|
-
if (allFiles.length >= MAX_FILES)
|
|
538
|
+
if (allFiles.length >= MAX_FILES) {
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
479
541
|
}
|
|
480
542
|
}
|
|
481
543
|
|
|
482
544
|
// 如果没有 target 收集到文件,回退到目录扫描
|
|
483
545
|
if (allFiles.length === 0) {
|
|
484
|
-
this.#logger.info(
|
|
546
|
+
this.#logger.info(
|
|
547
|
+
'[ModuleService] scanProject: No module targets, falling back to directory scan'
|
|
548
|
+
);
|
|
485
549
|
this.#walkProjectForFiles(allFiles, seenPaths, MAX_FILES);
|
|
486
550
|
}
|
|
487
551
|
|
|
@@ -610,9 +674,7 @@ export class ModuleService {
|
|
|
610
674
|
* @returns {Promise<Array<{ name: string, path: string, depth: number, language: string, sourceFileCount: number, hasSourceFiles: boolean }>>}
|
|
611
675
|
*/
|
|
612
676
|
async browseDirectories(basePath = '', maxDepth = 2) {
|
|
613
|
-
const root = basePath
|
|
614
|
-
? _pathJoin(this.#projectRoot, basePath)
|
|
615
|
-
: this.#projectRoot;
|
|
677
|
+
const root = basePath ? _pathJoin(this.#projectRoot, basePath) : this.#projectRoot;
|
|
616
678
|
|
|
617
679
|
if (!existsSync(root)) {
|
|
618
680
|
return [];
|
|
@@ -726,7 +788,9 @@ export class ModuleService {
|
|
|
726
788
|
createProvider,
|
|
727
789
|
} = this.#aiFactory;
|
|
728
790
|
let ai = await getProviderWithFallback();
|
|
729
|
-
if (!ai)
|
|
791
|
+
if (!ai) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
730
794
|
|
|
731
795
|
// 加载语言参考 Skill
|
|
732
796
|
let extractOpts = {};
|
|
@@ -740,7 +804,9 @@ export class ModuleService {
|
|
|
740
804
|
extractOpts = { skillReference: skillCtx.languageSkill.substring(0, 2000) };
|
|
741
805
|
}
|
|
742
806
|
}
|
|
743
|
-
} catch {
|
|
807
|
+
} catch {
|
|
808
|
+
/* Skills not available */
|
|
809
|
+
}
|
|
744
810
|
|
|
745
811
|
try {
|
|
746
812
|
return await ai.extractRecipes(targetName, files, extractOpts);
|
|
@@ -797,13 +863,21 @@ export class ModuleService {
|
|
|
797
863
|
* 目录遍历 — 浏览子目录结构
|
|
798
864
|
*/
|
|
799
865
|
#walkDirsForBrowse(dir, dirs, depth, maxDepth) {
|
|
800
|
-
if (depth >= maxDepth)
|
|
866
|
+
if (depth >= maxDepth) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
801
869
|
try {
|
|
802
870
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
803
871
|
for (const entry of entries) {
|
|
804
|
-
if (!entry.isDirectory())
|
|
805
|
-
|
|
806
|
-
|
|
872
|
+
if (!entry.isDirectory()) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (entry.name.startsWith('.')) {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (SCAN_EXCLUDE_DIRS.has(entry.name)) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
807
881
|
|
|
808
882
|
const fullPath = _pathJoin(dir, entry.name);
|
|
809
883
|
const relativePath = relative(this.#projectRoot, fullPath);
|
|
@@ -825,14 +899,18 @@ export class ModuleService {
|
|
|
825
899
|
|
|
826
900
|
this.#walkDirsForBrowse(fullPath, dirs, depth + 1, maxDepth);
|
|
827
901
|
}
|
|
828
|
-
} catch {
|
|
902
|
+
} catch {
|
|
903
|
+
/* skip */
|
|
904
|
+
}
|
|
829
905
|
}
|
|
830
906
|
|
|
831
907
|
/**
|
|
832
908
|
* 递归统计目录下源码文件数(限深度 + 上限 999 防止超大目录卡顿)
|
|
833
909
|
*/
|
|
834
910
|
#countSourceFilesDeep(dir, maxDepth, depth = 0) {
|
|
835
|
-
if (depth >= maxDepth)
|
|
911
|
+
if (depth >= maxDepth) {
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
836
914
|
let count = 0;
|
|
837
915
|
try {
|
|
838
916
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -842,9 +920,13 @@ export class ModuleService {
|
|
|
842
920
|
} else if (e.isDirectory() && !e.name.startsWith('.') && !SCAN_EXCLUDE_DIRS.has(e.name)) {
|
|
843
921
|
count += this.#countSourceFilesDeep(_pathJoin(dir, e.name), maxDepth, depth + 1);
|
|
844
922
|
}
|
|
845
|
-
if (count >= 999)
|
|
923
|
+
if (count >= 999) {
|
|
924
|
+
return count;
|
|
925
|
+
}
|
|
846
926
|
}
|
|
847
|
-
} catch {
|
|
927
|
+
} catch {
|
|
928
|
+
/* skip */
|
|
929
|
+
}
|
|
848
930
|
return count;
|
|
849
931
|
}
|
|
850
932
|
|
|
@@ -861,12 +943,18 @@ export class ModuleService {
|
|
|
861
943
|
* 递归收集源码文件
|
|
862
944
|
*/
|
|
863
945
|
#walkCollectSourceFiles(dir, rootDir, files, depth, maxDepth) {
|
|
864
|
-
if (depth > maxDepth || files.length > 500)
|
|
946
|
+
if (depth > maxDepth || files.length > 500) {
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
865
949
|
try {
|
|
866
950
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
867
951
|
for (const entry of entries) {
|
|
868
|
-
if (entry.name.startsWith('.'))
|
|
869
|
-
|
|
952
|
+
if (entry.name.startsWith('.')) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (SCAN_EXCLUDE_DIRS.has(entry.name)) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
870
958
|
|
|
871
959
|
const fullPath = _pathJoin(dir, entry.name);
|
|
872
960
|
if (entry.isDirectory()) {
|
|
@@ -883,7 +971,9 @@ export class ModuleService {
|
|
|
883
971
|
}
|
|
884
972
|
}
|
|
885
973
|
}
|
|
886
|
-
} catch {
|
|
974
|
+
} catch {
|
|
975
|
+
/* skip */
|
|
976
|
+
}
|
|
887
977
|
}
|
|
888
978
|
|
|
889
979
|
/**
|
|
@@ -894,15 +984,21 @@ export class ModuleService {
|
|
|
894
984
|
try {
|
|
895
985
|
const entries = readdirSync(dirPath, { withFileTypes: true });
|
|
896
986
|
for (const entry of entries) {
|
|
897
|
-
if (!entry.isFile())
|
|
987
|
+
if (!entry.isFile()) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
898
990
|
const ext = _pathExtname(entry.name).toLowerCase();
|
|
899
|
-
if (!SOURCE_CODE_EXTS.has(ext))
|
|
991
|
+
if (!SOURCE_CODE_EXTS.has(ext)) {
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
900
994
|
const lang = inferLang(entry.name);
|
|
901
995
|
if (lang) {
|
|
902
996
|
langCount[lang] = (langCount[lang] || 0) + 1;
|
|
903
997
|
}
|
|
904
998
|
}
|
|
905
|
-
} catch {
|
|
999
|
+
} catch {
|
|
1000
|
+
/* skip */
|
|
1001
|
+
}
|
|
906
1002
|
|
|
907
1003
|
let maxLang = 'unknown';
|
|
908
1004
|
let maxCount = 0;
|
|
@@ -920,29 +1016,56 @@ export class ModuleService {
|
|
|
920
1016
|
*/
|
|
921
1017
|
#walkProjectForFiles(allFiles, seenPaths, maxFiles) {
|
|
922
1018
|
const srcDirs = [
|
|
923
|
-
'Sources',
|
|
924
|
-
'
|
|
1019
|
+
'Sources',
|
|
1020
|
+
'src',
|
|
1021
|
+
'lib',
|
|
1022
|
+
'app',
|
|
1023
|
+
'pages',
|
|
1024
|
+
'components',
|
|
1025
|
+
'modules',
|
|
1026
|
+
'packages',
|
|
1027
|
+
'cmd',
|
|
1028
|
+
'internal',
|
|
1029
|
+
'pkg',
|
|
925
1030
|
];
|
|
926
1031
|
|
|
927
1032
|
const walkDir = (dir, targetName) => {
|
|
928
|
-
if (allFiles.length >= maxFiles)
|
|
1033
|
+
if (allFiles.length >= maxFiles) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
929
1036
|
let entries;
|
|
930
|
-
try {
|
|
1037
|
+
try {
|
|
1038
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1039
|
+
} catch {
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
931
1042
|
for (const ent of entries) {
|
|
932
|
-
if (allFiles.length >= maxFiles)
|
|
933
|
-
|
|
1043
|
+
if (allFiles.length >= maxFiles) {
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
if (ent.name.startsWith('.')) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
934
1049
|
const fp = _pathJoin(dir, ent.name);
|
|
935
1050
|
if (ent.isDirectory()) {
|
|
936
|
-
if (SCAN_EXCLUDE_DIRS.has(ent.name))
|
|
1051
|
+
if (SCAN_EXCLUDE_DIRS.has(ent.name)) {
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
937
1054
|
walkDir(fp, targetName);
|
|
938
1055
|
} else if (ent.isFile() && SOURCE_CODE_EXTS.has(_pathExtname(ent.name).toLowerCase())) {
|
|
939
|
-
if (seenPaths.has(fp))
|
|
1056
|
+
if (seenPaths.has(fp)) {
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
940
1059
|
seenPaths.add(fp);
|
|
941
1060
|
try {
|
|
942
1061
|
const st = statSync(fp);
|
|
943
|
-
if (st.size > 512 * 1024)
|
|
1062
|
+
if (st.size > 512 * 1024) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
944
1065
|
const content = readFileSync(fp, 'utf8');
|
|
945
|
-
if (content.split('\n').length < 5)
|
|
1066
|
+
if (content.split('\n').length < 5) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
946
1069
|
allFiles.push({
|
|
947
1070
|
name: ent.name,
|
|
948
1071
|
path: fp,
|
|
@@ -950,7 +1073,9 @@ export class ModuleService {
|
|
|
950
1073
|
content,
|
|
951
1074
|
targetName,
|
|
952
1075
|
});
|
|
953
|
-
} catch {
|
|
1076
|
+
} catch {
|
|
1077
|
+
/* unreadable */
|
|
1078
|
+
}
|
|
954
1079
|
}
|
|
955
1080
|
}
|
|
956
1081
|
};
|
|
@@ -9,17 +9,18 @@ import { LanguageService } from '../../shared/LanguageService.js';
|
|
|
9
9
|
|
|
10
10
|
/* ── V3 必填字段 ── */
|
|
11
11
|
const REQUIRED_FIELDS = [
|
|
12
|
-
'title',
|
|
13
|
-
'trigger',
|
|
14
|
-
'category',
|
|
15
|
-
'language',
|
|
16
|
-
'kind',
|
|
17
|
-
'doClause',
|
|
12
|
+
'title', // 中文简短标题
|
|
13
|
+
'trigger', // @前缀 kebab-case
|
|
14
|
+
'category', // View/Service/Tool/Model/Network/Storage/UI/Utility
|
|
15
|
+
'language', // 编程语言标识
|
|
16
|
+
'kind', // rule / pattern / fact
|
|
17
|
+
'doClause', // 英文祈使句正向指令
|
|
18
18
|
'description', // 中文简述 ≤80字
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
/* ── 需要 content 子对象有内容 ── */
|
|
22
|
-
|
|
22
|
+
// NOTE: reserved for future content sub-field validation
|
|
23
|
+
// const REQUIRED_CONTENT_FIELDS = ['pattern', 'markdown', 'rationale'];
|
|
23
24
|
|
|
24
25
|
const VALID_CATEGORIES = new Set([
|
|
25
26
|
'view',
|
|
@@ -94,7 +95,9 @@ export class RecipeCandidateValidator {
|
|
|
94
95
|
|
|
95
96
|
// ── category 合法性 ──
|
|
96
97
|
if (candidate.category && !VALID_CATEGORIES.has(candidate.category.toLowerCase())) {
|
|
97
|
-
warnings.push(
|
|
98
|
+
warnings.push(
|
|
99
|
+
`category "${candidate.category}" 不在标准列表(View/Service/Tool/Model/Network/Storage/UI/Utility)`
|
|
100
|
+
);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
// ── language 合法性 ──
|
|
@@ -659,7 +659,7 @@ export class SearchEngine {
|
|
|
659
659
|
}
|
|
660
660
|
|
|
661
661
|
/**
|
|
662
|
-
* 补充详细字段(content / description / trigger
|
|
662
|
+
* 补充详细字段(content / description / trigger / delivery 字段)— 批量 IN 查询
|
|
663
663
|
* 用于向量搜索结果与 BM25 结果的一致性
|
|
664
664
|
*/
|
|
665
665
|
_supplementDetails(items) {
|
|
@@ -674,7 +674,8 @@ export class SearchEngine {
|
|
|
674
674
|
rows = this.db
|
|
675
675
|
.prepare(
|
|
676
676
|
`SELECT id, content, description, trigger, headers, moduleName,
|
|
677
|
-
tags, language, category, updatedAt, createdAt, quality, stats, difficulty
|
|
677
|
+
tags, language, category, updatedAt, createdAt, quality, stats, difficulty,
|
|
678
|
+
whenClause, doClause
|
|
678
679
|
FROM knowledge_entries WHERE id IN (${placeholders})`
|
|
679
680
|
)
|
|
680
681
|
.all(...ids);
|
|
@@ -694,6 +695,13 @@ export class SearchEngine {
|
|
|
694
695
|
if (row.moduleName) {
|
|
695
696
|
item.moduleName = row.moduleName;
|
|
696
697
|
}
|
|
698
|
+
// Cursor 交付字段 — 供 Agent 投影生成 actionHint
|
|
699
|
+
if (!item.whenClause && row.whenClause) {
|
|
700
|
+
item.whenClause = row.whenClause;
|
|
701
|
+
}
|
|
702
|
+
if (!item.doClause && row.doClause) {
|
|
703
|
+
item.doClause = row.doClause;
|
|
704
|
+
}
|
|
697
705
|
// 排序信号补充 — 确保 Funnel/Ranker 有真实数据
|
|
698
706
|
if (!item.language && row.language) {
|
|
699
707
|
item.language = row.language;
|
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* factory.generate(spec, 'xcode') — 按 target 生成
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { XcodeCodec } from './codecs/XcodeCodec.js';
|
|
15
|
-
|
|
16
14
|
export class SnippetFactory {
|
|
17
15
|
/** @type {Map<string, import('./codecs/SnippetCodec.js').SnippetCodec>} */
|
|
18
16
|
#codecs = new Map();
|
|
@@ -174,7 +172,9 @@ export class SnippetFactory {
|
|
|
174
172
|
#resolveCodec(target) {
|
|
175
173
|
const codec = this.#codecs.get(target);
|
|
176
174
|
if (!codec) {
|
|
177
|
-
throw new Error(
|
|
175
|
+
throw new Error(
|
|
176
|
+
`No codec registered for target "${target}". Available: [${this.getRegisteredTargets().join(', ')}]`
|
|
177
|
+
);
|
|
178
178
|
}
|
|
179
179
|
return codec;
|
|
180
180
|
}
|
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
* 行为由注入的 SnippetCodec 决定。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
unlinkSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from 'node:fs';
|
|
12
19
|
import { join } from 'node:path';
|
|
13
20
|
|
|
14
21
|
export class SnippetInstaller {
|
|
@@ -106,8 +113,11 @@ export class SnippetInstaller {
|
|
|
106
113
|
for (const spec of specs) {
|
|
107
114
|
const result = this.install(spec, projectRoot);
|
|
108
115
|
details.push(result);
|
|
109
|
-
if (result.success)
|
|
110
|
-
|
|
116
|
+
if (result.success) {
|
|
117
|
+
successCount++;
|
|
118
|
+
} else {
|
|
119
|
+
errorCount++;
|
|
120
|
+
}
|
|
111
121
|
}
|
|
112
122
|
|
|
113
123
|
return { success: errorCount === 0, count: recipes.length, successCount, errorCount, details };
|
|
@@ -122,13 +132,17 @@ export class SnippetInstaller {
|
|
|
122
132
|
*/
|
|
123
133
|
listInstalled(projectRoot) {
|
|
124
134
|
const dir = this.#resolveDir(projectRoot);
|
|
125
|
-
if (!existsSync(dir))
|
|
135
|
+
if (!existsSync(dir)) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
126
138
|
|
|
127
139
|
const bundleFilename = this.#codec?.getBundleFilename();
|
|
128
140
|
if (bundleFilename) {
|
|
129
141
|
// VSCode: 检查 bundle 文件是否存在
|
|
130
142
|
const bundlePath = join(dir, bundleFilename);
|
|
131
|
-
if (!existsSync(bundlePath))
|
|
143
|
+
if (!existsSync(bundlePath)) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
132
146
|
try {
|
|
133
147
|
const content = JSON.parse(readFileSync(bundlePath, 'utf-8'));
|
|
134
148
|
return Object.keys(content).map((key) => ({
|
|
@@ -142,7 +156,11 @@ export class SnippetInstaller {
|
|
|
142
156
|
|
|
143
157
|
// Xcode: 列出 com.autosnippet.*.codesnippet 文件
|
|
144
158
|
return readdirSync(dir)
|
|
145
|
-
.filter(
|
|
159
|
+
.filter(
|
|
160
|
+
(f) =>
|
|
161
|
+
f.startsWith('com.autosnippet.') &&
|
|
162
|
+
f.endsWith(this.#codec?.fileExtension || '.codesnippet')
|
|
163
|
+
)
|
|
146
164
|
.map((f) => ({ filename: f, path: join(dir, f) }));
|
|
147
165
|
}
|
|
148
166
|
|
|
@@ -210,15 +228,21 @@ export class SnippetInstaller {
|
|
|
210
228
|
// ─────────────── Private ───────────────
|
|
211
229
|
|
|
212
230
|
#assertCodec() {
|
|
213
|
-
if (!this.#codec)
|
|
231
|
+
if (!this.#codec) {
|
|
232
|
+
throw new Error('SnippetCodec not set');
|
|
233
|
+
}
|
|
214
234
|
}
|
|
215
235
|
|
|
216
236
|
#assertFactory() {
|
|
217
|
-
if (!this.#snippetFactory)
|
|
237
|
+
if (!this.#snippetFactory) {
|
|
238
|
+
throw new Error('SnippetFactory not set');
|
|
239
|
+
}
|
|
218
240
|
}
|
|
219
241
|
|
|
220
242
|
#resolveDir(projectRoot) {
|
|
221
|
-
if (this.#snippetsDirOverride)
|
|
243
|
+
if (this.#snippetsDirOverride) {
|
|
244
|
+
return this.#snippetsDirOverride;
|
|
245
|
+
}
|
|
222
246
|
return this.#codec?.getInstallDir(projectRoot || process.cwd()) || '';
|
|
223
247
|
}
|
|
224
248
|
|
|
@@ -247,7 +271,7 @@ export class SnippetInstaller {
|
|
|
247
271
|
const entryKey = Object.keys(content)[0];
|
|
248
272
|
bundle[key] = content[entryKey];
|
|
249
273
|
|
|
250
|
-
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2)
|
|
274
|
+
writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
251
275
|
return { success: true, path: bundlePath, message: `Installed: ${key}` };
|
|
252
276
|
}
|
|
253
277
|
|
|
@@ -296,7 +320,7 @@ export class SnippetInstaller {
|
|
|
296
320
|
return { success: false, message: `Snippet not found in bundle: ${identifier}` };
|
|
297
321
|
}
|
|
298
322
|
delete bundle[keyToRemove];
|
|
299
|
-
writeFileSync(bundlePath, JSON.stringify(bundle, null, 2)
|
|
323
|
+
writeFileSync(bundlePath, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
300
324
|
return { success: true, message: `Uninstalled: ${keyToRemove}` };
|
|
301
325
|
} catch (error) {
|
|
302
326
|
return { success: false, message: error.message };
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { join } from 'node:path';
|
|
12
|
-
import { SnippetCodec } from './SnippetCodec.js';
|
|
13
12
|
import { PlaceholderConverter } from '../PlaceholderConverter.js';
|
|
13
|
+
import { SnippetCodec } from './SnippetCodec.js';
|
|
14
14
|
|
|
15
15
|
/** AutoSnippet language → VSCode snippet scope */
|
|
16
16
|
const VSCODE_LANGUAGE_MAP = {
|
|
@@ -58,7 +58,7 @@ export class VSCodeCodec extends SnippetCodec {
|
|
|
58
58
|
const key = `Recipe: ${spec.title || spec.identifier}`;
|
|
59
59
|
bundle[key] = this.#specToEntry(spec);
|
|
60
60
|
}
|
|
61
|
-
return JSON.stringify(bundle, null, 2)
|
|
61
|
+
return `${JSON.stringify(bundle, null, 2)}\n`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|