feat(infra): grupo claude-fleet — FleetView TUI + orquestacion de Claudes
Sistema FleetView para centralizar la flota de procesos Claude Code vivos en una sola ventana kitty + tmux (socket aislado -L fleet) con un panel TUI: - list_claude_fleet (+ tipo claude_fleet): escanea ~/.claude/sessions + goals + runtime, valida procesos vivos (anti-PID-reciclado), join por sessionId. - list_resumable_claudes (+ tipo resumable_claude): sesiones cerradas reanudables. - wrappers tmux: tmux_new_claude_window (con --resume), tmux_swap_window_into_console (preserva ancho del sidebar), tmux_map_claude_panes. - launch_kittyclaude: comando entrypoint; instala atajos alt+flechas/enter/n/0/k/r, mouse on, remain-on-exit off; fija el ancho del sidebar con hooks. - docs/capabilities/claude-fleet.md + entrada en el INDEX. Incluye ademas funciones datascience en progreso (excel/duckdb/postgres) y ajustes varios de docs e infra de otra sesion, agrupados aqui para no perderlos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
//go:build !windows
|
||||
|
||||
package infra
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResumableClaude describes a CLOSED Claude Code session that still has a saved
|
||||
// goal and can therefore be reopened with `claude --resume <SessionID>`. The
|
||||
// fleetview TUI consumes these for its "resume" picker.
|
||||
type ResumableClaude struct {
|
||||
SessionID string `json:"session_id"`
|
||||
Goal string `json:"goal"` // from goals/<id>.json .goal ("" if absent)
|
||||
Emojis string `json:"emojis"` // from goals/<id>.json .emojis ("" if absent)
|
||||
Name string `json:"name"` // from goals/<id>.json .rename ("" if absent)
|
||||
LastActive int64 `json:"last_active"` // mtime of the goal.json file, epoch seconds
|
||||
}
|
||||
|
||||
// maxResumable caps the number of resumable sessions returned, keeping only the
|
||||
// most recently touched ones.
|
||||
const maxResumable = 40
|
||||
|
||||
// ListResumableClaudes scans the current user's ~/.claude directory and returns
|
||||
// the closed sessions that can be reopened with `claude --resume`. It is a thin
|
||||
// wrapper over ListResumableClaudesFrom resolving the home directory.
|
||||
func ListResumableClaudes() ([]ResumableClaude, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve home dir: %w", err)
|
||||
}
|
||||
return ListResumableClaudesFrom(filepath.Join(home, ".claude"))
|
||||
}
|
||||
|
||||
// ListResumableClaudesFrom scans claudeDir (e.g. ~/.claude) and returns the
|
||||
// sessions that have a goal (goals/<id>.json) whose process is NOT alive — i.e.
|
||||
// candidates to reopen with `claude --resume <SessionID>`.
|
||||
//
|
||||
// A session is considered live (and thus excluded) when sessions/<PID>.json
|
||||
// reports a PID whose /proc starttime matches the recorded procStart, using the
|
||||
// exact same liveness criterion as ListClaudeFleetFrom (procIsAlive). Goals
|
||||
// without a non-empty goal string are skipped. Results are ordered by
|
||||
// LastActive descending and capped at maxResumable.
|
||||
func ListResumableClaudesFrom(claudeDir string) ([]ResumableClaude, error) {
|
||||
sessionsDir := filepath.Join(claudeDir, "sessions")
|
||||
goalsDir := filepath.Join(claudeDir, "goals")
|
||||
|
||||
// 1. Build the set of LIVE sessionIds from sessions/*.json.
|
||||
live := liveSessionIDs(sessionsDir)
|
||||
|
||||
// 2. Scan goals/*.json.
|
||||
entries, err := os.ReadDir(goalsDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return []ResumableClaude{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read goals dir %q: %w", goalsDir, err)
|
||||
}
|
||||
|
||||
out := make([]ResumableClaude, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if entry.IsDir() || !strings.HasSuffix(name, ".json") {
|
||||
continue
|
||||
}
|
||||
sessionID := strings.TrimSuffix(name, ".json")
|
||||
if sessionID == "" {
|
||||
continue
|
||||
}
|
||||
// Skip sessions that are alive (already in the fleet, not resumable).
|
||||
if live[sessionID] {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(goalsDir, name)
|
||||
raw, readErr := os.ReadFile(path)
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
var g goalFile
|
||||
if json.Unmarshal(raw, &g) != nil {
|
||||
continue
|
||||
}
|
||||
// No real work to resume without a goal.
|
||||
if strings.TrimSpace(g.Goal) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
info, statErr := os.Stat(path)
|
||||
if statErr != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, ResumableClaude{
|
||||
SessionID: sessionID,
|
||||
Goal: g.Goal,
|
||||
Emojis: g.Emojis,
|
||||
Name: g.Rename,
|
||||
LastActive: info.ModTime().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Order by LastActive descending (most recent first).
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
return out[i].LastActive > out[j].LastActive
|
||||
})
|
||||
|
||||
// 4. Cap at maxResumable.
|
||||
if len(out) > maxResumable {
|
||||
out = out[:maxResumable]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// liveSessionIDs scans sessionsDir (sessions/*.json) and returns the set of
|
||||
// sessionIds whose process is currently alive, applying the same anti-PID-
|
||||
// recycling check as ListClaudeFleetFrom (procIsAlive matches /proc starttime
|
||||
// against the recorded procStart). Missing or unparseable files are ignored.
|
||||
func liveSessionIDs(sessionsDir string) map[string]bool {
|
||||
live := make(map[string]bool)
|
||||
entries, err := os.ReadDir(sessionsDir)
|
||||
if err != nil {
|
||||
return live
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
raw, readErr := os.ReadFile(filepath.Join(sessionsDir, entry.Name()))
|
||||
if readErr != nil {
|
||||
continue
|
||||
}
|
||||
var sess sessionFile
|
||||
if json.Unmarshal(raw, &sess) != nil {
|
||||
continue
|
||||
}
|
||||
if sess.PID == 0 || sess.SessionID == "" {
|
||||
continue
|
||||
}
|
||||
if procIsAlive(sess.PID, sess.ProcStart) {
|
||||
live[sess.SessionID] = true
|
||||
}
|
||||
}
|
||||
return live
|
||||
}
|
||||
Reference in New Issue
Block a user