claudehq 1.0.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/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "claudehq",
3
+ "version": "1.0.0",
4
+ "description": "Claude HQ - A real-time command center for Claude Code sessions",
5
+ "main": "lib/server.js",
6
+ "bin": {
7
+ "claudehq": "./bin/cli.js",
8
+ "chq": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node lib/server.js",
12
+ "setup": "node scripts/setup.js install",
13
+ "uninstall": "node scripts/setup.js uninstall"
14
+ },
15
+ "keywords": [
16
+ "claude",
17
+ "claude-code",
18
+ "task-board",
19
+ "kanban",
20
+ "ai",
21
+ "productivity"
22
+ ],
23
+ "author": "",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/alexwalz/claudehq"
28
+ },
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ },
32
+ "dependencies": {
33
+ "commander": "^12.0.0"
34
+ },
35
+ "files": [
36
+ "bin/",
37
+ "lib/",
38
+ "hooks/",
39
+ "scripts/",
40
+ "README.md"
41
+ ]
42
+ }
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync } = require('child_process');
7
+
8
+ // Paths
9
+ const CLAUDE_DIR = path.join(os.homedir(), '.claude');
10
+ const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
11
+ const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
12
+ const DATA_DIR = path.join(CLAUDE_DIR, 'tasks-board');
13
+ const HOOK_SCRIPT_NAME = 'tasks-board-hook.sh';
14
+ const HOOK_SCRIPT_DEST = path.join(HOOKS_DIR, HOOK_SCRIPT_NAME);
15
+
16
+ // Source hook script (bundled with package)
17
+ const HOOK_SCRIPT_SRC = path.join(__dirname, '..', 'hooks', HOOK_SCRIPT_NAME);
18
+
19
+ // The hooks we need to install
20
+ const REQUIRED_HOOKS = {
21
+ PreToolUse: {
22
+ matcher: '',
23
+ hooks: [{ type: 'command', command: `~/.claude/hooks/${HOOK_SCRIPT_NAME}` }]
24
+ },
25
+ PostToolUse: {
26
+ matcher: '',
27
+ hooks: [{ type: 'command', command: `~/.claude/hooks/${HOOK_SCRIPT_NAME}` }]
28
+ },
29
+ Stop: {
30
+ matcher: '',
31
+ hooks: [{ type: 'command', command: `~/.claude/hooks/${HOOK_SCRIPT_NAME}` }]
32
+ },
33
+ UserPromptSubmit: {
34
+ matcher: '',
35
+ hooks: [{ type: 'command', command: `~/.claude/hooks/${HOOK_SCRIPT_NAME}` }]
36
+ },
37
+ SubagentStop: {
38
+ matcher: '',
39
+ hooks: [{ type: 'command', command: `~/.claude/hooks/${HOOK_SCRIPT_NAME}` }]
40
+ }
41
+ };
42
+
43
+ // Colors for terminal output
44
+ const colors = {
45
+ reset: '\x1b[0m',
46
+ green: '\x1b[32m',
47
+ yellow: '\x1b[33m',
48
+ red: '\x1b[31m',
49
+ cyan: '\x1b[36m',
50
+ dim: '\x1b[2m'
51
+ };
52
+
53
+ function log(msg, color = '') {
54
+ console.log(`${color}${msg}${colors.reset}`);
55
+ }
56
+
57
+ function success(msg) { log(`✓ ${msg}`, colors.green); }
58
+ function warn(msg) { log(`⚠ ${msg}`, colors.yellow); }
59
+ function error(msg) { log(`✗ ${msg}`, colors.red); }
60
+ function info(msg) { log(` ${msg}`, colors.dim); }
61
+
62
+ // Check if jq is installed
63
+ function checkJq() {
64
+ try {
65
+ execSync('which jq', { stdio: 'ignore' });
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ // Load existing settings or create default
73
+ function loadSettings() {
74
+ if (fs.existsSync(SETTINGS_FILE)) {
75
+ try {
76
+ return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8'));
77
+ } catch (e) {
78
+ warn(`Could not parse ${SETTINGS_FILE}, creating backup`);
79
+ fs.copyFileSync(SETTINGS_FILE, `${SETTINGS_FILE}.backup`);
80
+ return {};
81
+ }
82
+ }
83
+ return {};
84
+ }
85
+
86
+ // Save settings
87
+ function saveSettings(settings) {
88
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
89
+ }
90
+
91
+ // Check if our hook is already in an array
92
+ function hasOurHook(hooksArray) {
93
+ if (!Array.isArray(hooksArray)) return false;
94
+ return hooksArray.some(entry => {
95
+ if (!entry.hooks) return false;
96
+ return entry.hooks.some(h =>
97
+ h.command && h.command.includes(HOOK_SCRIPT_NAME)
98
+ );
99
+ });
100
+ }
101
+
102
+ // Add our hook to a hook type array, preserving existing hooks
103
+ function addHookToArray(existingArray, newHook) {
104
+ if (!Array.isArray(existingArray)) {
105
+ existingArray = [];
106
+ }
107
+
108
+ // Check if we already have a hook with the same matcher
109
+ const matcherIndex = existingArray.findIndex(entry =>
110
+ entry.matcher === newHook.matcher
111
+ );
112
+
113
+ if (matcherIndex >= 0) {
114
+ // Add our hook command to existing entry if not already there
115
+ const entry = existingArray[matcherIndex];
116
+ if (!entry.hooks) entry.hooks = [];
117
+
118
+ const alreadyHasOurHook = entry.hooks.some(h =>
119
+ h.command && h.command.includes(HOOK_SCRIPT_NAME)
120
+ );
121
+
122
+ if (!alreadyHasOurHook) {
123
+ entry.hooks.push(...newHook.hooks);
124
+ }
125
+ } else {
126
+ // Add new entry
127
+ existingArray.push(newHook);
128
+ }
129
+
130
+ return existingArray;
131
+ }
132
+
133
+ // Install hooks
134
+ async function install(options = {}) {
135
+ console.log('\n' + colors.cyan + '━━━ Claude HQ Setup ━━━' + colors.reset + '\n');
136
+
137
+ // Step 1: Check for jq
138
+ log('Checking dependencies...');
139
+ if (!checkJq()) {
140
+ error('jq is required but not installed');
141
+ console.log('\n Install jq:');
142
+ if (process.platform === 'darwin') {
143
+ info('brew install jq');
144
+ } else {
145
+ info('sudo apt-get install jq (Debian/Ubuntu)');
146
+ info('sudo yum install jq (RHEL/CentOS)');
147
+ }
148
+ console.log('');
149
+ process.exit(1);
150
+ }
151
+ success('jq is installed');
152
+
153
+ // Step 2: Create directories
154
+ log('\nSetting up directories...');
155
+
156
+ if (!fs.existsSync(CLAUDE_DIR)) {
157
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
158
+ success(`Created ${CLAUDE_DIR}`);
159
+ }
160
+
161
+ if (!fs.existsSync(HOOKS_DIR)) {
162
+ fs.mkdirSync(HOOKS_DIR, { recursive: true });
163
+ success(`Created ${HOOKS_DIR}`);
164
+ }
165
+
166
+ if (!fs.existsSync(DATA_DIR)) {
167
+ fs.mkdirSync(DATA_DIR, { recursive: true });
168
+ success(`Created ${DATA_DIR}`);
169
+ }
170
+
171
+ // Step 3: Copy hook script
172
+ log('\nInstalling hook script...');
173
+
174
+ if (fs.existsSync(HOOK_SCRIPT_DEST) && !options.force) {
175
+ info(`Hook script already exists at ${HOOK_SCRIPT_DEST}`);
176
+ info('Use --force to overwrite');
177
+ } else {
178
+ if (!fs.existsSync(HOOK_SCRIPT_SRC)) {
179
+ error(`Hook script not found at ${HOOK_SCRIPT_SRC}`);
180
+ process.exit(1);
181
+ }
182
+ fs.copyFileSync(HOOK_SCRIPT_SRC, HOOK_SCRIPT_DEST);
183
+ fs.chmodSync(HOOK_SCRIPT_DEST, '755');
184
+ success(`Installed hook script to ${HOOK_SCRIPT_DEST}`);
185
+ }
186
+
187
+ // Step 4: Configure Claude settings
188
+ log('\nConfiguring Claude Code hooks...');
189
+
190
+ const settings = loadSettings();
191
+
192
+ if (!settings.hooks) {
193
+ settings.hooks = {};
194
+ }
195
+
196
+ let hooksAdded = 0;
197
+ for (const [hookType, hookConfig] of Object.entries(REQUIRED_HOOKS)) {
198
+ if (!hasOurHook(settings.hooks[hookType])) {
199
+ settings.hooks[hookType] = addHookToArray(
200
+ settings.hooks[hookType] || [],
201
+ hookConfig
202
+ );
203
+ hooksAdded++;
204
+ success(`Added ${hookType} hook`);
205
+ } else {
206
+ info(`${hookType} hook already configured`);
207
+ }
208
+ }
209
+
210
+ if (hooksAdded > 0) {
211
+ saveSettings(settings);
212
+ success(`Updated ${SETTINGS_FILE}`);
213
+ }
214
+
215
+ // Done!
216
+ console.log('\n' + colors.green + '━━━ Setup Complete! ━━━' + colors.reset);
217
+ console.log(`
218
+ Next steps:
219
+
220
+ 1. ${colors.cyan}Restart Claude Code${colors.reset} for hooks to take effect
221
+
222
+ 2. Start Claude HQ:
223
+ ${colors.dim}npx claudehq${colors.reset}
224
+ ${colors.dim}# or${colors.reset}
225
+ ${colors.dim}chq${colors.reset}
226
+
227
+ 3. Open ${colors.cyan}http://localhost:3456${colors.reset} in your browser
228
+
229
+ `);
230
+ }
231
+
232
+ // Uninstall hooks
233
+ async function uninstall() {
234
+ console.log('\n' + colors.cyan + '━━━ Claude HQ Uninstall ━━━' + colors.reset + '\n');
235
+
236
+ // Remove hook script
237
+ if (fs.existsSync(HOOK_SCRIPT_DEST)) {
238
+ fs.unlinkSync(HOOK_SCRIPT_DEST);
239
+ success(`Removed ${HOOK_SCRIPT_DEST}`);
240
+ }
241
+
242
+ // Remove hooks from settings
243
+ if (fs.existsSync(SETTINGS_FILE)) {
244
+ const settings = loadSettings();
245
+
246
+ if (settings.hooks) {
247
+ let modified = false;
248
+
249
+ for (const hookType of Object.keys(REQUIRED_HOOKS)) {
250
+ if (Array.isArray(settings.hooks[hookType])) {
251
+ const before = settings.hooks[hookType].length;
252
+
253
+ settings.hooks[hookType] = settings.hooks[hookType].map(entry => {
254
+ if (entry.hooks) {
255
+ entry.hooks = entry.hooks.filter(h =>
256
+ !h.command || !h.command.includes(HOOK_SCRIPT_NAME)
257
+ );
258
+ }
259
+ return entry;
260
+ }).filter(entry => entry.hooks && entry.hooks.length > 0);
261
+
262
+ if (settings.hooks[hookType].length !== before) {
263
+ modified = true;
264
+ success(`Removed ${hookType} hook`);
265
+ }
266
+
267
+ // Clean up empty arrays
268
+ if (settings.hooks[hookType].length === 0) {
269
+ delete settings.hooks[hookType];
270
+ }
271
+ }
272
+ }
273
+
274
+ if (modified) {
275
+ saveSettings(settings);
276
+ success(`Updated ${SETTINGS_FILE}`);
277
+ }
278
+ }
279
+ }
280
+
281
+ console.log('\n' + colors.green + 'Uninstall complete!' + colors.reset);
282
+ info('You may also want to remove ~/.claude/tasks-board/ if you no longer need the event data\n');
283
+ }
284
+
285
+ // Check status
286
+ async function status() {
287
+ console.log('\n' + colors.cyan + '━━━ Claude HQ Status ━━━' + colors.reset + '\n');
288
+
289
+ // Check jq
290
+ if (checkJq()) {
291
+ success('jq is installed');
292
+ } else {
293
+ error('jq is NOT installed');
294
+ }
295
+
296
+ // Check hook script
297
+ if (fs.existsSync(HOOK_SCRIPT_DEST)) {
298
+ success(`Hook script installed at ${HOOK_SCRIPT_DEST}`);
299
+ } else {
300
+ error('Hook script NOT installed');
301
+ }
302
+
303
+ // Check settings
304
+ if (fs.existsSync(SETTINGS_FILE)) {
305
+ const settings = loadSettings();
306
+ let allHooksConfigured = true;
307
+
308
+ for (const hookType of Object.keys(REQUIRED_HOOKS)) {
309
+ if (hasOurHook(settings.hooks?.[hookType])) {
310
+ success(`${hookType} hook configured`);
311
+ } else {
312
+ error(`${hookType} hook NOT configured`);
313
+ allHooksConfigured = false;
314
+ }
315
+ }
316
+ } else {
317
+ error('Claude settings file not found');
318
+ }
319
+
320
+ // Check data directory
321
+ if (fs.existsSync(DATA_DIR)) {
322
+ const eventsFile = path.join(DATA_DIR, 'events.jsonl');
323
+ if (fs.existsSync(eventsFile)) {
324
+ const stats = fs.statSync(eventsFile);
325
+ success(`Events file exists (${(stats.size / 1024).toFixed(1)} KB)`);
326
+ } else {
327
+ info('Events file not yet created (will be created when Claude runs)');
328
+ }
329
+ } else {
330
+ warn('Data directory not created yet');
331
+ }
332
+
333
+ // Check if server is running
334
+ try {
335
+ const http = require('http');
336
+ const req = http.get('http://localhost:3456/api/health', { timeout: 1000 }, (res) => {
337
+ if (res.statusCode === 200) {
338
+ success('Server is running on port 3456');
339
+ }
340
+ });
341
+ req.on('error', () => {
342
+ info('Server is not running');
343
+ });
344
+ req.end();
345
+ } catch {
346
+ info('Server is not running');
347
+ }
348
+
349
+ console.log('');
350
+ }
351
+
352
+ module.exports = { install, uninstall, status };
353
+
354
+ // Allow running directly
355
+ if (require.main === module) {
356
+ const cmd = process.argv[2];
357
+ if (cmd === 'install' || cmd === 'setup') {
358
+ install({ force: process.argv.includes('--force') });
359
+ } else if (cmd === 'uninstall') {
360
+ uninstall();
361
+ } else if (cmd === 'status') {
362
+ status();
363
+ } else {
364
+ console.log('Usage: setup.js [install|uninstall|status]');
365
+ }
366
+ }