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:
shokollm
2026-03-29 10:50:14 +00:00
parent aba9d5321f
commit 7edb54cd3f
3 changed files with 508 additions and 0 deletions

125
skills/kugetsu/SKILL.md Normal file
View File

@@ -0,0 +1,125 @@
---
name: kugetsu
description: Session manager for opencode CLI. Use when managing long-running opencode sessions, resuming interrupted work, or tracking session state across disconnects. Features state tracking (used/idle/left), auto-fill last message on resume, and safe locking via confirmation prompts.
license: MIT
compatibility: Requires opencode CLI, bash, and filesystem access for session state.
metadata:
author: shoko
version: "1.0"
---
# kugetsu - OpenCode Session Manager
Manages opencode CLI sessions with state tracking and safe resume.
## Installation
### For Human Users
Run once on a new host:
```bash
. skills/kugetsu/scripts/kugetsu-install.sh
```
### For Agents (Self-Install)
Copy the script to your PATH:
```bash
cp skills/kugetsu/scripts/kugetsu ~/.local/bin/kugetsu
chmod +x ~/.local/bin/kugetsu
```
Or source directly when needed:
```bash
. skills/kugetsu/scripts/kugetsu
```
## Session State
| State | Meaning | Resumable? |
|-------|---------|------------|
| `used` | Session is active (process running) | Yes (with confirmation) |
| `idle` | Session ended gracefully | No |
| `left` | Session interrupted/crashed | Yes |
| `invalid` | Session data missing/corrupt | No |
## Session Directory
Sessions are stored in `~/.kugetsu/sessions/<session_id>/`:
- `state` - current state (used/idle/left/invalid)
- `message` - last user message (for auto-fill)
- `pid` - active process PID (when used)
## Commands
### kugetsu start `<session_id>` `<message>`
Start a new session:
```bash
kugetsu start mytask "fix bug #1"
```
- Creates session directory
- Sets state to `used`
- Stores PID and message
- Runs: `opencode run --session <session_id> <message>`
### kugetsu list [--all]
List sessions:
```bash
kugetsu list # Shows only `left` (resumable)
kugetsu list --all # Shows all states
```
### kugetsu resume `<session_id>` [message]
Resume an interrupted session:
```bash
kugetsu resume mytask # Auto-fills last message
kugetsu resume mytask "continue" # Uses provided message
```
- If state is `used`: prompts for confirmation (someone else might be using)
- If state is `idle`: errors (not resumable)
- If state is `left`: proceeds with message
### kugetsu stop `<session_id>`
Stop a session gracefully:
```bash
kugetsu stop mytask
```
- Sends SIGTERM to process
- Sets state to `idle`
### kugetsu help
Show usage help.
## State Transitions
```
start ──────────────► used ──────► idle (stop/SIGTERM)
└──────► left (kill/SIGINT/crash)
```
## Example Workflow
```bash
# Start a long-running task
kugetsu start issue42 "implement feature X"
# ... time passes, connection drops ...
# Check what sessions are resumable
kugetsu list
# Resume with auto-filled message
kugetsu resume issue42
# Later, when done
kugetsu stop issue42
```
## Without kugetsu
If kugetsu is not installed, use opencode directly:
```bash
opencode run --session mytask "task description"
opencode run --continue --session mytask "continue"
opencode session list
```
Tradeoff: No state tracking, no auto-fill, no filtered list, no confirmation prompts.

330
skills/kugetsu/scripts/kugetsu Executable file
View 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 "$@"

View 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"