ktg-plugin-marketplace/plugins/linkedin-thought-leadership/hooks/scripts/user-prompt-context.mjs
Kjell Tore Guttormsen 39f8b275a6 feat(linkedin-thought-leadership): v1.0.0 — initial open-source import
Build LinkedIn thought leadership with algorithmic understanding,
strategic consistency, and AI-assisted content creation. Updated for
the January 2026 360Brew algorithm change.

16 agents, 25 commands, 6 skills, 9 hooks, 24 reference docs.

Personal data sanitized: voice samples generalized to template,
high-engagement posts cleared, region-specific references replaced
with placeholders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 22:09:03 +02:00

151 lines
4.8 KiB
JavaScript

#!/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('<!--'))
.slice(0, 5);
if (recentLines.length > 0) {
context += `**Recent posts (avoid repetition):**\n${recentLines.join('\n')}\n\n`;
}
}
// Next planned topic from YAML frontmatter
const topicMatch = stateContent.match(/^next_planned_topic:\s*"?([^"\n]*)"?\s*$/m);
if (topicMatch && topicMatch[1].trim()) {
context += `**Planned next topic:** ${topicMatch[1].trim()}\n\n`;
}
// Weekly progress from YAML frontmatter
const postsMatch = stateContent.match(/^posts_this_week:\s*(\d+)/m);
const goalMatch = stateContent.match(/^weekly_goal:\s*(\d+)/m);
if (postsMatch && goalMatch) {
context += `**Weekly progress:** ${postsMatch[1]}/${goalMatch[1]} posts this week.\n\n`;
}
} catch {
// State file read error — skip enrichment
}
}
// 5.5 URL detection hint
const urlMatch = (input.query ?? input.content ?? input.prompt ?? '').match(/https?:\/\/\S+/);
if (urlMatch) {
context += '**URL detected:** Consider using /linkedin:react for this URL.\n\n';
}
// 5. Quality scorecard reminder
context += '**Remember:** Use `assets/checklists/quality-scorecard.md` before finalizing.\n';
process.stdout.write(JSON.stringify({ continue: true, systemMessage: context }));
process.exit(0);