mcp-probe-kit 3.0.18 → 3.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +87 -55
  2. package/build/index.js +3 -1
  3. package/build/lib/__tests__/agents-md-template.unit.test.d.ts +1 -0
  4. package/build/lib/__tests__/agents-md-template.unit.test.js +27 -0
  5. package/build/lib/__tests__/memory-config.unit.test.js +9 -0
  6. package/build/lib/__tests__/memory-injection.unit.test.d.ts +1 -0
  7. package/build/lib/__tests__/memory-injection.unit.test.js +51 -0
  8. package/build/lib/__tests__/memory-orchestration.unit.test.d.ts +1 -0
  9. package/build/lib/__tests__/memory-orchestration.unit.test.js +84 -0
  10. package/build/lib/__tests__/memory-payload.unit.test.d.ts +1 -0
  11. package/build/lib/__tests__/memory-payload.unit.test.js +35 -0
  12. package/build/lib/__tests__/project-context-layout.unit.test.d.ts +1 -0
  13. package/build/lib/__tests__/project-context-layout.unit.test.js +80 -0
  14. package/build/lib/agents-md-template.d.ts +25 -0
  15. package/build/lib/agents-md-template.js +57 -0
  16. package/build/lib/memory-client.d.ts +8 -1
  17. package/build/lib/memory-client.js +53 -44
  18. package/build/lib/memory-config.d.ts +8 -0
  19. package/build/lib/memory-config.js +19 -0
  20. package/build/lib/memory-orchestration.d.ts +10 -3
  21. package/build/lib/memory-orchestration.js +146 -7
  22. package/build/lib/memory-payload.d.ts +21 -0
  23. package/build/lib/memory-payload.js +65 -0
  24. package/build/lib/merge-agents-md.d.ts +6 -0
  25. package/build/lib/merge-agents-md.js +51 -0
  26. package/build/lib/project-context-layout.d.ts +78 -0
  27. package/build/lib/project-context-layout.js +350 -0
  28. package/build/lib/workspace-root.js +6 -1
  29. package/build/resources/ui-ux-data/metadata.json +1 -1
  30. package/build/schemas/index.d.ts +62 -11
  31. package/build/schemas/memory-tools.d.ts +38 -9
  32. package/build/schemas/memory-tools.js +24 -9
  33. package/build/schemas/project-tools.d.ts +24 -2
  34. package/build/schemas/project-tools.js +24 -2
  35. package/build/tools/__tests__/code_insight.unit.test.js +3 -3
  36. package/build/tools/__tests__/init_project_context.unit.test.js +32 -21
  37. package/build/tools/__tests__/start_feature.unit.test.js +2 -1
  38. package/build/tools/code_insight.js +11 -9
  39. package/build/tools/index.d.ts +1 -0
  40. package/build/tools/index.js +1 -0
  41. package/build/tools/init_project_context.js +563 -506
  42. package/build/tools/memorize_asset.js +12 -0
  43. package/build/tools/scan_and_extract_patterns.js +7 -7
  44. package/build/tools/search_memory.d.ts +7 -0
  45. package/build/tools/search_memory.js +57 -0
  46. package/build/tools/start_bugfix.js +257 -251
  47. package/build/tools/start_feature.js +140 -134
  48. package/build/tools/start_ui.js +405 -405
  49. package/docs/.mcp-probe/layout.json +11 -0
  50. package/docs/data/tools.js +18 -0
  51. package/docs/i18n/all-tools/en.json +6 -1
  52. package/docs/i18n/all-tools/ja.json +2 -1
  53. package/docs/i18n/all-tools/ko.json +2 -1
  54. package/docs/i18n/all-tools/zh-CN.json +7 -2
  55. package/docs/i18n/en.json +38 -7
  56. package/docs/i18n/ja.json +9 -2
  57. package/docs/i18n/ko.json +9 -2
  58. package/docs/i18n/zh-CN.json +40 -9
  59. package/docs/memory-local-setup.md +314 -0
  60. package/docs/memory-local-setup.zh-CN.md +283 -0
  61. package/docs/pages/getting-started.html +252 -33
  62. package/package.json +2 -2
@@ -0,0 +1,57 @@
1
+ import { relativeLink } from "./project-context-layout.js";
2
+ function link(layout, targetRel) {
3
+ return relativeLink(layout.indexPath, targetRel);
4
+ }
5
+ function memorySection(locale) {
6
+ if (locale === "zh-CN") {
7
+ return `
8
+ 记忆(需 MEMORY_QDRANT_URL 等已配置):
9
+ - 检索:\`start_*\` 命中后**自动注入**历史经验全文;中途补查可用 \`search_memory\`;单条精读仍可用 \`read_memory_asset\`
10
+ - 沉淀:跨仓库共享**勿填** source_project/source_path;路径写进 content;summary 写检索关键词
11
+ - Bug 修完验证通过 → **必须** \`memorize_asset\` type=\`bugfix\` tags=\`bugfix,root-cause\`(content 含【现象】【根因】【修复】【验证】)
12
+ - 功能/UI 可复用产出 → \`memorize_asset\` type=\`pattern\`/\`component\``;
13
+ }
14
+ return `
15
+ Memory (requires MEMORY_* env):
16
+ - Search: \`start_*\` auto-injects full memory hits; use \`search_memory\` mid-task; \`read_memory_asset\` for a specific id
17
+ - Store: do NOT use source_project/source_path for cross-repo pools; put paths in content; write keyword-rich summary
18
+ - After verified bugfix → MUST \`memorize_asset\` type=\`bugfix\` (sections: symptom, root cause, fix, verification)
19
+ - Reusable feature/UI → \`memorize_asset\` type=\`pattern\`/\`component\``;
20
+ }
21
+ /**
22
+ * Ultra-compact AGENTS.md body (inside mcp-probe block).
23
+ */
24
+ export function generateAgentsMdInner(input) {
25
+ const { layout, locale } = input;
26
+ const graph = link(layout, layout.latestMarkdownPath);
27
+ const ctxIndex = link(layout, layout.legacyIndexPath);
28
+ if (locale === "zh-CN") {
29
+ return `## MCP(必须先调)
30
+ 需已配置 mcp-probe-kit。\`start_*\` 若返回 delegated plan,逐步执行完再结束。
31
+
32
+ - 新功能 → \`start_feature\`(会先搜记忆)
33
+ - Bug → \`start_bugfix\`(会先搜记忆)
34
+ - UI → \`start_ui\`(会先搜记忆)
35
+ - 不熟代码 / 影响面 → \`code_insight\`(context / impact / auto)
36
+ - 缺上下文 → \`init_project_context\`
37
+ - 提交 → \`gencommit\`
38
+
39
+ 上下文:写代码前先读 [project-context](${ctxIndex})(链到 \`${layout.modularDir}/\` 各文档)
40
+ 图谱:大改前读 [latest](${graph});过期 \`code_insight\` mode=auto save_to_docs=true${memorySection(locale)}`;
41
+ }
42
+ return `## MCP (call first)
43
+ Requires mcp-probe-kit. Complete every \`start_*\` delegated plan step before done.
44
+
45
+ - Feature → \`start_feature\` (searches memory first)
46
+ - Bug → \`start_bugfix\` (searches memory first)
47
+ - UI → \`start_ui\` (searches memory first)
48
+ - Unfamiliar code / impact → \`code_insight\` (context / impact / auto)
49
+ - Missing context → \`init_project_context\`
50
+ - Commit → \`gencommit\`
51
+
52
+ Context: before coding read [project-context](${ctxIndex}) (links to \`${layout.modularDir}/\`)
53
+ Graph: read [latest](${graph}) before large changes; refresh \`code_insight\` mode=auto save_to_docs=true${memorySection(locale)}`;
54
+ }
55
+ export function generateAgentsMdTemplate(input) {
56
+ return generateAgentsMdInner(input);
57
+ }
@@ -24,8 +24,15 @@ export interface MemorySearchResult {
24
24
  description: string;
25
25
  summary: string;
26
26
  tags: string[];
27
+ sourceProject?: string;
27
28
  sourcePath?: string;
28
29
  }
30
+ export interface MemorySearchOptions {
31
+ limit?: number;
32
+ minScore?: number;
33
+ preferTypes?: string[];
34
+ preferTags?: string[];
35
+ }
29
36
  export declare function normalizeContentForHash(content: string): string;
30
37
  export declare function sha256Hex(value: string): string;
31
38
  export declare function buildMemoryContentHashes(content: string): {
@@ -55,7 +62,7 @@ export declare class MemoryClient {
55
62
  sourcePath?: string;
56
63
  usage?: string;
57
64
  }): Promise<MemoryAsset>;
58
- search(query: string, limit?: number): Promise<MemorySearchResult[]>;
65
+ search(query: string, options?: MemorySearchOptions): Promise<MemorySearchResult[]>;
59
66
  getAsset(assetId: string): Promise<MemoryAsset | null>;
60
67
  }
61
68
  export declare function createMemoryClient(): MemoryClient;
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { getMemoryConfig, isMemoryEnabled, isMemoryReadEnabled } from './memory-config.js';
3
+ import { normalizeMemoryPayload, payloadToMemoryFields } from './memory-payload.js';
3
4
  function truncate(value, maxChars) {
4
5
  if (value.length <= maxChars) {
5
6
  return value;
@@ -155,26 +156,14 @@ export class MemoryClient {
155
156
  },
156
157
  }),
157
158
  });
158
- const payload = data.result?.points?.[0]?.payload;
159
- if (!payload) {
159
+ const rawPayload = data.result?.points?.[0]?.payload;
160
+ if (!rawPayload) {
160
161
  return null;
161
162
  }
163
+ const fields = payloadToMemoryFields(rawPayload);
162
164
  return {
163
- id: String(payload.id || ''),
164
- name: String(payload.name || ''),
165
- type: String(payload.type || ''),
166
- description: String(payload.description || ''),
167
- summary: String(payload.summary || ''),
168
- content: String(payload.content || ''),
169
- tags: ensureArray(payload.tags),
170
- confidence: numberOr(payload.confidence, 0.5),
171
- sourceProject: typeof payload.sourceProject === 'string' ? payload.sourceProject : undefined,
172
- sourcePath: typeof payload.sourcePath === 'string' ? payload.sourcePath : undefined,
173
- usage: typeof payload.usage === 'string' ? payload.usage : undefined,
174
- contentHash: typeof payload.contentHash === 'string' ? payload.contentHash : undefined,
175
- normalizedContentHash: typeof payload.normalizedContentHash === 'string' ? payload.normalizedContentHash : undefined,
176
- createdAt: String(payload.createdAt || ''),
177
- updatedAt: String(payload.updatedAt || ''),
165
+ ...fields,
166
+ id: fields.id || String(rawPayload.id || ''),
178
167
  };
179
168
  }
180
169
  async upsertAsset(input) {
@@ -229,33 +218,41 @@ export class MemoryClient {
229
218
  });
230
219
  return asset;
231
220
  }
232
- async search(query, limit = this.config.searchLimit) {
221
+ async search(query, options = {}) {
233
222
  if (!this.isEnabled()) {
234
223
  return [];
235
224
  }
225
+ const limit = options.limit ?? this.config.searchLimit;
226
+ const minScore = options.minScore ?? this.config.searchMinScore;
227
+ const fetchLimit = Math.min(Math.max(limit * 4, limit), 50);
236
228
  const vector = await this.embed(query);
237
229
  const data = await this.requestJson(`${this.config.qdrantUrl}/collections/${encodeURIComponent(this.config.qdrantCollection)}/points/search`, {
238
230
  method: 'POST',
239
231
  headers: this.buildHeaders(),
240
232
  body: JSON.stringify({
241
233
  vector,
242
- limit,
234
+ limit: fetchLimit,
243
235
  with_payload: true,
244
236
  }),
245
237
  });
246
- return (data.result || []).map((point) => {
247
- const payload = point.payload || {};
238
+ const mapped = (data.result || []).map((point) => {
239
+ const payload = normalizeMemoryPayload(point.payload || {});
240
+ const fields = payloadToMemoryFields(payload);
248
241
  return {
249
242
  id: String(point.id),
250
243
  score: numberOr(point.score, 0),
251
- name: String(payload.name || ''),
252
- type: String(payload.type || ''),
253
- description: String(payload.description || ''),
254
- summary: truncate(String(payload.summary || ''), this.config.summaryMaxChars),
255
- tags: ensureArray(payload.tags),
256
- sourcePath: typeof payload.sourcePath === 'string' ? payload.sourcePath : undefined,
244
+ name: fields.name,
245
+ type: fields.type,
246
+ description: fields.description,
247
+ summary: truncate(fields.summary, this.config.summaryMaxChars),
248
+ tags: fields.tags,
249
+ sourceProject: fields.sourceProject,
250
+ sourcePath: fields.sourcePath,
257
251
  };
258
252
  });
253
+ const ranked = rankSearchResults(mapped, options.preferTypes, options.preferTags);
254
+ const filtered = minScore > 0 ? ranked.filter((item) => item.score >= minScore) : ranked;
255
+ return filtered.slice(0, limit);
259
256
  }
260
257
  async getAsset(assetId) {
261
258
  if (!this.isReadEnabled()) {
@@ -265,29 +262,41 @@ export class MemoryClient {
265
262
  method: 'GET',
266
263
  headers: this.buildHeaders(false),
267
264
  });
268
- const payload = data.result?.payload;
269
- if (!payload) {
265
+ const rawPayload = data.result?.payload;
266
+ if (!rawPayload) {
270
267
  return null;
271
268
  }
269
+ const fields = payloadToMemoryFields(rawPayload);
272
270
  return {
273
- id: String(payload.id || assetId),
274
- name: String(payload.name || ''),
275
- type: String(payload.type || ''),
276
- description: String(payload.description || ''),
277
- summary: String(payload.summary || ''),
278
- content: String(payload.content || ''),
279
- tags: ensureArray(payload.tags),
280
- confidence: numberOr(payload.confidence, 0.5),
281
- sourceProject: typeof payload.sourceProject === 'string' ? payload.sourceProject : undefined,
282
- sourcePath: typeof payload.sourcePath === 'string' ? payload.sourcePath : undefined,
283
- usage: typeof payload.usage === 'string' ? payload.usage : undefined,
284
- contentHash: typeof payload.contentHash === 'string' ? payload.contentHash : undefined,
285
- normalizedContentHash: typeof payload.normalizedContentHash === 'string' ? payload.normalizedContentHash : undefined,
286
- createdAt: String(payload.createdAt || ''),
287
- updatedAt: String(payload.updatedAt || ''),
271
+ ...fields,
272
+ id: fields.id || assetId,
288
273
  };
289
274
  }
290
275
  }
276
+ function rankSearchResults(results, preferTypes = [], preferTags = []) {
277
+ if (preferTypes.length === 0 && preferTags.length === 0) {
278
+ return [...results].sort((a, b) => b.score - a.score);
279
+ }
280
+ const preferredTypes = new Set(preferTypes.map((item) => item.toLowerCase()));
281
+ const preferredTags = new Set(preferTags.map((item) => item.toLowerCase()));
282
+ const scoreBoost = (item) => {
283
+ let boost = 0;
284
+ if (preferredTypes.has(item.type.toLowerCase())) {
285
+ boost += 2;
286
+ }
287
+ if (item.tags.some((tag) => preferredTags.has(tag.toLowerCase()))) {
288
+ boost += 1;
289
+ }
290
+ return boost;
291
+ };
292
+ return [...results].sort((a, b) => {
293
+ const boostDiff = scoreBoost(b) - scoreBoost(a);
294
+ if (boostDiff !== 0) {
295
+ return boostDiff;
296
+ }
297
+ return b.score - a.score;
298
+ });
299
+ }
291
300
  export function createMemoryClient() {
292
301
  return new MemoryClient();
293
302
  }
@@ -8,6 +8,14 @@ export interface MemoryConfig {
8
8
  embeddingProvider: 'ollama' | 'openai-compatible';
9
9
  searchLimit: number;
10
10
  summaryMaxChars: number;
11
+ /** Show sourcePath in search injection (default false for cross-repo pools) */
12
+ searchShowSource: boolean;
13
+ /** Minimum cosine score to include in search results; 0 = disabled */
14
+ searchMinScore: number;
15
+ /** When set, show sourcePath only if payload.sourceProject matches */
16
+ repoId: string;
17
+ /** Max chars of each asset content injected into start_* guides */
18
+ injectionContentMaxChars: number;
11
19
  }
12
20
  export declare function getMemoryConfig(): MemoryConfig;
13
21
  export declare function isMemoryEnabled(config?: MemoryConfig): boolean;
@@ -9,6 +9,21 @@ function getNumberEnv(name, fallback) {
9
9
  const parsed = Number(raw);
10
10
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
11
11
  }
12
+ function getOptionalNumberEnv(name, fallback) {
13
+ const raw = process.env[name]?.trim();
14
+ if (!raw) {
15
+ return fallback;
16
+ }
17
+ const parsed = Number(raw);
18
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
19
+ }
20
+ function getBooleanEnv(name, fallback) {
21
+ const raw = process.env[name]?.trim().toLowerCase();
22
+ if (!raw) {
23
+ return fallback;
24
+ }
25
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
26
+ }
12
27
  export function getMemoryConfig() {
13
28
  const provider = (process.env.MEMORY_EMBEDDING_PROVIDER || 'ollama').trim().toLowerCase();
14
29
  return {
@@ -21,6 +36,10 @@ export function getMemoryConfig() {
21
36
  embeddingProvider: provider === 'openai-compatible' ? 'openai-compatible' : 'ollama',
22
37
  searchLimit: getNumberEnv('MEMORY_SEARCH_LIMIT', 3),
23
38
  summaryMaxChars: getNumberEnv('MEMORY_SUMMARY_MAX_CHARS', 280),
39
+ searchShowSource: getBooleanEnv('MEMORY_SEARCH_SHOW_SOURCE', false),
40
+ searchMinScore: getOptionalNumberEnv('MEMORY_SEARCH_MIN_SCORE', 0),
41
+ repoId: (process.env.MEMORY_REPO_ID || '').trim(),
42
+ injectionContentMaxChars: getNumberEnv('MEMORY_INJECTION_CONTENT_MAX_CHARS', 1500),
24
43
  };
25
44
  }
26
45
  export function isMemoryEnabled(config = getMemoryConfig()) {
@@ -1,15 +1,21 @@
1
- import type { MemorySearchResult } from './memory-client.js';
1
+ import type { MemoryAsset, MemorySearchResult } from './memory-client.js';
2
+ import { type MemoryConfig } from './memory-config.js';
3
+ export type MemoryPlanKind = 'feature' | 'bugfix' | 'ui' | 'default';
2
4
  export interface MemoryInjectionContext {
3
5
  enabled: boolean;
4
6
  available: boolean;
5
7
  degraded: boolean;
6
8
  query: string;
7
9
  results: MemorySearchResult[];
10
+ /** Full assets keyed by search hit id (auto-loaded for start_* injection) */
11
+ assetsById: Record<string, MemoryAsset>;
8
12
  error?: string;
9
13
  }
10
- export declare function loadMemoryInjectionContext(query: string): Promise<MemoryInjectionContext>;
14
+ export declare function truncateInjectionText(value: string, maxChars: number): string;
15
+ export declare function loadMemoryInjectionContext(query: string, kind?: MemoryPlanKind): Promise<MemoryInjectionContext>;
16
+ export declare function shouldShowSourceInSearch(item: MemorySearchResult, config?: MemoryConfig): boolean;
11
17
  export declare function renderMemoryGuideSection(context: MemoryInjectionContext): string;
12
- export declare function buildMemoryPlanStep(): {
18
+ export declare function buildMemoryPlanStep(kind?: MemoryPlanKind): {
13
19
  id: string;
14
20
  tool: string;
15
21
  when: string;
@@ -20,6 +26,7 @@ export declare function buildMemoryPlanStep(): {
20
26
  summary: string;
21
27
  content: string;
22
28
  usage: string;
29
+ tags: string[];
23
30
  confidence: number;
24
31
  };
25
32
  outputs: never[];
@@ -1,5 +1,35 @@
1
1
  import { createMemoryClient } from './memory-client.js';
2
- export async function loadMemoryInjectionContext(query) {
2
+ import { getMemoryConfig } from './memory-config.js';
3
+ function kindSearchPreferences(kind) {
4
+ switch (kind) {
5
+ case 'bugfix':
6
+ return { preferTypes: ['bugfix'], preferTags: ['bugfix', 'root-cause'] };
7
+ case 'ui':
8
+ return { preferTypes: ['component', 'pattern'], preferTags: ['ui', 'pattern'] };
9
+ case 'feature':
10
+ return { preferTypes: ['pattern', 'code'], preferTags: ['feature', 'pattern'] };
11
+ default:
12
+ return { preferTypes: [], preferTags: [] };
13
+ }
14
+ }
15
+ export function truncateInjectionText(value, maxChars) {
16
+ if (value.length <= maxChars) {
17
+ return value;
18
+ }
19
+ return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
20
+ }
21
+ async function loadFullAssets(results) {
22
+ const client = createMemoryClient();
23
+ if (!client.isReadEnabled() || results.length === 0) {
24
+ return {};
25
+ }
26
+ const entries = await Promise.all(results.map(async (item) => {
27
+ const asset = await client.getAsset(item.id);
28
+ return asset ? [item.id, asset] : null;
29
+ }));
30
+ return Object.fromEntries(entries.filter((entry) => entry !== null));
31
+ }
32
+ export async function loadMemoryInjectionContext(query, kind = 'default') {
3
33
  const client = createMemoryClient();
4
34
  if (!client.isEnabled()) {
5
35
  return {
@@ -8,16 +38,23 @@ export async function loadMemoryInjectionContext(query) {
8
38
  degraded: false,
9
39
  query,
10
40
  results: [],
41
+ assetsById: {},
11
42
  };
12
43
  }
13
44
  try {
14
- const results = await client.search(query);
45
+ const prefs = kindSearchPreferences(kind);
46
+ const results = await client.search(query, {
47
+ preferTypes: prefs.preferTypes,
48
+ preferTags: prefs.preferTags,
49
+ });
50
+ const assetsById = await loadFullAssets(results);
15
51
  return {
16
52
  enabled: true,
17
53
  available: true,
18
54
  degraded: false,
19
55
  query,
20
56
  results,
57
+ assetsById,
21
58
  };
22
59
  }
23
60
  catch (error) {
@@ -27,11 +64,57 @@ export async function loadMemoryInjectionContext(query) {
27
64
  degraded: true,
28
65
  query,
29
66
  results: [],
67
+ assetsById: {},
30
68
  error: error instanceof Error ? error.message : String(error),
31
69
  };
32
70
  }
33
71
  }
72
+ function formatMemoryResultLabel(item) {
73
+ const kind = item.type === 'bugfix' || item.tags.includes('bugfix')
74
+ ? '历史 Bug 修复'
75
+ : item.type === 'pattern' || item.type === 'component'
76
+ ? '可复用模式'
77
+ : '历史资产';
78
+ return `${item.name} [${item.type}] (${kind})`;
79
+ }
80
+ export function shouldShowSourceInSearch(item, config = getMemoryConfig()) {
81
+ if (config.searchShowSource) {
82
+ return Boolean(item.sourcePath);
83
+ }
84
+ if (!config.repoId || !item.sourceProject || !item.sourcePath) {
85
+ return false;
86
+ }
87
+ return item.sourceProject === config.repoId;
88
+ }
89
+ function formatSourceHint(item, config) {
90
+ if (!shouldShowSourceInSearch(item, config)) {
91
+ return '';
92
+ }
93
+ return `\n - 来源: ${item.sourcePath}`;
94
+ }
95
+ function formatAssetBody(asset, config) {
96
+ const lines = [
97
+ `### ${asset.name}`,
98
+ `- asset_id: ${asset.id}`,
99
+ asset.description ? `- 描述: ${asset.description}` : '',
100
+ asset.usage ? `- 适用: ${asset.usage}` : '',
101
+ asset.tags.length > 0 ? `- 标签: ${asset.tags.join(', ')}` : '',
102
+ '',
103
+ truncateInjectionText(asset.content, config.injectionContentMaxChars),
104
+ ].filter(Boolean);
105
+ return lines.join('\n');
106
+ }
107
+ function formatResultBlock(item, index, context, config) {
108
+ const label = formatMemoryResultLabel(item);
109
+ const asset = context.assetsById[item.id];
110
+ const header = `${index + 1}. ${label} score=${item.score.toFixed(3)}\n - 摘要: ${item.summary}${formatSourceHint(item, config)}`;
111
+ if (asset?.content) {
112
+ return `${header}\n\n${formatAssetBody(asset, config)}\n`;
113
+ }
114
+ return `${header}\n - 全文加载失败,可手动: read_memory_asset {"asset_id": "${item.id}"}\n`;
115
+ }
34
116
  export function renderMemoryGuideSection(context) {
117
+ const config = getMemoryConfig();
35
118
  if (!context.enabled) {
36
119
  return '';
37
120
  }
@@ -39,25 +122,81 @@ export function renderMemoryGuideSection(context) {
39
122
  return `\n\n## 🧠 记忆系统\n- 状态: 已配置但本次检索降级\n- 原因: ${context.error || '未知错误'}\n- 处理: 忽略记忆注入,继续主流程\n`;
40
123
  }
41
124
  if (context.results.length === 0) {
42
- return `\n\n## 🧠 记忆系统\n- 状态: 已启用\n- 检索结果: 未找到高相关历史资产\n- 处理: 继续主流程;若本次产出存在高价值通用资产,结束后调用 \`memorize_asset\` 沉淀\n`;
125
+ return `\n\n## 🧠 记忆系统\n- 状态: 已启用\n- 检索结果: 未找到高相关记录(含历史 Bug 修复与可复用模式)\n- 处理: 继续主流程;Bug 修复验证通过后必须 \`memorize_asset\` 沉淀;功能/UI 有可复用产出再沉淀\n`;
43
126
  }
127
+ const loadedCount = context.results.filter((item) => context.assetsById[item.id]?.content).length;
44
128
  const items = context.results
45
- .map((item, index) => `${index + 1}. ${item.name} [${item.type}] score=${item.score.toFixed(3)}\n - 摘要: ${item.summary}\n - 读取: read_memory_asset {\"asset_id\": \"${item.id}\"}${item.sourcePath ? `\n - 来源: ${item.sourcePath}` : ''}`)
129
+ .map((item, index) => formatResultBlock(item, index, context, config))
46
130
  .join('\n');
47
- return `\n\n## 🧠 记忆系统\n- 状态: 已启用\n- 指令: 优先复用以下历史成功经验;如果某条相关,再用 \`read_memory_asset\` 拉取完整内容,不要直接重写\n- 检索结果:\n${items}\n`;
131
+ return `\n\n## 🧠 记忆系统\n- 状态: 已启用\n- 指令: 下列为已自动加载的历史经验全文(${loadedCount}/${context.results.length} 条);开干前直接复用,无需再调 \`read_memory_asset\`\n- 检索结果:\n${items}`;
48
132
  }
49
- export function buildMemoryPlanStep() {
133
+ export function buildMemoryPlanStep(kind = 'default') {
134
+ if (kind === 'bugfix') {
135
+ return {
136
+ id: 'memorize-bugfix',
137
+ tool: 'memorize_asset',
138
+ when: 'Bug 已修复且验证通过后(必须沉淀,便于下次同类问题检索)',
139
+ args: {
140
+ name: '[问题简述,如 登录超时-Redis连接池]',
141
+ type: 'bugfix',
142
+ description: '[现象、报错信息、复现条件]',
143
+ summary: '[检索用:关键词 + 根因 + 修复要点,一句话]',
144
+ content: '【现象】...\n【根因】...\n【修复】具体改动与关键代码/配置\n【验证】如何确认已修好',
145
+ usage: '[再次遇到何种症状时可参考]',
146
+ tags: ['bugfix', 'root-cause'],
147
+ confidence: 0.85,
148
+ },
149
+ outputs: [],
150
+ };
151
+ }
152
+ if (kind === 'ui') {
153
+ return {
154
+ id: 'memorize-ui',
155
+ tool: 'memorize_asset',
156
+ when: 'UI 实现完成且存在可复用组件/布局/交互模式',
157
+ args: {
158
+ name: '[UI 资产名称]',
159
+ type: 'component',
160
+ description: '[该 UI 模式解决什么问题]',
161
+ summary: '[检索用摘要]',
162
+ content: '[组件结构、样式约定或可复用片段]',
163
+ usage: '[适用页面/场景]',
164
+ tags: ['ui', 'pattern'],
165
+ confidence: 0.75,
166
+ },
167
+ outputs: [],
168
+ };
169
+ }
170
+ if (kind === 'feature') {
171
+ return {
172
+ id: 'memorize-feature',
173
+ tool: 'memorize_asset',
174
+ when: '功能完成且存在可复用实现/规范',
175
+ args: {
176
+ name: '[功能/模式名称]',
177
+ type: 'pattern',
178
+ description: '[该资产解决什么问题]',
179
+ summary: '[检索用摘要]',
180
+ content: '[可复用代码或流程]',
181
+ usage: '[适用场景与限制]',
182
+ tags: ['feature', 'pattern'],
183
+ confidence: 0.75,
184
+ },
185
+ outputs: [],
186
+ };
187
+ }
50
188
  return {
51
189
  id: 'memorize',
52
190
  tool: 'memorize_asset',
53
191
  when: '本次实现完成且确认存在可复用资产',
54
192
  args: {
55
193
  name: '[资产名称]',
56
- type: 'code',
194
+ type: 'pattern',
57
195
  description: '[该资产解决了什么问题]',
58
196
  summary: '[用于后续检索的简洁摘要]',
59
197
  content: '[可复用代码或规范内容]',
60
198
  usage: '[适用场景与限制]',
199
+ tags: ['pattern'],
61
200
  confidence: 0.7,
62
201
  },
63
202
  outputs: [],
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Unify legacy Qdrant payloads (kind/title/source) with the current asset schema.
3
+ */
4
+ export declare function normalizeMemoryPayload(payload: Record<string, unknown>): Record<string, unknown>;
5
+ export declare function payloadToMemoryFields(payload: Record<string, unknown>): {
6
+ id: string;
7
+ name: string;
8
+ type: string;
9
+ description: string;
10
+ summary: string;
11
+ content: string;
12
+ tags: string[];
13
+ confidence: number;
14
+ sourceProject?: string;
15
+ sourcePath?: string;
16
+ usage?: string;
17
+ contentHash?: string;
18
+ normalizedContentHash?: string;
19
+ createdAt: string;
20
+ updatedAt: string;
21
+ };
@@ -0,0 +1,65 @@
1
+ function truncate(value, maxChars) {
2
+ if (value.length <= maxChars) {
3
+ return value;
4
+ }
5
+ return `${value.slice(0, Math.max(0, maxChars - 3))}...`;
6
+ }
7
+ /**
8
+ * Unify legacy Qdrant payloads (kind/title/source) with the current asset schema.
9
+ */
10
+ export function normalizeMemoryPayload(payload) {
11
+ const normalized = { ...payload };
12
+ if (!normalized.name && typeof normalized.title === 'string') {
13
+ normalized.name = normalized.title;
14
+ }
15
+ if (!normalized.type && typeof normalized.kind === 'string') {
16
+ const kind = normalized.kind;
17
+ normalized.type = kind === 'extracted_pattern' ? 'pattern' : kind;
18
+ }
19
+ if (!normalized.description) {
20
+ if (typeof normalized.title === 'string') {
21
+ normalized.description = normalized.title;
22
+ }
23
+ else if (typeof normalized.source === 'string') {
24
+ normalized.description = `Legacy memory from ${normalized.source}`;
25
+ }
26
+ }
27
+ if (!normalized.summary && typeof normalized.content === 'string') {
28
+ normalized.summary = truncate(normalized.content, 280);
29
+ }
30
+ if (!normalized.createdAt && typeof normalized.created_at === 'string') {
31
+ normalized.createdAt = normalized.created_at;
32
+ }
33
+ if (!normalized.updatedAt) {
34
+ if (typeof normalized.updated_at === 'string') {
35
+ normalized.updatedAt = normalized.updated_at;
36
+ }
37
+ else if (typeof normalized.createdAt === 'string') {
38
+ normalized.updatedAt = normalized.createdAt;
39
+ }
40
+ }
41
+ return normalized;
42
+ }
43
+ export function payloadToMemoryFields(payload) {
44
+ const p = normalizeMemoryPayload(payload);
45
+ const tags = Array.isArray(p.tags)
46
+ ? p.tags.filter((item) => typeof item === 'string' && item.trim().length > 0)
47
+ : [];
48
+ return {
49
+ id: String(p.id || ''),
50
+ name: String(p.name || ''),
51
+ type: String(p.type || ''),
52
+ description: String(p.description || ''),
53
+ summary: String(p.summary || ''),
54
+ content: String(p.content || ''),
55
+ tags,
56
+ confidence: typeof p.confidence === 'number' && Number.isFinite(p.confidence) ? p.confidence : 0.5,
57
+ sourceProject: typeof p.sourceProject === 'string' ? p.sourceProject : undefined,
58
+ sourcePath: typeof p.sourcePath === 'string' ? p.sourcePath : undefined,
59
+ usage: typeof p.usage === 'string' ? p.usage : undefined,
60
+ contentHash: typeof p.contentHash === 'string' ? p.contentHash : undefined,
61
+ normalizedContentHash: typeof p.normalizedContentHash === 'string' ? p.normalizedContentHash : undefined,
62
+ createdAt: String(p.createdAt || ''),
63
+ updatedAt: String(p.updatedAt || ''),
64
+ };
65
+ }
@@ -0,0 +1,6 @@
1
+ export type AgentsMdMergeMode = "created" | "prepended" | "replaced-and-moved-to-top" | "skipped-empty";
2
+ export declare function wrapMcpProbeBlock(innerMarkdown: string): string;
3
+ export declare function mergeAgentsMdBlock(existingContent: string | null | undefined, generatedInner: string): {
4
+ content: string;
5
+ mergeMode: AgentsMdMergeMode;
6
+ };
@@ -0,0 +1,51 @@
1
+ const BLOCK_BEGIN = "<!-- mcp-probe:context begin — auto-generated; re-run init_project_context updates this block only -->";
2
+ const BLOCK_END = "<!-- mcp-probe:context end -->";
3
+ export function wrapMcpProbeBlock(innerMarkdown) {
4
+ return `${BLOCK_BEGIN}\n${innerMarkdown.trim()}\n${BLOCK_END}`;
5
+ }
6
+ function stripExistingBlock(content) {
7
+ const beginIdx = content.indexOf(BLOCK_BEGIN);
8
+ if (beginIdx === -1) {
9
+ const legacyBegin = content.indexOf("<!-- mcp-probe:context begin");
10
+ if (legacyBegin === -1) {
11
+ return content.trim();
12
+ }
13
+ const legacyEnd = content.indexOf(BLOCK_END);
14
+ if (legacyEnd === -1) {
15
+ return content.trim();
16
+ }
17
+ const legacyLineEnd = content.indexOf("-->", legacyBegin);
18
+ const blockStart = legacyLineEnd === -1 ? legacyBegin : legacyLineEnd + 3;
19
+ const before = content.slice(0, legacyBegin).trimEnd();
20
+ const after = content.slice(legacyEnd + BLOCK_END.length).trimStart();
21
+ return [before, after].filter(Boolean).join("\n\n").trim();
22
+ }
23
+ const endIdx = content.indexOf(BLOCK_END);
24
+ if (endIdx === -1) {
25
+ return content.trim();
26
+ }
27
+ const before = content.slice(0, beginIdx).trimEnd();
28
+ const after = content.slice(endIdx + BLOCK_END.length).trimStart();
29
+ return [before, after].filter(Boolean).join("\n\n").trim();
30
+ }
31
+ export function mergeAgentsMdBlock(existingContent, generatedInner) {
32
+ const block = wrapMcpProbeBlock(generatedInner);
33
+ if (!existingContent?.trim()) {
34
+ return { content: `${block}\n`, mergeMode: "created" };
35
+ }
36
+ const userBody = stripExistingBlock(existingContent);
37
+ const hadBlock = existingContent.includes(BLOCK_BEGIN) || existingContent.includes("<!-- mcp-probe:context begin");
38
+ if (!userBody) {
39
+ return { content: `${block}\n`, mergeMode: hadBlock ? "replaced-and-moved-to-top" : "created" };
40
+ }
41
+ if (!hadBlock) {
42
+ return {
43
+ content: `${block}\n\n${userBody}\n`,
44
+ mergeMode: "prepended",
45
+ };
46
+ }
47
+ return {
48
+ content: `${block}\n\n${userBody}\n`,
49
+ mergeMode: "replaced-and-moved-to-top",
50
+ };
51
+ }