@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,173 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import type {
|
|
3
|
+
StreamMessage,
|
|
4
|
+
StreamMessageSystem,
|
|
5
|
+
StreamMessageAssistant,
|
|
6
|
+
StreamMessageResult,
|
|
7
|
+
ClientEvent,
|
|
8
|
+
TextContent,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export class StreamParser extends EventEmitter {
|
|
12
|
+
private buffer = "";
|
|
13
|
+
private sessionId: string;
|
|
14
|
+
|
|
15
|
+
constructor(sessionId: string) {
|
|
16
|
+
super();
|
|
17
|
+
this.sessionId = sessionId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Process incoming data chunk
|
|
21
|
+
write(chunk: string): void {
|
|
22
|
+
this.buffer += chunk;
|
|
23
|
+
|
|
24
|
+
// Process complete lines (NDJSON format)
|
|
25
|
+
const lines = this.buffer.split("\n");
|
|
26
|
+
this.buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
|
27
|
+
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (line.trim()) {
|
|
30
|
+
this.parseLine(line);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Flush any remaining buffer
|
|
36
|
+
end(): void {
|
|
37
|
+
if (this.buffer.trim()) {
|
|
38
|
+
this.parseLine(this.buffer);
|
|
39
|
+
}
|
|
40
|
+
this.buffer = "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private parseLine(line: string): void {
|
|
44
|
+
try {
|
|
45
|
+
const message: StreamMessage = JSON.parse(line);
|
|
46
|
+
const event = this.transformToClientEvent(message);
|
|
47
|
+
if (event) {
|
|
48
|
+
this.emit("event", event);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error("Failed to parse stream line:", line, err);
|
|
52
|
+
this.emit("parse_error", { type: "parse_error", line, error: err });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private transformToClientEvent(message: StreamMessage): ClientEvent | null {
|
|
57
|
+
const timestamp = new Date().toISOString();
|
|
58
|
+
|
|
59
|
+
switch (message.type) {
|
|
60
|
+
// Handle system init event
|
|
61
|
+
case "system": {
|
|
62
|
+
const sysMsg = message as StreamMessageSystem;
|
|
63
|
+
if (sysMsg.subtype === "init") {
|
|
64
|
+
return {
|
|
65
|
+
type: "init",
|
|
66
|
+
sessionId: this.sessionId,
|
|
67
|
+
timestamp,
|
|
68
|
+
data: { claudeSessionId: sysMsg.session_id || "" },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Handle assistant message (actual Claude response)
|
|
75
|
+
case "assistant": {
|
|
76
|
+
const assistantMsg = message as StreamMessageAssistant;
|
|
77
|
+
const msg = assistantMsg.message;
|
|
78
|
+
if (!msg?.content) return null;
|
|
79
|
+
|
|
80
|
+
const textBlocks = msg.content
|
|
81
|
+
.filter((c) => c.type === "text" && c.text)
|
|
82
|
+
.map((c) => c.text || "");
|
|
83
|
+
|
|
84
|
+
if (textBlocks.length > 0) {
|
|
85
|
+
return {
|
|
86
|
+
type: "text",
|
|
87
|
+
sessionId: this.sessionId,
|
|
88
|
+
timestamp,
|
|
89
|
+
data: {
|
|
90
|
+
role: msg.role || "assistant",
|
|
91
|
+
text: textBlocks.join(""),
|
|
92
|
+
content: msg.content.filter(
|
|
93
|
+
(c): c is TextContent => c.type === "text" && !!c.text
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Legacy message format (if used)
|
|
102
|
+
case "message": {
|
|
103
|
+
const textBlocks = message.content
|
|
104
|
+
.filter((c): c is TextContent => c.type === "text")
|
|
105
|
+
.map((c) => c.text);
|
|
106
|
+
|
|
107
|
+
if (textBlocks.length > 0) {
|
|
108
|
+
return {
|
|
109
|
+
type: "text",
|
|
110
|
+
sessionId: this.sessionId,
|
|
111
|
+
timestamp,
|
|
112
|
+
data: {
|
|
113
|
+
role: message.role,
|
|
114
|
+
text: textBlocks.join(""),
|
|
115
|
+
content: message.content,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case "tool_use":
|
|
123
|
+
return {
|
|
124
|
+
type: "tool_start",
|
|
125
|
+
sessionId: this.sessionId,
|
|
126
|
+
timestamp,
|
|
127
|
+
data: {
|
|
128
|
+
toolName: message.tool_name,
|
|
129
|
+
input: message.tool_input,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
case "tool_result":
|
|
134
|
+
return {
|
|
135
|
+
type: "tool_end",
|
|
136
|
+
sessionId: this.sessionId,
|
|
137
|
+
timestamp,
|
|
138
|
+
data: {
|
|
139
|
+
toolName: message.tool_name,
|
|
140
|
+
output: message.output,
|
|
141
|
+
status: message.status,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
case "result": {
|
|
146
|
+
const resultMsg = message as StreamMessageResult;
|
|
147
|
+
if (resultMsg.subtype === "success" || resultMsg.status === "success") {
|
|
148
|
+
return {
|
|
149
|
+
type: "complete",
|
|
150
|
+
sessionId: this.sessionId,
|
|
151
|
+
timestamp,
|
|
152
|
+
data: {
|
|
153
|
+
durationMs: resultMsg.duration_ms,
|
|
154
|
+
output: resultMsg.result || resultMsg.output,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
} else {
|
|
158
|
+
return {
|
|
159
|
+
type: "error",
|
|
160
|
+
sessionId: this.sessionId,
|
|
161
|
+
timestamp,
|
|
162
|
+
data: {
|
|
163
|
+
error: resultMsg.error || "Unknown error",
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
default:
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Stream-JSON message types from Claude CLI
|
|
2
|
+
|
|
3
|
+
export interface StreamMessageInit {
|
|
4
|
+
type: "init";
|
|
5
|
+
session_id: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface StreamMessageSystem {
|
|
10
|
+
type: "system";
|
|
11
|
+
subtype: "init" | string;
|
|
12
|
+
session_id?: string;
|
|
13
|
+
timestamp?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface StreamMessageAssistant {
|
|
17
|
+
type: "assistant";
|
|
18
|
+
message: {
|
|
19
|
+
role: string;
|
|
20
|
+
content: Array<{ type: string; text?: string }>;
|
|
21
|
+
};
|
|
22
|
+
timestamp?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StreamMessageContent {
|
|
26
|
+
type: "message";
|
|
27
|
+
role: "assistant" | "user";
|
|
28
|
+
content: ContentBlock[];
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StreamMessageToolUse {
|
|
33
|
+
type: "tool_use";
|
|
34
|
+
tool_name: string;
|
|
35
|
+
tool_input: Record<string, unknown>;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface StreamMessageToolResult {
|
|
40
|
+
type: "tool_result";
|
|
41
|
+
tool_name: string;
|
|
42
|
+
output: string;
|
|
43
|
+
status: "success" | "error";
|
|
44
|
+
timestamp: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface StreamMessageResult {
|
|
48
|
+
type: "result";
|
|
49
|
+
subtype?: "success" | "error";
|
|
50
|
+
status?: "success" | "error";
|
|
51
|
+
duration_ms?: number;
|
|
52
|
+
result?: string;
|
|
53
|
+
output?: string;
|
|
54
|
+
error?: string;
|
|
55
|
+
timestamp?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type StreamMessage =
|
|
59
|
+
| StreamMessageInit
|
|
60
|
+
| StreamMessageSystem
|
|
61
|
+
| StreamMessageAssistant
|
|
62
|
+
| StreamMessageContent
|
|
63
|
+
| StreamMessageToolUse
|
|
64
|
+
| StreamMessageToolResult
|
|
65
|
+
| StreamMessageResult;
|
|
66
|
+
|
|
67
|
+
// Content block types
|
|
68
|
+
export interface TextContent {
|
|
69
|
+
type: "text";
|
|
70
|
+
text: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface ToolUseContent {
|
|
74
|
+
type: "tool_use";
|
|
75
|
+
id: string;
|
|
76
|
+
name: string;
|
|
77
|
+
input: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ToolResultContent {
|
|
81
|
+
type: "tool_result";
|
|
82
|
+
tool_use_id: string;
|
|
83
|
+
content: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type ContentBlock = TextContent | ToolUseContent | ToolResultContent;
|
|
87
|
+
|
|
88
|
+
// Client events (sent to WebSocket clients)
|
|
89
|
+
export interface ClientEventInit {
|
|
90
|
+
type: "init";
|
|
91
|
+
sessionId: string;
|
|
92
|
+
timestamp: string;
|
|
93
|
+
data: { claudeSessionId: string };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ClientEventText {
|
|
97
|
+
type: "text";
|
|
98
|
+
sessionId: string;
|
|
99
|
+
timestamp: string;
|
|
100
|
+
data: { role: string; text: string; content: ContentBlock[] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ClientEventToolStart {
|
|
104
|
+
type: "tool_start";
|
|
105
|
+
sessionId: string;
|
|
106
|
+
timestamp: string;
|
|
107
|
+
data: { toolName: string; input: Record<string, unknown> };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ClientEventToolEnd {
|
|
111
|
+
type: "tool_end";
|
|
112
|
+
sessionId: string;
|
|
113
|
+
timestamp: string;
|
|
114
|
+
data: { toolName: string; output: string; status: string };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ClientEventComplete {
|
|
118
|
+
type: "complete";
|
|
119
|
+
sessionId: string;
|
|
120
|
+
timestamp: string;
|
|
121
|
+
data: { durationMs?: number; output?: string };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ClientEventError {
|
|
125
|
+
type: "error";
|
|
126
|
+
sessionId: string;
|
|
127
|
+
timestamp: string;
|
|
128
|
+
data: { error: string };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ClientEventStatus {
|
|
132
|
+
type: "status";
|
|
133
|
+
sessionId: string;
|
|
134
|
+
timestamp: string;
|
|
135
|
+
data: { status: string; exitCode?: number };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export type ClientEvent =
|
|
139
|
+
| ClientEventInit
|
|
140
|
+
| ClientEventText
|
|
141
|
+
| ClientEventToolStart
|
|
142
|
+
| ClientEventToolEnd
|
|
143
|
+
| ClientEventComplete
|
|
144
|
+
| ClientEventError
|
|
145
|
+
| ClientEventStatus;
|
|
146
|
+
|
|
147
|
+
// Session options
|
|
148
|
+
export interface ClaudeSessionOptions {
|
|
149
|
+
model?: string;
|
|
150
|
+
workingDirectory?: string;
|
|
151
|
+
resume?: boolean; // Use --continue or --resume
|
|
152
|
+
claudeSessionId?: string; // For --resume
|
|
153
|
+
systemPrompt?: string;
|
|
154
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { watch } from "chokidar";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { WebSocket } from "ws";
|
|
5
|
+
import { invalidateProject, invalidateAll } from "./jsonl-cache";
|
|
6
|
+
|
|
7
|
+
const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
|
|
8
|
+
|
|
9
|
+
const updateClients = new Set<WebSocket>();
|
|
10
|
+
|
|
11
|
+
export function addUpdateClient(ws: WebSocket): void {
|
|
12
|
+
updateClients.add(ws);
|
|
13
|
+
ws.on("close", () => updateClients.delete(ws));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function broadcast(msg: object): void {
|
|
17
|
+
const data = JSON.stringify(msg);
|
|
18
|
+
for (const ws of updateClients) {
|
|
19
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
20
|
+
ws.send(data);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
26
|
+
|
|
27
|
+
function handleFileChange(filePath: string): void {
|
|
28
|
+
const relative = path.relative(CLAUDE_PROJECTS_DIR, filePath);
|
|
29
|
+
const projectName = relative.split(path.sep)[0];
|
|
30
|
+
if (!projectName) return;
|
|
31
|
+
|
|
32
|
+
const existing = debounceTimers.get(projectName);
|
|
33
|
+
if (existing) clearTimeout(existing);
|
|
34
|
+
|
|
35
|
+
debounceTimers.set(
|
|
36
|
+
projectName,
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
debounceTimers.delete(projectName);
|
|
39
|
+
invalidateProject(projectName);
|
|
40
|
+
broadcast({ type: "project-updated", projectName });
|
|
41
|
+
}, 150)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function startWatcher(): void {
|
|
46
|
+
try {
|
|
47
|
+
const watcher = watch(CLAUDE_PROJECTS_DIR, {
|
|
48
|
+
ignoreInitial: true,
|
|
49
|
+
depth: 2,
|
|
50
|
+
ignored: [/node_modules/, /\.git/, /subagents/],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
watcher.on("change", handleFileChange);
|
|
54
|
+
watcher.on("add", (fp) => {
|
|
55
|
+
handleFileChange(fp);
|
|
56
|
+
const relative = path.relative(CLAUDE_PROJECTS_DIR, fp);
|
|
57
|
+
if (!relative.includes(path.sep)) {
|
|
58
|
+
invalidateAll();
|
|
59
|
+
broadcast({ type: "projects-changed" });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
watcher.on("addDir", () => {
|
|
63
|
+
invalidateAll();
|
|
64
|
+
broadcast({ type: "projects-changed" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log("> File watcher started on ~/.claude/projects/");
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("Failed to start file watcher:", err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side registry for preserving terminal state across navigation
|
|
3
|
+
* Stores scroll positions and other ephemeral state that should persist
|
|
4
|
+
* when switching between tabs/sessions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface TerminalState {
|
|
8
|
+
scrollTop: number;
|
|
9
|
+
scrollHeight: number;
|
|
10
|
+
lastActivity: number;
|
|
11
|
+
cursorY: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SessionEntry {
|
|
15
|
+
tabId: string;
|
|
16
|
+
sessionId?: string;
|
|
17
|
+
attachedTmux?: string;
|
|
18
|
+
terminalState?: TerminalState;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class SessionRegistry {
|
|
22
|
+
private sessions: Map<string, SessionEntry> = new Map();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a unique key for a pane+tab combination
|
|
26
|
+
*/
|
|
27
|
+
private getKey(paneId: string, tabId: string): string {
|
|
28
|
+
return `${paneId}:${tabId}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register or update a session entry
|
|
33
|
+
*/
|
|
34
|
+
register(
|
|
35
|
+
paneId: string,
|
|
36
|
+
tabId: string,
|
|
37
|
+
data: Partial<Omit<SessionEntry, "tabId">>
|
|
38
|
+
): void {
|
|
39
|
+
const key = this.getKey(paneId, tabId);
|
|
40
|
+
const existing = this.sessions.get(key);
|
|
41
|
+
|
|
42
|
+
this.sessions.set(key, {
|
|
43
|
+
tabId,
|
|
44
|
+
...existing,
|
|
45
|
+
...data,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get session entry
|
|
51
|
+
*/
|
|
52
|
+
get(paneId: string, tabId: string): SessionEntry | undefined {
|
|
53
|
+
return this.sessions.get(this.getKey(paneId, tabId));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Save terminal state (scroll position, cursor, etc.)
|
|
58
|
+
*/
|
|
59
|
+
saveTerminalState(paneId: string, tabId: string, state: TerminalState): void {
|
|
60
|
+
const key = this.getKey(paneId, tabId);
|
|
61
|
+
const existing = this.sessions.get(key);
|
|
62
|
+
|
|
63
|
+
this.sessions.set(key, {
|
|
64
|
+
tabId,
|
|
65
|
+
...existing,
|
|
66
|
+
terminalState: state,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get saved terminal state
|
|
72
|
+
*/
|
|
73
|
+
getTerminalState(paneId: string, tabId: string): TerminalState | undefined {
|
|
74
|
+
return this.sessions.get(this.getKey(paneId, tabId))?.terminalState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Remove session entry
|
|
79
|
+
*/
|
|
80
|
+
remove(paneId: string, tabId: string): void {
|
|
81
|
+
this.sessions.delete(this.getKey(paneId, tabId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Clear all entries for a pane
|
|
86
|
+
*/
|
|
87
|
+
clearPane(paneId: string): void {
|
|
88
|
+
for (const key of this.sessions.keys()) {
|
|
89
|
+
if (key.startsWith(`${paneId}:`)) {
|
|
90
|
+
this.sessions.delete(key);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear all entries
|
|
97
|
+
*/
|
|
98
|
+
clear(): void {
|
|
99
|
+
this.sessions.clear();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get count of active sessions
|
|
104
|
+
*/
|
|
105
|
+
get size(): number {
|
|
106
|
+
return this.sessions.size;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Singleton instance
|
|
111
|
+
export const sessionRegistry = new SessionRegistry();
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if ripgrep is available on the system
|
|
5
|
+
*/
|
|
6
|
+
export function isRipgrepAvailable(): boolean {
|
|
7
|
+
try {
|
|
8
|
+
execSync("which rg", { encoding: "utf-8", stdio: "pipe" });
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SearchOptions {
|
|
16
|
+
maxResults?: number;
|
|
17
|
+
contextLines?: number;
|
|
18
|
+
filePattern?: string;
|
|
19
|
+
caseSensitive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SearchMatch {
|
|
23
|
+
type: "match";
|
|
24
|
+
data: {
|
|
25
|
+
path: { text: string };
|
|
26
|
+
lines: { text: string };
|
|
27
|
+
line_number: number;
|
|
28
|
+
absolute_offset: number;
|
|
29
|
+
submatches: Array<{ match: { text: string }; start: number; end: number }>;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FormattedMatch {
|
|
34
|
+
file: string;
|
|
35
|
+
line: number;
|
|
36
|
+
column: number;
|
|
37
|
+
matchText: string;
|
|
38
|
+
lineText: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function searchCode(
|
|
42
|
+
workingDir: string,
|
|
43
|
+
query: string,
|
|
44
|
+
options: SearchOptions = {}
|
|
45
|
+
): SearchMatch[] {
|
|
46
|
+
const {
|
|
47
|
+
maxResults = 100,
|
|
48
|
+
contextLines = 2,
|
|
49
|
+
filePattern = "*",
|
|
50
|
+
caseSensitive = false,
|
|
51
|
+
} = options;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Use spawn instead of execSync for better control
|
|
55
|
+
const { spawnSync } = require("child_process");
|
|
56
|
+
|
|
57
|
+
const args = [
|
|
58
|
+
"--json",
|
|
59
|
+
`--max-count=${Math.ceil(maxResults / 10)}`,
|
|
60
|
+
`--context=${contextLines}`,
|
|
61
|
+
"--ignore-case",
|
|
62
|
+
query,
|
|
63
|
+
".", // CRITICAL: Tell ripgrep to search current directory explicitly
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const result = spawnSync("rg", args, {
|
|
67
|
+
cwd: workingDir,
|
|
68
|
+
encoding: "utf-8",
|
|
69
|
+
timeout: 10000,
|
|
70
|
+
maxBuffer: 1024 * 1024 * 5,
|
|
71
|
+
stdio: ["ignore", "pipe", "pipe"], // Ignore stdin so ripgrep doesn't wait for it
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (result.error) {
|
|
75
|
+
throw result.error;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Status 1 = no matches (not an error for ripgrep)
|
|
79
|
+
if (result.status !== 0 && result.status !== 1) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const output = result.stdout || "";
|
|
84
|
+
const matches: SearchMatch[] = [];
|
|
85
|
+
const lines = output.trim().split("\n").filter(Boolean);
|
|
86
|
+
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(line);
|
|
90
|
+
if (parsed.type === "match") {
|
|
91
|
+
matches.push(parsed);
|
|
92
|
+
if (matches.length >= maxResults) break;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return matches;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Error in searchCode:", error);
|
|
102
|
+
// ENOENT = command not found
|
|
103
|
+
if ((error as any).code === "ENOENT") {
|
|
104
|
+
throw new Error(
|
|
105
|
+
"ripgrep (rg) not found. Install with: brew install ripgrep"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
// Other errors - return empty
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatSearchResults(matches: SearchMatch[]): FormattedMatch[] {
|
|
114
|
+
return matches.map((match) => ({
|
|
115
|
+
file: match.data.path.text,
|
|
116
|
+
line: match.data.line_number,
|
|
117
|
+
column: match.data.submatches[0]?.start || 0,
|
|
118
|
+
matchText: match.data.submatches[0]?.match.text || "",
|
|
119
|
+
lineText: match.data.lines.text,
|
|
120
|
+
}));
|
|
121
|
+
}
|
package/lib/db/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { createSchema } from "./schema";
|
|
5
|
+
import { runMigrations } from "./migrations";
|
|
6
|
+
|
|
7
|
+
export * from "./types";
|
|
8
|
+
export { queries } from "./queries";
|
|
9
|
+
|
|
10
|
+
const DB_DIR = path.join(os.homedir(), ".claude-deck");
|
|
11
|
+
const DB_PATH = process.env.DB_PATH || path.join(DB_DIR, "data.db");
|
|
12
|
+
|
|
13
|
+
let _db: Database.Database | null = null;
|
|
14
|
+
|
|
15
|
+
export function getDb(): Database.Database {
|
|
16
|
+
if (!_db) {
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
19
|
+
|
|
20
|
+
_db = new Database(DB_PATH);
|
|
21
|
+
_db.pragma("journal_mode = WAL");
|
|
22
|
+
_db.pragma("foreign_keys = ON");
|
|
23
|
+
_db.pragma("busy_timeout = 5000");
|
|
24
|
+
}
|
|
25
|
+
return _db;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let _initialized = false;
|
|
29
|
+
|
|
30
|
+
export async function initDb(): Promise<Database.Database> {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
|
|
33
|
+
if (!_initialized) {
|
|
34
|
+
createSchema(db);
|
|
35
|
+
runMigrations(db);
|
|
36
|
+
_initialized = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return db;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function closeDb(): Promise<void> {
|
|
43
|
+
if (_db) {
|
|
44
|
+
_db.close();
|
|
45
|
+
_db = null;
|
|
46
|
+
_initialized = false;
|
|
47
|
+
}
|
|
48
|
+
}
|