autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
771 lines (770 loc) • 30.5 kB
JavaScript
/**
* MCP Handlers — Skills 加载与发现
*
* 为 MCP 外部 Agent 提供 Skills 访问能力,使其能按需获取领域操作指南。
* Skills 是 Agent 的知识增强文档,指导如何正确使用 AutoSnippet 工具。
*
* 设计原则:
* - Skills 是只读文档,不涉及 AI 调用,不需要 Gateway gating
* - 外部 Agent 应根据当前任务类型选择加载合适的 Skill
* - list_skills 返回摘要帮助 Agent 判断该加载哪个 Skill
*/
import fs from 'node:fs';
import path from 'node:path';
import { getProjectSkillsPath } from '#infra/config/Paths.js';
import pathGuard from '#shared/PathGuard.js';
import { SKILLS_DIR } from '#shared/package-root.js';
import { resolveProjectRoot } from '#shared/resolveProjectRoot.js';
/**
* 获取项目级 Skills 目录(运行时动态解析)
* 路径: {projectRoot}/AutoSnippet/skills/ — 跟随项目走
*/
function _getProjectSkillsDir(ctx) {
return getProjectSkillsPath(resolveProjectRoot(ctx?.container));
}
/**
* 解析 SKILL.md frontmatter 全部元数据
*
* 返回 { description, createdBy, createdAt },缺失字段为 null。
* 同时兼容旧格式(无 createdBy 的 SKILL.md)。
*/
function _parseSkillMeta(skillName, baseDir = SKILLS_DIR) {
try {
const content = fs.readFileSync(path.join(baseDir, skillName, 'SKILL.md'), 'utf8');
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
const meta = {
description: skillName,
createdBy: null,
createdAt: null,
};
if (fmMatch) {
const fm = fmMatch[1];
const descMatch = fm.match(/^description:\s*(.+?)$/m);
if (descMatch) {
const desc = descMatch[1].trim();
const firstSentence = desc.split(/\.\s/)[0];
meta.description =
firstSentence.length < desc.length ? `${firstSentence}.` : desc.substring(0, 120);
}
const cbMatch = fm.match(/^createdBy:\s*(.+?)$/m);
if (cbMatch) {
meta.createdBy = cbMatch[1].trim();
}
const caMatch = fm.match(/^createdAt:\s*(.+?)$/m);
if (caMatch) {
meta.createdAt = caMatch[1].trim();
}
}
return meta;
}
catch {
return { description: skillName, createdBy: null, createdAt: null };
}
}
/** Skill 适用场景映射 — 帮助 Agent 判断何时该加载哪个 Skill */
const SKILL_USE_CASES = {
'autosnippet-create': '将代码模式/规则/事实提交到知识库',
'autosnippet-guard': '代码规范审计(Guard 规则检查)',
'autosnippet-recipes': '查询/使用项目标准(Recipe 上下文检索)',
'autosnippet-structure': '了解项目结构(Target / 依赖图谱 / 知识图谱)',
'autosnippet-devdocs': '保存开发文档(架构决策、调试报告、设计文档)',
};
// ═══════════════════════════════════════════════════════════
// Handler: listSkills
// ═══════════════════════════════════════════════════════════
/**
* 列出所有可用 Skills 及其摘要描述
*
* @returns JSON envelope
*/
export function listSkills(ctx) {
try {
const skillMap = new Map();
// 内置 Skills
const builtinDirs = fs
.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const name of builtinDirs) {
const meta = _parseSkillMeta(name, SKILLS_DIR);
skillMap.set(name, {
name,
source: 'builtin',
summary: meta.description,
createdBy: null,
createdAt: null,
useCase: SKILL_USE_CASES[name] || null,
});
}
// 项目级 Skills(覆盖同名内置)
try {
const projectSkillsDir = _getProjectSkillsDir(ctx ?? undefined);
const projectDirs = fs
.readdirSync(projectSkillsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const name of projectDirs) {
const meta = _parseSkillMeta(name, projectSkillsDir);
skillMap.set(name, {
name,
source: 'project',
summary: meta.description,
createdBy: meta.createdBy,
createdAt: meta.createdAt,
useCase: SKILL_USE_CASES[name] || null,
});
}
}
catch {
/* no project skills */
}
const skills = [...skillMap.values()].sort((a, b) => a.name.localeCompare(b.name));
// _meta:附带 SignalCollector 推荐计数(如果后台服务可用)
let suggestionCount = 0;
try {
const g = globalThis;
if (g._signalCollector) {
const snapshot = g._signalCollector.getSnapshot();
suggestionCount = snapshot?.lastResult?.newSuggestions || 0;
}
}
catch {
/* silent */
}
return JSON.stringify({
success: true,
data: {
skills,
total: skills.length,
hint: '根据当前任务选择合适的 Skill 加载(load_skill)。',
_meta: { signalSuggestions: suggestionCount },
},
});
}
catch (err) {
return JSON.stringify({
success: false,
error: {
code: 'SKILLS_READ_ERROR',
message: err instanceof Error ? err.message : String(err),
},
});
}
}
// ═══════════════════════════════════════════════════════════
// Handler: loadSkill
// ═══════════════════════════════════════════════════════════
/**
* 加载指定 Skill 的完整文档内容
*
* @param _ctx MCP context(未使用,保持签名一致)
* @param args { skillName: string, section?: string }
* @returns JSON envelope
*/
export function loadSkill(ctx, args) {
const { skillName, section } = args || {};
if (!skillName) {
return JSON.stringify({
success: false,
error: { code: 'MISSING_PARAM', message: 'skillName is required' },
});
}
// 项目级 Skills 优先
const projectSkillsDir = _getProjectSkillsDir(ctx ?? undefined);
const projectSkillPath = path.join(projectSkillsDir, skillName, 'SKILL.md');
const builtinSkillPath = path.join(SKILLS_DIR, skillName, 'SKILL.md');
const skillPath = fs.existsSync(projectSkillPath) ? projectSkillPath : builtinSkillPath;
const source = skillPath === projectSkillPath ? 'project' : 'builtin';
try {
let content = fs.readFileSync(skillPath, 'utf8');
// 如果指定了 section,只返回对应章节
if (section) {
const sectionRe = new RegExp(`^##\\s+.*${section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*$\\n([\\s\\S]*?)(?=^##\\s|$)`, 'mi');
const match = content.match(sectionRe);
if (match) {
content = match[0];
}
}
// 提取 createdBy/createdAt
const meta = _parseSkillMeta(skillName, source === 'project' ? projectSkillsDir : SKILLS_DIR);
// ── SkillHooks: onSkillLoad (fire-and-forget) ──
try {
const skillHooks = ctx?.container?.get?.('skillHooks');
if (skillHooks?.has?.('onSkillLoad')) {
skillHooks.run('onSkillLoad', { skillName, source }).catch(() => {
/* fire-and-forget */
});
}
}
catch {
/* skillHooks not available */
}
return JSON.stringify({
success: true,
data: {
skillName,
source,
content,
charCount: content.length,
createdBy: source === 'project' ? meta.createdBy : null,
createdAt: source === 'project' ? meta.createdAt : null,
useCase: SKILL_USE_CASES[skillName] || null,
relatedSkills: _getRelatedSkills(skillName),
},
});
}
catch {
// 列出所有可用 Skills
const available = new Set();
try {
fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
.forEach((d) => {
available.add(d.name);
});
}
catch {
/* skip: SKILLS_DIR may not exist */
}
try {
fs.readdirSync(_getProjectSkillsDir(ctx ?? undefined), { withFileTypes: true })
.filter((d) => d.isDirectory())
.forEach((d) => {
available.add(d.name);
});
}
catch {
/* skip: project skills dir may not exist */
}
return JSON.stringify({
success: false,
error: {
code: 'SKILL_NOT_FOUND',
message: `Skill "${skillName}" not found`,
availableSkills: [...available],
},
});
}
}
export function createSkill(ctx, args) {
const { name, description, content, overwrite = false, createdBy = 'external-ai', title, } = args || {};
// ── 参数校验 ──
if (!name || !description || !content) {
return JSON.stringify({
success: false,
error: { code: 'MISSING_PARAM', message: 'name, description, content are all required' },
});
}
// 名称格式校验:kebab-case(允许字母、数字、连字符)
if (!/^[a-z][a-z0-9-]*[a-z0-9]$/.test(name) || name.length < 3 || name.length > 64) {
return JSON.stringify({
success: false,
error: {
code: 'INVALID_NAME',
message: `Skill name must be kebab-case (a-z, 0-9, -), 3-64 chars. Got: "${name}"`,
},
});
}
// 不允许覆盖内置 Skill
const builtinSkillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
if (fs.existsSync(builtinSkillPath)) {
return JSON.stringify({
success: false,
error: {
code: 'BUILTIN_CONFLICT',
message: `"${name}" is a built-in Skill and cannot be overwritten. Choose a different name.`,
},
});
}
// 检查同名项目级 Skill
const projectSkillsDir = _getProjectSkillsDir(ctx ?? undefined);
const skillDir = path.join(projectSkillsDir, name);
const skillPath = path.join(skillDir, 'SKILL.md');
if (fs.existsSync(skillPath) && !overwrite) {
return JSON.stringify({
success: false,
error: {
code: 'ALREADY_EXISTS',
message: `Project skill "${name}" already exists. Set overwrite=true to replace.`,
},
});
}
// ── 写入 SKILL.md ──
try {
// 路径安全检查 — name 来自用户输入,可能含路径字符
pathGuard.assertProjectWriteSafe(skillDir);
fs.mkdirSync(skillDir, { recursive: true });
// 自动推断 title: 优先使用传入参数,否则从 content 的第一个 # heading 提取
const resolvedTitle = title ||
(() => {
const m = (content || '').match(/^#\s+(.+)/m);
return m ? m[1].trim() : '';
})();
const fmLines = ['---', `name: ${name}`];
if (resolvedTitle) {
fmLines.push(`title: "${resolvedTitle.replace(/"/g, '\\"')}"`);
}
fmLines.push(`description: ${description}`, `createdBy: ${createdBy}`, `createdAt: ${new Date().toISOString()}`, '---', '');
const frontmatter = fmLines.join('\n');
fs.writeFileSync(skillPath, frontmatter + content, 'utf8');
}
catch (err) {
return JSON.stringify({
success: false,
error: {
code: 'WRITE_ERROR',
message: `Failed to write SKILL.md: ${err instanceof Error ? err.message : String(err)}`,
},
});
}
// ── regenerate 编辑器索引 ──
const indexResult = _regenerateEditorIndex(ctx ?? undefined);
// ── 清理 SignalCollector 已创建的 pendingSuggestions ──
try {
const g = globalThis;
if (g._signalCollector) {
g._signalCollector.removePendingSuggestion(name);
}
}
catch {
/* silent */
}
// ── SkillHooks: onSkillCreated (fire-and-forget) ──
try {
const skillHooks = ctx?.container?.get?.('skillHooks');
if (skillHooks?.has?.('onSkillCreated')) {
skillHooks
.run('onSkillCreated', { name, description, createdBy, path: skillPath })
.catch(() => {
/* fire-and-forget */
});
}
}
catch {
/* skillHooks not available */
}
return JSON.stringify({
success: true,
data: {
skillName: name,
path: skillPath,
overwritten: fs.existsSync(skillPath) && overwrite,
editorIndex: indexResult,
hint: `Skill "${name}" created. Use autosnippet_skill({ operation: "load", name: "${name}" }) to verify content.`,
},
});
}
/**
* Regenerate .cursor/rules/autosnippet-skills.mdc 索引文件
* 扫描所有项目级 Skills,生成摘要索引供 External Agent 被动发现
*
* @returns }
*/
function _regenerateEditorIndex(ctx) {
try {
// 扫描项目级 Skills
const projectSkills = [];
const projectSkillsDir = _getProjectSkillsDir(ctx);
try {
const dirs = fs
.readdirSync(projectSkillsDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
for (const name of dirs) {
const meta = _parseSkillMeta(name, projectSkillsDir);
projectSkills.push({ name, summary: meta.description });
}
}
catch {
/* no project skills dir */
}
const projectRoot = resolveProjectRoot(ctx?.container);
const rulesDir = path.join(projectRoot, '.cursor', 'rules');
if (projectSkills.length === 0) {
// 没有项目级 Skills 时,删除索引文件(如果存在)
const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
try {
fs.unlinkSync(indexPath);
}
catch {
/* not exists */
}
return { success: true, skillCount: 0 };
}
// 生成 .mdc 内容
const skillLines = projectSkills.map((s) => `- **${s.name}**: ${s.summary}`).join('\n');
const mdcContent = [
'---',
'description: AutoSnippet 项目级 Skills 索引(自动生成,请勿手动编辑)',
'alwaysApply: true',
'---',
'',
'# AutoSnippet Project Skills',
'',
`本项目已注册 ${projectSkills.length} 个自定义 Skill。使用 \`autosnippet_skill({ operation: "load", name })\` 加载完整内容。`,
'',
skillLines,
'',
].join('\n');
// 写入 .cursor/rules/
pathGuard.assertProjectWriteSafe(rulesDir);
fs.mkdirSync(rulesDir, { recursive: true });
const indexPath = path.join(rulesDir, 'autosnippet-skills.mdc');
fs.writeFileSync(indexPath, mdcContent, 'utf8');
return { success: true, path: indexPath, skillCount: projectSkills.length };
}
catch (err) {
return { success: false, error: err instanceof Error ? err.message : String(err) };
}
}
// ═══════════════════════════════════════════════════════════
// Handler: deleteSkill
// ═══════════════════════════════════════════════════════════
/**
* 删除项目级 Skill — 移除 {projectRoot}/AutoSnippet/skills/<name>/ 整个目录
* 内置 Skill 不可删除。删除后自动 regenerate 编辑器索引。
*
* @param _ctx MCP context
* @param args { name: string }
* @returns JSON envelope
*/
export function deleteSkill(ctx, args) {
const { name } = args || {};
if (!name) {
return JSON.stringify({
success: false,
error: { code: 'MISSING_PARAM', message: 'name is required' },
});
}
// 不允许删除内置 Skill
const builtinSkillPath = path.join(SKILLS_DIR, name);
if (fs.existsSync(builtinSkillPath)) {
return JSON.stringify({
success: false,
error: {
code: 'BUILTIN_PROTECTED',
message: `"${name}" is a built-in Skill and cannot be deleted.`,
},
});
}
// 检查项目级 Skill 是否存在
const projectSkillsDir = _getProjectSkillsDir(ctx ?? undefined);
const skillDir = path.join(projectSkillsDir, name);
if (!fs.existsSync(skillDir)) {
return JSON.stringify({
success: false,
error: {
code: 'SKILL_NOT_FOUND',
message: `Project skill "${name}" not found.`,
},
});
}
// ── 路径安全检查 ──
try {
pathGuard.assertProjectWriteSafe(skillDir);
}
catch (err) {
return JSON.stringify({
success: false,
error: { code: 'PATH_GUARD', message: err instanceof Error ? err.message : String(err) },
});
}
// ── 删除目录 ──
try {
fs.rmSync(skillDir, { recursive: true, force: true });
}
catch (err) {
return JSON.stringify({
success: false,
error: {
code: 'DELETE_ERROR',
message: `Failed to delete skill: ${err instanceof Error ? err.message : String(err)}`,
},
});
}
// ── regenerate 编辑器索引 ──
const indexResult = _regenerateEditorIndex(ctx ?? undefined);
// ── SkillHooks: onSkillExpired (fire-and-forget) ──
try {
const skillHooks = ctx?.container?.get?.('skillHooks');
if (skillHooks?.has?.('onSkillExpired')) {
skillHooks.run('onSkillExpired', { name, reason: 'deleted' }).catch(() => {
/* fire-and-forget */
});
}
}
catch {
/* skillHooks not available */
}
return JSON.stringify({
success: true,
data: {
skillName: name,
deleted: true,
editorIndex: indexResult,
hint: `Skill "${name}" deleted successfully.`,
},
});
}
export function updateSkill(ctx, args) {
const { name, description, content } = args || {};
if (!name) {
return JSON.stringify({
success: false,
error: { code: 'MISSING_PARAM', message: 'name is required' },
});
}
if (!description && !content) {
return JSON.stringify({
success: false,
error: {
code: 'NOTHING_TO_UPDATE',
message: 'At least one of description or content must be provided.',
},
});
}
// 不允许更新内置 Skill
const builtinSkillPath = path.join(SKILLS_DIR, name, 'SKILL.md');
if (fs.existsSync(builtinSkillPath)) {
return JSON.stringify({
success: false,
error: {
code: 'BUILTIN_PROTECTED',
message: `"${name}" is a built-in Skill and cannot be updated. Fork it as a project skill instead.`,
},
});
}
// 检查项目级 Skill 是否存在
const projectSkillsDir = _getProjectSkillsDir(ctx ?? undefined);
const skillPath = path.join(projectSkillsDir, name, 'SKILL.md');
if (!fs.existsSync(skillPath)) {
return JSON.stringify({
success: false,
error: {
code: 'SKILL_NOT_FOUND',
message: `Project skill "${name}" not found. Use autosnippet_skill({ operation: "create" }) to create it first.`,
},
});
}
try {
// ── 读取现有文件 ──
const existing = fs.readFileSync(skillPath, 'utf8');
// 解析现有 frontmatter
const fmMatch = existing.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
let oldFm = '';
let oldBody = existing;
if (fmMatch) {
oldFm = fmMatch[1];
oldBody = fmMatch[2];
}
// 解析已有字段
const getField = (fm, key) => {
const m = fm.match(new RegExp(`^${key}:\\s*(.+?)$`, 'm'));
return m ? m[1].trim() : null;
};
const newDesc = description || getField(oldFm, 'description') || name;
const newBody = content !== undefined && content !== null ? content : oldBody;
// 保留原有字段
const createdBy = getField(oldFm, 'createdBy') || 'external-ai';
const createdAt = getField(oldFm, 'createdAt') || new Date().toISOString();
const title = getField(oldFm, 'title');
// 重建 frontmatter
const fmLines = ['---', `name: ${name}`];
if (title) {
fmLines.push(`title: ${title}`);
}
fmLines.push(`description: ${newDesc}`, `createdBy: ${createdBy}`, `createdAt: ${createdAt}`, `updatedAt: ${new Date().toISOString()}`, '---', '');
pathGuard.assertProjectWriteSafe(path.join(projectSkillsDir, name));
fs.writeFileSync(skillPath, fmLines.join('\n') + newBody, 'utf8');
}
catch (err) {
return JSON.stringify({
success: false,
error: {
code: 'UPDATE_ERROR',
message: `Failed to update skill: ${err instanceof Error ? err.message : String(err)}`,
},
});
}
// ── regenerate 编辑器索引 ──
const indexResult = _regenerateEditorIndex(ctx ?? undefined);
return JSON.stringify({
success: true,
data: {
skillName: name,
updated: true,
fieldsUpdated: [description ? 'description' : null, content ? 'content' : null].filter(Boolean),
editorIndex: indexResult,
hint: `Skill "${name}" updated. Use autosnippet_skill({ operation: "load", name: "${name}" }) to verify content.`,
},
});
}
// ═══════════════════════════════════════════════════════════
// Handler: suggestSkills
// ═══════════════════════════════════════════════════════════
/**
* 基于项目使用模式分析,推荐创建 Skill
*
* 分析维度:Guard 违规模式、Memory 偏好积累、Recipe 分布缺口、候选积压
* Agent 可根据推荐结果自行决定是否调用 createSkill 创建
*
* @param ctx MCP context(含 container)
* @returns JSON envelope
*/
export async function suggestSkills(ctx) {
try {
// ── 优先使用 RecommendationPipeline (统一推荐管线) ──
const pipeline = ctx?.container?.get?.('recommendationPipeline');
if (pipeline && typeof pipeline.recommend === 'function') {
const database = ctx?.container?.get?.('database');
const projectRoot = resolveProjectRoot(ctx?.container);
const existingSkills = _listExistingProjectSkillNames(ctx);
const recommendations = await pipeline.recommend({
projectRoot,
database: database?.getDb?.() || database || null,
container: ctx?.container,
existingSkills,
});
// 记录展示指标
try {
const metrics = ctx?.container?.get?.('recommendationMetrics');
if (metrics && typeof metrics.trackDisplayed === 'function') {
metrics.trackDisplayed(recommendations);
}
}
catch {
/* metrics tracking is best-effort */
}
return JSON.stringify({
success: true,
data: {
suggestions: recommendations,
existingProjectSkills: [...existingSkills],
hint: recommendations.length > 0
? `发现 ${recommendations.length} 个 Skill 创建建议(powered by RecommendationPipeline)。`
: '当前项目使用模式暂无明确的 Skill 创建建议。',
},
});
}
// ── Fallback: 直接使用 SkillAdvisor ──
const { SkillAdvisor } = await import('#service/skills/SkillAdvisor.js');
const projectRoot = resolveProjectRoot(ctx?.container);
const knowledgeRepo = ctx?.container?.get?.('knowledgeRepository') || null;
const auditRepo = ctx?.container?.get?.('auditRepository') || null;
const advisor = new SkillAdvisor(projectRoot, { knowledgeRepo, auditRepo });
const result = await advisor.suggest();
return JSON.stringify({
success: true,
data: result,
});
}
catch (err) {
return JSON.stringify({
success: false,
error: { code: 'SUGGEST_ERROR', message: err instanceof Error ? err.message : String(err) },
});
}
}
/** 获取已有的项目级 Skill 名称集合 */
function _listExistingProjectSkillNames(ctx) {
const names = new Set();
try {
const dir = _getProjectSkillsDir(ctx ?? undefined);
for (const d of fs.readdirSync(dir, { withFileTypes: true })) {
if (d.isDirectory()) {
names.add(d.name);
}
}
}
catch {
/* no project skills */
}
return names;
}
/** 推荐相关 Skills(基于静态映射) */
function _getRelatedSkills(skillName) {
const relations = {
'autosnippet-create': ['autosnippet-recipes'],
'autosnippet-guard': ['autosnippet-recipes'],
'autosnippet-recipes': ['autosnippet-guard', 'autosnippet-structure', 'autosnippet-create'],
'autosnippet-structure': ['autosnippet-recipes', 'autosnippet-create'],
'autosnippet-devdocs': ['autosnippet-recipes', 'autosnippet-create'],
};
return relations[skillName] || [];
}
// ═══════════════════════════════════════════════════════
// 推荐反馈
// ═══════════════════════════════════════════════════════
/**
* 记录推荐反馈
*
* operation: 'feedback'
* @param args.recommendationId 推荐 ID
* @param args.action 'adopted' | 'dismissed' | 'expired' | 'viewed' | 'modified'
* @param args.reason 可选 — 忽略原因
* @param args.source 可选 — 推荐来源
* @param args.category 可选 — 推荐类别
*/
export async function recordFeedback(ctx, args) {
try {
const validActions = ['adopted', 'dismissed', 'expired', 'viewed', 'modified'];
if (!args.recommendationId || !args.action) {
return JSON.stringify({
success: false,
error: { code: 'MISSING_PARAMS', message: 'recommendationId and action are required' },
});
}
if (!validActions.includes(args.action)) {
return JSON.stringify({
success: false,
error: {
code: 'INVALID_ACTION',
message: `action must be one of: ${validActions.join(', ')}`,
},
});
}
// 获取 FeedbackStore
const feedbackStore = ctx?.container?.get?.('feedbackStore');
if (!feedbackStore || typeof feedbackStore.record !== 'function') {
return JSON.stringify({
success: false,
error: { code: 'STORE_UNAVAILABLE', message: 'FeedbackStore not initialized' },
});
}
await feedbackStore.record({
recommendationId: args.recommendationId,
action: args.action,
timestamp: new Date().toISOString(),
source: args.source,
category: args.category,
reason: args.reason,
});
// 触发 SkillHooks: onRecommendFeedback
try {
const skillHooks = ctx?.container?.get?.('skillHooks');
if (skillHooks?.has?.('onRecommendFeedback')) {
await skillHooks.run('onRecommendFeedback', {
recommendationId: args.recommendationId,
action: args.action,
reason: args.reason,
});
}
}
catch {
/* hook error is non-blocking */
}
return JSON.stringify({
success: true,
data: { recorded: true, recommendationId: args.recommendationId, action: args.action },
});
}
catch (err) {
return JSON.stringify({
success: false,
error: {
code: 'FEEDBACK_ERROR',
message: err instanceof Error ? err.message : String(err),
},
});
}
}