claudeck 1.1.1 → 1.3.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 +94 -9
- package/cli.js +53 -4
- package/config/skillsmp-config.json +5 -0
- package/db.js +248 -0
- package/package.json +12 -2
- package/public/css/core/responsive.css +2 -2
- package/public/css/panels/git-panel.css +220 -0
- package/public/css/panels/skills-manager.css +975 -0
- package/public/css/ui/file-picker.css +243 -17
- package/public/css/ui/input-history.css +109 -0
- package/public/css/ui/messages.css +123 -9
- package/public/css/ui/notification-bell.css +421 -0
- package/public/css/ui/sessions.css +41 -0
- package/public/css/ui/toolbox.css +43 -0
- package/public/css/ui/worktree.css +442 -0
- package/public/index.html +110 -742
- package/public/js/components/add-project-modal.js +27 -0
- package/public/js/components/agent-modal.js +73 -0
- package/public/js/components/agent-monitor-modal.js +19 -0
- package/public/js/components/bg-confirm-modal.js +22 -0
- package/public/js/components/chain-modal.js +52 -0
- package/public/js/components/cost-dashboard-modal.js +39 -0
- package/public/js/components/dag-editor-modal.js +55 -0
- package/public/js/components/file-picker-modal.js +45 -0
- package/public/js/components/linear-create-modal.js +43 -0
- package/public/js/components/mcp-modal.js +58 -0
- package/public/js/components/orchestrate-modal.js +40 -0
- package/public/js/components/permission-modal.js +30 -0
- package/public/js/components/prompt-modal.js +31 -0
- package/public/js/components/shortcuts-modal.js +45 -0
- package/public/js/components/status-bar.js +97 -0
- package/public/js/components/system-prompt-modal.js +29 -0
- package/public/js/components/telegram-modal.js +84 -0
- package/public/js/components/welcome-overlay.js +60 -0
- package/public/js/components/workflow-modal.js +41 -0
- package/public/js/core/api.js +93 -0
- package/public/js/core/dom.js +18 -2
- package/public/js/core/ws.js +7 -1
- package/public/js/features/attachments.js +226 -23
- package/public/js/features/background-sessions.js +11 -0
- package/public/js/features/chat.js +501 -3
- package/public/js/features/input-history.js +122 -0
- package/public/js/features/projects.js +23 -1
- package/public/js/features/sessions.js +77 -30
- package/public/js/main.js +25 -0
- package/public/js/panels/git-panel.js +385 -6
- package/public/js/panels/skills-manager.js +1005 -0
- package/public/js/ui/messages.js +58 -0
- package/public/js/ui/notification-bell.js +240 -0
- package/public/js/ui/notification-history.js +210 -0
- package/public/js/ui/parallel.js +11 -0
- package/public/js/ui/shortcuts.js +4 -8
- package/public/js/ui/tab-sdk.js +1 -1
- package/public/login.html +470 -0
- package/public/offline.html +300 -168
- package/public/style.css +4 -0
- package/public/sw.js +10 -2
- package/server/agent-loop.js +13 -0
- package/server/auth.js +141 -0
- package/server/notification-logger.js +27 -0
- package/server/routes/notifications.js +57 -1
- package/server/routes/sessions.js +41 -0
- package/server/routes/skills.js +454 -0
- package/server/routes/worktrees.js +93 -0
- package/server/utils/git-worktree.js +297 -0
- package/server/ws-handler.js +708 -629
- package/server.js +31 -4
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
A browser-based UI for <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> — chat, workflows, agents, cost tracking, and more.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.producthunt.com/products/claudeck?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-claudeck" target="_blank" rel="noopener noreferrer"><img alt="Claudeck on Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1105387&theme=light&t=1774340505179" /></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
11
15
|
<p align="center">
|
|
12
16
|
<a href="https://www.npmjs.com/package/claudeck"><img src="https://img.shields.io/npm/v/claudeck?color=cb3837&label=npm" alt="npm version" /></a>
|
|
13
17
|
<a href="https://github.com/hamedafarag/claudeck/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/claudeck?color=blue" alt="license" /></a>
|
|
@@ -27,6 +31,9 @@ npx claudeck
|
|
|
27
31
|
# Custom port
|
|
28
32
|
npx claudeck --port 3000
|
|
29
33
|
|
|
34
|
+
# Enable authentication (for remote access via Cloudflare Tunnel, etc.)
|
|
35
|
+
npx claudeck --auth
|
|
36
|
+
|
|
30
37
|
# Or install globally
|
|
31
38
|
npm install -g claudeck
|
|
32
39
|
claudeck
|
|
@@ -42,10 +49,11 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
|
|
|
42
49
|
|
|
43
50
|
## Why Claudeck?
|
|
44
51
|
|
|
45
|
-
- **Zero-framework** — Vanilla JS, 6 npm dependencies, no build step
|
|
52
|
+
- **Zero-framework** — Vanilla JS + Web Components, 6 npm dependencies, no build step
|
|
46
53
|
- **Full agent orchestration** — Chains, DAGs, orchestrator, and monitoring dashboard
|
|
47
54
|
- **Persistent memory** — Cross-session project knowledge with FTS5 search and AI optimization
|
|
48
55
|
- **Cost visibility** — Per-session tracking, daily charts, token breakdowns
|
|
56
|
+
- **Secure remote access** — Token-based auth for Cloudflare Tunnel or reverse proxy setups
|
|
49
57
|
- **Works everywhere** — PWA, mobile responsive, Telegram AFK approval
|
|
50
58
|
- **Extensible** — Full-stack plugin system with auto-discovery
|
|
51
59
|
|
|
@@ -59,6 +67,8 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
|
|
|
59
67
|
- **Parallel mode** — 2x2 grid of 4 independent conversations
|
|
60
68
|
- Background sessions that keep running when you switch away
|
|
61
69
|
- Session search, pinning, auto-generated titles
|
|
70
|
+
- **Session branching** — fork any conversation at an assistant message to explore alternatives
|
|
71
|
+
- **Message recall** — press `↑` on empty input to cycle through previous messages, or click the history button to browse and re-use
|
|
62
72
|
- Voice input via Web Speech API (Chrome/Safari)
|
|
63
73
|
|
|
64
74
|
### Autonomous Agents
|
|
@@ -78,7 +88,9 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
|
|
|
78
88
|
### Code & Files
|
|
79
89
|
|
|
80
90
|
- **File Explorer** — Lazy tree, syntax-highlighted preview, drag-to-chat
|
|
81
|
-
- **
|
|
91
|
+
- **File Picker** — Attach files with type dots, binary detection, search, selected chips
|
|
92
|
+
- **Git Panel** — Branch switching, staging, commit, log, inline diff viewer
|
|
93
|
+
- **Git Worktrees** — Run any chat/agent task in an isolated worktree; merge, diff, or discard results
|
|
82
94
|
- **Repos Manager** — Organize repos in nested groups with GitHub links
|
|
83
95
|
- Code diff viewer with LCS-based line highlighting
|
|
84
96
|
|
|
@@ -97,6 +109,25 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
|
|
|
97
109
|
- AI-powered optimization (consolidation via Claude Haiku)
|
|
98
110
|
- Memory panel in right sidebar with search, filtering, and inline editing
|
|
99
111
|
|
|
112
|
+
### Notifications
|
|
113
|
+
|
|
114
|
+
- **Notification Bell** — Persistent notification history with unread badge in the header
|
|
115
|
+
- Background session events (completed, errored, input needed) logged automatically
|
|
116
|
+
- Agent completion/error notifications with cost and duration metrics
|
|
117
|
+
- Full history modal with type/status filters, bulk actions, and pagination
|
|
118
|
+
- 4 read strategies: explicit click, mark all, auto-read on view, click-through to session
|
|
119
|
+
- Real-time cross-tab sync via WebSocket broadcasts
|
|
120
|
+
|
|
121
|
+
### Skills Marketplace
|
|
122
|
+
|
|
123
|
+
- **SkillsMP Integration** — Browse and install agent skills from the [SkillsMP](https://skillsmp.com/) registry
|
|
124
|
+
- Keyword search and AI semantic search with mode toggle
|
|
125
|
+
- Install skills globally (`~/.claude/skills/`) or per-project (`.claude/skills/`)
|
|
126
|
+
- Enable/disable skills via toggle (renames `SKILL.md` ↔ `SKILL.md.disabled`)
|
|
127
|
+
- Installed skills auto-register as `/` slash commands
|
|
128
|
+
- "Skill used" system messages in chat for both user-invoked and model-invoked skills
|
|
129
|
+
- Token-gated — enter your free SkillsMP API key to activate
|
|
130
|
+
|
|
100
131
|
### Integrations
|
|
101
132
|
|
|
102
133
|
- **MCP Manager** — Add/edit/remove MCP servers (global + per-project)
|
|
@@ -119,8 +150,9 @@ User data lives in `~/.claudeck/` (config, database, plugins) — safe for NPX u
|
|
|
119
150
|
| **Confirm All** | Prompt for every tool call |
|
|
120
151
|
| **Plan Mode** | No execution, planning only |
|
|
121
152
|
|
|
122
|
-
###
|
|
153
|
+
### Security & UI
|
|
123
154
|
|
|
155
|
+
- **Authentication** — `--auth` flag enables token-based auth with login page, HttpOnly cookies, and WebSocket verification. Localhost bypasses auth by default (auto-detected proxy headers like `X-Forwarded-For` disable the bypass for tunneled requests).
|
|
124
156
|
- Dark theme (terminal CRT aesthetic) and light theme
|
|
125
157
|
- Installable as a PWA with offline fallback
|
|
126
158
|
- Mobile responsive with tablet/mobile breakpoints
|
|
@@ -138,7 +170,10 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
|
|
|
138
170
|
server/agent-loop.js ├── config/ (JSON configs)
|
|
139
171
|
server/orchestrator.js ├── plugins/ (user plugins)
|
|
140
172
|
server/dag-executor.js ├── data.db (SQLite + memories)
|
|
141
|
-
server/
|
|
173
|
+
server/notification-logger.js
|
|
174
|
+
server/utils/git-worktree.js
|
|
175
|
+
server/auth.js
|
|
176
|
+
server/memory-optimizer.js └── .env (VAPID keys, auth token)
|
|
142
177
|
plugins/
|
|
143
178
|
```
|
|
144
179
|
|
|
@@ -148,7 +183,8 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
|
|
|
148
183
|
| Backend | Express 4, WebSocket (ws 8), web-push 3 |
|
|
149
184
|
| AI SDK | @anthropic-ai/claude-code |
|
|
150
185
|
| Database | SQLite via better-sqlite3 (WAL mode) |
|
|
151
|
-
| Frontend | Vanilla JS ES modules, CSS custom properties |
|
|
186
|
+
| Frontend | Vanilla JS ES modules + Web Components (Light DOM), CSS custom properties |
|
|
187
|
+
| Testing | Vitest + happy-dom (2,400+ unit tests, 55% coverage) + WS perf benchmarks |
|
|
152
188
|
| Rendering | highlight.js, Mermaid (diagrams) — CDN |
|
|
153
189
|
|
|
154
190
|
---
|
|
@@ -158,7 +194,7 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
|
|
|
158
194
|
```
|
|
159
195
|
/clear /new /parallel /export /theme /shortcuts App
|
|
160
196
|
/costs /analytics Dashboards
|
|
161
|
-
/files /git /repos /events /mcp /tips
|
|
197
|
+
/files /git /repos /events /mcp /tips /skills Panels
|
|
162
198
|
/remember Memory
|
|
163
199
|
/review-pr /onboard-repo /migration-plan /code-health Workflows
|
|
164
200
|
/agent-pr-reviewer /agent-bug-hunter /agent-test-writer Agents
|
|
@@ -177,8 +213,9 @@ browser ──── WebSocket ──── server.js ──── Claude Code S
|
|
|
177
213
|
| `Cmd+N` | New session |
|
|
178
214
|
| `Cmd+B` | Toggle right panel |
|
|
179
215
|
| `Cmd+/` | Show all shortcuts |
|
|
180
|
-
| `Cmd+Shift+E/G/R/V/T` | Files / Git / Repos / Events / Tips
|
|
216
|
+
| `Cmd+Shift+E/G/R/V/T` | Files / Git / Repos / Events / Tips |
|
|
181
217
|
| `Cmd+1`-`4` | Focus parallel pane |
|
|
218
|
+
| `↑` / `↓` | Recall previous/next message (empty input) |
|
|
182
219
|
|
|
183
220
|
---
|
|
184
221
|
|
|
@@ -197,10 +234,11 @@ All user data lives in `~/.claudeck/` (override with `CLAUDECK_HOME`):
|
|
|
197
234
|
│ ├── agent-dags.json Dependency graphs
|
|
198
235
|
│ ├── repos.json Repository groups
|
|
199
236
|
│ ├── bot-prompt.json Assistant bot prompt
|
|
200
|
-
│
|
|
237
|
+
│ ├── telegram-config.json Telegram config
|
|
238
|
+
│ └── skillsmp-config.json Skills Marketplace config
|
|
201
239
|
├── plugins/ User-installed plugins
|
|
202
240
|
├── data.db SQLite database
|
|
203
|
-
└── .env VAPID keys, port
|
|
241
|
+
└── .env VAPID keys, port, auth token
|
|
204
242
|
```
|
|
205
243
|
|
|
206
244
|
Defaults are copied on first run. User edits are never overwritten on upgrade.
|
|
@@ -246,6 +284,52 @@ npx skills add https://github.com/hamedafarag/claudeck-skills
|
|
|
246
284
|
|
|
247
285
|
---
|
|
248
286
|
|
|
287
|
+
## Testing
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
npm test # Run all 2,400+ tests
|
|
291
|
+
npm test -- --coverage # With coverage report
|
|
292
|
+
npm run test:perf # WebSocket performance benchmarks
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
| Layer | Tests | Coverage |
|
|
296
|
+
|-------|-------|----------|
|
|
297
|
+
| **components/** (Web Components) | 170+ | 100% |
|
|
298
|
+
| **core/** | 110+ | 90% |
|
|
299
|
+
| **ui/** | 280+ | 65% |
|
|
300
|
+
| **features/** | 210+ | 22% |
|
|
301
|
+
| **panels/** | 150+ | 35% |
|
|
302
|
+
| **server/** | 1,350+ | 95% |
|
|
303
|
+
|
|
304
|
+
19 Web Components in `public/js/components/` — each is a self-contained Custom Element (Light DOM) that owns its HTML, testable with zero mocks.
|
|
305
|
+
|
|
306
|
+
### Performance Benchmarks
|
|
307
|
+
|
|
308
|
+
The `test:perf` suite measures WebSocket relay performance with real TCP connections over localhost (no mocked sockets). Results from 4 scenarios:
|
|
309
|
+
|
|
310
|
+
**Approval Round-Trip Latency** — server sends `permission_request` → client responds → server receives:
|
|
311
|
+
|
|
312
|
+
| Concurrent Sessions | p50 | p95 | p99 |
|
|
313
|
+
|---|---|---|---|
|
|
314
|
+
| 1 | 70 µs | 132 µs | 196 µs |
|
|
315
|
+
| 5 | 187 µs | 222 µs | 244 µs |
|
|
316
|
+
| 10 | 300 µs | 466 µs | 721 µs |
|
|
317
|
+
| 25 | 382 µs | 570 µs | 764 µs |
|
|
318
|
+
|
|
319
|
+
**Message Throughput** — streaming text chunks to connected clients:
|
|
320
|
+
|
|
321
|
+
| Clients | Total msg/s |
|
|
322
|
+
|---|---|
|
|
323
|
+
| 1 | ~295k |
|
|
324
|
+
| 10 | ~393k |
|
|
325
|
+
| 50 | ~435k |
|
|
326
|
+
|
|
327
|
+
**Connection Scaling** — 100 simultaneous connections: p50 establish time 156 µs, ~35 KB memory per connection.
|
|
328
|
+
|
|
329
|
+
**Broadcast Fan-Out** — notification delivery to all connected clients: p50 under 1 ms even at 100 clients.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
249
333
|
## Contributing
|
|
250
334
|
|
|
251
335
|
Contributions are welcome! Fork the repo, make your changes, and open a PR.
|
|
@@ -255,6 +339,7 @@ git clone https://github.com/hamedafarag/claudeck.git
|
|
|
255
339
|
cd claudeck
|
|
256
340
|
npm install
|
|
257
341
|
npm start
|
|
342
|
+
npm test # Run tests before submitting
|
|
258
343
|
```
|
|
259
344
|
|
|
260
345
|
See [DOCUMENTATION.md](docs/DOCUMENTATION.md) for architecture details and [CONFIGURATION.md](docs/CONFIGURATION.md) for the config system.
|
package/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "os";
|
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5
5
|
import { createInterface } from "readline";
|
|
6
|
+
import crypto from "crypto";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_PORT = 9009;
|
|
8
9
|
const envDir = process.env.CLAUDECK_HOME || join(homedir(), ".claudeck");
|
|
@@ -13,16 +14,21 @@ function readEnv() {
|
|
|
13
14
|
try { return readFileSync(envPath, "utf-8"); } catch { return ""; }
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function
|
|
17
|
+
function saveEnvVar(key, value) {
|
|
17
18
|
let content = readEnv();
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const re = new RegExp(`^${key}=.*`, "m");
|
|
20
|
+
if (re.test(content)) {
|
|
21
|
+
content = content.replace(re, `${key}=${value}`);
|
|
20
22
|
} else {
|
|
21
|
-
content = content.trimEnd() + `\
|
|
23
|
+
content = content.trimEnd() + `\n${key}=${value}\n`;
|
|
22
24
|
}
|
|
23
25
|
writeFileSync(envPath, content);
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
function savePort(port) {
|
|
29
|
+
saveEnvVar("PORT", port);
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
function getSavedPort() {
|
|
27
33
|
const match = readEnv().match(/^PORT=(\d+)/m);
|
|
28
34
|
return match ? match[1] : null;
|
|
@@ -35,7 +41,50 @@ function ask(question) {
|
|
|
35
41
|
});
|
|
36
42
|
}
|
|
37
43
|
|
|
44
|
+
function handleAuthFlags() {
|
|
45
|
+
// --no-auth: explicitly disable for this run
|
|
46
|
+
if (process.argv.includes("--no-auth")) {
|
|
47
|
+
process.env.CLAUDECK_AUTH = "false";
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --token <value> or --token=<value>: set custom token + enable auth
|
|
52
|
+
const tokenArg = process.argv.find(a => a.startsWith("--token"));
|
|
53
|
+
if (tokenArg) {
|
|
54
|
+
const token = tokenArg.includes("=")
|
|
55
|
+
? tokenArg.split("=")[1]
|
|
56
|
+
: process.argv[process.argv.indexOf(tokenArg) + 1];
|
|
57
|
+
if (token) {
|
|
58
|
+
process.env.CLAUDECK_TOKEN = token;
|
|
59
|
+
process.env.CLAUDECK_AUTH = "true";
|
|
60
|
+
saveEnvVar("CLAUDECK_TOKEN", token);
|
|
61
|
+
saveEnvVar("CLAUDECK_AUTH", "true");
|
|
62
|
+
console.log(`\x1b[2m Auth token set and saved to ~/.claudeck/.env\x1b[0m`);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --auth: enable auth, auto-generate token if missing
|
|
68
|
+
if (process.argv.includes("--auth")) {
|
|
69
|
+
process.env.CLAUDECK_AUTH = "true";
|
|
70
|
+
const envContent = readEnv();
|
|
71
|
+
const existingToken = envContent.match(/^CLAUDECK_TOKEN=(.+)/m);
|
|
72
|
+
if (existingToken) {
|
|
73
|
+
process.env.CLAUDECK_TOKEN = existingToken[1];
|
|
74
|
+
} else {
|
|
75
|
+
const token = crypto.randomBytes(32).toString("hex");
|
|
76
|
+
process.env.CLAUDECK_TOKEN = token;
|
|
77
|
+
saveEnvVar("CLAUDECK_TOKEN", token);
|
|
78
|
+
saveEnvVar("CLAUDECK_AUTH", "true");
|
|
79
|
+
console.log(`\x1b[2m Generated auth token and saved to ~/.claudeck/.env\x1b[0m`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
38
84
|
async function main() {
|
|
85
|
+
// Handle auth flags before anything else
|
|
86
|
+
handleAuthFlags();
|
|
87
|
+
|
|
39
88
|
// --port flag takes priority
|
|
40
89
|
const portArg = process.argv.find(a => a.startsWith('--port'));
|
|
41
90
|
if (portArg) {
|
package/db.js
CHANGED
|
@@ -81,6 +81,9 @@ try { db.exec(`ALTER TABLE sessions ADD COLUMN summary TEXT DEFAULT NULL`); } ca
|
|
|
81
81
|
try { db.exec(`ALTER TABLE todos ADD COLUMN archived INTEGER DEFAULT 0`); } catch { /* exists */ }
|
|
82
82
|
// Todo priority (0=none, 1=low, 2=medium, 3=high)
|
|
83
83
|
try { db.exec(`ALTER TABLE todos ADD COLUMN priority INTEGER DEFAULT 0`); } catch { /* exists */ }
|
|
84
|
+
// Session branching / conversation forking
|
|
85
|
+
try { db.exec(`ALTER TABLE sessions ADD COLUMN parent_session_id TEXT DEFAULT NULL`); } catch { /* exists */ }
|
|
86
|
+
try { db.exec(`ALTER TABLE sessions ADD COLUMN fork_message_id INTEGER DEFAULT NULL`); } catch { /* exists */ }
|
|
84
87
|
|
|
85
88
|
// Agent context (shared memory between agents in a chain/orchestration run)
|
|
86
89
|
db.exec(`
|
|
@@ -167,6 +170,41 @@ db.exec(`
|
|
|
167
170
|
END;
|
|
168
171
|
`);
|
|
169
172
|
|
|
173
|
+
// ── Notifications table ──────────────────────────────────
|
|
174
|
+
db.exec(`
|
|
175
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
176
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
177
|
+
type TEXT NOT NULL,
|
|
178
|
+
title TEXT NOT NULL,
|
|
179
|
+
body TEXT,
|
|
180
|
+
metadata TEXT,
|
|
181
|
+
source_session_id TEXT,
|
|
182
|
+
source_agent_id TEXT,
|
|
183
|
+
read_at INTEGER DEFAULT NULL,
|
|
184
|
+
created_at INTEGER DEFAULT (unixepoch())
|
|
185
|
+
);
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_notif_created ON notifications(created_at DESC);
|
|
187
|
+
CREATE INDEX IF NOT EXISTS idx_notif_unread ON notifications(read_at) WHERE read_at IS NULL;
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
// ── Worktrees table ──────────────────────────────────────
|
|
191
|
+
db.exec(`
|
|
192
|
+
CREATE TABLE IF NOT EXISTS worktrees (
|
|
193
|
+
id TEXT PRIMARY KEY,
|
|
194
|
+
session_id TEXT,
|
|
195
|
+
project_path TEXT NOT NULL,
|
|
196
|
+
worktree_path TEXT NOT NULL,
|
|
197
|
+
branch_name TEXT NOT NULL,
|
|
198
|
+
base_branch TEXT NOT NULL,
|
|
199
|
+
status TEXT DEFAULT 'active',
|
|
200
|
+
user_prompt TEXT,
|
|
201
|
+
created_at INTEGER DEFAULT (unixepoch()),
|
|
202
|
+
completed_at INTEGER DEFAULT NULL
|
|
203
|
+
);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_wt_project ON worktrees(project_path);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_wt_status ON worktrees(status);
|
|
206
|
+
`);
|
|
207
|
+
|
|
170
208
|
// Backfill content_hash for existing rows
|
|
171
209
|
const unhashed = db.prepare(`SELECT id, project_path, content FROM memories WHERE content_hash IS NULL`).all();
|
|
172
210
|
if (unhashed.length > 0) {
|
|
@@ -208,6 +246,7 @@ db.exec(`
|
|
|
208
246
|
CREATE INDEX IF NOT EXISTS idx_costs_created_at ON costs(created_at);
|
|
209
247
|
CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path);
|
|
210
248
|
CREATE INDEX IF NOT EXISTS idx_sessions_pinned_last_used ON sessions(pinned DESC, last_used_at DESC);
|
|
249
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_parent ON sessions(parent_session_id) WHERE parent_session_id IS NOT NULL;
|
|
211
250
|
`);
|
|
212
251
|
|
|
213
252
|
// Deduplicated mode CASE subquery — used in 4 session listing queries
|
|
@@ -289,6 +328,22 @@ const stmts = {
|
|
|
289
328
|
`SELECT s.*, ${MODE_CASE}
|
|
290
329
|
FROM sessions s WHERE (s.title LIKE ? OR s.project_name LIKE ?) ORDER BY s.pinned DESC, s.last_used_at DESC LIMIT ?`
|
|
291
330
|
),
|
|
331
|
+
// Session branching
|
|
332
|
+
getMessagesByIdRange: db.prepare(
|
|
333
|
+
`SELECT role, content, created_at FROM messages WHERE session_id = ? AND id <= ? AND chat_id IS NULL ORDER BY id ASC`
|
|
334
|
+
),
|
|
335
|
+
getLastMessageId: db.prepare(
|
|
336
|
+
`SELECT MAX(id) as maxId FROM messages WHERE session_id = ? AND chat_id IS NULL`
|
|
337
|
+
),
|
|
338
|
+
getBranches: db.prepare(
|
|
339
|
+
`SELECT s.*, ${MODE_CASE} FROM sessions s WHERE s.parent_session_id = ? ORDER BY s.created_at DESC`
|
|
340
|
+
),
|
|
341
|
+
getBranchCount: db.prepare(
|
|
342
|
+
`SELECT COUNT(*) as count FROM sessions WHERE parent_session_id = ?`
|
|
343
|
+
),
|
|
344
|
+
orphanChildren: db.prepare(
|
|
345
|
+
`UPDATE sessions SET parent_session_id = NULL WHERE parent_session_id = ?`
|
|
346
|
+
),
|
|
292
347
|
getSessionCosts: db.prepare(
|
|
293
348
|
`SELECT s.id, s.title, s.project_name, s.last_used_at,
|
|
294
349
|
COALESCE(SUM(c.cost_usd), 0) AS total_cost,
|
|
@@ -460,12 +515,74 @@ export function searchSessions(query, limit = 20, projectPath) {
|
|
|
460
515
|
}
|
|
461
516
|
|
|
462
517
|
export const deleteSession = db.transaction((id) => {
|
|
518
|
+
// Orphan child forks before deleting parent
|
|
519
|
+
stmts.orphanChildren.run(id);
|
|
463
520
|
db.prepare("DELETE FROM claude_sessions WHERE session_id = ?").run(id);
|
|
464
521
|
db.prepare("DELETE FROM costs WHERE session_id = ?").run(id);
|
|
465
522
|
db.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
|
|
466
523
|
db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
|
|
467
524
|
});
|
|
468
525
|
|
|
526
|
+
// ── Session Branching / Forking ─────────────────────────
|
|
527
|
+
export const forkSession = db.transaction((parentSessionId, forkMessageId) => {
|
|
528
|
+
const parent = stmts.getSession.get(parentSessionId);
|
|
529
|
+
if (!parent) throw new Error("Session not found");
|
|
530
|
+
|
|
531
|
+
if (!forkMessageId) {
|
|
532
|
+
const last = stmts.getLastMessageId.get(parentSessionId);
|
|
533
|
+
forkMessageId = last?.maxId;
|
|
534
|
+
if (!forkMessageId) throw new Error("No messages to fork");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const newId = createHash("sha256")
|
|
538
|
+
.update(parentSessionId + Date.now() + Math.random())
|
|
539
|
+
.digest("hex")
|
|
540
|
+
.slice(0, 36);
|
|
541
|
+
const title = `Fork of: ${parent.title || parent.project_name || "Untitled"}`;
|
|
542
|
+
|
|
543
|
+
db.prepare(
|
|
544
|
+
`INSERT INTO sessions (id, project_name, project_path, title, parent_session_id, fork_message_id)
|
|
545
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
546
|
+
).run(newId, parent.project_name, parent.project_path, title, parentSessionId, forkMessageId);
|
|
547
|
+
|
|
548
|
+
const messages = stmts.getMessagesByIdRange.all(parentSessionId, forkMessageId);
|
|
549
|
+
const insertMsg = db.prepare(
|
|
550
|
+
"INSERT INTO messages (session_id, role, content, created_at) VALUES (?, ?, ?, ?)"
|
|
551
|
+
);
|
|
552
|
+
for (const msg of messages) {
|
|
553
|
+
insertMsg.run(newId, msg.role, msg.content, msg.created_at);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return stmts.getSession.get(newId);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
export function getSessionBranches(sessionId) {
|
|
560
|
+
return stmts.getBranches.all(sessionId);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export function getSessionBranchCount(sessionId) {
|
|
564
|
+
return stmts.getBranchCount.get(sessionId).count;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function getSessionLineage(sessionId) {
|
|
568
|
+
const ancestors = [];
|
|
569
|
+
let current = stmts.getSession.get(sessionId);
|
|
570
|
+
while (current && current.parent_session_id) {
|
|
571
|
+
const parent = stmts.getSession.get(current.parent_session_id);
|
|
572
|
+
if (!parent) break;
|
|
573
|
+
ancestors.unshift(parent);
|
|
574
|
+
current = parent;
|
|
575
|
+
}
|
|
576
|
+
// Get siblings (other forks of the same parent)
|
|
577
|
+
const session = stmts.getSession.get(sessionId);
|
|
578
|
+
let siblings = [];
|
|
579
|
+
if (session?.parent_session_id) {
|
|
580
|
+
siblings = stmts.getBranches.all(session.parent_session_id)
|
|
581
|
+
.filter(s => s.id !== sessionId);
|
|
582
|
+
}
|
|
583
|
+
return { ancestors, siblings };
|
|
584
|
+
}
|
|
585
|
+
|
|
469
586
|
export function getSessionCosts(projectPath) {
|
|
470
587
|
if (projectPath) {
|
|
471
588
|
return stmts.getSessionCosts.all(projectPath);
|
|
@@ -1263,6 +1380,137 @@ export function getAgentRunsDaily() {
|
|
|
1263
1380
|
return runStmts.dailyRuns.all();
|
|
1264
1381
|
}
|
|
1265
1382
|
|
|
1383
|
+
// ── Notifications ────────────────────────────────────────
|
|
1384
|
+
const notifStmts = {
|
|
1385
|
+
insert: db.prepare(
|
|
1386
|
+
`INSERT INTO notifications (type, title, body, metadata, source_session_id, source_agent_id)
|
|
1387
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1388
|
+
),
|
|
1389
|
+
history: db.prepare(
|
|
1390
|
+
`SELECT * FROM notifications ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
1391
|
+
),
|
|
1392
|
+
historyUnread: db.prepare(
|
|
1393
|
+
`SELECT * FROM notifications WHERE read_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
1394
|
+
),
|
|
1395
|
+
historyByType: db.prepare(
|
|
1396
|
+
`SELECT * FROM notifications WHERE type = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
1397
|
+
),
|
|
1398
|
+
historyByTypeUnread: db.prepare(
|
|
1399
|
+
`SELECT * FROM notifications WHERE type = ? AND read_at IS NULL ORDER BY created_at DESC LIMIT ? OFFSET ?`
|
|
1400
|
+
),
|
|
1401
|
+
unreadCount: db.prepare(
|
|
1402
|
+
`SELECT COUNT(*) as count FROM notifications WHERE read_at IS NULL`
|
|
1403
|
+
),
|
|
1404
|
+
markRead: db.prepare(
|
|
1405
|
+
`UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`
|
|
1406
|
+
),
|
|
1407
|
+
markAllRead: db.prepare(
|
|
1408
|
+
`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`
|
|
1409
|
+
),
|
|
1410
|
+
markReadBefore: db.prepare(
|
|
1411
|
+
`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL AND created_at < ?`
|
|
1412
|
+
),
|
|
1413
|
+
purgeOld: db.prepare(
|
|
1414
|
+
`DELETE FROM notifications WHERE created_at < unixepoch() - (? * 86400)`
|
|
1415
|
+
),
|
|
1416
|
+
markStaleRead: db.prepare(
|
|
1417
|
+
`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL AND created_at < unixepoch() - (7 * 86400)`
|
|
1418
|
+
),
|
|
1419
|
+
};
|
|
1420
|
+
|
|
1421
|
+
export function createNotification(type, title, body = null, metadata = null, sourceSessionId = null, sourceAgentId = null) {
|
|
1422
|
+
const result = notifStmts.insert.run(type, title, body, metadata, sourceSessionId, sourceAgentId);
|
|
1423
|
+
return {
|
|
1424
|
+
id: result.lastInsertRowid,
|
|
1425
|
+
type, title, body, metadata,
|
|
1426
|
+
source_session_id: sourceSessionId,
|
|
1427
|
+
source_agent_id: sourceAgentId,
|
|
1428
|
+
read_at: null,
|
|
1429
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
export function getNotificationHistory(limit = 20, offset = 0, unreadOnly = false, type = null) {
|
|
1434
|
+
if (type && unreadOnly) return notifStmts.historyByTypeUnread.all(type, limit, offset);
|
|
1435
|
+
if (type) return notifStmts.historyByType.all(type, limit, offset);
|
|
1436
|
+
if (unreadOnly) return notifStmts.historyUnread.all(limit, offset);
|
|
1437
|
+
return notifStmts.history.all(limit, offset);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
export function getUnreadNotificationCount() {
|
|
1441
|
+
return notifStmts.unreadCount.get().count;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
export function markNotificationsRead(ids) {
|
|
1445
|
+
const tx = db.transaction((idList) => {
|
|
1446
|
+
for (const id of idList) notifStmts.markRead.run(id);
|
|
1447
|
+
});
|
|
1448
|
+
tx(ids);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
export function markAllNotificationsRead() {
|
|
1452
|
+
notifStmts.markAllRead.run();
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
export function markNotificationsReadBefore(timestamp) {
|
|
1456
|
+
notifStmts.markReadBefore.run(timestamp);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
export function purgeOldNotifications(days = 90) {
|
|
1460
|
+
notifStmts.markStaleRead.run();
|
|
1461
|
+
notifStmts.purgeOld.run(days);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// ── Worktrees ─────────────────────────────────────────────
|
|
1465
|
+
const wtStmts = {
|
|
1466
|
+
create: db.prepare(
|
|
1467
|
+
`INSERT INTO worktrees (id, session_id, project_path, worktree_path, branch_name, base_branch, status, user_prompt)
|
|
1468
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`
|
|
1469
|
+
),
|
|
1470
|
+
get: db.prepare(`SELECT * FROM worktrees WHERE id = ?`),
|
|
1471
|
+
listByProject: db.prepare(
|
|
1472
|
+
`SELECT * FROM worktrees WHERE project_path = ? ORDER BY created_at DESC`
|
|
1473
|
+
),
|
|
1474
|
+
listActive: db.prepare(
|
|
1475
|
+
`SELECT * FROM worktrees WHERE status IN ('active', 'completed') ORDER BY created_at DESC`
|
|
1476
|
+
),
|
|
1477
|
+
updateStatus: db.prepare(
|
|
1478
|
+
`UPDATE worktrees SET status = ?, completed_at = unixepoch() WHERE id = ?`
|
|
1479
|
+
),
|
|
1480
|
+
updateSession: db.prepare(
|
|
1481
|
+
`UPDATE worktrees SET session_id = ? WHERE id = ?`
|
|
1482
|
+
),
|
|
1483
|
+
delete: db.prepare(`DELETE FROM worktrees WHERE id = ?`),
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
export function createWorktreeRecord(id, sessionId, projectPath, worktreePath, branchName, baseBranch, userPrompt) {
|
|
1487
|
+
wtStmts.create.run(id, sessionId, projectPath, worktreePath, branchName, baseBranch, userPrompt);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
export function getWorktreeRecord(id) {
|
|
1491
|
+
return wtStmts.get.get(id);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
export function listWorktreesByProject(projectPath) {
|
|
1495
|
+
return wtStmts.listByProject.all(projectPath);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export function listActiveWorktrees() {
|
|
1499
|
+
return wtStmts.listActive.all();
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
export function updateWorktreeStatus(id, status) {
|
|
1503
|
+
wtStmts.updateStatus.run(status, id);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
export function updateWorktreeSession(id, sessionId) {
|
|
1507
|
+
wtStmts.updateSession.run(sessionId, id);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
export function deleteWorktreeRecord(id) {
|
|
1511
|
+
wtStmts.delete.run(id);
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1266
1514
|
// ── Memories (persistent cross-session context) ──────────
|
|
1267
1515
|
function hashContent(projectPath, content) {
|
|
1268
1516
|
return createHash("sha256").update(`${projectPath}:${content}`).digest("hex");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
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",
|
|
@@ -42,7 +42,11 @@
|
|
|
42
42
|
"node": ">=18.0.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"start": "node server.js"
|
|
45
|
+
"start": "node server.js",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"test:coverage": "vitest run --coverage",
|
|
49
|
+
"test:perf": "vitest run --config vitest.config.perf.js"
|
|
46
50
|
},
|
|
47
51
|
"dependencies": {
|
|
48
52
|
"@anthropic-ai/claude-code": "^1.0.128",
|
|
@@ -51,5 +55,11 @@
|
|
|
51
55
|
"express": "^4",
|
|
52
56
|
"web-push": "^3.6.7",
|
|
53
57
|
"ws": "^8"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
61
|
+
"happy-dom": "^20.8.4",
|
|
62
|
+
"supertest": "^7.2.2",
|
|
63
|
+
"vitest": "^4.1.0"
|
|
54
64
|
}
|
|
55
65
|
}
|
|
@@ -150,8 +150,8 @@ body.sidebar-open #sidebar-backdrop {
|
|
|
150
150
|
padding: 6px 8px;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/* Hide toolbox
|
|
154
|
-
.
|
|
153
|
+
/* Hide toolbox strip on mobile */
|
|
154
|
+
.toolbox-strip {
|
|
155
155
|
display: none;
|
|
156
156
|
}
|
|
157
157
|
|