Leon4gr45/builder
0
1/**2 * Safe extraction of analytics properties from tool call arguments.3 * Only whitelisted, enumerated values are emitted — no file paths, contents, or user text.4 */5 6import { getBuiltInSkillIds } from '@/lib/vfs/skills/registry';7import { isBuiltInInterviewTemplateId } from '@/lib/interview/templates';8 9/** Built-in interview template ids may be reported; custom ones are anonymized. */10export function bucketInterviewTemplateId(id: string): string {11 return isBuiltInInterviewTemplateId(id) ? id : 'custom';12}13 14const BASH_COMMAND_WHITELIST = new Set([15 'cat', 'head', 'tail', 'nl', 'ls', 'tree', 'grep', 'rg', 'find',16 'mkdir', 'mv', 'cp', 'rm', 'rmdir', 'touch', 'sed', 'ss', 'echo', 'wc',17 'sort', 'uniq', 'tr', 'curl', 'sleep', 'sqlite3', 'build', 'status', 'agent', 'delegate',18 'preview', 'python', 'python3', 'lua', 'runtime', 'generate-image'19]);20 21function extractShellAnalytics(args: Record<string, unknown>): Record<string, unknown> {22 const result: Record<string, unknown> = {};23 const rawCmd = (args.command ?? args.cmd) as string | undefined;24 const cmd = typeof rawCmd === 'string' ? rawCmd.trim() : '';25 if (cmd) {26 const firstWord = cmd.split(/\s+/)[0];27 result.command = BASH_COMMAND_WHITELIST.has(firstWord) ? firstWord : 'other';28 result.has_pipe = cmd.includes(' | ');29 result.has_redirect = / >>? /.test(cmd);30 }31 return result;32}33 34export function extractToolAnalytics(35 toolName: string,36 argsJson: string,37 success: boolean38): Record<string, unknown> {39 const base: Record<string, unknown> = { tool: toolName, success };40 41 let args: Record<string, unknown>;42 try {43 args = JSON.parse(argsJson);44 if (!args || typeof args !== 'object' || Array.isArray(args)) return base;45 } catch {46 return base;47 }48 49 if (toolName === 'bash' || toolName === 'shell') {50 return { ...base, ...extractShellAnalytics(args) };51 }52 return base;53}54 55/**56 * Detect a skill-file read from a bash cat command. Returns the built-in57 * skill id, 'custom' for user skills, or null when not a skill read.58 * Never returns the path itself.59 */60export function extractSkillRead(argsJson: string): { skill: string } | null {61 try {62 const args = JSON.parse(argsJson);63 const raw = (args?.command ?? args?.cmd) as string | undefined;64 if (typeof raw !== 'string') return null;65 const cmd = raw.trim();66 if (!cmd.startsWith('cat ')) return null;67 const m = cmd.match(/\/\.skills\/([A-Za-z0-9_-]+)\.md/);68 if (!m) return null;69 const id = m[1];70 return { skill: getBuiltInSkillIds().includes(id) ? id : 'custom' };71 } catch {72 return null;73 }74}75 