autosnippet 3.0.11 → 3.0.13
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/cli.js +64 -1
- package/config/default.json +9 -0
- package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
- package/dashboard/dist/index.html +1 -1
- package/lib/cli/SetupService.js +92 -5
- package/lib/cli/UpgradeService.js +14 -5
- package/lib/core/discovery/GenericDiscoverer.js +4 -28
- package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +246 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +80 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +600 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +125 -342
- package/lib/external/mcp/handlers/bootstrap/refine.js +362 -0
- package/lib/external/mcp/handlers/bootstrap.js +6 -590
- package/lib/external/mcp/handlers/browse.js +119 -9
- package/lib/external/mcp/handlers/guard.js +25 -6
- package/lib/external/mcp/handlers/search.js +56 -24
- package/lib/http/routes/guardRules.js +9 -17
- package/lib/injection/ServiceContainer.js +12 -3
- package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
- package/lib/platform/ios/xcode/XcodeIntegration.js +40 -659
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +220 -0
- package/lib/service/chat/ChatAgent.js +39 -418
- package/lib/service/chat/ChatAgentPrompts.js +149 -0
- package/lib/service/chat/ChatAgentTasks.js +297 -0
- 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 +496 -0
- package/lib/service/chat/tools/guard.js +265 -0
- package/lib/service/chat/tools/index.js +250 -0
- package/lib/service/chat/tools/infrastructure.js +222 -0
- package/lib/service/chat/tools/knowledge-graph.js +234 -0
- package/lib/service/chat/tools/lifecycle.js +469 -0
- package/lib/service/chat/tools/project-access.js +923 -0
- package/lib/service/chat/tools/query.js +264 -0
- package/lib/service/chat/tools.js +14 -3994
- package/lib/service/cursor/AgentInstructionsGenerator.js +395 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +70 -11
- package/lib/service/cursor/FileProtection.js +116 -0
- package/lib/service/cursor/KnowledgeCompressor.js +61 -11
- package/lib/service/cursor/SkillsSyncer.js +5 -3
- package/lib/service/cursor/TopicClassifier.js +19 -3
- package/lib/service/guard/ExclusionManager.js +26 -2
- package/lib/service/guard/GuardCheckEngine.js +38 -370
- package/lib/service/guard/GuardCodeChecks.js +362 -0
- package/lib/service/guard/GuardCrossFileChecks.js +307 -0
- package/lib/service/guard/GuardPatternUtils.js +180 -0
- package/lib/service/guard/GuardService.js +80 -38
- package/lib/service/module/ModuleService.js +1 -0
- package/lib/service/search/SearchEngine.js +10 -2
- package/lib/service/wiki/WikiGenerator.js +226 -1532
- package/lib/service/wiki/WikiRenderers.js +1878 -0
- package/lib/service/wiki/WikiUtils.js +907 -0
- package/lib/shared/LanguageService.js +299 -0
- package/package.json +1 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentInstructionsGenerator — 通用 AI Agent 指令文件生成器
|
|
3
|
+
*
|
|
4
|
+
* Channel F: 为多种 AI 编码工具生成项目指令文件
|
|
5
|
+
* - AGENTS.md → OpenAI Codex / 通用 Agent
|
|
6
|
+
* - CLAUDE.md → Claude Code
|
|
7
|
+
* - .github/copilot-instructions.md → GitHub Copilot(动态版,替代静态模板)
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:
|
|
10
|
+
* 1. 内容来源统一 — 从 _loadEntries() 已加载的知识条目中提取
|
|
11
|
+
* 2. 格式差异化 — 同一数据按目标工具特性适配输出
|
|
12
|
+
* 3. 轻量索引 — 只输出摘要和规则,详细内容引导至 MCP 工具
|
|
13
|
+
* 4. 幂等生成 — 每次 deliver 重写全部文件,不做增量 diff
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { checkWriteSafety, safeWriteFile } from './FileProtection.js';
|
|
19
|
+
import { estimateTokens } from './TokenBudget.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Agent 指令文件 token 预算
|
|
23
|
+
* AGENTS.md / CLAUDE.md 不受 Cursor 200K 限制,但需控制体积以便 Agent 快速消化
|
|
24
|
+
*/
|
|
25
|
+
const AGENT_BUDGET = Object.freeze({
|
|
26
|
+
MAX_RULES: 15,
|
|
27
|
+
MAX_PATTERNS: 10,
|
|
28
|
+
MAX_SKILLS: 10,
|
|
29
|
+
MAX_TOTAL_TOKENS: 3000,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MCP 工具清单 — 精简版(跟随实际 MCP handler 注册名称)
|
|
34
|
+
*/
|
|
35
|
+
const MCP_TOOLS_SUMMARY = [
|
|
36
|
+
{ name: 'autosnippet_search', desc: 'Search knowledge base (mode: auto/context/keyword/semantic)' },
|
|
37
|
+
{ name: 'autosnippet_knowledge', desc: 'Knowledge CRUD (operation: list/get/insights/confirm_usage)' },
|
|
38
|
+
{ name: 'autosnippet_submit_knowledge', desc: 'Submit a knowledge candidate (strict validation)' },
|
|
39
|
+
{ name: 'autosnippet_submit_knowledge_batch', desc: 'Batch submit candidates (with dedup + throttle)' },
|
|
40
|
+
{ name: 'autosnippet_guard', desc: 'Code compliance check (single file or batch audit)' },
|
|
41
|
+
{ name: 'autosnippet_structure', desc: 'Project structure discovery (targets/files/metadata)' },
|
|
42
|
+
{ name: 'autosnippet_graph', desc: 'Knowledge graph query (query/impact/path/stats)' },
|
|
43
|
+
{ name: 'autosnippet_skill', desc: 'Skill management (list/load/create/update/delete/suggest)' },
|
|
44
|
+
{ name: 'autosnippet_save_document', desc: 'Save development document (auto-publish)' },
|
|
45
|
+
{ name: 'autosnippet_bootstrap', desc: 'Project cold-start & scan (knowledge/refine/scan)' },
|
|
46
|
+
{ name: 'autosnippet_health', desc: 'Service health & KB statistics' },
|
|
47
|
+
{ name: 'autosnippet_capabilities', desc: 'List all available MCP tools (self-discovery)' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export class AgentInstructionsGenerator {
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} projectRoot
|
|
53
|
+
* @param {string} projectName
|
|
54
|
+
* @param {Object} [logger]
|
|
55
|
+
*/
|
|
56
|
+
constructor(projectRoot, projectName = 'Project', logger = console) {
|
|
57
|
+
this.projectRoot = projectRoot;
|
|
58
|
+
this.projectName = projectName;
|
|
59
|
+
this.logger = logger;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 生成所有 Agent 指令文件
|
|
64
|
+
*
|
|
65
|
+
* @param {Object} params
|
|
66
|
+
* @param {Array<Object>} params.rules - kind='rule' 的条目(已排序)
|
|
67
|
+
* @param {Array<Object>} params.patterns - kind='pattern' 的条目(已排序)
|
|
68
|
+
* @param {string[]} params.skills - 可用 Skill 名称列表
|
|
69
|
+
* @returns {{ agents: Object, claude: Object, copilot: Object }}
|
|
70
|
+
*/
|
|
71
|
+
generate({ rules = [], patterns = [], skills = [] }) {
|
|
72
|
+
const startTime = Date.now();
|
|
73
|
+
|
|
74
|
+
// 构建共享内容块
|
|
75
|
+
const sections = this._buildSections({ rules, patterns, skills });
|
|
76
|
+
|
|
77
|
+
// 生成 3 个目标文件
|
|
78
|
+
const agents = this._writeAgentsMd(sections);
|
|
79
|
+
const claude = this._writeClaudeMd(sections);
|
|
80
|
+
const copilot = this._writeCopilotInstructions(sections);
|
|
81
|
+
|
|
82
|
+
const duration = Date.now() - startTime;
|
|
83
|
+
const filesWritten = [agents, claude, copilot].filter((r) => !r.skipped).length;
|
|
84
|
+
const skippedFiles = [agents, claude, copilot].filter((r) => r.skipped);
|
|
85
|
+
if (skippedFiles.length > 0) {
|
|
86
|
+
this.logger.info?.(
|
|
87
|
+
`[AgentInstructions] Skipped ${skippedFiles.length} file(s) — ` +
|
|
88
|
+
`user-owned files will not be overwritten: ${skippedFiles.map((f) => f.filePath).join(', ')}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
this.logger.info?.(
|
|
92
|
+
`[AgentInstructions] Generated ${filesWritten} files in ${duration}ms — ` +
|
|
93
|
+
`AGENTS.md: ${agents.tokensUsed}t, CLAUDE.md: ${claude.tokensUsed}t, ` +
|
|
94
|
+
`copilot-instructions: ${copilot.tokensUsed}t`
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
agents,
|
|
99
|
+
claude,
|
|
100
|
+
copilot,
|
|
101
|
+
stats: {
|
|
102
|
+
filesWritten,
|
|
103
|
+
filesSkipped: skippedFiles.length,
|
|
104
|
+
totalTokens: agents.tokensUsed + claude.tokensUsed + copilot.tokensUsed,
|
|
105
|
+
duration,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── 内容构建 ──────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 从知识条目构建共享内容段
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
_buildSections({ rules, patterns, skills }) {
|
|
117
|
+
// 编码规则(Channel A 格式,一行一条)
|
|
118
|
+
const ruleLines = rules
|
|
119
|
+
.slice(0, AGENT_BUDGET.MAX_RULES)
|
|
120
|
+
.filter((e) => e.doClause)
|
|
121
|
+
.map((e) => {
|
|
122
|
+
const langPrefix = e.language && e.scope !== 'universal' ? `[${e.language}] ` : '';
|
|
123
|
+
const doText = e.doClause.replace(/\.+$/, '');
|
|
124
|
+
let line = `${langPrefix}${doText}`;
|
|
125
|
+
if (e.dontClause) {
|
|
126
|
+
// 有明确否定词的统一为 "Do NOT",否则保留原文(如 "Avoid ...")
|
|
127
|
+
const hasNegPrefix = /^(Don't|Do not|Never)\s+/i.test(e.dontClause);
|
|
128
|
+
if (hasNegPrefix) {
|
|
129
|
+
const stripped = e.dontClause.replace(/^(Don't|Do not|Never)\s+/i, '').replace(/\.+$/, '');
|
|
130
|
+
line += `. Do NOT ${stripped}`;
|
|
131
|
+
} else {
|
|
132
|
+
line += `. ${e.dontClause.replace(/\.+$/, '')}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return `- ${line}.`;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// 架构模式(摘要表格行)
|
|
139
|
+
const patternRows = patterns
|
|
140
|
+
.slice(0, AGENT_BUDGET.MAX_PATTERNS)
|
|
141
|
+
.filter((e) => e.trigger && e.doClause)
|
|
142
|
+
.map((e) => {
|
|
143
|
+
const trigger = e.trigger.startsWith('@') ? e.trigger : `@${e.trigger}`;
|
|
144
|
+
const when = (e.whenClause || '').substring(0, 60);
|
|
145
|
+
const doText = (e.doClause || '').substring(0, 80);
|
|
146
|
+
return `| ${trigger} | ${when} | ${doText} |`;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Skills 列表
|
|
150
|
+
const skillLines = skills
|
|
151
|
+
.slice(0, AGENT_BUDGET.MAX_SKILLS)
|
|
152
|
+
.map((s) => `- \`${s}\``);
|
|
153
|
+
|
|
154
|
+
// MCP 工具列表
|
|
155
|
+
const toolLines = MCP_TOOLS_SUMMARY.map(
|
|
156
|
+
(t) => `- \`${t.name}\` — ${t.desc}`
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return { ruleLines, patternRows, skillLines, toolLines };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── AGENTS.md ─────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
_writeAgentsMd(sections) {
|
|
168
|
+
const lines = [
|
|
169
|
+
`# ${this.projectName} — Agent Instructions`,
|
|
170
|
+
'',
|
|
171
|
+
'> Auto-generated by [AutoSnippet](https://github.com/anthropic/autosnippet). Do not edit manually.',
|
|
172
|
+
'> This file is regenerated when the knowledge base changes.',
|
|
173
|
+
'',
|
|
174
|
+
'## Project Knowledge Base',
|
|
175
|
+
'',
|
|
176
|
+
`This project uses **AutoSnippet** as its knowledge management system.`,
|
|
177
|
+
`The knowledge base contains coding standards, architecture patterns, and best practices`,
|
|
178
|
+
`accessible through MCP tools.`,
|
|
179
|
+
'',
|
|
180
|
+
...this._renderConstraints(),
|
|
181
|
+
'',
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// Coding Standards
|
|
185
|
+
if (sections.ruleLines.length > 0) {
|
|
186
|
+
lines.push('## Coding Standards', '');
|
|
187
|
+
lines.push(...sections.ruleLines);
|
|
188
|
+
lines.push('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Architecture Patterns
|
|
192
|
+
if (sections.patternRows.length > 0) {
|
|
193
|
+
lines.push('## Architecture Patterns', '');
|
|
194
|
+
lines.push('| Trigger | When | Do |');
|
|
195
|
+
lines.push('|---------|------|----|');
|
|
196
|
+
lines.push(...sections.patternRows);
|
|
197
|
+
lines.push('');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// MCP Tools
|
|
201
|
+
lines.push('## MCP Tools (AutoSnippet)', '');
|
|
202
|
+
lines.push('Use these MCP tools to access the full knowledge base:', '');
|
|
203
|
+
lines.push(...sections.toolLines);
|
|
204
|
+
lines.push('');
|
|
205
|
+
|
|
206
|
+
// Skills
|
|
207
|
+
if (sections.skillLines.length > 0) {
|
|
208
|
+
lines.push('## Available Skills', '');
|
|
209
|
+
lines.push('Load with `autosnippet_skill({ operation: "load", name: "<skill>" })`:', '');
|
|
210
|
+
lines.push(...sections.skillLines);
|
|
211
|
+
lines.push('');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Workflow
|
|
215
|
+
lines.push(...this._renderWorkflow());
|
|
216
|
+
|
|
217
|
+
const content = `${lines.join('\n')}\n`;
|
|
218
|
+
const filePath = path.join(this.projectRoot, 'AGENTS.md');
|
|
219
|
+
const result = safeWriteFile(filePath, content, { logger: this.logger });
|
|
220
|
+
|
|
221
|
+
return { filePath, tokensUsed: estimateTokens(content), skipped: !result.written };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── CLAUDE.md ─────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @private
|
|
228
|
+
*/
|
|
229
|
+
_writeClaudeMd(sections) {
|
|
230
|
+
const lines = [
|
|
231
|
+
`# ${this.projectName} — Claude Code Instructions`,
|
|
232
|
+
'',
|
|
233
|
+
'> Auto-generated by AutoSnippet. Regenerated when knowledge base changes.',
|
|
234
|
+
'',
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
// Constraints (Claude prefers clear bullet points)
|
|
238
|
+
lines.push(...this._renderConstraints());
|
|
239
|
+
lines.push('');
|
|
240
|
+
|
|
241
|
+
// Coding Standards
|
|
242
|
+
if (sections.ruleLines.length > 0) {
|
|
243
|
+
lines.push('## Coding Standards', '');
|
|
244
|
+
lines.push('These are mandatory project rules extracted from the knowledge base:', '');
|
|
245
|
+
lines.push(...sections.ruleLines);
|
|
246
|
+
lines.push('');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Patterns (Claude benefits from When/Do format)
|
|
250
|
+
if (sections.patternRows.length > 0) {
|
|
251
|
+
lines.push('## Key Patterns', '');
|
|
252
|
+
lines.push('| Trigger | When | Do |');
|
|
253
|
+
lines.push('|---------|------|----|');
|
|
254
|
+
lines.push(...sections.patternRows);
|
|
255
|
+
lines.push('');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// MCP — Claude Code natively supports MCP
|
|
259
|
+
lines.push('## MCP Integration', '');
|
|
260
|
+
lines.push(
|
|
261
|
+
'This project has an AutoSnippet MCP server configured. ',
|
|
262
|
+
'Use the following tools to access project knowledge:',
|
|
263
|
+
''
|
|
264
|
+
);
|
|
265
|
+
lines.push(...sections.toolLines);
|
|
266
|
+
lines.push('');
|
|
267
|
+
|
|
268
|
+
// Key tools highlight for Claude
|
|
269
|
+
lines.push('### Recommended Workflow', '');
|
|
270
|
+
lines.push('1. **Before writing code**: `autosnippet_search({ query: "<topic>" })` to find relevant patterns');
|
|
271
|
+
lines.push('2. **Check compliance**: `autosnippet_guard({ code: "<your code>" })` before committing');
|
|
272
|
+
lines.push('3. **Record knowledge**: `autosnippet_submit_knowledge({ ... })` when discovering reusable patterns');
|
|
273
|
+
lines.push('4. **Confirm adoption**: `autosnippet_knowledge({ operation: "confirm_usage", id: "<id>" })`');
|
|
274
|
+
lines.push('');
|
|
275
|
+
|
|
276
|
+
// Skills
|
|
277
|
+
if (sections.skillLines.length > 0) {
|
|
278
|
+
lines.push('## Skills', '');
|
|
279
|
+
lines.push(...sections.skillLines);
|
|
280
|
+
lines.push('');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const content = `${lines.join('\n')}\n`;
|
|
284
|
+
const filePath = path.join(this.projectRoot, 'CLAUDE.md');
|
|
285
|
+
const result = safeWriteFile(filePath, content, { logger: this.logger });
|
|
286
|
+
|
|
287
|
+
return { filePath, tokensUsed: estimateTokens(content), skipped: !result.written };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── copilot-instructions.md ───────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 动态生成 copilot-instructions.md
|
|
294
|
+
* 替代原有的静态模板复制
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
_writeCopilotInstructions(sections) {
|
|
298
|
+
const lines = [
|
|
299
|
+
'# AutoSnippet Copilot Instructions',
|
|
300
|
+
'',
|
|
301
|
+
'## Project Overview',
|
|
302
|
+
`- Project: **${this.projectName}**`,
|
|
303
|
+
'- Knowledge System: AutoSnippet V3 (ESM, SQLite, MCP)',
|
|
304
|
+
'- Knowledge Base: `AutoSnippet/` directory (recipes, skills, constitution)',
|
|
305
|
+
'',
|
|
306
|
+
...this._renderConstraints(),
|
|
307
|
+
'',
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
// Coding Standards
|
|
311
|
+
if (sections.ruleLines.length > 0) {
|
|
312
|
+
lines.push('## Coding Standards', '');
|
|
313
|
+
lines.push(...sections.ruleLines);
|
|
314
|
+
lines.push('');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// MCP Tools (compact for Copilot)
|
|
318
|
+
lines.push('## MCP Tools', '');
|
|
319
|
+
lines.push('Access the knowledge base through MCP:', '');
|
|
320
|
+
// Copilot: show fewer, most essential tools
|
|
321
|
+
const essentialTools = [
|
|
322
|
+
'- `autosnippet_search` — Search knowledge (mode: auto/context/keyword/semantic)',
|
|
323
|
+
'- `autosnippet_knowledge` — Browse/get recipes (operation: list/get/insights)',
|
|
324
|
+
'- `autosnippet_submit_knowledge` — Submit candidate (strict validation, all fields required)',
|
|
325
|
+
'- `autosnippet_guard` — Code compliance check',
|
|
326
|
+
'- `autosnippet_skill` — Load project skills (list/load)',
|
|
327
|
+
'- `autosnippet_health` — Service health & KB stats',
|
|
328
|
+
];
|
|
329
|
+
lines.push(...essentialTools);
|
|
330
|
+
lines.push('');
|
|
331
|
+
|
|
332
|
+
// Knowledge Types
|
|
333
|
+
lines.push('## Knowledge Types', '');
|
|
334
|
+
lines.push('- **rule** — Coding standards, enforced by Guard');
|
|
335
|
+
lines.push('- **pattern** — Reusable code patterns and architecture');
|
|
336
|
+
lines.push('- **fact** — Structural knowledge (relations, data flow)');
|
|
337
|
+
lines.push('');
|
|
338
|
+
|
|
339
|
+
// Workflow
|
|
340
|
+
lines.push('## Workflow', '');
|
|
341
|
+
lines.push('1. Search before coding: `autosnippet_search({ query: "..." })`');
|
|
342
|
+
lines.push('2. Prefer Recipe over raw source code');
|
|
343
|
+
lines.push('3. Submit discoveries: `autosnippet_submit_knowledge({ ... })`');
|
|
344
|
+
lines.push('4. Do NOT directly modify `AutoSnippet/` or `.autosnippet/` files');
|
|
345
|
+
lines.push('');
|
|
346
|
+
|
|
347
|
+
const content = `${lines.join('\n')}\n`;
|
|
348
|
+
const destDir = path.join(this.projectRoot, '.github');
|
|
349
|
+
const filePath = path.join(destDir, 'copilot-instructions.md');
|
|
350
|
+
const { canWrite } = checkWriteSafety(filePath);
|
|
351
|
+
if (canWrite) {
|
|
352
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
353
|
+
}
|
|
354
|
+
const result = safeWriteFile(filePath, content, { logger: this.logger });
|
|
355
|
+
|
|
356
|
+
return { filePath, tokensUsed: estimateTokens(content), skipped: !result.written };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── 共享模板片段 ──────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* 核心约束(所有 Agent 共享)
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
_renderConstraints() {
|
|
366
|
+
return [
|
|
367
|
+
'## Mandatory Constraints',
|
|
368
|
+
'',
|
|
369
|
+
'1. **Do NOT modify** knowledge base files directly (`AutoSnippet/recipes/`, `.autosnippet/`).',
|
|
370
|
+
'2. Create or update knowledge **only** through MCP tools (`autosnippet_submit_knowledge`).',
|
|
371
|
+
'3. **Prefer Recipes** as project standards; source code is supplementary.',
|
|
372
|
+
'4. Use `autosnippet_search` for knowledge retrieval; do not retry on failure in the same turn.',
|
|
373
|
+
'5. Skills handle semantics and workflow; MCP handles capabilities — do not hardcode URLs in Skills.',
|
|
374
|
+
];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 推荐工作流(AGENTS.md 用)
|
|
379
|
+
* @private
|
|
380
|
+
*/
|
|
381
|
+
_renderWorkflow() {
|
|
382
|
+
return [
|
|
383
|
+
'## Recommended Workflow',
|
|
384
|
+
'',
|
|
385
|
+
'1. **Search first**: `autosnippet_search({ query: "<topic>" })` before writing code',
|
|
386
|
+
'2. **Check patterns**: Look for existing `@trigger` patterns before implementing',
|
|
387
|
+
'3. **Guard check**: `autosnippet_guard({ code: "<code>" })` to validate compliance',
|
|
388
|
+
'4. **Submit discoveries**: `autosnippet_submit_knowledge({ ... })` for reusable patterns',
|
|
389
|
+
'5. **Confirm adoption**: `autosnippet_knowledge({ operation: "confirm_usage" })`',
|
|
390
|
+
'',
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export default AgentInstructionsGenerator;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CursorDeliveryPipeline —
|
|
2
|
+
* CursorDeliveryPipeline — 6 通道交付主入口
|
|
3
3
|
*
|
|
4
|
-
* 读取知识库 → 筛选 + 分类 + 排序 + 压缩 → 写入
|
|
4
|
+
* 读取知识库 → 筛选 + 分类 + 排序 + 压缩 → 写入 6 个通道
|
|
5
|
+
*
|
|
6
|
+
* Channel A: .cursor/rules/autosnippet-project-rules.mdc (alwaysApply rules)
|
|
7
|
+
* Channel B: .cursor/rules/autosnippet-patterns-{topic}.mdc (smart rules)
|
|
8
|
+
* Channel C: .cursor/skills/ (project skills sync)
|
|
9
|
+
* Channel D: .cursor/skills/autosnippet-devdocs/ (dev documents)
|
|
10
|
+
* Channel F: AGENTS.md + CLAUDE.md + .github/copilot-instructions.md (agent instructions)
|
|
11
|
+
* + Mirror: .qoder/ .trae/ (IDE mirror)
|
|
5
12
|
*
|
|
6
13
|
* 触发时机:
|
|
7
14
|
* 1. bootstrap 完成后自动触发
|
|
@@ -13,6 +20,7 @@
|
|
|
13
20
|
import fs from 'node:fs';
|
|
14
21
|
import path from 'node:path';
|
|
15
22
|
import { DELIVERY_RANK, KNOWLEDGE_CONFIDENCE } from '../../shared/constants.js';
|
|
23
|
+
import { AgentInstructionsGenerator } from './AgentInstructionsGenerator.js';
|
|
16
24
|
import { KnowledgeCompressor } from './KnowledgeCompressor.js';
|
|
17
25
|
import { RulesGenerator } from './RulesGenerator.js';
|
|
18
26
|
import { SkillsSyncer } from './SkillsSyncer.js';
|
|
@@ -38,11 +46,12 @@ export class CursorDeliveryPipeline {
|
|
|
38
46
|
this.topicClassifier = new TopicClassifier(this.projectName);
|
|
39
47
|
this.rulesGenerator = new RulesGenerator(projectRoot, this.projectName);
|
|
40
48
|
this.skillsSyncer = new SkillsSyncer(projectRoot, this.projectName, knowledgeService);
|
|
49
|
+
this.agentInstructions = new AgentInstructionsGenerator(projectRoot, this.projectName, logger);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
44
|
-
* 完整交付流程 — 生成
|
|
45
|
-
* @returns {Promise<{ channelA: Object, channelB: Object, channelC: Object, stats: Object }>}
|
|
53
|
+
* 完整交付流程 — 生成 6 通道消费物料
|
|
54
|
+
* @returns {Promise<{ channelA: Object, channelB: Object, channelC: Object, channelD: Object, channelF: Object, stats: Object }>}
|
|
46
55
|
*/
|
|
47
56
|
async deliver() {
|
|
48
57
|
const startTime = Date.now();
|
|
@@ -51,6 +60,7 @@ export class CursorDeliveryPipeline {
|
|
|
51
60
|
channelB: { topicCount: 0, patternsCount: 0, totalTokens: 0 },
|
|
52
61
|
channelC: { synced: 0, skipped: 0, errors: 0 },
|
|
53
62
|
channelD: { documentsCount: 0, filesWritten: 0 },
|
|
63
|
+
channelF: { filesWritten: 0, totalTokens: 0 },
|
|
54
64
|
totalTokensUsed: 0,
|
|
55
65
|
duration: 0,
|
|
56
66
|
};
|
|
@@ -85,12 +95,13 @@ export class CursorDeliveryPipeline {
|
|
|
85
95
|
const channelD = this._generateChannelD(documents);
|
|
86
96
|
stats.channelD = channelD;
|
|
87
97
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
// ── Channel F: Agent Instructions (AGENTS.md / CLAUDE.md / copilot-instructions) ──
|
|
99
|
+
const channelF = this._generateChannelF(rules, patterns);
|
|
100
|
+
stats.channelF = channelF;
|
|
101
|
+
|
|
102
|
+
// NOTE: .qoder/ .trae/ 镜像不再自动执行,由 `asd mirror` 按需触发
|
|
92
103
|
|
|
93
|
-
stats.totalTokensUsed = channelA.tokensUsed + channelB.totalTokens;
|
|
104
|
+
stats.totalTokensUsed = channelA.tokensUsed + channelB.totalTokens + (channelF.totalTokens || 0);
|
|
94
105
|
stats.duration = Date.now() - startTime;
|
|
95
106
|
|
|
96
107
|
this.logger.info?.(
|
|
@@ -98,10 +109,11 @@ export class CursorDeliveryPipeline {
|
|
|
98
109
|
`A: ${channelA.rulesCount} rules (${channelA.tokensUsed} tokens), ` +
|
|
99
110
|
`B: ${channelB.topicCount} topics (${channelB.totalTokens} tokens), ` +
|
|
100
111
|
`C: ${channelC.synced} skills synced, ` +
|
|
101
|
-
`D: ${channelD.documentsCount} documents`
|
|
112
|
+
`D: ${channelD.documentsCount} documents, ` +
|
|
113
|
+
`F: ${channelF.filesWritten} agent files`
|
|
102
114
|
);
|
|
103
115
|
|
|
104
|
-
return { channelA, channelB, channelC, channelD, stats };
|
|
116
|
+
return { channelA, channelB, channelC, channelD, channelF, stats };
|
|
105
117
|
} catch (error) {
|
|
106
118
|
this.logger.error?.(`[CursorDelivery] Error: ${error.message}`);
|
|
107
119
|
throw error;
|
|
@@ -403,6 +415,53 @@ export class CursorDeliveryPipeline {
|
|
|
403
415
|
return result;
|
|
404
416
|
}
|
|
405
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Channel F — Agent Instructions 生成
|
|
420
|
+
* 生成 AGENTS.md / CLAUDE.md / .github/copilot-instructions.md
|
|
421
|
+
* @private
|
|
422
|
+
*/
|
|
423
|
+
_generateChannelF(rules, patterns) {
|
|
424
|
+
try {
|
|
425
|
+
// 收集可用 Skills 名称
|
|
426
|
+
const skillsDir = path.join(this.projectRoot, 'AutoSnippet', 'skills');
|
|
427
|
+
let skills = [];
|
|
428
|
+
if (fs.existsSync(skillsDir)) {
|
|
429
|
+
skills = fs
|
|
430
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
431
|
+
.filter((d) => d.isDirectory())
|
|
432
|
+
.map((d) => d.name);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 排序后传入
|
|
436
|
+
const rankedRules = this._rank(rules);
|
|
437
|
+
const rankedPatterns = this._rank(patterns);
|
|
438
|
+
|
|
439
|
+
const result = this.agentInstructions.generate({
|
|
440
|
+
rules: rankedRules,
|
|
441
|
+
patterns: rankedPatterns,
|
|
442
|
+
skills,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
this.logger.info?.(
|
|
446
|
+
`[CursorDelivery] Channel F: ${result.stats.filesWritten} agent instruction files ` +
|
|
447
|
+
`(${result.stats.totalTokens} tokens)`
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
filesWritten: result.stats.filesWritten,
|
|
452
|
+
totalTokens: result.stats.totalTokens,
|
|
453
|
+
files: {
|
|
454
|
+
agents: result.agents.filePath,
|
|
455
|
+
claude: result.claude.filePath,
|
|
456
|
+
copilot: result.copilot.filePath,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
} catch (err) {
|
|
460
|
+
this.logger.warn?.(`[CursorDelivery] Channel F error (non-blocking): ${err.message}`);
|
|
461
|
+
return { filesWritten: 0, totalTokens: 0, files: {} };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
406
465
|
/**
|
|
407
466
|
* 文件名安全 slug 化
|
|
408
467
|
* @private
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileProtection — 非 AutoSnippet 独有文件的写入保护
|
|
3
|
+
*
|
|
4
|
+
* 保护逻辑:
|
|
5
|
+
* - 如果目标文件不存在 → 直接写入(首次生成)
|
|
6
|
+
* - 如果目标文件存在且包含 AutoSnippet 签名 → 允许覆盖(我们生成的)
|
|
7
|
+
* - 如果目标文件存在但不包含 AutoSnippet 签名 → 拒绝覆盖(用户原有文件)
|
|
8
|
+
*
|
|
9
|
+
* 签名标记(任意一个匹配即视为 AutoSnippet 所有):
|
|
10
|
+
* - "Auto-generated by AutoSnippet"
|
|
11
|
+
* - "Auto-generated by [AutoSnippet]"
|
|
12
|
+
* - "auto-generated by autosnippet" (case-insensitive)
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AutoSnippet 文件签名模式(case-insensitive)
|
|
19
|
+
* 检查文件前 1024 字节即可——签名总在文件头部
|
|
20
|
+
*/
|
|
21
|
+
const SIGNATURE_PATTERN = /auto-generated by (?:\[)?autosnippet(?:\])?/i;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 检查文件是否可以被 AutoSnippet 安全写入
|
|
25
|
+
*
|
|
26
|
+
* @param {string} filePath - 目标文件绝对路径
|
|
27
|
+
* @returns {{ canWrite: boolean, reason: string }}
|
|
28
|
+
* - canWrite: true → 可以安全写入
|
|
29
|
+
* - canWrite: false → 文件存在且非 AutoSnippet 生成,不应覆盖
|
|
30
|
+
*/
|
|
31
|
+
export function checkWriteSafety(filePath) {
|
|
32
|
+
if (!fs.existsSync(filePath)) {
|
|
33
|
+
return { canWrite: true, reason: 'file-not-exist' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// 只读前 1024 字节检测签名,避免大文件全量读取
|
|
38
|
+
const fd = fs.openSync(filePath, 'r');
|
|
39
|
+
const buf = Buffer.alloc(1024);
|
|
40
|
+
const bytesRead = fs.readSync(fd, buf, 0, 1024, 0);
|
|
41
|
+
fs.closeSync(fd);
|
|
42
|
+
|
|
43
|
+
const header = buf.toString('utf8', 0, bytesRead);
|
|
44
|
+
if (SIGNATURE_PATTERN.test(header)) {
|
|
45
|
+
return { canWrite: true, reason: 'autosnippet-owned' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { canWrite: false, reason: 'user-owned' };
|
|
49
|
+
} catch {
|
|
50
|
+
// 读取失败(权限等问题),保守处理:不覆盖
|
|
51
|
+
return { canWrite: false, reason: 'read-error' };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 安全写入文件 — 带保护机制
|
|
57
|
+
*
|
|
58
|
+
* @param {string} filePath - 目标文件绝对路径
|
|
59
|
+
* @param {string} content - 文件内容
|
|
60
|
+
* @param {Object} [options]
|
|
61
|
+
* @param {boolean} [options.force=false] - 强制覆盖(忽略保护)
|
|
62
|
+
* @param {Object} [options.logger] - 日志器
|
|
63
|
+
* @returns {{ written: boolean, reason: string, filePath: string }}
|
|
64
|
+
*/
|
|
65
|
+
export function safeWriteFile(filePath, content, options = {}) {
|
|
66
|
+
const { force = false, logger } = options;
|
|
67
|
+
|
|
68
|
+
if (force) {
|
|
69
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
70
|
+
return { written: true, reason: 'force', filePath };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { canWrite, reason } = checkWriteSafety(filePath);
|
|
74
|
+
|
|
75
|
+
if (canWrite) {
|
|
76
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
77
|
+
return { written: true, reason, filePath };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 用户原有文件 → 跳过
|
|
81
|
+
logger?.info?.(
|
|
82
|
+
`[FileProtection] Skipped "${filePath}" — ${reason} (file exists and is not AutoSnippet-generated)`
|
|
83
|
+
);
|
|
84
|
+
return { written: false, reason, filePath };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 安全复制文件 — 带保护机制
|
|
89
|
+
*
|
|
90
|
+
* @param {string} srcPath - 源文件路径
|
|
91
|
+
* @param {string} destPath - 目标文件路径
|
|
92
|
+
* @param {Object} [options]
|
|
93
|
+
* @param {boolean} [options.force=false] - 强制覆盖
|
|
94
|
+
* @param {Object} [options.logger] - 日志器
|
|
95
|
+
* @returns {{ written: boolean, reason: string, filePath: string }}
|
|
96
|
+
*/
|
|
97
|
+
export function safeCopyFile(srcPath, destPath, options = {}) {
|
|
98
|
+
const { force = false, logger } = options;
|
|
99
|
+
|
|
100
|
+
if (force) {
|
|
101
|
+
fs.copyFileSync(srcPath, destPath);
|
|
102
|
+
return { written: true, reason: 'force', filePath: destPath };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { canWrite, reason } = checkWriteSafety(destPath);
|
|
106
|
+
|
|
107
|
+
if (canWrite) {
|
|
108
|
+
fs.copyFileSync(srcPath, destPath);
|
|
109
|
+
return { written: true, reason, filePath: destPath };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logger?.info?.(
|
|
113
|
+
`[FileProtection] Skipped "${destPath}" — ${reason} (file exists and is not AutoSnippet-generated)`
|
|
114
|
+
);
|
|
115
|
+
return { written: false, reason, filePath: destPath };
|
|
116
|
+
}
|