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 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
- Open **http://localhost:9009** in your browser.
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
- On first run, Claudeck creates `~/.claudeck/` with your config, database, and plugins directory — safe for NPX upgrades.
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: add a `plugins/<name>/` directory with `client.js` and optionally `server.js`, `client.css`, `config.json`. See [CONFIGURATION.md](docs/CONFIGURATION.md#plugins) for details.
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("./server.js");
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.2",
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",
@@ -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
+ }
@@ -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
- <input type="text" class="repos-dialog-input" name="path" placeholder="/path/to/repo" autocomplete="off">
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', () => ctx.api.execCommand('open -a Terminal .', repo.path)));
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: 18px;
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 state management.</p>
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>&lt;script type="module"&gt;</code> from <code>main.js</code>.</p>
162
+ <p>Claudeck is a vanilla ES module frontend with no bundler. All modules are loaded via <code>&lt;script type="module"&gt;</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
- ├── store.js → Reactive state store
166
- ├── dom.js → Centralized DOM references
167
- ├── constants.js Shared constants
168
- ├── events.js Event bus (on/emit/off)
169
- ├── utils.js Shared utilities
170
- ├── ... Feature modules
171
- ├── right-panel.js Right panel + Tab SDK init
172
- ├── plugin-loader.js Loads enabled plugins from plugins/
173
- └── ...</code></pre>
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 &amp; shortcuts feed
203
+ │ ├── assistant-bot.js → Whaly bot assistant
204
+ │ └── dev-docs.js → This documentation modal
205
+ └── plugin-loader.js → Auto-discovers &amp; 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>, <code>emit(event, data)</code>, <code>off(event, fn)</code>. All cross-module communication uses this.</li>
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. Server runs on port 9009.</li>
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>Common Events</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>session:changed</td><td>New session ID</td></tr>
189
- <tr><td>rightPanel:opened</td><td>Active tab name</td></tr>
190
- <tr><td>rightPanel:tabChanged</td><td>New tab name</td></tr>
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>sessionId</td><td>Current active session ID</td></tr>
199
- <tr><td>projectPath</td><td>Current project path</td></tr>
200
- <tr><td>isStreaming</td><td>Whether AI is currently responding</td></tr>
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.</div>
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: 'Adding Features',
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>Adding New Features</h2>
216
- <p>Follow these patterns when extending the application.</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 New Plugin Tab</h3>
263
+ <h3>Adding a Built-in Plugin</h3>
219
264
  <ol>
220
- <li>Create a directory: <code>plugins/my-feature/</code></li>
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 = '&lt;h3&gt;Hello from my plugin!&lt;/h3&gt;';
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>&lt;package&gt;/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/&lt;name&gt;/</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) =&gt; {
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 &amp; 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);
@@ -23,9 +23,11 @@ router.post("/", (req, res) => {
23
23
  });
24
24
  };
25
25
 
26
- // Use execFile for simple "binary ." commands to avoid shell escaping issues
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
- if (parts.length <= 2 && !command.includes("|") && !command.includes(">") && !command.includes("&")) {
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);
@@ -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
- execFile(claudeBin, ["auth", "status"], { timeout: 10000 }, (err, stdout) => {
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
- console.log(`Claudeck running at http://localhost:${PORT}`);
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