ccdock 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cchub contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # ccdock
2
+
3
+ A TUI sidebar to orchestrate VS Code windows and track Claude Code agents.
4
+
5
+ ## Why?
6
+
7
+ Running multiple Claude Code agents across git worktrees is powerful — but managing the VS Code windows that go with them is a nightmare. You end up Alt-Tabbing through a dozen windows, losing track of which agent is doing what, and manually arranging editors every time you switch context.
8
+
9
+ Existing "hub" tools either force you into a CLI-only workflow or require a proprietary editor. But you already have VS Code. You just need something to **keep it organized**.
10
+
11
+ ccdock sits in a narrow terminal sidebar and takes care of the rest: auto-positioning VS Code windows, tracking every Claude Code agent in real time, and letting you switch between sessions with a single click.
12
+
13
+ ## Features
14
+
15
+ - **VS Code orchestration** — Auto-open, position, and switch VS Code (or Cursor) windows next to the sidebar. Click a session, and the right editor snaps into focus.
16
+ - **Real-time agent monitoring** — See exactly what each Claude Code agent is doing: which tool it's calling, what file it's reading, what command it's running.
17
+ - **Git worktree management** — Create, switch, and delete worktrees via [git-wt](https://github.com/k1LoW/git-wt) integration. Each worktree gets its own session.
18
+ - **Activity log** — Live feed of tool invocations with session numbers (#N) across all active agents.
19
+ - **Mouse + keyboard** — Click to select sessions, scroll wheel to navigate, or use vim-style `j`/`k` keys.
20
+ - **Auto-layout** — VS Code windows automatically resize and reposition when the terminal resizes.
21
+
22
+ ## Requirements
23
+
24
+ - **macOS** (uses AppleScript for window management)
25
+ - [Bun](https://bun.sh/) runtime (v1.0+)
26
+ - [VS Code](https://code.visualstudio.com/) or [Cursor](https://cursor.sh/)
27
+ - [git-wt](https://github.com/k1LoW/git-wt) for worktree creation (`go install github.com/k1LoW/git-wt@latest`)
28
+ - [Ghostty](https://ghostty.org/) terminal (used for sidebar window detection)
29
+ - A terminal font with [Nerd Font](https://www.nerdfonts.com/) support (for icons)
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ # ccdock itself
35
+ bun install -g ccdock
36
+
37
+ # git-wt (required for worktree creation)
38
+ go install github.com/k1LoW/git-wt@latest
39
+ ```
40
+
41
+ ## Setup
42
+
43
+ ### 1. Configure workspace directories
44
+
45
+ Edit `~/.config/ccdock/config.json` (auto-created on first run):
46
+
47
+ ```json
48
+ {
49
+ "workspace_dirs": ["~/workspace"],
50
+ "editor": "code"
51
+ }
52
+ ```
53
+
54
+ | Key | Description |
55
+ | ---------------- | --------------------------------------------------------- |
56
+ | `workspace_dirs` | Directories to scan for git repositories |
57
+ | `editor` | Editor command: `"code"` for VS Code, `"cursor"` for Cursor |
58
+
59
+ ### 2. Set up Claude Code hooks
60
+
61
+ Add to `~/.claude/settings.json` to enable agent status monitoring:
62
+
63
+ ```json
64
+ {
65
+ "hooks": {
66
+ "PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PreToolUse" }] }],
67
+ "PostToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PostToolUse" }] }],
68
+ "PermissionRequest": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code PermissionRequest" }] }],
69
+ "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code Stop" }] }],
70
+ "Notification": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code Notification" }] }],
71
+ "SessionEnd": [{ "matcher": "", "hooks": [{ "type": "command", "command": "ccdock hook claude-code SessionEnd" }] }]
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Usage
77
+
78
+ ```sh
79
+ ccdock # start the sidebar TUI
80
+ ccdock help # show help
81
+ ```
82
+
83
+ ### Keybindings
84
+
85
+ | Key | Action |
86
+ | ------------ | --------------------------------------- |
87
+ | `j` / `k` | Navigate between sessions |
88
+ | `Enter` | Focus editor window for selected session |
89
+ | `Tab` | Focus editor window (same as Enter) |
90
+ | `n` | Create new session (interactive wizard) |
91
+ | `d` | Delete session |
92
+ | `r` | Realign all VS Code windows |
93
+ | `c` | Toggle compact mode |
94
+ | `l` | Toggle activity log |
95
+ | `q` / Ctrl+C | Quit (with option to close editors) |
96
+ | Mouse click | Select session |
97
+ | Scroll wheel | Navigate between sessions |
98
+
99
+ ### Session card states
100
+
101
+ | Card appearance | Meaning |
102
+ | --------------- | ------- |
103
+ | White border + green `●` | Editor is focused |
104
+ | Normal border + green `●` | Editor is open but not focused |
105
+ | Spinning `⠋` indicator | Editor is launching |
106
+ | Dim border, no dot | Editor is closed |
107
+
108
+ ### Agent status
109
+
110
+ | Icon | Status | Description |
111
+ | ---- | ------ | ----------- |
112
+ | `●` green | running | Agent is executing tools |
113
+ | `●`/`○` yellow pulse | waiting | Awaiting user permission |
114
+ | `○` teal | idle | Agent is ready |
115
+
116
+ ## How it works
117
+
118
+ ### Architecture
119
+
120
+ ```
121
+ Claude Code hooks --> ccdock hook --> writes agent JSON files
122
+ |
123
+ ccdock sidebar (polls every 2s) <----------+
124
+ |
125
+ +--> reads session + agent state files
126
+ +--> queries VS Code windows via AppleScript
127
+ +--> renders TUI with merged state
128
+ ```
129
+
130
+ - **State** — `~/.local/state/ccdock/` stores session and agent state as JSON files
131
+ - **Hooks** — `ccdock hook` writes agent state files when Claude Code fires events
132
+ - **Window management** — AppleScript via `osascript` to position VS Code next to the sidebar
133
+ - **Wizard** — `n` key scans workspace dirs, offers create/existing/root worktree options via `git wt`
134
+
135
+ ### File structure
136
+
137
+ ```
138
+ src/
139
+ main.ts — CLI entry point
140
+ sidebar.ts — Main event loop, input handling
141
+ types.ts — Type definitions
142
+ config/config.ts — Config (~/.config/ccdock/)
143
+ workspace/state.ts — Session/agent state persistence
144
+ workspace/editor.ts — VS Code open/focus
145
+ workspace/window.ts — AppleScript window management
146
+ worktree/manager.ts — Git worktree operations
147
+ worktree/scanner.ts — Repository discovery
148
+ tui/render.ts — Sidebar rendering
149
+ tui/wizard.ts — Session wizard rendering
150
+ tui/input.ts — Keyboard input parsing
151
+ tui/ansi.ts — ANSI escape codes
152
+ agent/hook.ts — Hook handler
153
+ ```
154
+
155
+ ## Development
156
+
157
+ ```sh
158
+ # Clone
159
+ git clone https://github.com/shibutani/ccdock.git
160
+ cd ccdock
161
+ bun install
162
+
163
+ # Run directly
164
+ bun run dev
165
+
166
+ # Type check
167
+ bun run typecheck
168
+
169
+ # Format
170
+ bun run format
171
+
172
+ # Build standalone binary (optional)
173
+ bun run build
174
+ ```
175
+
176
+ ### Project structure
177
+
178
+ The project uses [Bun](https://bun.sh/) as runtime and [Biome](https://biomejs.dev/) for formatting.
179
+
180
+ - `src/main.ts` — CLI entry point, routes `start` / `hook` / `help` commands
181
+ - `src/sidebar.ts` — Main event loop: keyboard input, timers, state refresh
182
+ - `src/tui/` — Terminal UI rendering (cards, wizard, input parsing, ANSI codes)
183
+ - `src/workspace/` — File-based state, AppleScript window management, editor control
184
+ - `src/worktree/` — Git worktree operations and repository scanning
185
+ - `src/agent/` — Claude Code hook handler
186
+
187
+ ### Publishing
188
+
189
+ ```sh
190
+ npm publish
191
+ ```
192
+
193
+ ## License
194
+
195
+ MIT
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "ccdock",
3
+ "version": "0.1.0",
4
+ "description": "A TUI sidebar to orchestrate VS Code windows and track Claude Code agents.",
5
+ "type": "module",
6
+ "bin": {
7
+ "ccdock": "src/main.ts"
8
+ },
9
+ "scripts": {
10
+ "dev": "bun run src/main.ts",
11
+ "build": "bun build --compile src/main.ts --outfile ccdock",
12
+ "typecheck": "bunx tsc --noEmit",
13
+ "format": "bunx @biomejs/biome format --write src/"
14
+ },
15
+ "keywords": [
16
+ "claude-code",
17
+ "worktree",
18
+ "git",
19
+ "tui",
20
+ "sidebar",
21
+ "vscode",
22
+ "terminal"
23
+ ],
24
+ "author": "shibutani",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/shibukazu/ccdock"
29
+ },
30
+ "engines": {
31
+ "bun": ">=1.0.0"
32
+ },
33
+ "os": [
34
+ "darwin"
35
+ ],
36
+ "files": [
37
+ "src/**/*.ts",
38
+ "LICENSE",
39
+ "README.md"
40
+ ],
41
+ "devDependencies": {
42
+ "@types/bun": "latest",
43
+ "@biomejs/biome": "^1.9.0"
44
+ },
45
+ "peerDependencies": {
46
+ "typescript": "^5"
47
+ }
48
+ }
@@ -0,0 +1,134 @@
1
+ import type { AgentState, AgentType } from "../types.ts";
2
+ import {
3
+ findSessionByPath,
4
+ readAgentState,
5
+ removeAgentFile,
6
+ writeAgentState,
7
+ } from "../workspace/state.ts";
8
+
9
+ function sanitize(path: string): string {
10
+ return path.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 100);
11
+ }
12
+
13
+ function extractToolDetail(toolName: string, toolInput: Record<string, unknown>): string {
14
+ switch (toolName) {
15
+ case "Bash":
16
+ return (toolInput.command as string) ?? "";
17
+ case "Read":
18
+ case "Write":
19
+ return shortenPath((toolInput.file_path as string) ?? "");
20
+ case "Edit":
21
+ return shortenPath((toolInput.file_path as string) ?? "");
22
+ case "Grep":
23
+ return `/${(toolInput.pattern as string) ?? ""}/`;
24
+ case "Glob":
25
+ return (toolInput.pattern as string) ?? "";
26
+ case "Agent":
27
+ case "Task":
28
+ return (toolInput.description as string) ?? (toolInput.prompt as string)?.slice(0, 80) ?? "";
29
+ case "WebSearch":
30
+ return (toolInput.query as string) ?? "";
31
+ case "WebFetch":
32
+ return (toolInput.url as string) ?? "";
33
+ default:
34
+ return "";
35
+ }
36
+ }
37
+
38
+ function shortenPath(path: string): string {
39
+ const home = process.env.HOME ?? "";
40
+ if (home && path.startsWith(home)) {
41
+ return `~${path.slice(home.length)}`;
42
+ }
43
+ return path;
44
+ }
45
+
46
+ function buildPrompt(eventName: string, payload: Record<string, unknown>): string {
47
+ if (eventName === "PreToolUse" || eventName === "PostToolUse") {
48
+ const toolName = (payload.tool_name as string) ?? "";
49
+ return `[${eventName}] ${toolName}`;
50
+ }
51
+ if (eventName === "Stop") {
52
+ const reason = (payload.stop_reason as string) ?? "completed";
53
+ return `[Stop] ${reason}`;
54
+ }
55
+ return `[${eventName}]`;
56
+ }
57
+
58
+ const STATUS_MAP: Record<string, string> = {
59
+ PreToolUse: "running",
60
+ PostToolUse: "running",
61
+ SubagentToolUse: "running",
62
+ PermissionRequest: "waiting",
63
+ Stop: "idle",
64
+ SessionEnd: "remove",
65
+ };
66
+
67
+ export async function handleHook(agentType: string, eventName: string): Promise<void> {
68
+ let payload: Record<string, unknown> = {};
69
+ try {
70
+ const input = await Bun.stdin.text();
71
+ if (input.trim()) {
72
+ payload = JSON.parse(input) as Record<string, unknown>;
73
+ }
74
+ } catch {
75
+ // No stdin or invalid JSON - continue with empty payload
76
+ }
77
+
78
+ const cwd = (payload.cwd as string) ?? process.cwd();
79
+ const claudeSessionId = payload.session_id as string | undefined;
80
+
81
+ if (!claudeSessionId) {
82
+ return;
83
+ }
84
+
85
+ const filename = `${sanitize(cwd)}-${claudeSessionId}.json`;
86
+
87
+ const mappedStatus = STATUS_MAP[eventName];
88
+
89
+ if (mappedStatus === "remove") {
90
+ removeAgentFile(filename);
91
+ return;
92
+ }
93
+
94
+ // Notification doesn't carry meaningful status info — preserve previous state
95
+ if (eventName === "Notification") {
96
+ const prev = readAgentState(filename);
97
+ if (prev) {
98
+ prev.updatedAt = Date.now();
99
+ writeAgentState(prev, filename);
100
+ }
101
+ return;
102
+ }
103
+
104
+ const status = mappedStatus ?? "unknown";
105
+ const sessionId = findSessionByPath(cwd);
106
+ const rawToolName = (payload.tool_name as string) ?? "";
107
+ const toolInput = (payload.tool_input as Record<string, unknown>) ?? {};
108
+ const rawToolDetail = extractToolDetail(rawToolName, toolInput);
109
+
110
+ // Preserve previous toolName/toolDetail only for events that don't carry tool info
111
+ // but clear them on Stop (idle) since the agent is no longer doing anything
112
+ let toolName = rawToolName;
113
+ let toolDetail = rawToolDetail;
114
+ if (!toolName && status !== "idle") {
115
+ const prev = readAgentState(filename);
116
+ if (prev) {
117
+ toolName = prev.toolName ?? "";
118
+ toolDetail = prev.toolDetail ?? "";
119
+ }
120
+ }
121
+
122
+ const state: AgentState = {
123
+ sessionId,
124
+ agentType: agentType as AgentType,
125
+ status: status as AgentState["status"],
126
+ prompt: buildPrompt(eventName, payload),
127
+ toolName,
128
+ toolDetail,
129
+ cwd,
130
+ updatedAt: Date.now(),
131
+ };
132
+
133
+ writeAgentState(state, filename);
134
+ }
@@ -0,0 +1,58 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { HubConfig } from "../types.ts";
4
+
5
+ const CONFIG_DIR_NAME = "ccdock";
6
+
7
+ function getConfigDir(): string {
8
+ const home = process.env.HOME ?? "";
9
+ const configBase = process.env.XDG_CONFIG_HOME ?? join(home, ".config");
10
+ return join(configBase, CONFIG_DIR_NAME);
11
+ }
12
+
13
+ function getConfigPath(): string {
14
+ return join(getConfigDir(), "config.json");
15
+ }
16
+
17
+ function expandTilde(p: string): string {
18
+ const home = process.env.HOME ?? "";
19
+ if (p.startsWith("~/")) {
20
+ return join(home, p.slice(2));
21
+ }
22
+ return p;
23
+ }
24
+
25
+ const DEFAULT_CONFIG: HubConfig = {
26
+ workspace_dirs: ["~/workspace"],
27
+ editor: "code",
28
+ };
29
+
30
+ export function loadConfig(): HubConfig {
31
+ const configDir = getConfigDir();
32
+ const configPath = getConfigPath();
33
+
34
+ // Auto-create config directory and file on first run
35
+ if (!existsSync(configDir)) {
36
+ mkdirSync(configDir, { recursive: true });
37
+ }
38
+
39
+ if (!existsSync(configPath)) {
40
+ writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
41
+ return resolveConfig(DEFAULT_CONFIG);
42
+ }
43
+
44
+ try {
45
+ const raw = readFileSync(configPath, "utf-8");
46
+ const parsed = JSON.parse(raw) as HubConfig;
47
+ return resolveConfig(parsed);
48
+ } catch {
49
+ return resolveConfig(DEFAULT_CONFIG);
50
+ }
51
+ }
52
+
53
+ function resolveConfig(config: HubConfig): HubConfig {
54
+ return {
55
+ workspace_dirs: config.workspace_dirs.map(expandTilde).filter((dir) => existsSync(dir)),
56
+ editor: config.editor ?? "code",
57
+ };
58
+ }
package/src/main.ts ADDED
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bun
2
+ import { handleHook } from "./agent/hook.ts";
3
+ import { runSidebar } from "./sidebar.ts";
4
+
5
+ function printHelp(): void {
6
+ const help = `
7
+ ccdock - TUI sidebar for managing git worktree development sessions
8
+
9
+ USAGE:
10
+ ccdock [command]
11
+
12
+ COMMANDS:
13
+ start Start the sidebar TUI (default)
14
+ hook Handle agent hook events (called by Claude Code hooks)
15
+ help Show this help message
16
+
17
+ HOOK USAGE:
18
+ ccdock hook <agent-type> <event-name>
19
+
20
+ Agent types: claude-code, codex
21
+ Events: PreToolUse, PostToolUse, Stop, session.end, Notification
22
+
23
+ KEYBINDINGS (sidebar):
24
+ j/k Navigate sessions
25
+ Enter/Tab Focus editor window for selected session
26
+ n Create new session (wizard)
27
+ d Delete session
28
+ c Toggle compact mode
29
+ l Toggle activity log
30
+ q/Ctrl+C Quit sidebar
31
+
32
+ CONFIG:
33
+ ~/.config/ccdock/config.json
34
+
35
+ STATE:
36
+ ~/.local/state/ccdock/
37
+ `.trim();
38
+
39
+ console.log(help);
40
+ }
41
+
42
+ async function main(): Promise<void> {
43
+ const [command, ...args] = process.argv.slice(2);
44
+
45
+ switch (command) {
46
+ case "start":
47
+ case undefined:
48
+ await runSidebar();
49
+ break;
50
+ case "hook":
51
+ await handleHook(args[0] ?? "claude-code", args[1] ?? "unknown");
52
+ break;
53
+ case "help":
54
+ case "--help":
55
+ case "-h":
56
+ printHelp();
57
+ break;
58
+ default:
59
+ console.error(`Unknown command: ${command}`);
60
+ printHelp();
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ await main();