claudeck 1.0.2 → 1.0.4
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 +29 -3
- package/cli.js +66 -1
- package/package.json +1 -1
- package/plugins/repos/client.css +111 -0
- package/plugins/repos/client.js +73 -3
- package/public/css/panels/dev-docs.css +7 -2
- package/public/js/panels/dev-docs.js +191 -29
- package/public/js/ui/notifications.js +12 -0
- package/server/routes/exec.js +4 -2
- package/server/routes/stats.js +4 -1
- package/server.js +15 -1
package/README.md
CHANGED
|
@@ -24,16 +24,19 @@
|
|
|
24
24
|
# One-command launch (no install needed)
|
|
25
25
|
npx claudeck
|
|
26
26
|
|
|
27
|
+
# Custom port
|
|
28
|
+
npx claudeck --port 3000
|
|
29
|
+
|
|
27
30
|
# Or install globally
|
|
28
31
|
npm install -g claudeck
|
|
29
32
|
claudeck
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
On first run, Claudeck will ask you to choose a port (default: `9009`), then open your browser to the URL shown in the terminal. The port is saved to `~/.claudeck/.env` for future runs.
|
|
33
36
|
|
|
34
37
|
> Requires **Node.js 18+** and Claude Code CLI authentication (`claude auth login`).
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX upgrades.
|
|
37
40
|
|
|
38
41
|
---
|
|
39
42
|
|
|
@@ -207,7 +210,15 @@ Claudeck includes 6 built-in plugins and supports user plugins via `~/.claudeck/
|
|
|
207
210
|
| **Event Stream** | Real-time WebSocket event viewer |
|
|
208
211
|
| **Games** | Tic-tac-toe and Sudoku |
|
|
209
212
|
|
|
210
|
-
Create your own
|
|
213
|
+
**Create your own** — drop files in `~/.claudeck/plugins/<name>/` (persists across upgrades) with `client.js` and optionally `server.js`, `client.css`, `config.json`. No fork needed. See [CONFIGURATION.md](docs/CONFIGURATION.md#user-plugins) for details.
|
|
214
|
+
|
|
215
|
+
**Scaffold with Claude Code** — install the plugin creator skill and let Claude build plugins for you:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx skills add https://github.com/hamedafarag/claudeck-skills
|
|
219
|
+
# Then in Claude Code:
|
|
220
|
+
/claudeck-plugin-create my-widget A dashboard showing system metrics
|
|
221
|
+
```
|
|
211
222
|
|
|
212
223
|
---
|
|
213
224
|
|
|
@@ -222,6 +233,21 @@ Create your own: add a `plugins/<name>/` directory with `client.js` and optional
|
|
|
222
233
|
|
|
223
234
|
---
|
|
224
235
|
|
|
236
|
+
## Contributing
|
|
237
|
+
|
|
238
|
+
Contributions are welcome! Fork the repo, make your changes, and open a PR.
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
git clone https://github.com/hamedafarag/claudeck.git
|
|
242
|
+
cd claudeck
|
|
243
|
+
npm install
|
|
244
|
+
npm start
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
See [DOCUMENTATION.md](docs/DOCUMENTATION.md) for architecture details and [CONFIGURATION.md](docs/CONFIGURATION.md) for the config system.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
225
251
|
## License
|
|
226
252
|
|
|
227
253
|
[MIT](LICENSE)
|
package/cli.js
CHANGED
|
@@ -1,2 +1,67 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { createInterface } from "readline";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PORT = 9009;
|
|
8
|
+
const envDir = process.env.CLAUDECK_HOME || join(homedir(), ".claudeck");
|
|
9
|
+
const envPath = join(envDir, ".env");
|
|
10
|
+
mkdirSync(envDir, { recursive: true });
|
|
11
|
+
|
|
12
|
+
function readEnv() {
|
|
13
|
+
try { return readFileSync(envPath, "utf-8"); } catch { return ""; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function savePort(port) {
|
|
17
|
+
let content = readEnv();
|
|
18
|
+
if (/^PORT=.*/m.test(content)) {
|
|
19
|
+
content = content.replace(/^PORT=.*/m, `PORT=${port}`);
|
|
20
|
+
} else {
|
|
21
|
+
content = content.trimEnd() + `\nPORT=${port}\n`;
|
|
22
|
+
}
|
|
23
|
+
writeFileSync(envPath, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSavedPort() {
|
|
27
|
+
const match = readEnv().match(/^PORT=(\d+)/m);
|
|
28
|
+
return match ? match[1] : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ask(question) {
|
|
32
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
33
|
+
return new Promise(resolve => {
|
|
34
|
+
rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
// --port flag takes priority
|
|
40
|
+
const portArg = process.argv.find(a => a.startsWith('--port'));
|
|
41
|
+
if (portArg) {
|
|
42
|
+
const port = portArg.includes('=') ? portArg.split('=')[1] : process.argv[process.argv.indexOf(portArg) + 1];
|
|
43
|
+
if (port) {
|
|
44
|
+
process.env.PORT = port;
|
|
45
|
+
savePort(port);
|
|
46
|
+
return import("./server.js");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If port already saved, use it
|
|
51
|
+
const saved = getSavedPort();
|
|
52
|
+
if (saved) {
|
|
53
|
+
process.env.PORT = saved;
|
|
54
|
+
return import("./server.js");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// First run — ask user
|
|
58
|
+
console.log(`\n\x1b[36m Claudeck\x1b[0m — first-time setup\n`);
|
|
59
|
+
const answer = await ask(` Which port would you like to use? \x1b[2m(default: ${DEFAULT_PORT})\x1b[0m `);
|
|
60
|
+
const port = answer && /^\d+$/.test(answer) ? answer : String(DEFAULT_PORT);
|
|
61
|
+
process.env.PORT = port;
|
|
62
|
+
savePort(port);
|
|
63
|
+
console.log(`\x1b[2m Saved to ~/.claudeck/.env\x1b[0m\n`);
|
|
64
|
+
return import("./server.js");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeck",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A browser-based UI for Claude Code — chat, run workflows, manage MCP servers, track costs, and orchestrate autonomous agents from a local web interface. Installable as a PWA.",
|
|
6
6
|
"main": "server.js",
|
package/plugins/repos/client.css
CHANGED
|
@@ -547,3 +547,114 @@
|
|
|
547
547
|
background: var(--accent-dim);
|
|
548
548
|
color: var(--accent);
|
|
549
549
|
}
|
|
550
|
+
|
|
551
|
+
/* ── Path row with browse button ────────────────────── */
|
|
552
|
+
.repos-path-row {
|
|
553
|
+
display: flex;
|
|
554
|
+
gap: 4px;
|
|
555
|
+
align-items: center;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.repos-path-input {
|
|
559
|
+
flex: 1;
|
|
560
|
+
min-width: 0;
|
|
561
|
+
cursor: pointer;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.repos-browse-btn {
|
|
565
|
+
flex-shrink: 0;
|
|
566
|
+
padding: 5px 10px !important;
|
|
567
|
+
font-size: 10px !important;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ── Inline folder browser ──────────────────────────── */
|
|
571
|
+
.repos-browser {
|
|
572
|
+
border: 1px solid var(--border);
|
|
573
|
+
border-radius: var(--radius);
|
|
574
|
+
background: var(--bg);
|
|
575
|
+
max-height: 200px;
|
|
576
|
+
display: flex;
|
|
577
|
+
flex-direction: column;
|
|
578
|
+
overflow: hidden;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.repos-browser-breadcrumb {
|
|
582
|
+
display: flex;
|
|
583
|
+
align-items: center;
|
|
584
|
+
gap: 2px;
|
|
585
|
+
padding: 5px 8px;
|
|
586
|
+
font-size: 10px;
|
|
587
|
+
font-family: var(--font-mono);
|
|
588
|
+
color: var(--text-dim);
|
|
589
|
+
border-bottom: 1px solid var(--border);
|
|
590
|
+
overflow-x: auto;
|
|
591
|
+
white-space: nowrap;
|
|
592
|
+
flex-shrink: 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.repos-browser-breadcrumb::-webkit-scrollbar { display: none; }
|
|
596
|
+
|
|
597
|
+
.repos-browser-crumb {
|
|
598
|
+
cursor: pointer;
|
|
599
|
+
color: var(--accent);
|
|
600
|
+
padding: 1px 3px;
|
|
601
|
+
border-radius: 2px;
|
|
602
|
+
transition: background 0.1s;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.repos-browser-crumb:hover {
|
|
606
|
+
background: var(--accent-dim);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.repos-browser-crumb:last-child {
|
|
610
|
+
color: var(--text);
|
|
611
|
+
font-weight: 600;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.repos-browser-sep {
|
|
615
|
+
color: var(--text-dim);
|
|
616
|
+
opacity: 0.4;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.repos-browser-list {
|
|
620
|
+
overflow-y: auto;
|
|
621
|
+
flex: 1;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.repos-browser-list::-webkit-scrollbar { width: 4px; }
|
|
625
|
+
.repos-browser-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
|
626
|
+
|
|
627
|
+
.repos-browser-item {
|
|
628
|
+
display: flex;
|
|
629
|
+
align-items: center;
|
|
630
|
+
gap: 6px;
|
|
631
|
+
padding: 4px 8px;
|
|
632
|
+
font-size: 11px;
|
|
633
|
+
font-family: var(--font-mono);
|
|
634
|
+
color: var(--text);
|
|
635
|
+
cursor: pointer;
|
|
636
|
+
transition: background 0.1s;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.repos-browser-item:hover {
|
|
640
|
+
background: var(--bg-tertiary);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.repos-browser-item svg {
|
|
644
|
+
width: 12px;
|
|
645
|
+
height: 12px;
|
|
646
|
+
flex-shrink: 0;
|
|
647
|
+
color: var(--accent);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.repos-browser-parent {
|
|
651
|
+
color: var(--text-dim);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.repos-browser-empty {
|
|
655
|
+
padding: 12px 8px;
|
|
656
|
+
font-size: 10px;
|
|
657
|
+
color: var(--text-dim);
|
|
658
|
+
text-align: center;
|
|
659
|
+
font-family: var(--font-mono);
|
|
660
|
+
}
|
package/plugins/repos/client.js
CHANGED
|
@@ -227,7 +227,7 @@ registerTab({
|
|
|
227
227
|
row.querySelector('.repos-inline-input').addEventListener('blur', handleInlineEditBlur);
|
|
228
228
|
setTimeout(() => row.querySelector('.repos-inline-input')?.focus(), 0);
|
|
229
229
|
} else {
|
|
230
|
-
const pathShort = repo.path ? repo.path.replace(/^\/Users\/[^/]+/, '~') : '';
|
|
230
|
+
const pathShort = repo.path ? repo.path.replace(/^\/Users\/[^/]+/, '~').replace(/^[A-Z]:\\Users\\[^\\]+/, '~') : '';
|
|
231
231
|
row.innerHTML = `
|
|
232
232
|
<span class="repos-chevron-spacer"></span>
|
|
233
233
|
${ICONS.repo}
|
|
@@ -355,8 +355,15 @@ registerTab({
|
|
|
355
355
|
<input type="text" class="repos-dialog-input" name="name" placeholder="my-project" autocomplete="off">
|
|
356
356
|
</label>
|
|
357
357
|
<label class="repos-dialog-label">Local Path
|
|
358
|
-
<
|
|
358
|
+
<div class="repos-path-row">
|
|
359
|
+
<input type="text" class="repos-dialog-input repos-path-input" name="path" placeholder="/path/to/repo" autocomplete="off" readonly>
|
|
360
|
+
<button class="repos-btn repos-browse-btn" type="button" title="Browse folders">Browse</button>
|
|
361
|
+
</div>
|
|
359
362
|
</label>
|
|
363
|
+
<div class="repos-browser" style="display:none;">
|
|
364
|
+
<div class="repos-browser-breadcrumb"></div>
|
|
365
|
+
<div class="repos-browser-list"></div>
|
|
366
|
+
</div>
|
|
360
367
|
<label class="repos-dialog-label">Remote URL
|
|
361
368
|
<input type="text" class="repos-dialog-input" name="url" placeholder="https://github.com/..." autocomplete="off">
|
|
362
369
|
</label>
|
|
@@ -373,6 +380,64 @@ registerTab({
|
|
|
373
380
|
</div>
|
|
374
381
|
`;
|
|
375
382
|
|
|
383
|
+
const browserEl = overlay.querySelector('.repos-browser');
|
|
384
|
+
const breadcrumbEl = overlay.querySelector('.repos-browser-breadcrumb');
|
|
385
|
+
const browserListEl = overlay.querySelector('.repos-browser-list');
|
|
386
|
+
const pathInput = overlay.querySelector('[name="path"]');
|
|
387
|
+
let browserVisible = false;
|
|
388
|
+
|
|
389
|
+
async function navigateBrowser(dir) {
|
|
390
|
+
try {
|
|
391
|
+
const data = await ctx.api.browseFolders(dir || undefined);
|
|
392
|
+
pathInput.value = data.current;
|
|
393
|
+
|
|
394
|
+
// Breadcrumb
|
|
395
|
+
const parts = data.current.split(/[/\\]/).filter(Boolean);
|
|
396
|
+
let crumbPath = data.current.startsWith('/') ? '/' : '';
|
|
397
|
+
let crumbHtml = '';
|
|
398
|
+
// Root
|
|
399
|
+
const rootLabel = data.current.startsWith('/') ? '/' : parts[0];
|
|
400
|
+
const rootPath = data.current.startsWith('/') ? '/' : parts[0] + '\\';
|
|
401
|
+
crumbHtml += `<span class="repos-browser-crumb" data-path="${escapeHtml(rootPath)}">${escapeHtml(rootLabel)}</span>`;
|
|
402
|
+
const startIdx = data.current.startsWith('/') ? 0 : 1;
|
|
403
|
+
for (let i = startIdx; i < parts.length; i++) {
|
|
404
|
+
crumbPath += (i > 0 || !data.current.startsWith('/') ? (data.current.includes('\\') ? '\\' : '/') : '') + parts[i];
|
|
405
|
+
if (i >= startIdx) {
|
|
406
|
+
crumbHtml += `<span class="repos-browser-sep">/</span><span class="repos-browser-crumb" data-path="${escapeHtml(crumbPath)}">${escapeHtml(parts[i])}</span>`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
breadcrumbEl.innerHTML = crumbHtml;
|
|
410
|
+
breadcrumbEl.querySelectorAll('.repos-browser-crumb').forEach(crumb => {
|
|
411
|
+
crumb.addEventListener('click', () => navigateBrowser(crumb.dataset.path));
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Directory list
|
|
415
|
+
let listHtml = '';
|
|
416
|
+
if (data.parent) {
|
|
417
|
+
listHtml += `<div class="repos-browser-item repos-browser-parent" data-path="${escapeHtml(data.parent)}">${ICONS.folderClosed} <span>..</span></div>`;
|
|
418
|
+
}
|
|
419
|
+
for (const d of data.dirs) {
|
|
420
|
+
const fullPath = data.current + (data.current.endsWith('/') || data.current.endsWith('\\') ? '' : '/') + d.name;
|
|
421
|
+
listHtml += `<div class="repos-browser-item" data-path="${escapeHtml(fullPath)}">${ICONS.folderClosed} <span>${escapeHtml(d.name)}</span></div>`;
|
|
422
|
+
}
|
|
423
|
+
if (!data.dirs.length && !data.parent) {
|
|
424
|
+
listHtml = '<div class="repos-browser-empty">No subdirectories</div>';
|
|
425
|
+
}
|
|
426
|
+
browserListEl.innerHTML = listHtml;
|
|
427
|
+
browserListEl.querySelectorAll('.repos-browser-item').forEach(item => {
|
|
428
|
+
item.addEventListener('click', () => navigateBrowser(item.dataset.path));
|
|
429
|
+
});
|
|
430
|
+
} catch (err) {
|
|
431
|
+
browserListEl.innerHTML = `<div class="repos-browser-empty">Error: ${escapeHtml(err.message)}</div>`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
overlay.querySelector('.repos-browse-btn').addEventListener('click', () => {
|
|
436
|
+
browserVisible = !browserVisible;
|
|
437
|
+
browserEl.style.display = browserVisible ? '' : 'none';
|
|
438
|
+
if (browserVisible) navigateBrowser(pathInput.value || undefined);
|
|
439
|
+
});
|
|
440
|
+
|
|
376
441
|
overlay.querySelector('.repos-dialog-cancel').addEventListener('click', () => overlay.remove());
|
|
377
442
|
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
|
378
443
|
|
|
@@ -510,7 +575,12 @@ registerTab({
|
|
|
510
575
|
|
|
511
576
|
if (repo.path) {
|
|
512
577
|
ctxMenu.appendChild(createMenuItem('Open in VS Code', () => ctx.api.execCommand('code .', repo.path)));
|
|
513
|
-
ctxMenu.appendChild(createMenuItem('Open in Terminal', () =>
|
|
578
|
+
ctxMenu.appendChild(createMenuItem('Open in Terminal', () => {
|
|
579
|
+
const isWin = navigator.platform.startsWith('Win');
|
|
580
|
+
const isMac = navigator.platform.startsWith('Mac');
|
|
581
|
+
const cmd = isWin ? 'start cmd /k' : isMac ? 'open -a Terminal .' : 'x-terminal-emulator || xterm';
|
|
582
|
+
ctx.api.execCommand(cmd, repo.path);
|
|
583
|
+
}));
|
|
514
584
|
ctxMenu.appendChild(createMenuItem('Copy Path', () => navigator.clipboard.writeText(repo.path)));
|
|
515
585
|
}
|
|
516
586
|
|
|
@@ -176,9 +176,14 @@
|
|
|
176
176
|
margin: 0 0 12px;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
.dev-docs-content ul
|
|
179
|
+
.dev-docs-content ul,
|
|
180
|
+
.dev-docs-content ol {
|
|
180
181
|
margin: 0 0 12px;
|
|
181
|
-
padding-left:
|
|
182
|
+
padding-left: 22px;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.dev-docs-content ol {
|
|
186
|
+
list-style-type: decimal;
|
|
182
187
|
}
|
|
183
188
|
|
|
184
189
|
.dev-docs-content li {
|
|
@@ -25,10 +25,11 @@ registerDocSection({
|
|
|
25
25
|
<h2>Tab SDK — Plugin Guide</h2>
|
|
26
26
|
<p>Register custom tabs in the right panel with a single function call.
|
|
27
27
|
No HTML or <code>dom.js</code> changes needed — the SDK handles DOM creation,
|
|
28
|
-
lifecycle hooks, badges, and
|
|
28
|
+
lifecycle hooks, badges, state management, and marketplace integration.</p>
|
|
29
29
|
|
|
30
30
|
<h3>Quick Start</h3>
|
|
31
|
-
<pre><code>// plugins/my-tab/client.js
|
|
31
|
+
<pre><code>// ~/.claudeck/plugins/my-tab/client.js (user plugin)
|
|
32
|
+
// — or plugins/my-tab/client.js (built-in plugin)
|
|
32
33
|
import { registerTab } from '/js/ui/tab-sdk.js';
|
|
33
34
|
|
|
34
35
|
registerTab({
|
|
@@ -158,50 +159,94 @@ registerDocSection({
|
|
|
158
159
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
|
|
159
160
|
render: () => `
|
|
160
161
|
<h2>Architecture Overview</h2>
|
|
161
|
-
<p>Claudeck is a vanilla ES module frontend with no bundler. All modules are loaded via <code><script type="module"></code> from <code>main.js</code
|
|
162
|
+
<p>Claudeck is a vanilla ES module frontend with no bundler. All modules are loaded via <code><script type="module"></code> from <code>main.js</code>. Server port is configurable (default 9009).</p>
|
|
162
163
|
|
|
163
164
|
<h3>Module Loading Order</h3>
|
|
164
165
|
<pre><code>main.js
|
|
165
|
-
├──
|
|
166
|
-
├──
|
|
167
|
-
├──
|
|
168
|
-
├──
|
|
169
|
-
├──
|
|
170
|
-
├──
|
|
171
|
-
├──
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
├── core/
|
|
167
|
+
│ ├── store.js → Reactive state store (getState/setState/on)
|
|
168
|
+
│ ├── dom.js → Centralized DOM references ($)
|
|
169
|
+
│ ├── constants.js → Shared constants
|
|
170
|
+
│ ├── events.js → Event bus (on/emit)
|
|
171
|
+
│ ├── utils.js → Shared utilities
|
|
172
|
+
│ ├── api.js → All fetch() helpers
|
|
173
|
+
│ └── ws.js → WebSocket connection manager
|
|
174
|
+
├── ui/
|
|
175
|
+
│ ├── formatting.js → Markdown rendering, code highlighting
|
|
176
|
+
│ ├── diff.js → Code diff viewer
|
|
177
|
+
│ ├── commands.js → Slash command registry
|
|
178
|
+
│ ├── messages.js → Chat message rendering
|
|
179
|
+
│ ├── parallel.js → 2×2 parallel chat mode
|
|
180
|
+
│ ├── notifications.js → Push notifications + sound
|
|
181
|
+
│ ├── permissions.js → Tool approval modes
|
|
182
|
+
│ ├── model-selector.js → Model picker (Opus/Sonnet/Haiku)
|
|
183
|
+
│ ├── right-panel.js → Right panel + Tab SDK init
|
|
184
|
+
│ ├── context-gauge.js → Token usage indicator
|
|
185
|
+
│ └── shortcuts.js → Keyboard shortcuts
|
|
186
|
+
├── features/
|
|
187
|
+
│ ├── sessions.js → Session management + search
|
|
188
|
+
│ ├── projects.js → Project picker + system prompts
|
|
189
|
+
│ ├── chat.js → Main chat loop
|
|
190
|
+
│ ├── prompts.js → Prompt templates
|
|
191
|
+
│ ├── workflows.js → Multi-step workflows
|
|
192
|
+
│ ├── agents.js → Agent definitions, chains, DAGs
|
|
193
|
+
│ ├── home.js → Home screen + activity grid
|
|
194
|
+
│ ├── attachments.js → File/image attachments
|
|
195
|
+
│ ├── voice-input.js → Web Speech API input
|
|
196
|
+
│ ├── telegram.js → Telegram integration
|
|
197
|
+
│ └── welcome.js → Guided tour (Driver.js)
|
|
198
|
+
├── panels/
|
|
199
|
+
│ ├── file-explorer.js → File tree + preview
|
|
200
|
+
│ ├── git-panel.js → Git status, staging, commit
|
|
201
|
+
│ ├── mcp-manager.js → MCP server management
|
|
202
|
+
│ ├── tips-feed.js → Tips & shortcuts feed
|
|
203
|
+
│ ├── assistant-bot.js → Whaly bot assistant
|
|
204
|
+
│ └── dev-docs.js → This documentation modal
|
|
205
|
+
└── plugin-loader.js → Auto-discovers & loads plugins</code></pre>
|
|
174
206
|
|
|
175
207
|
<h3>Key Patterns</h3>
|
|
176
208
|
<ul>
|
|
177
|
-
<li><strong>Event Bus</strong> — <code>events.js</code> provides <code>on(event, fn)</code
|
|
209
|
+
<li><strong>Event Bus</strong> — <code>events.js</code> provides <code>on(event, fn)</code> and <code>emit(event, data)</code>. All cross-module communication uses this.</li>
|
|
178
210
|
<li><strong>Reactive Store</strong> — <code>store.js</code> provides <code>getState(key)</code>, <code>setState(key, val)</code>, and <code>on(key, fn)</code> for reactive subscriptions.</li>
|
|
179
211
|
<li><strong>DOM Registry</strong> — <code>dom.js</code> exports <code>$</code> object with cached element references. Only used for built-in (HTML-defined) elements.</li>
|
|
180
|
-
<li><strong>API Layer</strong> — <code>api.js</code> contains all <code>fetch()</code> calls
|
|
212
|
+
<li><strong>API Layer</strong> — <code>api.js</code> contains all <code>fetch()</code> calls for server communication.</li>
|
|
213
|
+
<li><strong>Plugin System</strong> — Plugins are auto-discovered from <code>plugins/</code> (built-in) and <code>~/.claudeck/plugins/</code> (user). Managed via the Marketplace (<code>+</code> button in tab bar).</li>
|
|
181
214
|
</ul>
|
|
182
215
|
|
|
183
|
-
<h3>
|
|
216
|
+
<h3>Event Bus Events</h3>
|
|
184
217
|
<table class="param-table">
|
|
185
218
|
<thead><tr><th>Event</th><th>Payload</th></tr></thead>
|
|
186
219
|
<tbody>
|
|
187
220
|
<tr><td>ws:message</td><td>Parsed WebSocket message object</td></tr>
|
|
188
|
-
<tr><td>
|
|
189
|
-
<tr><td>
|
|
190
|
-
<tr><td>
|
|
221
|
+
<tr><td>ws:connected</td><td><em>none</em> — initial connection established</td></tr>
|
|
222
|
+
<tr><td>ws:reconnected</td><td><em>none</em> — reconnected after disconnect</td></tr>
|
|
223
|
+
<tr><td>ws:disconnected</td><td><em>none</em> — connection lost</td></tr>
|
|
224
|
+
<tr><td>rightPanel:opened</td><td>Active tab name (string)</td></tr>
|
|
225
|
+
<tr><td>rightPanel:tabChanged</td><td>New tab name (string)</td></tr>
|
|
191
226
|
</tbody>
|
|
192
227
|
</table>
|
|
193
228
|
|
|
194
229
|
<h3>Store Keys</h3>
|
|
195
230
|
<table class="param-table">
|
|
196
|
-
<thead><tr><th>Key</th><th>Description</th></tr></thead>
|
|
231
|
+
<thead><tr><th>Key</th><th>Type</th><th>Description</th></tr></thead>
|
|
197
232
|
<tbody>
|
|
198
|
-
<tr><td>
|
|
199
|
-
<tr><td>
|
|
200
|
-
<tr><td>
|
|
233
|
+
<tr><td>view</td><td>string</td><td>Current view: <code>"home"</code> or <code>"chat"</code></td></tr>
|
|
234
|
+
<tr><td>sessionId</td><td>string|null</td><td>Current active session ID</td></tr>
|
|
235
|
+
<tr><td>parallelMode</td><td>boolean</td><td>Whether 2×2 parallel mode is active</td></tr>
|
|
236
|
+
<tr><td>streamingCharCount</td><td>number</td><td>Character count during streaming</td></tr>
|
|
237
|
+
<tr><td>notificationsEnabled</td><td>boolean</td><td>Whether push notifications are on</td></tr>
|
|
238
|
+
<tr><td>sessionTokens</td><td>object</td><td><code>{ input, output, cacheRead, cacheCreation }</code></td></tr>
|
|
239
|
+
<tr><td>prompts</td><td>array</td><td>Loaded prompt templates</td></tr>
|
|
240
|
+
<tr><td>workflows</td><td>array</td><td>Loaded workflow definitions</td></tr>
|
|
241
|
+
<tr><td>agents</td><td>array</td><td>Loaded agent definitions</td></tr>
|
|
242
|
+
<tr><td>projectsData</td><td>array</td><td>Configured projects list</td></tr>
|
|
243
|
+
<tr><td>attachedFiles</td><td>array</td><td>Files attached to current message</td></tr>
|
|
244
|
+
<tr><td>imageAttachments</td><td>array</td><td>Images attached to current message</td></tr>
|
|
245
|
+
<tr><td>backgroundSessions</td><td>Map</td><td>Sessions running in background</td></tr>
|
|
201
246
|
</tbody>
|
|
202
247
|
</table>
|
|
203
248
|
|
|
204
|
-
<div class="callout">All modules are independent — import only what you need. No global state beyond the event bus and store
|
|
249
|
+
<div class="callout">All modules are independent — import only what you need. No global state beyond the event bus and store. Plugins get access to both via the <code>ctx</code> object in <code>init()</code>.</div>
|
|
205
250
|
`,
|
|
206
251
|
});
|
|
207
252
|
|
|
@@ -209,15 +254,15 @@ registerDocSection({
|
|
|
209
254
|
|
|
210
255
|
registerDocSection({
|
|
211
256
|
id: 'adding-features',
|
|
212
|
-
title: '
|
|
257
|
+
title: 'Contributing',
|
|
213
258
|
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
|
|
214
259
|
render: () => `
|
|
215
|
-
<h2>
|
|
216
|
-
<p>
|
|
260
|
+
<h2>Contributing to Claudeck</h2>
|
|
261
|
+
<p>This guide is for contributors who have <strong>forked or cloned the Claudeck repo</strong> and want to extend the core application. If you're using Claudeck via <code>npx claudeck</code> and want to create your own plugins, see the <strong>User Plugins</strong> section instead.</p>
|
|
217
262
|
|
|
218
|
-
<h3>Adding a
|
|
263
|
+
<h3>Adding a Built-in Plugin</h3>
|
|
219
264
|
<ol>
|
|
220
|
-
<li>Create a directory: <code>plugins/my-feature/</code
|
|
265
|
+
<li>Create a directory: <code>plugins/my-feature/</code> in the repo</li>
|
|
221
266
|
<li>Create <code>plugins/my-feature/client.js</code> — import <code>registerTab()</code> from <code>/js/ui/tab-sdk.js</code></li>
|
|
222
267
|
<li>Build all DOM inside <code>init(ctx)</code></li>
|
|
223
268
|
<li>Optionally add <code>client.css</code> in the same directory (auto-injected)</li>
|
|
@@ -246,7 +291,7 @@ registerDocSection({
|
|
|
246
291
|
<li>Use CSS variables from <code>variables.css</code> for consistency</li>
|
|
247
292
|
</ol>
|
|
248
293
|
|
|
249
|
-
<h3>Plugin Structure</h3>
|
|
294
|
+
<h3>Built-in Plugin Structure</h3>
|
|
250
295
|
<ul>
|
|
251
296
|
<li>Plugin directories: <code>plugins/kebab-case/</code></li>
|
|
252
297
|
<li>Client module: <code>client.js</code> (required)</li>
|
|
@@ -260,6 +305,123 @@ registerDocSection({
|
|
|
260
305
|
`,
|
|
261
306
|
});
|
|
262
307
|
|
|
308
|
+
// ── Built-in: User Plugins Guide ────────────────────────
|
|
309
|
+
|
|
310
|
+
registerDocSection({
|
|
311
|
+
id: 'user-plugins',
|
|
312
|
+
title: 'User Plugins',
|
|
313
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 18l6-6-6-6"/><path d="M8 6l-6 6 6 6"/></svg>',
|
|
314
|
+
render: () => `
|
|
315
|
+
<h2>User Plugins</h2>
|
|
316
|
+
<p>This guide is for developers using Claudeck via <code>npx claudeck</code> who want to create their own plugins. User plugins live in <code>~/.claudeck/plugins/</code>, persist across npm upgrades, and work identically to built-in plugins — no need to fork the repo.</p>
|
|
317
|
+
|
|
318
|
+
<h3>Creating a User Plugin</h3>
|
|
319
|
+
<ol>
|
|
320
|
+
<li>Create a directory in <code>~/.claudeck/plugins/</code>:
|
|
321
|
+
<pre><code>mkdir -p ~/.claudeck/plugins/my-plugin</code></pre>
|
|
322
|
+
</li>
|
|
323
|
+
<li>Create <code>client.js</code> with a <code>registerTab()</code> call:
|
|
324
|
+
<pre><code>// ~/.claudeck/plugins/my-plugin/client.js
|
|
325
|
+
import { registerTab } from '/js/ui/tab-sdk.js';
|
|
326
|
+
|
|
327
|
+
registerTab({
|
|
328
|
+
id: 'my-plugin',
|
|
329
|
+
title: 'My Plugin',
|
|
330
|
+
lazy: true,
|
|
331
|
+
init(ctx) {
|
|
332
|
+
const root = document.createElement('div');
|
|
333
|
+
root.innerHTML = '<h3>Hello from my plugin!</h3>';
|
|
334
|
+
return root;
|
|
335
|
+
},
|
|
336
|
+
});</code></pre>
|
|
337
|
+
</li>
|
|
338
|
+
<li>Optionally add <code>client.css</code> for styles (auto-injected)</li>
|
|
339
|
+
<li>Optionally add <code>server.js</code> for backend routes (requires <code>CLAUDECK_USER_SERVER_PLUGINS=true</code> in <code>~/.claudeck/.env</code>)</li>
|
|
340
|
+
<li>Optionally add <code>config.json</code> for default settings (auto-copied to <code>~/.claudeck/config/</code>)</li>
|
|
341
|
+
</ol>
|
|
342
|
+
|
|
343
|
+
<h3>Plugin Directories</h3>
|
|
344
|
+
<table class="param-table">
|
|
345
|
+
<thead><tr><th>Directory</th><th>URL Path</th><th>Writable</th></tr></thead>
|
|
346
|
+
<tbody>
|
|
347
|
+
<tr><td><code><package>/plugins/</code></td><td><code>/plugins/</code></td><td>No (built-in)</td></tr>
|
|
348
|
+
<tr><td><code>~/.claudeck/plugins/</code></td><td><code>/user-plugins/</code></td><td>Yes (user)</td></tr>
|
|
349
|
+
</tbody>
|
|
350
|
+
</table>
|
|
351
|
+
|
|
352
|
+
<h3>Server-Side Routes</h3>
|
|
353
|
+
<p>If your plugin has a <code>server.js</code>, it exports an Express router that is auto-mounted at <code>/api/plugins/<name>/</code>:</p>
|
|
354
|
+
<pre><code>// ~/.claudeck/plugins/my-plugin/server.js
|
|
355
|
+
import { Router } from 'express';
|
|
356
|
+
const router = Router();
|
|
357
|
+
|
|
358
|
+
router.get('/data', (req, res) => {
|
|
359
|
+
res.json({ message: 'Hello from server!' });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
export default router;</code></pre>
|
|
363
|
+
<div class="callout"><strong>Security:</strong> User server plugins are disabled by default. Set <code>CLAUDECK_USER_SERVER_PLUGINS=true</code> in <code>~/.claudeck/.env</code> to enable them.</div>
|
|
364
|
+
|
|
365
|
+
<h3>Discovery</h3>
|
|
366
|
+
<p>Plugins are auto-discovered on page load. No server restart is needed for client-only plugins. The Marketplace tab shows all plugins with their source (<code>builtin</code> or <code>user</code>).</p>
|
|
367
|
+
|
|
368
|
+
<h3>Scaffolding Plugins with Claude Code</h3>
|
|
369
|
+
<p>Install the Claudeck plugin creator skill, then let Claude Code scaffold plugins for you:</p>
|
|
370
|
+
<pre><code>npx skills add https://github.com/hamedafarag/claudeck-skills</code></pre>
|
|
371
|
+
<p>Then in Claude Code, run:</p>
|
|
372
|
+
<pre><code>/claudeck-plugin-create my-plugin A tab that shows GitHub notifications</code></pre>
|
|
373
|
+
<p>Claude will generate the full plugin files in <code>~/.claudeck/plugins/</code> based on your description. Examples:</p>
|
|
374
|
+
<ul>
|
|
375
|
+
<li><code>/claudeck-plugin-create github-notifs Show my GitHub notifications</code></li>
|
|
376
|
+
<li><code>/claudeck-plugin-create sys-metrics A dashboard showing system metrics</code></li>
|
|
377
|
+
<li><code>/claudeck-plugin-create api-proxy A plugin with a server route that proxies an external API</code></li>
|
|
378
|
+
</ul>
|
|
379
|
+
`,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ── Built-in: Links & Resources ─────────────────────────
|
|
383
|
+
|
|
384
|
+
registerDocSection({
|
|
385
|
+
id: 'resources',
|
|
386
|
+
title: 'Resources',
|
|
387
|
+
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>',
|
|
388
|
+
render: () => `
|
|
389
|
+
<h2>Resources & Links</h2>
|
|
390
|
+
|
|
391
|
+
<h3>GitHub</h3>
|
|
392
|
+
<ul>
|
|
393
|
+
<li><a href="https://github.com/hamedafarag/claudeck" target="_blank" style="color:var(--accent)">github.com/hamedafarag/claudeck</a> — Source code, issues, and discussions</li>
|
|
394
|
+
<li><a href="https://github.com/hamedafarag/claudeck/issues" target="_blank" style="color:var(--accent)">Issues</a> — Report bugs or request features</li>
|
|
395
|
+
<li><a href="https://github.com/hamedafarag/claudeck/blob/main/docs/DOCUMENTATION.md" target="_blank" style="color:var(--accent)">Full Documentation</a> — Complete feature docs and API reference</li>
|
|
396
|
+
<li><a href="https://github.com/hamedafarag/claudeck/blob/main/docs/CONFIGURATION.md" target="_blank" style="color:var(--accent)">Configuration Guide</a> — User data directory, config files, plugin system</li>
|
|
397
|
+
</ul>
|
|
398
|
+
|
|
399
|
+
<h3>npm</h3>
|
|
400
|
+
<ul>
|
|
401
|
+
<li><a href="https://www.npmjs.com/package/claudeck" target="_blank" style="color:var(--accent)">npmjs.com/package/claudeck</a> — Package page</li>
|
|
402
|
+
</ul>
|
|
403
|
+
|
|
404
|
+
<h3>Claude Code Skills</h3>
|
|
405
|
+
<ul>
|
|
406
|
+
<li><a href="https://github.com/hamedafarag/claudeck-skills" target="_blank" style="color:var(--accent)">github.com/hamedafarag/claudeck-skills</a> — Plugin creator skill for Claude Code</li>
|
|
407
|
+
</ul>
|
|
408
|
+
<pre><code>npx skills add https://github.com/hamedafarag/claudeck-skills</code></pre>
|
|
409
|
+
|
|
410
|
+
<h3>Quick Reference</h3>
|
|
411
|
+
<table class="param-table">
|
|
412
|
+
<thead><tr><th>Command</th><th>Description</th></tr></thead>
|
|
413
|
+
<tbody>
|
|
414
|
+
<tr><td><code>npx claudeck</code></td><td>Launch Claudeck (installs if needed)</td></tr>
|
|
415
|
+
<tr><td><code>npx claudeck --port 3000</code></td><td>Launch on a custom port</td></tr>
|
|
416
|
+
<tr><td><code>npm i -g claudeck</code></td><td>Install globally</td></tr>
|
|
417
|
+
</tbody>
|
|
418
|
+
</table>
|
|
419
|
+
|
|
420
|
+
<h3>License</h3>
|
|
421
|
+
<p>Claudeck is open-source under the <strong>MIT License</strong>. Contributions are welcome!</p>
|
|
422
|
+
`,
|
|
423
|
+
});
|
|
424
|
+
|
|
263
425
|
// ── Modal renderer ──────────────────────────────────────
|
|
264
426
|
|
|
265
427
|
let overlayEl = null;
|
|
@@ -162,6 +162,18 @@ async function unsubscribeFromPush() {
|
|
|
162
162
|
export async function toggleNotifications() {
|
|
163
163
|
const current = getState('notificationsEnabled');
|
|
164
164
|
if (!current) {
|
|
165
|
+
if (!('Notification' in window)) {
|
|
166
|
+
alert('Notifications are not supported in this browser.');
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (Notification.permission === 'denied') {
|
|
170
|
+
alert('Notifications are blocked. Please enable them in your browser settings for this site.');
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (window.location.protocol !== 'https:' && window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1') {
|
|
174
|
+
alert('Notifications require HTTPS or localhost. Please access Claudeck via https:// or http://localhost.');
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
165
177
|
const granted = await requestNotificationPermission();
|
|
166
178
|
if (!granted) return false;
|
|
167
179
|
setState('notificationsEnabled', true);
|
package/server/routes/exec.js
CHANGED
|
@@ -23,9 +23,11 @@ router.post("/", (req, res) => {
|
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// On Windows, always use exec (shell) so PATH-resolved commands like "code" work
|
|
27
|
+
// On Unix, use execFile for simple commands to avoid shell escaping issues
|
|
27
28
|
const parts = command.split(/\s+/);
|
|
28
|
-
|
|
29
|
+
const isSimple = parts.length <= 2 && !command.includes("|") && !command.includes(">") && !command.includes("&");
|
|
30
|
+
if (isSimple && process.platform !== "win32") {
|
|
29
31
|
execFile(parts[0], parts.slice(1), execOpts, callback);
|
|
30
32
|
} else {
|
|
31
33
|
exec(command, execOpts, callback);
|
package/server/routes/stats.js
CHANGED
|
@@ -42,7 +42,10 @@ router.get("/account", async (req, res) => {
|
|
|
42
42
|
}
|
|
43
43
|
try {
|
|
44
44
|
const data = await new Promise((resolve, reject) => {
|
|
45
|
-
|
|
45
|
+
const opts = { timeout: 10000 };
|
|
46
|
+
// On Windows, use shell to resolve claude from PATH
|
|
47
|
+
if (process.platform === "win32") opts.shell = true;
|
|
48
|
+
execFile(claudeBin, ["auth", "status"], opts, (err, stdout) => {
|
|
46
49
|
if (err) return reject(err);
|
|
47
50
|
try { resolve(JSON.parse(stdout)); } catch (e) { reject(e); }
|
|
48
51
|
});
|
package/server.js
CHANGED
|
@@ -164,7 +164,21 @@ const PORT = process.env.PORT || 9009;
|
|
|
164
164
|
// Mount full-stack plugin routes, then start server
|
|
165
165
|
mountPluginRoutes(app, fullStackPluginsDir).then(() => {
|
|
166
166
|
server.listen(PORT, () => {
|
|
167
|
-
|
|
167
|
+
const url = `http://localhost:${PORT}`;
|
|
168
|
+
console.log(`
|
|
169
|
+
\x1b[36m _____ _ _ _
|
|
170
|
+
/ ____| | | | | |
|
|
171
|
+
| | | | __ _ _ _ __| | ___ ___| | __
|
|
172
|
+
| | | |/ _\` | | | |/ _\` |/ _ \\/ __| |/ /
|
|
173
|
+
| |____| | (_| | |_| | (_| | __/ (__| <
|
|
174
|
+
\\_____|_|\\__,_|\\__,_|\\__,_|\\___|\\___|_|\\_\\\x1b[0m
|
|
175
|
+
|
|
176
|
+
\x1b[2m Browser UI for Claude Code\x1b[0m
|
|
177
|
+
|
|
178
|
+
\x1b[1m\x1b[32m➜\x1b[0m \x1b[1mReady:\x1b[0m ${url}
|
|
179
|
+
\x1b[2m➜ Port:\x1b[0m ${PORT}
|
|
180
|
+
\x1b[2m➜ Data:\x1b[0m ~/.claudeck/
|
|
181
|
+
`);
|
|
168
182
|
});
|
|
169
183
|
});
|
|
170
184
|
|