@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/worktrees.ts
ADDED
|
@@ -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);
|