UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

463 lines (401 loc) 15.2 kB
/** * ConciergeResponseTranslator — converts raw skill/agent/flow output into * composed, appropriately-toned replies for the daemon Concierge. * * Pipeline position: * [ Skill / Agent / Flow ] → raw output * ↓ * [ ResponseTranslator ] * - Apply tone rules (prompt, pertinent, pleasant, professional, discreet) * - Reduce technical noise (paths, stack traces, debug lines) * - Reformat for audience * - Preserve fidelity — no actionable information is silently dropped * ↓ * [ User ] * * Bypass: callers may pass { raw: true } or { verbose: true } to skip * translation entirely (for debugging or power-user access). * * @issue #607 * @tests @test/unit/daemon/concierge/response-translator.test.js */ // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** * Sensitive operation categories. * When raw output is classified as one of these, discreet mode is applied * automatically unless the caller explicitly requests verbose output. */ const SENSITIVE_OPERATION_CATEGORIES = new Set([ 'token-rotation', 'credential-update', 'security-finding', 'key-management', ]); /** * Patterns that identify sensitive content in raw output. * Matched lines are suppressed in discreet mode. */ const SENSITIVE_PATTERNS = [ /\b(token|secret|api[_-]?key|password|credential|private[_-]?key|bearer)\s*[:=]\s*\S+/i, /eyJ[A-Za-z0-9_-]{10,}/, // JWT token shape /ghp_[A-Za-z0-9]{36}/, // GitHub PAT /sk-[A-Za-z0-9]{32,}/, // OpenAI key shape /\b[0-9a-f]{40}\b/, // 40-char hex (token/hash) ]; /** * Patterns that indicate technical noise to suppress by default. */ const NOISE_PATTERNS = [ /^\s+at\s+\S+\s+\(\S+:\d+:\d+\)/m, // JS stack frame /^\s+at\s+\S+:\d+:\d+$/m, // Node stack frame (no parens) /^(node|npm|yarn|pnpm):.*error/im, // Toolchain noise /\bDEBUG\b.*:/i, // Debug log lines /\bTRACE\b.*:/i, // Trace log lines /^\[.*\]\s*(debug|trace|verbose)\b/im, // Structured debug/trace entries ]; /** * Absolute file paths that should be redacted in discreet mode. * Replaced with a short placeholder. */ const PATH_PATTERN = /(?:\/(?:home|Users|root|tmp|var|opt|mnt|srv|etc|usr|private)\S+|[A-Z]:[\\\/]\S+)/g; /** * Filler phrases that violate the "pertinent" tone rule. * Stripped from translated output. */ const FILLER_PHRASES = [ /\bi have\s+(successfully|just|now)\s+/gi, /\bas\s+you\s+requested[,.]?\s*/gi, /\bcertainly[!,.]?\s*/gi, /\bof\s+course[!,.]?\s*/gi, /\bgreat\s+(question|idea|suggestion)[!.]?\s*/gi, /\bi('d|would)\s+be\s+happy\s+to\s+/gi, /\bplease\s+note\s+that\s*/gi, /\bit('s|is)\s+important\s+to\s+note\s+that\s*/gi, /\bthank\s+you\s+for\s+(your\s+)?(patience|question|feedback)[.!]?\s*/gi, ]; // --------------------------------------------------------------------------- // Raw output type classifier // --------------------------------------------------------------------------- /** * Classify the raw output into a translation strategy. * * @param {string} raw * @param {string} [sourceType] Explicit type hint from caller ('doctor', 'sync', 'agent', etc.) * @returns {string} Output type key */ function classifyOutput(raw, sourceType) { if (sourceType) return sourceType; if (!raw || typeof raw !== 'string') return 'empty'; const lower = raw.toLowerCase(); if (lower.includes('error') && (lower.includes('stack') || lower.includes(' at '))) { return 'stack-trace'; } if (/✓|✗|●\s*(pass|fail)|tests?\s+(passed|failed)/i.test(raw)) { return 'test-results'; } if (/\b(synced?|updated?\s+to|redeployed|provider)\b/i.test(raw)) { return 'sync-log'; } if (/\b(healthy?|ok|warning|error|issue)\b.*\n.*\b(healthy?|ok|warning|error|issue)\b/i.test(raw)) { return 'doctor-output'; } if (/^(error|warn(ing)?|info|debug):/im.test(raw)) { return 'log-output'; } if (raw.trim().length === 0) { return 'empty'; } return 'agent-result'; } // --------------------------------------------------------------------------- // ConciergeResponseTranslator // --------------------------------------------------------------------------- export class ConciergeResponseTranslator { /** * @param {Object} [options] * @param {boolean} [options.redactPaths=true] Strip absolute paths from output * @param {boolean} [options.stripFiller=true] Remove filler phrases * @param {boolean} [options.suppressNoise=true] Remove debug/trace lines * @param {number} [options.summaryThreshold=500] Character count above which * output is summarised with an expand offer (0 = never summarise) */ constructor({ redactPaths = true, stripFiller = true, suppressNoise = true, summaryThreshold = 500, } = {}) { this.redactPaths = redactPaths; this.stripFiller = stripFiller; this.suppressNoise = suppressNoise; this.summaryThreshold = summaryThreshold; } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- /** * Translate raw output into a composed, concierge-voiced response. * * @param {string} raw Raw output from the underlying skill/agent/flow * @param {Object} [options] * @param {boolean} [options.raw] Skip translation (return raw as-is) * @param {boolean} [options.verbose] Alias for raw * @param {string} [options.sourceType] Output type hint (see classifyOutput) * @param {boolean} [options.discreet] Force discreet mode * @param {boolean} [options.isSensitive] Mark as sensitive (enables discreet mode) * @param {string} [options.context] Brief context string for anonymous outputs * @returns {TranslationResult} */ translate(raw, options = {}) { const { raw: rawMode, verbose, sourceType, discreet, isSensitive, context } = options; const bypass = rawMode || verbose; if (bypass) { return { translated: raw ?? '', bypassed: true, discreetApplied: false }; } const outputType = classifyOutput(raw, sourceType); const applyDiscreet = discreet || isSensitive || SENSITIVE_OPERATION_CATEGORIES.has(outputType); let result = raw ?? ''; // 1. Suppress sensitive content in discreet mode if (applyDiscreet) { result = this._applySensitiveRedaction(result); } // 2. Remove technical noise (stack frames, debug lines) if (this.suppressNoise) { result = this._suppressNoise(result); } // 3. Redact absolute file paths if (this.redactPaths && applyDiscreet) { result = result.replace(PATH_PATTERN, '[path]'); } // 4. Strip filler phrases if (this.stripFiller) { result = this._stripFiller(result); } // 5. Apply type-specific translation result = this._translateByType(result, outputType, context); // 6. Normalise whitespace result = result.replace(/\n{3,}/g, '\n\n').trim(); return { translated: result, bypassed: false, discreetApplied: applyDiscreet, outputType, }; } /** * Check whether a string contains sensitive content. * Used by callers to decide whether to enable discreet mode. * * @param {string} text * @returns {boolean} */ isSensitiveContent(text) { if (!text) return false; return SENSITIVE_PATTERNS.some((p) => p.test(text)); } /** * Determine whether a given operation category triggers discreet mode. * * @param {string} category * @returns {boolean} */ isSensitiveCategory(category) { return SENSITIVE_OPERATION_CATEGORIES.has(category); } // ------------------------------------------------------------------------- // Type-specific translation strategies // ------------------------------------------------------------------------- /** * Route to the appropriate translation strategy by output type. */ _translateByType(text, outputType, context) { switch (outputType) { case 'doctor-output': return this._translateDoctorOutput(text); case 'stack-trace': return this._translateStackTrace(text, context); case 'sync-log': return this._translateSyncLog(text); case 'test-results': return this._translateTestResults(text); case 'log-output': return this._translateLogOutput(text); case 'empty': return this._translateEmpty(context); case 'token-rotation': case 'credential-update': case 'key-management': return this._translateSensitiveOp(text, context); default: return this._translateAgentResult(text); } } /** * `aiwg doctor` output → concise health summary */ _translateDoctorOutput(text) { const issues = []; const lines = text.split('\n'); let healthyCount = 0; let issueCount = 0; for (const line of lines) { if (/\b(ok|healthy|pass|✓)\b/i.test(line)) { healthyCount++; } else if (/\b(error|fail|warn(ing)?|issue|missing|✗)\b/i.test(line)) { issueCount++; // Extract a short label from the line for the summary const label = line.replace(/^.*?(error|fail|warn(ing)?|issue|missing|✗)[:\s]*/i, '').trim(); if (label && label.length < 120) { issues.push(label); } } } if (issueCount === 0) { return 'All systems healthy.'; } const summary = `${issueCount} issue${issueCount === 1 ? '' : 's'} found.`; const detail = issues.slice(0, 3).join('; '); return issues.length > 0 ? `${summary} ${detail}${issues.length > 3 ? ` (and ${issues.length - 3} more)` : '.'}` : summary; } /** * Stack trace / error → actionable summary */ _translateStackTrace(text, context) { // Extract the top-level error message (first non-blank line) const lines = text.split('\n').filter((l) => l.trim()); const topError = lines.find((l) => /\b(error|exception|failed|cannot|unable)\b/i.test(l)); const errorLabel = topError ? topError.replace(/^(Error|TypeError|RangeError|SyntaxError):\s*/i, '').trim() : 'an unexpected error'; const what = context ? `${context}` : 'the operation'; return `I encountered a problem with ${what}${errorLabel}. The details have been logged.`; } /** * Verbose sync log → compact confirmation */ _translateSyncLog(text) { // Look for version number const versionMatch = text.match(/v?(\d{4}\.\d+\.\d+)/); const version = versionMatch ? `v${versionMatch[1]}` : null; // Count redeployed providers const redeployMatches = text.match(/redeploy(ed|ing)/gi); const redeployCount = redeployMatches ? redeployMatches.length : null; if (version && redeployCount) { return `Updated to ${version}. ${redeployCount} provider${redeployCount === 1 ? '' : 's'} redeployed.`; } if (version) { return `Updated to ${version}.`; } return 'Sync complete.'; } /** * Test results → concise pass/fail summary */ _translateTestResults(text) { const passMatch = text.match(/(\d+)\s+(tests?\s+)?(passed|✓)/i); const failMatch = text.match(/(\d+)\s+(tests?\s+)?(failed|✗|●)/i); const passed = passMatch ? parseInt(passMatch[1], 10) : null; const failed = failMatch ? parseInt(failMatch[1], 10) : null; if (failed !== null && failed > 0) { const detail = passed !== null ? ` (${passed} passed)` : ''; return `${failed} test${failed === 1 ? '' : 's'} failed${detail}. Review the output for details.`; } if (passed !== null) { return `All ${passed} test${passed === 1 ? '' : 's'} passed.`; } // Fallback: preserve the result but strip noise return text.trim(); } /** * Structured log output → surface warnings/errors only */ _translateLogOutput(text) { const lines = text.split('\n'); const important = lines.filter((l) => /^(error|warn(ing)?|fatal):/i.test(l.trim())); if (important.length === 0) { return 'Completed successfully.'; } return important.slice(0, 5).join('\n'); } /** * Empty output → in-persona acknowledgment */ _translateEmpty(context) { return context ? `${context} — no output returned.` : 'Completed — no output to report.'; } /** * Sensitive operation output → minimal confirmation */ _translateSensitiveOp(_text, context) { const what = context || 'The operation'; return `${what} completed. Ask for details if needed.`; } /** * Generic agent/skill result → structured summary */ _translateAgentResult(text) { if (!text || !text.trim()) return this._translateEmpty(); // If already compact, return as-is if (text.trim().length <= this.summaryThreshold) { return text.trim(); } // Long output: extract the first meaningful paragraph as a summary const paragraphs = text.split(/\n\n+/).filter((p) => p.trim()); if (paragraphs.length > 1) { const summary = paragraphs[0].trim(); return summary + '\n\n*(Full details available — ask to expand.)*'; } return text.trim(); } // ------------------------------------------------------------------------- // Noise / filler / redaction helpers // ------------------------------------------------------------------------- /** * Redact lines containing sensitive credential patterns. */ _applySensitiveRedaction(text) { return text .split('\n') .map((line) => { for (const pattern of SENSITIVE_PATTERNS) { if (pattern.test(line)) { return '[sensitive content redacted]'; } } return line; }) .join('\n'); } /** * Remove debug/trace noise lines while preserving the rest. */ _suppressNoise(text) { return text .split('\n') .filter((line) => !NOISE_PATTERNS.some((p) => p.test(line))) .join('\n'); } /** * Strip filler phrases that violate the "pertinent" tone rule. */ _stripFiller(text) { let result = text; for (const pattern of FILLER_PHRASES) { result = result.replace(pattern, ''); } return result; } } // --------------------------------------------------------------------------- // Exports // --------------------------------------------------------------------------- export { SENSITIVE_OPERATION_CATEGORIES, SENSITIVE_PATTERNS, NOISE_PATTERNS, FILLER_PHRASES, classifyOutput, }; /** * @typedef {Object} TranslationResult * @property {string} translated The translated (or raw-bypassed) response * @property {boolean} bypassed True when raw/verbose bypass was applied * @property {boolean} discreetApplied True when discreet mode was applied * @property {string} [outputType] Classified output type (absent when bypassed) */