@atercates/claude-deck 0.2.1 → 0.2.3
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 +4 -18
- package/app/api/auth/login/route.ts +57 -0
- package/app/api/auth/logout/route.ts +13 -0
- package/app/api/auth/session/route.ts +29 -0
- package/app/api/auth/setup/route.ts +67 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +1 -1
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +1 -1
- package/app/api/sessions/[id]/fork/route.ts +1 -1
- package/app/api/sessions/[id]/pr/route.ts +1 -1
- package/app/api/sessions/[id]/preview/route.ts +1 -1
- package/app/api/sessions/[id]/route.ts +13 -4
- package/app/api/sessions/[id]/send-keys/route.ts +1 -1
- package/app/api/sessions/route.ts +2 -2
- package/app/login/page.tsx +192 -0
- package/app/setup/page.tsx +279 -0
- package/components/ConductorPanel.tsx +1 -1
- package/components/DevServers/ServerLogsModal.tsx +24 -21
- package/components/DiffViewer/DiffModal.tsx +0 -1
- package/components/FileExplorer/index.tsx +1 -1
- package/components/GitDrawer/FileEditDialog.tsx +1 -1
- package/components/GitPanel/FileChanges.tsx +6 -2
- package/components/GitPanel/index.tsx +1 -1
- package/components/Pane/index.tsx +16 -15
- package/components/Projects/ProjectCard.tsx +1 -1
- package/components/QuickSwitcher.tsx +1 -0
- package/components/SessionList/SessionList.types.ts +1 -1
- package/components/SessionList/index.tsx +8 -8
- package/components/Terminal/hooks/useTerminalConnection.ts +3 -2
- package/components/Terminal/hooks/websocket-connection.ts +1 -0
- package/data/git/queries.ts +0 -1
- package/lib/auth/index.ts +15 -0
- package/lib/auth/password.ts +14 -0
- package/lib/auth/rate-limit.ts +40 -0
- package/lib/auth/session.ts +83 -0
- package/lib/auth/totp.ts +36 -0
- package/lib/claude/process-manager.ts +1 -1
- package/lib/code-search.ts +5 -5
- package/lib/db/index.ts +1 -1
- package/lib/db/queries.ts +64 -0
- package/lib/db/schema.ts +19 -0
- package/lib/db/types.ts +16 -0
- package/lib/git-history.ts +1 -1
- package/lib/git.ts +0 -1
- package/lib/multi-repo-git.ts +0 -1
- package/lib/projects.ts +29 -8
- package/package.json +8 -4
- package/scripts/agent-os +1 -1
- package/scripts/install.sh +2 -2
- package/server.ts +20 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import { queries } from "@/lib/db";
|
|
3
|
+
import type { User } from "@/lib/db";
|
|
4
|
+
|
|
5
|
+
const SESSION_DURATION_DAYS = 30;
|
|
6
|
+
const SESSION_TOKEN_BYTES = 32;
|
|
7
|
+
|
|
8
|
+
export function createSession(userId: string): {
|
|
9
|
+
token: string;
|
|
10
|
+
expiresAt: string;
|
|
11
|
+
} {
|
|
12
|
+
const id = randomBytes(16).toString("hex");
|
|
13
|
+
const token = randomBytes(SESSION_TOKEN_BYTES).toString("hex");
|
|
14
|
+
const expiresAt = new Date(
|
|
15
|
+
Date.now() + SESSION_DURATION_DAYS * 24 * 60 * 60 * 1000
|
|
16
|
+
).toISOString();
|
|
17
|
+
|
|
18
|
+
queries.createAuthSession(id, token, userId, expiresAt);
|
|
19
|
+
|
|
20
|
+
return { token, expiresAt };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateSession(token: string): User | null {
|
|
24
|
+
if (!token || token.length !== SESSION_TOKEN_BYTES * 2) return null;
|
|
25
|
+
|
|
26
|
+
const session = queries.getAuthSessionByToken(token);
|
|
27
|
+
if (!session) return null;
|
|
28
|
+
|
|
29
|
+
if (new Date(session.expires_at) < new Date()) {
|
|
30
|
+
queries.deleteAuthSession(token);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const user = queries.getUserById(session.user_id);
|
|
35
|
+
if (!user) {
|
|
36
|
+
queries.deleteAuthSession(token);
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return user;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renewSession(token: string): void {
|
|
44
|
+
const expiresAt = new Date(
|
|
45
|
+
Date.now() + SESSION_DURATION_DAYS * 24 * 60 * 60 * 1000
|
|
46
|
+
).toISOString();
|
|
47
|
+
queries.renewAuthSession(token, expiresAt);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function deleteSession(token: string): void {
|
|
51
|
+
queries.deleteAuthSession(token);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function cleanupExpiredSessions(): void {
|
|
55
|
+
queries.deleteExpiredAuthSessions();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const COOKIE_NAME = "claude_deck_session";
|
|
59
|
+
|
|
60
|
+
export function buildSessionCookie(token: string): string {
|
|
61
|
+
const maxAge = SESSION_DURATION_DAYS * 24 * 60 * 60;
|
|
62
|
+
return `${COOKIE_NAME}=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${maxAge}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildClearCookie(): string {
|
|
66
|
+
return `${COOKIE_NAME}=; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=0`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseCookies(
|
|
70
|
+
cookieHeader: string | undefined
|
|
71
|
+
): Record<string, string> {
|
|
72
|
+
if (!cookieHeader) return {};
|
|
73
|
+
return Object.fromEntries(
|
|
74
|
+
cookieHeader.split(";").map((c) => {
|
|
75
|
+
const [key, ...rest] = c.trim().split("=");
|
|
76
|
+
return [key, rest.join("=")];
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function hasUsers(): boolean {
|
|
82
|
+
return queries.getUserCount() > 0;
|
|
83
|
+
}
|
package/lib/auth/totp.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { TOTP, Secret } from "otpauth";
|
|
2
|
+
|
|
3
|
+
const ISSUER = "ClaudeDeck";
|
|
4
|
+
|
|
5
|
+
export function generateTotpSecret(username: string): {
|
|
6
|
+
secret: string;
|
|
7
|
+
uri: string;
|
|
8
|
+
} {
|
|
9
|
+
const secret = new Secret({ size: 20 });
|
|
10
|
+
const totp = new TOTP({
|
|
11
|
+
issuer: ISSUER,
|
|
12
|
+
label: username,
|
|
13
|
+
algorithm: "SHA1",
|
|
14
|
+
digits: 6,
|
|
15
|
+
period: 30,
|
|
16
|
+
secret,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
secret: secret.base32,
|
|
21
|
+
uri: totp.toString(),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function verifyTotpCode(secret: string, code: string): boolean {
|
|
26
|
+
const totp = new TOTP({
|
|
27
|
+
issuer: ISSUER,
|
|
28
|
+
algorithm: "SHA1",
|
|
29
|
+
digits: 6,
|
|
30
|
+
period: 30,
|
|
31
|
+
secret: Secret.fromBase32(secret),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const delta = totp.validate({ token: code, window: 1 });
|
|
35
|
+
return delta !== null;
|
|
36
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, ChildProcess } from "child_process";
|
|
2
2
|
import { WebSocket } from "ws";
|
|
3
3
|
import { StreamParser } from "./stream-parser";
|
|
4
|
-
import { queries
|
|
4
|
+
import { queries } from "../db";
|
|
5
5
|
import type { ClaudeSessionOptions, ClientEvent } from "./types";
|
|
6
6
|
|
|
7
7
|
interface ManagedSession {
|
package/lib/code-search.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execSync, spawnSync } from "child_process";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Check if ripgrep is available on the system
|
|
@@ -46,13 +46,13 @@ export function searchCode(
|
|
|
46
46
|
const {
|
|
47
47
|
maxResults = 100,
|
|
48
48
|
contextLines = 2,
|
|
49
|
-
filePattern = "*",
|
|
50
|
-
caseSensitive = false,
|
|
49
|
+
filePattern: _filePattern = "*",
|
|
50
|
+
caseSensitive: _caseSensitive = false,
|
|
51
51
|
} = options;
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
54
|
// Use spawn instead of execSync for better control
|
|
55
|
-
|
|
55
|
+
// spawnSync imported at top level
|
|
56
56
|
|
|
57
57
|
const args = [
|
|
58
58
|
"--json",
|
|
@@ -100,7 +100,7 @@ export function searchCode(
|
|
|
100
100
|
} catch (error) {
|
|
101
101
|
console.error("Error in searchCode:", error);
|
|
102
102
|
// ENOENT = command not found
|
|
103
|
-
if ((error as
|
|
103
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
104
104
|
throw new Error(
|
|
105
105
|
"ripgrep (rg) not found. Install with: brew install ripgrep"
|
|
106
106
|
);
|
package/lib/db/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import os from "os";
|
|
4
5
|
import { createSchema } from "./schema";
|
|
@@ -14,7 +15,6 @@ let _db: Database.Database | null = null;
|
|
|
14
15
|
|
|
15
16
|
export function getDb(): Database.Database {
|
|
16
17
|
if (!_db) {
|
|
17
|
-
const fs = require("fs");
|
|
18
18
|
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
19
19
|
|
|
20
20
|
_db = new Database(DB_PATH);
|
package/lib/db/queries.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
ProjectDevServer,
|
|
7
7
|
ProjectRepository,
|
|
8
8
|
DevServer,
|
|
9
|
+
User,
|
|
10
|
+
AuthSession,
|
|
9
11
|
} from "./types";
|
|
10
12
|
|
|
11
13
|
function query<T>(sql: string, params: unknown[] = []): T[] {
|
|
@@ -457,4 +459,66 @@ export const queries = {
|
|
|
457
459
|
itemType,
|
|
458
460
|
itemId,
|
|
459
461
|
]),
|
|
462
|
+
|
|
463
|
+
getUserCount(): number {
|
|
464
|
+
return (
|
|
465
|
+
queryOne<{ count: number }>("SELECT COUNT(*) as count FROM users") ?? {
|
|
466
|
+
count: 0,
|
|
467
|
+
}
|
|
468
|
+
).count;
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
getUserByUsername(username: string): User | null {
|
|
472
|
+
return queryOne<User>("SELECT * FROM users WHERE username = ?", [username]);
|
|
473
|
+
},
|
|
474
|
+
|
|
475
|
+
getUserById(id: string): User | null {
|
|
476
|
+
return queryOne<User>("SELECT * FROM users WHERE id = ?", [id]);
|
|
477
|
+
},
|
|
478
|
+
|
|
479
|
+
createUser(
|
|
480
|
+
id: string,
|
|
481
|
+
username: string,
|
|
482
|
+
passwordHash: string,
|
|
483
|
+
totpSecret: string | null
|
|
484
|
+
): void {
|
|
485
|
+
execute(
|
|
486
|
+
"INSERT INTO users (id, username, password_hash, totp_secret) VALUES (?, ?, ?, ?)",
|
|
487
|
+
[id, username, passwordHash, totpSecret]
|
|
488
|
+
);
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
getAuthSessionByToken(token: string): AuthSession | null {
|
|
492
|
+
return queryOne<AuthSession>(
|
|
493
|
+
"SELECT * FROM auth_sessions WHERE token = ?",
|
|
494
|
+
[token]
|
|
495
|
+
);
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
createAuthSession(
|
|
499
|
+
id: string,
|
|
500
|
+
token: string,
|
|
501
|
+
userId: string,
|
|
502
|
+
expiresAt: string
|
|
503
|
+
): void {
|
|
504
|
+
execute(
|
|
505
|
+
"INSERT INTO auth_sessions (id, token, user_id, expires_at) VALUES (?, ?, ?, ?)",
|
|
506
|
+
[id, token, userId, expiresAt]
|
|
507
|
+
);
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
renewAuthSession(token: string, expiresAt: string): void {
|
|
511
|
+
execute("UPDATE auth_sessions SET expires_at = ? WHERE token = ?", [
|
|
512
|
+
expiresAt,
|
|
513
|
+
token,
|
|
514
|
+
]);
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
deleteAuthSession(token: string): void {
|
|
518
|
+
execute("DELETE FROM auth_sessions WHERE token = ?", [token]);
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
deleteExpiredAuthSessions(): void {
|
|
522
|
+
execute("DELETE FROM auth_sessions WHERE expires_at < datetime('now')");
|
|
523
|
+
},
|
|
460
524
|
};
|
package/lib/db/schema.ts
CHANGED
|
@@ -110,5 +110,24 @@ export function createSchema(db: Database.Database): void {
|
|
|
110
110
|
|
|
111
111
|
INSERT OR IGNORE INTO projects (id, name, working_directory, is_uncategorized, sort_order)
|
|
112
112
|
VALUES ('uncategorized', 'Uncategorized', '~', 1, 999999);
|
|
113
|
+
|
|
114
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
115
|
+
id TEXT PRIMARY KEY,
|
|
116
|
+
username TEXT NOT NULL UNIQUE,
|
|
117
|
+
password_hash TEXT NOT NULL,
|
|
118
|
+
totp_secret TEXT,
|
|
119
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
CREATE TABLE IF NOT EXISTS auth_sessions (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
token TEXT NOT NULL UNIQUE,
|
|
125
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
126
|
+
expires_at TEXT NOT NULL,
|
|
127
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_token ON auth_sessions(token);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions(expires_at);
|
|
113
132
|
`);
|
|
114
133
|
}
|
package/lib/db/types.ts
CHANGED
|
@@ -90,3 +90,19 @@ export interface DevServer {
|
|
|
90
90
|
created_at: string;
|
|
91
91
|
updated_at: string;
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
export interface User {
|
|
95
|
+
id: string;
|
|
96
|
+
username: string;
|
|
97
|
+
password_hash: string;
|
|
98
|
+
totp_secret: string | null;
|
|
99
|
+
created_at: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface AuthSession {
|
|
103
|
+
id: string;
|
|
104
|
+
token: string;
|
|
105
|
+
user_id: string;
|
|
106
|
+
expires_at: string;
|
|
107
|
+
created_at: string;
|
|
108
|
+
}
|
package/lib/git-history.ts
CHANGED
package/lib/git.ts
CHANGED
package/lib/multi-repo-git.ts
CHANGED
package/lib/projects.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* Sessions inherit settings from their parent project.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { randomUUID } from "crypto";
|
|
9
8
|
import fs from "fs";
|
|
10
9
|
import path from "path";
|
|
11
10
|
import { exec } from "child_process";
|
|
@@ -187,7 +186,9 @@ export async function getAllProjects(): Promise<Project[]> {
|
|
|
187
186
|
/**
|
|
188
187
|
* Get all projects with their dev server configurations
|
|
189
188
|
*/
|
|
190
|
-
export async function getAllProjectsWithDevServers(): Promise<
|
|
189
|
+
export async function getAllProjectsWithDevServers(): Promise<
|
|
190
|
+
ProjectWithRepositories[]
|
|
191
|
+
> {
|
|
191
192
|
const projects = await getAllProjects();
|
|
192
193
|
const result: ProjectWithRepositories[] = [];
|
|
193
194
|
for (const p of projects) {
|
|
@@ -242,7 +243,10 @@ export async function updateProject(
|
|
|
242
243
|
/**
|
|
243
244
|
* Toggle project expanded state
|
|
244
245
|
*/
|
|
245
|
-
export async function toggleProjectExpanded(
|
|
246
|
+
export async function toggleProjectExpanded(
|
|
247
|
+
id: string,
|
|
248
|
+
expanded: boolean
|
|
249
|
+
): Promise<void> {
|
|
246
250
|
await queries.updateProjectExpanded(expanded, id);
|
|
247
251
|
}
|
|
248
252
|
|
|
@@ -273,7 +277,9 @@ export async function deleteProject(id: string): Promise<boolean> {
|
|
|
273
277
|
/**
|
|
274
278
|
* Get sessions for a project
|
|
275
279
|
*/
|
|
276
|
-
export async function getProjectSessions(
|
|
280
|
+
export async function getProjectSessions(
|
|
281
|
+
projectId: string
|
|
282
|
+
): Promise<Session[]> {
|
|
277
283
|
return queries.getSessionsByProject(projectId);
|
|
278
284
|
}
|
|
279
285
|
|
|
@@ -478,7 +484,9 @@ export function validateWorkingDirectory(dir: string): boolean {
|
|
|
478
484
|
/**
|
|
479
485
|
* Get repositories for a project
|
|
480
486
|
*/
|
|
481
|
-
export async function getProjectRepositories(
|
|
487
|
+
export async function getProjectRepositories(
|
|
488
|
+
projectId: string
|
|
489
|
+
): Promise<ProjectRepository[]> {
|
|
482
490
|
const rawRepos = await queries.getProjectRepositories(projectId);
|
|
483
491
|
return rawRepos.map((r) => ({
|
|
484
492
|
...r,
|
|
@@ -509,14 +517,23 @@ export async function addProjectRepository(
|
|
|
509
517
|
for (const repo of existing) {
|
|
510
518
|
if (repo.is_primary) {
|
|
511
519
|
await queries.updateProjectRepository(
|
|
512
|
-
repo.name,
|
|
520
|
+
repo.name,
|
|
521
|
+
repo.path,
|
|
522
|
+
false,
|
|
523
|
+
repo.sort_order,
|
|
524
|
+
repo.id
|
|
513
525
|
);
|
|
514
526
|
}
|
|
515
527
|
}
|
|
516
528
|
}
|
|
517
529
|
|
|
518
530
|
await queries.createProjectRepository(
|
|
519
|
-
id,
|
|
531
|
+
id,
|
|
532
|
+
projectId,
|
|
533
|
+
opts.name,
|
|
534
|
+
opts.path,
|
|
535
|
+
isPrimary,
|
|
536
|
+
maxOrder + 1
|
|
520
537
|
);
|
|
521
538
|
|
|
522
539
|
const raw = (await queries.getProjectRepository(id))!;
|
|
@@ -549,7 +566,11 @@ export async function updateProjectRepository(
|
|
|
549
566
|
for (const repo of allRepos) {
|
|
550
567
|
if (repo.is_primary && repo.id !== id) {
|
|
551
568
|
await queries.updateProjectRepository(
|
|
552
|
-
repo.name,
|
|
569
|
+
repo.name,
|
|
570
|
+
repo.path,
|
|
571
|
+
false,
|
|
572
|
+
repo.sort_order,
|
|
573
|
+
repo.id
|
|
553
574
|
);
|
|
554
575
|
}
|
|
555
576
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atercates/claude-deck",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Self-hosted web UI for managing Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-deck": "./scripts/claude-deck"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/
|
|
31
|
+
"url": "git+https://github.com/ATERCATES/claude-deck.git"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
34
|
"claude",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"node": ">=24"
|
|
44
44
|
},
|
|
45
45
|
"bugs": {
|
|
46
|
-
"url": "https://github.com/
|
|
46
|
+
"url": "https://github.com/ATERCATES/claude-deck/issues"
|
|
47
47
|
},
|
|
48
|
-
"homepage": "https://github.com/
|
|
48
|
+
"homepage": "https://github.com/ATERCATES/claude-deck#readme",
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@anthropic-ai/claude-agent-sdk": "^0.2.104",
|
|
51
51
|
"@codemirror/lang-css": "^6.3.1",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"@xterm/addon-search": "^0.16.0",
|
|
79
79
|
"@xterm/addon-web-links": "^0.12.0",
|
|
80
80
|
"@xterm/xterm": "^6.0.0",
|
|
81
|
+
"bcryptjs": "^3.0.3",
|
|
81
82
|
"better-sqlite3": "^12.8.0",
|
|
82
83
|
"chokidar": "^5.0.0",
|
|
83
84
|
"class-variance-authority": "^0.7.1",
|
|
@@ -88,6 +89,8 @@
|
|
|
88
89
|
"next": "^16.2.3",
|
|
89
90
|
"next-themes": "^0.4.6",
|
|
90
91
|
"node-pty": "1.2.0-beta.12",
|
|
92
|
+
"otpauth": "^9.5.0",
|
|
93
|
+
"qrcode": "^1.5.4",
|
|
91
94
|
"react": "^19.2.5",
|
|
92
95
|
"react-dom": "^19.2.5",
|
|
93
96
|
"react-markdown": "^10.1.0",
|
|
@@ -107,6 +110,7 @@
|
|
|
107
110
|
"@tauri-apps/cli": "^2.10.1",
|
|
108
111
|
"@types/better-sqlite3": "^7.6.13",
|
|
109
112
|
"@types/node": "^25.6.0",
|
|
113
|
+
"@types/qrcode": "^1.5.6",
|
|
110
114
|
"@types/react": "^19.2.14",
|
|
111
115
|
"@types/react-dom": "^19.2.3",
|
|
112
116
|
"@types/ws": "^8.18.1",
|
package/scripts/agent-os
CHANGED
|
@@ -9,7 +9,7 @@ set -euo pipefail
|
|
|
9
9
|
# Configuration
|
|
10
10
|
AGENT_OS_HOME="${AGENT_OS_HOME:-$HOME/.agent-os}"
|
|
11
11
|
PORT="${AGENT_OS_PORT:-3011}"
|
|
12
|
-
REPO_URL="https://github.com/
|
|
12
|
+
REPO_URL="https://github.com/ATERCATES/agent-os.git"
|
|
13
13
|
|
|
14
14
|
# Derived paths
|
|
15
15
|
REPO_DIR="$AGENT_OS_HOME/repo"
|
package/scripts/install.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# ClaudeDeck Installer
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# curl -fsSL https://raw.githubusercontent.com/
|
|
6
|
+
# curl -fsSL https://raw.githubusercontent.com/ATERCATES/claude-deck/main/scripts/install.sh | bash
|
|
7
7
|
#
|
|
8
8
|
|
|
9
9
|
set -e
|
|
@@ -19,7 +19,7 @@ log_info() { echo -e "${BLUE}==>${NC} $1"; }
|
|
|
19
19
|
log_success() { echo -e "${GREEN}==>${NC} $1"; }
|
|
20
20
|
log_error() { echo -e "${RED}==>${NC} $1"; }
|
|
21
21
|
|
|
22
|
-
REPO_URL="https://github.com/
|
|
22
|
+
REPO_URL="https://github.com/ATERCATES/claude-deck.git"
|
|
23
23
|
INSTALL_DIR="$HOME/.claude-deck/repo"
|
|
24
24
|
|
|
25
25
|
echo ""
|
package/server.ts
CHANGED
|
@@ -5,6 +5,12 @@ import { WebSocketServer, WebSocket } from "ws";
|
|
|
5
5
|
import * as pty from "node-pty";
|
|
6
6
|
import { initDb } from "./lib/db";
|
|
7
7
|
import { startWatcher, addUpdateClient } from "./lib/claude/watcher";
|
|
8
|
+
import {
|
|
9
|
+
validateSession,
|
|
10
|
+
parseCookies,
|
|
11
|
+
COOKIE_NAME,
|
|
12
|
+
hasUsers,
|
|
13
|
+
} from "./lib/auth";
|
|
8
14
|
|
|
9
15
|
const dev = process.env.NODE_ENV !== "production";
|
|
10
16
|
const hostname = "0.0.0.0";
|
|
@@ -35,6 +41,19 @@ app.prepare().then(async () => {
|
|
|
35
41
|
server.on("upgrade", (request, socket, head) => {
|
|
36
42
|
const { pathname } = parse(request.url || "");
|
|
37
43
|
|
|
44
|
+
// Validate auth for WebSocket connections
|
|
45
|
+
if (hasUsers()) {
|
|
46
|
+
const cookies = parseCookies(request.headers.cookie);
|
|
47
|
+
const token = cookies[COOKIE_NAME];
|
|
48
|
+
const user = token ? validateSession(token) : null;
|
|
49
|
+
|
|
50
|
+
if (!user) {
|
|
51
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
52
|
+
socket.destroy();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
if (pathname === "/ws/terminal") {
|
|
39
58
|
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
|
40
59
|
terminalWss.emit("connection", ws, request);
|
|
@@ -49,7 +68,7 @@ app.prepare().then(async () => {
|
|
|
49
68
|
|
|
50
69
|
// Heartbeat: ping every 30s, kill if no pong in 10s
|
|
51
70
|
const HEARTBEAT_INTERVAL = 30000;
|
|
52
|
-
const
|
|
71
|
+
const _HEARTBEAT_TIMEOUT = 10000;
|
|
53
72
|
|
|
54
73
|
function setupHeartbeat(ws: WebSocket) {
|
|
55
74
|
let alive = true;
|