2026-07-10 14:59:17 +02:00
#!/usr/bin/env node
"use strict" ;
const fs = require ( "fs" );
2026-07-25 18:24:53 +02:00
const path = require ( "path" );
2026-07-10 14:59:17 +02:00
const settingsPath = process . argv [ 2 ];
if ( ! settingsPath ) {
console . error ( "usage: enable-plugins.js <settings.json>" );
process . exit ( 2 );
}
const MARKETPLACES = {
caveman : { source : { source : "github" , repo : "JuliusBrussee/caveman" } },
ponytail : { source : { source : "github" , repo : "DietrichGebert/ponytail" } },
2026-11-04 00:15:07 +01:00
};
const PLUGINS = {
"caveman@caveman" : true ,
"ponytail@ponytail" : true ,
2026-07-10 14:59:17 +02:00
};
2026-11-04 11:18:47 +01:00
// Trip-wire: `codebase-memory-mcp install` registers the same server for every agent, Claude Code included; the plugin on top runs it twice.
const RETIRED_PLUGINS = { "codebase-memory@codebase-memory" : "codebase-memory" };
2026-11-03 15:45:57 +01:00
const REMINDERS = [ "kvb-dream" , "kvb-coach" ];
2026-11-03 13:43:15 +01:00
const RETIRED = [
"autotune/hooks/reminder.sh" ,
"kvb-skills-tune/hooks/reminder.sh" ,
"kvb-skill-tune/hooks/reminder.sh" ,
];
2026-10-31 23:46:28 +01:00
const RENAMED = {
2026-11-03 13:43:15 +01:00
"dream/hooks/reminder.sh" : "kvb-dream/hooks/reminder.sh" ,
"kvb-skills-dream/hooks/reminder.sh" : "kvb-dream/hooks/reminder.sh" ,
"kvb-skill-dream/hooks/reminder.sh" : "kvb-dream/hooks/reminder.sh" ,
2026-10-31 23:46:28 +01:00
"democracy/scripts/council.py" : "kvb-org-organize/scripts/council.py" ,
"democracy/scripts/watch.py" : "kvb-org-organize/scripts/watch.py" ,
"subagent-model/scripts/log.py" : "kvb-org-perform/scripts/log.py" ,
2026-11-04 16:54:20 +01:00
"kvb-org-organize/scripts/news.py" : "kvb-org-radio/scripts/radio.py" ,
2026-10-31 23:46:28 +01:00
};
2026-07-10 14:59:17 +02:00
let data = {};
if ( fs . existsSync ( settingsPath )) {
const raw = fs . readFileSync ( settingsPath , "utf8" ). trim ();
if ( raw ) {
try {
data = JSON . parse ( raw );
} catch ( err ) {
console . error ( `skills: ${ settingsPath } is not valid JSON; leaving it untouched ( ${ err . message } ).` );
process . exit ( 0 );
}
}
}
data . extraKnownMarketplaces = data . extraKnownMarketplaces || {};
for ( const [ name , entry ] of Object . entries ( MARKETPLACES )) {
if ( ! data . extraKnownMarketplaces [ name ]) data . extraKnownMarketplaces [ name ] = entry ;
}
data . enabledPlugins = Object . assign ( data . enabledPlugins || {}, PLUGINS );
2026-11-04 11:18:47 +01:00
for ( const [ plugin , marketplace ] of Object . entries ( RETIRED_PLUGINS )) {
delete data . enabledPlugins [ plugin ];
delete data . extraKnownMarketplaces [ marketplace ];
}
2026-11-03 23:22:46 +01:00
data . env = data . env || {};
if ( ! data . env . CLAUDE_CODE_AUTO_COMPACT_WINDOW ) data . env . CLAUDE_CODE_AUTO_COMPACT_WINDOW = "500000" ;
2026-07-10 14:59:17 +02:00
2026-11-03 23:21:06 +01:00
const pluginRepos = new Set ( Object . values ( MARKETPLACES ). map (( entry ) => entry . source . repo ));
const lock = JSON . parse ( fs . readFileSync ( path . join ( __dirname , ".." , "skills-lock.json" ), "utf8" ));
for ( const [ name , entry ] of Object . entries ( lock . skills || {})) {
if ( pluginRepos . has ( entry . source )) {
fs . rmSync ( path . join ( path . dirname ( settingsPath ), "skills" , name ), { recursive : true , force : true });
}
}
2026-07-25 18:24:53 +02:00
data . hooks = data . hooks || {};
2026-11-03 13:43:15 +01:00
const retired = ( hook ) =>
typeof hook . command === "string" && RETIRED . some (( r ) => hook . command . includes ( `/skills/ ${ r } ` ));
for ( const [ event , entries ] of Object . entries ( data . hooks )) {
if ( ! Array . isArray ( entries )) continue ;
data . hooks [ event ] = entries
. map (( entry ) =>
Array . isArray ( entry . hooks ) ? { ... entry , hooks : entry . hooks . filter (( h ) => ! retired ( h )) } : entry )
. filter (( entry ) => ! Array . isArray ( entry . hooks ) || entry . hooks . length > 0 );
}
2026-10-31 23:46:28 +01:00
for ( const entries of Object . values ( data . hooks )) {
if ( ! Array . isArray ( entries )) continue ;
for ( const entry of entries ) {
for ( const hook of entry . hooks || []) {
if ( typeof hook . command !== "string" ) continue ;
for ( const [ from , to ] of Object . entries ( RENAMED )) {
hook . command = hook . command . replace ( `/skills/ ${ from } ` , `/skills/ ${ to } ` );
}
}
}
}
2026-07-25 18:24:53 +02:00
data . hooks . UserPromptSubmit = data . hooks . UserPromptSubmit || [];
2026-08-02 19:34:05 +02:00
const alreadyRegistered = JSON . stringify ( data . hooks . UserPromptSubmit );
for ( const skill of REMINDERS ) {
if ( alreadyRegistered . includes ( ` ${ skill } /hooks/reminder.sh` )) continue ;
const reminder = path . join ( path . dirname ( settingsPath ), "skills" , skill , "hooks" , "reminder.sh" );
2026-07-25 18:24:53 +02:00
data . hooks . UserPromptSubmit . push ({
hooks : [{ type : "command" , command : `bash ${ JSON . stringify ( reminder ) } ` }],
});
}
2026-10-30 15:23:32 +01:00
data . hooks . PreToolUse = data . hooks . PreToolUse || [];
2026-10-31 23:46:28 +01:00
if ( ! JSON . stringify ( data . hooks . PreToolUse ). includes ( "kvb-org-organize/scripts/council.py" )) {
const council = path . join ( path . dirname ( settingsPath ), "skills" , "kvb-org-organize" , "scripts" , "council.py" );
2026-10-30 15:23:32 +01:00
data . hooks . PreToolUse . push ({
matcher : "Edit|Write|MultiEdit|NotebookEdit" ,
hooks : [{ type : "command" , command : `python3 ${ JSON . stringify ( council ) } gate` }],
});
}
2026-10-31 17:27:26 +01:00
const SCRIPT_HOOKS = [
2026-11-04 16:54:20 +01:00
[ "kvb-org-organize/scripts/watch.py" , "UserPromptSubmit" , null , "watch" , {}],
[ "kvb-org-radio/scripts/radio.py" , "UserPromptSubmit" , null , "hook" , {}],
[ "kvb-org-radio/scripts/radio.py" , "PostToolBatch" , null , "tool" , {}],
[ "kvb-org-radio/scripts/radio.py" , "Stop" , null , "listen" , { asyncRewake : true , timeout : 21600 }],
[ "kvb-org-organize/scripts/watch.py" , "PostToolUse" , "Bash" , "watch" , {}],
[ "kvb-org-organize/scripts/watch.py" , "PreToolUse" , "Bash" , "block" , {}],
[ "kvb-org-queue/scripts/queue.py" , "PreToolUse" , "Bash" , "block" , {}],
[ "kvb-org-perform/scripts/log.py" , "PreToolUse" , "Agent|Task" , "spawn" , {}],
[ "kvb-org-perform/scripts/log.py" , "PostToolUse" , "Bash" , "persist" , {}],
2026-10-30 17:37:47 +01:00
];
2026-11-04 16:54:20 +01:00
for ( const [ script , event , matcher , sub , extra ] of SCRIPT_HOOKS ) {
2026-10-30 17:37:47 +01:00
data . hooks [ event ] = data . hooks [ event ] || [];
const registered = data . hooks [ event ]. some (( entry ) =>
( entry . hooks || []). some (( hook ) =>
2026-10-31 17:27:26 +01:00
String ( hook . command ). includes ( script ) && String ( hook . command ). endsWith ( ` ${ sub } ` )));
2026-10-30 17:37:47 +01:00
if ( registered ) continue ;
2026-11-04 16:54:20 +01:00
const file = JSON . stringify ( path . join ( path . dirname ( settingsPath ), "skills" , ... script . split ( "/" )));
// Trip-wire: python3 on a missing file exits 2, which blocks every prompt or tool call; the test makes it exit 1.
const command = `[ -f ${ file } ] && python3 ${ file } ${ sub } ` ;
const entry = { hooks : [{ type : "command" , command , ... extra }] };
2026-10-30 17:37:47 +01:00
data . hooks [ event ]. push ( matcher ? { matcher , ... entry } : entry );
}
2026-07-10 14:59:17 +02:00
fs . writeFileSync ( settingsPath , JSON . stringify ( data , null , 2 ) + "\n" );
2026-11-04 16:54:20 +01:00
console . log ( `skills: enabled the caveman and ponytail plugins (plugin skill copies dropped from this .claude), the ${ REMINDERS . join ( " + " ) } reminders, the kvb-org-organize claim gate and resource watch, the kvb-org-radio hooks, the kvb-org-queue gate, and the kvb-org-perform log in ${ settingsPath } ` );