@atercates/claude-deck 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- package/tsconfig.json +33 -0
package/lib/banner.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ClaudeDeck session initialization
|
|
2
|
+
// Writes an init script that shows the banner, configures tmux, then runs the agent
|
|
3
|
+
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate an init script that shows the ClaudeDeck banner and configures tmux
|
|
10
|
+
*/
|
|
11
|
+
function generateInitScript(agentCommand: string): string {
|
|
12
|
+
return `#!/bin/bash
|
|
13
|
+
# ClaudeDeck Session Init Script
|
|
14
|
+
# Auto-generated - do not edit manually
|
|
15
|
+
|
|
16
|
+
# ANSI Colors (purple theme)
|
|
17
|
+
C_RESET=$'\\033[0m'
|
|
18
|
+
C_PURPLE=$'\\033[38;5;141m'
|
|
19
|
+
C_PURPLE2=$'\\033[38;5;177m'
|
|
20
|
+
C_PINK=$'\\033[38;5;213m'
|
|
21
|
+
C_MUTED=$'\\033[38;5;245m'
|
|
22
|
+
C_BOLD=$'\\033[1m'
|
|
23
|
+
|
|
24
|
+
# Configure tmux status bar
|
|
25
|
+
tmux set-option status-style 'bg=#1e1e2e,fg=#cdd6f4' 2>/dev/null
|
|
26
|
+
tmux set-option status-left '#[fg=#cba6f7,bold] ClaudeDeck #[fg=#6c7086]| ' 2>/dev/null
|
|
27
|
+
tmux set-option status-left-length 20 2>/dev/null
|
|
28
|
+
tmux set-option status-right '#[fg=#6c7086]| #[fg=#89b4fa]#S #[fg=#6c7086]| #[fg=#a6adc8]%H:%M ' 2>/dev/null
|
|
29
|
+
tmux set-option status-right-length 40 2>/dev/null
|
|
30
|
+
tmux set-option status-position bottom 2>/dev/null
|
|
31
|
+
|
|
32
|
+
# Clear and show banner
|
|
33
|
+
clear
|
|
34
|
+
|
|
35
|
+
printf "\\n"
|
|
36
|
+
printf "\${C_PURPLE} _ _ ___ ____ \${C_RESET}\\n"
|
|
37
|
+
printf "\${C_PURPLE} / \\\\ __ _ ___ _ __ | |_ / _ \\\\/ ___| \${C_RESET}\\n"
|
|
38
|
+
printf "\${C_PURPLE2} / _ \\\\ / _\\\` |/ _ \\\\ '_ \\\\| __| | | \\\\___ \\\\ \${C_RESET}\\n"
|
|
39
|
+
printf "\${C_PURPLE2} / ___ \\\\ (_| | __/ | | | |_| |_| |___) |\${C_RESET}\\n"
|
|
40
|
+
printf "\${C_PINK} /_/ \\\\_\\\\__, |\\\\___|_| |_|\\\\__|\\\\___/|____/ \${C_RESET}\\n"
|
|
41
|
+
printf "\${C_PINK} |___/ \${C_RESET}\\n"
|
|
42
|
+
printf "\\n"
|
|
43
|
+
printf "\${C_MUTED} AI Coding Session Manager\${C_RESET}\\n"
|
|
44
|
+
printf "\\n"
|
|
45
|
+
|
|
46
|
+
# Brief pause to show banner
|
|
47
|
+
sleep 0.8
|
|
48
|
+
|
|
49
|
+
# Start the agent
|
|
50
|
+
exec ${agentCommand}
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Write init script to temp file and return the command to run it
|
|
56
|
+
*/
|
|
57
|
+
export function wrapWithBanner(agentCommand: string): string {
|
|
58
|
+
const scriptContent = generateInitScript(agentCommand);
|
|
59
|
+
const scriptPath = path.join(os.tmpdir(), `claude-deck-init-${Date.now()}.sh`);
|
|
60
|
+
|
|
61
|
+
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
62
|
+
|
|
63
|
+
// Return command that runs the script
|
|
64
|
+
return `bash ${scriptPath}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns just the banner string (for display elsewhere)
|
|
69
|
+
*/
|
|
70
|
+
export function getBanner(): string {
|
|
71
|
+
return `
|
|
72
|
+
_ _ ___ ____
|
|
73
|
+
/ \\ __ _ ___ _ __ | |_ / _ \\/ ___|
|
|
74
|
+
/ _ \\ / _\` |/ _ \\ '_ \\| __| | | \\___ \\
|
|
75
|
+
/ ___ \\ (_| | __/ | | | |_| |_| |___) |
|
|
76
|
+
/_/ \\_\\__, |\\___|_| |_|\\__|\\___/|____/
|
|
77
|
+
|___/
|
|
78
|
+
|
|
79
|
+
AI Coding Session Manager
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { listSessions as sdkListSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import {
|
|
3
|
+
extractProjectDirectory,
|
|
4
|
+
getSessions,
|
|
5
|
+
getClaudeProjectNames,
|
|
6
|
+
type SessionInfo,
|
|
7
|
+
} from "./jsonl-reader";
|
|
8
|
+
|
|
9
|
+
export interface CachedProject {
|
|
10
|
+
name: string;
|
|
11
|
+
directory: string | null;
|
|
12
|
+
displayName: string;
|
|
13
|
+
sessionCount: number;
|
|
14
|
+
lastActivity: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function deriveDisplayName(directory: string | null, encoded: string): string {
|
|
18
|
+
if (directory) {
|
|
19
|
+
const parts = directory.split("/");
|
|
20
|
+
return parts[parts.length - 1] || directory;
|
|
21
|
+
}
|
|
22
|
+
const decoded = encoded.replace(/^-/, "/").replace(/-/g, "/");
|
|
23
|
+
const parts = decoded.split("/");
|
|
24
|
+
return parts[parts.length - 1] || decoded;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let projectsData: CachedProject[] | null = null;
|
|
28
|
+
let projectsBuilding: Promise<CachedProject[]> | null = null;
|
|
29
|
+
|
|
30
|
+
async function buildProjects(): Promise<CachedProject[]> {
|
|
31
|
+
const projectNames = getClaudeProjectNames();
|
|
32
|
+
|
|
33
|
+
const allSessions = await sdkListSessions();
|
|
34
|
+
const cwdToDir = new Map<string, string>();
|
|
35
|
+
for (const s of allSessions) {
|
|
36
|
+
if (s.cwd) {
|
|
37
|
+
const encoded = s.cwd.replace(/\//g, "-");
|
|
38
|
+
if (!cwdToDir.has(encoded)) cwdToDir.set(encoded, s.cwd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Promise.all(
|
|
43
|
+
projectNames.map(async (name) => {
|
|
44
|
+
const directory =
|
|
45
|
+
cwdToDir.get(name) || (await extractProjectDirectory(name));
|
|
46
|
+
const projectSessions = allSessions
|
|
47
|
+
.filter((s) => s.cwd === directory)
|
|
48
|
+
.sort((a, b) => b.lastModified - a.lastModified);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name,
|
|
52
|
+
directory,
|
|
53
|
+
displayName: deriveDisplayName(directory, name),
|
|
54
|
+
sessionCount: projectSessions.length,
|
|
55
|
+
lastActivity: projectSessions[0]
|
|
56
|
+
? new Date(projectSessions[0].lastModified).toISOString()
|
|
57
|
+
: null,
|
|
58
|
+
};
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getCachedProjects(): Promise<CachedProject[]> {
|
|
64
|
+
if (projectsData) return projectsData;
|
|
65
|
+
if (projectsBuilding) return projectsBuilding;
|
|
66
|
+
|
|
67
|
+
projectsBuilding = buildProjects();
|
|
68
|
+
projectsData = await projectsBuilding;
|
|
69
|
+
projectsBuilding = null;
|
|
70
|
+
return projectsData;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function getCachedSessions(
|
|
74
|
+
projectName: string
|
|
75
|
+
): Promise<SessionInfo[]> {
|
|
76
|
+
const { sessions } = await getSessions(projectName, 200, 0);
|
|
77
|
+
return sessions;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function invalidateProject(_projectName: string): void {
|
|
81
|
+
projectsData = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function invalidateAll(): void {
|
|
85
|
+
projectsData = null;
|
|
86
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import readline from "readline";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { listSessions as sdkListSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
+
|
|
7
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
8
|
+
|
|
9
|
+
export interface JsonlEntry {
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
timestamp?: string;
|
|
12
|
+
type?: string;
|
|
13
|
+
message?: {
|
|
14
|
+
role: "user" | "assistant";
|
|
15
|
+
content: string | ContentBlock[];
|
|
16
|
+
};
|
|
17
|
+
uuid?: string;
|
|
18
|
+
parentUuid?: string | null;
|
|
19
|
+
leafUuid?: string;
|
|
20
|
+
summary?: string;
|
|
21
|
+
customTitle?: string;
|
|
22
|
+
cwd?: string;
|
|
23
|
+
isApiErrorMessage?: boolean;
|
|
24
|
+
toolUseResult?: {
|
|
25
|
+
agentId?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ContentBlock {
|
|
30
|
+
type: string;
|
|
31
|
+
text?: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
input?: Record<string, unknown>;
|
|
35
|
+
tool_use_id?: string;
|
|
36
|
+
content?: string;
|
|
37
|
+
is_error?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SessionInfo {
|
|
41
|
+
sessionId: string;
|
|
42
|
+
summary: string;
|
|
43
|
+
lastActivity: string;
|
|
44
|
+
messageCount: number;
|
|
45
|
+
cwd: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SessionMessage {
|
|
49
|
+
sessionId: string;
|
|
50
|
+
timestamp: string;
|
|
51
|
+
role: "user" | "assistant";
|
|
52
|
+
content: ContentBlock[];
|
|
53
|
+
uuid?: string;
|
|
54
|
+
parentUuid?: string | null;
|
|
55
|
+
subagentTools?: AgentTool[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AgentTool {
|
|
59
|
+
toolId: string;
|
|
60
|
+
toolName: string;
|
|
61
|
+
toolInput: Record<string, unknown>;
|
|
62
|
+
toolResult?: string;
|
|
63
|
+
timestamp: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getClaudeProjectNames(): string[] {
|
|
67
|
+
try {
|
|
68
|
+
return fs
|
|
69
|
+
.readdirSync(CLAUDE_PROJECTS_DIR, { withFileTypes: true })
|
|
70
|
+
.filter((d) => d.isDirectory())
|
|
71
|
+
.map((d) => d.name);
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getProjectDir(projectName: string): string {
|
|
78
|
+
return path.join(CLAUDE_PROJECTS_DIR, projectName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getJsonlFiles(projectDir: string): string[] {
|
|
82
|
+
try {
|
|
83
|
+
return fs
|
|
84
|
+
.readdirSync(projectDir)
|
|
85
|
+
.filter((f) => f.endsWith(".jsonl") && !f.startsWith("agent-"))
|
|
86
|
+
.map((f) => path.join(projectDir, f));
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getAgentFiles(projectDir: string): string[] {
|
|
93
|
+
try {
|
|
94
|
+
return fs
|
|
95
|
+
.readdirSync(projectDir)
|
|
96
|
+
.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"))
|
|
97
|
+
.map((f) => path.join(projectDir, f));
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function readJsonlFile(filePath: string): Promise<JsonlEntry[]> {
|
|
104
|
+
const entries: JsonlEntry[] = [];
|
|
105
|
+
const stream = fs.createReadStream(filePath);
|
|
106
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
107
|
+
|
|
108
|
+
for await (const line of rl) {
|
|
109
|
+
if (!line.trim()) continue;
|
|
110
|
+
try {
|
|
111
|
+
entries.push(JSON.parse(line));
|
|
112
|
+
} catch {
|
|
113
|
+
// skip malformed lines
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function extractProjectDirectory(
|
|
120
|
+
projectName: string
|
|
121
|
+
): Promise<string | null> {
|
|
122
|
+
const projectDir = getProjectDir(projectName);
|
|
123
|
+
const files = getJsonlFiles(projectDir);
|
|
124
|
+
|
|
125
|
+
for (const file of files.slice(0, 3)) {
|
|
126
|
+
const stream = fs.createReadStream(file);
|
|
127
|
+
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
128
|
+
for await (const line of rl) {
|
|
129
|
+
if (!line.trim()) continue;
|
|
130
|
+
try {
|
|
131
|
+
const entry = JSON.parse(line);
|
|
132
|
+
if (entry.cwd) {
|
|
133
|
+
rl.close();
|
|
134
|
+
stream.destroy();
|
|
135
|
+
return entry.cwd;
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getSessions(
|
|
144
|
+
projectName: string,
|
|
145
|
+
limit = 20,
|
|
146
|
+
offset = 0
|
|
147
|
+
): Promise<{ sessions: SessionInfo[]; total: number }> {
|
|
148
|
+
const dir = await extractProjectDirectory(projectName);
|
|
149
|
+
const sdkSessions = await sdkListSessions(dir ? { dir } : undefined);
|
|
150
|
+
|
|
151
|
+
const sessions: SessionInfo[] = sdkSessions
|
|
152
|
+
.filter((s) => !s.summary?.startsWith('{ "'))
|
|
153
|
+
.map((s) => ({
|
|
154
|
+
sessionId: s.sessionId,
|
|
155
|
+
summary: s.customTitle || s.summary || "New Session",
|
|
156
|
+
lastActivity: new Date(s.lastModified).toISOString(),
|
|
157
|
+
messageCount: (s.fileSize ?? 0) > 500 ? 3 : 0,
|
|
158
|
+
cwd: s.cwd || dir || null,
|
|
159
|
+
}));
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
sessions: sessions.slice(offset, offset + limit),
|
|
163
|
+
total: sessions.length,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function getSessionMessages(
|
|
168
|
+
projectName: string,
|
|
169
|
+
sessionId: string,
|
|
170
|
+
limit = 100,
|
|
171
|
+
offset = 0
|
|
172
|
+
): Promise<{ messages: SessionMessage[]; total: number; hasMore: boolean }> {
|
|
173
|
+
const projectDir = getProjectDir(projectName);
|
|
174
|
+
const files = getJsonlFiles(projectDir);
|
|
175
|
+
const agentToolsMap = await loadAgentTools(projectDir);
|
|
176
|
+
const messages: SessionMessage[] = [];
|
|
177
|
+
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const entries = await readJsonlFile(file);
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (entry.sessionId !== sessionId) continue;
|
|
182
|
+
if (entry.isApiErrorMessage) continue;
|
|
183
|
+
if (!entry.message?.role) continue;
|
|
184
|
+
|
|
185
|
+
const content: ContentBlock[] = Array.isArray(entry.message.content)
|
|
186
|
+
? entry.message.content
|
|
187
|
+
: [{ type: "text", text: String(entry.message.content) }];
|
|
188
|
+
|
|
189
|
+
const msg: SessionMessage = {
|
|
190
|
+
sessionId: entry.sessionId,
|
|
191
|
+
timestamp: entry.timestamp || new Date(0).toISOString(),
|
|
192
|
+
role: entry.message.role,
|
|
193
|
+
content,
|
|
194
|
+
uuid: entry.uuid,
|
|
195
|
+
parentUuid: entry.parentUuid,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if (entry.toolUseResult?.agentId) {
|
|
199
|
+
const tools = agentToolsMap.get(entry.toolUseResult.agentId);
|
|
200
|
+
if (tools) {
|
|
201
|
+
msg.subagentTools = tools;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
messages.push(msg);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
messages.sort(
|
|
210
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
const total = messages.length;
|
|
214
|
+
const sliced = messages.slice(offset, offset + limit);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
messages: sliced,
|
|
218
|
+
total,
|
|
219
|
+
hasMore: offset + limit < total,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function loadAgentTools(
|
|
224
|
+
projectDir: string
|
|
225
|
+
): Promise<Map<string, AgentTool[]>> {
|
|
226
|
+
const agentFiles = getAgentFiles(projectDir);
|
|
227
|
+
const map = new Map<string, AgentTool[]>();
|
|
228
|
+
|
|
229
|
+
for (const file of agentFiles) {
|
|
230
|
+
const agentId = path.basename(file).replace(/^agent-|\.jsonl$/g, "");
|
|
231
|
+
const entries = await readJsonlFile(file);
|
|
232
|
+
const tools: AgentTool[] = [];
|
|
233
|
+
const toolUseMap = new Map<
|
|
234
|
+
string,
|
|
235
|
+
{ name: string; input: Record<string, unknown>; timestamp: string }
|
|
236
|
+
>();
|
|
237
|
+
|
|
238
|
+
for (const entry of entries) {
|
|
239
|
+
if (!entry.message?.content || !Array.isArray(entry.message.content))
|
|
240
|
+
continue;
|
|
241
|
+
|
|
242
|
+
for (const block of entry.message.content) {
|
|
243
|
+
if (block.type === "tool_use" && block.id && block.name) {
|
|
244
|
+
toolUseMap.set(block.id, {
|
|
245
|
+
name: block.name,
|
|
246
|
+
input: block.input || {},
|
|
247
|
+
timestamp: entry.timestamp || "",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
251
|
+
const use = toolUseMap.get(block.tool_use_id);
|
|
252
|
+
if (use) {
|
|
253
|
+
tools.push({
|
|
254
|
+
toolId: block.tool_use_id,
|
|
255
|
+
toolName: use.name,
|
|
256
|
+
toolInput: use.input,
|
|
257
|
+
toolResult: block.content || undefined,
|
|
258
|
+
timestamp: use.timestamp,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (tools.length > 0) {
|
|
266
|
+
map.set(agentId, tools);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return map;
|
|
271
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from "child_process";
|
|
2
|
+
import { WebSocket } from "ws";
|
|
3
|
+
import { StreamParser } from "./stream-parser";
|
|
4
|
+
import { queries, type Session } from "../db";
|
|
5
|
+
import type { ClaudeSessionOptions, ClientEvent } from "./types";
|
|
6
|
+
|
|
7
|
+
interface ManagedSession {
|
|
8
|
+
process: ChildProcess | null;
|
|
9
|
+
parser: StreamParser;
|
|
10
|
+
clients: Set<WebSocket>;
|
|
11
|
+
status: "idle" | "running" | "waiting" | "error";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ClaudeProcessManager {
|
|
15
|
+
private sessions: Map<string, ManagedSession> = new Map();
|
|
16
|
+
|
|
17
|
+
// Register a WebSocket client for a session
|
|
18
|
+
registerClient(sessionId: string, ws: WebSocket): void {
|
|
19
|
+
let session = this.sessions.get(sessionId);
|
|
20
|
+
|
|
21
|
+
if (!session) {
|
|
22
|
+
session = {
|
|
23
|
+
process: null,
|
|
24
|
+
parser: new StreamParser(sessionId),
|
|
25
|
+
clients: new Set(),
|
|
26
|
+
status: "idle",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Set up parser event handlers
|
|
30
|
+
session.parser.on("event", (event: ClientEvent) => {
|
|
31
|
+
this.broadcastToSession(sessionId, event);
|
|
32
|
+
this.handleEvent(sessionId, event);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
session.parser.on("parse_error", (error) => {
|
|
36
|
+
this.broadcastToSession(sessionId, {
|
|
37
|
+
type: "error",
|
|
38
|
+
sessionId,
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
data: { error: `Parse error: ${error.error}` },
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
this.sessions.set(sessionId, session);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
session.clients.add(ws);
|
|
48
|
+
|
|
49
|
+
// Send current status
|
|
50
|
+
ws.send(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
type: "status",
|
|
53
|
+
sessionId,
|
|
54
|
+
timestamp: new Date().toISOString(),
|
|
55
|
+
data: { status: session.status },
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Unregister a WebSocket client
|
|
61
|
+
unregisterClient(sessionId: string, ws: WebSocket): void {
|
|
62
|
+
const session = this.sessions.get(sessionId);
|
|
63
|
+
if (session) {
|
|
64
|
+
session.clients.delete(ws);
|
|
65
|
+
|
|
66
|
+
// Clean up if no clients remain and process not running
|
|
67
|
+
if (session.clients.size === 0 && !session.process) {
|
|
68
|
+
this.sessions.delete(sessionId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Send a prompt to Claude
|
|
74
|
+
async sendPrompt(
|
|
75
|
+
sessionId: string,
|
|
76
|
+
prompt: string,
|
|
77
|
+
options: ClaudeSessionOptions = {}
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
const session = this.sessions.get(sessionId);
|
|
80
|
+
if (!session) {
|
|
81
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (session.process) {
|
|
85
|
+
throw new Error(`Session ${sessionId} already has a running process`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Build Claude CLI command
|
|
89
|
+
const args = ["-p", "--output-format", "stream-json", "--verbose"];
|
|
90
|
+
|
|
91
|
+
// Add model if specified
|
|
92
|
+
if (options.model) {
|
|
93
|
+
args.push("--model", options.model);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Handle session continuity
|
|
97
|
+
const dbSession = await queries.getSession(sessionId);
|
|
98
|
+
|
|
99
|
+
if (dbSession?.claude_session_id) {
|
|
100
|
+
// Resume existing Claude session
|
|
101
|
+
args.push("--resume", dbSession.claude_session_id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add system prompt if specified
|
|
105
|
+
if (options.systemPrompt) {
|
|
106
|
+
args.push("--system-prompt", options.systemPrompt);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Add the prompt
|
|
110
|
+
args.push(prompt);
|
|
111
|
+
|
|
112
|
+
// Spawn Claude process
|
|
113
|
+
const cwd =
|
|
114
|
+
options.workingDirectory ||
|
|
115
|
+
dbSession?.working_directory?.replace("~", process.env.HOME || "") ||
|
|
116
|
+
process.env.HOME ||
|
|
117
|
+
"/";
|
|
118
|
+
|
|
119
|
+
console.log(`Spawning Claude for session ${sessionId}:`, args.join(" "));
|
|
120
|
+
console.log(`CWD: ${cwd}`);
|
|
121
|
+
|
|
122
|
+
// Reset parser for new conversation turn
|
|
123
|
+
session.parser = new StreamParser(sessionId);
|
|
124
|
+
session.parser.on("event", (event: ClientEvent) => {
|
|
125
|
+
console.log(
|
|
126
|
+
`Parser event [${sessionId}]:`,
|
|
127
|
+
event.type,
|
|
128
|
+
JSON.stringify(event.data).substring(0, 100)
|
|
129
|
+
);
|
|
130
|
+
this.broadcastToSession(sessionId, event);
|
|
131
|
+
this.handleEvent(sessionId, event);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Find claude binary path
|
|
135
|
+
const claudePath =
|
|
136
|
+
process.env.HOME + "/.nvm/versions/node/v20.19.0/bin/claude";
|
|
137
|
+
|
|
138
|
+
const claudeProcess = spawn(claudePath, args, {
|
|
139
|
+
cwd,
|
|
140
|
+
env: {
|
|
141
|
+
...process.env,
|
|
142
|
+
PATH: `/usr/local/bin:/opt/homebrew/bin:${process.env.PATH}`,
|
|
143
|
+
},
|
|
144
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
session.process = claudeProcess;
|
|
148
|
+
session.status = "running";
|
|
149
|
+
this.updateDbStatus(sessionId, "running");
|
|
150
|
+
|
|
151
|
+
this.broadcastToSession(sessionId, {
|
|
152
|
+
type: "status",
|
|
153
|
+
sessionId,
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
data: { status: "running" },
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Handle stdout (stream-json output)
|
|
159
|
+
claudeProcess.stdout?.on("data", (data: Buffer) => {
|
|
160
|
+
const text = data.toString();
|
|
161
|
+
console.log(`Claude stdout [${sessionId}]:`, text.substring(0, 200));
|
|
162
|
+
session.parser.write(text);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Handle stderr (errors and other output)
|
|
166
|
+
claudeProcess.stderr?.on("data", (data: Buffer) => {
|
|
167
|
+
const text = data.toString();
|
|
168
|
+
console.error(`Claude stderr [${sessionId}]:`, text);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
claudeProcess.on("error", (err) => {
|
|
172
|
+
console.error(`Claude spawn error [${sessionId}]:`, err);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Handle process exit
|
|
176
|
+
claudeProcess.on("close", (code) => {
|
|
177
|
+
console.log(
|
|
178
|
+
`Claude process exited for session ${sessionId} with code ${code}`
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
session.parser.end();
|
|
182
|
+
session.process = null;
|
|
183
|
+
session.status = code === 0 ? "idle" : "error";
|
|
184
|
+
|
|
185
|
+
this.updateDbStatus(sessionId, session.status);
|
|
186
|
+
|
|
187
|
+
this.broadcastToSession(sessionId, {
|
|
188
|
+
type: "status",
|
|
189
|
+
sessionId,
|
|
190
|
+
timestamp: new Date().toISOString(),
|
|
191
|
+
data: { status: session.status, exitCode: code || 0 },
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
claudeProcess.on("error", (err) => {
|
|
196
|
+
console.error(`Claude process error for session ${sessionId}:`, err);
|
|
197
|
+
|
|
198
|
+
session.process = null;
|
|
199
|
+
session.status = "error";
|
|
200
|
+
|
|
201
|
+
this.updateDbStatus(sessionId, "error");
|
|
202
|
+
|
|
203
|
+
this.broadcastToSession(sessionId, {
|
|
204
|
+
type: "error",
|
|
205
|
+
sessionId,
|
|
206
|
+
timestamp: new Date().toISOString(),
|
|
207
|
+
data: { error: err.message },
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Cancel a running Claude process
|
|
213
|
+
cancelSession(sessionId: string): void {
|
|
214
|
+
const session = this.sessions.get(sessionId);
|
|
215
|
+
if (session?.process) {
|
|
216
|
+
session.process.kill("SIGTERM");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Get session status
|
|
221
|
+
getSessionStatus(
|
|
222
|
+
sessionId: string
|
|
223
|
+
): "idle" | "running" | "waiting" | "error" | null {
|
|
224
|
+
return this.sessions.get(sessionId)?.status ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Broadcast event to all clients of a session
|
|
228
|
+
private broadcastToSession(sessionId: string, event: ClientEvent): void {
|
|
229
|
+
const session = this.sessions.get(sessionId);
|
|
230
|
+
if (!session) {
|
|
231
|
+
console.log(`No session found for broadcast: ${sessionId}`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(
|
|
236
|
+
`Broadcasting to ${session.clients.size} clients for session ${sessionId}`
|
|
237
|
+
);
|
|
238
|
+
const message = JSON.stringify(event);
|
|
239
|
+
for (const client of session.clients) {
|
|
240
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
241
|
+
client.send(message);
|
|
242
|
+
console.log(`Sent message to client`);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(`Client not open, state: ${client.readyState}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Handle events for persistence
|
|
250
|
+
private handleEvent(sessionId: string, event: ClientEvent): void {
|
|
251
|
+
switch (event.type) {
|
|
252
|
+
case "init": {
|
|
253
|
+
// Store Claude's session ID for future --resume
|
|
254
|
+
const claudeSessionId = event.data.claudeSessionId;
|
|
255
|
+
if (claudeSessionId) {
|
|
256
|
+
queries.updateSessionClaudeId(claudeSessionId, sessionId);
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
case "complete": {
|
|
262
|
+
// Update session timestamp
|
|
263
|
+
queries.updateSessionStatus("idle", sessionId);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
case "error": {
|
|
268
|
+
queries.updateSessionStatus("error", sessionId);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Update session status in database
|
|
275
|
+
private updateDbStatus(sessionId: string, status: string): void {
|
|
276
|
+
queries.updateSessionStatus(status, sessionId);
|
|
277
|
+
}
|
|
278
|
+
}
|