@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { queries, type Session } from "@/lib/db";
|
|
5
|
+
import { appendFileSync } from "fs";
|
|
6
|
+
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
// Log to file for debugging
|
|
10
|
+
const LOG_FILE = "/tmp/claude-deck-send-keys.log";
|
|
11
|
+
function log(msg: string) {
|
|
12
|
+
const timestamp = new Date().toISOString();
|
|
13
|
+
const line = `[${timestamp}] ${msg}\n`;
|
|
14
|
+
console.log(`[send-keys] ${msg}`);
|
|
15
|
+
try {
|
|
16
|
+
appendFileSync(LOG_FILE, line);
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// POST /api/sessions/[id]/send-keys - Send text to a tmux session
|
|
21
|
+
export async function POST(
|
|
22
|
+
request: NextRequest,
|
|
23
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
24
|
+
) {
|
|
25
|
+
try {
|
|
26
|
+
const { id } = await params;
|
|
27
|
+
const body = await request.json();
|
|
28
|
+
const { text, pressEnter = true } = body;
|
|
29
|
+
|
|
30
|
+
log(`=== START send-keys for session ${id} ===`);
|
|
31
|
+
log(`Text length: ${text?.length || 0}, pressEnter: ${pressEnter}`);
|
|
32
|
+
|
|
33
|
+
if (!text) {
|
|
34
|
+
log("ERROR: No text provided");
|
|
35
|
+
return NextResponse.json({ error: "No text provided" }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const session = await queries.getSession(id);
|
|
39
|
+
|
|
40
|
+
if (!session) {
|
|
41
|
+
log(`ERROR: Session ${id} not found in DB`);
|
|
42
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tmuxSessionName = `${session.agent_type}-${id}`;
|
|
46
|
+
log(`Tmux session name: ${tmuxSessionName}`);
|
|
47
|
+
|
|
48
|
+
// Check if tmux session exists
|
|
49
|
+
try {
|
|
50
|
+
await execAsync(`tmux has-session -t "${tmuxSessionName}" 2>/dev/null`);
|
|
51
|
+
log(`Tmux session exists`);
|
|
52
|
+
} catch {
|
|
53
|
+
log(`ERROR: Tmux session ${tmuxSessionName} not running`);
|
|
54
|
+
return NextResponse.json(
|
|
55
|
+
{ error: "Tmux session not running" },
|
|
56
|
+
{ status: 400 }
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Write text to a temp file
|
|
61
|
+
const tempFile = `/tmp/claude-deck-send-${id}.txt`;
|
|
62
|
+
const fs = await import("fs/promises");
|
|
63
|
+
await fs.writeFile(tempFile, text);
|
|
64
|
+
log(`Wrote ${text.length} bytes to ${tempFile}`);
|
|
65
|
+
|
|
66
|
+
// Use a named buffer to avoid race conditions
|
|
67
|
+
const bufferName = `send-${id}`;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Load file into named tmux buffer
|
|
71
|
+
log(`Loading buffer "${bufferName}" from ${tempFile}`);
|
|
72
|
+
const loadCmd = `tmux load-buffer -b "${bufferName}" "${tempFile}"`;
|
|
73
|
+
log(`Running: ${loadCmd}`);
|
|
74
|
+
const loadResult = await execAsync(loadCmd);
|
|
75
|
+
log(
|
|
76
|
+
`Load stdout: "${loadResult.stdout}", stderr: "${loadResult.stderr}"`
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Paste the named buffer to the session
|
|
80
|
+
log(`Pasting buffer "${bufferName}" to ${tmuxSessionName}`);
|
|
81
|
+
const pasteCmd = `tmux paste-buffer -b "${bufferName}" -t "${tmuxSessionName}"`;
|
|
82
|
+
log(`Running: ${pasteCmd}`);
|
|
83
|
+
const pasteResult = await execAsync(pasteCmd);
|
|
84
|
+
log(
|
|
85
|
+
`Paste stdout: "${pasteResult.stdout}", stderr: "${pasteResult.stderr}"`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Delete the buffer after use
|
|
89
|
+
await execAsync(`tmux delete-buffer -b "${bufferName}"`).catch(() => {});
|
|
90
|
+
|
|
91
|
+
// Send Enter if requested
|
|
92
|
+
if (pressEnter) {
|
|
93
|
+
log(`Sending Enter to ${tmuxSessionName}`);
|
|
94
|
+
const enterCmd = `tmux send-keys -t "${tmuxSessionName}" Enter`;
|
|
95
|
+
log(`Running: ${enterCmd}`);
|
|
96
|
+
const enterResult = await execAsync(enterCmd);
|
|
97
|
+
log(
|
|
98
|
+
`Enter stdout: "${enterResult.stdout}", stderr: "${enterResult.stderr}"`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
log(`=== SUCCESS ===`);
|
|
103
|
+
return NextResponse.json({ success: true });
|
|
104
|
+
} catch (cmdError) {
|
|
105
|
+
const msg =
|
|
106
|
+
cmdError instanceof Error ? cmdError.message : String(cmdError);
|
|
107
|
+
log(`ERROR in commands: ${msg}`);
|
|
108
|
+
throw cmdError;
|
|
109
|
+
} finally {
|
|
110
|
+
// Clean up temp file
|
|
111
|
+
await fs.unlink(tempFile).catch(() => {});
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
115
|
+
log(`ERROR: ${msg}`);
|
|
116
|
+
console.error("Error sending keys:", error);
|
|
117
|
+
return NextResponse.json({ error: msg }, { status: 500 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec, spawn } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { queries, getDb, type Session } from "@/lib/db";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
// Get Claude session ID from tmux environment
|
|
12
|
+
async function getClaudeSessionId(tmuxSession: string): Promise<string | null> {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execAsync(
|
|
15
|
+
`tmux show-environment -t "${tmuxSession}" CLAUDE_SESSION_ID 2>/dev/null || echo ""`
|
|
16
|
+
);
|
|
17
|
+
const line = stdout.trim();
|
|
18
|
+
if (line.startsWith("CLAUDE_SESSION_ID=")) {
|
|
19
|
+
const sessionId = line.replace("CLAUDE_SESSION_ID=", "");
|
|
20
|
+
return sessionId && sessionId !== "null" ? sessionId : null;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Encode path for Claude's project directory format (/ becomes -)
|
|
29
|
+
function encodeProjectPath(cwd: string): string {
|
|
30
|
+
return cwd.replace(/\//g, "-");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Read and parse Claude session JSONL file
|
|
34
|
+
function readClaudeSessionHistory(
|
|
35
|
+
cwd: string,
|
|
36
|
+
claudeSessionId: string
|
|
37
|
+
): string | null {
|
|
38
|
+
const projectPath = encodeProjectPath(cwd);
|
|
39
|
+
const jsonlPath = `${homedir()}/.claude/projects/${projectPath}/${claudeSessionId}.jsonl`;
|
|
40
|
+
|
|
41
|
+
if (!existsSync(jsonlPath)) {
|
|
42
|
+
console.log(`[summarize] JSONL not found: ${jsonlPath}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(jsonlPath, "utf-8");
|
|
48
|
+
const lines = content.trim().split("\n");
|
|
49
|
+
const messages: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
try {
|
|
53
|
+
const entry = JSON.parse(line);
|
|
54
|
+
|
|
55
|
+
// Extract user messages
|
|
56
|
+
if (entry.type === "user" && entry.message?.content) {
|
|
57
|
+
const content =
|
|
58
|
+
typeof entry.message.content === "string"
|
|
59
|
+
? entry.message.content
|
|
60
|
+
: JSON.stringify(entry.message.content);
|
|
61
|
+
messages.push(`User: ${content}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract assistant text responses (skip tool calls and thinking)
|
|
65
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
66
|
+
const textBlocks = entry.message.content
|
|
67
|
+
.filter((block: { type: string }) => block.type === "text")
|
|
68
|
+
.map((block: { text: string }) => block.text)
|
|
69
|
+
.join("\n");
|
|
70
|
+
if (textBlocks) {
|
|
71
|
+
messages.push(`Assistant: ${textBlocks}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Skip malformed lines
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return messages.join("\n\n");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`[summarize] Error reading JSONL:`, error);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback: Capture recent tmux scrollback (last 500 lines)
|
|
87
|
+
async function captureScrollback(sessionName: string): Promise<string> {
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await execAsync(
|
|
90
|
+
`tmux capture-pane -t "${sessionName}" -p -S -500 2>/dev/null`
|
|
91
|
+
);
|
|
92
|
+
return stdout;
|
|
93
|
+
} catch {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get the actual working directory from tmux pane
|
|
99
|
+
async function getTmuxCwd(sessionName: string): Promise<string | null> {
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await execAsync(
|
|
102
|
+
`tmux display-message -t "${sessionName}" -p "#{pane_current_path}" 2>/dev/null`
|
|
103
|
+
);
|
|
104
|
+
return stdout.trim() || null;
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Generate summary using Claude CLI with stdin
|
|
111
|
+
async function generateSummary(conversation: string): Promise<string> {
|
|
112
|
+
const prompt = `Summarize this Claude Code conversation in under 300 words. Focus on: what was built, key files changed, current state, and any pending work. Be specific.`;
|
|
113
|
+
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const claude = spawn("claude", ["-p", prompt], {
|
|
116
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
let stdout = "";
|
|
120
|
+
let stderr = "";
|
|
121
|
+
|
|
122
|
+
claude.stdout.on("data", (data) => {
|
|
123
|
+
stdout += data.toString();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
claude.stderr.on("data", (data) => {
|
|
127
|
+
stderr += data.toString();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
claude.on("close", (code) => {
|
|
131
|
+
if (code === 0) {
|
|
132
|
+
resolve(stdout.trim());
|
|
133
|
+
} else {
|
|
134
|
+
console.error("Claude CLI failed:", stderr);
|
|
135
|
+
reject(new Error(`Claude CLI exited with code ${code}`));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
claude.on("error", (err) => {
|
|
140
|
+
reject(err);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Write conversation to stdin
|
|
144
|
+
claude.stdin.write(conversation);
|
|
145
|
+
claude.stdin.end();
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Wait for Claude prompt to appear in tmux session
|
|
150
|
+
async function waitForClaudeReady(
|
|
151
|
+
sessionName: string,
|
|
152
|
+
maxAttempts = 30
|
|
153
|
+
): Promise<boolean> {
|
|
154
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
155
|
+
try {
|
|
156
|
+
const { stdout } = await execAsync(
|
|
157
|
+
`tmux capture-pane -t "${sessionName}" -p 2>/dev/null`
|
|
158
|
+
);
|
|
159
|
+
// Look for Claude's status line which appears when UI is ready
|
|
160
|
+
if (stdout.includes("⏵⏵") || stdout.includes("accept edits")) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Ignore errors, keep polling
|
|
165
|
+
}
|
|
166
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Send text to tmux session using load-buffer + paste-buffer
|
|
172
|
+
async function sendToTmux(
|
|
173
|
+
sessionName: string,
|
|
174
|
+
text: string,
|
|
175
|
+
pressEnter = true
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const tempFile = `/tmp/claude-deck-send-${Date.now()}.txt`;
|
|
178
|
+
const bufferName = `send-${Date.now()}`;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
writeFileSync(tempFile, text);
|
|
182
|
+
await execAsync(`tmux load-buffer -b "${bufferName}" "${tempFile}"`);
|
|
183
|
+
await execAsync(`tmux paste-buffer -b "${bufferName}" -t "${sessionName}"`);
|
|
184
|
+
await execAsync(`tmux delete-buffer -b "${bufferName}"`).catch(() => {});
|
|
185
|
+
|
|
186
|
+
if (pressEnter) {
|
|
187
|
+
// Wait for Claude to process pasted text before sending Enter
|
|
188
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
189
|
+
await execAsync(`tmux send-keys -t "${sessionName}" Enter`);
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(tempFile);
|
|
194
|
+
} catch {}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// POST /api/sessions/[id]/summarize - Summarize and create fresh session
|
|
199
|
+
export async function POST(
|
|
200
|
+
request: NextRequest,
|
|
201
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
202
|
+
) {
|
|
203
|
+
try {
|
|
204
|
+
const { id } = await params;
|
|
205
|
+
const body = await request.json().catch(() => ({}));
|
|
206
|
+
const { createFork = true, sendContext = true } = body;
|
|
207
|
+
|
|
208
|
+
const session = (await queries.getSession(id)) as Session | undefined;
|
|
209
|
+
|
|
210
|
+
if (!session) {
|
|
211
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get tmux session name (pattern: {agent_type}-{id})
|
|
215
|
+
const tmuxSessionName = `${session.agent_type}-${id}`;
|
|
216
|
+
|
|
217
|
+
// Get actual working directory from tmux
|
|
218
|
+
const cwd =
|
|
219
|
+
(await getTmuxCwd(tmuxSessionName)) || session.working_directory;
|
|
220
|
+
const cwdExpanded =
|
|
221
|
+
cwd?.replace(/^~/, process.env.HOME || "~") || process.env.HOME || "~";
|
|
222
|
+
|
|
223
|
+
// Try to get full conversation from Claude's JSONL (only for Claude sessions)
|
|
224
|
+
let conversation: string | null = null;
|
|
225
|
+
if (session.agent_type === "claude") {
|
|
226
|
+
const claudeSessionId = await getClaudeSessionId(tmuxSessionName);
|
|
227
|
+
if (claudeSessionId && cwdExpanded) {
|
|
228
|
+
console.log(`[summarize] Found Claude session ID: ${claudeSessionId}`);
|
|
229
|
+
conversation = readClaudeSessionHistory(cwdExpanded, claudeSessionId);
|
|
230
|
+
if (conversation) {
|
|
231
|
+
console.log(
|
|
232
|
+
`[summarize] Read ${conversation.length} chars from JSONL`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Fallback to terminal scrollback for non-Claude or if JSONL not available
|
|
239
|
+
if (!conversation) {
|
|
240
|
+
console.log(
|
|
241
|
+
`[summarize] Using terminal scrollback for ${session.agent_type}`
|
|
242
|
+
);
|
|
243
|
+
conversation = await captureScrollback(tmuxSessionName);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!conversation || conversation.trim().length < 100) {
|
|
247
|
+
return NextResponse.json(
|
|
248
|
+
{ error: "No conversation found to summarize" },
|
|
249
|
+
{ status: 400 }
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Generate summary
|
|
254
|
+
const summary = await generateSummary(conversation);
|
|
255
|
+
|
|
256
|
+
// Create a new session with the summary as context
|
|
257
|
+
let newSession: Session | null = null;
|
|
258
|
+
if (createFork) {
|
|
259
|
+
const newId = randomUUID();
|
|
260
|
+
const newName = `${session.name} (fresh)`;
|
|
261
|
+
const agentType = session.agent_type || "claude";
|
|
262
|
+
const tmuxName = `${agentType}-${newId}`;
|
|
263
|
+
|
|
264
|
+
// Create new session in DB (using cwd already fetched above)
|
|
265
|
+
getDb()
|
|
266
|
+
.prepare(
|
|
267
|
+
`INSERT INTO sessions (id, name, tmux_name, working_directory, parent_session_id, model, initial_prompt, group_path, agent_type, auto_approve, project_id)
|
|
268
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
269
|
+
)
|
|
270
|
+
.run(
|
|
271
|
+
newId,
|
|
272
|
+
newName,
|
|
273
|
+
tmuxName,
|
|
274
|
+
cwd,
|
|
275
|
+
null,
|
|
276
|
+
session.model,
|
|
277
|
+
`Continue from previous session. Here's a summary of the work so far:\n\n${summary}`,
|
|
278
|
+
session.group_path,
|
|
279
|
+
agentType,
|
|
280
|
+
session.auto_approve ? 1 : 0,
|
|
281
|
+
session.project_id || "uncategorized"
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
newSession = (await queries.getSession(newId)) as Session;
|
|
285
|
+
const newTmuxSession = tmuxName;
|
|
286
|
+
|
|
287
|
+
// Start new tmux session with Claude directly
|
|
288
|
+
const isRoot = process.getuid?.() === 0;
|
|
289
|
+
const envPrefix = isRoot ? "IS_SANDBOX=1 " : "";
|
|
290
|
+
const claudeCmd = session.auto_approve
|
|
291
|
+
? `${envPrefix}claude --dangerously-skip-permissions`
|
|
292
|
+
: "claude";
|
|
293
|
+
|
|
294
|
+
const tmuxCmd = `tmux set -g mouse on 2>/dev/null; tmux new-session -d -s "${newTmuxSession}" -c "${cwdExpanded}" "${claudeCmd}"`;
|
|
295
|
+
console.log(`[summarize] Creating tmux session: ${tmuxCmd}`);
|
|
296
|
+
await execAsync(tmuxCmd);
|
|
297
|
+
console.log(`[summarize] Tmux session created: ${newTmuxSession}`);
|
|
298
|
+
|
|
299
|
+
// Give Claude a moment to start up before polling
|
|
300
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
301
|
+
|
|
302
|
+
// Wait for Claude to be ready and send context
|
|
303
|
+
if (sendContext) {
|
|
304
|
+
console.log(`[summarize] Waiting for Claude to be ready...`);
|
|
305
|
+
const ready = await waitForClaudeReady(newTmuxSession);
|
|
306
|
+
console.log(`[summarize] Claude ready: ${ready}`);
|
|
307
|
+
if (ready) {
|
|
308
|
+
const contextMessage = `Here's a summary of the previous session to continue from:\n\n${summary}\n\nPlease acknowledge you've received this context and are ready to continue.`;
|
|
309
|
+
console.log(
|
|
310
|
+
`[summarize] Sending context message (${contextMessage.length} chars)`
|
|
311
|
+
);
|
|
312
|
+
await sendToTmux(newTmuxSession, contextMessage, true);
|
|
313
|
+
console.log(`[summarize] Context sent!`);
|
|
314
|
+
} else {
|
|
315
|
+
console.log(
|
|
316
|
+
`[summarize] WARNING: Claude not ready, skipping context send`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return NextResponse.json({
|
|
323
|
+
summary,
|
|
324
|
+
newSession,
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error("Error summarizing session:", error);
|
|
328
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
329
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate an init script that shows the ClaudeDeck banner and configures tmux
|
|
8
|
+
*/
|
|
9
|
+
function generateInitScript(agentCommand: string): string {
|
|
10
|
+
return `#!/bin/bash
|
|
11
|
+
# ClaudeDeck Session Init Script
|
|
12
|
+
# Auto-generated - do not edit manually
|
|
13
|
+
|
|
14
|
+
# ANSI Colors (purple theme)
|
|
15
|
+
C_RESET=$'\\033[0m'
|
|
16
|
+
C_PURPLE=$'\\033[38;5;141m'
|
|
17
|
+
C_PURPLE2=$'\\033[38;5;177m'
|
|
18
|
+
C_PINK=$'\\033[38;5;213m'
|
|
19
|
+
C_MUTED=$'\\033[38;5;245m'
|
|
20
|
+
|
|
21
|
+
# Configure tmux status bar
|
|
22
|
+
tmux set-option status-style 'bg=#1e1e2e,fg=#cdd6f4' 2>/dev/null
|
|
23
|
+
tmux set-option status-left '#[fg=#cba6f7,bold] ClaudeDeck #[fg=#6c7086]| ' 2>/dev/null
|
|
24
|
+
tmux set-option status-left-length 20 2>/dev/null
|
|
25
|
+
tmux set-option status-right '#[fg=#6c7086]| #[fg=#89b4fa]#S #[fg=#6c7086]| #[fg=#a6adc8]%H:%M ' 2>/dev/null
|
|
26
|
+
tmux set-option status-right-length 40 2>/dev/null
|
|
27
|
+
tmux set-option status-position bottom 2>/dev/null
|
|
28
|
+
|
|
29
|
+
# Clear and show banner
|
|
30
|
+
clear
|
|
31
|
+
|
|
32
|
+
printf "\\n"
|
|
33
|
+
printf "\${C_PURPLE} _ _ ___ ____ \${C_RESET}\\n"
|
|
34
|
+
printf "\${C_PURPLE} / \\\\ __ _ ___ _ __ | |_ / _ \\\\/ ___| \${C_RESET}\\n"
|
|
35
|
+
printf "\${C_PURPLE2} / _ \\\\ / _\\\` |/ _ \\\\ '_ \\\\| __| | | \\\\___ \\\\ \${C_RESET}\\n"
|
|
36
|
+
printf "\${C_PURPLE2} / ___ \\\\ (_| | __/ | | | |_| |_| |___) |\${C_RESET}\\n"
|
|
37
|
+
printf "\${C_PINK} /_/ \\\\_\\\\__, |\\\\___|_| |_|\\\\__|\\\\___/|____/ \${C_RESET}\\n"
|
|
38
|
+
printf "\${C_PINK} |___/ \${C_RESET}\\n"
|
|
39
|
+
printf "\\n"
|
|
40
|
+
printf "\${C_MUTED} AI Coding Session Manager\${C_RESET}\\n"
|
|
41
|
+
printf "\\n"
|
|
42
|
+
|
|
43
|
+
# Brief pause to show banner
|
|
44
|
+
sleep 0.8
|
|
45
|
+
|
|
46
|
+
# Ensure ~/.local/bin is in PATH (where claude is installed)
|
|
47
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
48
|
+
|
|
49
|
+
# If running as root, set IS_SANDBOX=1 so Claude Code allows --dangerously-skip-permissions
|
|
50
|
+
if [ "$(id -u)" = "0" ]; then
|
|
51
|
+
export IS_SANDBOX=1
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Start the agent
|
|
55
|
+
exec ${agentCommand}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// POST /api/sessions/init-script - Create init script and return path
|
|
60
|
+
export async function POST(request: NextRequest) {
|
|
61
|
+
try {
|
|
62
|
+
const { agentCommand } = await request.json();
|
|
63
|
+
|
|
64
|
+
if (!agentCommand) {
|
|
65
|
+
return NextResponse.json(
|
|
66
|
+
{ error: "agentCommand is required" },
|
|
67
|
+
{ status: 400 }
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scriptContent = generateInitScript(agentCommand);
|
|
72
|
+
const scriptPath = path.join(os.tmpdir(), `claude-deck-init-${Date.now()}.sh`);
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
75
|
+
|
|
76
|
+
return NextResponse.json({ scriptPath, command: `bash ${scriptPath}` });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error("Error creating init script:", error);
|
|
79
|
+
return NextResponse.json(
|
|
80
|
+
{ error: "Failed to create init script" },
|
|
81
|
+
{ status: 500 }
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|