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>
151 lines
4.8 KiB
JavaScript
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);
|