#!/usr/bin/env node // user-prompt-context.mjs // UserPromptSubmit hook for linkedin-thought-leadership plugin // // Two-tier keyword matching in user prompts: // Tier 1: Strong signals (slash commands, explicit phrases) // Tier 2: "linkedin" + intent word, excluding plugin dev phrases // // When matched, injects voice profile reference, recent posts, // planned topic, weekly progress, and quality scorecard reminder. // // Exit codes: // 0 - Always allow (informational hook) import { readFileSync, existsSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const pluginRoot = join(__dirname, '..', '..'); const home = process.env.HOME || process.env.USERPROFILE || ''; const stateFile = join(home, '.claude', 'linkedin-thought-leadership.local.md'); // Read stdin JSON let input; try { input = JSON.parse(readFileSync(0, 'utf-8')); } catch { process.stdout.write(JSON.stringify({ continue: true })); process.exit(0); } const userPrompt = (input.query ?? input.content ?? input.prompt ?? '').toLowerCase(); if (!userPrompt) { process.stdout.write(JSON.stringify({ continue: true })); process.exit(0); } // === Two-tier keyword matching === let isLinkedin = false; // Tier 1: Strong signals const strongSignals = [ '/linkedin:post', '/linkedin:quick', '/linkedin:batch', '/linkedin:pipeline', '/linkedin:publish', '/linkedin:video', '/linkedin:multiplatform', '/linkedin:react', '/linkedin:summarize', 'linkedin post', 'lag en post', 'skriv en post', 'write a post', 'quick post', 'create post', 'react to this', 'turn this article into', ]; for (const signal of strongSignals) { if (userPrompt.includes(signal)) { isLinkedin = true; break; } } // Tier 1.5: URL + intent — detect URLs with LinkedIn-relevant intent if (!isLinkedin) { const urlPattern = /https?:\/\/\S+/; if (urlPattern.test(userPrompt)) { const urlIntentWords = ['react', 'post', 'share', 'write', 'comment', 'turn', 'create', 'linkedin']; for (const word of urlIntentWords) { if (userPrompt.includes(word)) { isLinkedin = true; break; } } } } // Tier 2: "linkedin" + intent word (excluding plugin dev phrases) if (!isLinkedin && userPrompt.includes('linkedin')) { const intentWords = [ 'write', 'create', 'draft', 'publish', 'skriv', 'lag', 'post', 'innlegg', 'article', 'artikkel', ]; const devExclude = /(update|fix|change|modify|edit|refactor|debug|test).*(plugin|hook|script|command|agent|skill|config)/i; for (const intent of intentWords) { if (userPrompt.includes(intent)) { if (!devExclude.test(userPrompt)) { isLinkedin = true; break; } } } } if (!isLinkedin) { process.stdout.write(JSON.stringify({ continue: true })); process.exit(0); } // === Build context enrichment === let context = '**LinkedIn Context Enrichment (auto-injected):**\n\n'; // 1. Voice profile reference const voiceFile = join(pluginRoot, 'assets', 'voice-samples', 'authentic-voice-samples.md'); if (existsSync(voiceFile)) { context += '**Voice Profile:** Read `assets/voice-samples/authentic-voice-samples.md` for tone matching.\n\n'; } // 2-4. State file data if (existsSync(stateFile)) { try { const stateContent = readFileSync(stateFile, 'utf-8'); // Recent posts section const recentMatch = stateContent.match(/^## Recent Posts\s*\n([\s\S]*?)(?=^## |$)/m); if (recentMatch) { const recentLines = recentMatch[1] .split('\n') .filter(l => l.trim() && !l.startsWith('