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/README.md +88 -0
- package/bin/cli.js +56 -0
- package/hooks/settings.example.json +58 -0
- package/hooks/tasks-board-hook.sh +166 -0
- package/lib/server.js +8670 -0
- package/package.json +42 -0
- package/scripts/setup.js +366 -0
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
|
+
}
|
package/scripts/setup.js
ADDED
|
@@ -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
|
+
}
|