refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)

BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.

- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
  config-audit v5.0.0 snapshots) intentionally retain the old slug

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-29 11:32:02 +02:00
commit 52fa87a255
59 changed files with 148 additions and 122 deletions

View file

@ -29,7 +29,7 @@ describe('generateIcal', () => {
const ical = generateIcal([]);
assert.match(ical, /^BEGIN:VCALENDAR\r\n/);
assert.match(ical, /\r\nEND:VCALENDAR\r\n$/);
assert.match(ical, /PRODID:-\/\/linkedin-thought-leadership\/\/EN/);
assert.match(ical, /PRODID:-\/\/linkedin-studio\/\/EN/);
assert.match(ical, /VERSION:2\.0/);
assert.ok(!ical.includes('BEGIN:VEVENT'), 'should not contain VEVENT');
});
@ -39,7 +39,7 @@ describe('generateIcal', () => {
assert.match(ical, /BEGIN:VEVENT/);
assert.match(ical, /DTSTART;TZID=Europe\/Oslo:20260414T083000/);
assert.match(ical, /SUMMARY:LinkedIn: AI strategy in public sector/);
assert.match(ical, /UID:post-2026-04-14-ai-strategy@linkedin-thought-leadership/);
assert.match(ical, /UID:post-2026-04-14-ai-strategy@linkedin-studio/);
assert.match(ical, /END:VEVENT/);
});

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Cross-platform clipboard helper for linkedin-thought-leadership plugin
// Cross-platform clipboard helper for linkedin-studio plugin
// Copies text to system clipboard using platform-native commands.
// Standalone: reads stdin and copies it. Import: export { copyToClipboard, clipboardAvailable }

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
// content-gatekeeper.mjs
// Unified PreToolUse/PostToolUse gatekeeper for linkedin-thought-leadership plugin
// Unified PreToolUse/PostToolUse gatekeeper for linkedin-studio plugin
//
// Replaces 4 nearly identical bash scripts:
// pre-content-quality-gate.sh, pre-voice-guardian.sh,

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// RFC 5545 iCal generator for linkedin-thought-leadership plugin
// RFC 5545 iCal generator for linkedin-studio plugin
// Import: import { generateIcal, generateIcalFromQueue, writeIcalFile } from './ical-generator.mjs';
// Standalone: node ical-generator.mjs --from-queue --output path/to/schedule.ics
@ -136,7 +136,7 @@ export function generateIcal(events, options = {}) {
const lines = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//linkedin-thought-leadership//EN',
'PRODID:-//linkedin-studio//EN',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
];
@ -153,7 +153,7 @@ export function generateIcal(events, options = {}) {
lines.push(
'BEGIN:VEVENT',
`UID:${event.id}@linkedin-thought-leadership`,
`UID:${event.id}@linkedin-studio`,
`DTSTAMP:${now}`,
`DTSTART;TZID=${tz}:${dtstart}`,
`DTEND;TZID=${tz}:${dtend}`,

View file

@ -33,7 +33,7 @@ export function isLinkedInContent(filePath) {
// POSITIVE: explicit LinkedIn content paths only
if (normalized.startsWith('assets/drafts/') || normalized.includes('/assets/drafts/')) return true;
if (normalized.includes('/linkedin-posts/')) return true;
if (normalized.includes('/linkedin-thought-leadership/assets/')) return true;
if (normalized.includes('/linkedin-studio/assets/')) return true;
// DEFAULT: everything else is NOT LinkedIn content
return false;

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Personalization score calculator for linkedin-thought-leadership plugin
// Personalization score calculator for linkedin-studio plugin
// Checks 8 asset categories for real user data vs placeholder templates
// Standalone: outputs SCORE:N|M/8 assets personalized
// Import: export function calculateScore(pluginRoot) => { score, personalized, categories }

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Notification hook for linkedin-thought-leadership plugin
// Notification hook for linkedin-studio plugin
// Fires on idle_prompt to show posting reminders. Rate-limited: max once per 30 min.
import { readFileSync, existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
@ -10,7 +10,7 @@ import { queueToday, queueOverdue } from './queue-manager.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
const HOME = process.env.HOME || process.env.USERPROFILE || '';
const STATE_FILE = join(HOME, '.claude', 'linkedin-thought-leadership.local.md');
const STATE_FILE = join(HOME, '.claude', 'linkedin-studio.local.md');
const SESSION_DIR = '/tmp/linkedin-hooks';
const COOLDOWN_FILE = join(SESSION_DIR, 'last-notification');
const COOLDOWN_SECONDS = 1800;

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
// pre-compact.mjs
// PreCompact hook for linkedin-thought-leadership plugin
// PreCompact hook for linkedin-studio plugin
// Reminds Claude to preserve critical LinkedIn session context before compaction
//
// Exit codes:

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Queue management library for linkedin-thought-leadership plugin
// Queue management library for linkedin-studio plugin
// Import: import { queueRead, queueToday, ... } from './queue-manager.mjs';
// Replaces python3 dependency with native Node.js JSON/Date operations

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// Quick-import helper for linkedin-thought-leadership plugin
// Quick-import helper for linkedin-studio plugin
// Opens LinkedIn analytics in browser, watches ~/Downloads for new CSV files
import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'node:fs';

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
// SessionStart hook for linkedin-thought-leadership plugin
// SessionStart hook for linkedin-studio plugin
// Reads persistent state and session context, outputs JSON with additionalContext
import { readFileSync, existsSync, copyFileSync, writeFileSync, mkdirSync } from 'node:fs';
@ -12,7 +12,7 @@ import { applyWeekRollover } from './week-rollover.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
const HOME = process.env.HOME || process.env.USERPROFILE || '';
const STATE_FILE = join(HOME, '.claude', 'linkedin-thought-leadership.local.md');
const STATE_FILE = join(HOME, '.claude', 'linkedin-studio.local.md');
function extractYaml(content, key) {
const re = new RegExp(`^${key}: *"?([^"\\n]*)"?`, 'm');
@ -234,7 +234,7 @@ if (existsSync(STATE_FILE)) {
}
// Build context output
context = 'LinkedIn Thought Leadership session context loaded.\\n\\n';
context = 'LinkedIn Studio session context loaded.\\n\\n';
context += `## Status\\n\`\`\`\\n${statusLine}\\n\`\`\`\\n\\n`;
if (weekResetNote) context += `**${weekResetNote}**\\n\\n`;

View file

@ -1,4 +1,4 @@
// Deterministic state mutation functions for linkedin-thought-leadership plugin.
// Deterministic state mutation functions for linkedin-studio plugin.
// Pure functions operate on string content (same pattern as week-rollover.mjs).
// I/O wrapper (writeState) handles file reads/writes (same pattern as queue-manager.mjs).
@ -9,7 +9,7 @@ import { applyWeekRollover } from './week-rollover.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const HOME = process.env.HOME || process.env.USERPROFILE || '';
const STATE_FILE = process.env.STATE_FILE || join(HOME, '.claude', 'linkedin-thought-leadership.local.md');
const STATE_FILE = process.env.STATE_FILE || join(HOME, '.claude', 'linkedin-studio.local.md');
function replaceField(content, field, value) {
return content.replace(

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
// stop-reminder.mjs
// Stop hook for linkedin-thought-leadership plugin
// Stop hook for linkedin-studio plugin
//
// Only fires if LinkedIn content was worked on (session marker exists).
// First stop: blocks with reason (Claude processes reminders).

View file

@ -1,6 +1,6 @@
#!/usr/bin/env node
// user-prompt-context.mjs
// UserPromptSubmit hook for linkedin-thought-leadership plugin
// UserPromptSubmit hook for linkedin-studio plugin
//
// Two-tier keyword matching in user prompts:
// Tier 1: Strong signals (slash commands, explicit phrases)
@ -19,7 +19,7 @@ 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');
const stateFile = join(home, '.claude', 'linkedin-studio.local.md');
// Read stdin JSON
let input;