cc-sidebar 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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * IPC Server for sidebar communication
3
+ * Uses Unix domain sockets for real-time updates
4
+ */
5
+
6
+ import { unlinkSync, existsSync } from "fs";
7
+
8
+ export interface IPCMessage {
9
+ type: string;
10
+ data?: unknown;
11
+ }
12
+
13
+ export interface IPCServerOptions {
14
+ socketPath: string;
15
+ onMessage?: (message: IPCMessage) => void;
16
+ onConnect?: () => void;
17
+ onDisconnect?: () => void;
18
+ onError?: (error: Error) => void;
19
+ }
20
+
21
+ export interface IPCServer {
22
+ broadcast: (message: IPCMessage) => void;
23
+ close: () => void;
24
+ }
25
+
26
+ export function createIPCServer(options: IPCServerOptions): IPCServer {
27
+ const { socketPath, onMessage, onConnect, onDisconnect, onError } = options;
28
+
29
+ // Clean up existing socket file
30
+ if (existsSync(socketPath)) {
31
+ try {
32
+ unlinkSync(socketPath);
33
+ } catch {
34
+ // Ignore cleanup errors
35
+ }
36
+ }
37
+
38
+ const clients = new Set<ReturnType<typeof Bun.listen>["data"]>();
39
+
40
+ const server = Bun.listen({
41
+ unix: socketPath,
42
+ socket: {
43
+ open(socket) {
44
+ clients.add(socket);
45
+ onConnect?.();
46
+ },
47
+ close(socket) {
48
+ clients.delete(socket);
49
+ onDisconnect?.();
50
+ },
51
+ data(socket, data) {
52
+ try {
53
+ const text = Buffer.from(data).toString("utf-8");
54
+ // Handle line-delimited JSON
55
+ const lines = text.split("\n").filter((line) => line.trim());
56
+ for (const line of lines) {
57
+ const message = JSON.parse(line) as IPCMessage;
58
+ onMessage?.(message);
59
+ }
60
+ } catch (err) {
61
+ onError?.(err instanceof Error ? err : new Error(String(err)));
62
+ }
63
+ },
64
+ error(socket, error) {
65
+ onError?.(error);
66
+ },
67
+ },
68
+ });
69
+
70
+ return {
71
+ broadcast(message: IPCMessage) {
72
+ const data = JSON.stringify(message) + "\n";
73
+ for (const client of clients) {
74
+ try {
75
+ (client as { write: (data: string) => void }).write(data);
76
+ } catch {
77
+ // Client may have disconnected
78
+ }
79
+ }
80
+ },
81
+ close() {
82
+ server.stop();
83
+ if (existsSync(socketPath)) {
84
+ try {
85
+ unlinkSync(socketPath);
86
+ } catch {
87
+ // Ignore cleanup errors
88
+ }
89
+ }
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Minimal terminal input test - absolutely no rendering during input
4
+ */
5
+
6
+ // Hide cursor and clear screen once
7
+ process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
8
+ process.stdout.write('Type something (press Enter to echo, Escape to quit):\n> ');
9
+ process.stdout.write('\x1b[?25h'); // Show cursor
10
+
11
+ let buffer = '';
12
+
13
+ process.stdin.setRawMode(true);
14
+ process.stdin.resume();
15
+ process.stdin.setEncoding('utf8');
16
+
17
+ process.stdin.on('data', (key: string) => {
18
+ // Escape - quit
19
+ if (key === '\x1b') {
20
+ process.stdout.write('\n\x1b[?25h\x1b[0m');
21
+ process.exit(0);
22
+ }
23
+
24
+ // Enter - echo and reset
25
+ if (key === '\r' || key === '\n') {
26
+ process.stdout.write(`\nYou typed: ${buffer}\n> `);
27
+ buffer = '';
28
+ return;
29
+ }
30
+
31
+ // Backspace
32
+ if (key === '\x7f') {
33
+ if (buffer.length > 0) {
34
+ buffer = buffer.slice(0, -1);
35
+ process.stdout.write('\b \b');
36
+ }
37
+ return;
38
+ }
39
+
40
+ // Regular character - just echo it
41
+ if (key.length === 1 && key.charCodeAt(0) >= 32) {
42
+ buffer += key;
43
+ process.stdout.write(key);
44
+ }
45
+ });
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Node.js minimal terminal input test
4
+ * Testing if the flicker is Bun-specific
5
+ */
6
+
7
+ import * as readline from 'readline';
8
+
9
+ // Clear screen and show prompt
10
+ process.stdout.write('\x1b[2J\x1b[H');
11
+ process.stdout.write('Node.js test - Type something (Enter to echo, Escape to quit):\n> ');
12
+
13
+ readline.emitKeypressEvents(process.stdin);
14
+ if (process.stdin.isTTY) {
15
+ process.stdin.setRawMode(true);
16
+ }
17
+
18
+ let buffer = '';
19
+
20
+ process.stdin.on('keypress', (str, key) => {
21
+ // Escape or Ctrl+C - quit
22
+ if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
23
+ process.stdout.write('\n');
24
+ process.exit(0);
25
+ }
26
+
27
+ // Enter - echo and reset
28
+ if (key.name === 'return') {
29
+ process.stdout.write(`\nYou typed: ${buffer}\n> `);
30
+ buffer = '';
31
+ return;
32
+ }
33
+
34
+ // Backspace
35
+ if (key.name === 'backspace') {
36
+ if (buffer.length > 0) {
37
+ buffer = buffer.slice(0, -1);
38
+ process.stdout.write('\b \b');
39
+ }
40
+ return;
41
+ }
42
+
43
+ // Regular character
44
+ if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
45
+ buffer += str;
46
+ process.stdout.write(str);
47
+ }
48
+ });
49
+
50
+ process.stdin.resume();
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Persistence layer for sidebar data
3
+ * Stores tasks (queue), active task, and history per-project in ~/.claude-sidebar/projects/<hash>/
4
+ * Global data (statusline, socket) stored in ~/.claude-sidebar/
5
+ */
6
+
7
+ import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync } from "fs";
8
+ import { createHash } from "crypto";
9
+ import { homedir } from "os";
10
+ import { join } from "path";
11
+
12
+ const SIDEBAR_DIR = join(homedir(), ".claude-sidebar");
13
+
14
+ // Get a short hash of the working directory for project isolation
15
+ function getProjectHash(): string {
16
+ const cwd = process.cwd();
17
+ return createHash("sha256").update(cwd).digest("hex").slice(0, 12);
18
+ }
19
+
20
+ // Get the project-specific directory
21
+ function getProjectDir(): string {
22
+ return join(SIDEBAR_DIR, "projects", getProjectHash());
23
+ }
24
+
25
+ // Store a mapping of hash -> path for easier debugging
26
+ function updateProjectMapping(): void {
27
+ const mappingPath = join(SIDEBAR_DIR, "projects", "mapping.json");
28
+ let mapping: Record<string, string> = {};
29
+ try {
30
+ if (existsSync(mappingPath)) {
31
+ mapping = JSON.parse(readFileSync(mappingPath, "utf-8"));
32
+ }
33
+ } catch {}
34
+ mapping[getProjectHash()] = process.cwd();
35
+ ensureDir();
36
+ mkdirSync(join(SIDEBAR_DIR, "projects"), { recursive: true });
37
+ writeFileSync(mappingPath, JSON.stringify(mapping, null, 2));
38
+ }
39
+
40
+ export interface Task {
41
+ id: string;
42
+ content: string;
43
+ createdAt: string;
44
+ clarified?: boolean; // Was this task clarified via interview/brain-dump?
45
+ priority?: number; // Sort order (lower = higher priority, 1 = most important)
46
+ recommended?: boolean; // Claude's top picks (shown with star)
47
+ planPath?: string; // Filename of associated Atomic Plan
48
+ }
49
+
50
+ export interface ActiveTask {
51
+ id: string;
52
+ content: string;
53
+ sentAt: string;
54
+ }
55
+
56
+ export interface DoneTask {
57
+ id: string;
58
+ content: string;
59
+ completedAt: string;
60
+ }
61
+
62
+ export interface StatuslineData {
63
+ contextPercent: number;
64
+ contextTokens: number;
65
+ contextSize: number;
66
+ costUsd: number;
67
+ durationMin: number;
68
+ model: string;
69
+ branch: string;
70
+ repo: string;
71
+ updatedAt: string;
72
+ }
73
+
74
+ export interface ClaudeConfig {
75
+ enabledPlugins: string[];
76
+ mcpServers: string[];
77
+ }
78
+
79
+ export interface ClaudeTodo {
80
+ content: string;
81
+ status: "pending" | "in_progress" | "completed";
82
+ }
83
+
84
+ export interface ClaudeTodosData {
85
+ todos: ClaudeTodo[];
86
+ updatedAt: string;
87
+ }
88
+
89
+ // Get the data directory path
90
+ export function getDataDir(): string {
91
+ return SIDEBAR_DIR;
92
+ }
93
+
94
+ // Ensure the sidebar directory exists
95
+ export function ensureDir(): void {
96
+ if (!existsSync(SIDEBAR_DIR)) {
97
+ mkdirSync(SIDEBAR_DIR, { recursive: true });
98
+ }
99
+ }
100
+
101
+ // Generic read/write helpers for GLOBAL data
102
+ function readJson<T>(filename: string, defaultValue: T): T {
103
+ const filepath = join(SIDEBAR_DIR, filename);
104
+ try {
105
+ if (existsSync(filepath)) {
106
+ return JSON.parse(readFileSync(filepath, "utf-8"));
107
+ }
108
+ } catch {
109
+ // Return default on error
110
+ }
111
+ return defaultValue;
112
+ }
113
+
114
+ function writeJson<T>(filename: string, data: T): void {
115
+ ensureDir();
116
+ const filepath = join(SIDEBAR_DIR, filename);
117
+ writeFileSync(filepath, JSON.stringify(data, null, 2));
118
+ }
119
+
120
+ // Read/write helpers for PROJECT-SPECIFIC data
121
+ function readProjectJson<T>(filename: string, defaultValue: T): T {
122
+ const projectDir = getProjectDir();
123
+ const filepath = join(projectDir, filename);
124
+ try {
125
+ if (existsSync(filepath)) {
126
+ return JSON.parse(readFileSync(filepath, "utf-8"));
127
+ }
128
+ } catch {
129
+ // Return default on error
130
+ }
131
+ return defaultValue;
132
+ }
133
+
134
+ function writeProjectJson<T>(filename: string, data: T): void {
135
+ const projectDir = getProjectDir();
136
+ mkdirSync(projectDir, { recursive: true });
137
+ updateProjectMapping();
138
+ const filepath = join(projectDir, filename);
139
+ writeFileSync(filepath, JSON.stringify(data, null, 2));
140
+ }
141
+
142
+ // Tasks (user's task queue) - PROJECT SPECIFIC
143
+ export function getTasks(): Task[] {
144
+ return readProjectJson<Task[]>("tasks.json", []);
145
+ }
146
+
147
+ export function setTasks(tasks: Task[]): void {
148
+ writeProjectJson("tasks.json", tasks);
149
+ }
150
+
151
+ export function addTask(
152
+ content: string,
153
+ options?: {
154
+ clarified?: boolean;
155
+ priority?: number;
156
+ recommended?: boolean;
157
+ planPath?: string;
158
+ }
159
+ ): Task {
160
+ const tasks = getTasks();
161
+ const task: Task = {
162
+ id: crypto.randomUUID(),
163
+ content,
164
+ createdAt: new Date().toISOString(),
165
+ clarified: options?.clarified,
166
+ priority: options?.priority,
167
+ recommended: options?.recommended,
168
+ planPath: options?.planPath,
169
+ };
170
+ tasks.push(task);
171
+ setTasks(tasks);
172
+ return task;
173
+ }
174
+
175
+ export function updateTask(id: string, content: string): void {
176
+ const tasks = getTasks();
177
+ const task = tasks.find((t) => t.id === id);
178
+ if (task) {
179
+ task.content = content;
180
+ setTasks(tasks);
181
+ }
182
+ }
183
+
184
+ export function markTaskClarified(id: string): void {
185
+ const tasks = getTasks();
186
+ const task = tasks.find((t) => t.id === id);
187
+ if (task) {
188
+ task.clarified = true;
189
+ setTasks(tasks);
190
+ }
191
+ }
192
+
193
+ export function removeTask(id: string): void {
194
+ const tasks = getTasks().filter((t) => t.id !== id);
195
+ setTasks(tasks);
196
+ }
197
+
198
+ // Active task (currently being worked on by Claude) - PROJECT SPECIFIC
199
+ export function getActiveTask(): ActiveTask | null {
200
+ return readProjectJson<ActiveTask | null>("active.json", null);
201
+ }
202
+
203
+ // Statusline data (from Claude Code)
204
+ export function getStatusline(): StatuslineData | null {
205
+ return readJson<StatuslineData | null>("statusline.json", null);
206
+ }
207
+
208
+ // Claude's todo list (from TodoWrite hook)
209
+ export function getClaudeTodos(): ClaudeTodosData | null {
210
+ return readJson<ClaudeTodosData | null>("claude-todos.json", null);
211
+ }
212
+
213
+ // Claude Code config (plugins and MCPs)
214
+ export function getClaudeConfig(): ClaudeConfig {
215
+ const claudeSettingsPath = join(homedir(), ".claude", "settings.json");
216
+ const defaultConfig: ClaudeConfig = { enabledPlugins: [], mcpServers: [] };
217
+
218
+ try {
219
+ if (existsSync(claudeSettingsPath)) {
220
+ const settings = JSON.parse(readFileSync(claudeSettingsPath, "utf-8"));
221
+
222
+ // Extract enabled plugins (keys where value is true)
223
+ const enabledPlugins: string[] = settings.enabledPlugins
224
+ ? Object.entries(settings.enabledPlugins)
225
+ .filter(([_, enabled]) => enabled === true)
226
+ .map(([name]) => name.split("@")[0] || name)
227
+ : [];
228
+
229
+ // Extract MCP server names
230
+ const mcpServers: string[] = settings.mcpServers
231
+ ? Object.keys(settings.mcpServers)
232
+ : [];
233
+
234
+ return { enabledPlugins, mcpServers };
235
+ }
236
+ } catch {
237
+ // Return default on error
238
+ }
239
+ return defaultConfig;
240
+ }
241
+
242
+ export function setActiveTask(task: Task | null): void {
243
+ if (task) {
244
+ writeProjectJson("active.json", {
245
+ id: task.id,
246
+ content: task.content,
247
+ sentAt: new Date().toISOString(),
248
+ });
249
+ } else {
250
+ writeProjectJson("active.json", null);
251
+ }
252
+ }
253
+
254
+ export function clearActiveTask(): void {
255
+ writeProjectJson("active.json", null);
256
+ }
257
+
258
+ // History (completed tasks - append-only log) - PROJECT SPECIFIC
259
+ export function appendToHistory(content: string): void {
260
+ const projectDir = getProjectDir();
261
+ mkdirSync(projectDir, { recursive: true });
262
+ const filepath = join(projectDir, "history.log");
263
+ const timestamp = new Date().toISOString();
264
+ const entry = `${timestamp} | ${content}\n`;
265
+ appendFileSync(filepath, entry);
266
+ }
267
+
268
+ // Move task from queue to active - PROJECT SPECIFIC
269
+ export function activateTask(taskId: string): ActiveTask | null {
270
+ const tasks = getTasks();
271
+ const taskIndex = tasks.findIndex((t) => t.id === taskId);
272
+ const task = tasks[taskIndex];
273
+
274
+ if (taskIndex === -1 || !task) return null;
275
+
276
+ // Remove from queue
277
+ tasks.splice(taskIndex, 1);
278
+ setTasks(tasks);
279
+
280
+ // Set as active
281
+ const activeTask: ActiveTask = {
282
+ id: task.id,
283
+ content: task.content,
284
+ sentAt: new Date().toISOString(),
285
+ };
286
+ writeProjectJson("active.json", activeTask);
287
+
288
+ return activeTask;
289
+ }
290
+
291
+ // Recently done tasks - PROJECT SPECIFIC
292
+ export function getRecentlyDone(): DoneTask[] {
293
+ return readProjectJson<DoneTask[]>("done.json", []);
294
+ }
295
+
296
+ export function addToDone(task: { id: string; content: string }): void {
297
+ const done = getRecentlyDone();
298
+ const doneTask: DoneTask = {
299
+ id: task.id,
300
+ content: task.content,
301
+ completedAt: new Date().toISOString(),
302
+ };
303
+ // Keep only last 10 done tasks
304
+ done.unshift(doneTask);
305
+ if (done.length > 10) done.pop();
306
+ writeProjectJson("done.json", done);
307
+ }
308
+
309
+ export function removeFromDone(id: string): void {
310
+ const done = getRecentlyDone().filter((t) => t.id !== id);
311
+ writeProjectJson("done.json", done);
312
+ }
313
+
314
+ // Return a done task back to active (user says "not done yet")
315
+ export function returnToActive(id: string): void {
316
+ const done = getRecentlyDone();
317
+ const task = done.find((t) => t.id === id);
318
+ if (task) {
319
+ // Remove from done
320
+ writeProjectJson("done.json", done.filter((t) => t.id !== id));
321
+ // Set as active
322
+ writeProjectJson("active.json", {
323
+ id: task.id,
324
+ content: task.content,
325
+ sentAt: new Date().toISOString(),
326
+ });
327
+ }
328
+ }
329
+
330
+ // Complete active task (move to done list and history)
331
+ export function completeActiveTask(): void {
332
+ const active = getActiveTask();
333
+ if (active) {
334
+ addToDone(active);
335
+ appendToHistory(active.content);
336
+ clearActiveTask();
337
+ }
338
+ }
339
+
340
+ // Socket path for IPC
341
+ export function getSocketPath(): string {
342
+ return join(SIDEBAR_DIR, "sidebar.sock");
343
+ }
344
+
345
+ // Export the directory path
346
+ export { SIDEBAR_DIR };
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Standalone input test - completely isolated from sidebar code
4
+ * Tests if basic terminal input causes flicker
5
+ */
6
+
7
+ const CSI = '\x1b[';
8
+
9
+ // Clear screen, draw a simple UI
10
+ process.stdout.write(`${CSI}2J${CSI}H`);
11
+ process.stdout.write(`${CSI}?25l`); // Hide cursor initially
12
+
13
+ // Draw simple box
14
+ const width = process.stdout.columns || 50;
15
+ const height = process.stdout.rows || 20;
16
+
17
+ function drawUI(inputText: string = '', inputMode: boolean = false) {
18
+ let output = `${CSI}?2026h`; // Begin sync
19
+ output += `${CSI}H`; // Home
20
+
21
+ output += ` Simple Input Test\n`;
22
+ output += ` ─────────────────\n`;
23
+ output += `\n`;
24
+ output += ` Status: ${inputMode ? 'INPUT MODE' : 'Normal'}\n`;
25
+ output += `\n`;
26
+ output += ` Text: [${inputText}]${' '.repeat(Math.max(0, width - inputText.length - 12))}\n`;
27
+ output += `\n`;
28
+ output += ` Press 'i' to enter input mode\n`;
29
+ output += ` Press 'Esc' to exit input mode or quit\n`;
30
+
31
+ if (inputMode) {
32
+ output += `${CSI}?25h`; // Show cursor
33
+ } else {
34
+ output += `${CSI}?25l`; // Hide cursor
35
+ }
36
+
37
+ output += `${CSI}?2026l`; // End sync
38
+ process.stdout.write(output);
39
+ }
40
+
41
+ // Initial draw
42
+ drawUI();
43
+
44
+ // Setup input
45
+ process.stdin.setRawMode(true);
46
+ process.stdin.resume();
47
+
48
+ let inputMode = false;
49
+ let inputBuffer = '';
50
+
51
+ process.stdin.on('data', (data) => {
52
+ const key = data.toString();
53
+
54
+ if (inputMode) {
55
+ // In input mode
56
+ if (key === '\x1b') {
57
+ // Escape - exit input mode
58
+ inputMode = false;
59
+ drawUI(inputBuffer, false);
60
+ return;
61
+ }
62
+
63
+ if (key === '\r' || key === '\n') {
64
+ // Enter - submit
65
+ inputMode = false;
66
+ drawUI(inputBuffer, false);
67
+ return;
68
+ }
69
+
70
+ if (key === '\x7f') {
71
+ // Backspace
72
+ if (inputBuffer.length > 0) {
73
+ inputBuffer = inputBuffer.slice(0, -1);
74
+ // Just update the text line, not full redraw
75
+ process.stdout.write(`${CSI}?2026h${CSI}6;10H[${inputBuffer}]${' '.repeat(20)}${CSI}?2026l`);
76
+ }
77
+ return;
78
+ }
79
+
80
+ // Regular char
81
+ if (key.length === 1 && key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) {
82
+ inputBuffer += key;
83
+ // Just update the text line
84
+ process.stdout.write(`${CSI}?2026h${CSI}6;10H[${inputBuffer}]${CSI}?2026l`);
85
+ }
86
+ } else {
87
+ // Normal mode
88
+ if (key === '\x1b') {
89
+ // Escape - quit
90
+ process.stdout.write(`${CSI}?25h${CSI}0m${CSI}2J${CSI}H`);
91
+ process.exit(0);
92
+ }
93
+
94
+ if (key === 'i') {
95
+ // Enter input mode
96
+ inputMode = true;
97
+ inputBuffer = '';
98
+ drawUI('', true);
99
+ }
100
+ }
101
+ });