@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.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/app/api/claude/hidden/route.ts +66 -0
  4. package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
  5. package/app/api/claude/projects/route.ts +44 -0
  6. package/app/api/code-search/available/route.ts +12 -0
  7. package/app/api/code-search/route.ts +47 -0
  8. package/app/api/dev-servers/[id]/logs/route.ts +23 -0
  9. package/app/api/dev-servers/[id]/restart/route.ts +20 -0
  10. package/app/api/dev-servers/[id]/route.ts +51 -0
  11. package/app/api/dev-servers/[id]/stop/route.ts +20 -0
  12. package/app/api/dev-servers/detect/route.ts +39 -0
  13. package/app/api/dev-servers/route.ts +48 -0
  14. package/app/api/exec/route.ts +60 -0
  15. package/app/api/files/content/route.ts +76 -0
  16. package/app/api/files/route.ts +37 -0
  17. package/app/api/files/upload-temp/route.ts +41 -0
  18. package/app/api/git/check/route.ts +54 -0
  19. package/app/api/git/clone/route.ts +99 -0
  20. package/app/api/git/commit/route.ts +75 -0
  21. package/app/api/git/discard/route.ts +38 -0
  22. package/app/api/git/file-content/route.ts +64 -0
  23. package/app/api/git/history/[hash]/diff/route.ts +38 -0
  24. package/app/api/git/history/[hash]/route.ts +34 -0
  25. package/app/api/git/history/route.ts +27 -0
  26. package/app/api/git/multi-status/route.ts +46 -0
  27. package/app/api/git/pr/route.ts +164 -0
  28. package/app/api/git/push/route.ts +64 -0
  29. package/app/api/git/stage/route.ts +40 -0
  30. package/app/api/git/status/route.ts +51 -0
  31. package/app/api/git/unstage/route.ts +46 -0
  32. package/app/api/groups/[...path]/route.ts +136 -0
  33. package/app/api/groups/route.ts +93 -0
  34. package/app/api/orchestrate/spawn/route.ts +45 -0
  35. package/app/api/orchestrate/workers/[id]/route.ts +89 -0
  36. package/app/api/orchestrate/workers/route.ts +31 -0
  37. package/app/api/projects/[id]/detect/route.ts +27 -0
  38. package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
  39. package/app/api/projects/[id]/dev-servers/route.ts +51 -0
  40. package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
  41. package/app/api/projects/[id]/repositories/route.ts +74 -0
  42. package/app/api/projects/[id]/route.ts +108 -0
  43. package/app/api/projects/detect/route.ts +33 -0
  44. package/app/api/projects/route.ts +59 -0
  45. package/app/api/sessions/[id]/claude-session/route.ts +42 -0
  46. package/app/api/sessions/[id]/fork/route.ts +74 -0
  47. package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
  48. package/app/api/sessions/[id]/messages/route.ts +60 -0
  49. package/app/api/sessions/[id]/pr/route.ts +188 -0
  50. package/app/api/sessions/[id]/preview/route.ts +42 -0
  51. package/app/api/sessions/[id]/route.ts +229 -0
  52. package/app/api/sessions/[id]/send-keys/route.ts +119 -0
  53. package/app/api/sessions/[id]/summarize/route.ts +331 -0
  54. package/app/api/sessions/init-script/route.ts +84 -0
  55. package/app/api/sessions/route.ts +209 -0
  56. package/app/api/sessions/status/route.ts +237 -0
  57. package/app/api/system/route.ts +9 -0
  58. package/app/api/tmux/kill-all/route.ts +57 -0
  59. package/app/api/tmux/rename/route.ts +30 -0
  60. package/app/globals.css +174 -0
  61. package/app/icon.svg +11 -0
  62. package/app/layout.tsx +122 -0
  63. package/app/page.tsx +629 -0
  64. package/components/ChatMessage.tsx +65 -0
  65. package/components/ChatView.tsx +276 -0
  66. package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
  67. package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
  68. package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
  69. package/components/ClaudeProjects/index.ts +1 -0
  70. package/components/CodeSearch/CodeSearchResults.tsx +177 -0
  71. package/components/ConductorPanel.tsx +256 -0
  72. package/components/DevServers/DevServerCard.tsx +311 -0
  73. package/components/DevServers/DevServersSection.tsx +91 -0
  74. package/components/DevServers/ServerLogsModal.tsx +151 -0
  75. package/components/DevServers/StartServerDialog.tsx +359 -0
  76. package/components/DevServers/index.ts +4 -0
  77. package/components/DiffViewer/DiffModal.tsx +151 -0
  78. package/components/DiffViewer/UnifiedDiff.tsx +185 -0
  79. package/components/DiffViewer/index.tsx +2 -0
  80. package/components/DirectoryPicker.tsx +355 -0
  81. package/components/FileExplorer/FileEditor.tsx +276 -0
  82. package/components/FileExplorer/FileTabs.tsx +118 -0
  83. package/components/FileExplorer/FileTree.tsx +214 -0
  84. package/components/FileExplorer/HtmlRenderer.tsx +16 -0
  85. package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
  86. package/components/FileExplorer/index.tsx +520 -0
  87. package/components/FilePicker.tsx +339 -0
  88. package/components/FolderPicker.tsx +201 -0
  89. package/components/GitDrawer/FileEditDialog.tsx +400 -0
  90. package/components/GitDrawer/index.tsx +464 -0
  91. package/components/GitPanel/CommitForm.tsx +205 -0
  92. package/components/GitPanel/CommitHistory.tsx +174 -0
  93. package/components/GitPanel/CommitItem.tsx +196 -0
  94. package/components/GitPanel/FileChanges.tsx +414 -0
  95. package/components/GitPanel/GitPanelTabs.tsx +39 -0
  96. package/components/GitPanel/index.tsx +817 -0
  97. package/components/MessageInput.tsx +82 -0
  98. package/components/NewClaudeSessionDialog.tsx +166 -0
  99. package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
  100. package/components/NewSessionDialog/AgentSelector.tsx +37 -0
  101. package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
  102. package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
  103. package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
  104. package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
  105. package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
  106. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
  107. package/components/NewSessionDialog/index.tsx +106 -0
  108. package/components/NotificationSettings.tsx +127 -0
  109. package/components/PRCreationModal.tsx +272 -0
  110. package/components/Pane/DesktopTabBar.tsx +353 -0
  111. package/components/Pane/MobileTabBar.tsx +210 -0
  112. package/components/Pane/OpenInVSCode.tsx +69 -0
  113. package/components/Pane/PaneSkeletons.tsx +57 -0
  114. package/components/Pane/index.tsx +558 -0
  115. package/components/PaneLayout.tsx +60 -0
  116. package/components/Projects/DevServersSection.tsx +140 -0
  117. package/components/Projects/DirectoryField.tsx +92 -0
  118. package/components/Projects/NewProjectDialog.tsx +188 -0
  119. package/components/Projects/NewProjectDialog.types.ts +46 -0
  120. package/components/Projects/ProjectCard.tsx +276 -0
  121. package/components/Projects/ProjectSettingsDialog.tsx +811 -0
  122. package/components/Projects/hooks/useNewProjectForm.ts +249 -0
  123. package/components/Projects/index.ts +3 -0
  124. package/components/Providers.tsx +49 -0
  125. package/components/QuickSwitcher.tsx +306 -0
  126. package/components/SessionList/KillAllConfirm.tsx +46 -0
  127. package/components/SessionList/SelectionToolbar.tsx +164 -0
  128. package/components/SessionList/SessionList.types.ts +37 -0
  129. package/components/SessionList/SessionListHeader.tsx +71 -0
  130. package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
  131. package/components/SessionList/index.tsx +189 -0
  132. package/components/ShellDrawer/index.tsx +106 -0
  133. package/components/SidebarFooter.tsx +55 -0
  134. package/components/Terminal/KeybarToggleButton.tsx +45 -0
  135. package/components/Terminal/ScrollToBottomButton.tsx +32 -0
  136. package/components/Terminal/SearchBar.tsx +71 -0
  137. package/components/Terminal/TerminalToolbar.tsx +551 -0
  138. package/components/Terminal/VirtualKeyboard.tsx +711 -0
  139. package/components/Terminal/constants.ts +20 -0
  140. package/components/Terminal/hooks/index.ts +5 -0
  141. package/components/Terminal/hooks/resize-handlers.ts +140 -0
  142. package/components/Terminal/hooks/terminal-init.ts +151 -0
  143. package/components/Terminal/hooks/touch-scroll.ts +155 -0
  144. package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
  145. package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
  146. package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
  147. package/components/Terminal/hooks/websocket-connection.ts +274 -0
  148. package/components/Terminal/index.tsx +320 -0
  149. package/components/ThemeToggle.tsx +168 -0
  150. package/components/TmuxSessions.tsx +132 -0
  151. package/components/ToolCallDisplay.tsx +71 -0
  152. package/components/WorkerCard.tsx +245 -0
  153. package/components/a/ABadge.tsx +115 -0
  154. package/components/a/AButton.tsx +163 -0
  155. package/components/a/ADialog.tsx +93 -0
  156. package/components/a/ADropdownMenu.tsx +279 -0
  157. package/components/a/AIconButton.tsx +190 -0
  158. package/components/a/ASheet.tsx +150 -0
  159. package/components/a/ATooltip.tsx +77 -0
  160. package/components/a/index.ts +64 -0
  161. package/components/mobile/SwipeSidebar.tsx +122 -0
  162. package/components/ui/badge.tsx +41 -0
  163. package/components/ui/button.tsx +60 -0
  164. package/components/ui/context-menu.tsx +197 -0
  165. package/components/ui/dialog.tsx +143 -0
  166. package/components/ui/dropdown-menu.tsx +257 -0
  167. package/components/ui/input.tsx +21 -0
  168. package/components/ui/scroll-area.tsx +52 -0
  169. package/components/ui/select.tsx +159 -0
  170. package/components/ui/skeleton.tsx +111 -0
  171. package/components/ui/switch.tsx +31 -0
  172. package/components/ui/textarea.tsx +21 -0
  173. package/components/ui/tooltip.tsx +32 -0
  174. package/components/views/DesktopView.tsx +244 -0
  175. package/components/views/MobileView.tsx +110 -0
  176. package/components/views/types.ts +75 -0
  177. package/contexts/PaneContext.tsx +336 -0
  178. package/data/claude/index.ts +9 -0
  179. package/data/claude/keys.ts +6 -0
  180. package/data/claude/queries.ts +120 -0
  181. package/data/claude/useClaudeUpdates.ts +37 -0
  182. package/data/code-search/index.ts +2 -0
  183. package/data/code-search/keys.ts +7 -0
  184. package/data/code-search/queries.ts +61 -0
  185. package/data/dev-servers/index.ts +8 -0
  186. package/data/dev-servers/keys.ts +4 -0
  187. package/data/dev-servers/queries.ts +104 -0
  188. package/data/files/index.ts +3 -0
  189. package/data/files/keys.ts +4 -0
  190. package/data/files/queries.ts +25 -0
  191. package/data/git/keys.ts +15 -0
  192. package/data/git/queries.ts +395 -0
  193. package/data/groups/index.ts +1 -0
  194. package/data/groups/mutations.ts +95 -0
  195. package/data/projects/index.ts +10 -0
  196. package/data/projects/keys.ts +4 -0
  197. package/data/projects/queries.ts +193 -0
  198. package/data/repositories/index.ts +7 -0
  199. package/data/repositories/keys.ts +5 -0
  200. package/data/repositories/queries.ts +122 -0
  201. package/data/sessions/index.ts +12 -0
  202. package/data/sessions/keys.ts +8 -0
  203. package/data/sessions/queries.ts +218 -0
  204. package/data/statuses/index.ts +1 -0
  205. package/data/statuses/queries.ts +69 -0
  206. package/hooks/useCopyToClipboard.ts +48 -0
  207. package/hooks/useDevServersManager.ts +73 -0
  208. package/hooks/useDirectoryBrowser.ts +90 -0
  209. package/hooks/useDrawerAnimation.ts +27 -0
  210. package/hooks/useFileDrop.ts +87 -0
  211. package/hooks/useFileEditor.ts +184 -0
  212. package/hooks/useGroups.ts +37 -0
  213. package/hooks/useHomePath.ts +34 -0
  214. package/hooks/useKeyRepeat.ts +55 -0
  215. package/hooks/useKeybarVisibility.ts +42 -0
  216. package/hooks/useNotifications.ts +257 -0
  217. package/hooks/useProjects.ts +53 -0
  218. package/hooks/useSessionStatuses.ts +30 -0
  219. package/hooks/useSessions.ts +86 -0
  220. package/hooks/useSpeechRecognition.ts +124 -0
  221. package/hooks/useViewport.ts +32 -0
  222. package/hooks/useViewportHeight.ts +50 -0
  223. package/lib/async-operations.ts +35 -0
  224. package/lib/banner.ts +81 -0
  225. package/lib/claude/jsonl-cache.ts +86 -0
  226. package/lib/claude/jsonl-reader.ts +271 -0
  227. package/lib/claude/process-manager.ts +278 -0
  228. package/lib/claude/stream-parser.ts +173 -0
  229. package/lib/claude/types.ts +154 -0
  230. package/lib/claude/watcher.ts +71 -0
  231. package/lib/client/session-registry.ts +111 -0
  232. package/lib/code-search.ts +121 -0
  233. package/lib/db/index.ts +48 -0
  234. package/lib/db/migrations.ts +45 -0
  235. package/lib/db/queries.ts +460 -0
  236. package/lib/db/schema.ts +114 -0
  237. package/lib/db/types.ts +92 -0
  238. package/lib/db.ts +2 -0
  239. package/lib/dev-servers.ts +509 -0
  240. package/lib/diff-parser.ts +221 -0
  241. package/lib/env-setup.ts +285 -0
  242. package/lib/file-upload.ts +34 -0
  243. package/lib/file-utils.ts +50 -0
  244. package/lib/files.ts +207 -0
  245. package/lib/git-history.ts +294 -0
  246. package/lib/git-status.ts +391 -0
  247. package/lib/git.ts +257 -0
  248. package/lib/mcp-config.ts +81 -0
  249. package/lib/multi-repo-git.ts +179 -0
  250. package/lib/notifications.ts +219 -0
  251. package/lib/orchestration.ts +448 -0
  252. package/lib/panes.ts +232 -0
  253. package/lib/ports.ts +97 -0
  254. package/lib/pr-generation.ts +307 -0
  255. package/lib/pr.ts +234 -0
  256. package/lib/projects.ts +578 -0
  257. package/lib/providers/registry.ts +70 -0
  258. package/lib/providers.ts +121 -0
  259. package/lib/query-client.ts +14 -0
  260. package/lib/rangeSelectionUtils.ts +65 -0
  261. package/lib/status-detector.ts +375 -0
  262. package/lib/terminal-themes.ts +265 -0
  263. package/lib/theme-config.ts +327 -0
  264. package/lib/utils.ts +6 -0
  265. package/lib/worktrees.ts +262 -0
  266. package/mcp/orchestration-server.ts +438 -0
  267. package/package.json +139 -0
  268. package/postcss.config.mjs +7 -0
  269. package/public/icon.svg +10 -0
  270. package/public/icons/icon-128x128.png +0 -0
  271. package/public/icons/icon-144x144.png +0 -0
  272. package/public/icons/icon-152x152.png +0 -0
  273. package/public/icons/icon-192x192.png +0 -0
  274. package/public/icons/icon-384x384.png +0 -0
  275. package/public/icons/icon-512x512.png +0 -0
  276. package/public/icons/icon-72x72.png +0 -0
  277. package/public/icons/icon-96x96.png +0 -0
  278. package/public/manifest.json +61 -0
  279. package/public/sw.js +64 -0
  280. package/scripts/agent-os +91 -0
  281. package/scripts/install.sh +48 -0
  282. package/scripts/lib/ai-clis.sh +132 -0
  283. package/scripts/lib/commands.sh +487 -0
  284. package/scripts/lib/common.sh +89 -0
  285. package/scripts/lib/prerequisites.sh +462 -0
  286. package/scripts/setup.sh +134 -0
  287. package/server.ts +155 -0
  288. package/stores/fileOpen.ts +26 -0
  289. package/stores/index.ts +1 -0
  290. package/stores/initialPrompt.ts +24 -0
  291. package/stores/sessionSelection.ts +48 -0
  292. package/styles/themes.css +603 -0
  293. 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
+ }