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>
This commit is contained in:
parent
7194a37129
commit
39f8b275a6
143 changed files with 32662 additions and 0 deletions
|
|
@ -0,0 +1,151 @@
|
|||
#!/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);
|
||||
Loading…
Add table
Add a link
Reference in a new issue