Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions packages/agents/scripts/extract-plugin-skills.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail

# extract-plugin-skills.sh — Extract skills from Claude Code plugins for Rovo Dev.
#
# Copies SKILL.md files (and companion files) from the plugin cache to agents/rovodev/skills/.
# Finds the latest installed version of the plugin automatically.
#
# Usage:
# extract-plugin-skills.sh <plugin> [skill...]
# extract-plugin-skills.sh --help

readonly PROG="$(basename "$0")"

source "$(git rev-parse --show-toplevel)/functions/colors.sh"

repo_root="$(git rev-parse --show-toplevel)"
plugin_cache="$HOME/.claude/plugins/cache/claude-plugins-official"
output_base="$repo_root/agents/rovodev/skills"

# -- Main flow --

main() {
# Show help (manual check -- getopts cannot parse long options)
if [[ "${1:-}" == "--help" ]]; then
show_usage 0
fi

# Parse options
while getopts ":h" opt; do
case $opt in
h) show_usage 0 ;;
*)
echo "$PROG: unknown option -$OPTARG" >&2
show_usage
;;
esac
done
shift $((OPTIND - 1))

# Validate arguments
if [[ $# -lt 1 ]]; then
echo "$PROG: plugin name is required" >&2
show_usage
fi

plugin="$1"
shift
requested_skills=("$@")

# Find the latest version of the plugin
plugin_dir="$plugin_cache/$plugin"
if [[ ! -d "$plugin_dir" ]]; then
echo "${red}x${normal} $PROG: plugin not found: $plugin" >&2
echo " Looked in: $plugin_dir" >&2
exit 1
fi

latest_version=$(ls -1 "$plugin_dir" | sort -V | tail -1)
if [[ -z "$latest_version" ]]; then
echo "${red}x${normal} $PROG: no versions found for plugin: $plugin" >&2
exit 1
fi

source_dir="$plugin_dir/$latest_version/skills"
if [[ ! -d "$source_dir" ]]; then
echo "${red}x${normal} $PROG: no skills directory in $plugin v$latest_version" >&2
exit 1
fi

echo "Extracting from ${plugin} v${latest_version}"
echo ""

# Determine which skills to extract
if [[ ${#requested_skills[@]} -eq 0 ]]; then
mapfile -t skill_dirs < <(find "$source_dir" -mindepth 1 -maxdepth 1 -type d | sort)
else
skill_dirs=()
for skill in "${requested_skills[@]}"; do
if [[ -d "$source_dir/$skill" ]]; then
skill_dirs+=("$source_dir/$skill")
else
echo "${yellow}!${normal} Skill not found: $skill (skipping)" >&2
fi
done
fi

if [[ ${#skill_dirs[@]} -eq 0 ]]; then
echo "${red}x${normal} $PROG: no skills to extract" >&2
exit 1
fi

extracted=0
for skill_path in "${skill_dirs[@]}"; do
skill_name=$(basename "$skill_path")
target_dir="$output_base/$skill_name"

mkdir -p "$target_dir"

for file in "$skill_path"/*; do
[[ -f "$file" ]] || continue
filename=$(basename "$file")
[[ "$filename" == "CREATION-LOG.md" ]] && continue
cp "$file" "$target_dir/$filename"
done

echo "${green}ok${normal} $skill_name -> $target_dir"
((extracted++)) || true
done

echo ""
echo "Extracted $extracted skill(s) from $plugin v$latest_version"
}

# region | Helper functions
show_usage() {
cat >&2 <<USAGE
Extract skills from Claude Code plugins for Rovo Dev.

Usage:
$PROG <plugin> [skill...]
$PROG --help

Arguments:
<plugin> Plugin name, e.g., superpowers (required)
[skill] Specific skill name(s) to extract (default: all)

Options:
-h, --help Show this help

Examples:
$PROG superpowers
$PROG superpowers brainstorming
$PROG superpowers brainstorming writing-plans
USAGE
exit "${1:-1}"
}
# endregion | Helper functions

main "$@"
14 changes: 14 additions & 0 deletions packages/agents/scripts/functions/colors.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash

# Terminal color definitions for shell scripts.
#
# Source this file to use color variables in your scripts.
# Uses conditional assignment so callers can override colors before sourcing.
#
# Usage:
# source "$repo_dir/functions/colors.sh"

: "${green:=$(tput setaf 2)}"
: "${yellow:=$(tput setaf 3)}"
: "${red:=$(tput setaf 1)}"
: "${normal:=$(tput sgr0)}"
32 changes: 32 additions & 0 deletions packages/factory/scripts/generate-placeholder-pngs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';

const SPRITE_SIZE = 32;
const COLS = 4;
const ROWS = 3;
const WIDTH = SPRITE_SIZE * COLS;
const HEIGHT = SPRITE_SIZE * ROWS;

function generatePlaceholderSvg(color: string, label: string): string {
const frames: string[] = [];
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const x = col * SPRITE_SIZE;
const y = row * SPRITE_SIZE;
const frameNum = row * COLS + col;
frames.push(
`<rect x="${x + 2}" y="${y + 2}" width="${SPRITE_SIZE - 4}" height="${SPRITE_SIZE - 4}" rx="4" fill="${color}" opacity="0.8" />`,
`<text x="${x + SPRITE_SIZE / 2}" y="${y + SPRITE_SIZE / 2 + 3}" text-anchor="middle" font-size="8" font-family="monospace" fill="white">${label}${frameNum}</text>`,
);
}
}
return `<svg xmlns="http://www.w3.org/2000/svg" width="${WIDTH}" height="${HEIGHT}">${frames.join('')}</svg>`;
}

const outDir = join(import.meta.dirname, '../src/client/visualizations/catwalk/sprites/assets');
mkdirSync(outDir, { recursive: true });

writeFileSync(join(outDir, 'subagent.svg'), generatePlaceholderSvg('#888899', 'S'));
writeFileSync(join(outDir, 'orchestrator.svg'), generatePlaceholderSvg('#CCAA44', 'O'));

console.info('Placeholder sprite sheets written to', outDir);
Original file line number Diff line number Diff line change
@@ -1,52 +1,23 @@
import { Actor, BaseAlign, Color, Font, GraphicsGroup, Rectangle, Text, TextAlign, vec, type Vector } from 'excalibur';
import { Actor, type Vector } from 'excalibur';

import { ORCH_IDLE_OPACITY, ORCH_PULSE_MAX, ORCH_PULSE_MIN, PULSE_FREQUENCY } from '../constants/animation.js';
import { ORCH_RADIUS } from '../constants/dimensions.js';
import { WALK_SPEED } from '../constants/timing.js';

const ORCH_W = Math.round(ORCH_RADIUS * 2.2);
const ORCH_H = Math.round(ORCH_RADIUS * 1.6);
const ORCH_COLOR = '#FFD700';
import { getAnimation } from '../sprites/catwalk-sprite-loader.js';

export interface OrchestratorActorConfig {
working: boolean;
}

/** Renders the orchestrator as a gold rectangle with label, supporting walk and pulse animations. */
/** Renders the orchestrator as an animated sprite on the catwalk rail, supporting walk and pulse animations. */
export class OrchestratorActor extends Actor {
private _working = false;
private _elapsed = 0;

constructor(config: OrchestratorActorConfig, position: Vector) {
super({ pos: position });

const rect = new Rectangle({
width: ORCH_W,
height: ORCH_H,
color: Color.fromHex(ORCH_COLOR),
});

const label = new Text({
text: 'ORCH',
color: Color.fromHex('#111111'),
font: new Font({
size: 9,
bold: true,
family: 'monospace',
textAlign: TextAlign.Center,
baseAlign: BaseAlign.Middle,
}),
});

const group = new GraphicsGroup({
useAnchor: false,
members: [
{ graphic: rect, offset: vec(0, 0) },
{ graphic: label, offset: vec(ORCH_W / 2, ORCH_H / 2), useBounds: false },
],
});

this.graphics.use(group);
const animation = getAnimation('orchestrator', config.working ? 'working' : 'idle');
this.graphics.use(animation);
this._working = config.working;
this.graphics.opacity = config.working ? ORCH_PULSE_MAX : ORCH_IDLE_OPACITY;
}
Expand All @@ -56,9 +27,11 @@ export class OrchestratorActor extends Actor {
this.actions.moveTo(pos, WALK_SPEED);
}

/** Toggle the pulsing working glow. */
/** Toggle the pulsing working glow and switch sprite animation. */
setWorking(working: boolean): void {
this._working = working;
const animation = getAnimation('orchestrator', working ? 'working' : 'idle');
this.graphics.use(animation);
if (!working) {
this._elapsed = 0;
this.graphics.opacity = ORCH_IDLE_OPACITY;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Actor, BaseAlign, Circle, Color, Font, GraphicsGroup, Text, TextAlign, vec, type Vector } from 'excalibur';
import { Actor, Color, GraphicsGroup, Rectangle, vec, type Vector } from 'excalibur';

import { SPRITE_SIZE } from '../../../game/sprites/sprite-definitions.js';
import { AGENT_PULSE_MAX, AGENT_PULSE_MIN, DEACTIVATED_OPACITY, PULSE_FREQUENCY } from '../constants/animation.js';
import { AGENT_RADIUS } from '../constants/dimensions.js';
import { PAUSE_DURATION } from '../constants/timing.js';
import { getAnimation } from '../sprites/catwalk-sprite-loader.js';
import type { AgentAnimationState } from '../types.js';

const ACCENT_BAR_HEIGHT = 4;

export interface StationAgentActorConfig {
id: string;
role: string;
Expand Down Expand Up @@ -33,50 +36,28 @@ function opacityForState(state: AgentAnimationState): number {
}
}

/** Renders a station-bound agent as a colored circle with a role label, dimmed by animation state. */
/** Renders a station-bound agent as an animated sprite with a colored accent bar, dimmed by animation state. */
export class StationAgentActor extends Actor {
private _state: AgentAnimationState;
private _config: StationAgentActorConfig;
private _pulsing = false;
private _elapsed = 0;

constructor(config: StationAgentActorConfig, position: Vector) {
super({ pos: position });
this._state = config.state;
this._config = config;
this._pulsing = config.state === 'working';

const circle = new Circle({
radius: AGENT_RADIUS,
color: Color.fromHex(config.color),
});

const label = new Text({
text: config.role,
color: Color.fromHex('#111111'),
font: new Font({
size: 9,
bold: true,
family: 'monospace',
textAlign: TextAlign.Center,
baseAlign: BaseAlign.Middle,
}),
});

const group = new GraphicsGroup({
useAnchor: false,
members: [
{ graphic: circle, offset: vec(0, 0) },
{ graphic: label, offset: vec(AGENT_RADIUS, AGENT_RADIUS), useBounds: false },
],
});

this.graphics.use(group);
this.applyGraphics(config);
this.graphics.opacity = opacityForState(config.state);
}

/** Animate a transition to a new state with opacity fade and optional pulse. */
animateToState(state: AgentAnimationState): void {
this._state = state;
this._pulsing = state === 'working';
this.applyGraphics({ ...this._config, state });
if (this._pulsing) {
this._elapsed = 0;
} else {
Expand All @@ -96,4 +77,25 @@ export class StationAgentActor extends Actor {
const t = Math.sin((this._elapsed * PULSE_FREQUENCY * Math.PI * 2) / 1000);
this.graphics.opacity = AGENT_PULSE_MIN + ((AGENT_PULSE_MAX - AGENT_PULSE_MIN) * (t + 1)) / 2;
}

/** Builds a GraphicsGroup with the sprite animation and an accent bar, and applies it. */
private applyGraphics(config: StationAgentActorConfig): void {
const animation = getAnimation('subagent', config.state);

const accentBar = new Rectangle({
width: SPRITE_SIZE,
height: ACCENT_BAR_HEIGHT,
color: Color.fromHex(config.color),
});

const group = new GraphicsGroup({
useAnchor: false,
members: [
{ graphic: animation, offset: vec(0, 0) },
{ graphic: accentBar, offset: vec(0, SPRITE_SIZE) },
],
});

this.graphics.use(group);
}
}
Loading