feat: add kugetsu session manager skill
- skills/kugetsu/SKILL.md: Agent skill documentation following agentskills.io spec - skills/kugetsu/scripts/kugetsu: Shell wrapper for opencode session management - Commands: start, list [--all], resume, stop, help - State tracking: used → idle (graceful) or left (interrupted) - Auto-fill message on resume - Confirmation prompt when resuming used session - skills/kugetsu/scripts/kugetsu-install.sh: Installation script for users Implements Phase 1 of issue #11 - basic session management layer for remote agent control without Hermes dependency.
This commit is contained in:
330
skills/kugetsu/scripts/kugetsu
Executable file
330
skills/kugetsu/scripts/kugetsu
Executable file
@@ -0,0 +1,330 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}"
|
||||
SESSIONS_DIR="$KUGETSU_DIR/sessions"
|
||||
BIN_DIR="$KUGETSU_DIR/bin"
|
||||
|
||||
usage() {
|
||||
cat << 'EOF'
|
||||
kugetsu - OpenCode Session Manager
|
||||
|
||||
Usage:
|
||||
kugetsu start <session_id> <message> Start a new session
|
||||
kugetsu list [--all] List sessions (default: left only)
|
||||
kugetsu resume <session_id> [message] Resume a session
|
||||
kugetsu stop <session_id> Stop a session gracefully
|
||||
kugetsu help Show this help
|
||||
|
||||
States:
|
||||
used - Session is active (process running)
|
||||
idle - Session ended gracefully (not resumable)
|
||||
left - Session interrupted/crashed (resumable)
|
||||
invalid - Session data missing/corrupt
|
||||
|
||||
Examples:
|
||||
kugetsu start mytask "fix bug #1"
|
||||
kugetsu list
|
||||
kugetsu list --all
|
||||
kugetsu resume mytask
|
||||
kugetsu resume mytask "continue working"
|
||||
kugetsu stop mytask
|
||||
EOF
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$SESSIONS_DIR" "$BIN_DIR"
|
||||
}
|
||||
|
||||
get_session_dir() {
|
||||
local session_id="$1"
|
||||
echo "$SESSIONS_DIR/$session_id"
|
||||
}
|
||||
|
||||
get_state() {
|
||||
local session_dir="$1"
|
||||
if [ -f "$session_dir/state" ]; then
|
||||
cat "$session_dir/state"
|
||||
else
|
||||
echo "invalid"
|
||||
fi
|
||||
}
|
||||
|
||||
set_state() {
|
||||
local session_dir="$1"
|
||||
local state="$2"
|
||||
echo "$state" > "$session_dir/state"
|
||||
}
|
||||
|
||||
is_process_running() {
|
||||
local pid="$1"
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
return 0
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_and_update_state() {
|
||||
local session_dir="$1"
|
||||
local state=$(get_state "$session_dir")
|
||||
|
||||
if [ "$state" = "used" ]; then
|
||||
local pid_file="$session_dir/pid"
|
||||
if [ -f "$pid_file" ]; then
|
||||
local pid=$(cat "$pid_file")
|
||||
if ! is_process_running "$pid"; then
|
||||
set_state "$session_dir" "left"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
set_state "$session_dir" "left"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_start() {
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Error: start requires <session_id> and <message>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local session_id="$1"
|
||||
local message="$2"
|
||||
local session_dir=$(get_session_dir "$session_id")
|
||||
|
||||
ensure_dirs
|
||||
|
||||
if [ -d "$session_dir" ]; then
|
||||
local state=$(get_state "$session_dir")
|
||||
check_and_update_state "$session_dir"
|
||||
state=$(get_state "$session_dir")
|
||||
|
||||
if [ "$state" = "used" ]; then
|
||||
echo "Error: session '$session_id' is already in use (state=used)" >&2
|
||||
echo "Use 'kugetsu list' to see all sessions, or 'kugetsu resume $session_id' to resume" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$state" = "left" ]; then
|
||||
echo "Warning: session '$session_id' was left interrupted" >&2
|
||||
echo "Resuming instead of starting new..." >&2
|
||||
cmd_resume "$session_id" "$message"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$session_dir"
|
||||
set_state "$session_dir" "used"
|
||||
echo "$$" > "$session_dir/pid"
|
||||
echo "$message" > "$session_dir/message"
|
||||
|
||||
echo "Starting session '$session_id'..."
|
||||
opencode run --session "$session_id" "$message"
|
||||
local exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
set_state "$session_dir" "idle"
|
||||
else
|
||||
set_state "$session_dir" "left"
|
||||
fi
|
||||
|
||||
rm -f "$session_dir/pid"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local show_all=false
|
||||
if [ $# -ge 1 ] && [ "$1" = "--all" ]; then
|
||||
show_all=true
|
||||
fi
|
||||
|
||||
ensure_dirs
|
||||
|
||||
printf "%-20s %-10s %-s\n" "SESSION_ID" "STATE" "LAST_MESSAGE"
|
||||
printf "%-20s %-10s %-s\n" "──────────" "─────" "───────────"
|
||||
|
||||
for session_dir in "$SESSIONS_DIR"/*; do
|
||||
if [ -d "$session_dir" ]; then
|
||||
local session_id=$(basename "$session_dir")
|
||||
check_and_update_state "$session_dir"
|
||||
local state=$(get_state "$session_dir")
|
||||
|
||||
if [ "$show_all" = false ] && [ "$state" != "left" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
local message=""
|
||||
if [ -f "$session_dir/message" ]; then
|
||||
message=$(cat "$session_dir/message")
|
||||
if [ ${#message} -gt 40 ]; then
|
||||
message="${message:0:37}..."
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "%-20s %-10s %-s\n" "$session_id" "$state" "$message"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_resume() {
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Error: resume requires <session_id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local session_id="$1"
|
||||
local message=""
|
||||
|
||||
if [ $# -ge 2 ]; then
|
||||
message="$2"
|
||||
fi
|
||||
|
||||
local session_dir=$(get_session_dir "$session_id")
|
||||
|
||||
if [ ! -d "$session_dir" ]; then
|
||||
echo "Error: session '$session_id' not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_and_update_state "$session_dir"
|
||||
local state=$(get_state "$session_dir")
|
||||
|
||||
if [ "$state" = "used" ]; then
|
||||
echo "Warning: session '$session_id' is marked as used" >&2
|
||||
local pid=""
|
||||
if [ -f "$session_dir/pid" ]; then
|
||||
pid=$(cat "$session_dir/pid")
|
||||
fi
|
||||
if [ -n "$pid" ] && is_process_running "$pid"; then
|
||||
echo "Error: process $pid is still running for this session" >&2
|
||||
echo "Use 'kugetsu stop $session_id' first, or 'kugetsu resume $session_id' to force resume anyway" >&2
|
||||
exit 1
|
||||
else
|
||||
set_state "$session_dir" "left"
|
||||
state="left"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$state" = "idle" ]; then
|
||||
echo "Error: session '$session_id' ended gracefully (state=idle)" >&2
|
||||
echo "This session cannot be resumed. Start a new session instead." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$state" = "invalid" ]; then
|
||||
echo "Error: session '$session_id' is invalid (state=invalid)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$message" ]; then
|
||||
if [ -f "$session_dir/message" ]; then
|
||||
message=$(cat "$session_dir/message")
|
||||
echo "Auto-filled message: $message"
|
||||
else
|
||||
echo "Error: no message stored for session '$session_id'" >&2
|
||||
echo "Provide a message as second argument: kugetsu resume $session_id <message>" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
set_state "$session_dir" "used"
|
||||
echo "$$" > "$session_dir/pid"
|
||||
echo "$message" > "$session_dir/message"
|
||||
|
||||
echo "Resuming session '$session_id'..."
|
||||
opencode run --continue --session "$session_id" "$message"
|
||||
local exit_code=$?
|
||||
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
set_state "$session_dir" "idle"
|
||||
else
|
||||
set_state "$session_dir" "left"
|
||||
fi
|
||||
|
||||
rm -f "$session_dir/pid"
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Error: stop requires <session_id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local session_id="$1"
|
||||
local session_dir=$(get_session_dir "$session_id")
|
||||
|
||||
if [ ! -d "$session_dir" ]; then
|
||||
echo "Error: session '$session_id' not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local state=$(get_state "$session_dir")
|
||||
|
||||
if [ "$state" != "used" ]; then
|
||||
echo "Error: session '$session_id' is not in use (state=$state)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local pid=""
|
||||
if [ -f "$session_dir/pid" ]; then
|
||||
pid=$(cat "$session_dir/pid")
|
||||
fi
|
||||
|
||||
if [ -n "$pid" ] && is_process_running "$pid"; then
|
||||
echo "Sending SIGTERM to process $pid..."
|
||||
kill -TERM "$pid" 2>/dev/null || true
|
||||
|
||||
local count=0
|
||||
while is_process_running "$pid" && [ $count -lt 10 ]; do
|
||||
sleep 0.5
|
||||
count=$((count + 1))
|
||||
done
|
||||
|
||||
if is_process_running "$pid"; then
|
||||
echo "Process still running, sending SIGKILL..." >&2
|
||||
kill -KILL "$pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
set_state "$session_dir" "idle"
|
||||
rm -f "$session_dir/pid"
|
||||
|
||||
echo "Session '$session_id' stopped"
|
||||
}
|
||||
|
||||
main() {
|
||||
if [ $# -eq 0 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local command="$1"
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
help|--help|-h)
|
||||
usage
|
||||
;;
|
||||
start)
|
||||
cmd_start "$@"
|
||||
;;
|
||||
list)
|
||||
cmd_list "$@"
|
||||
;;
|
||||
resume)
|
||||
cmd_resume "$@"
|
||||
;;
|
||||
stop)
|
||||
cmd_stop "$@"
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown command '$command'" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
53
skills/kugetsu/scripts/kugetsu-install.sh
Executable file
53
skills/kugetsu/scripts/kugetsu-install.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
KUGETSU_DIR="${KUGETSU_DIR:-$HOME/.kugetsu}"
|
||||
BIN_DIR="$KUGETSU_DIR/bin"
|
||||
|
||||
echo "Installing kugetsu to $KUGETSU_DIR..."
|
||||
|
||||
mkdir -p "$BIN_DIR"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cp "$SCRIPT_DIR/kugetsu" "$BIN_DIR/kugetsu"
|
||||
chmod +x "$BIN_DIR/kugetsu"
|
||||
|
||||
add_to_shell() {
|
||||
local rc_file="$1"
|
||||
local export_line="export PATH=\"\$HOME/.kugetsu/bin:\$PATH\""
|
||||
|
||||
if [ -f "$rc_file" ]; then
|
||||
if grep -q "$export_line" "$rc_file" 2>/dev/null; then
|
||||
echo "$rc_file already has kugetsu in PATH"
|
||||
else
|
||||
echo "" >> "$rc_file"
|
||||
echo "# kugetsu - opencode session manager" >> "$rc_file"
|
||||
echo "$export_line" >> "$rc_file"
|
||||
echo "Added to $rc_file"
|
||||
fi
|
||||
else
|
||||
echo "" >> "$rc_file"
|
||||
echo "# kugetsu - opencode session manager" >> "$rc_file"
|
||||
echo "$export_line" >> "$rc_file"
|
||||
echo "Created $rc_file with kugetsu PATH"
|
||||
fi
|
||||
}
|
||||
|
||||
add_to_shell "$HOME/.bashrc"
|
||||
add_to_shell "$HOME/.zshrc"
|
||||
|
||||
echo ""
|
||||
echo "Installation complete!"
|
||||
echo ""
|
||||
echo "Run this to start using kugetsu immediately:"
|
||||
echo " export PATH=\"\$HOME/.kugetsu/bin:\$PATH\""
|
||||
echo ""
|
||||
echo "Or start a new shell."
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " kugetsu start <session_id> <message> Start a new session"
|
||||
echo " kugetsu list List sessions"
|
||||
echo " kugetsu resume <session_id> [msg] Resume a session"
|
||||
echo " kugetsu stop <session_id> Stop a session"
|
||||
echo " kugetsu help Show help"
|
||||
Reference in New Issue
Block a user