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>
90 lines
2.4 KiB
JavaScript
90 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// stop-reminder.mjs
|
|
// Stop hook for linkedin-thought-leadership plugin
|
|
//
|
|
// Only fires if LinkedIn content was worked on (session marker exists).
|
|
// First stop: blocks with reason (Claude processes reminders).
|
|
// Subsequent stops within 60s: allows (prevents infinite loop).
|
|
//
|
|
// Exit codes:
|
|
// 0 - Allow (pass through or second stop)
|
|
// 2 - Not used; uses {"decision": "block"} JSON instead
|
|
|
|
import { readFileSync, writeFileSync, existsSync, statSync, unlinkSync, mkdirSync } 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 promptFile = join(pluginRoot, 'hooks', 'prompts', 'state-update-reminder.md');
|
|
|
|
const sessionDir = '/tmp/linkedin-hooks';
|
|
const sessionMarker = join(sessionDir, 'session-active');
|
|
const lockFile = join(sessionDir, 'stop-hook.lock');
|
|
|
|
function nowSeconds() {
|
|
return Date.now() / 1000;
|
|
}
|
|
|
|
function fileAgeSeconds(filePath) {
|
|
try {
|
|
return nowSeconds() - statSync(filePath).mtime.getTime() / 1000;
|
|
} catch {
|
|
return Infinity;
|
|
}
|
|
}
|
|
|
|
function safeUnlink(filePath) {
|
|
try { unlinkSync(filePath); } catch { /* ignore */ }
|
|
}
|
|
|
|
// Read stdin
|
|
let input;
|
|
try {
|
|
input = JSON.parse(readFileSync(0, 'utf-8'));
|
|
} catch {
|
|
input = {};
|
|
}
|
|
|
|
// Infinite loop prevention: if Claude is already continuing from a Stop hook
|
|
if (input.stop_hook_active === true) {
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
// No session marker = no LinkedIn work done
|
|
if (!existsSync(sessionMarker)) {
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Staleness check: ignore markers older than 12 hours (43200 seconds)
|
|
if (fileAgeSeconds(sessionMarker) > 43200) {
|
|
safeUnlink(sessionMarker);
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
// Infinite-loop prevention: lock file within 60 seconds = second stop
|
|
if (existsSync(lockFile)) {
|
|
if (fileAgeSeconds(lockFile) < 60) {
|
|
safeUnlink(lockFile);
|
|
safeUnlink(sessionMarker);
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
safeUnlink(lockFile);
|
|
}
|
|
|
|
// First stop: create lock and block with reminder prompt
|
|
mkdirSync(sessionDir, { recursive: true });
|
|
writeFileSync(lockFile, '');
|
|
|
|
if (!existsSync(promptFile)) {
|
|
process.stdout.write('{}');
|
|
process.exit(0);
|
|
}
|
|
|
|
const promptContent = readFileSync(promptFile, 'utf-8');
|
|
process.stdout.write(JSON.stringify({ decision: 'block', reason: promptContent }));
|
|
process.exit(0);
|