@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
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Git Worktree management for isolated feature development
3
+ */
4
+
5
+ import { exec } from "child_process";
6
+ import { promisify } from "util";
7
+ import * as path from "path";
8
+ import * as fs from "fs";
9
+ import * as os from "os";
10
+ import {
11
+ isGitRepo,
12
+ branchExists,
13
+ getRepoName,
14
+ slugify,
15
+ generateBranchName,
16
+ } from "./git";
17
+
18
+ const execAsync = promisify(exec);
19
+
20
+ // Base directory for all worktrees
21
+ const WORKTREES_DIR = path.join(os.homedir(), ".claude-deck", "worktrees");
22
+
23
+ export interface WorktreeInfo {
24
+ worktreePath: string;
25
+ branchName: string;
26
+ baseBranch: string;
27
+ projectPath: string;
28
+ projectName: string;
29
+ }
30
+
31
+ export interface CreateWorktreeOptions {
32
+ projectPath: string;
33
+ featureName: string;
34
+ baseBranch?: string;
35
+ }
36
+
37
+ /**
38
+ * Ensure the worktrees directory exists
39
+ */
40
+ async function ensureWorktreesDir(): Promise<void> {
41
+ await fs.promises.mkdir(WORKTREES_DIR, { recursive: true });
42
+ }
43
+
44
+ /**
45
+ * Resolve a path, expanding ~ to home directory
46
+ */
47
+ function resolvePath(p: string): string {
48
+ return p.replace(/^~/, os.homedir());
49
+ }
50
+
51
+ /**
52
+ * Generate a unique worktree directory name
53
+ */
54
+ function generateWorktreeDirName(
55
+ projectName: string,
56
+ featureName: string
57
+ ): string {
58
+ const featureSlug = slugify(featureName);
59
+ return `${projectName}-${featureSlug}`;
60
+ }
61
+
62
+ /**
63
+ * Create a new worktree for a feature branch
64
+ */
65
+ export async function createWorktree(
66
+ options: CreateWorktreeOptions
67
+ ): Promise<WorktreeInfo> {
68
+ const { projectPath, featureName, baseBranch = "main" } = options;
69
+
70
+ const resolvedProjectPath = resolvePath(projectPath);
71
+
72
+ // Validate project is a git repo
73
+ if (!(await isGitRepo(resolvedProjectPath))) {
74
+ throw new Error(`Not a git repository: ${projectPath}`);
75
+ }
76
+
77
+ // Generate branch name
78
+ const branchName = generateBranchName(featureName);
79
+
80
+ // Check if branch already exists
81
+ if (await branchExists(resolvedProjectPath, branchName)) {
82
+ throw new Error(`Branch already exists: ${branchName}`);
83
+ }
84
+
85
+ // Generate worktree path
86
+ const projectName = getRepoName(resolvedProjectPath);
87
+ const worktreeDirName = generateWorktreeDirName(projectName, featureName);
88
+ const worktreePath = path.join(WORKTREES_DIR, worktreeDirName);
89
+
90
+ // Check if worktree path already exists
91
+ if (fs.existsSync(worktreePath)) {
92
+ throw new Error(`Worktree path already exists: ${worktreePath}`);
93
+ }
94
+
95
+ // Ensure worktrees directory exists
96
+ await ensureWorktreesDir();
97
+
98
+ // Create the worktree with a new branch
99
+ // Try multiple ref formats to avoid "ambiguous refname" errors
100
+ const refFormats = [
101
+ `origin/${baseBranch}`, // Try remote first (most explicit)
102
+ `refs/heads/${baseBranch}`, // Then local branch
103
+ baseBranch, // Finally, bare name as fallback
104
+ ];
105
+
106
+ let lastError: Error | null = null;
107
+ for (const ref of refFormats) {
108
+ try {
109
+ await execAsync(
110
+ `git -C "${resolvedProjectPath}" worktree add -b "${branchName}" "${worktreePath}" "${ref}"`,
111
+ { timeout: 30000 }
112
+ );
113
+ lastError = null;
114
+ break; // Success!
115
+ } catch (error: unknown) {
116
+ lastError = error instanceof Error ? error : new Error(String(error));
117
+ // Continue to next ref format
118
+ }
119
+ }
120
+
121
+ if (lastError) {
122
+ throw new Error(`Failed to create worktree: ${lastError.message}`);
123
+ }
124
+
125
+ return {
126
+ worktreePath,
127
+ branchName,
128
+ baseBranch,
129
+ projectPath: resolvedProjectPath,
130
+ projectName,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Delete a worktree and optionally its branch
136
+ */
137
+ export async function deleteWorktree(
138
+ worktreePath: string,
139
+ projectPath: string,
140
+ deleteBranch = false
141
+ ): Promise<void> {
142
+ const resolvedProjectPath = resolvePath(projectPath);
143
+ const resolvedWorktreePath = resolvePath(worktreePath);
144
+
145
+ // Get the branch name before removing (for optional deletion)
146
+ let branchName: string | null = null;
147
+ if (deleteBranch) {
148
+ try {
149
+ const { stdout } = await execAsync(
150
+ `git -C "${resolvedWorktreePath}" rev-parse --abbrev-ref HEAD`,
151
+ { timeout: 5000 }
152
+ );
153
+ branchName = stdout.trim();
154
+ } catch {
155
+ // Ignore - worktree might already be gone
156
+ }
157
+ }
158
+
159
+ // Remove the worktree
160
+ try {
161
+ await execAsync(
162
+ `git -C "${resolvedProjectPath}" worktree remove "${resolvedWorktreePath}" --force`,
163
+ { timeout: 30000 }
164
+ );
165
+ } catch {
166
+ // If git worktree remove fails, try manual cleanup
167
+ if (fs.existsSync(resolvedWorktreePath)) {
168
+ await fs.promises.rm(resolvedWorktreePath, {
169
+ recursive: true,
170
+ force: true,
171
+ });
172
+ }
173
+ // Prune worktree references
174
+ try {
175
+ await execAsync(`git -C "${resolvedProjectPath}" worktree prune`, {
176
+ timeout: 10000,
177
+ });
178
+ } catch {
179
+ // Ignore prune errors
180
+ }
181
+ }
182
+
183
+ // Optionally delete the branch
184
+ if (
185
+ deleteBranch &&
186
+ branchName &&
187
+ branchName !== "main" &&
188
+ branchName !== "master"
189
+ ) {
190
+ try {
191
+ await execAsync(
192
+ `git -C "${resolvedProjectPath}" branch -D "${branchName}"`,
193
+ { timeout: 10000 }
194
+ );
195
+ } catch {
196
+ // Ignore branch deletion errors (might be merged or checked out elsewhere)
197
+ }
198
+ }
199
+ }
200
+
201
+ /**
202
+ * List all worktrees for a project
203
+ */
204
+ export async function listWorktrees(projectPath: string): Promise<
205
+ Array<{
206
+ path: string;
207
+ branch: string;
208
+ head: string;
209
+ }>
210
+ > {
211
+ const resolvedProjectPath = resolvePath(projectPath);
212
+
213
+ try {
214
+ const { stdout } = await execAsync(
215
+ `git -C "${resolvedProjectPath}" worktree list --porcelain`,
216
+ { timeout: 10000 }
217
+ );
218
+
219
+ const worktrees: Array<{ path: string; branch: string; head: string }> = [];
220
+ const entries = stdout.split("\n\n").filter(Boolean);
221
+
222
+ for (const entry of entries) {
223
+ const lines = entry.split("\n");
224
+ let worktreePath = "";
225
+ let branch = "";
226
+ let head = "";
227
+
228
+ for (const line of lines) {
229
+ if (line.startsWith("worktree ")) {
230
+ worktreePath = line.slice(9);
231
+ } else if (line.startsWith("branch ")) {
232
+ branch = line.slice(7).replace("refs/heads/", "");
233
+ } else if (line.startsWith("HEAD ")) {
234
+ head = line.slice(5);
235
+ }
236
+ }
237
+
238
+ if (worktreePath) {
239
+ worktrees.push({ path: worktreePath, branch, head });
240
+ }
241
+ }
242
+
243
+ return worktrees;
244
+ } catch {
245
+ return [];
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Check if a path is inside an ClaudeDeck worktree
251
+ */
252
+ export function isClaudeDeckWorktree(worktreePath: string): boolean {
253
+ const resolvedPath = resolvePath(worktreePath);
254
+ return resolvedPath.startsWith(WORKTREES_DIR);
255
+ }
256
+
257
+ /**
258
+ * Get the worktrees base directory
259
+ */
260
+ export function getWorktreesDir(): string {
261
+ return WORKTREES_DIR;
262
+ }
@@ -0,0 +1,438 @@
1
+ #!/usr/bin/env npx ts-node
2
+ /**
3
+ * MCP Server for Session Orchestration
4
+ *
5
+ * Exposes tools for any Claude session to become a "conductor" that spawns
6
+ * and manages worker sessions. Each worker gets its own git worktree.
7
+ *
8
+ * Setup (one-time, in ~/.claude/settings.json or project .mcp.json):
9
+ * {
10
+ * "mcpServers": {
11
+ * "claude-deck": {
12
+ * "command": "npx",
13
+ * "args": ["tsx", "/path/to/claude-deck/mcp/orchestration-server.ts"],
14
+ * "env": {
15
+ * "AGENTOS_URL": "http://localhost:3011"
16
+ * }
17
+ * }
18
+ * }
19
+ * }
20
+ *
21
+ * Usage: Any session can spawn workers by calling spawn_worker with its own
22
+ * session ID as conductorId. The UI will show the conductor/worker hierarchy.
23
+ */
24
+
25
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
26
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
27
+ import {
28
+ CallToolRequestSchema,
29
+ ListToolsRequestSchema,
30
+ } from "@modelcontextprotocol/sdk/types.js";
31
+
32
+ const AGENTOS_URL = process.env.AGENTOS_URL || "http://localhost:3011";
33
+
34
+ // Optional: Get conductor session ID from environment (can also be passed per-call)
35
+ const DEFAULT_CONDUCTOR_ID = process.env.CONDUCTOR_SESSION_ID || "";
36
+
37
+ async function apiCall(path: string, options?: RequestInit) {
38
+ const url = `${AGENTOS_URL}${path}`;
39
+ const response = await fetch(url, {
40
+ ...options,
41
+ headers: {
42
+ "Content-Type": "application/json",
43
+ ...options?.headers,
44
+ },
45
+ });
46
+ return response.json();
47
+ }
48
+
49
+ const server = new Server(
50
+ {
51
+ name: "claude-deck-orchestration",
52
+ version: "1.0.0",
53
+ },
54
+ {
55
+ capabilities: {
56
+ tools: {},
57
+ },
58
+ }
59
+ );
60
+
61
+ // List available tools
62
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
63
+ return {
64
+ tools: [
65
+ {
66
+ name: "spawn_worker",
67
+ description:
68
+ "Spawn a new worker session to handle a task. Creates an isolated git worktree for the worker.",
69
+ inputSchema: {
70
+ type: "object" as const,
71
+ properties: {
72
+ conductorId: {
73
+ type: "string",
74
+ description:
75
+ "Your session ID (the conductor). Required unless CONDUCTOR_SESSION_ID env var is set.",
76
+ },
77
+ task: {
78
+ type: "string",
79
+ description: "The task/prompt to send to the worker",
80
+ },
81
+ workingDirectory: {
82
+ type: "string",
83
+ description:
84
+ "The git repository path for the worker to operate in",
85
+ },
86
+ branchName: {
87
+ type: "string",
88
+ description:
89
+ "Optional branch name for the worktree (auto-generated if not provided)",
90
+ },
91
+ useWorktree: {
92
+ type: "boolean",
93
+ description:
94
+ "Whether to create an isolated worktree (default: true)",
95
+ default: true,
96
+ },
97
+ model: {
98
+ type: "string",
99
+ description: "Model to use (sonnet, opus, haiku)",
100
+ default: "sonnet",
101
+ },
102
+ },
103
+ required: ["task", "workingDirectory"],
104
+ },
105
+ },
106
+ {
107
+ name: "list_workers",
108
+ description: "List all worker sessions spawned by a conductor",
109
+ inputSchema: {
110
+ type: "object" as const,
111
+ properties: {
112
+ conductorId: {
113
+ type: "string",
114
+ description:
115
+ "The conductor session ID. Required unless CONDUCTOR_SESSION_ID env var is set.",
116
+ },
117
+ },
118
+ },
119
+ },
120
+ {
121
+ name: "get_worker_output",
122
+ description: "Get recent terminal output from a worker",
123
+ inputSchema: {
124
+ type: "object" as const,
125
+ properties: {
126
+ workerId: {
127
+ type: "string",
128
+ description: "The worker session ID",
129
+ },
130
+ lines: {
131
+ type: "number",
132
+ description: "Number of lines to retrieve (default: 50)",
133
+ default: 50,
134
+ },
135
+ },
136
+ required: ["workerId"],
137
+ },
138
+ },
139
+ {
140
+ name: "send_to_worker",
141
+ description: "Send a message or command to a worker",
142
+ inputSchema: {
143
+ type: "object" as const,
144
+ properties: {
145
+ workerId: {
146
+ type: "string",
147
+ description: "The worker session ID",
148
+ },
149
+ message: {
150
+ type: "string",
151
+ description: "The message to send",
152
+ },
153
+ },
154
+ required: ["workerId", "message"],
155
+ },
156
+ },
157
+ {
158
+ name: "complete_worker",
159
+ description: "Mark a worker as completed (task finished successfully)",
160
+ inputSchema: {
161
+ type: "object" as const,
162
+ properties: {
163
+ workerId: {
164
+ type: "string",
165
+ description: "The worker session ID",
166
+ },
167
+ },
168
+ required: ["workerId"],
169
+ },
170
+ },
171
+ {
172
+ name: "kill_worker",
173
+ description:
174
+ "Kill a worker session and optionally clean up its worktree",
175
+ inputSchema: {
176
+ type: "object" as const,
177
+ properties: {
178
+ workerId: {
179
+ type: "string",
180
+ description: "The worker session ID",
181
+ },
182
+ cleanupWorktree: {
183
+ type: "boolean",
184
+ description: "Whether to delete the worktree (default: false)",
185
+ default: false,
186
+ },
187
+ },
188
+ required: ["workerId"],
189
+ },
190
+ },
191
+ {
192
+ name: "get_workers_summary",
193
+ description: "Get a summary count of workers by status",
194
+ inputSchema: {
195
+ type: "object" as const,
196
+ properties: {
197
+ conductorId: {
198
+ type: "string",
199
+ description:
200
+ "The conductor session ID. Required unless CONDUCTOR_SESSION_ID env var is set.",
201
+ },
202
+ },
203
+ },
204
+ },
205
+ ],
206
+ };
207
+ });
208
+
209
+ // Helper to get conductor ID from args or env
210
+ function getConductorId(
211
+ args: Record<string, unknown> | undefined
212
+ ): string | null {
213
+ return (args?.conductorId as string) || DEFAULT_CONDUCTOR_ID || null;
214
+ }
215
+
216
+ // Handle tool calls
217
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
218
+ const { name, arguments: args } = request.params;
219
+
220
+ try {
221
+ switch (name) {
222
+ case "spawn_worker": {
223
+ const conductorId = getConductorId(args);
224
+ if (!conductorId) {
225
+ return {
226
+ content: [
227
+ {
228
+ type: "text" as const,
229
+ text: "Error: conductorId is required. Pass it as a parameter or set CONDUCTOR_SESSION_ID env var.",
230
+ },
231
+ ],
232
+ };
233
+ }
234
+ const result = await apiCall("/api/orchestrate/spawn", {
235
+ method: "POST",
236
+ body: JSON.stringify({
237
+ conductorSessionId: conductorId,
238
+ task: args?.task,
239
+ workingDirectory: args?.workingDirectory,
240
+ branchName: args?.branchName,
241
+ useWorktree: args?.useWorktree ?? true,
242
+ model: args?.model || "sonnet",
243
+ }),
244
+ });
245
+ return {
246
+ content: [
247
+ {
248
+ type: "text" as const,
249
+ text: result.error
250
+ ? `Error: ${result.error}`
251
+ : `Worker spawned successfully!\nID: ${result.session.id}\nName: ${result.session.name}\nWorktree: ${result.session.worktree_path || "none"}`,
252
+ },
253
+ ],
254
+ };
255
+ }
256
+
257
+ case "list_workers": {
258
+ const conductorId = getConductorId(args);
259
+ if (!conductorId) {
260
+ return {
261
+ content: [
262
+ {
263
+ type: "text" as const,
264
+ text: "Error: conductorId is required. Pass it as a parameter or set CONDUCTOR_SESSION_ID env var.",
265
+ },
266
+ ],
267
+ };
268
+ }
269
+ const result = await apiCall(
270
+ `/api/orchestrate/workers?conductorId=${conductorId}`
271
+ );
272
+ if (result.error) {
273
+ return {
274
+ content: [
275
+ { type: "text" as const, text: `Error: ${result.error}` },
276
+ ],
277
+ };
278
+ }
279
+ const workers = result.workers || [];
280
+ if (workers.length === 0) {
281
+ return {
282
+ content: [
283
+ { type: "text" as const, text: "No workers spawned yet." },
284
+ ],
285
+ };
286
+ }
287
+ const list = workers
288
+ .map(
289
+ (w: {
290
+ id: string;
291
+ name: string;
292
+ status: string;
293
+ task: string;
294
+ branchName: string | null;
295
+ }) =>
296
+ `- [${w.status.toUpperCase()}] ${w.name} (${w.id.slice(0, 8)})\n Task: ${w.task}\n Branch: ${w.branchName || "none"}`
297
+ )
298
+ .join("\n\n");
299
+ return {
300
+ content: [{ type: "text" as const, text: `Workers:\n\n${list}` }],
301
+ };
302
+ }
303
+
304
+ case "get_worker_output": {
305
+ const result = await apiCall(
306
+ `/api/orchestrate/workers/${args?.workerId}?lines=${args?.lines || 50}`
307
+ );
308
+ return {
309
+ content: [
310
+ {
311
+ type: "text" as const,
312
+ text: result.error
313
+ ? `Error: ${result.error}`
314
+ : result.output || "(no output)",
315
+ },
316
+ ],
317
+ };
318
+ }
319
+
320
+ case "send_to_worker": {
321
+ const result = await apiCall(
322
+ `/api/orchestrate/workers/${args?.workerId}`,
323
+ {
324
+ method: "POST",
325
+ body: JSON.stringify({
326
+ action: "send",
327
+ message: args?.message,
328
+ }),
329
+ }
330
+ );
331
+ return {
332
+ content: [
333
+ {
334
+ type: "text" as const,
335
+ text: result.error
336
+ ? `Error: ${result.error}`
337
+ : "Message sent successfully.",
338
+ },
339
+ ],
340
+ };
341
+ }
342
+
343
+ case "complete_worker": {
344
+ const result = await apiCall(
345
+ `/api/orchestrate/workers/${args?.workerId}`,
346
+ {
347
+ method: "POST",
348
+ body: JSON.stringify({ action: "complete" }),
349
+ }
350
+ );
351
+ return {
352
+ content: [
353
+ {
354
+ type: "text" as const,
355
+ text: result.error
356
+ ? `Error: ${result.error}`
357
+ : "Worker marked as completed.",
358
+ },
359
+ ],
360
+ };
361
+ }
362
+
363
+ case "kill_worker": {
364
+ const cleanup = args?.cleanupWorktree ? "?cleanup=true" : "";
365
+ const result = await apiCall(
366
+ `/api/orchestrate/workers/${args?.workerId}${cleanup}`,
367
+ { method: "DELETE" }
368
+ );
369
+ return {
370
+ content: [
371
+ {
372
+ type: "text" as const,
373
+ text: result.error
374
+ ? `Error: ${result.error}`
375
+ : "Worker killed successfully.",
376
+ },
377
+ ],
378
+ };
379
+ }
380
+
381
+ case "get_workers_summary": {
382
+ const conductorId = getConductorId(args);
383
+ if (!conductorId) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: "text" as const,
388
+ text: "Error: conductorId is required. Pass it as a parameter or set CONDUCTOR_SESSION_ID env var.",
389
+ },
390
+ ],
391
+ };
392
+ }
393
+ const result = await apiCall(
394
+ `/api/orchestrate/workers?conductorId=${conductorId}&summary=true`
395
+ );
396
+ if (result.error) {
397
+ return {
398
+ content: [
399
+ { type: "text" as const, text: `Error: ${result.error}` },
400
+ ],
401
+ };
402
+ }
403
+ const s = result.summary;
404
+ return {
405
+ content: [
406
+ {
407
+ type: "text" as const,
408
+ text: `Workers Summary:\n- Total: ${s.total}\n- Pending: ${s.pending}\n- Running: ${s.running}\n- Waiting: ${s.waiting}\n- Completed: ${s.completed}\n- Failed: ${s.failed}`,
409
+ },
410
+ ],
411
+ };
412
+ }
413
+
414
+ default:
415
+ return {
416
+ content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
417
+ };
418
+ }
419
+ } catch (error) {
420
+ return {
421
+ content: [
422
+ {
423
+ type: "text" as const,
424
+ text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
425
+ },
426
+ ],
427
+ };
428
+ }
429
+ });
430
+
431
+ // Start the server
432
+ async function main() {
433
+ const transport = new StdioServerTransport();
434
+ await server.connect(transport);
435
+ console.error("ClaudeDeck Orchestration MCP Server started");
436
+ }
437
+
438
+ main().catch(console.error);